@compare-ui/storybook
v0.1.0
Published
Storybook helpers for design-comparison workflows.
Readme
@compare-ui/storybook
Storybook helpers for design-comparison workflows.
This package lets you:
- define design-comparison metadata directly on Storybook stories
- register Playwright tests from Storybook stories
- turn one story into multiple design-comparison cases
Installation
pnpm add @compare-ui/storybook @compare-ui/runner @compare-ui/core
pnpm add @playwright/testThis package assumes your app already has its normal Storybook packages installed.
What it exports
Main public APIs:
defineStorybookDesignComparison<TArgs>(...)registerStorybookDesignComparisonTests(...)from@compare-ui/storybook/playwright
Main public types:
StorybookDesignComparisonConfig<TArgs>StorybookDesignComparisonCase<TArgs>
The published public entrypoints are:
@compare-ui/storybook@compare-ui/storybook/playwright
Story metadata
Use defineStorybookDesignComparison<TArgs>(...) inside parameters.designComparison.
Example:
import { artifactRefs, gridRowArtifact, overlayArtifact } from '@compare-ui/core/config';
import { defineStorybookDesignComparison } from '@compare-ui/storybook';
import type { Meta, StoryObj } from '@storybook/react-vite';
import { InteractiveExampleScreen } from './interactive-example-screen';
type ExampleStoryArgs = {
title: string;
mode: 'default' | 'fullScreen';
};
const meta = {
title: 'Screens/ExampleScreen',
render: (args: ExampleStoryArgs) => <InteractiveExampleScreen {...args} />,
} satisfies Meta<ExampleStoryArgs>;
export default meta;
type Story = StoryObj<typeof meta>;
export const My: Story = {
args: {
title: 'Interactive Screen 123',
mode: 'default',
},
parameters: {
designComparison: defineStorybookDesignComparison<ExampleStoryArgs>({
compare: {
threshold: 0.1,
acceptance: {
maxDiffPercent: 0.5,
},
},
artifacts: [
overlayArtifact(),
gridRowArtifact({
items: [artifactRefs.overlay, artifactRefs.actual, artifactRefs.reference],
size: 16,
lineWidth: 2,
}),
],
cases: [
{
name: 'default',
args: {
mode: 'default',
},
reference: {
type: 'fs',
path: './tests/fixtures/example-screen-default.png',
},
},
{
name: 'full-screen',
args: {
mode: 'fullScreen',
},
reference: {
type: 'fs',
path: './tests/fixtures/example-screen-full-screen.png',
crop: {
bottom: 20,
},
},
},
],
}),
},
};Cases
Each story may define one or more comparison cases.
The shared story-level config may also define capture defaults for all cases:
targetSelectorsettleDelayMsstrictRenderHealthsurfaceBackground
Each case can provide:
nameargsreferenceviewporttargetSelectorsettleDelayMsstrictRenderHealthsurfaceBackground- optional compare overrides
- optional artifact overrides
- optional reporting overrides
args are typed as Partial<TArgs>, where TArgs is the same args type used by the story.
That means one story can describe multiple comparison states without creating multiple story exports.
When both shared config and case config provide the same capture option, the case value wins.
Playwright package setup
This package uses the consuming app's Playwright installation.
Supported peer range:
@playwright/test >=1.40.0 <2
If your workspace also uses Playwright component testing, keep these packages on the exact same version:
@playwright/test@playwright/experimental-ct-react
This is a workspace package setup requirement. The package docs and workspace validation should keep these versions aligned; the Storybook helper does not need extra runtime enforcement for this.
The workspace also runs tarball compatibility smoke tests in CI so we keep validating the published package against more than one consumer dependency shape.
TypeScript resolver compatibility
These package exports rely on modern exports resolution.
Recommended tsconfig.json resolver modes:
moduleResolution: "bundler"moduleResolution: "node16"moduleResolution: "nodenext"
Older moduleResolution: "node" setups may still work at runtime but often produce TypeScript friction for subpath imports such as:
@compare-ui/storybook/playwright@compare-ui/core/config
If TypeScript reports import-resolution errors for those subpaths, switch to one of the recommended resolver modes above.
Args support
Storybook comparison cases should support normal JSON-serializable Storybook args.
That includes:
- strings
- numbers
- booleans
null- arrays
- nested objects
Example:
args: {
title: 'Interactive Screen 123',
media: {
src: '/hero.png',
alt: 'Hero image',
},
products: [
{ id: '1', name: 'One' },
{ id: '2', name: 'Two' },
],
selectedProductId: null,
}The Playwright helper applies supported args through Storybook's preview channel before capture so the story is rendered in the intended state without relying on a primitive-only args contract.
For values that are not safely serializable into Storybook args, such as:
undefined- functions
- JSX elements
- symbols
- bigint
- dates
- maps
- sets
- class instances
- cyclic objects
the recommended Storybook approach is to use argTypes.mapping or another story-level indirection instead of passing those values directly through cases[].args.
Registering tests
Use registerStorybookDesignComparisonTests(...) inside a Playwright test file.
Recommended location:
tests/storybook-design-comparison.spec.ts
Example:
import { registerStorybookDesignComparisonTests } from '@compare-ui/storybook/playwright';
import * as exampleScreenStories from '../src/example-screen.stories';
registerStorybookDesignComparisonTests({
storybookUrl: 'http://127.0.0.1:6007',
storyModules: [exampleScreenStories],
});The helper should:
- inspect the provided story modules
- find stories that define
parameters.designComparison - create one Playwright test per comparison case
Comparison execution follows the shared @compare-ui/runner and @compare-ui/core pipeline.
That means image-processing behavior such as:
- crop handling
- explicit size normalization
- explicit transparent-background flattening
comes from the shared comparison flow rather than from Storybook-specific image rules in this package.
Capture target
By default, Storybook comparisons capture #storybook-root.
Use targetSelector when the comparison should capture a narrower element inside the story canvas.
Example:
{
name: 'card',
targetSelector: '[data-testid="card-root"]',
reference: {
type: 'fs',
path: './tests/fixtures/card.png',
},
}If targetSelector is omitted, the default capture target remains #storybook-root.
Shared config may also define a default targetSelector for all cases in the story. A case-level targetSelector overrides that shared value.
Selector capture uses an integer-snapped clip rectangle internally so fractional layout positions do not drift by a pixel between runs.
Surface background
Use surfaceBackground when one logical surface color should drive:
- the Storybook capture surface
- transparent-PNG flattening before compare, when
flattenBackgroundis not already configured explicitly - generated
grid-roworjoinartifact backgrounds, when those artifact instructions do not already define one
Example:
defineStorybookDesignComparison({
surfaceBackground: [17, 17, 17, 255],
cases: [
{
name: 'card',
targetSelector: '[data-testid="card-root"]',
reference: {
type: 'fs',
path: './tests/fixtures/card.png',
},
},
],
})Case-level surfaceBackground overrides the shared story-level value.
Viewport handling
When a case defines viewport, the helper should apply that size to the browser page before taking the screenshot.
That means viewport is capture behavior, not reporting metadata only.
When the capture target is the default #storybook-root, the helper also constrains the Storybook root to that viewport size before capture.
So with the default root capture:
- the page viewport is set to
viewport #storybook-rootis sized to match it- the resulting screenshot is intended to represent a fixed viewport-sized surface instead of a content-height root
When a case uses a custom targetSelector, the page viewport is still applied, but the final screenshot dimensions follow the selected element.
Example:
{
name: 'mobile',
viewport: { x: 361, y: 423 },
reference: {
type: 'fs',
path: './tests/fixtures/mobile.png',
},
}Storybook URL
registerStorybookDesignComparisonTests(...) should support this resolution order:
- explicit
storybookUrl process.env.STORYBOOK_URL- default
http://127.0.0.1:6006
That means storybookUrl may be omitted when the default local Storybook port is being used.
Example with the default:
registerStorybookDesignComparisonTests({
storyModules: [exampleScreenStories],
});Example with an override:
registerStorybookDesignComparisonTests({
storybookUrl: 'http://127.0.0.1:6007',
storyModules: [exampleScreenStories],
});Starting Storybook
Storybook should already be running before the tests execute.
Typical local workflow:
- start Storybook
- run the Playwright Storybook comparison tests
Typical CI workflow:
- build or serve Storybook
- run the Playwright Storybook comparison tests
This package should not start Storybook for you. It expects a reachable Storybook URL.
Readiness model
The helper should capture only after the story is actually ready for comparison.
Expected flow:
- navigate to the story iframe with normal document readiness
- apply
viewportwhen provided - wait for Storybook render and
playcompletion through Storybook lifecycle events - apply supported
argswhen the case defines them - resolve the capture target
- wait for the target to exist and be visible
- apply
settleDelayMswhen provided - capture the screenshot
The readiness model should follow Storybook lifecycle/events plus explicit settling. It should not rely on networkidle.
Render health
Use strictRenderHealth when a case should fail before compare if the preview is obviously unhealthy.
When enabled, the helper fails early on signals such as:
- page runtime errors
- failed network requests for important preview assets
- HTTP error responses for scripts, stylesheets, images, and fetch/XHR requests
- broken images inside the capture target
- zero-size capture targets
Example:
{
name: 'strict-preview',
strictRenderHealth: true,
reference: {
type: 'fs',
path: './tests/fixtures/strict-preview.png',
},
}This stays opt-in so existing comparison flows are not forced into a stricter failure policy unless you want that behavior.
Story ids
The package resolves Storybook story ids from Storybook metadata.
You do not need to hardcode ids like:
screens-examplescreen--with-play-function
This keeps tests aligned with Storybook's generated story ids.
play functions
Stories can use normal Storybook play functions.
The generated tests should take the screenshot after the story has rendered and its play function has completed.
When a case needs extra visual settling after play, provide settleDelayMs.
Example:
{
name: 'animated-state',
settleDelayMs: 150,
reference: {
type: 'fs',
path: './tests/fixtures/animated-state.png',
},
}That means the same story can define:
- render state
- interactive state
- design-comparison metadata
in one place.
Shared config may also define a default settleDelayMs for all cases in the story. A case-level settleDelayMs overrides that shared value.
Focusing on one story
Generated test names include:
- the Storybook title
- the Storybook story name
- the design-comparison case name
Example generated test name:
[design-comparison] Screens/ExampleScreen :: My :: full-screenThe recommended workflow is to use Playwright's normal --grep support.
Example:
pnpm test:storybook-design-comparison -- --grep "ExampleScreen"or:
pnpm test:storybook-design-comparison -- --grep "full-screen"The most reliable --grep values are:
- part of the Storybook title, such as
ExampleScreen - the Storybook story name, such as
My - the case name, such as
full-screen
Typical workflow
- Create or update a Storybook story.
- Add
parameters.designComparison. - Define one or more cases.
- Register the story module in a Storybook design-comparison test file.
- Run the Playwright tests.
Recipe: Storybook Story With A Local Reference PNG
Story file:
import { defineStorybookDesignComparison } from '@compare-ui/storybook';
import type { Meta, StoryObj } from '@storybook/react-vite';
import { ExampleScreen } from './example-screen';
type ExampleStoryArgs = {
title: string;
};
const meta = {
title: 'Screens/ExampleScreen',
render: (args: ExampleStoryArgs) => <ExampleScreen {...args} />,
} satisfies Meta<ExampleStoryArgs>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
title: 'Interactive Screen 123',
},
parameters: {
designComparison: defineStorybookDesignComparison<ExampleStoryArgs>({
cases: [
{
name: 'default',
reference: {
type: 'fs',
path: './tests/fixtures/example-screen-reference.png',
},
},
],
}),
},
};Playwright spec:
import { registerStorybookDesignComparisonTests } from '@compare-ui/storybook/playwright';
import * as exampleScreenStories from '../src/example-screen.stories';
registerStorybookDesignComparisonTests({
storyModules: [exampleScreenStories],
});Workflow:
- start Storybook
- run the Playwright Storybook comparison spec
Recipe: Storybook Story With A Figma Reference
Bootstrap the Figma resolver once before registering the Storybook comparison tests.
Playwright spec:
import '@compare-ui/figma';
import { registerFigmaFromEnv } from '@compare-ui/figma';
import { registerStorybookDesignComparisonTests } from '@compare-ui/storybook/playwright';
import * as exampleScreenStories from '../src/example-screen.stories';
registerFigmaFromEnv();
registerStorybookDesignComparisonTests({
storyModules: [exampleScreenStories],
});If TypeScript does not recognize type: 'figma', keep the import '@compare-ui/figma'; line in a setup module or in this spec so the augmentation is included in the program.
Story file:
import { defineStorybookDesignComparison } from '@compare-ui/storybook';
import type { Meta, StoryObj } from '@storybook/react-vite';
import { ExampleScreen } from './example-screen';
type ExampleStoryArgs = {
title: string;
};
const meta = {
title: 'Screens/ExampleScreen',
render: (args: ExampleStoryArgs) => <ExampleScreen {...args} />,
} satisfies Meta<ExampleStoryArgs>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
title: 'Interactive Screen 123',
},
parameters: {
designComparison: defineStorybookDesignComparison<ExampleStoryArgs>({
cases: [
{
name: 'figma-reference',
reference: {
type: 'figma',
url: 'https://figma.com/design/abc123/Example?node-id=2020-1617',
crop: {
bottom: 20,
},
},
},
],
}),
},
};Workflow:
- start Storybook
- register the Figma resolver once in the test bootstrap or spec file
- run the Playwright Storybook comparison spec
Notes
- This package is Storybook-specific.
- Import
defineStorybookDesignComparison(...)from@compare-ui/storybook. - Import
registerStorybookDesignComparisonTests(...)from@compare-ui/storybook/playwright. - It is intended to work with Storybook stories plus Playwright-based execution.
- Comparison execution itself is handled by lower-level packages.
cases[].argsshould follow Storybook's JSON-serializable args model.- supported case args are applied through Storybook's preview-channel update flow before capture.
- image-processing policy stays in the shared core/runner flow.
strictRenderHealthis opt-in and is intended for teams that want preview-health failures before compare.surfaceBackgroundis the high-level Storybook knob for keeping capture background, flattening, and composite artifact background aligned.
