---
title: "For testing"
description: "The manual and automated passes that make up an accessibility test plan for a health screen, and how to reproduce each one."
url: "https://opsinjs.pensievelabs.org/accessibility/for-testing"
source: "https://opsinjs.pensievelabs.org/accessibility/for-testing.md"
section: "Accessibility"
kind: "accessibility"
reviewed: "2026-09-02"
reviewer: "engineering"
aliases: ["qa", "test plan", "accessibility testing", "axe"]
---

> Elements written as `<PascalCase … />` below are opsinjs documentation
> components. Their attributes are the content: the values they render are
> generated from `tokens/*.json` and `registry/catalogue.ts` and are
> published separately at https://opsinjs.pensievelabs.org/r/index.json and under the Reference
> section.
> Nothing is missing from this page. The data simply does not live in
> the prose.

<PageTemplate kind="accessibility" />

## What we guarantee [#what-we-guarantee]

One thing, and it is worth stating precisely: &#x2A;*automated testing finds a
minority of accessibility defects, and in health UI it finds the least dangerous
ones.** An automated pass will catch a missing label and a low-contrast pair. It
will not catch a status pill whose colour and whose word disagree, a trend arrow
pointing the wrong way, or a stale reading presented as current. Those are the
defects that hurt somebody.

So the guarantee is about the shape of the test plan rather than a tool: every
accessibility property this system claims is written on the relevant page as a
**reproducible check with an expected result and a described failure**, so a
tester who has never used a screen reader can still produce a trustworthy answer.

## What you own [#what-you-own]

The plan itself, its schedule, and the decision about what blocks a release. Also:

* **Choosing the tested matrix.** Which screen reader and browser combinations
  matter is a function of who uses your product. Ours is on
  [Screen readers](./screen-readers.mdx) and it is a proposal, not a
  prescription.
* **Test data that includes the bad cases.** Most health screens are tested with
  a comfortable number in the middle of the range. Test the empty case, the
  single-reading case, the stale case, the out-of-range case, the implausible
  case (a value no human produces, arriving from a broken sensor) and the case
  where two sources disagree.
* **Regression coverage of the announcement contract.** A live region that stops
  announcing is invisible to every visual test you own.

## How to check [#how-to-check]

Six passes. Run them in this order; each is cheaper than the one after it and
catches different defects.

<Steps>
  ### The automated pass [#the-automated-pass]

  Run an automated rule engine over every route in a real browser. Treat its output
  as a lower bound: zero violations means "no violations of the rules this engine
  can express", which is a much smaller claim than "accessible". Record the engine
  and its version in the result, because the rule set changes between versions and
  an unversioned pass cannot be reproduced.

  ### The keyboard pass [#the-keyboard-pass]

  Complete the primary task with the keyboard only, then complete the *recovery*
  task. Recovery means cancelling it, correcting a validation error, dismissing a
  dialog. Expected result: every stop shows a visible focus indicator, no stop is
  obscured by sticky chrome, and no sequence leaves you unable to progress.
  Failure looks like a focus indicator that disappears behind a bottom bar, which
  is SC 2.4.11.

  ### The zoom and text-size pass [#the-zoom-and-text-size-pass]

  Set the browser to 400% zoom at a 1280 px viewport width. That is the SC 1.4.10
  Reflow condition, and it is equivalent to a 320 px viewport. Separately, set
  text size to 200% without zooming. Expected result: no horizontal scrolling of
  the page as a whole, no clipped text, no truncated numeric value. Failure in a
  health screen usually looks like a reading truncated to its first two digits.
  Method on [Text resizing and zoom](./text-resizing-and-zoom.mdx).

  ### The greyscale pass [#the-greyscale-pass]

  Apply a full desaturation filter and repeat the primary task. Expected result: the
  status of every value is still readable, because the word and the icon are still
  there. Failure looks like two pills that were red and amber becoming the same
  grey. Method on [Colour independence](./colour-independence.mdx).

  ### The preference pass [#the-preference-pass]

  Run the task four times with `prefers-reduced-motion`, `prefers-reduced-transparency`,
  `prefers-contrast` and forced colours turned on in turn. Expected result: the
  information does not change, only its presentation. Failure looks like a state
  change that was carried entirely by a transition, so with motion reduced nothing
  appears to happen at all.

  ### The screen-reader pass [#the-screen-reader-pass]

  Complete the task with a screen reader on a device you have actually
  configured. Do not do it with the developer-tools accessibility tree, which
  shows you the data rather than the experience. Record what was announced, in
  what order, and what was silent. A silent success is a defect; a reader who
  submits a log entry and hears nothing does not know whether it saved.
</Steps>

<SafetyCallout severity="attention" evidence="opinion">
  Add one pass that no general accessibility guide will tell you about: &#x2A;*read the
  screen out loud to somebody who does not work on it and ask them what they should
  do next.** In consumer health the most expensive defects are comprehension
  defects, and no tool measures those. This recommendation is our opinion, not a
  research finding.
</SafetyCallout>

## Measured results [#measured-results]

<NoDataYet script="scripts/run-evals.mts" />

There are no published test results for this system. Playwright is in no
`package.json`, so a normal clone does not have it and
`scripts/capture-registry.mts` detects its absence and no-ops rather than
pretending to produce artefacts. The one job that installs it at job time is
`.github/workflows/nightly.yml`, and it runs the accessibility rig's browser
half against the built `/view` routes. That half measures hit area, survival at
1.3x and 200% with no truncated value, and it publishes nothing: it fails on
what is wrong and records nothing about what passed. When a suite exists, its
dated results appear here and on [Conformance report](./conformance-report.mdx).

## Known gaps [#known-gaps]

* **No test suite exists,** automated or manual, for the design system itself.
* **The regression gate covers rules, not criteria.** `pnpm run check:a11y`
  blocks a pull request on the rules in `scripts/check-a11y.mts`, and the nightly
  adds the three browser measurements above. Between them they touch a handful of
  the criteria on [Conformance report](./conformance-report.mdx) and none of the
  ones a person has to judge. Axe does not run over a rendered page anywhere,
  and no other rule engine does either. The nightly is on a schedule, so it goes
  red the morning after a merge rather than before it.
* **No assistive technology has been used against this codebase.** The docs site
  itself is unaudited; see [This site's accessibility](./this-site.mdx).

## Updates to this page [#updates-to-this-page]

<Reviewed />
