import { test, expect, Page } from '@playwright/test'; import AxeBuilder from '@axe-core/playwright'; import fs from 'node:fs'; import path from 'node:path'; import { policyAuthorSession } from '../../src/app/testing'; const shouldFail = process.env.FAIL_ON_A11Y === '1'; const reportDir = path.join(process.cwd(), 'test-results'); const mockConfig = { authority: { issuer: 'https://authority.local', clientId: 'stellaops-ui', authorizeEndpoint: 'https://authority.local/connect/authorize', tokenEndpoint: 'https://authority.local/connect/token', logoutEndpoint: 'https://authority.local/connect/logout', redirectUri: 'http://127.0.0.1:4400/auth/callback', postLogoutRedirectUri: 'http://127.0.0.1:4400/', scope: 'openid profile email ui.read authority:tenants.read advisory:read vex:read exceptions:read exceptions:approve aoc:verify findings:read orch:read vuln:view vuln:investigate vuln:operate vuln:audit', audience: 'https://scanner.local', dpopAlgorithms: ['ES256'], refreshLeewaySeconds: 60, }, apiBaseUrls: { authority: 'https://authority.local', scanner: 'https://scanner.local', policy: 'https://scanner.local', concelier: 'https://concelier.local', attestor: 'https://attestor.local', }, quickstartMode: true, }; async function writeReport(filename: string, data: unknown) { fs.mkdirSync(reportDir, { recursive: true }); fs.writeFileSync(path.join(reportDir, filename), JSON.stringify(data, null, 2)); } async function runA11y(url: string, page: Page) { await page.goto(url); const results = await new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa']).analyze(); const violations = [...results.violations].sort((a, b) => a.id.localeCompare(b.id)); await writeReport( `a11y-${url.replace(/\W+/g, '_') || 'home'}.json`, { url: page.url(), violations } ); if (shouldFail) { expect(violations).toEqual([]); } return violations; } test.describe('a11y-smoke', () => { test.beforeEach(async ({ page }) => { await page.addInitScript((session) => { try { window.sessionStorage.clear(); } catch { // ignore storage errors in restricted contexts } (window as any).__stellaopsTestSession = session; }, policyAuthorSession); await page.route('**/config.json', (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(mockConfig), }) ); await page.route('https://authority.local/**', (route) => route.abort()); }); test('home page baseline', async ({ page }, testInfo) => { const violations = await runA11y('/', page); testInfo.annotations.push({ type: 'a11y', description: `${violations.length} violations (set FAIL_ON_A11Y=1 to fail on any)`, }); }); test('graph explorer shell', async ({ page }, testInfo) => { const violations = await runA11y('/graph', page); testInfo.annotations.push({ type: 'a11y', description: `${violations.length} violations (/graph)`, }); }); test('triage VEX modal', async ({ page }, testInfo) => { await page.goto('/triage/artifacts/asset-web-prod'); await expect(page.getByRole('heading', { name: 'Artifact triage' })).toBeVisible({ timeout: 10000 }); await page.getByRole('button', { name: 'VEX' }).first().click(); await expect(page.getByRole('dialog', { name: 'VEX decision' })).toBeVisible({ timeout: 10000 }); const results = await new AxeBuilder({ page }) .withTags(['wcag2a', 'wcag2aa']) .include('.modal__container') .analyze(); const violations = [...results.violations].sort((a, b) => a.id.localeCompare(b.id)); await writeReport('a11y-triage_vex_modal.json', { url: page.url(), violations }); if (shouldFail) { expect(violations).toEqual([]); } testInfo.annotations.push({ type: 'a11y', description: `${violations.length} violations (/triage VEX modal)`, }); }); });