033c9ec315
- vitest: dictionary parity, slug-map bijection, content-schema validation, JSON-LD (23) - playwright: both locales, language-switch mapping (incl. no-JS), CV downloads, form happy-path + honeypot, axe a11y on 5 templates x 2 themes (23) - fix: raise ink-faint + light accent to meet WCAG AA 4.5:1 (axe-verified) - fix: gate reveal animations behind html.js so content is visible without JS (progressive enhancement - was hidden at opacity 0); scan reduced-motion path - fix: validate contact form before the anti-bot time-trap (no false success) - chore: ESLint node globals, typed diagram props, resvg fontFiles, prettier pass Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
42 lines
1.5 KiB
TypeScript
42 lines
1.5 KiB
TypeScript
import { test, expect } from '@playwright/test';
|
|
import AxeBuilder from '@axe-core/playwright';
|
|
|
|
/*
|
|
Accessibility gate (TECH_SPEC §9.4): zero critical/serious violations on the main
|
|
templates, both locales and both themes. wcag2a/2aa + best-practice rules.
|
|
*/
|
|
const pages = [
|
|
{ name: 'home EN', url: '/' },
|
|
{ name: 'home NO', url: '/no/' },
|
|
{ name: 'case study', url: '/projects/inboxintel/' },
|
|
{ name: 'contact', url: '/contact/' },
|
|
{ name: 'experience', url: '/experience/' },
|
|
];
|
|
|
|
for (const p of pages) {
|
|
for (const theme of ['dark', 'light'] as const) {
|
|
test(`${p.name} (${theme}) has no serious a11y violations`, async ({ page }) => {
|
|
// Scan the reduced-motion rendering: it's the accessible path and shows all
|
|
// reveal content immediately (entrance animations are otherwise mid-flight).
|
|
await page.emulateMedia({ colorScheme: theme, reducedMotion: 'reduce' });
|
|
await page.addInitScript((t) => {
|
|
try {
|
|
localStorage.setItem('theme', t);
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}, theme);
|
|
await page.goto(p.url);
|
|
const results = await new AxeBuilder({ page })
|
|
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
|
|
.analyze();
|
|
const serious = results.violations.filter(
|
|
(v) => v.impact === 'serious' || v.impact === 'critical',
|
|
);
|
|
expect(serious, JSON.stringify(serious.map((v) => ({ id: v.id, nodes: v.nodes.length })))).toEqual(
|
|
[],
|
|
);
|
|
});
|
|
}
|
|
}
|