// Real built React application; controlled API responses only, never a live service. import { chromium } from '@playwright/test'; import AxeBuilder from '@axe-core/playwright'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import path from 'node:path'; const out = path.resolve(import.meta.dirname, '../../docs/ui-modernisation/evidence'); (async () => { const browser = await chromium.launch(); const rows = [], errors = [], writes = [], external = []; try { for (const width of [320, 390, 768, 1440]) for (const language of ['en', 'nb']) for (const mode of ['light', 'dark']) { const context = await browser.newContext({ viewport: { width, height: 1000 }, reducedMotion: 'reduce' }); const page = await context.newPage(); page.on('pageerror', error => errors.push(error.message)); await page.addInitScript(({ language, mode }) => { localStorage.setItem('uiLanguage', language); localStorage.setItem('jobtracker.themeMode', mode); }, { language, mode }); await page.route('**/*', route => { const request = route.request(), url = new URL(request.url()); if (!['localhost', '127.0.0.1'].includes(url.hostname)) { external.push(url.origin); return route.abort(); } if (!url.pathname.startsWith('/api/')) return route.continue(); if (request.method() !== 'GET') { writes.push({ method: request.method(), path: url.pathname }); return route.abort(); } const fixtures = { '/api/auth/config': { requireAuth: true, allowRegistration: true, googleEnabled: false, microsoftEnabled: false, turnstileEnabled: false }, '/api/auth/me': { id: 'a3-synthetic-owner', sessionContext: 'a3-synthetic-context', email: 'alex@example.test', roles: [], uiLanguage: language === 'nb' ? 'nb-NO' : 'en' }, '/api/companies': [{ id: 7, name: 'Nordlys synthetic company', location: 'Tromsø', source: 'Manual' }], '/api/jobapplications/reminders': [], '/api/notifications/unread-count': { count: 0 }, }; return route.fulfill({ status: Object.hasOwn(fixtures, url.pathname) ? 200 : 404, contentType: 'application/json', body: JSON.stringify(fixtures[url.pathname] ?? { message: 'Not part of this controlled theme check' }) }); }); for (const screen of ['login', 'companies']) { await page.goto(`http://localhost:4338/${screen}`); await page.locator('main h1').waitFor(); assert.equal(await page.locator('html').getAttribute('data-color-scheme'), mode); assert.equal(await page.locator('html').getAttribute('lang'), language === 'nb' ? 'nb-NO' : 'en'); if (screen === 'companies') await page.getByText('Nordlys synthetic company', { exact: true }).waitFor(); const geometry = await page.evaluate(() => ({ overflow: document.documentElement.scrollWidth > innerWidth + 1, background: getComputedStyle(document.body).backgroundColor })); assert.equal(geometry.overflow, false); assert.equal(geometry.background, mode === 'light' ? 'rgb(255, 255, 255)' : 'rgb(40, 35, 52)'); const scan = await new AxeBuilder({ page }).include('main').withTags(['wcag2a', 'wcag2aa', 'wcag21aa', 'wcag22aa']).analyze(); rows.push({ screen, width, language, mode, geometry, violations: scan.violations }); await page.screenshot({ path: path.join(out, `${screen}-${language}-${mode}-${width}.png`), fullPage: true }); assert.deepEqual(scan.violations, [], `${screen}/${mode}/${language}/${width} scoped accessibility`); if (screen === 'login') { await page.locator('button[type=submit]').click(); assert.ok(await page.locator('input[aria-invalid=true]').count() >= 1); assert.equal(await page.locator('input').first().evaluate(node => node === document.activeElement), true); assert.equal(await page.locator('input').first().evaluate(node => getComputedStyle(node.closest('.MuiOutlinedInput-root').querySelector('fieldset')).borderTopWidth), '3px'); } else { const edit = page.locator('[data-company-edit="7"]'); await edit.focus(); await page.keyboard.press('Tab'); await page.keyboard.press('Shift+Tab'); assert.equal(await edit.evaluate(node => node === document.activeElement), true); assert.equal(await edit.evaluate(node => getComputedStyle(node).outlineWidth), '3px'); await edit.press('Enter'); await page.getByRole('dialog').waitFor(); const dialog = await new AxeBuilder({ page }).include('[role=dialog]').withTags(['wcag2a', 'wcag2aa', 'wcag21aa', 'wcag22aa']).analyze(); rows.push({ screen: 'company-dialog', width, language, mode, violations: dialog.violations }); assert.deepEqual(dialog.violations, []); await page.keyboard.press('Escape'); await page.getByRole('dialog').waitFor({ state: 'hidden' }); assert.equal(await edit.evaluate(node => node === document.activeElement), true); } } await context.close(); } assert.deepEqual(errors, []); assert.deepEqual(writes, []); // Existing font stylesheet requests are blocked, not fetched or needed by A3's system stack. assert.ok(external.every(origin => origin === 'https://fonts.googleapis.com'), 'unexpected external request'); console.log('PASS: 32 actual-application screen conditions, 16 company-dialog keyboard journeys; controlled API only.'); } finally { fs.writeFileSync(path.join(out, 'BROWSER.json'), JSON.stringify({ rows, errors, writes, blockedExternalRequests: external, scope: 'actual static-export React application with controlled API; existing remote font requests aborted; no backend or provider qualification' }, null, 2)); await browser.close(); } })().catch(error => { console.error(error); process.exitCode = 1; });