--- id: web-testing title: The app has a test runner, not regexes over source status: continuous repos: [headquarters] dependsOn: [] exitCriterion: > A test can render a screen and assert on what the accessibility tree says, rather than on the source that produced it. --- # web-testing `npm test` builds and runs `node --test` over `web/scripts`. That is enough for the checks that read `dist/` and for `destinations.ts` — node strips the types off a `.ts` import on its own now. What it cannot reach is anything that needs a DOM, and the app is mostly things that need a DOM. The route predicate in `router.ts` is the first thing worth covering and still is not. ## How far the fake goes `dom.test.mjs` is the first test that needed a DOM and got it from a twenty-line fake `document` rather than a runner. That works because `el()` touches one method. `render.test.mjs` is the limit: parentage, `contains`, `replaceChildren` and one `activeElement`, which is enough to hold `render()`'s focus rule from both sides. It already asserts that the only selector ever passed is `h1`, because answering a real one would mean writing a query engine. `router.ts` reads `window.location` and renders, and faking that is where the fake stops paying. ## What the absence costs `signin-form.test.mjs` is the bill. The thing worth asserting is what the accessibility tree says about the rendered form, and with no DOM it asserts the source shapes that produce it instead. Every one of those is a regex on a file, and they will all have to be rewritten the day there is a runner. That is the argument for doing this: not that the current tests are wrong, but that they are written against the wrong thing and the cost of the rewrite grows with every one added. ## Open - [ ] **Choose a runner.** The decision is not made and should be written down when it is, along with what it costs the build. - [ ] **`router.ts`'s route predicate**, which is the reason to have one. - [ ] **Rewrite `signin-form.test.mjs`** against the rendered form. - [ ] **A rendered-page check for [site-a11y](site-a11y.md)**, which currently has no gate at all. ## Done - [x] **A fake DOM, as far as it goes.** `dom.test.mjs` got a twenty-line fake `document` rather than a runner, which works because `el()` touches one method. `render.test.mjs` extends it to parentage, `contains`, `replaceChildren` and one `activeElement` — enough to hold `render()`'s focus rule from both sides, and about the limit: it already asserts that the only selector ever passed is `h1`, because answering a real one would mean writing a query engine.