storybook-visual-regression-testing
Sets up visual regression coverage for a Storybook project - either via the official @chromatic-com/storybook addon (hosted) or via @storybook/test-runner with a postVisit hook that calls Playwright's toHaveScreenshot (self-hosted). Covers test-runner install, lifecycle hooks (setup / preVisit / postVisit), and CI integration.
storybook-visual-regression-testing
Overview
Storybook ships two officially-supported paths to visual regression testing:
This skill covers both. For full Chromatic CLI behavior (TurboSnap, exit codes, config file), pair with chromatic-visual-regression-testing; for toHaveScreenshot option syntax, pair with playwright-snapshots.
When to use
Path 1 - Hosted via Chromatic addon
Install
npm install --save-dev @chromatic-com/storybook(Per storybook-visual-testing.)
Activate
The addon registers itself in Storybook on next start. Sign into Chromatic from the Storybook UI, link the project, then click "Catch a UI change" to capture the first baseline (storybook-visual-testing).
After the first baseline, every story functions as a visual test automatically - no per-story parameters block, no custom assertion code.
CI integration
Chromatic CI invocation is identical to running it standalone - see chromatic-visual-regression-testing for the npx chromatic CLI flag set, exit codes, TurboSnap, and chromatic.config.json schema.
Path 2 - Self-hosted via test-runner
Install
npm install --save-dev @storybook/test-runner(Per storybook-test-runner.)
Add the script to package.json:
{
"scripts": {
"test-storybook": "test-storybook"
}
}How it works
@storybook/test-runner turns every story into an executable test powered by Jest + Playwright under the hood (storybook-test-runner). It requires a Storybook running on a discoverable URL (default http://localhost:6006) or a published Storybook static build.
Lifecycle hooks
Configure in .storybook/test-runner.ts - three hooks (storybook-test-runner):
| Hook | When | Use for |
|---|---|---|
setup() | Once, before all tests | One-time setup (e.g. starting a mock server). |
preVisit(page, context) | Before each story renders in the browser | Set viewport, inject auth, mock APIs. |
| `postVisit(page, context) | After each story has fully rendered | Visual snapshot, accessibility audit, custom assertion. |
context carries id, title, and name for the current story.
Visual regression via postVisit + Playwright snapshots
.storybook/test-runner.ts:
import type { TestRunnerConfig } from '@storybook/test-runner';
import { expect } from '@playwright/test';
import { getStoryContext } from '@storybook/test-runner';
const config: TestRunnerConfig = {
async postVisit(page, context) {
// Skip stories explicitly opted out
const storyContext = await getStoryContext(page, context);
if (storyContext.parameters?.visualRegression?.disable) return;
// Locate the story's root element and snapshot it
const root = page.locator('#storybook-root');
await expect(root).toHaveScreenshot(
`${context.id}.png`,
{ animations: 'disabled', mask: [page.locator('[data-mask]')] }
);
},
};
export default config;This captures one PNG per story under tests/__snapshots__/<context.id>.png (or the path your playwright.config.ts snapshotPathTemplate resolves to). Per-story opt-out via a story parameters.visualRegression.disable = true.
For the full toHaveScreenshot option set (mask, threshold, maxDiffPixels, etc.) and the per-OS / per-browser baseline structure, see playwright-snapshots.
Running
Two-step: start Storybook, then run the test-runner against it (storybook-test-runner):
# Terminal A — long-running
npm run storybook
# Terminal B
npm run test-storybookFor CI, build a static Storybook and serve it locally during the test run:
npx storybook build && \
npx http-server storybook-static --port 6006 --silent &
SERVER_PID=$!
npx wait-on http://localhost:6006
npm run test-storybook
kill $SERVER_PIDThe wait-on step is important - test-storybook will fail unhelpfully if the server is not up before it starts.
Choosing between paths
| Question | Path 1 (Chromatic) | Path 2 (self-hosted) |
|---|---|---|
| Want a hosted UI for diff review? | ✅ | ❌ |
| Need TurboSnap (story-aware change detection)? | ✅ | ❌ |
| Want baselines committed to the repo? | ❌ | ✅ |
| Acceptable to add a paid SaaS dependency? | ✅ | ❌ (free) |
| Cross-browser baselines? | ✅ built-in | ⚠️ via Playwright projects, locally |
| Per-story opt-out granularity? | ✅ via parameters.chromatic.disableSnapshot | ✅ via parameters.visualRegression.disable |
A common mid-size setup: Path 1 in CI, with Path 2 for local authoring so engineers can iterate without consuming Chromatic snapshot quota.
CI integration
For Path 2, the workflow is the standard test-runner pattern with Playwright browsers installed:
# .github/workflows/storybook-visual.yml
name: storybook-visual
on:
pull_request:
push:
branches: [main]
jobs:
test-storybook:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps
- name: Build Storybook
run: npx storybook build
- name: Serve + test
run: |
npx concurrently -k -s first -n "SB,TEST" -c "magenta,blue" \
"npx http-server storybook-static --port 6006 --silent" \
"npx wait-on http://localhost:6006 && npm run test-storybook"
- name: Upload Playwright report
if: always()
uses: actions/upload-artifact@v4
with:
name: storybook-test-runner-report
path: |
test-results/
playwright-report/
retention-days: 14concurrently -k -s first ensures the server is killed when the test process exits - without -k, the workflow hangs.