Last active
September 29, 2026 07:48
-
-
Save AriPerkkio/99b9eedc7d8f71ff6e6770f9425a4be4 to your computer and use it in GitHub Desktop.
Test runner recipe
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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, | |
| }, | |
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 }; | |
| } | |
| ); | |
| }, | |
| }); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { test } from '@playwright/test'; | |
| import { loadStories } from './stories-setup.ts'; | |
| for (const [title, body] of loadStories()) { | |
| test(title, body); | |
| } |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
pnpm create vite+pnpm create storybook: