// Playwright configuration for end-to-end tests. // // What this covers vs. Vitest // ─────────────────────────── // Vitest (in src/**/*.test.js) covers pure functions and the React // hook-state portion of useDesignEditor — anything testable without // mounting Konva. Playwright covers what Vitest can't: real Konva // stages, the actual keyboard handler attached to window, persistence // across reloads, the multi-component flows where state has to thread // through App.jsx, the Sidebar, the canvas, and back. // // Test layout // ─────────── // /e2e/ // fixtures/ ← shared setup helpers // text-editing.spec.js ← adding/editing/deleting text elements // history.spec.js ← undo/redo across operations // persistence.spec.js ← state survives page reload // crop.spec.js ← the headline May 22 bug — crop + Cmd-Z // must restore the uncropped image // // Webserver // ───────── // `npm run dev` runs Vite (3000) + the Node server (3001) via // concurrently. Playwright waits for the Vite URL to be reachable // before starting tests. The 120-second timeout accommodates cold // starts on first run (Vite optimizes deps, the Node server's // dependencies are heavy). // // `reuseExistingServer: !CI` means if a dev server is already running // locally, Playwright connects to that rather than starting a second // one (which would fail on the port collision). In CI we always start // a fresh one for hermetic runs. // // Browsers // ──────── // Chromium only. The app's behaviour is engine-dependent in subtle // places (Konva canvas pixel rounding, native EyeDropper API is // Chromium-only), so Firefox/WebKit coverage would either flake or // have to skip large chunks of the suite. Chromium is what the // dominant user base will hit; adding more engines is a future // quality decision once Chromium is solid. import { defineConfig, devices } from '@playwright/test'; const PORT = 3000; const BASE_URL = `http://localhost:${PORT}`; export default defineConfig({ testDir: './e2e', // The dev server is shared across tests in a file; running them in // parallel within a file would mean simultaneous localStorage // wipes and weird cross-test state. Within a file, tests run // sequentially (default). Across files, parallel is fine — each // file gets a fresh browser context. fullyParallel: true, // Hard fail in CI on .only(). Locally, .only() is a useful // workflow for iterating on a single failing test. forbidOnly: !!process.env.CI, // Retry once in CI for flakes (Konva mount timing, font load // races). Locally, no retries — a flake should fail and be // investigated, not papered over. retries: process.env.CI ? 1 : 0, // CI: single worker for hermetic runs. Local: Playwright picks // based on cores (typically 50% of available, capped at 8). workers: process.env.CI ? 1 : undefined, reporter: process.env.CI ? 'github' : 'list', use: { baseURL: BASE_URL, // Capture trace on first retry so flaky tests in CI leave us // breadcrumbs (full DOM snapshot, network log, screenshots). // Tracing is expensive — we don't want it on every run. trace: 'on-first-retry', // Screenshot on failure for the same reason. The screenshot is // attached to the test report automatically. screenshot: 'only-on-failure', // Don't record video by default — produces large files and is // rarely the right tool. Enable per-test if needed. video: 'off', // Default timeout for individual actions (click, fill, etc.). // The app's UI is responsive; 10s is a generous ceiling for // anything that should be near-instant. actionTimeout: 10_000, // Navigation timeout — `goto`/reload waits up to 30s. Cold // first paint can be slow when Vite is also compiling deps. navigationTimeout: 30_000, }, projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'] }, }, ], webServer: { command: 'npm run dev', url: BASE_URL, // 120s for cold starts (Vite optimizes deps, fonts load, etc). timeout: 120 * 1000, // Locally, if a dev server is already up, just connect to it. // In CI, always spin a fresh one. reuseExistingServer: !process.env.CI, // Pipe server output to stdout for debugging when tests fail — // a 500 from the API or a Vite compile error becomes visible // in the Playwright output rather than disappearing into the // background process. stdout: 'pipe', stderr: 'pipe', }, });