- Add playwright.config.storybook.ts: runs against storybook-static on :6007 - Add e2e/tests/storybook/storybook-all.spec.ts: one test per story (load iframe, no errors) - Add scripts/serve-storybook-static.cjs: serves build or stub (empty index) when no build - npm run test:storybook:playwright (after build-storybook) for full coverage - Storybook decorator: use bg-background / design tokens for dark (#121212) - Preview: default dark background #121212 - Button: secondary/ghost/glass aligned to Spotify/Discord (white/5, white/10 hover) - KodoEmptyState: softer orbs, compact copy, primary CTA without heavy glow Co-authored-by: Cursor <cursoragent@cursor.com>
80 lines
2.4 KiB
TypeScript
80 lines
2.4 KiB
TypeScript
import { test, expect } from '@playwright/test';
|
||
import * as fs from 'fs';
|
||
import * as path from 'path';
|
||
|
||
const INDEX_PATH = path.join(process.cwd(), 'storybook-static', 'index.json');
|
||
const IFRAME_URL = (id: string) => `/iframe.html?id=${encodeURIComponent(id)}&viewMode=story`;
|
||
const NAV_TIMEOUT_MS = 20000;
|
||
const POST_LOAD_MS = 200;
|
||
|
||
/** Story IDs from built Storybook index (available at load time). */
|
||
function getStoryIds(): string[] {
|
||
if (!fs.existsSync(INDEX_PATH)) return [];
|
||
try {
|
||
const index = JSON.parse(fs.readFileSync(INDEX_PATH, 'utf8'));
|
||
const entries = index.entries ?? {};
|
||
return Object.values(entries).map((e: { id?: string }) => e.id).filter(Boolean);
|
||
} catch {
|
||
return [];
|
||
}
|
||
}
|
||
|
||
const storyIds = getStoryIds();
|
||
|
||
test.describe('Storybook – all stories', () => {
|
||
if (storyIds.length === 0) {
|
||
test('run build-storybook first', async () => {
|
||
test.skip(true, 'Run npm run build-storybook first. Then start a server on port 6007 (e.g. npx serve storybook-static -l 6007) or use the Playwright webServer.');
|
||
});
|
||
return;
|
||
}
|
||
|
||
for (const storyId of storyIds) {
|
||
test(storyId, async ({ page }) => {
|
||
const consoleErrors: string[] = [];
|
||
const pageErrors: string[] = [];
|
||
|
||
page.on('console', (msg) => {
|
||
const type = msg.type();
|
||
if (type === 'error') {
|
||
const text = msg.text();
|
||
if (!isIgnoredConsoleError(text)) consoleErrors.push(text);
|
||
}
|
||
});
|
||
page.on('pageerror', (err) => {
|
||
pageErrors.push(err.message);
|
||
});
|
||
|
||
const response = await page.goto(IFRAME_URL(storyId), {
|
||
waitUntil: 'domcontentloaded',
|
||
timeout: NAV_TIMEOUT_MS,
|
||
});
|
||
expect(response?.status()).toBe(200);
|
||
await page.waitForTimeout(POST_LOAD_MS);
|
||
|
||
const errors = [...pageErrors, ...consoleErrors];
|
||
expect(
|
||
errors,
|
||
errors.length ? `Story ${storyId}: ${errors.slice(0, 3).join('; ')}` : undefined
|
||
).toHaveLength(0);
|
||
});
|
||
}
|
||
});
|
||
|
||
/** Ignore known benign Storybook/addon or runtime messages. */
|
||
function isIgnoredConsoleError(text: string): boolean {
|
||
const ignored = [
|
||
'ResizeObserver',
|
||
'Warning: ReactDOM.render',
|
||
'Download the React DevTools',
|
||
'sb-manager',
|
||
'sb-addons',
|
||
'sb-common-assets',
|
||
'mockServiceWorker',
|
||
'Failed to load resource: net::ERR_ABORTED',
|
||
'ChunkLoadError',
|
||
'Loading chunk',
|
||
'hydration',
|
||
];
|
||
return ignored.some((s) => text.includes(s));
|
||
}
|