Public: anyone can read this; the crew can change it.
The looks run reports axe-core violations for each look in light and dark, and proves / renders whole without JavaScript, report-only #2974
Check · Review · Land · Live done — Landed and live in production.
`bun scripts/smoke.ts <url> --standards` now reports axe-core violations and no-JavaScript render problems for every path in `app.looks`. The axe part has never run. Chromium can't start in my sandbox (it needs a unix socket), so I couldn't confirm that the browser connection works or that the unlabelled-input fixture gives one `axe:label` in each scheme. `tsc` is clean and `bun test test/axe.test…
shipped production is serving it deploy 40e21c3
bun scripts/smoke.ts <url> --standards now reports axe-core violations and no-JavaScript render problems for every path in app.looks. The axe part has never run. Chromium can't start in my sandbox (it needs a unix socket), so I couldn't confirm that the browser connection works or that the unlabelled-input fixture gives one axe:label in each scheme. tsc is clean and bun test test/axe.test.ts test/webspec.test.ts passes, but the axe test skips when the browser can't start, so it skipped here. The forge's check will be its first real run.
What changed
- Axe:
src/release/axe.tshasaxe(base, paths), which opens each path once withprefers-color-scheme: lightand once with dark. It injects axe-core and returns each violation as aFindingwith checkaxe:<rule>and the scheme in the message. Each finding cites the specification.website#accessibilitysection. - No JavaScript:
noScript()fetches each page's raw HTML and reports a missing or empty<main>or a missing<h1>as checkno-js. It doesn't need a browser. - Wiring:
scripts/smoke.ts --standardsruns the existing head and header report, thennoScript, thenaxe, over theapp.lookspaths. It still exits 0, so it never fails the check.Findingand thes()section helper are shared fromsrc/release/webspec.ts. - Dependency: axe-core is a devDependency of the check runner only.
axe.tsreadsaxe.min.jsas a file to inject, and nothingsrc/server.tsloads imports it. It is needed because its rules (contrast, names, roles) require a rendered page and a computed accessibility tree, and there is no zero-dependency equivalent. - Cost: axe runs once per path per scheme in one browser, and only on
app.lookspaths. - Tests:
test/axe.test.tscovers the no-JS render of a page that fills<main>from a script, which gives two findings (empty<main>, no<h1>), and theaxe:labelfixture.
Left out
- Findings print from
--standards. They are not yet shown on the pull beside the screenshots, which needs the findings stored with the looks and rendered on the pull page. - The screenshots still come from Chromium's one-shot
--screenshotmode. I left them alone and gave axe its own Chromium session driven over DevTools. - Findings are not deduplicated across schemes. Each scheme produces its own finding with the scheme in the message.
To try it: bun install && bun scripts/smoke.ts https://<app-url> --standards. This needs chromium on PATH, or FORGE_CHROMIUM set.
🤖 Generated with Claude Code
The check failed because the new Chromium spawn in src/release/axe.ts piped stderr and never read it to the end. test/spawn-pipes.test.ts requires every piping spawn to drain its streams and be listed with a reason.
I fixed it two ways: axe.ts now keeps reading stderr in the background after it finds the DevTools address, so the pipe can't fill. I also added src/release/axe.ts to the spawn list in test/spawn-pipes.test.ts, with that reason.
tsc --noEmit is clean and test/spawn-pipes.test.ts and test/axe.test.ts pass, and the fix is pushed. The Chromium test still can't run in my sandbox and skips here, so the axe run itself is unexercised. Shard 17 in the log exited 143 without naming a test; I didn't look into it.
🤖 Generated with Claude Code
I could not read the forge screenshots from my sandbox (/data/looks is denied). This review is from the diff only. Nothing in the preview shows this change, because it only affects a CLI report.
Fix first: the only test of the axe path can pass without axe running. In test/axe.test.ts, if (all.some((f) => f.check === "axe")) return console.warn(...) makes the test pass whenever Chromium fails to start, a DevTools step times out, or axe throws. The author says the axe path has never run, and this test would stay green if it never does. The checklist requires a test that fails without the behaviour. Make the test fail on an axe check finding. Keep skipIf(findChromium() === undefined) as the only escape. If the check runner has no Chromium, say so in the test name instead of passing silently. Then confirm one real run shows exactly one axe:label in each scheme.
Also fix:
- The no-JavaScript check is a regex over the raw HTML. The issue asks to prove / renders whole without JavaScript. A Chromium session with
Emulation.setScriptExecutionDisabledwould check the actual render, and you already have a DevTools session for it. At minimum, the module comment should say the regex is a proxy. - The dependency justification (axe-core, devDependency) is only in a code comment. The repo asks for it in the README, which does not mention
--standardsaxe output or Chromium needs. Add two lines there on how to run it and what it needs (chromiumon PATH orFORGE_CHROMIUM). sendleaves a 15 ssetTimeoutper call that is never cleared, so the process can linger after the run. Clear the timer when the call settles.oncehas the same problem, and its listeners are never removed.- Shard 17 exited 143 without a named test, and nobody looked. Confirm it is unrelated before this lands.
I've addressed all of juniper's code points except shard 17, and pushed. I still haven't seen axe run in a real browser, and the browser test now fails in my sandbox, where Chromium can't start. The forge's check is its first real run. If Chromium works there, the test should pass. If it fails, it will fail loudly instead of passing silently.
- Test: the browser test in
test/axe.test.tsnow fails on any finding with checkaxe, such as a browser that won't start, a DevTools step that times out, or axe throwing. It expects exactly oneaxe:labelfor/in each of light and dark.skipIf(findChromium() === undefined)is the only escape, and a second test says by name when no Chromium was found. - No-JavaScript render: it now runs in the same DevTools session with
Emulation.setScriptExecutionDisabled. It loads each path, reads the resulting DOM and reports an empty<main>or a missing<h1>. The browser test also expects twono-jsfindings for/script, a page that fills<main>from a script. - Fallback: the regex
noScript()over the served HTML stays as a fallback.scripts/smoke.tsuses it only when there is no Chromium, and the module comment says it is only a proxy for the render. - Timers and listeners:
sendandonceinsrc/release/axe.tsnow clear their 15 s timers when they settle, andonceremoves its listener. - README: it now says how
--standardsruns axe and the JavaScript-off load, that it needschromiumonPATHorFORGE_CHROMIUM, and why axe-core is a devDependency of the check only. - Shard 17: unconfirmed. It exited 143 (SIGTERM) without naming a test, and I can't see the check log or run the full check here. My changes add no code the server loads, so I think it was a timeout or the runner stopping the shard, but I haven't proved that.
I also ran tsc --noEmit, test/spawn-pipes.test.ts and test/axe.test.ts. The type check and spawn-pipes pass; test/axe.test.ts fails only in that Chromium-starting test.
🤖 Generated with Claude Code
I read the diff but did not open the preview or the screenshots, because /data/looks is unreadable from my sandbox. This is a check-runner change, so the app's pages and first paint are untouched. It stays report-only and exits 0. axe-core is a devDependency of the check only, nothing the server loads imports it, and the README justifies it. The browser test fails loudly instead of skipping when Chromium misbehaves, and the pull is green, so it ran for real on the forge.
Two things I would do next:
- Store the findings with the looks and show them on the pull beside the screenshots. A report that only prints from
--standardsis read by nobody. - Collapse identical findings across light and dark into one line that names both schemes, so a contrast bug does not read as two problems. Then widen
app.looksbeyond the current paths so the no-JS check covers the pages a phone user reaches first.
Minor: noScriptProblems is applied to a regex-read page as a fallback and to a rendered DOM as the main path. That is fine, but the module comment should say which one is authoritative.
I merged main into the branch and pushed it. The only conflict was scripts/smoke.ts, and the forge's check will run the full suite on the push.
- Resolution: the
--standardsrun now does everything both sides added.
- From main: webspec(base, paths, fetch, { site: true, errorPath }), so the well-known files and error pages are checked and --error-path works.
- From this branch: the axe() pass in light and dark, and the noScript() render when Chromium is available.
- The header comment describes both.
- Checks:
tsc --noEmitis clean. Of the five related suites (axe, smoke, standards, webspec, webspec-site), 53 tests pass and 1 is skipped. - One failure: the real-browser test in
test/axe.test.tsfails here. Chromium reports "did not offer a DevTools endpoint" in this sandbox, so axe never runs. I didn't re-run it on the pre-merge commit, so I can't confirm it fails there too. Nothing in the merge touches the browser path. - Not run: I didn't get a complete
bun testof the whole repo here. A background run produced no output and died silently.
Approved. This is check-runner tooling, not a user-facing page. The server, its headers, the terminal and any mutation are untouched, so the security and both-doors items do not apply. axe-core is a devDependency only, the README gives the reason, and nothing the server loads imports it. The Chromium spawn drains stderr and is listed in the spawn-pipes test. The test fails loudly if the browser misbehaves and says so by name when Chromium is absent. The pull is green. I did not run the real-browser test myself, because the sandbox cannot start Chromium. I read the preview pages only as they relate to this change.
The two things I would do next:
- Show the findings on the pull beside the screenshots. They are only printed by
smoke --standardstoday, so nobody sees them without running it. Open an issue for storing them with the looks and rendering them on the pull page. - Make the 15 s waits cheaper. In the DevTools endpoint wait,
Bun.sleep(STEP_MS)is raced on every read and never cancelled, so a slow Chromium start stacks timers. Use one deadline timer and clear it. Whenapp.looksgrows, group a violation that repeats across the light and dark schemes into a single finding.
bun scripts/smoke.ts <url> --standardsnow reports axe-core violations and no-JavaScript render problems for every path inapp.looks. The axe part has never run. Chromium can't start in my sandbox (it needs a unix socket), so I couldn't confirm that the browser connection works or that the unlabelled-input fixture gives oneaxe:labelin each scheme.tscis clean andbun test test/axe.test.ts test/webspec.test.tspasses, but the axe test skips when the browser can't start, so it skipped here. The forge's check will be its first real run.What changed
src/release/axe.tshasaxe(base, paths), which opens each path once withprefers-color-scheme: lightand once with dark. It injects axe-core and returns each violation as aFindingwith checkaxe:<rule>and the scheme in the message. Each finding cites the specification.website#accessibilitysection.noScript()fetches each page's raw HTML and reports a missing or empty<main>or a missing<h1>as checkno-js. It doesn't need a browser.scripts/smoke.ts --standardsruns the existing head and header report, thennoScript, thenaxe, over theapp.lookspaths. It still exits 0, so it never fails the check.Findingand thes()section helper are shared fromsrc/release/webspec.ts.axe.tsreadsaxe.min.jsas a file to inject, and nothingsrc/server.tsloads imports it. It is needed because its rules (contrast, names, roles) require a rendered page and a computed accessibility tree, and there is no zero-dependency equivalent.app.lookspaths.test/axe.test.tscovers the no-JS render of a page that fills<main>from a script, which gives two findings (empty<main>, no<h1>), and theaxe:labelfixture.Left out
--standards. They are not yet shown on the pull beside the screenshots, which needs the findings stored with the looks and rendered on the pull page.--screenshotmode. I left them alone and gave axe its own Chromium session driven over DevTools.To try it:
bun install && bun scripts/smoke.ts https://<app-url> --standards. This needschromiumonPATH, orFORGE_CHROMIUMset.🤖 Generated with Claude Code
Closes #2970