test(a11y): add zoom, reflow, and large-text regression tests - #121
Merged
Hallab7 merged 2 commits intoSep 1, 2026
Merged
Conversation
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
|
@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! 🚀 |
|
@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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
e2e/accessibility/fixtures/display-modes.tse2e/accessibility/zoom-reflow.spec.tsplaywright.a11y.config.tsdocs/accessibility.mdcomponents/proofs/create-proof-flow.tsxpackage.jsontest:e2e:a11y:zoom.No
ci.ymlchange is needed: the existingnpm run test:e2e:a11ystep runs the wholee2e/accessibilitydirectory, so these specs are picked up automatically.The four modes
zoom-200reflow-320text-spacinglarge-textPlaywright 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 thanpage.addStyleTag, because the app's strictstyle-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:
hidden md:flexwrapper, so the links themselves still reportdisplay: blockwhile not being rendered —checkVisibility()accounts for ancestors, a per-elementdisplaycheck does not.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
scrollWidthmay not exceedclientWidth. A region that genuinely needs a two-dimensional layout claims the WCAG 1.4.10 exemption withdata-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:
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.A grid item defaults to
min-width: auto, so thedateinputs (wide by default) pushed past the viewport.Both are documented in
docs/accessibility.mdas the two failure classes to look for.Prerequisite:
playwright.config.tswas a syntax errorplaywright.config.tsondevelopcontains two concatenateddefineConfig()blocks — the accessibility config and the functional e2e config, merged into one file by a bad merge. It does not parse (TS1005: ':' expectedat line 57), sonpm run test:e2eandnpm run test:e2e:a11ycannot run at all, andnpm run buildfails type checking on it.This PR splits them the same way commit
f356924split out the visual config:playwright.a11y.config.tsholds the accessibility suite (one worker, its own mocked API origin) andplaywright.config.tsis restored to the functional e2e config alone.test:e2e:a11ypoints 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:middleware.tsissues 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 ondevelop—/verifyserves only its Suspense fallback, forms never respond, no error alert ever appears.The accessibility config therefore sets
bypassCSP: truein 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 andtests/security/headers.test.tsstill 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:
New specs alone, before the two component fixes, showed the failures they were written to catch:
and after:
Lint and type check over everything this PR touches:
npm test(jest) is untouched by this PR: same 6 pre-existing failed suites / 18 failed tests before and after.Scope notes
mdis reported, not failed.public-nav.tsxhas 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 indocs/accessibility.mdas outstanding work.ci.ymlchange, so this does not collide with perf(bundle): add bundle composition and duplicate dependency budgets #102, which repairs the stray emptyaccessibility:job key that currently makes the whole workflow invalid YAML.Pre-existing CI state on
develop(not caused by this PR).github/workflows/ci.ymlis invalid YAML — stray emptyaccessibility:job key. (Fixed in perf(bundle): add bundle composition and duplicate dependency budgets #102.)npm cifails —package.jsonlists@vitest/uibutpackage-lock.jsondoes not contain it.npm run buildfails type checking ontests/api/timeout-retry-cancel.test.tsandtests/components/use-api-data.test.tsx(new Promise(() {}), missing=>) andtests/contracts/schema-drift.test.ts(TS7053). Theplaywright.config.tserrors in that list are fixed here.npm testwas already red: 6 suites / 18 tests, unchanged by this PR.