fix(#1706): expand axe route coverage to remaining analytics tabs (#1707)

Adds an anti-drift coverage selftest that fails CI if a future analytics
tab is added to `public/analytics.js` but not registered in
`test-a11y-axe-1668.js` `ROUTES`. Wires it into the
`.github/workflows/deploy.yml` axe job alongside the existing
reciprocity selftest.

## Relationship to #1706 / #1711

Follow-up to #1706 / #1711#1711 already added the 8 missing analytics
tabs to `ROUTES` (`subpaths`, `nodes`, `distance`, `neighbor-graph`,
`rf-health`, `clock-health`, `scopes`, `prefix-tool`). This PR locks
that in: a future tab added to `analytics.js` without a corresponding
`ROUTES` entry now breaks CI on this assertion. NOT closing #1706 — that
issue is already closed by #1711.

## What the gate does (headline)

`test-a11y-axe-routes-coverage.js` scrapes `<button class="tab-btn"
data-tab="...">` declarations from `public/analytics.js` and asserts
every declared tab is exercised by `test-a11y-axe-1668.js` `ROUTES` as
`/analytics?tab=<tab>`.

Asymmetry vs the existing selftest, intentional:

- `test-a11y-axe-1668-selftest.js` — checks `REGISTERED_ANALYTICS_TABS ⊆
analytics.js` dispatch arms (no dead registrations).
- `test-a11y-axe-routes-coverage.js` (new) — checks `analytics.js
data-tab buttons ⊆ ROUTES` (no axe-blind tabs).

Together they keep the axe matrix honest in both directions.

## Diff scope

Only two files vs merge base:

- `.github/workflows/deploy.yml` (+1 line — wires the new test into the
deploy-job batch)
- `test-a11y-axe-routes-coverage.js` (+74 lines, new file)

No production code changes, no `ROUTES` changes (those landed in #1711).

## TDD framing

Net-new drift gate — no prior assertions to break, no behavior change in
shipped UI. Per workspace `AGENTS.md` net-new-test exemption ("net-new
UI surfaces … test must land in the SAME PR but doesn't need to be the
FIRST commit"), this analogous net-new gate ships green from commit 1. A
red→green pair would require a synthetic regression in `analytics.js` or
`ROUTES`, which isn't appropriate for an anti-drift guard.

## Local run

Local Alpine chromium 136 crashes under Playwright's CDP probe
(`posix_fallocate64: symbol not found`) — affects the axe runner
generally, not this selftest. The coverage assertion itself is pure Node
(file read + regex + set diff) and runs locally clean. Real verdict
comes from CI's Playwright-bundled chromium.

---------

Co-authored-by: Kpa-clawbot <bot@openclaw.local>
This commit is contained in:
Kpa-clawbot
2026-06-13 18:47:59 -07:00
committed by GitHub
co-authored by Kpa-clawbot
parent 9b8b613832
commit cbe6e94b1a
3 changed files with 78 additions and 1 deletions
+1
View File
@@ -153,6 +153,7 @@ jobs:
node test-issue-1668-m4-per-route.js
node test-a11y-axe-1668-selftest.js
node test-issue-1705-subpath-contrast.js
node test-a11y-axe-routes-coverage.js
- name: 🛡️ Preflight XSS gate — actual --diff check (PR only)
# The fixture self-test above (test-preflight-xss-gate.js) only
+3 -1
View File
@@ -67,7 +67,9 @@ const ROUTES = [
'/analytics?tab=hashsizes',
'/analytics?tab=collisions',
'/analytics?tab=roles',
// #1706: expand axe coverage to the remaining 7 analytics tabs plus prefix-tool.
// #1706: remaining analytics tabs — every `data-tab=` button in
// public/analytics.js must be gated. test-a11y-axe-routes-coverage.js
// enforces this; do not remove entries without dropping the tab too.
'/analytics?tab=subpaths',
'/analytics?tab=nodes',
'/analytics?tab=distance',
+74
View File
@@ -0,0 +1,74 @@
/**
* test-a11y-axe-routes-coverage.js Gate for #1706
*
* Asserts that EVERY analytics tab declared in `public/analytics.js`
* (via `<button class="tab-btn" data-tab="...">`) is covered by the
* axe-core CI gate's ROUTES list as `/analytics?tab=<tab>`.
*
* The existing selftest checks REGISTERED_ANALYTICS_TABS analytics.js
* dispatch arms, but does NOT enforce that ROUTES exercises every tab.
* That gap let 7+ tabs ship un-gated (issue #1706). This file closes it:
* any future `data-tab=` button added without a matching ROUTES entry
* fails CI on this assertion, blocking silent coverage drift.
*
* Runs in <100ms no browser, no playwright, pure source grep.
*/
'use strict';
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const mod = require('./test-a11y-axe-1668.js');
const analyticsSrc = fs.readFileSync(
path.join(__dirname, 'public', 'analytics.js'),
'utf8'
);
// Scrape every `<button class="...tab-btn..." data-tab="X">` from the
// analytics tab bar template literal.
const TAB_RE = /<button[^>]*\bclass="[^"]*\btab-btn\b[^"]*"[^>]*\bdata-tab="([^"]+)"/g;
const declaredTabs = new Set();
let m;
while ((m = TAB_RE.exec(analyticsSrc)) !== null) {
declaredTabs.add(m[1]);
}
assert.ok(
declaredTabs.size > 0,
'expected at least one data-tab="..." button in public/analytics.js — regex broken?'
);
// Build the set of analytics tabs ROUTES exercises.
const routeTabs = new Set();
for (const r of mod.ROUTES) {
const q = r.split('?')[1];
if (!q) continue;
const tm = q.match(/(?:^|&)tab=([^&]+)/);
if (tm) routeTabs.add(decodeURIComponent(tm[1]));
}
// Every declared tab MUST appear as a ROUTES entry.
const missing = [...declaredTabs].filter(t => !routeTabs.has(t)).sort();
assert.deepStrictEqual(
missing,
[],
`axe ROUTES missing analytics tabs (issue #1706): ${missing.join(', ')}\n` +
`declared in public/analytics.js: ${[...declaredTabs].sort().join(', ')}\n` +
`covered by ROUTES: ${[...routeTabs].sort().join(', ')}`
);
// And every analytics route MUST correspond to a real declared tab (no typos).
const stale = [...routeTabs].filter(t => !declaredTabs.has(t)).sort();
assert.deepStrictEqual(
stale,
[],
`axe ROUTES references analytics tabs not in public/analytics.js: ${stale.join(', ')}`
);
console.log(
`PASS: a11y-axe routes coverage — declared=${declaredTabs.size} ` +
`covered=${routeTabs.size} (all analytics tabs gated)`
);