From d7c56c160f80896e5dcc5c662642bbd95d72c529 Mon Sep 17 00:00:00 2001 From: Mao Nakamoto <41178744+maonakamoto@users.noreply.github.com> Date: Thu, 20 Aug 2026 15:29:11 +0200 Subject: [PATCH] fix(ci): the UI audit measured labels where the page paints boxes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three false-positive classes from the live fleet sweep, each pinned by fixtures on both sides (12/12 self-tests): - A bare wrapper anchor around a styled button measured the wrapper's never-painted ink — surf-your-life's 'Konto erstellen' read 1:1 against the button fill it never renders on. Wrappers whose label a nested action paints are skipped; the nested control is scanned in its own right. - Disabled controls are dim BECAUSE they are disabled; WCAG exempts inactive controls, so they no longer bury real findings (their enabled twins still report). - A row of padded buttons measured its glyph left, not its box edge: aoz-wohnen's flush hero CTA row read as a 16px ragged column. Rows whose leading element paints its own box are measured at the border edge. En route this exposed that the nested-group exemption compared line tops EXACTLY — two buttons one row apart by a 1px border read as a two-line group and were exempted wholesale; tops now compare with an 8px tolerance, and a genuinely offset button row is a fixture that must keep firing. Co-Authored-By: Claude Fable 5 --- scripts/ci/test-ui-defect-audit.mjs | 65 +++++++++++++++++++++++++++++ scripts/ci/ui-defect-audit.mjs | 42 +++++++++++++++---- 2 files changed, 100 insertions(+), 7 deletions(-) diff --git a/scripts/ci/test-ui-defect-audit.mjs b/scripts/ci/test-ui-defect-audit.mjs index 9019212..723fd64 100644 --- a/scripts/ci/test-ui-defect-audit.mjs +++ b/scripts/ci/test-ui-defect-audit.mjs @@ -95,6 +95,47 @@ const FIXTURES = {

Every feature — health tracking, digital twin, vet network, marketplace, and adoption listings — is included free with no pet limits.

`, + + // A bare wrapper anchor around a styled button. The anchor's own ink is + // never painted — the button paints the label — so measuring the anchor read + // 1:1 on surf-your-life's "Konto erstellen". The button itself passes. + wrapperAnchor: ` +
+ +
`, + + // A disabled control is dim BECAUSE it is disabled — WCAG exempts it. Its + // enabled twin with the same colors is a real finding. + disabledControl: ` +
+ + +
`, + + // A row of padded buttons in a text column: the BUTTON BOX is flush with the + // column and only the labels sit padding deeper — aoz-wohnen's hero read as + // ragged for exactly this. + buttonRow: ` +
+
+

Gemeinsam wohnen

+

Die Wohnung, auf die ihr euch einigen könnt.

+
AusprobierenAnmelden
+

Kein Konto nötig.

+
+
`, + + // The same hero with the button row genuinely off the column: the BOX edge + // returns 16px out and back, and the box edge is what must be measured. + buttonRowRagged: ` +
+
+

Gemeinsam wohnen

+

Die Wohnung, auf die ihr euch einigen könnt.

+
AusprobierenAnmelden
+

Kein Konto nötig.

+
+
`, }; function assert(cond, msg) { @@ -177,6 +218,30 @@ async function main() { ); }); + await check("ignores a wrapper anchor whose label a nested button paints", async () => { + const r = await measure(FIXTURES.wrapperAnchor); + assert(!r.contrast.some((c) => c.tag === "a"), `the wrapper must be skipped, got ${JSON.stringify(r.contrast)}`); + const btn = r.contrast.find((c) => c.tag === "button"); + assert(btn && btn.value >= btn.floor, `the painted button passes on its own fill, got ${btn && btn.value}`); + }); + + await check("exempts a disabled control but reports its enabled twin", async () => { + const r = await measure(FIXTURES.disabledControl); + assert(!r.contrast.some((c) => c.text === "Send"), "the disabled button is exempt (WCAG inactive)"); + const twin = r.contrast.find((c) => c.text === "Send twin"); + assert(twin && twin.value < twin.floor, `the enabled twin is a real finding, got ${twin && twin.value}`); + }); + + await check("measures a padded button row by its box edge, not its label", async () => { + const r = await measure(FIXTURES.buttonRow); + assert(r.ragged.length === 0, `a flush button row is aligned, got ${JSON.stringify(r.ragged)}`); + }); + + await check("still catches a button row whose box is off the column", async () => { + const r = await measure(FIXTURES.buttonRowRagged); + assert(r.ragged.length >= 1, "a 16px box offset that returns must be reported"); + }); + await check("does NOT flag centered copy as a missing hanging indent", async () => { const r = await measure(FIXTURES.centeredCopy); assert( diff --git a/scripts/ci/ui-defect-audit.mjs b/scripts/ci/ui-defect-audit.mjs index d328e94..8d9befe 100755 --- a/scripts/ci/ui-defect-audit.mjs +++ b/scripts/ci/ui-defect-audit.mjs @@ -160,6 +160,15 @@ export const MEASURE = String.raw`(() => { var el = actions[i]; var text = (el.innerText || "").trim(); if (!text) continue; + // A wrapper whose visible label is painted by a nested action measures the + // wrapper's own never-painted ink: surf-your-life's bare around a + // styled