// tests/e2e/voyage-playground-a11y.spec.mjs // v4.3 Group D e2e a11y + pixel-diff + SC24 XSS guard specs. // // 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 // // 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'; test.describe('voyage-playground a11y (axe-core)', () => { 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'); document.documentElement.setAttribute('data-theme', 'light'); document.documentElement.style.colorScheme = 'light'; }); await page.reload(); await page.waitForLoadState('domcontentloaded'); const results = await new AxeBuilder({ page }) .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']) .analyze(); 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 — zero critical/serious violations (absolute)', async ({ page }) => { await page.goto('voyage-playground.html'); await page.evaluate(() => { window.localStorage.setItem('voyage-theme', 'dark'); document.documentElement.setAttribute('data-theme', 'dark'); document.documentElement.style.colorScheme = 'dark'; }); await page.reload(); await page.waitForLoadState('domcontentloaded'); const results = await new AxeBuilder({ page }) .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']) .analyze(); 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). // Injects a pre-built artifacts object with screenshots[] via the // window.__voyage.scheduleRender hook (avoids webkitdirectory which // is not programmatically triggerable). Asserts the dashboard renders // at least one data:image PNG tag. test('SC1.6 inline gallery — data:image PNGs rendered (31d28f65)', async ({ page }) => { await page.goto('voyage-playground.html'); await page.waitForLoadState('domcontentloaded'); // 1×1 transparent PNG (same base64 as the fixture file) const SAMPLE_DATA_URL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg=='; await page.evaluate((dataUrl) => { window.__voyage.scheduleRender({ artifacts: { basePath: 'fixture-project', storageKey: 'voyage_proj_fixture', brief: { path: 'brief.md', content: '# Fixture', frontmatter: {} }, plan: null, review: null, progress: null, research: [], architecture: { overview: null, gaps: null, looseFiles: [] }, screenshots: [{ path: 'docs/screenshots/dashboard/sample.png', dataUrl: dataUrl }], looseFiles: [], }, }); }, SAMPLE_DATA_URL); // The gallery is rendered inside #voyage-dashboard const imgCount = await page.locator('#voyage-dashboard img[src^="data:image/png"]').count(); expect(imgCount, 'expected at least one data:image/png in the gallery').toBeGreaterThan(0); }); test('pixel-diff smoke 1280×900 — light + dark within 2% threshold (SC1 backup)', async ({ page }) => { await page.setViewportSize({ width: 1280, height: 900 }); // Light theme baseline 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'); await expect(page).toHaveScreenshot('voyage-playground-light.png', { maxDiffPixelRatio: 0.02, fullPage: false, }); // Dark theme baseline await page.evaluate(() => { window.localStorage.setItem('voyage-theme', 'dark'); document.documentElement.setAttribute('data-theme', 'dark'); document.documentElement.style.colorScheme = 'dark'; }); await page.reload(); await page.waitForLoadState('domcontentloaded'); await expect(page).toHaveScreenshot('voyage-playground-dark.png', { maxDiffPixelRatio: 0.02, fullPage: false, }); }); // v4.3 Step 2 — Group D Playwright XSS injection runtime guard // (finding 1d3591d4). Behavioral counterpart to the DOMPurify fix in // renderArtifact (Step 1). Injects a markdown // payload via scheduleRender and verifies neither a JS dialog fires nor // a \n# title', }); }); expect(dialogCount, `expected zero dialogs but got ${dialogCount}`).toBe(0); expect(await page.locator('#voyage-viewport script').count()).toBe(0); }); });