// Actual exported React UI; synthetic API/render 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 visualMatrix = process.env.A3_VISUAL_MATRIX === '1'; const evidenceOnly = process.env.A3_EVIDENCE_REVIEW === '1'; assert.ok(!evidenceOnly || !process.env.A3_BOUNDARY, 'Evidence review does not repeat editor boundary qualification.'); const out = path.resolve(import.meta.dirname, evidenceOnly ? '../../docs/ui-modernisation/visual-correction/evidence/career-evidence' : visualMatrix ? '../../docs/ui-modernisation/visual-correction/evidence/career' : '../../docs/ui-modernisation/evidence/career/browser'); fs.mkdirSync(out, { recursive: true }); const report = { screens: [], journeys: [], errors: [], unexpectedWrites: [], mockedWrites: [], renderReads: 0, unavailableReads: [], blockedExternal: [] }; const browser = await chromium.launch(); try { const configurations = process.env.A3_BOUNDARY ? [320, 390, 768, 1000, 1001, 1440].map(width => [width, 'en', 'light']) : process.env.A3_PILOT ? (evidenceOnly ? [[320, 'en', 'light'], [1440, 'en', 'light']] : [[320, 'en', 'light']]) : (visualMatrix || evidenceOnly ? [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)); await page.addInitScript(({ language, mode }) => { localStorage.setItem('uiLanguage', language); localStorage.setItem('jobtracker.themeMode', mode); }, { language, mode }); let career = { revision: 7, cvText: 'Retained raw source', profile: { version: '1', metadata: { profileVersion: 4, fields: {} }, contact: { fullName: 'Alex Sample', email: 'alex@example.test' }, summary: ['Recorded teacher summary'], skills: ['Teaching'], languages: [{ name: 'Norwegian', level: 'A2' }], projects: [{ id: 'project-one', name: 'Community project', role: 'Coordinator', start: '2024', end: '2025', bullets: ['Recorded outcome'], skills: ['Teaching'] }], certifications: [{ id: 'cert-one', name: 'First aid', issuer: 'Recorded issuer', date: '2024', details: ['Recorded detail'] }], jobs: [], education: [], interests: [], awards: [], publications: [], organisations: [], references: [], otherSections: [], sections: [], }, completeness: { percent: 70, missing: ['Experience', 'Education'], sections: ['personal', 'summary', 'experience', 'education', 'skills', 'projects', 'languages'].map(key => ({ key, label: key, complete: !['experience', 'education'].includes(key), count: 1 })) } }; let variant = { id: 3, name: 'Teacher CV', version: 4, settings: { themeId: 'modern', showPhoto: false, showPageNumbers: false, showIcons: true, sections: [], overrides: {}, customSections: [] }, isPublic: false, publicSlug: 'synthetic', jobApplicationId: null, updatedAtUtc: '2026-11-03T10:00:00Z' }; let letter = { revision: 7, text: 'Dear team\n\nRecorded teaching experience.', currentVersion: 2, versions: [{ version: 2, source: 'manual', length: 45, createdAtUtc: '2026-11-03T10:00:00Z', isCurrent: true }], aiSuggestionCount: 0 }; const theme = { 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'] }; let evidenceState = 'populated'; const evidenceItems = [{ id: 1, category: 'achievement', title: 'Recorded nurse mentoring', statement: 'Mentored new colleagues in a synthetic training scenario.', tags: ['Training', 'Communication'], sourceReference: 'Synthetic review' }, { id: 2, category: 'project', title: 'Recorded electrician project', statement: 'Documented a synthetic installation handover.', tags: ['Safety'], sourceReference: 'Synthetic project notes' }]; await page.route('**/*', route => { const req = route.request(), url = new URL(req.url()), p = url.pathname; if (evidenceOnly && 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 => report.mockedWrites.push({ width, language, mode, action }); if (evidenceOnly && req.method() !== 'GET') { report.unexpectedWrites.push(p); return route.abort(); } if (evidenceOnly && p === '/api/career/evidence') return evidenceState === 'error' ? route.fulfill({ status: 503, contentType: 'application/json', body: '{}' }) : answer(evidenceState === 'empty' ? [] : evidenceItems); if (req.method() === 'PUT' && p === '/api/career/profile') { const body = req.postDataJSON(); assert.equal(body.expectedVersion, career.revision); assert.equal(body.cvText, career.cvText); assert.equal(body.profile.projects[0].id, 'project-one'); assert.equal(body.profile.projects[0].name, 'Updated community project'); assert.deepEqual(body.profile.certifications, career.profile.certifications); assert.deepEqual(body.profile.metadata, career.profile.metadata); career = { ...career, profile: body.profile, revision: career.revision + 1 }; note('canonicalProjectSave'); return answer(career); } if (req.method() === 'PUT' && p === '/api/cv/variants/3') { const body = req.postDataJSON(); assert.equal(body.expectedVersion, variant.version); assert.deepEqual(body.settings, variant.settings); variant = { ...variant, name: body.name, version: variant.version + 1 }; note('variantRename'); return answer(variant); } if (req.method() === 'PUT' && p === '/api/jobapplications/42/cover-letter') { const body = req.postDataJSON(); assert.equal(body.expectedVersion, letter.revision); assert.equal(body.source, 'manual'); assert.equal(body.text, 'Dear team\n\nUpdated teaching experience.'); letter = { ...letter, text: body.text, revision: letter.revision + 1, currentVersion: 3 }; note('manualLetterSave'); return answer(letter); } if (req.method() === 'POST' && p === '/api/cv/preview') { report.renderReads++; return answer({ themeId: 'modern', html: 'Controlled CV preview

Alex Sample

Recorded teaching experience

', suggestedFileName: 'teacher.pdf' }); } if (req.method() !== 'GET') { report.unexpectedWrites.push(p); return route.abort(); } const fixtures = { '/api/auth/config': { requireAuth: true }, '/api/auth/me': { id: 'a3-career-owner', sessionContext: 'a3-career-session', provider: 'local', firstName: 'Alex', displayName: 'Alex Sample', roles: [], uiLanguage: language === 'nb' ? 'nb-NO' : 'en', aiEnabled: false }, '/api/jobapplications/reminders': [], '/api/notifications/unread-count': { count: 0 }, '/api/career/profile': career, '/api/career/profile/versions': [], '/api/career/evidence': [], '/api/profile-cv/runs': [{ id: 41, trigger: 'upload', status: 'pending_review', startedAtUtc: '2026-11-03T10:00:00Z', parserVersion: 'offline-fixture', normalizerVersion: 'offline-fixture', llmPromptVersion: 'offline-fixture' }], '/api/profile-cv/runs/41/diff': { runId: 41, status: 'pending_review', revision: career.revision, diff: { totalAdded: 1, totalUpdated: 1, totalLowConfidence: 1, hasChanges: true, categories: [{ category: 'Languages', unchangedCount: 0, lowConfidenceCount: 1, added: [{ id: 'Languages|french', label: 'French', confidence: 'Low', fieldChanges: [] }], updated: [{ id: 'Languages|norwegian', label: 'Norwegian', confidence: 'Medium', fieldChanges: [{ field: 'Level', oldValue: 'A2', newValue: 'B1' }] }] }] } }, '/api/cv/variants': [{ ...variant, themeId: 'modern', language: 'en' }], '/api/cv/variants/3': variant, '/api/cv/themes': [theme], '/api/cv/variants/3/versions': [], '/api/cv/outline': { fullName: 'Alex Sample', headline: 'Teacher', sections: [{ key: 'summary', title: 'Professional Summary', kind: 'bullets', bullets: ['Recorded summary'], tags: [], entries: [] }] }, '/api/jobapplications/42/workspace': { id: 42, revision: letter.revision, jobTitle: 'Primary teacher', company: 'Synthetic school', status: 'Saved', stageGroup: 'Applied', stageOrder: 1, location: 'Oslo', countryCode: 'NO', jobUrl: null, source: 'nav', description: 'Recorded source advertisement.', descriptionLanguage: 'en', tags: [], notes: '', applicationAnswerDraft: '', recruiterMessageDraft: '', hasJobDescription: true, cv: { variantId: 3, variantName: variant.name, themeId: 'modern', hasTailoredCvText: false }, hasCoverLetter: true, documentCount: 0, hasPortfolio: false, aiInteractionCount: 0, recentActivity: [], nextStep: null, checklistProgress: { total: 0, completed: 0, dismissed: 0, percent: 0 } }, '/api/jobapplications/42/cover-letter': letter, '/api/jobapplications/42/status-suggestion': null, }; 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, target) => { if (target) await target.evaluate(node => node.scrollIntoView({ block: 'center', behavior: 'instant' })); else await page.evaluate(() => window.scrollTo(0, 0)); 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(); report.screens.push({ width, language, mode, state, capture: target ? 'targeted viewport' : width >= 768 ? 'full page at top' : 'viewport at top', violations: result.violations }); assert.deepEqual(result.violations, [], `${state} accessibility`); await page.screenshot({ path: path.join(out, `${state}-${language}-${mode}-${width}.png`), fullPage: width >= 768 && !target }); }; const careerSection = async key => { if (width >= 600) await page.getByRole('tab', { name: t(key), exact: true }).click(); else { await page.getByRole('combobox', { name: t('careerWorkspaceNavigation'), exact: true }).click(); await page.getByRole('option', { name: t(key), exact: true }).click(); } }; if (evidenceOnly) { const openEvidence = async () => { await page.goto('http://localhost:4338/career'); await careerSection('careerWorkspaceEvidenceTab'); }; await openEvidence(); const title = page.getByRole('textbox', { name: new RegExp(t('careerEvidenceItemTitle')) }); await expect(page.getByRole('article', { name: evidenceItems[0].title, exact: true })).toBeVisible(); await title.fill('Unsaved evidence draft'); await scan('form-draft', title); await careerSection('careerWorkspaceOverviewTab'); await careerSection('careerWorkspaceEvidenceTab'); await expect(title).toHaveValue('Unsaved evidence draft'); const article = page.getByRole('article', { name: evidenceItems[0].title, exact: true }); await scan('populated', article); await article.getByRole('button').click(); const dialog = page.getByRole('dialog'); await expect(dialog).toContainText(evidenceItems[0].title); await scan('delete-confirmation', dialog); await dialog.getByRole('button', { name: t('cancel'), exact: true }).click(); await expect(article).toBeVisible(); evidenceState = 'empty'; await openEvidence(); await expect(page.getByText(t('careerEvidenceEmpty'), { exact: true })).toBeVisible(); await scan('empty'); evidenceState = 'error'; await openEvidence(); await expect(page.getByRole('main').getByRole('alert')).toContainText(t('careerEvidenceLoadFailed')); await title.fill('Retained failure draft'); await page.getByRole('textbox', { name: new RegExp(t('careerEvidenceStatement')) }).fill('Synthetic statement retained during retry.'); await expect(page.getByRole('button', { name: t('careerEvidenceSave'), exact: true })).toBeDisabled(); await scan('error'); evidenceState = 'populated'; await page.getByRole('button', { name: t('retry'), exact: true }).click(); await expect(page.getByRole('article', { name: evidenceItems[0].title, exact: true })).toBeVisible(); await expect(title).toHaveValue('Retained failure draft'); await expect(page.getByRole('button', { name: t('careerEvidenceSave'), exact: true })).toBeEnabled(); await scan('retry', page.getByRole('article', { name: evidenceItems[0].title, exact: true })); report.journeys.push({ width, language, mode, evidenceDraftAcrossSections: 'PASS', deletionCancelled: 'PASS', emptyErrorRetry: 'PASS', evidenceSaveDelete: 'NOT RUN', editorImportAndExport: 'NOT RUN' }); await context.close(); continue; } await page.goto('http://localhost:4338/career'); await expect(page.locator('[data-edit-career-section]')).toHaveCount(14); await scan('overview'); for (const key of ['contact', 'summary', 'skills', 'interests', 'awards', 'publications', 'organisations', 'references', 'languages', 'jobs', 'education', 'certifications', 'projects', 'otherSections']) { await page.locator(`[data-edit-career-section="${key}"]`).press('Enter'); await expect.poll(() => page.locator(`#career-section-${key}`).evaluate(node => node.contains(document.activeElement))).toBe(true); if (process.env.A3_BOUNDARY || visualMatrix) await expect.poll(async () => page.evaluate(() => { const active = document.activeElement, rect = active.getBoundingClientRect(); const x = Math.min(innerWidth - 1, Math.max(0, rect.x + rect.width / 2)), y = Math.min(innerHeight - 1, Math.max(0, rect.y + rect.height / 2)); const hit = document.elementFromPoint(x, y); return rect.bottom > 0 && rect.top < innerHeight && active.contains(hit); }), { message: `Focused ${key} must remain visible and unobscured at ${width}px` }).toBe(true); await careerSection('careerWorkspaceOverviewTab'); } await page.locator('[data-edit-career-section="projects"]').click(); const projectName = page.locator('#career-section-projects input').first(); await projectName.fill('Updated community project'); await page.getByRole('button', { name: t('profileSaveChanges'), exact: true }).click(); await expect.poll(() => career.revision).toBe(8); await expect(projectName).toBeEnabled(); await scan('profile', projectName); await careerSection('careerWorkspaceImportTab'); const run = page.locator('#career-import-run-41'); await expect(run).toContainText('A2'); await expect(run).toContainText('B1'); await run.getByRole('checkbox').check(); await expect(run.getByRole('checkbox')).toBeChecked(); await scan('import-review', run); assert.equal(report.mockedWrites.filter(row => row.width === width && row.language === language && row.mode === mode).length, 1); await page.goto('http://localhost:4338/career/builder'); const cvCard = page.getByRole('link', { name: t('cvDashboardOpenNamed').replace('{name}', 'Teacher CV'), exact: true }); await cvCard.waitFor(); await expect(cvCard).toContainText(t('cvTemplateModernName')); await scan('library'); await cvCard.press('Enter'); const editor = page.getByRole('region', { name: t('cvEditorEditingRegion'), exact: true }); const preview = page.getByRole('region', { name: t('cvEditorLivePreview'), exact: true }); await editor.waitFor(); if (width < 768) { await page.getByRole('button', { name: t('cvEditorPreview'), exact: true }).click(); await expect(editor).toBeHidden(); await expect(preview).toBeVisible(); await page.getByRole('button', { name: t('cvEditorEdit'), exact: true }).click(); } else { await expect(preview).toBeVisible(); const left = await editor.boundingBox(), right = await preview.locator('..').boundingBox(); assert.ok(width > 1000 ? right.x >= left.x + left.width - 2 : right.y >= left.y + left.height - 2, 'CV pane geometry'); } const cvName = page.getByRole('textbox', { name: t('cvEditorName'), exact: true }); await cvName.fill('Updated teacher CV'); await expect.poll(() => variant.name, { timeout: 15000 }).toBe('Updated teacher CV'); await scan('cv-editor', cvName); await page.goto('http://localhost:4338/jobs/42?section=application&sub=letter'); const text = page.getByRole('textbox', { name: t('jobDetailsCoverLetter'), exact: true }); await expect(text).toBeEnabled(); await text.fill('Dear team\n\nUpdated teaching experience.'); const letterPreview = page.getByRole('region', { name: t('assetsLetterTextPreview'), exact: true }); if (width < 768) { await page.getByRole('tab', { name: t('assetsLetterPreview'), exact: true }).click(); await expect(text).toBeHidden(); await expect(letterPreview).toContainText('Updated teaching experience.'); await page.getByRole('tab', { name: t('assetsLetterWrite'), exact: true }).click(); } else { await expect(letterPreview).toContainText('Updated teaching experience.'); const panes = page.locator('[data-letter-studio-layout] > div'); const left = await panes.nth(0).boundingBox(), right = await panes.nth(1).boundingBox(); assert.ok(width > 1000 ? right.x >= left.x + left.width - 2 : right.y >= left.y + left.height - 2, 'Letter pane geometry'); } await page.getByRole('button', { name: t('save'), exact: true }).click(); await expect.poll(() => letter.revision).toBe(8); await expect(text).toBeEnabled(); await scan('letter', text); report.journeys.push({ width, language, mode, all14KeyboardEditTargets: 'PASS', canonicalSaveStableIds: 'PASS', legacyImportComparisonSelectionNoApply: 'PASS', libraryKeyboardOpen: 'PASS', cvResponsiveRename: 'PASS', letterResponsiveManualSave: 'PASS' }); await context.close(); } assert.equal(report.screens.length, configurations.length * 6); assert.equal(report.journeys.length, configurations.length); assert.equal(report.mockedWrites.length, evidenceOnly ? 0 : configurations.length * 3); 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,${report.mockedWrites.length} fixture mutations.`); } finally { fs.writeFileSync(path.join(out, process.env.A3_BOUNDARY ? 'BOUNDARY.json' : process.env.A3_PILOT ? 'PILOT.json' : 'BROWSER.json'), JSON.stringify({ ...report, limits: ['All API and preview responses are controlled fixtures; not backend end-to-end or physical PDF qualification.', evidenceOnly ? 'Evidence-only scope: delete confirmation cancelled, all mutations blocked; profile/editor/import/export workflows NOT RUN.' : 'Legacy import comparison and selection only; no parser, model, import acceptance or source mutation.', 'No native device, screen reader, provider, integrated security, deployment or release acceptance.'] }, null, 2)); await browser.close(); }