Skip to content

Instantly share code, notes, and snippets.

@AriPerkkio
Last active September 29, 2026 07:48
Show Gist options
  • Select an option

  • Save AriPerkkio/99b9eedc7d8f71ff6e6770f9425a4be4 to your computer and use it in GitHub Desktop.

Select an option

Save AriPerkkio/99b9eedc7d8f71ff6e6770f9425a4be4 to your computer and use it in GitHub Desktop.
Test runner recipe
import { defineConfig } from '@playwright/test';
const storybookUrl = process.env.STORYBOOK_URL ?? 'http://localhost:6006';
export default defineConfig({
testDir: './playwright',
globalSetup: './playwright/stories-setup.ts',
use: { baseURL: storybookUrl },
fullyParallel: true,
webServer: process.env.STORYBOOK_URL
? undefined
: {
command: 'npm run storybook -- --ci',
url: `${storybookUrl}/index.json`,
reuseExistingServer: true,
},
});
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { type test, type Page } from '@playwright/test';
declare global {
var __storyResult: { status: string; errors: string[] };
}
interface Entry {
id: string;
title: string;
name: string;
type: 'story' | 'docs';
tags?: string[];
}
interface Report {
type: string;
status: string;
result?: { violations?: { id: string; help: string }[] };
}
const storybookURL = process.env.STORYBOOK_URL ?? 'http://localhost:6006';
const indexFile = resolve('node_modules/.cache/storybook-playwright/index.json');
export default async function globalSetup() {
const response = await fetch(`${storybookURL}/index.json`);
if (!response.ok) {
throw new Error(`Failed to fetch ${storybookURL}/index.json: ${response.status}`);
}
mkdirSync(dirname(indexFile), { recursive: true });
writeFileSync(indexFile, await response.text());
}
export function loadStories(): [string, Parameters<typeof test>[2]][] {
const index = JSON.parse(readFileSync(indexFile, 'utf8')) as {
entries: Record<string, Entry>;
};
return Object.values(index.entries)
.filter((entry) => entry.type === 'story' && entry.tags?.includes('test'))
.map((entry) => [
`${entry.title} › ${entry.name}`,
async ({ page }) => visitStory(page, entry),
]);
}
async function visitStory(page: Page, entry: Entry) {
await page.addInitScript(captureStoryResult);
await page.goto(`/iframe.html?id=${entry.id}&viewMode=story`);
const handle = await page.waitForFunction(() => globalThis.__storyResult);
const result = await handle.jsonValue();
if (result.status !== 'success' || result.errors.length > 0) {
throw new Error(
[
`Story "${entry.title} › ${entry.name}" ${result.status === 'missing' ? 'is missing' : 'failed'}.`,
`Open it in Storybook: ${storybookURL}/?path=/story/${entry.id}`,
'',
...result.errors,
].join('\n')
);
}
}
function captureStoryResult() {
const errors: string[] = [];
const serialize = (error: Error) => error.stack ?? `${error.name}: ${error.message}`;
let channel: any;
Object.defineProperty(globalThis, '__STORYBOOK_ADDONS_CHANNEL__', {
configurable: true,
get: () => channel,
set: (value) => {
channel = value;
channel.on('storyErrored', ({ title, description }: Record<string, string>) =>
errors.push(`${title}\n${description}`)
);
channel.on('storyThrewException', (error: Error) => errors.push(serialize(error)));
channel.on('playFunctionThrewException', (error: Error) => errors.push(serialize(error)));
channel.on('unhandledErrorsWhilePlaying', (unhandled: Error[]) =>
errors.push(...unhandled.map(serialize))
);
channel.on('storyMissing', (id: string) => {
globalThis.__storyResult = {
status: 'missing',
errors: [`Story "${id}" not found`],
};
});
channel.on(
'storyFinished',
({ status, reporters }: { status: string; reporters: Report[] }) => {
for (const report of reporters.filter((report) => report.status === 'failed')) {
const violations = report.result?.violations ?? [];
errors.push(
[
`${report.type} report failed`,
...violations.map((v) => `- ${v.id}: ${v.help}`),
].join('\n')
);
}
globalThis.__storyResult = { status, errors };
}
);
},
});
}
import { test } from '@playwright/test';
import { loadStories } from './stories-setup.ts';
for (const [title, body] of loadStories()) {
test(title, body);
}
@AriPerkkio

Copy link
Copy Markdown
Author

pnpm create vite + pnpm create storybook:

$ pnpm test:storybook

Running 8 tests using 7 workers

  ✓  1 playwright/stories.spec.ts:6:3 › Example/Header › Logged In (1.0s)
  ✓  2 playwright/stories.spec.ts:6:3 › Example/Header › Logged Out (1.0s)
  ✓  3 playwright/stories.spec.ts:6:3 › Example/Button › Secondary (993ms)
  ✓  4 playwright/stories.spec.ts:6:3 › Example/Page › Logged Out (1.0s)
  ✓  5 playwright/stories.spec.ts:6:3 › Example/Button › Large (992ms)
  ✓  6 playwright/stories.spec.ts:6:3 › Example/Button › Primary (994ms)
  ✓  7 playwright/stories.spec.ts:6:3 › Example/Button › Small (1.0s)
  ✓  8 playwright/stories.spec.ts:6:3 › Example/Page › Logged In (360ms)

  8 passed (2.6s)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment