Target: WCAG 2.2 level AA.
This app has unusual accessibility problems because it captures the keyboard globally. The requirements below are acceptance criteria, not aspirations: a change that breaks one of them is a bug, not a polish item.
@axe-core/playwright runs against every view in both themes in CI. Axe cannot
see most of what matters here, so the manual checklist below is the real test.
| What | Where |
|---|---|
| Axe, WCAG 2.0/2.1/2.2 A and AA rules | tests/e2e/load-layout.spec.ts |
| Contrast, both themes, computed styles | tests/e2e/contrast.spec.ts |
| Contrast maths | tests/unit/contrast.test.ts |
| Reflow at 320, 600 and 1280 px | tests/e2e/reflow.spec.ts |
| Visible focus ring | tests/e2e/load-layout.spec.ts |
| Keyboard-only reachability | tests/e2e/load-layout.spec.ts |
| Adjustable and disableable time limits | tests/unit/limits.test.ts |
| Sprint duration control, countdown and warnings | tests/e2e/sprint.spec.ts |
| A sprint timer never acting on another drill | tests/regression/prototype-failures.test.ts |
| Escape releases capture, and Tab still moves focus | tests/e2e/pending.spec.ts |
| Marks told apart by decoration, not colour alone | tests/e2e/drill.spec.ts |
| The drill surface's name and its description | tests/e2e/drill.spec.ts |
| Words never broken across lines, at three widths | tests/e2e/drill.spec.ts |
| Both button variants, both themes | tests/e2e/drill.spec.ts |
| Nothing moves on the drill surface under reduced motion | tests/e2e/drill.spec.ts |
| Drill text, marks, capture and result behaviour | tests/functional/drill-view.test.ts |
Run through this before any release, and whenever the drill surface changes.
Items marked automated are also checked in CI, at the test named beside them; the rest need a person. An automated item still belongs on this list, because a test can only check what it was told to look for.
The drill surface captures printable keys at the document, in the capture phase, and has no input element of its own. Three things make that escapable, and all three are asserted: Escape releases capture, Tab is never consumed in any state, and focus moving off the surface releases capture on its own.
- Escape releases capture from anywhere in a drill, every time. Automated,
mid-drill and from the result screen, in
tests/e2e/pending.spec.tsandtests/functional/drill-view.test.ts. - After Escape, Tab moves through the page normally. Automated, forwards
and backwards, in
tests/e2e/pending.spec.ts. - How to escape is stated in visible text, not only in a tooltip or a title
attribute. Automated: the visible text is asserted, and the absence of a
titlewith it, intests/functional/drill-view.test.ts. - Focus is never trapped. There is always a way out with the keyboard alone. Automated as far as a test can go: Tab is never consumed, in any drill state. Whether it feels escapable still needs a person.
- Printable keys are captured with no visible input element. Automated in
tests/e2e/drill.spec.ts. - Correct, wrong and pending characters are told apart by text decoration and
weight as well as by colour. Automated from computed styles, both themes,
in
tests/e2e/drill.spec.ts. - The next-key readout names hand, finger and row in words, and the board
highlight follows it rather than leading. Automated in
tests/e2e/drill.spec.ts. - The result is shown at the end with the next step named in scoring's own
words. Automated in
tests/e2e/drill.spec.tsandtests/e2e/pending.spec.ts. - A word is never broken across two lines, at 320, 600 and 1200 CSS pixels.
Automated from client rects in
tests/e2e/drill.spec.ts; the structure and width budget that guarantee it are intests/regression/prototype-failures.test.ts. - A drill the loaded layout cannot type is refused with a message, not by dropping characters. Automated as a thrown error; the wording still needs a person to read it.
Tested with VoiceOver on macOS and NVDA on Windows. The regions, roles and the text put into them are asserted; what a screen reader actually says is not, and cannot be, so every item here still needs a manual pass.
- Individual keystrokes are not announced. No live region is written per keystroke, which is asserted; the announcement itself is manual.
- The current word is announced when it changes. The text is asserted in
tests/functional/drill-view.test.ts; the announcement is manual. - Errors are announced. A wrong key writes the expected key, its finger and its row into the polite region, which is asserted; hearing it is manual.
- The end-of-drill result is announced once, via an assertive region.
- Progress uses a polite region; the result uses assertive. Not the reverse. The roles are asserted; the behaviour is manual.
- The drill surface has an accessible name. Automated in
tests/e2e/drill.spec.ts. - Its description states the next key, the finger and the row. Automated:
the resolved
aria-describedbytext is asserted intests/e2e/drill.spec.ts.
- Finger identity is carried by text as well as colour, everywhere it appears.
- The board legend names each finger in words.
- The next-key readout names hand, finger and row. That is the primary
channel; colour reinforces it. Automated in
tests/e2e/drill.spec.tsandtests/functional/drill-view.test.ts. - Nothing about progress, error state or key state is conveyed by colour alone.
Automated for the drill marks, which carry
data-markand differ in decoration and weight; a person still has to check anything added later.
- The SVG is
aria-hidden, and nothing is available only there. - Every fact the diagram shows is also in text.
- Every text pair passes AA, 4.5:1, in both themes.
- Key caps on the board pass against the board surface.
- Finger colours pass the non-text threshold, 3:1, against the board surface.
- Both button variants pass in both themes. The prototype shipped an outline
button that rendered white on white; this is why the contrast test exists.
Automated twice over: from computed styles in
tests/e2e/drill.spec.ts, and from the stylesheet's own tokens intests/regression/prototype-failures.test.ts, which also refuses a button whose colour comes from anywhere but its own variant rule. - Focus rings pass 3:1 against adjacent colours.
-
prefers-reduced-motion: reduceremoves every transition. Automated for the drill surface, element by element, intests/e2e/drill.spec.ts, and for the board intests/e2e/board.spec.ts. The drill text carries no transition at all, in either mode: a mark has to land on the keystroke. - It also removes any celebratory animation. There is no celebration yet.
Sprint mode imposes a limit, so WCAG 2.2.1 applies. The exemption for essential timing is deliberately not claimed: a sprint is a measurement, and a measurement someone cannot take is not essential to anything.
- A duration control is offered, built from
SPRINT_DURATIONS_MSso the options and the durations cannot drift apart. Automated intests/functional/sprint-view.test.tsandtests/e2e/sprint.spec.ts. - An untimed option is offered, and it genuinely never expires. Automated
three times over, at three scales:
hasReachedLimitatNumber.MAX_SAFE_INTEGERintests/regression/prototype-failures.test.ts, a century of elapsed time through a running sprint timer intests/unit/sprint.test.tsandtests/functional/sprint-view.test.ts, and an hour of faked wall clock in a real browser intests/e2e/pending.spec.ts. - The limit is adjustable before the sprint starts, and the current setting
is stated in visible text rather than only shown as a selected option.
Automated in
tests/e2e/sprint.spec.ts. - The exemption for essential timing is not claimed.
- The countdown is visible while a timed sprint runs, and is a
role="timer"whose implicit live setting is off, so it is never announced per second. Automated intests/functional/sprint-view.test.tsandtests/e2e/sprint.spec.ts. - Time running out is announced politely at a handful of milestones — a
minute, thirty seconds, ten — so the countdown is never the only channel.
Automated: the count of announcements over a whole sprint is asserted,
in
tests/functional/sprint-view.test.ts. Hearing it is still manual. - A sprint feels escapable to someone using a screen reader: the milestones land without talking over the word announcements. Needs a person.
- Usable at 320 CSS pixels wide with no horizontal page scrolling.
- Usable at 400 per cent zoom.
- Only tables, diagrams and code may scroll horizontally, each inside its own container.
- Every interactive control is at least 24 by 24 CSS pixels. WCAG 2.5.8.
- Every control has a visible focus style meeting 2.4.11.
- Focus is never obscured by a sticky header or overlay.
Tracked rather than forgotten. Each becomes a checklist item once the feature exists.
- Screen reader testing is manual. There is no automated substitute. The regions, their roles and the text written into them are asserted, and that is as far as a test reaches: no test can hear VoiceOver or NVDA.
- The repair drill does not exist yet, so nothing checks what it says about the keys it was built from.
- Nothing yet checks the drill surface at 400 per cent zoom by hand. Reflow at 320 CSS pixels is automated, which is the same measurement from the other side, but it is not the same experience.