Skip to content

test(a11y): add zoom, reflow, and large-text regression tests - #121

Merged
Hallab7 merged 2 commits into
veridatum-labs:developfrom
noevidence1017:test/zoom-reflow-large-text
Sep 1, 2026
Merged

test(a11y): add zoom, reflow, and large-text regression tests#121
Hallab7 merged 2 commits into
veridatum-labs:developfrom
noevidence1017:test/zoom-reflow-large-text

Conversation

@noevidence1017

Copy link
Copy Markdown
Contributor

closes #81

Why

The existing axe and keyboard suites prove the app is accessible at a normal desktop and mobile viewport. That is not the same as proving it works for someone who zooms to 200%, reflows at 400%, enlarges their default font, or applies a text-spacing override — those change how much content fits without changing the window.

What this adds

File Role
e2e/accessibility/fixtures/display-modes.ts The four display modes and the DOM probes behind them.
e2e/accessibility/zoom-reflow.spec.ts 108 checks over six core routes and both public workflows.
playwright.a11y.config.ts Dedicated config for the accessibility suite (see prerequisite below).
docs/accessibility.md The modes, the exemption marker, how to fix each failure class, known gaps.
components/proofs/create-proof-flow.tsx Two real reflow fixes the suite found.
package.json Points the a11y scripts at the new config, adds test:e2e:a11y:zoom.

No ci.yml change is needed: the existing npm run test:e2e:a11y step runs the whole e2e/accessibility directory, so these specs are picked up automatically.

The four modes

Mode Produced by Criterion
zoom-200 640x400 CSS px (a 1280x800 window at 200% zoom) WCAG 1.4.4
reflow-320 320x256 CSS px (1280x1024 at 400%) WCAG 1.4.10
text-spacing line-height 1.5, letter-spacing 0.12em, word-spacing 0.16em, paragraph spacing 2em WCAG 1.4.12
large-text root font size 200% enlarged default font

Playwright cannot drive the browser's own zoom control, so the zoom modes set the CSS viewport to what that zoom level presents — which is exactly the condition the layout has to survive. The override stylesheets are installed through the CSSOM (adoptedStyleSheets) rather than page.addStyleTag, because the app's strict style-src 'self' 'nonce-...' correctly blocks an injected inline <style> and that policy is worth keeping.

Acceptance criteria

Core routes work at 200% zoom without clipped controls or hidden focus. Every focusable control must have a non-zero box, sit inside the viewport, and not be cut off by an ancestor hiding its overflow. Focus is tracked across 15 tab stops per route and must stay on screen and reachable in the hit-test stack.

Two refinements were needed to make these checks mean what they say, both documented in the fixture:

  • Elements that are not rendered at all are skipped. The desktop nav uses a hidden md:flex wrapper, so the links themselves still report display: block while not being rendered — checkVisibility() accounts for ancestors, a per-element display check does not.
  • Focus coverage is hit-tested at five points across the control and passes if the control appears anywhere in the stack at any of them. A label painted over part of its own checkbox is normal; being absent from every stack is what actually means the focus ring is behind something.

Content reflows at an equivalent 320 CSS-pixel width without two-dimensional scrolling except approved data regions. No element may extend past the viewport and scrollWidth may not exceed clientWidth. A region that genuinely needs a two-dimensional layout claims the WCAG 1.4.10 exemption with data-allow-horizontal-scroll — deliberately an explicit marker in the markup, so granting an exemption is a reviewable change in the component rather than a silent pass in the test. Nothing in the app claims it today.

Text spacing overrides do not obscure or overlap essential content. Leaf text elements are compared pairwise; more than 4px of shared edge in both axes is an overlap. This is the failure mode a spacing override produces first and it is invisible to an axe scan — the content is still in the accessibility tree while being unreadable on screen.

Proof creation and verification remain keyboard-completable in each mode. In all four modes: tab to the proof ID field, type, tab to submit, press Enter, and get a result panel; submit an invalid credential and land focus on its error alert; tab to the wallet button on /proofs/create, press Enter, and reach an enabled payments step with nothing clipped.

Real bugs this found

Two reflow bugs in the proof creation flow. At 320 CSS px the connected state forced the whole document ~465px wide:

- <p className="break-words">
+ <p className="break-all">
    Connected as <span className="text-cyan-200">{user.walletAddress}</span>

break-words (overflow-wrap) lets a long word wrap but does not reduce the element's min-content width, so a 56-character wallet address still forces the page hundreds of pixels wide. break-all (word-break) does reduce it, which is what an opaque identifier needs. The same fix applies to the transaction hash in each payment row.

- <label className="grid gap-2 …">
+ <label className="grid min-w-0 gap-2 …">
-   <input className="h-11 rounded-md …" />
+   <input className="h-11 w-full rounded-md …" />

A grid item defaults to min-width: auto, so the date inputs (wide by default) pushed past the viewport.

Both are documented in docs/accessibility.md as the two failure classes to look for.

Prerequisite: playwright.config.ts was a syntax error

playwright.config.ts on develop contains two concatenated defineConfig() blocks — the accessibility config and the functional e2e config, merged into one file by a bad merge. It does not parse (TS1005: ':' expected at line 57), so npm run test:e2e and npm run test:e2e:a11y cannot run at all, and npm run build fails type checking on it.

This PR splits them the same way commit f356924 split out the visual config: playwright.a11y.config.ts holds the accessibility suite (one worker, its own mocked API origin) and playwright.config.ts is restored to the functional e2e config alone. test:e2e:a11y points at the new file.

A zoom/reflow suite that cannot be executed is not a deliverable, so this repair is included rather than left as a note.

Known issue: the CSP nonce blocks hydration on prerendered pages

