From 4bee1f2f7ef06f3a70425aaef19daa5b3603cc4a Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Sun, 10 May 2026 21:44:31 +0200 Subject: [PATCH] test(voyage): convert SC2 a11y spec to absolute 0-violation assertion (09132940) --- .../tests/e2e/snapshots/a11y-baseline.json | 9 -- .../tests/e2e/voyage-playground-a11y.spec.mjs | 135 +++--------------- 2 files changed, 23 insertions(+), 121 deletions(-) delete mode 100644 plugins/voyage/tests/e2e/snapshots/a11y-baseline.json diff --git a/plugins/voyage/tests/e2e/snapshots/a11y-baseline.json b/plugins/voyage/tests/e2e/snapshots/a11y-baseline.json deleted file mode 100644 index 8ac33cd..0000000 --- a/plugins/voyage/tests/e2e/snapshots/a11y-baseline.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "light": { - "aria-hidden-focus": 1, - "color-contrast": 4 - }, - "dark": { - "aria-hidden-focus": 1 - } -} diff --git a/plugins/voyage/tests/e2e/voyage-playground-a11y.spec.mjs b/plugins/voyage/tests/e2e/voyage-playground-a11y.spec.mjs index 8f5fade..b8f2bea 100644 --- a/plugins/voyage/tests/e2e/voyage-playground-a11y.spec.mjs +++ b/plugins/voyage/tests/e2e/voyage-playground-a11y.spec.mjs @@ -1,63 +1,24 @@ // tests/e2e/voyage-playground-a11y.spec.mjs -// v4.3 Step 30 — Group D e2e a11y + pixel-diff specs. +// v4.3 Group D e2e a11y + pixel-diff + SC24 XSS guard specs. // -// Three Playwright tests: -// 1. Light-theme axe-core scan — no NEW critical/serious violations vs baseline -// 2. Dark-theme axe-core scan — no NEW critical/serious violations vs baseline -// 3. Pixel-diff smoke (1280×900) against baseline-PNGs in +// Tests: +// 1. Light-theme axe-core scan — zero critical/serious violations (absolute) +// 2. Dark-theme axe-core scan — zero critical/serious violations (absolute) +// 3. SC1.6 inline gallery — data:image PNG rendered via scheduleRender hook +// 4. Pixel-diff smoke (1280×900) against baseline PNGs in // tests/e2e/snapshots/. Threshold maxDiffPixelRatio: 0.02. +// 5. SC24-security — script injection in artifact body does not execute // -// SC1 backup verification (autoritativ er manuell SC1-checklist Step 31). -// SC2 authoritative verification (axe-core). -// -// Baseline policy (v4.3 Sesjon 6 NEXT-SESSION-PROMPT recovery): the voyage -// playground HTML is FROZEN in Sesjon 6. Wave 7 = VERIFICATION ONLY, not -// FIX. Existing critical/serious violations are recorded as baseline -// (rule-id + node-count fingerprint) in tests/e2e/snapshots/a11y-baseline.json. -// The test fails only when: -// (a) A new violation rule-id appears, OR -// (b) A known rule's node-count increases. -// Actual a11y fix is deferred to v4.4 (HTML unfreeze). +// SC2 authoritative verification (axe-core). v4.3 Sesjon 17 (Wave 3 Step 5) +// converted the SC2 assertion from delta-baseline to absolute zero-violation +// after Wave 2 remediation (Step 4 color-contrast fix + Step 3 sidebar +// toggle restructure) reduced the critical/serious count to zero. import { test, expect } from '@playwright/test'; import AxeBuilder from '@axe-core/playwright'; -import { readFileSync, existsSync, writeFileSync } from 'node:fs'; -import { fileURLToPath } from 'node:url'; -import { dirname, join } from 'node:path'; - -const __dirname = dirname(fileURLToPath(import.meta.url)); -const BASELINE_PATH = join(__dirname, 'snapshots', 'a11y-baseline.json'); - -function fingerprint(violations) { - // Reduce violations to (id, node-count) tuples. Order-independent. - const map = {}; - for (const v of violations) { - map[v.id] = (map[v.id] || 0) + (Array.isArray(v.nodes) ? v.nodes.length : 0); - } - return map; -} - -function loadBaseline(theme) { - if (!existsSync(BASELINE_PATH)) return null; - const data = JSON.parse(readFileSync(BASELINE_PATH, 'utf-8')); - return data[theme] || null; -} - -function compareWithBaseline(currentFp, baselineFp) { - const newRules = []; - const grownRules = []; - for (const [id, count] of Object.entries(currentFp)) { - if (!(id in baselineFp)) { - newRules.push({ id, currentCount: count }); - } else if (count > baselineFp[id]) { - grownRules.push({ id, baselineCount: baselineFp[id], currentCount: count }); - } - } - return { newRules, grownRules }; -} test.describe('voyage-playground a11y (axe-core)', () => { - test('light theme — no NEW critical/serious violations vs baseline', async ({ page }) => { + test('light theme — zero critical/serious violations (absolute)', async ({ page }) => { await page.goto('voyage-playground.html'); await page.evaluate(() => { window.localStorage.setItem('voyage-theme', 'light'); @@ -69,27 +30,14 @@ test.describe('voyage-playground a11y (axe-core)', () => { const results = await new AxeBuilder({ page }) .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']) .analyze(); - const criticalOrSerious = results.violations.filter( - (v) => v.impact === 'critical' || v.impact === 'serious', - ); - const currentFp = fingerprint(criticalOrSerious); - const baseline = loadBaseline('light'); - if (baseline === null) { - // No baseline yet — write current as baseline (run once, then commit). - const baselineDoc = existsSync(BASELINE_PATH) - ? JSON.parse(readFileSync(BASELINE_PATH, 'utf-8')) - : {}; - baselineDoc.light = currentFp; - writeFileSync(BASELINE_PATH, JSON.stringify(baselineDoc, null, 2) + '\n'); - console.log('[a11y baseline] wrote light fingerprint:', currentFp); - return; // first run establishes baseline - } - const { newRules, grownRules } = compareWithBaseline(currentFp, baseline); - expect(newRules, `NEW rules vs baseline: ${JSON.stringify(newRules, null, 2)}`).toEqual([]); - expect(grownRules, `GROWN rule-counts vs baseline: ${JSON.stringify(grownRules, null, 2)}`).toEqual([]); + const violations = results.violations.filter(v => ['critical','serious'].includes(v.impact)); + expect( + violations, + JSON.stringify(violations.map(v => ({ id: v.id, impact: v.impact, nodes: v.nodes.length })), null, 2), + ).toEqual([]); }); - test('dark theme — no NEW critical/serious violations vs baseline', async ({ page }) => { + test('dark theme — zero critical/serious violations (absolute)', async ({ page }) => { await page.goto('voyage-playground.html'); await page.evaluate(() => { window.localStorage.setItem('voyage-theme', 'dark'); @@ -101,48 +49,11 @@ test.describe('voyage-playground a11y (axe-core)', () => { const results = await new AxeBuilder({ page }) .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']) .analyze(); - const criticalOrSerious = results.violations.filter( - (v) => v.impact === 'critical' || v.impact === 'serious', - ); - const currentFp = fingerprint(criticalOrSerious); - const baseline = loadBaseline('dark'); - if (baseline === null) { - const baselineDoc = existsSync(BASELINE_PATH) - ? JSON.parse(readFileSync(BASELINE_PATH, 'utf-8')) - : {}; - baselineDoc.dark = currentFp; - writeFileSync(BASELINE_PATH, JSON.stringify(baselineDoc, null, 2) + '\n'); - console.log('[a11y baseline] wrote dark fingerprint:', currentFp); - return; - } - const { newRules, grownRules } = compareWithBaseline(currentFp, baseline); - expect(newRules, `NEW rules vs baseline: ${JSON.stringify(newRules, null, 2)}`).toEqual([]); - expect(grownRules, `GROWN rule-counts vs baseline: ${JSON.stringify(grownRules, null, 2)}`).toEqual([]); - }); - - // v4.3 Step 4 — DIAGNOSTIC test (removed in Step 5/Wave 3). Prints the - // node selectors flagged by color-contrast so we can target scoped CSS - // overrides at exactly those nodes (finding 09132940). - // Asserts ZERO color-contrast violations after the inline-style override - // is applied — passes only when remediation is complete. - test('DIAGNOSTIC — print color-contrast node selectors (09132940)', async ({ page }) => { - await page.goto('voyage-playground.html'); - await page.evaluate(() => { - window.localStorage.setItem('voyage-theme', 'light'); - document.documentElement.setAttribute('data-theme', 'light'); - document.documentElement.style.colorScheme = 'light'; - }); - await page.reload(); - await page.waitForLoadState('domcontentloaded'); - const results = await new AxeBuilder({ page }) - .options({ runOnly: ['color-contrast'] }) - .analyze(); - const nodes = results.violations.flatMap((v) => - v.nodes.map((n) => ({ rule: v.id, target: n.target, html: (n.html || '').slice(0, 80) })), - ); - // Diagnostic emission — visible via --reporter=line - console.log('[DIAGNOSTIC color-contrast nodes]', JSON.stringify(nodes)); - expect(nodes, `color-contrast violations remain: ${JSON.stringify(nodes, null, 2)}`).toEqual([]); + const violations = results.violations.filter(v => ['critical','serious'].includes(v.impact)); + expect( + violations, + JSON.stringify(violations.map(v => ({ id: v.id, impact: v.impact, nodes: v.nodes.length })), null, 2), + ).toEqual([]); }); // v4.3 Step 8 — inline screenshot gallery (finding 31d28f65).