// Built application, synthetic document/mail/provider fixtures; no delivery or real storage. import { chromium, expect } from '@playwright/test'; import AxeBuilder from '@axe-core/playwright'; import ts from 'typescript'; import fs from 'node:fs'; import path from 'node:path'; import assert from 'node:assert/strict'; 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 labelOnly = process.env.A3_LABEL_COUNT === '1'; const reviewCounts = process.env.A3_REVIEW_COUNTS === '1'; const recoveryOnly = process.env.A3_MAIL_RECOVERY === '1' || reviewCounts; assert.ok(!(labelOnly && recoveryOnly), 'Count and recovery are separate scopes.'); const mailOnly = process.env.A3_MAIL_FLAT === '1' || labelOnly || recoveryOnly; const widths = process.env.A3_PILOT ? [320] : visualMatrix ? [320, 360, 390, 430, 768, 1024, 1280, 1440, 1920] : [320, 390, 768, 1440]; const expectedJourneys = widths.length * (process.env.A3_PILOT ? 1 : 4); const out = path.resolve(import.meta.dirname, reviewCounts ? '../../docs/ui-modernisation/visual-correction/evidence/mail-review-counts' : recoveryOnly ? '../../docs/ui-modernisation/visual-correction/evidence/mail-recovery' : labelOnly ? '../../docs/ui-modernisation/visual-correction/evidence/mail-labels' : mailOnly ? '../../docs/ui-modernisation/visual-correction/evidence/mail-flat' : visualMatrix ? '../../docs/ui-modernisation/visual-correction/evidence/documents-inbox' : '../../docs/ui-modernisation/evidence/documents-inbox/browser'); fs.mkdirSync(out, { recursive: true }); const report = { screens: [], journeys: [], errors: [], unexpectedWrites: [], mockedWrites: [], unavailableReads: [], blockedExternal: [] }; const browser = await chromium.launch(); try { for (const width of widths) for (const language of (process.env.A3_PILOT ? ['en'] : ['en', 'nb'])) for (const mode of (process.env.A3_PILOT ? ['light'] : ['light', 'dark'])) { 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', acceptDownloads: true }); const page = await context.newPage(); await page.addInitScript(({ language, mode }) => { localStorage.setItem('uiLanguage', language); localStorage.setItem('jobtracker.themeMode', mode); }, { language, mode }); page.on('pageerror', error => report.errors.push(error.message)); const document = { id: 'attachment:9', resourceId: '9', type: 'attachment', name: 'First aid certificate.txt', state: 'current', visibility: 'private', jobApplicationId: 42, jobTitle: 'Primary teacher', companyName: 'Synthetic school', jobIsDeleted: false, recordedAt: '2026-02-12T09:15:00.1234567', dateKind: 'uploaded-local', version: null, historyCount: null, attachmentCount: null, cvVariantId: null, fileType: 'text/plain', fileSize: 2048, purpose: 'certificate', useForAi: false, sourceHref: '/jobs/42?section=documents', downloadHref: '/api/attachments/download/9', publicHref: null, hasCurrentContent: true }; const cv = { ...document, id: 'cv:3', resourceId: '3', type: 'cv', name: 'Teacher CV', dateKind: 'updated-utc', recordedAt: '2026-11-03T10:00:00Z', visibility: 'private', version: 4, sourceHref: '/career/builder/3', downloadHref: null }; const mail = { id: 1, jobApplicationId: 42, companyName: 'Synthetic school', jobTitle: 'Primary teacher', from: 'Recruiter', direction: 'inbound', subject: 'Interview invitation', channel: 'Email', date: '2026-11-03T10:00:00Z', contentPreview: 'Recorded invitation', externalThreadId: 'thread-1', externalMessageId: 'message-1', provider: 'gmail', externalFrom: 'recruiter@example.test', externalTo: 'alex@example.test', labelCount: 1, attachmentCount: 0 }; let draft = null, sends = 0, documentReads = 0, mailReads = 0; let reviewState = reviewCounts ? 'populated' : 'unavailable'; const reviewThread = { threadId: 'review-thread-1', subject: 'Research technician application', latestDate: mail.date, messageCount: reviewCounts ? 1 : 2, routing: 'suggested', hasImportedMessages: false, decisionNote: '', matchedQueries: ['"Research technician"'], jobCandidates: [], messages: [] }; const suggestion = { threadId: reviewThread.threadId, companyName: 'Synthetic laboratory', suggestedJobTitle: 'Research technician', subject: reviewThread.subject, preview: 'Recorded suggestion' }; const draftText = 'Thank you. Please confirm the available interview times.'; const revisedDraft = 'Unsaved revision after the interview was rescheduled.'; await page.route('**/*', route => { const req = route.request(), url = new URL(req.url()), p = url.pathname; 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) }); if (req.method() === 'POST' && p === '/api/email/drafts') { const body = req.postDataJSON(); assert.equal(body.jobApplicationId, 42); assert.equal(body.bodyText, draftText); draft = { ...body, id: '00000000-0000-4000-8000-000000000101', revision: 4, clientRequestId: '00000000-0000-4000-8000-000000000202', createdAtUtc: '2026-11-03T10:00:00Z', updatedAtUtc: '2026-11-03T10:00:00Z' }; report.mockedWrites.push({ width, language, mode, action: 'saveDraft' }); return answer(draft); } if (recoveryOnly && draft && req.method() === 'PUT' && p === '/api/email/drafts/' + draft.id) { const body = req.postDataJSON(); assert.equal(body.revision, draft.revision); assert.equal(body.bodyText, revisedDraft); report.mockedWrites.push({ width, language, mode, action: 'draftConflict' }); return route.fulfill({ status: 409, contentType: 'application/problem+json', body: '{"detail":"Controlled revision conflict."}' }); } if (!recoveryOnly && req.method() === 'POST' && p === '/api/email/send') { const body = req.postDataJSON(); assert.equal(body.confirmed, true); assert.equal(body.clientRequestId, draft.clientRequestId); assert.equal(body.bodyText, draft.bodyText); sends++; report.mockedWrites.push({ width, language, mode, action: 'confirmedSendFixtureUncertain' }); return answer({ attemptId: body.clientRequestId, status: 'uncertain', duplicate: false, failureCategory: 'controlled-transport-interruption' }); } if (req.method() !== 'GET') { report.unexpectedWrites.push(p); return route.abort(); } if (recoveryOnly && p === '/api/gmail/review-candidates') { if (reviewState === 'unavailable') return route.fulfill({ status: 503, contentType: 'application/problem+json', body: '{"detail":"Controlled queue unavailable."}' }); const threads = reviewState === 'populated' ? [reviewThread] : []; return answer({ queries: [], candidateThreadCount: threads.length, autoLinkThreadCount: 0, reviewThreadCount: 0, unmatchedThreadCount: 0, threads }); } if (recoveryOnly && p === '/api/gmail/suggested-jobs') { const items = reviewState === 'populated' ? [suggestion] : []; return answer({ count: items.length, items }); } if (p === '/api/attachments/download/9') return route.fulfill({ status: 200, contentType: 'text/plain', body: 'Controlled certificate fixture' }); if (p === '/api/documents') { documentReads++; const rows = [document, cv].filter(item => !url.searchParams.get('q') || item.name.toLowerCase().includes(url.searchParams.get('q').toLowerCase())); return answer({ items: rows, page: 1, pageSize: 25, total: rows.length, totalPages: 1 }); } if (p === '/api/correspondence/page') { mailReads++; return answer({ items: [mail], page: 1, pageSize: 50, total: 1, totalPages: 1 }); } const fixtures = { '/api/auth/config': { requireAuth: true }, '/api/auth/me': { id: 'a3-mail-owner', sessionContext: 'a3-mail-session', provider: 'local', firstName: 'Alex', displayName: 'Alex Sample', roles: [], uiLanguage: language === 'nb' ? 'nb-NO' : 'en', aiEnabled: false }, '/api/companies': [], '/api/jobapplications/reminders': [], '/api/notifications/unread-count': { count: 0 }, '/api/email/providers': [{ provider: 'gmail', displayName: 'Gmail', connected: true, address: 'alex@example.test', canRead: true, canSend: true }], '/api/email/drafts': draft ? [draft] : [], '/api/jobapplications/choices': [{ id: 42, jobTitle: 'Primary teacher', companyName: 'Synthetic school' }], '/api/email/message': { id: 'message-1', threadId: 'thread-1', subject: 'Interview invitation', from: 'recruiter@example.test', to: 'alex@example.test', date: mail.date, snippet: 'Recorded invitation', bodyText: 'Please choose an interview time. ', labels: ['INBOX'], attachments: [] }, }; 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, state); const axe = 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: axe.violations }); assert.deepEqual(axe.violations, []); await page.screenshot({ path: path.join(out, `${state}-${language}-${mode}-${width}.png`), fullPage: width >= 768 && !target }); }; if (!mailOnly) { await page.goto('http://localhost:4338/documents'); const docs = page.getByRole('region', { name: t('documentsYourDocuments'), exact: true }); await docs.getByText(document.name, { exact: true }).waitFor(); await scan('documents'); const reads = documentReads; await page.getByRole('button', { name: t('documentsJumpResults'), exact: true }).press('Enter'); assert.equal(await docs.evaluate(node => node === window.document.activeElement), true); await docs.getByRole('button', { name: t('documentsReturnFilters'), exact: true }).click(); const search = page.getByRole('textbox', { name: t('documentsSearch'), exact: true }); await expect(search).toBeFocused(); assert.equal(documentReads, reads); await search.fill('certificate'); await expect(docs.getByText(cv.name, { exact: true })).toHaveCount(0); await docs.getByText(document.name, { exact: true }).waitFor(); const download = page.waitForEvent('download'); await docs.getByRole('button', { name: t('documentsDownloadAttachment'), exact: true }).click(); const file = await download; assert.equal(file.suggestedFilename(), document.name); assert.equal(fs.readFileSync(await file.path(), 'utf8'), 'Controlled certificate fixture'); assert.equal(await docs.getByRole('link', { name: t('documentsOpenSource'), exact: true }).getAttribute('href'), document.sourceHref); assert.equal(await docs.locator('time').getAttribute('datetime'), document.recordedAt); await scan('document-filtered', docs.getByText(document.name, { exact: true })); } await page.goto('http://localhost:4338/correspondence'); const messages = page.getByRole('region', { name: t('emailInboxLinkedMessages'), exact: true }); await messages.getByText(mail.subject, { exact: true }).waitFor(); if (recoveryOnly) { const review = page.getByRole('button', { name: t('emailInboxReviewSuggestions'), exact: true }); await review.press('Enter'); const panel = page.getByRole('region', { name: t('gmailReviewEmbeddedTitle'), exact: true }); const article = panel.getByRole('article', { name: reviewThread.subject, exact: true }); if (reviewCounts) { await expect(panel.getByText(t('gmailReviewCandidateThread').replace('{count}', '1'), { exact: true })).toBeVisible(); await expect(panel.getByText(t('gmailReviewSuggestedJobCount').replace('{count}', '1'), { exact: true })).toBeVisible(); const summary = t('gmailReviewSingleMessageSummary').replace('{count}', '1').replace('{route}', t('gmailReviewRouteSuggested')); await expect(article.getByText(summary, { exact: true })).toBeVisible(); await scan('review-counts'); report.journeys.push({ width, language, mode, singularCandidateSuggestionAndMessage: 'PASS', send: 'NOT RUN' }); await context.close(); continue; } const retry = panel.getByRole('button', { name: t('retry'), exact: true }); const empty = panel.getByText(t('gmailReviewNoCandidates'), { exact: true }); await expect(panel.getByText(t('gmailReviewLoadFailed'), { exact: true })).toBeVisible(); await expect(retry).toBeVisible(); await expect(empty).toHaveCount(0); await scan('review-unavailable', retry); reviewState = 'empty'; await retry.press('Enter'); await expect(panel.getByRole('group', { name: t('gmailReviewEmbeddedTitle'), exact: true })).toBeFocused(); await expect(empty).toBeVisible(); await scan('review-empty', empty); reviewState = 'populated'; await panel.getByRole('button', { name: t('correspondenceRefresh'), exact: true }).press('Enter'); await expect(article).toContainText(suggestion.companyName); await scan('review-suggestion', article); await page.getByRole('button', { name: t('emailInboxLinkedMessages'), exact: true }).press('Enter'); await expect(panel).toBeHidden(); await expect(messages).toBeVisible(); await messages.getByRole('button', { name: t('emailInboxViewMessage'), exact: true }).click(); await messages.getByRole('button', { name: t('emailInboxReply'), exact: true }).click(); const compose = page.getByRole('region', { name: t('emailInboxDraftTitle'), exact: true }); const body = compose.getByRole('textbox', { name: t('emailInboxMessage'), exact: true }); const save = compose.getByRole('button', { name: t('emailInboxSaveDraft'), exact: true }); await compose.getByRole('textbox', { name: t('emailInboxRecipient'), exact: true }).fill('recruiter@example.test'); await body.fill(draftText); await save.click(); await expect.poll(() => draft?.revision).toBe(4); const update = compose.getByRole('button', { name: t('profileSaveChanges'), exact: true }); await expect(update).toBeEnabled(); await body.fill(revisedDraft); await update.click(); const conflict = compose.getByText(t('emailInboxDraftChangedElsewhere'), { exact: true }); await expect(conflict).toBeVisible(); await expect(body).toHaveValue(revisedDraft); assert.equal(draft.bodyText, draftText); await scan('draft-conflict', conflict); report.journeys.push({ width, language, mode, reviewUnavailableRetryEmptyAndSuggestion: 'PASS', revisionConflictPreservedUnsavedDraft: 'PASS', send: 'NOT RUN' }); await context.close(); continue; } if (labelOnly) { const label = messages.getByText(t('emailInboxLabelCount').replace('{count}', '1'), { exact: true }); await expect(label).toBeVisible(); await scan('label-count', label); report.journeys.push({ width, language, mode, singularRecordedLabelCount: 'PASS', documentFilterFocusDownloadSource: 'NOT RUN', inboxDraftSendWorkflow: 'NOT RUN' }); await context.close(); continue; } await scan('inbox'); const initialMailReads = mailReads; await page.getByRole('button', { name: t('emailInboxJumpMessages'), exact: true }).press('Enter'); await expect(messages).toBeFocused(); await messages.getByRole('button', { name: t('emailInboxReturnFilters'), exact: true }).click(); await expect(page.getByRole('textbox', { name: t('correspondenceSearch'), exact: true })).toBeFocused(); assert.equal(mailReads, initialMailReads); await messages.getByRole('button', { name: t('emailInboxViewMessage'), exact: true }).click(); const messageBody = messages.getByText('Please choose an interview time. ', { exact: true }); await messageBody.waitFor(); const article = messages.getByRole('article', { name: mail.subject, exact: true }); const applicationContext = article.getByRole('complementary', { name: t('emailInboxJob'), exact: true }); await expect(applicationContext).toContainText(mail.jobTitle); await expect(applicationContext).toContainText(mail.companyName); await expect(article.getByRole('button', { name: t('emailInboxOpenJob'), exact: true })).toHaveCount(1); const contentBounds = await article.locator('[data-mail-message-content]').boundingBox(), contextBounds = await applicationContext.boundingBox(); assert.ok(width > 1000 ? contextBounds.x >= contentBounds.x + contentBounds.width - 2 && Math.abs(contextBounds.y - contentBounds.y) < 2 : contextBounds.y + contextBounds.height <= contentBounds.y + 2, 'Recorded application context is right on desktop and before the body on narrow screens'); await scan('message', messageBody); await messages.getByRole('button', { name: t('emailInboxReply'), exact: true }).click(); const compose = page.getByRole('region', { name: t('emailInboxDraftTitle'), exact: true }); await compose.getByRole('textbox', { name: t('emailInboxRecipient'), exact: true }).fill('recruiter@example.test'); await compose.getByRole('textbox', { name: t('emailInboxMessage'), exact: true }).fill(draftText); await compose.getByRole('button', { name: t('emailInboxSaveDraft'), exact: true }).click(); await expect.poll(() => draft?.revision).toBe(4); await compose.getByRole('button', { name: t('emailInboxReviewSend'), exact: true }).click(); const dialog = page.getByRole('dialog', { name: t('emailInboxConfirmSendTitle'), exact: true }); await dialog.waitFor(); assert.equal(sends, 0); await scan('send-confirmation', dialog); await dialog.getByRole('button', { name: t('emailInboxSendEmail'), exact: true }).click(); await compose.getByText(t('emailInboxDeliveryUncertain'), { exact: true }).waitFor(); await expect(compose.getByRole('button', { name: t('emailInboxReviewSend'), exact: true })).toBeDisabled(); assert.equal(sends, 1); await expect(compose.getByRole('textbox', { name: t('emailInboxMessage'), exact: true })).toHaveValue(draft.bodyText); await scan('uncertain-send', compose.getByRole('textbox', { name: t('emailInboxMessage'), exact: true })); report.journeys.push({ width, language, mode, documentFilterFocusDownloadSource: mailOnly ? 'NOT RUN' : 'PASS', inboxFocusPlainTextReplySaveExplicitConfirmUncertainNoRetry: 'PASS' }); await context.close(); } const displayOnly = labelOnly || reviewCounts; assert.equal(report.journeys.length, expectedJourneys); assert.equal(report.screens.length, report.journeys.length * (displayOnly ? 1 : mailOnly ? 4 : 6)); assert.equal(report.mockedWrites.length, displayOnly ? 0 : report.journeys.length * 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} document/mail journeys,${report.screens.length} axe scans/captures,${report.mockedWrites.length} intercepted write requests.`); } finally { fs.writeFileSync(path.join(out, process.env.A3_PILOT ? 'PILOT.json' : 'BROWSER.json'), JSON.stringify({ ...report, limits: ['Synthetic metadata/binary/mail/provider fixtures; not live storage, mailbox, OAuth or delivery.', reviewCounts ? 'Only the populated review counts are exercised. No draft or provider actions.' : recoveryOnly ? 'Review uses only fixture reads. Draft create and rejected update are intercepted; sending is blocked.' : 'Browser covers draft save, explicit confirmation and controlled uncertain outcome only; other recovery/conflict paths remain targeted tests.', 'No real sending, native device, final integrated security or release acceptance.'] }, null, 2)); await browser.close(); }