mirror of
https://github.com/hanzo-apps/tabs.git
synced 2026-09-15 19:32:29 +00:00
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.
32 lines
1.3 KiB
TypeScript
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,
|
|
},
|
|
});
|