Files
jobtrackingapp/job-tracker-ui/scripts/verify-a3-theme.mjs
T

75 lines
5.7 KiB
JavaScript

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