// 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);
});
});