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. Use when a repo already has a working `.storybook/` config and the team wants per-story visual coverage rather than page-level snapshots.
75
94%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Storybook ships two officially-supported paths to visual regression testing:
@chromatic-com/storybook; every story becomes a Chromatic snapshot
automatically. Review happens in the Chromatic UI. This is the
default recommendation in the Storybook visual-testing docs.@storybook/test-runner and add a postVisit hook that calls
expect(page).toHaveScreenshot(). Baselines live in the repo, no
external service.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.
.storybook/ with a working Storybook config.npm install --save-dev @chromatic-com/storybook(Per storybook-visual-testing.)
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.
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.
npm install --save-dev @storybook/test-runner(Per storybook-test-runner.)
Add the script to package.json:
{
"scripts": {
"test-storybook": "test-storybook"
}
}@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.
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.
.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.
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.
| 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.
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.
@storybook/test-runner install,
lifecycle hooks, integration with Jest + Playwright.chromatic-visual-regression-testing - Chromatic CLI behavior, exit codes, TurboSnap.playwright-snapshots - full
toHaveScreenshot options for Path 2.