playwright.config.ts
Rama main · réplica de lectura del repositorio original.
import fs from 'node:fs'
import path from 'node:path'
import { defineConfig, devices } from '@playwright/test'
import { perRunWorkers } from './tests/e2e/browser/pw-concurrency.js'
import { engageGate } from './tests/e2e/browser/pw-gate.js'
// Every test browser is silent: an init script in every frame, and Chromium's own mute.
import { CHROMIUM_SILENT } from './tests/e2e/browser/pw-silence.js'
// Linux WebKit paints on the CPU: its GL painting threads crash the page on software GL.
import './tests/e2e/browser/pw-webkit-paint.js'
import { sideConfigSpecs } from './tests/e2e/browser/side-config-specs.js'
import { bunCommand } from './tests/helpers/bun-runtime.js'
const testPort = Number(process.env.PW_TEST_PORT ?? 3457)
// Machine-wide admission control. MUST run here, at config load: Playwright boots
// the webServer plugin before globalSetup, so this is the only hook early enough
// to queue behind a concurrent run and to reap an orphaned fixture server before
// `reuseExistingServer` silently attaches to it. See tests/e2e/browser/pw-gate.ts.
engageGate(testPort)
// Specs that swap the fixture server's one host fixture (POST /api/test/host-fixture)
// race each other when two workers run them at once: one loads its hosts while the
// other's test reads them (2026-10-29: banner-placement-round4 N3-2 failed 2 of 3 runs
// beside system-attention :267). They run in their own project, one at a time, while
// every other spec stays parallel. Found by what they call, so a new one joins itself.
const HOST_FIXTURE_CALL = /\b(?:loadFixture|hostFixture|resetServerHostFixture)\(/
const browserDir = path.join(import.meta.dirname, 'tests/e2e/browser')
const hostFixtureSpecs = fs.readdirSync(browserDir)
.filter((f) => f.endsWith('.spec.ts') && HOST_FIXTURE_CALL.test(fs.readFileSync(path.join(browserDir, f), 'utf8')))
.map((f) => `**/${f}`)
// Specs with their own config and fixture (replica, cloud host, host alone, ...).
// Every project names them again: a project's testIgnore replaces the top-level one.
const sideSpecs = sideConfigSpecs(browserDir)
export default defineConfig({
testDir: 'tests/e2e/browser',
// Their own fixture and config (tests/e2e/browser/side-config-specs.ts); the main
// config runs replica-smoke.spec.ts for the replica path instead.
testIgnore: sideSpecs,
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
// `undefined` here used to mean "half the cores" (7 on this Mac) — 28 chromium
// processes and ~2.7 GB per run, with nothing coordinating concurrent runs.
// Overrides: PW_WORKERS (per run), PW_NO_GATE=1 (skip the port lease).
workers: perRunWorkers(),
reporter: 'list',
// A CI runner has 4 cores and runs 4 workers: one cold load of the dev mode fixture
// (about 1,700 module requests) took 12 to 14 s there, so a test that loads the app
// twice ran out of 30 s with nothing wrong (2026-11-01: about 9 of 21 failures of one
// shard). Assertions keep their own timeouts; this is only the test's ceiling.
timeout: process.env.CI ? 60_000 : 30_000,
use: {
baseURL: `http://localhost:${testPort}`,
trace: 'on-first-retry',
screenshot: 'only-on-failure',
// PW_VIDEO=1 records every test (demo/verification clips); pair with
// `--output <dir>` outside test-results/, which peer runs wipe.
video: process.env.PW_VIDEO ? 'on' : 'off',
// The task panel opens Flat (web/src/components/tasks/panel-design.ts), its status
// sections and no list. The specs written against the list remember By project, in
// every page's storage, so a spec that serves its own ui-prefs keeps it too; a spec
// about Flat presets it (status-section-helpers.ts presetPanelDesign).
storageState: {
cookies: [],
origins: [{ origin: `http://localhost:${testPort}`, localStorage: [{ name: 'walnut-todo-panel-design', value: 'project' }] }],
},
},
projects: [
// Chromium mutes itself headless; `--mute-audio` keeps a headed run (--headed, a
// debug session) silent too. WebKit has no such switch: pw-silence.ts silences
// every frame of every context (2026-11-08).
{
name: 'chromium',
use: { ...devices['Desktop Chrome'], launchOptions: { args: CHROMIUM_SILENT } },
testIgnore: [...sideSpecs, ...hostFixtureSpecs],
},
{
name: 'chromium-host-fixture',
use: { ...devices['Desktop Chrome'], launchOptions: { args: CHROMIUM_SILENT } },
testMatch: hostFixtureSpecs,
testIgnore: sideSpecs,
workers: 1,
},
// The desktop app is a WKWebView, so a desktop-only scroll/paint bug needs a
// WebKit run to count as verified. Opt in: PW_WEBKIT=1 … --project webkit
// The replica smoke is one chromium spec (its WebKit runs are replica.config.ts's).
// The host fixture specs run one at a time there too, as on chromium-host-fixture
// (2026-11-06: with 2 workers, BP-C58's fixture hosts reached BP-C49's page and
// BP-C68's fixture was swapped under it, on every commit). Both projects are named
// `webkit`, so `--project webkit` keeps selecting every WebKit spec (Playwright
// matches projects by name and gives each its own id and worker limit).
...(process.env.PW_WEBKIT ? [
{ name: 'webkit', use: { ...devices['Desktop Safari'] }, testIgnore: [...sideSpecs, ...hostFixtureSpecs, '**/replica-smoke.spec.ts'] },
{ name: 'webkit', use: { ...devices['Desktop Safari'] }, testMatch: hostFixtureSpecs, testIgnore: sideSpecs, workers: 1 },
] : []),
],
webServer: {
// The fixture server runs on bun (tests/helpers/bun-runtime.ts), which runs
// its TypeScript as it is; only this runner stays on Node. Never `npx`: its
// registry resolution alone once took 88s here, past the whole boot budget.
command: `${bunCommand()} tests/e2e/browser/test-server.ts`,
url: `http://localhost:${testPort}/api/dashboard`,
reuseExistingServer: !process.env.CI,
// Cold boot of the fixture (real server + vite + isolated daemon) measured at
// ~20s on an idle Mac; under load it blew past the old 30s ceiling and the run
// died with "Timed out waiting 30000ms from config.webServer" — which reads
// like an app bug but is just a starved machine. 120s absorbs that.
timeout: 120_000,
stdout: 'pipe',
stderr: 'pipe',
// Without this Playwright ends EVERY run with SIGKILL on the fixture's process
// group (playwright-core processLauncher), so test-server.ts's SIGTERM handler
// — the only thing that stops its isolated daemon and removes its tmpdir —
// never ran, and each run left a `walnut-pw-<ts>` dir behind (401 of them on
// 2026-09-13). SIGTERM first; SIGKILL still follows if the handler hangs.
gracefulShutdown: { signal: 'SIGTERM', timeout: 15_000 },
},
})