ktg-plugin-marketplace/plugins/voyage/tests/e2e/voyage-playground-a11y.spec.mjs

143 lines
6.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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