Files
tabs/e2e/surface.spec.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

72 lines
2.8 KiB
TypeScript

import { test, expect } from '@playwright/test';
import { audit, PROSE_FLOOR, REACH, type Audit } from './audit';
// Every screen a visitor can reach without a machine linked. The terminal panes
// need a live machine and a signed-in identity, so they are not here; these two
// are what a stranger sees, and both were wrong until they were measured.
const SCREENS = [
{ path: '/', name: 'front page' },
{ path: '/app/', name: 'sign in' },
];
for (const screen of SCREENS) {
test.describe(screen.name, () => {
test('fits its viewport, and every control is reachable and readable', async ({ page }, info) => {
await page.goto(screen.path);
// The measure depends on layout, so wait for it to settle rather than for
// a network idle that a static export reaches before styles apply.
await page.waitForLoadState('load');
await expect(page.locator('body')).toBeVisible();
const a: Audit = await page.evaluate(audit);
const where = `${screen.name} at ${info.project.name}`;
// SOFT, all four of them. A hard expect throws on the first failure, so a
// short tap target would hide the contrast and the type size behind it and
// the screen would take one CI run per defect to come clean. An audit is
// worth having only if it reports everything it found.
expect
.soft(
a.scrollWidth,
`${where}: scrolls sideways — ${a.scrollWidth}px of content in ${a.innerWidth}px`,
)
.toBeLessThanOrEqual(a.innerWidth);
expect
.soft(
a.shortTargets,
`${where}: ${a.shortTargets.length} control(s) under ${REACH}px tall — ` +
a.shortTargets.map((t) => `"${t.text}" ${t.w}x${t.h}`).join(', '),
)
.toEqual([]);
expect
.soft(
a.contrastFailures,
`${where}: ${a.contrastFailures.length} colour pair(s) below AA — ` +
a.contrastFailures.map((c) => `"${c.text}" ${c.ratio}:1 needs ${c.need}:1`).join(', '),
)
.toEqual([]);
expect
.soft(
a.smallProse,
`${where}: prose under ${PROSE_FLOOR}px — ` +
a.smallProse.map((s) => `"${s.text}" at ${s.px}px`).join(', '),
)
.toEqual([]);
});
});
}
// The three readings above answer "is anything wrong". This one answers "is the
// instrument still looking", which silence cannot distinguish from the first.
test('the audit sees the page it was pointed at', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('load');
const a: Audit = await page.evaluate(audit);
expect(a.innerWidth, 'no viewport width; the audit ran against nothing').toBeGreaterThan(0);
const controls = await page.locator('a, button').count();
expect(controls, 'the front page has links and buttons; finding none means the walk broke').toBeGreaterThan(3);
});