While getting the suite running I found that no JavaScript executes on statically prerendered pages under next start:

Loading the script 'http://127.0.0.1:3100/_next/static/chunks/….js' violates the
following Content Security Policy directive: "script-src 'self'
'nonce-uaOdoIEIdVR99RsDP6sleg==' 'strict-dynamic'". The action has been blocked.

middleware.ts issues a fresh nonce per request; the prerendered HTML carries the nonce baked in at build time. They never match, so the page refuses to load its own scripts and never hydrates. This is production-affecting, and it is also why six of the seven existing keyboard specs fail on develop/verify serves only its Suspense fallback, forms never respond, no error alert ever appears.

The accessibility config therefore sets bypassCSP: true in the browser context, so the suite exercises a real interactive interface rather than an inert page. That is a measure to keep the suite meaningful, not a fix: the headers the app sends are unchanged and tests/security/headers.test.ts still asserts on them directly. The nonce mismatch needs its own change, which I have deliberately kept out of an accessibility PR. Happy to open it separately.

Validation output

Full accessibility suite (axe scans, dynamic states, keyboard, and the new zoom/reflow specs), both projects, against a production build:

$ npm run test:e2e:a11y
  272 passed (7.1m)

New specs alone, before the two component fixes, showed the failures they were written to catch:

Error: Controls unusable on /proofs/create at reflow-320:
  input "" - extends past the viewport
  input "" - extends past the viewport
  input "" - extends past the viewport

and after:

$ npx playwright test --config=playwright.a11y.config.ts --project="Desktop Chrome"
  136 passed

Lint and type check over everything this PR touches:

$ npx eslint e2e playwright.a11y.config.ts playwright.config.ts components/proofs/create-proof-flow.tsx
(no output — clean)

$ npx tsc --noEmit
(no errors outside the pre-existing broken files listed below;
 playwright.config.ts now type checks, where before it did not)

npm test (jest) is untouched by this PR: same 6 pre-existing failed suites / 18 failed tests before and after.

Scope notes

  • The nav gap below md is reported, not failed. public-nav.tsx has no mobile menu — the links are simply hidden below the breakpoint, so at 200% zoom the primary navigation is absent with nothing to replace it. Adding a menu is a product change, not a regression fix; it is recorded in docs/accessibility.md as outstanding work.
  • Fixed heights on single-line controls are left alone. They survive all four modes as written; changing ~25 of them across every component would be a large diff with no failing check behind it.
  • No ci.yml change, so this does not collide with perf(bundle): add bundle composition and duplicate dependency budgets #102, which repairs the stray empty accessibility: job key that currently makes the whole workflow invalid YAML.

Pre-existing CI state on develop (not caused by this PR)

  1. .github/workflows/ci.yml is invalid YAML — stray empty accessibility: job key. (Fixed in perf(bundle): add bundle composition and duplicate dependency budgets #102.)
  2. npm ci failspackage.json lists @vitest/ui but package-lock.json does not contain it.
  3. npm run build fails type checking on tests/api/timeout-retry-cancel.test.ts and tests/components/use-api-data.test.tsx (new Promise(() {}), missing =>) and tests/contracts/schema-drift.test.ts (TS7053). The playwright.config.ts errors in that list are fixed here.
  4. npm test was already red: 6 suites / 18 tests, unchanged by this PR.

Viewport-only responsive checks prove a layout works in a narrow window.
They do not prove the interface works for someone who zooms, enlarges their
default font, or applies a text-spacing override. Add browser regression
checks for all four display modes across the core routes and both public
workflows.

- e2e/accessibility/fixtures/display-modes.ts: the four modes (200% zoom,
  400% reflow at 320 CSS px, WCAG 1.4.12 text spacing, doubled root font)
  and the DOM probes behind them - clipped controls, horizontal overflow,
  overlapping text, and focus visibility. Every probe reports the offending
  element so a red run names the fix.
- e2e/accessibility/zoom-reflow.spec.ts: 108 checks over /, /faq, /status,
  /verify, /verify/credential and /proofs/create, plus keyboard completion
  of proof verification, credential verification and wallet connect in each
  mode.
- Two real reflow bugs the suite found, both in the proof creation flow:
  break-words does not reduce an element's min-content width, so a wallet
  address and a transaction hash forced the page ~400px wide at 320 CSS px
  (now break-all); and the threshold and period inputs kept their intrinsic
  width in a grid item (now min-w-0 plus w-full).
- playwright.a11y.config.ts: the accessibility suite gets its own config.
  It previously lived as a second defineConfig() block inside
  playwright.config.ts, which made that file a syntax error and stopped the
  suite running at all.
- The config sets bypassCSP in the browser context. The per-request nonce
  from middleware.ts does not match the nonce baked into prerendered HTML,
  so a statically generated page never loads its own scripts under
  next start - which is also why six of the seven existing keyboard specs
  fail on develop. Documented as an outstanding app bug, not fixed here.
- docs/accessibility.md documents the modes, the opt-in marker for approved
  two-dimensional regions, how to fix each failure class, and the known
  navigation gap below the md breakpoint.

Closes veridatum-labs#81
@drips-wave

drips-wave Bot commented Aug 28, 2026

Copy link
Copy Markdown

@noevidence1017 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@vercel

vercel Bot commented Aug 28, 2026

Copy link
Copy Markdown

@noevidence1017 is attempting to deploy a commit to the hallab's projects Team on Vercel.

A member of the Team first needs to authorize it.

@Hallab7
Hallab7 merged commit 97f47ec into veridatum-labs:develop Sep 1, 2026
0 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add zoom, reflow, and large-text regression tests

2 participants