Scrubbed extract of
ADP-UI/IPs/IP-OUI-11.3-ADP-UI-Test-And-A11y-Hardening.md— cited by the Chronicle (06-extraction-and-divergence). Redactions: phone / service detail / secrets / clearance / personal / internal hosts.
type: implementation-plan category: Work project: ADP-UI epic: null priority: Medium status: in-review created: 2026-07-12 effort: null precedence:
- “[[IP-OUI-11.2-Consume-Back-Refactor]]” parallel_group: null review-profile: standard model: null tokens: 0 tags: [implementation-plan, adp, adp-ui, testing, a11y, gap-review]
IP-OUI-11.3: Harden @adp/ui test coverage, a11y verification, and coverage config
Mission
From the 2026-07-12 ADP deep gap review of the adp-ui repo (Epic-11 shared package). Findings: (1) 35 component/module .tsx files but only 5 test files — the 22 src/components/common/* components share one aggregate test with no per-component visibility; (2) many components carry aria-/role attributes (Countdown, IconFrame, MeterBar, NumberInput, OptionCard, ProgressBar, Select, Toggle, ConnectionIndicator, GateAlertBanner) but no axe dependency exists — a11y is asserted by convention, never verified; (3) vitest.config.ts:1-9 has no coverage: section, so CI’s adp-ui-coverage artifact step likely uploads nothing meaningful; (4) no Storybook/visual-regression tooling for a design-system package — decide whether that’s an accepted scope cut and record it.
Execution Phases
- Per-component (or grouped-with-assertions-per-component) tests for
src/components/common/*— 5 new files, 79 → 217 tests -
vitest-axe/axe-coreadded; a11y assertions on the aria-bearing components -
coverage:config in vitest with a threshold; CI artifact verified non-empty - Storybook/visual-regression decision recorded — recommendation: descope; awaiting user confirmation (the only open phase)
Outcome (2026-08-22)
Delivered in [PR #14](https://internal host/homelab/adp-ui/pulls/14), release:minor.
Implementation dispatched to localmoe-qwen3.8-27b across six bounded lanes
(cluster 0 + A–E), each frontier-reviewed against git ground truth before the
next was dispatched.
| Metric | Before | After |
|---|---|---|
| Test files | 7 | 13 |
| Tests | 79 | 217 |
| Statements | 72.12 | 79 |
| Branches | 59.64 | 81.36 |
| Functions | 71.84 | 83.33 |
| Lines | 72.56 | 79.33 |
src/components/common reaches 100% statements / functions / lines, 94.11% branches.
Thresholds enforced at 70 / 55 / 70 / 70 — measured, then rounded down to the
nearest 5 so the gate has headroom without being decorative. Gate itself proved
by inversion (lines: 100 → exit 1 on the threshold error alone).
Finding 2 of the mission is why this IP mattered
“a11y is asserted by convention, never verified” turned out to be understating it.
Composer’s label prop rendered as a disconnected <span> — no htmlFor, no
aria-label — so its <textarea> had no accessible name at all. axe’s label
rule fires on every instance; screen-reader users got an unlabelled edit field.
It survived the full extraction (11.1/11.2) and both consumers precisely because
nothing asserted on the accessibility tree. Fixed with a real <label htmlFor>
via useId(); prop contract and class names unchanged.
Three tests that passed for the wrong reason
Each was caught by inversion, not by reading:
NumberInput“rejects letters” —type="number"sanitizes'abc'to''before the handler runs, so it never reachesNumber.isNaN. Kept with a scope comment; it still asserts the real user-facing contract.Toggledisabled —fireEvent.clickdispatches straight at the node and reaches React’s handler even on a disabled input. Rewritten throughuserEvent, which honoursdisabled.Tabscall count — Radix activates a trigger on bothmousedownandfocus, so one click emits two value changes. AssertstoHaveBeenCalledWithrather than a count that would pin Radix’s internals.
22 inverted proofs across the five lanes; all five clusters logged PASS in the model-perf ledger.
Follow-up, deliberately not in this PR
Tabs.tsx:30 renders item.description with className="adp-progress-bar__label"
— a copy-paste from ProgressBar. Renaming it is a consumer-visible styling-hook
change and does not belong in test hardening.
References
- Origin: captured via vault-pipeline 2026-07-12 from staged task
Tasks/Work/adp-ui-test-a11y-coverage-hardening.md(ADP deep gap review 2026-07-12)