Skip to content

Commit the screen-reader harness as test/manual - #3600

Merged
tanem merged 1 commit into
masterfrom
commit-the-screen-reader-harness
Aug 3, 2026
Merged

Commit the screen-reader harness as test/manual#3600
tanem merged 1 commit into
masterfrom
commit-the-screen-reader-harness

Conversation

@tanem

@tanem tanem commented Aug 3, 2026

Copy link
Copy Markdown
Owner

The suite runs in jsdom, which has no accessibility layer and no paint, so every accessibility claim this package makes is pinned as markup: that role="img" is set, that the <title> and <desc> nodes exist, that aria-labelledby points at their IDs. Nothing observes what assistive technology does with any of it. That is this repo's blind spot rather than a shared one, since @tanem/svg-injector ships no ARIA behaviour of its own — the wiring in beforeEach and the loading element this package mounts and unmounts are the whole accessibility contract.

The harness that answered the loading-announcement question was built outside the repo and would have been rebuilt from scratch next time, along with the wrong turns it already encodes: an instrument check that distinguishes a mutated live region from an inserted one, a request-count check behind each cached remount, and a port choice that WebKit blocks and Chromium quietly does not.

This commits it as test/manual/, following the same shape @tanem/svg-injector used for its transport checks: outside the run, deliberately not wired into CI, since the instrument is a real screen reader and automating it would mean simulating the thing it exists to escape. Jest's testMatch is test/*.spec.ts?(x), so nothing new is picked up, and files in package.json is dist and src, so nothing new is published.

AGENTS.md gains the paragraph stating the blind spot and pointing at the harness. That instruction did not exist before and is half the point of the change.

React comes from node_modules, not esm.sh

The harness previously pinned React through esm.sh, which meant a first load needed network and a second React version had to be kept in step with package.json by hand.

React ships no ES module build, so server.mjs now serves the CJS files it does ship, wrapped in a module/exports/require/process shim, and index.html loads them as classic scripts ahead of any module. The import map points react and react-dom/client at generated ESM shims over those globals, whose named exports are read off the real module as it is served — so a React upgrade that adds an export needs no edit here. The named exports matter because dist/react-svg.mjs does import * as React from 'react'; a default-only shim would hand it an empty namespace.

Two details are worth knowing before editing that code:

  • React's exports map does not expose ./cjs/*, so the files are located through each package's package.json rather than resolved directly.
  • A throw inside the request handler took the whole server down with it, which in a hand-run harness costs the run rather than one response. It now answers 500 on the failing request and keeps serving.

The harness is also covered by lint and format now instead of being excluded with the rest of the git-ignored scratch area. sort-keys is off for it, because app.mjs's step table is in the order the steps are run in.

Verification

npm test green, React matrix included.

The page itself ran end to end in Playwright chromium and then in stock Chrome 151: React 19.2.4 loaded from node_modules, all six steps ran, both cached remounts reported 0 requests served, the control held its loading element for 2509ms, and the console carried nothing but React's DevTools notice. The server's own hit counter corroborates the cache hits independently — exactly one request per icon across the whole session, none during either cached case.

No screen reader was running for that check, so it says only that the harness works. It is recorded in the README separately from the VoiceOver result for that reason. The Safari/VoiceOver measurement is carried across unchanged: no announcement, and lifetime is not the variable, because VoiceOver ignores a live region that arrives with its content already in it.

🤖 Generated with Claude Code

The suite is jsdom, so every accessibility claim this package makes is
pinned as markup and nothing observes what a screen reader does with
it. That is this repo's blind spot rather than a shared one, since
svg-injector ships no ARIA behaviour of its own. The harness that
answered the loading-announcement question was built outside the repo
and would have been rebuilt from scratch next time, along with the
wrong turns it already encodes: the instrument check that distinguishes
insertion from mutation, the request-count check behind a cached
remount, and the port choice WebKit blocks.

React now comes out of node_modules rather than esm.sh. It ships no ES
module build, so the server wraps the CJS files it does ship and the
page loads them as classic scripts ahead of any module, with generated
ESM shims behind the import map. The harness needs no network and has
no second React version to keep in step with package.json.

AGENTS.md gains the paragraph stating the blind spot and pointing at
the harness, which is the half of this that did not exist before.
@tanem tanem added the internal label Aug 3, 2026
@tanem
tanem merged commit d7aae91 into master Aug 3, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant