tests/e2e/color-contrast.spec.ts sweeps 40 routes × 2 themes and asserts results.violations is empty. axe-core also returns a pass for elements whose ratio it could not compute, with the ratio set to null and the message:
Element has sufficient color contrast of null
Those land in passes, so the gate counts them as covered.
Measured
scripthammer-dark, color-contrast-enhanced only, dev server, 1280×1024 — the gate's own viewport:
| Route |
passes |
of which ratio: null |
violations |
/ |
118 |
47 |
0 |
/sign-in/ |
48 |
19 |
0 |
/privacy-controls/ |
43 |
11 |
0 |
/blog/ |
142 |
8 |
0 |
/docs/ |
74 |
6 |
0 |
/status/ |
69 |
6 |
0 |
/contact/ |
53 |
7 |
0 |
/accessibility/ |
61 |
16 |
0 |
| total |
608 |
120 (20%) |
0 |
One in five elements the gate reports as passing was never measured. The incomplete bucket — which the spec deliberately treats as "expected, not a failure" — is not where these go. They are counted as passes.
What it let through
.btn.btn-neutral.btn-outline measures 1.34:1 on scripthammer-dark, the site's default theme (apply-theme.ts:17). Eleven sites, including the "Continue with Google" and "Continue with GitHub" buttons on /sign-in and /sign-up. Filed separately.
axe's own verdict on those two nodes:
passes: [
{ html: '<a class="btn btn-neutral btn-outline" href="/ScriptHammer/p',
ratio: null, msg: 'Element has sufficient color contrast of null' },
...
]
Confirmed by canvas readback and by eye in a screenshot. The gate has been green on it the whole time.
Fix
Assert on the unmeasured count, not just the violation count:
const unmeasured = results.passes
.flatMap((v) => v.nodes)
.filter((n) => n.any?.[0]?.data?.contrastRatio == null);
expect(unmeasured, `${unmeasured.length} elements were PASSED without being measured`).toHaveLength(0);
Expect this to be red on first run — 120 nodes on 8 routes, so likely several hundred across all 40. Some will be legitimately unmeasurable (an element whose only content is a glyph, which axe already routes to incomplete for a different reason). Triage by category, then either narrow the selector with a stated reason or fix the underlying colour. The repo already has the technique that works where axe does not: canvas readback (lesson_oklch_contrast_canvas_playwright), because getComputedStyle returns oklch() unparsed.
Do not raise a threshold or filter the null cases away to get green. That reproduces the defect with extra steps.
Family
Same shape as #411 (gate swept 4 of 43 routes), #425 (route templates enumerated by nothing), #444 (a cancelled run indistinguishable from a passing one), #454 (a sweep that lists /admin but measures an empty page), #457 (touch-target tests dismiss the banner before measuring). A signal that reports success without observing its subject.
The distinguishing feature here is that the gate is not merely narrow — it converts "could not measure" into "passed", which is the one direction a probe must never round.
tests/e2e/color-contrast.spec.tssweeps 40 routes × 2 themes and assertsresults.violationsis empty. axe-core also returns a pass for elements whose ratio it could not compute, with the ratio set tonulland the message:Those land in
passes, so the gate counts them as covered.Measured
scripthammer-dark,color-contrast-enhancedonly, dev server, 1280×1024 — the gate's own viewport:ratio: null//sign-in//privacy-controls//blog//docs//status//contact//accessibility/One in five elements the gate reports as passing was never measured. The
incompletebucket — which the spec deliberately treats as "expected, not a failure" — is not where these go. They are counted as passes.What it let through
.btn.btn-neutral.btn-outlinemeasures 1.34:1 onscripthammer-dark, the site's default theme (apply-theme.ts:17). Eleven sites, including the "Continue with Google" and "Continue with GitHub" buttons on/sign-inand/sign-up. Filed separately.axe's own verdict on those two nodes:
Confirmed by canvas readback and by eye in a screenshot. The gate has been green on it the whole time.
Fix
Assert on the unmeasured count, not just the violation count:
Expect this to be red on first run — 120 nodes on 8 routes, so likely several hundred across all 40. Some will be legitimately unmeasurable (an element whose only content is a glyph, which axe already routes to
incompletefor a different reason). Triage by category, then either narrow the selector with a stated reason or fix the underlying colour. The repo already has the technique that works where axe does not: canvas readback (lesson_oklch_contrast_canvas_playwright), becausegetComputedStylereturnsoklch()unparsed.Do not raise a threshold or filter the null cases away to get green. That reproduces the defect with extra steps.
Family
Same shape as #411 (gate swept 4 of 43 routes), #425 (route templates enumerated by nothing), #444 (a cancelled run indistinguishable from a passing one), #454 (a sweep that lists
/adminbut measures an empty page), #457 (touch-target tests dismiss the banner before measuring). A signal that reports success without observing its subject.The distinguishing feature here is that the gate is not merely narrow — it converts "could not measure" into "passed", which is the one direction a probe must never round.