143 lines
6.3 KiB
JavaScript
143 lines
6.3 KiB
JavaScript
// 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 <img> 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 <img> 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 <script>alert(1)</script> markdown
|
||
// payload via scheduleRender and verifies neither a JS dialog fires nor
|
||
// a <script> tag survives in #voyage-viewport. Defense in depth alongside
|
||
// the Group A static-grep guard.
|
||
test('SC24-security — script injection in artifact body does not execute (1d3591d4)', async ({ page }) => {
|
||
let dialogCount = 0;
|
||
page.on('dialog', (d) => {
|
||
dialogCount++;
|
||
d.dismiss();
|
||
});
|
||
await page.goto('voyage-playground.html');
|
||
await page.waitForLoadState('domcontentloaded');
|
||
await page.evaluate(() => {
|
||
window.__voyage.scheduleRender({
|
||
markdown: '<script>alert(1)</script>\n# title',
|
||
});
|
||
});
|
||
expect(dialogCount, `expected zero dialogs but got ${dialogCount}`).toBe(0);
|
||
expect(await page.locator('#voyage-viewport script').count()).toBe(0);
|
||
});
|
||
});
|