/** * Shared proofshot helper for Playwright. * * Basic usage: * import { captureProofshots } from './proofshots'; * captureProofshots('/persons', 'persons'); * * With per-test setup (e.g. seed localStorage before navigation): * captureProofshots('/persons', 'persons', { * setup: async (page) => { * await page.goto('/persons/some-id'); // populates any localStorage state * } * }); * * The setup callback runs before each screenshot's page.goto(url), so any * localStorage values it writes persist into the main navigation. * * Screenshots are saved to proofshot-artifacts/{featureName}/. */ import { type Page, test } from '@playwright/test'; import path from 'path'; import fs from 'fs'; import { fileURLToPath } from 'url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const viewports = [ { name: 'mobile', width: 390, height: 844 }, { name: 'tablet', width: 768, height: 1024 }, { name: 'desktop', width: 1440, height: 900 } ]; interface ProofshotOptions { /** * Optional async callback that runs before each screenshot's page.goto(url). * Use it to seed localStorage, visit a prerequisite page, etc. */ setup?: (page: Page) => Promise; } /** * Registers Playwright tests that navigate to `url`, apply each theme, * and capture full-page screenshots at all standard viewports. * * @param url The path to screenshot (e.g. '/', '/persons', '/admin') * @param featureName Used as the output directory name and screenshot file prefix * @param options Optional setup callback and other options */ export function captureProofshots( url: string, featureName: string, options?: ProofshotOptions ): void { const outDir = path.join(__dirname, '../../proofshot-artifacts', featureName); fs.mkdirSync(outDir, { recursive: true }); for (const vp of viewports) { for (const theme of ['light', 'dark'] as const) { test(`${featureName} – ${vp.name} – ${theme}`, async ({ page }) => { // Run optional setup before main navigation (e.g. seed localStorage) if (options?.setup) { await options.setup(page); } await page.setViewportSize({ width: vp.width, height: vp.height }); await page.goto(url); // Apply theme via data-theme attribute and localStorage await page.evaluate((t) => { document.documentElement.setAttribute('data-theme', t); localStorage.setItem('theme', t); }, theme); // 'networkidle' is unreliable in SvelteKit dev mode due to the HMR WebSocket. await page.waitForLoadState('domcontentloaded'); await page.waitForSelector('main', { state: 'visible' }); await page.screenshot({ path: path.join(outDir, `${featureName}-${vp.name}-${theme}.png`), fullPage: true }); }); } } }