Skip to content

Instantly share code, notes, and snippets.

@donaldpipowitch
Last active November 13, 2025 13:43
Show Gist options
  • Select an option

  • Save donaldpipowitch/605088fca125845aa0c4ecbeeb21a0f0 to your computer and use it in GitHub Desktop.

Select an option

Save donaldpipowitch/605088fca125845aa0c4ecbeeb21a0f0 to your computer and use it in GitHub Desktop.
Visual Regression Testing with Storybook Test Runner
import fs from 'fs';
import path from 'path';
import { StorybookConfig } from '@storybook/react-vite';
// custom stuff...
const config: StorybookConfig = {
stories: process.env.GENERATE_IMAGES_LOCALLY
? ['../stories/**/components/**/*.stories.@(js|jsx|ts|tsx)']
: [
'../stories/**/*.stories.mdx',
'../stories/**/*.stories.@(js|jsx|ts|tsx)',
],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
'@storybook/addon-styling',
'@storybook/addon-a11y',
'@storybook/test-runner',
],
viteFinal: async (config) => {
config.resolve = config.resolve ?? {};
config.resolve.alias = {
...config.resolve.alias,
stories: path.resolve(__dirname, '../stories'),
'.storybook': path.resolve(__dirname),
};
if (process.env.STORYBOOK_NOT_MINIFIED === 'true') {
config.build = config.build ?? {};
config.build.minify = false;
}
// custom stuff...
return config;
},
// custom stuff...
};
export default config;
const { getStoryContext } = require('@storybook/test-runner');
const { toMatchImageSnapshot } = require('jest-image-snapshot');
module.exports = {
setup() {
expect.extend({ toMatchImageSnapshot });
},
// https://github.com/storybookjs/test-runner#prepare
// https://github.com/storybookjs/test-runner/blob/next/src/setup-page.ts#L12
async prepare({ page, browserContext, testRunnerConfig }) {
// this line is customized!
const targetURL = process.env.STORYBOOK_TEST_RUNNER_CI
? 'http://127.0.0.1:58414'
: 'http://host.docker.internal:58414';
const iframeURL = new URL('iframe.html', targetURL).toString();
if (testRunnerConfig?.getHttpHeaders) {
const headers = await testRunnerConfig.getHttpHeaders(iframeURL);
await browserContext.setExtraHTTPHeaders(headers);
}
await page.goto(iframeURL, { waitUntil: 'load' }).catch((err) => {
if (err.message?.includes('ERR_CONNECTION_REFUSED')) {
const errorMessage = `Could not access the Storybook instance at ${targetURL}. Are you sure it's running?\n\n${err.message}`;
throw new Error(errorMessage);
}
throw err;
});
},
// context = { id, title, name }
async postRender(page, context) {
if (!context.title.includes('Components/')) return;
// storyContext.parameters gives you access to the parameters defined in the story and more
// (left it here to add per story filtering, custom delays or custom MatchImageSnapshot
// configs per story in the future)
const storyContext = await getStoryContext(page, context);
const imageParameters = storyContext.parameters?.image || {};
// Make sure assets (images, fonts) are loaded and ready
await page.waitForLoadState('domcontentloaded');
await page.waitForLoadState('load');
await page.waitForLoadState('networkidle');
await page.evaluate(() => document.fonts.ready);
if (imageParameters.waitTime)
await new Promise((resolve) =>
setTimeout(resolve, imageParameters.waitTime)
);
const image = await page.screenshot({
animations: 'disabled',
fullPage: true,
});
expect(image).toMatchImageSnapshot({
customSnapshotsDir: '.storybook-images',
customSnapshotIdentifier: context.id,
storeReceivedOnFailure: true,
});
},
};

Intro

After Loki.js served us well for some years we finally converted our Visual Regression Testing** logic to use the Storybook Test Runner.

The benefits:

  • it's way faster
  • it has better official support
  • it does more (component smoke tests, play tests, extensibility for more like a11y tests)
  • it's way more stable

But the setup might be non-trivial and there are some rough edges. The biggest downside: while it is way faster it could be even more faster, if we could just use generate images against the Storybook Dev Server. Sadly this was very flaky and error boundaries would always fail (I assume this is because of Reacts unfortunate decision to re-throw errors in Dev Mode). See also this issue.

What I do now in order to generate images locally is a custom "production" build (without minification and optimization and just the stories that I actually want to test visually).

The other tricky part was the Playwright setup. In order to get the same results across machines we want to run Playwright in a Docker container. For various reasons I decided to use the Network API of Playwright. That means I'll only run Playwright in the Docker container, but I keep my running Storybook instance on the host. (At least in the local setup. Within the Gitlab CI we run everything inside Docker.)

As a general note: We have a lot of stories in Storybook, but we only want to take images of stories that are inside a components/ directory.

package.json

...

test-runner-jest.config.js

..

.storybook/main.ts

..

{
"scripts": {
"test-storybook:run": "DEBUG_PRINT_LIMIT=0 test-storybook --index-json",
"test-storybook:build": "cross-env STORYBOOK_NOT_MINIFIED=true storybook build --quiet --output-dir storybook",
"test-storybook:server": "pnpm live-server --port=58414 storybook --no-browser",
"test-storybook:build-and-run": "pnpm test-storybook:build && start-server-and-test 'pnpm test-storybook:server' http-get://127.0.0.1:58414 'cross-env TARGET_URL=http://127.0.0.1:58414 pnpm test-storybook:run'",
"test-storybook:generate-images": "cross-env GENERATE_IMAGES_LOCALLY=true pnpm test-storybook:build && start-server-and-test 'pnpm test-storybook:server' http-get://127.0.0.1:58414 'cross-env TARGET_URL=http://127.0.0.1:58414 pnpm test-storybook:run -u'",
"start-playwright-server": "docker run -p 3000:3000 --rm --init -it mcr.microsoft.com/playwright:v1.39.0-jammy /bin/sh -c \"cd /home/pwuser && npx -y playwright@1.39.0 run-server --port 3000\"",
}
}
const { getJestConfig } = require('@storybook/test-runner');
/**
* @type {import('@jest/types').Config.InitialOptions}
*/
module.exports = {
// The default configuration comes from @storybook/test-runner
...getJestConfig(),
/** Add your own overrides below
* @see https://jestjs.io/docs/configuration
*/
testEnvironmentOptions: {
'jest-playwright': process.env.STORYBOOK_TEST_RUNNER_CI
? undefined
: {
connectOptions: {
chromium: {
wsEndpoint: 'ws://127.0.0.1:3000',
},
},
},
},
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment