// Exported React UI with synthetic API responses; no backend, model or provider access. import { chromium, expect } from '@playwright/test'; import AxeBuilder from '@axe-core/playwright'; import ts from 'typescript'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import path from 'node:path'; const compiled = ts.transpileModule(fs.readFileSync(new URL('../src/i18n/translations.ts', import.meta.url), 'utf8'), { compilerOptions: { module: ts.ModuleKind.ESNext } }).outputText; const { translations } = await import(`data:text/javascript;base64,${Buffer.from(compiled).toString('base64')}`); const reviewOnly = Boolean(process.env.A3_AI_REVIEW_ONLY); const visualMatrix = Boolean(process.env.A3_VISUAL_MATRIX); const settingsOnly = Boolean(process.env.A3_SETTINGS_FLAT); assert.ok(!settingsOnly || !(visualMatrix || reviewOnly), 'Settings read-only qualification is a separate scope.'); assert.ok(!visualMatrix || reviewOnly, 'Visual matrix reuses read/review-only journeys.'); const out = path.resolve(import.meta.dirname, settingsOnly ? '../../docs/ui-modernisation/visual-correction/evidence/settings-flat' : visualMatrix ? '../../docs/ui-modernisation/visual-correction/evidence/cv' : reviewOnly ? '../../docs/ui-modernisation/evidence/cross-app/browser-ai-review' : '../../docs/ui-modernisation/evidence/ai-settings/browser'); fs.mkdirSync(out, { recursive: true }); const report = { screens: [], journeys: [], errors: [], unexpectedWrites: [], writes: [], proposals: [], unavailableReads: [], blockedExternal: [] }; const browser = await chromium.launch(); try { const configurations = process.env.A3_PILOT ? (settingsOnly ? [[320, 'en', 'light'], [1440, 'en', 'light']] : [[320, 'en', 'light']]) : (visualMatrix || settingsOnly ? [320, 360, 390, 430, 768, 1024, 1280, 1440, 1920] : [320, 390, 768, 1440]).flatMap(width => ['en', 'nb'].flatMap(language => ['light', 'dark'].map(mode => [width, language, mode]))); for (const [width, language, mode] of configurations) { console.log(`Checking ${width}/${language}/${mode}`); const t = key => { assert.ok(translations[language][key], key); return translations[language][key]; }; 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)); const studioSection = async key => { await expect(page.getByRole('region', { name: t('cvEditorEditingRegion'), exact: true })).toBeVisible(); const tab = page.getByRole('tab', { name: t(key), exact: true }); if (await tab.isVisible()) await tab.click(); else { await page.getByRole('combobox', { name: new RegExp(`^${t('cvEditorStudioNavigation')}`) }).click(); await page.getByRole('option', { name: t(key), exact: true }).click(); } }; await page.addInitScript(({ language, mode }) => { localStorage.setItem('uiLanguage', language); localStorage.setItem('jobtracker.themeMode', mode); }, { language, mode }); let notifications = { emailFollowUpRemindersEnabled: true }; let privacy = { enabled: true, externalProcessingAllowed: false, externalProcessingAvailable: false, effectiveExternalProcessing: false, provider: 'local' }; let variant = { id: 3, name: 'Teacher CV', version: 4, settings: { language: 'en', themeId: 'modern', showPhoto: false, showPageNumbers: false, showIcons: true, sections: [], overrides: {}, customSections: [], ...(reviewOnly ? { headline: 'Manual teacher headline' } : {}) }, isPublic: false, publicSlug: 'synthetic', jobApplicationId: null, updatedAtUtc: '2026-11-03T10:00:00Z' }; let proposal; const writes = [], proposals = []; await page.route('**/*', route => { const req = route.request(), url = new URL(req.url()), p = url.pathname; if (settingsOnly && url.origin !== 'http://localhost:4338' && !(url.origin === 'http://localhost:5202' && p.startsWith('/api/'))) { report.blockedExternal.push(url.origin); return route.abort(); } if (!['localhost', '127.0.0.1'].includes(url.hostname)) { report.blockedExternal.push(url.origin); return route.abort(); } if (!p.startsWith('/api/')) return route.continue(); const answer = data => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(data) }); const note = action => { writes.push(action); report.writes.push({ width, language, mode, action }); }; if (settingsOnly && req.method() !== 'GET') { report.unexpectedWrites.push(p); return route.abort(); } if (req.method() === 'PUT' && p === '/api/notification-settings') { assert.deepEqual(req.postDataJSON(), { emailFollowUpRemindersEnabled: false }); notifications = req.postDataJSON(); note('notificationPreference'); return answer(notifications); } if (req.method() === 'PUT' && p === '/api/ai/settings') { const body = req.postDataJSON(); assert.equal(body.externalProcessingAllowed, false); privacy = { ...privacy, ...body }; note('aiPreference'); return answer(privacy); } if (req.method() === 'PUT' && p === '/api/cv/variants/3') { const body = req.postDataJSON(); assert.equal(body.expectedVersion, variant.version); assert.equal(body.settings.headline, 'Manual teacher headline'); variant = { ...variant, settings: body.settings, name: body.name, version: variant.version + 1 }; note('manualCvSave'); return answer(variant); } if (req.method() === 'POST' && p === '/api/cv/preview') return answer({ themeId: 'modern', html: 'Controlled preview

