feat(#1668): axe-core CI gate for WCAG AA color-contrast (M5) (#1696)

Partial fix for #1668 (M5 of 6).

After M1 (audit), M2 (color tokens, #1676), M3 (typography floor,
#1679), and M4 (per-route polish, #1681) cleared ~95% of
contrast/typography violations, M5 **locks in the wins** by adding an
axe-core CI gate that fails the build on any new WCAG AA color-contrast
regression.

## What's in the box

- `test-a11y-axe-1668.js` — Playwright + `@axe-core/playwright`. Runs
every major CoreScope route × `{dark, light}` at 1200×900 desktop,
injects axe, runs only the `color-contrast` rule, asserts net violations
=== 0.
- `test-a11y-axe-1668-selftest.js` — fast, deterministic, browser-free
unit test that exercises the YAML allowlist parser, the
`violationAllowed` matcher, and the route/theme metadata. Runs in the JS
unit block (no browser needed).
- `tests/a11y-allowlist.yaml` — operator-flagged false-positive
allowlist. **0 entries at M5 baseline.**

## Allowlist format

Each entry MUST cite a GH issue # and an `expires_at` date. Missing
fields = refused. Expired `expires_at` = refused (warning logged). This
**forces a periodic revisit** — no permanent suppressions.

```yaml
- route: /analytics?tab=channels
  selector: ".some-known-stale-element"
  rule: color-contrast
  issue: 1234
  expires_at: 2026-09-01
```

## Routes covered (19 × 2 themes = 38 cells)

`/`, `/packets`, `/nodes`, `/channels`, `/live`, `/map`, `/observers`,
`/compare`,
`/analytics?tab={overview,rf,topology,channels,hashsizes,collisions,roles,airtime}`,
`/audio-lab`, `/customize`, `/replay`.

## TDD red→green

- **RED** (`08adafdb`) — adds the gate + deliberately regresses
`--text-muted` from `palette-gray-700` (~10:1) to `#9ca3af` (~2.4:1).
axe-core fails on every light-theme cell.
- **GREEN** (`f62fb1e0`) — restores the M2 token. Net violations = 0
across all 38 cells.

## Scope discipline

- Only `color-contrast` (matches M2/M3/M4 scope). M6 owns `image-alt`,
`aria-required-attr`, `label`, mobile viewports, and letsmesh A/B.
- No new design tokens.
- M2-M4 tokens untouched.

## CI wiring

- `.github/workflows/deploy.yml:155` — selftest in JS unit block.
- `.github/workflows/deploy.yml:367` — real axe browser run in the
Playwright E2E block after the fixture server is up.

## Deps

`@axe-core/playwright@4.11.3` + `axe-core@4.12.1` added to
`devDependencies`. Pinned versions.

---------

Co-authored-by: openclaw-bot <bot@openclaw.local>
Co-authored-by: clawbot <clawbot@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-06-12 20:00:35 -07:00
1 parent e96f0f9f9f
commit d954ea7444
20 files changed
+870 -125

No files matched your search

+6
View File
@@ -151,6 +151,7 @@ jobs:
node test-warmup-banner.js
node test-issue-1633-hide-1byte-hops.js
node test-issue-1668-m4-per-route.js
node test-a11y-axe-1668-selftest.js
- name: 🛡️ Preflight XSS gate — actual --diff check (PR only)
# The fixture self-test above (test-preflight-xss-gate.js) only
@@ -362,6 +363,11 @@ jobs:
- name: Run Playwright E2E tests (fail-fast)
run: |
BASE_URL=http://localhost:13581 node test-e2e-playwright.js 2>&1 | tee e2e-output.txt
# M5 of #1668 — axe-core CI gate (color-contrast AA).
# Real browser run; fails on any net violation (raw − allowlist).
# Allowlist: tests/a11y-allowlist.yaml (0 entries at M5 baseline).
BASE_URL=http://localhost:13581 AXE_SCREENSHOT_DIR=/tmp/axe-1668 \
node test-a11y-axe-1668.js 2>&1 | tee -a e2e-output.txt
BASE_URL=http://localhost:13581 node test-filter-ux-e2e.js 2>&1 | tee -a e2e-output.txt
BASE_URL=http://localhost:13581 node test-channel-issue-1087-e2e.js 2>&1 | tee -a e2e-output.txt
BASE_URL=http://localhost:13581 node test-channel-issue-1111-e2e.js 2>&1 | tee -a e2e-output.txt