// Built React with synthetic NAV-shaped fixtures; no provider or backend access. import { chromium, expect } 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/discovery/browser'); fs.mkdirSync(out, { recursive: true }); const report = { screens: [], journeys: [], errors: [], unexpectedWrites: [], mockedWrites: [], blockedExternal: [] }; const browser = await chromium.launch(); 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 => report.errors.push(error.message)); await page.addInitScript(({ language, mode }) => { localStorage.setItem('uiLanguage', language); localStorage.setItem('jobtracker.themeMode', mode); }, { language, mode }); const jobs = ['Community nurse', 'Primary teacher', 'Electrical engineer'].map((title, index) => ({ id: `vacancy-${index}`, title, company: 'Synthetic employer', location: 'Oslo', modifiedAt: '2026-11-02T10:00:00Z', deadline: '2026-11-12T12:00:00Z', url: `https://example.test/vacancy-${index}`, source: 'nav', sourceName: 'NAV Arbeidsplassen', acquisitionType: 'searched', retrievedAt: '2026-11-03T10:00:00Z', countryCode: 'NO' })); let saved = null, searches = 0; await page.route('**/*', route => { const req = route.request(), url = new URL(req.url()); if (!['localhost', '127.0.0.1'].includes(url.hostname)) { report.blockedExternal.push(url.origin); return route.abort(); } if (!url.pathname.startsWith('/api/')) return route.continue(); const answer = data => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(data) }); const note = action => report.mockedWrites.push({ width, language, mode, action }); if (req.method() === 'POST' && url.pathname === '/api/job-discovery/saved-searches') { const data = req.postDataJSON(); assert.deepEqual(data, { name: 'Care roles', query: 'nurse', location: 'Oslo' }); saved = { ...data, id: 7, isActive: true, resultCount: 0, dismissedCount: 0 }; note('create'); return answer(saved); } if (req.method() === 'PATCH' && url.pathname === '/api/job-discovery/saved-searches/7') { assert.deepEqual(req.postDataJSON(), { isActive: false }); saved.isActive = false; note('pauseAlerts'); return answer(saved); } if (req.method() === 'POST' && url.pathname === '/api/job-discovery/saved-searches/7/run') { saved.resultCount = 1; note('manualRun'); return answer({ search: saved, jobs: [{ job: jobs[0], isNew: true, isDismissed: false }] }); } if (req.method() === 'PATCH' && url.pathname === '/api/job-discovery/saved-searches/7/results/vacancy-0/dismiss') { assert.deepEqual(req.postDataJSON(), { isDismissed: true }); note('dismiss'); return answer({}); } if (req.method() === 'DELETE' && url.pathname === '/api/job-discovery/saved-searches/7') { saved = null; note('confirmedDelete'); return answer({}); } if (req.method() !== 'GET') { report.unexpectedWrites.push(url.pathname); return route.abort(); } if (url.pathname === '/api/job-discovery/saved-searches') return answer(saved ? [saved] : []); if (url.pathname === '/api/job-discovery/search') { searches++; return answer(jobs.filter(job => !url.searchParams.get('q') || job.title.toLowerCase().includes(url.searchParams.get('q').toLowerCase()))); } const fixtures = { '/api/auth/config': { requireAuth: true }, '/api/auth/me': { id: 'a3-discovery-owner', sessionContext: 'a3-discovery-session', firstName: 'Alex', displayName: 'Alex Sample', roles: [], uiLanguage: language === 'nb' ? 'nb-NO' : 'en' }, '/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] ?? {}) }); }); const scan = async state => { assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth + 1), false); const result = await new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa', 'wcag21aa', 'wcag22aa']).analyze(); report.screens.push({ width, language, mode, state, violations: result.violations }); assert.deepEqual(result.violations, []); await page.evaluate(() => window.scrollTo(0, 0)); await page.screenshot({ path: path.join(out, `${state}-${language}-${mode}-${width}.png`), fullPage: true }); }; await page.goto('http://localhost:4338/discover'); const filters = page.getByRole('complementary', { name: language === 'en' ? 'Search filters' : 'Søkefiltre' }); await filters.waitFor(); const results = page.getByRole('region', { name: language === 'en' ? 'Search results' : 'Søkeresultater', exact: true }); await results.waitFor(); const role = page.getByRole('textbox', { name: language === 'en' ? 'Role or company' : 'Rolle eller bedrift', exact: true }); await filters.getByRole('button', { name: language === 'en' ? 'Jump to results' : 'Gå til resultater', exact: true }).click(); assert.equal(await results.evaluate(node => node === document.activeElement), true); await results.getByRole('button', { name: language === 'en' ? 'Return to search filters' : 'Tilbake til søkefiltre', exact: true }).click(); assert.equal(await role.evaluate(node => node === document.activeElement), true); assert.equal(searches, 0); const left = await filters.boundingBox(), right = await results.boundingBox(); assert.ok(width > 1000 ? right.x > left.x + left.width - 2 && Math.abs(right.y - left.y) < 2 : right.y >= left.y + left.height - 2); await scan('initial'); const search = page.getByRole('button', { name: language === 'en' ? 'Search NAV' : 'Søk i NAV', exact: true }); await search.click(); await results.getByRole('article', { name: 'Electrical engineer', exact: true }).waitFor(); assert.equal(await results.getByRole('article').count(), 3); await scan('results'); await role.fill('nurse'); await page.getByRole('textbox', { name: language === 'en' ? 'Municipality' : 'Kommune', exact: true }).fill('Oslo'); await page.getByRole('textbox', { name: language === 'en' ? 'Search name' : 'Navn på søket', exact: true }).fill('Care roles'); await page.getByRole('button', { name: language === 'en' ? 'Save search' : 'Lagre søk', exact: true }).click(); const group = page.getByRole('group', { name: 'Care roles', exact: true }); await group.waitFor(); const alerts = group.getByRole('switch', { name: language === 'en' ? 'Automatic alerts' : 'Automatiske varsler', exact: true }); await alerts.click(); await expect(alerts).not.toBeChecked(); await group.getByRole('button', { name: language === 'en' ? 'Check for new jobs' : 'Se etter nye stillinger', exact: true }).click(); await results.getByRole('article', { name: 'Community nurse', exact: true }).waitFor(); assert.equal(await results.getByRole('article').count(), 1); await scan('saved'); assert.equal(await results.getByRole('link', { name: language === 'en' ? 'View listing' : 'Vis stilling', exact: true }).getAttribute('href'), jobs[0].url); await results.getByRole('button', { name: language === 'en' ? 'Dismiss' : 'Avvis', exact: true }).click(); await results.getByRole('article').waitFor({ state: 'hidden' }); await group.getByRole('button', { name: language === 'en' ? 'Delete' : 'Slett', exact: true }).click(); const dialog = page.getByRole('dialog', { name: language === 'en' ? 'Delete saved search?' : 'Slette lagret søk?', exact: true }); await dialog.getByRole('button', { name: language === 'en' ? 'Delete' : 'Slett', exact: true }).click(); await group.waitFor({ state: 'hidden' }); report.journeys.push({ width, language, mode, searchSavePauseRunDismissConfirmedDelete: 'PASS' }); await context.close(); } assert.equal(report.screens.length, 48); assert.equal(report.journeys.length, 16); assert.equal(report.mockedWrites.length, 80); assert.deepEqual(report.errors, []); assert.deepEqual(report.unexpectedWrites, []); assert.ok(report.blockedExternal.every(origin => origin === 'https://fonts.googleapis.com')); console.log('PASS:16 controlled discovery journeys,48 axe scans/captures,80 intercepted fixture mutations.'); } finally { fs.writeFileSync(path.join(out, 'BROWSER.json'), JSON.stringify({ ...report, limits: ['synthetic provider/API fixtures, not live NAV or backend qualification', 'no new providers/geography or native/security/release acceptance'] }, null, 2)); await browser.close(); }