Files
tabs/playwright.config.ts
zooqueen 31a8b4d6de a surface guard, so the four defects cannot come back quietly
hanzo.yml already promised that "everything that needs a browser is verified
against a real one". Nothing kept it. Four defects were live on two screens and
were found by hand, which means the next four will be too.

The guard builds the export and walks every screen a stranger can reach —
the front page and the sign-in wall — at 390, 768 and 1440, asserting four
things a reader is owed: no sideways scroll, every control at least 44px tall,
every colour pair clearing AA, and no run of prose under 12px.

Three decisions worth keeping:

It serves out/ the way the bucket does, resolving a directory to its index. A
test server that routed differently from hanzoai/static would pass on pages that
404 live.

The contrast reading resolves each element's OWN painted background by walking
up to the nearest ancestor that paints one. Comparing everything to the body's
is wrong in the one direction that matters: dark text on a light button reads as
1:1 against a dark page and reports a failure that is not there. That false
positive is why the reading lives in a named function instead of at each call
site — it was hand-written wrong once already.

The four readings are SOFT. A hard expect throws on the first failure, so a short
tap target hid the type size behind it and the screen would have taken one CI run
per defect to come clean. Verified by putting all four defects back: it reports
tap targets and type size together, at all three widths, naming each element and
its measured size.

A label may be small; eight words may not. That is the line between the 11px
eyebrow, which is fine, and the 11px paragraph, which was not.

Not checked: line breaking. A widowed last line is a measure decision, and a rule
general enough to catch one would flag prose that is fine.
2026-08-17 03:50:35 -07:00

32 lines
1.3 KiB
TypeScript

import { defineConfig, devices } from '@playwright/test';
// Three widths, because the defects this guard exists to catch were width-bound:
// a measure that only widowed above the phone, and tap targets that only matter
// on one. A single viewport would have passed while two of the three were wrong.
const PORT = 4321;
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: 0, // a layout reading is deterministic; a retry would only hide a flake
reporter: process.env.CI ? [['list'], ['html', { open: 'never' }]] : 'list',
use: {
baseURL: `http://127.0.0.1:${PORT}`,
trace: 'retain-on-failure',
},
projects: [
{ name: '390', use: { ...devices['Desktop Chrome'], viewport: { width: 390, height: 844 } } },
{ name: '768', use: { ...devices['Desktop Chrome'], viewport: { width: 768, height: 1024 } } },
{ name: '1440', use: { ...devices['Desktop Chrome'], viewport: { width: 1440, height: 900 } } },
],
// Serves out/ the way the bucket does. The build has to have run: the guard
// checks what ships, not what a dev server renders.
webServer: {
command: `PORT=${PORT} node e2e/serve.mjs`,
url: `http://127.0.0.1:${PORT}/`,
reuseExistingServer: !process.env.CI,
timeout: 30_000,
},
});