Alex Sample

Recorded teacher profile

' }); if (req.method() === 'POST' && p === '/api/cv/ai/assist') { const body = req.postDataJSON(); assert.equal(privacy.enabled, true); assert.equal(body.expectedVersion, variant.version); assert.equal(body.action, 'document-translate'); assert.equal(body.variantId, 3); assert.equal(Object.hasOwn(body, 'text'), false); assert.equal(body.language, proposals.length ? 'nb-NO' : 'en'); proposal = { version: variant.version, language: body.language, token: `synthetic-${proposals.length}` }; proposals.push(body.language); report.proposals.push({ width, language, mode, outputLanguage: body.language }); return answer({ original: JSON.stringify([{ id: 'headline', items: [variant.settings.headline] }]), result: JSON.stringify([{ id: 'headline', items: ['Reviewed teacher headline'] }]), proposalToken: proposal.token }); } if (req.method() === 'POST' && p === '/api/cv/variants/3/ai/apply') { assert.deepEqual(req.postDataJSON(), { proposalToken: proposal.token }); assert.equal(proposal.version, variant.version); variant = { ...variant, version: variant.version + 1, settings: { ...variant.settings, headline: 'Reviewed teacher headline', language: proposal.language } }; note('explicitAiApply'); return answer(variant); } if (req.method() !== 'GET') { report.unexpectedWrites.push(p); return route.abort(); } const fixtures = { '/api/auth/config': { requireAuth: true }, '/api/auth/me': { id: 'a3-ai-owner', sessionContext: 'a3-ai-session', provider: 'local', firstName: 'Alex', displayName: 'Alex Sample', roles: [], plan: 'pro', entitlements: { ai: privacy.enabled, proThemes: true }, uiLanguage: language === 'nb' ? 'nb-NO' : 'en' }, '/api/jobapplications/reminders': [], '/api/notifications/unread-count': { count: 0 }, '/api/notification-settings': notifications, '/api/ai/settings': privacy, '/api/ai/usage': { currentMonth: { calls: 2, estimatedTokens: 200 }, plan: 'pro', monthlyCallLimit: 100, monthlyTokenLimit: 10000, storageUsedBytes: 0, storageLimitBytes: 250000000 }, '/api/billing/status': { enabled: false, canCheckout: false, canManage: false }, ...(settingsOnly ? { '/api/account-lifecycle/status': { deletionEnabled: true, deletionStatus: 'active', requiredConfirmation: 'DELETE alex@example.test', request: null } } : {}), '/api/rules': { id: 1, appliedFollowUpDays: 14, appliedGhostDays: 30, offerFollowUpDays: 7, offerGhostDays: 14, feedbackFollowUpDays: 7, feedbackGhostDays: 14 }, '/api/cv/variants/3': variant, '/api/cv/variants/3/versions': [], '/api/cv/outline': { fullName: 'Alex Sample', headline: 'Recorded teacher headline', sections: [] }, '/api/cv/themes': [{ id: 'modern', name: 'Modern', category: 'Professional', description: 'Synthetic template', layout: 'header-band', accent: '#3157d5', photoShape: 'none', supportsIcons: false, atsFriendly: true, requiresPro: false, available: true, swatches: ['#3157d5'] }], }; if (Object.hasOwn(fixtures, p)) return answer(fixtures[p]); report.unavailableReads.push(p); return route.fulfill({ status: 404, contentType: 'application/json', body: '{}' }); }); const scan = async (state, keepScroll = false) => { assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth + 1), false, `overflow:${state}`); const result = await new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa', 'wcag21aa', 'wcag22aa']).analyze(); if (settingsOnly && state === 'quick-capture') await expect(page.getByRole('alert').filter({ hasText: t('settingsQuickCaptureDragHint') })).toBeVisible(); const layout = visualMatrix ? await page.evaluate(() => { const bounds = element => { const rect = element?.getBoundingClientRect(); return rect ? { x: rect.x, y: rect.y, width: rect.width, height: rect.height } : null; }; const studio = document.querySelector('[data-cv-studio-layout]'); return { header: bounds(document.querySelector('.MuiAppBar-root')), editor: bounds(studio?.firstElementChild), preview: bounds(studio?.lastElementChild), outlinedPapersInStudio: studio?.querySelectorAll('.MuiPaper-outlined').length ?? 0 }; }) : undefined; report.screens.push({ width, language, mode, state, layout, violations: result.violations }); assert.deepEqual(result.violations, [], `${state} accessibility`); if (!keepScroll) { await page.evaluate(() => window.scrollTo({ top: 0, behavior: 'instant' })); await expect.poll(() => page.evaluate(() => window.scrollY)).toBe(0); } await page.screenshot({ path: path.join(out, `${state}-${language}-${mode}-${width}.png`), fullPage: width >= 768 && !keepScroll }); }; const settingsSection = async key => { if (width >= 600) await page.getByRole('tab', { name: t(key), exact: true }).click(); else { await page.getByRole('combobox', { name: t('settingsNavigation'), exact: true }).click(); await page.getByRole('option', { name: t(key), exact: true }).click(); } }; if (settingsOnly) { await page.goto('http://localhost:4338/settings'); for (const [key, state] of [['settingsTabGeneral', 'general'], ['settingsTabAutomation', 'automation'], ['settingsTabFollowUps', 'follow-ups'], ['settingsTabNotifications', 'notifications'], ['settingsTabBackup', 'data']]) { await settingsSection(key); await expect(page.getByRole('tabpanel', { name: t(key), exact: true })).toBeVisible(); await scan(state); } await settingsSection('settingsTabGeneral'); await page.locator('#settings-ai-controls').focus(); await expect(page.locator('#settings-ai-controls')).toBeFocused(); await scan('ai-controls', true); const manual = page.getByRole('textbox', { name: t('settingsQuickCaptureManual'), exact: true }); await expect(manual).toHaveValue(/^javascript:void\(window\.open\('http:\/\/localhost:4338\/\?add='/); assert.equal(await manual.evaluate(el => el.readOnly), true); await manual.focus(); await expect.poll(() => manual.evaluate(el => el.selectionEnd - el.selectionStart === el.value.length)).toBe(true); const captureLink = page.getByRole('link', { name: t('settingsQuickCaptureButton'), exact: true }); assert.equal(await captureLink.getAttribute('href'), await manual.inputValue()); const locationBefore = page.url(); await captureLink.click(); await expect(page.getByText(t('settingsQuickCaptureDragHint'), { exact: true }).last()).toBeVisible(); assert.equal(page.url(), locationBefore); await manual.focus(); await scan('quick-capture', true); assert.deepEqual(writes, []); assert.deepEqual(proposals, []); report.journeys.push({ width, language, mode, allSettingsSections: 'PASS', focusedAiControls: 'PASS', bookmarkletCopyAndClickHint: 'PASS', generationAndApply: 'NOT RUN', settingsSaveAndExport: 'NOT RUN' }); await context.close(); continue; } if (!reviewOnly) { await page.goto('http://localhost:4338/settings'); await settingsSection('settingsTabAutomation'); const reminder = page.getByRole('switch', { name: t('settingsNotificationsFollowUpReminders'), exact: true }); await reminder.uncheck(); assert.equal(writes.length, 0); await page.getByRole('button', { name: t('settingsNotificationsSave'), exact: true }).click(); await expect.poll(() => notifications.emailFollowUpRemindersEnabled).toBe(false); await scan('automation'); await page.getByRole('button', { name: t('settingsAutomationAi'), exact: true }).press('Enter'); await expect(page.locator('#settings-ai-controls')).toBeFocused(); await page.getByRole('switch', { name: t('settingsAiEnabled'), exact: true }).uncheck(); await page.getByRole('button', { name: t('settingsAiSave'), exact: true }).click(); await expect.poll(() => privacy.enabled).toBe(false); await scan('ai-disabled'); await page.goto('http://localhost:4338/career/builder/3'); await studioSection('cvEditorAiTools'); await expect(page.getByText(t('aiUnavailableTitle'), { exact: true })).toBeVisible(); await expect(page.getByRole('button', { name: t('cvDocumentAiReview'), exact: true }).first()).toBeDisabled(); await studioSection('cvEditorContent'); await page.getByRole('textbox', { name: t('cvEditorHeadlineOverride'), exact: true }).fill('Manual teacher headline'); await expect.poll(() => variant.version, { timeout: 15000 }).toBe(5); assert.equal(proposals.length, 0); await scan('manual-fallback'); await page.goto('http://localhost:4338/settings'); await page.getByRole('switch', { name: t('settingsAiEnabled'), exact: true }).check(); await page.getByRole('button', { name: t('settingsAiSave'), exact: true }).click(); await expect.poll(() => privacy.enabled).toBe(true); } await page.goto('http://localhost:4338/career/builder/3'); await studioSection('cvEditorAiTools'); await page.getByRole('button', { name: t('cvDocumentAiReview'), exact: true }).first().click(); await expect(page.getByRole('button', { name: t('cvAiApply'), exact: true })).toBeVisible(); assert.equal(writes.length, reviewOnly ? 0 : 4); await expect(page.locator('#cv-studio-panel-3').getByText(t('cvEditorHeadlineOverride'), { exact: true })).toBeVisible(); await scan('ai-review'); if (reviewOnly) { if (visualMatrix) { await studioSection('cvEditorContent'); await scan('content'); if (width < 768) await page.getByRole('button', { name: t('cvEditorPreview'), exact: true }).click(); await expect(page.getByRole('region', { name: t('cvEditorLivePreview'), exact: true })).toBeVisible(); await scan('preview'); } assert.deepEqual(writes, []); assert.deepEqual(proposals, ['en']); report.journeys.push({ width, language, mode, translatedReviewCaption: 'PASS', explicitReviewWithoutApply: 'PASS', uiLanguageDistinctFromOutputLanguage: 'PASS' }); await context.close(); continue; } await page.getByRole('combobox', { name: new RegExp(`^${t('cvAiLanguage')}`) }).click(); await page.getByRole('option', { name: t('languageNorwegianBokmal'), exact: true }).click(); await expect(page.getByRole('button', { name: t('cvAiApply'), exact: true })).toHaveCount(0); assert.equal(proposals.length, 1); assert.equal(variant.settings.headline, 'Manual teacher headline'); await scan('language-discard'); await page.getByRole('button', { name: t('cvDocumentAiReview'), exact: true }).first().click(); await page.getByRole('button', { name: t('cvAiApply'), exact: true }).click(); await expect.poll(() => variant.version).toBe(6); await studioSection('cvEditorContent'); await expect(page.getByRole('textbox', { name: t('cvEditorHeadlineOverride'), exact: true })).toHaveValue('Reviewed teacher headline'); await scan('explicit-apply'); assert.deepEqual(writes, ['notificationPreference', 'aiPreference', 'manualCvSave', 'aiPreference', 'explicitAiApply']); assert.deepEqual(proposals, ['en', 'nb-NO']); report.journeys.push({ width, language, mode, manualFallback: 'PASS', languageDiscardWithoutApply: 'PASS', explicitTokenApply: 'PASS', aiSettingsKeyboardFocus: 'PASS' }); await context.close(); } assert.equal(report.screens.length, configurations.length * (settingsOnly ? 7 : reviewOnly ? visualMatrix ? 3 : 1 : 6)); assert.equal(report.journeys.length, configurations.length); assert.equal(report.writes.length, reviewOnly || settingsOnly ? 0 : configurations.length * 5); assert.equal(report.proposals.length, settingsOnly ? 0 : configurations.length * (reviewOnly ? 1 : 2)); assert.deepEqual(report.errors, []); assert.deepEqual(report.unexpectedWrites, []); assert.ok(report.blockedExternal.every(origin => origin === 'https://fonts.googleapis.com')); console.log(`PASS:${report.journeys.length} controlled journeys,${report.screens.length} axe scans/captures.`); } finally { fs.writeFileSync(path.join(out, reviewOnly ? 'REVIEW.json' : process.env.A3_PILOT ? 'PILOT.json' : 'BROWSER.json'), JSON.stringify({ ...report, limits: ['All API, AI and preview results are synthetic fixtures. No model activation, semantic translation quality, backend end-to-end or physical PDF qualification.', 'No native device, screen reader, provider, integrated security, deployment or release acceptance.'] }, null, 2)); await browser.close(); }