From 01cecc217342ebd7b01814799704b1109dd4c42b Mon Sep 17 00:00:00 2001 From: Sergey Kozyrenko Date: Tue, 21 Jul 2026 15:14:25 +0700 Subject: [PATCH] test(e2e): make the visual gate actually sensitive, and fix its masking race MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The suite was carrying Playwright's 0.2 per-pixel default, a tolerance meant for cross-machine rendering noise that the pinned container removes by construction. It cost the gate its sensitivity: a real palette change passed while three baselines silently went stale. Compare strictly and bound the differing-pixel count instead. Measured over repeated container runs, glyph antialiasing jitters by exactly 2 pixels, while the palette change that slipped through moved 71 — so threshold 0 with a 20-pixel allowance tolerates the noise and catches the class of change that got away. Strict comparison also exposed a real defect the loose one hid: the flow detail's ready locator was the header button, but the visual spec masks the terminal, which mounts later. Capturing in that window compared live terminal pixels against a masked baseline — a 261k-pixel diff, roughly a third of the frame, appearing at random. The route now counts as rendered when its terminal exists. Seven consecutive container runs are clean at the new settings. Co-Authored-By: Claude Fable 5 --- frontend/e2e/playwright.config.ts | 5 +++++ frontend/e2e/routes.ts | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/frontend/e2e/playwright.config.ts b/frontend/e2e/playwright.config.ts index e6fcd9be..a6be7650 100644 --- a/frontend/e2e/playwright.config.ts +++ b/frontend/e2e/playwright.config.ts @@ -36,6 +36,11 @@ if (tier === 'stand' && !TIERS.stand.baseURL) { } export default defineConfig({ + // Playwright's 0.2 per-pixel default absorbs cross-machine rendering noise the + // pinned container removes by construction; it also let a palette change through + // while reporting green. Count differing pixels instead: glyph antialiasing + // jitters by 2, while the palette change that slipped moved 71. + expect: { toHaveScreenshot: { maxDiffPixels: 20, threshold: 0 } }, forbidOnly: isCI, fullyParallel: true, globalTimeout: isCI ? 10 * 60_000 : undefined, diff --git a/frontend/e2e/routes.ts b/frontend/e2e/routes.ts index 1aa12151..c8664b50 100644 --- a/frontend/e2e/routes.ts +++ b/frontend/e2e/routes.ts @@ -44,7 +44,9 @@ export const ROUTE_MANIFEST: RouteManifestEntry[] = [ { cassette: flowsCassette, path: routes.flow('5'), - ready: (page) => page.getByRole('button', { name: 'Flow actions' }), + // The terminal mounts after the header, and the visual spec masks it — + // capturing before it exists compares live pixels against a masked baseline. + ready: (page) => page.locator('.xterm').first(), sources: [ 'src/pages/flows', 'src/features/flows',