import { expect, test, type APIRequestContext } from '@playwright/test'; import fs from 'node:fs/promises'; import path from 'node:path'; import { createHash } from 'node:crypto'; import { previewDocumentHtml } from '../src/cvPreview'; import { translations } from '../src/i18n/translations'; const apiUrl = 'http://localhost:5302/api'; const output = path.resolve(process.env.LETTER_FIDELITY_OUTPUT || '../.artifacts/cv-ux-01/letter-fidelity'); const password = 'E2ePassword123!'; async function login(api: APIRequestContext, email: string) { expect((await api.post(`${apiUrl}/auth/login`, { data: { email, password } })).status()).toBe(200); const me = await (await api.get(`${apiUrl}/auth/me`)).json(); return { 'X-CSRF-TOKEN': (await api.storageState()).cookies.find(cookie => cookie.name === 'XSRF-TOKEN')!.value, 'X-Session-Context': me.sessionContext as string }; } test('cover letter native preview/PDF fidelity records four independent physical documents', async ({ page, context, browser, playwright }, info) => { test.setTimeout(360_000); // Existing attempts remain reviewable, including a partially produced failed run. await fs.mkdir(output, { recursive: true }); await fs.writeFile(path.join(output, 'started.json'), JSON.stringify({ startedUtc: new Date().toISOString(), browserVersion: browser.version() }), { flag: 'wx' }); await context.route('**/*', route => { const url = new URL(route.request().url()); return ['localhost', '127.0.0.1'].includes(url.hostname) || ['data:', 'blob:', 'about:'].includes(url.protocol) ? route.continue() : route.abort(); }); await context.addInitScript(() => { const query = new URLSearchParams(location.search); if (query.has('fidelityMode')) localStorage.setItem('jobtracker.themeMode', query.get('fidelityMode')!); if (query.has('fidelityLanguage')) localStorage.setItem('uiLanguage', query.get('fidelityLanguage')!); }); const admin = await playwright.request.newContext(); const adminHeaders = await login(admin, 'e2e@example.test'); const email = `letter-fidelity-${Date.now()}@example.test`; try { expect((await admin.post(`${apiUrl}/users`, { headers: adminHeaders, data: { email, password, roles: [] } })).status()).toBe(200); } finally { await admin.dispose(); } const headers = await login(page.request, email); const sourceBefore = await (await page.request.get(`${apiUrl}/career/profile`)).json(); const companyResponse = await page.request.post(`${apiUrl}/companies`, { headers, data: { name: 'Nordlys Synthetic AS', source: 'User' } }); expect(companyResponse.status()).toBe(201); const company = await companyResponse.json(); const fixtures = []; for (const [mode, language, pageSize] of [['light', 'en', 'a4'], ['dark', 'nb', 'a4'], ['light', 'nb', 'letter'], ['dark', 'en', 'letter']] as const) { const fixture = `${mode}-${language}-${pageSize}`; const directory = path.join(output, fixture); await fs.mkdir(directory); const t = translations[language]; const sentence = language === 'nb' ? 'Dokumenterte pålitelige arbeidsflyter og tilgjengelig navigasjon for norske arbeidssøkere. ' : 'Documented reliable workflows and accessible navigation using reviewed source evidence. '; const text = Array.from({ length: 24 }, (_, index) => `B${String(index + 1).padStart(2, '0')} ${sentence.repeat(4)}`) .concat(['- S01 **Reviewed achievement** — æøå.', '- S02 *Verified collaboration* — ÆØÅ.', 'R99 Final reviewed letter content — Åse Østby.']).join('\n\n'); const documentSettings = { schemaVersion: 1, senderName: 'R01 Åse Østby', senderTitle: 'P01 Platform engineer', senderEmail: 'ase@example.test', senderPhone: '+47 12345678', senderLocation: 'Oslo, Norge', recipient: 'R03 Hiring team\nNordlys Synthetic AS\nOslo, Norge', subject: language === 'nb' ? 'R02 Søknad om plattformutvikler' : 'R02 Application for platform engineer', date: '2026-10-25', themeId: 'minimal', accentColor: '#126b55', bodyFont: 'Arial, Helvetica, sans-serif', baseFontSizePt: 11, lineHeight: 1.4, pageMarginMm: 20, pageSize }; const created = await page.request.post(`${apiUrl}/jobapplications`, { headers, data: { companyId: company.id, jobTitle: fixture, status: 'Saved', description: 'Synthetic letter fidelity fixture.' } }); expect(created.status()).toBe(201); const job = await created.json(); const letterUrl = `${apiUrl}/jobapplications/${job.id}/cover-letter`; const original = await (await page.request.get(letterUrl)).json(); const save = await page.request.put(letterUrl, { headers, data: { text, documentSettings, source: 'manual', expectedVersion: original.revision } }); expect(save.status()).toBe(200); const acknowledgement = await save.json(); // MariaDB stores datetime(6); compare subsequent reads with durable history timestamps. const saved = await (await page.request.get(letterUrl)).json(); expect(saved.revision).toBe(acknowledgement.revision); expect(saved.text).toBe(text); const preview = await page.request.post(`${letterUrl}/preview`, { headers, data: { text: saved.text, documentSettings: saved.documentSettings } }); expect(preview.status()).toBe(200); const rendered = await preview.json(); expect(rendered.html).toContain('class="page letter-paper"'); const pdf = await page.request.post(`${letterUrl}/export-pdf?expectedVersion=${saved.revision}`, { headers }); expect(pdf.status()).toBe(200); expect(pdf.headers()['content-type']).toContain('application/pdf'); const bytes = await pdf.body(); expect(bytes.subarray(0, 5).toString()).toBe('%PDF-'); await fs.writeFile(path.join(directory, 'export.pdf'), bytes); await fs.writeFile(path.join(directory, 'render.html'), rendered.html); expect(await (await page.request.get(letterUrl)).json()).toEqual(saved); expect((await page.request.put(`${apiUrl}/auth/preferences/language`, { headers, data: { language: language === 'nb' ? 'nb-NO' : 'en' } })).status()).toBe(204); await page.setViewportSize({ width: 1440, height: 1000 }); await page.goto(`/jobs/${job.id}?section=application&sub=letter&fidelityMode=${mode}&fidelityLanguage=${language}#materials`); await expect(page.locator('html')).toHaveAttribute('data-color-scheme', mode); const frame = page.getByTitle(t.letterDocumentPreview, { exact: true }); const previewWorkspace = frame.locator('..').locator('..').locator('..'); await expect(frame).toBeVisible(); await expect.poll(() => frame.evaluate(element => (element as HTMLIFrameElement).contentDocument?.documentElement.dataset.cvPreviewReady)).toBe('true'); const measured = await frame.evaluate(async element => { const doc = (element as HTMLIFrameElement).contentDocument!; const win = (element as HTMLIFrameElement).contentWindow!; await doc.fonts.ready; const geometry = doc.documentElement.dataset; const width = Number(geometry.cvPreviewWidth); const height = Number(geometry.cvPreviewHeight); const tokens = []; const walker = doc.createTreeWalker(doc.body, NodeFilter.SHOW_TEXT); let node; while ((node = walker.nextNode())) { if (['SCRIPT', 'STYLE'].includes(node.parentElement?.tagName || '')) continue; for (const match of (node.textContent || '').matchAll(/\b[SEBPR]\d{2}\b/g)) { const range = doc.createRange(); range.setStart(node, match.index!); range.setEnd(node, match.index! + match[0].length); const rect = range.getClientRects()[0]; const style = win.getComputedStyle(node.parentElement!); const index = Math.floor(rect.x / width); tokens.push({ token: match[0], x: rect.x - index * width, y: rect.y + index * height, page: index + 1, fontSize: style.fontSize, fontFamily: style.fontFamily, color: style.color }); } } return { width, height, contentHeight: height * Number(geometry.cvPreviewPages), pages: Number(geometry.cvPreviewPages), effectiveMargin: win.getComputedStyle(doc.documentElement).getPropertyValue('--cv-page-margin'), text: doc.body.innerText, bold: doc.querySelectorAll('strong').length, italic: doc.querySelectorAll('em').length, tokens }; }); expect(measured.pages).toBeGreaterThan(1); expect(measured.pages).toBeLessThan(15); expect(measured.tokens.map(token => token.token).sort()).toEqual(['R01', 'R02', 'R03', 'R99', 'P01', 'S01', 'S02', ...Array.from({ length: 24 }, (_, index) => `B${String(index + 1).padStart(2, '0')}`)].sort()); expect(measured.text).toContain('ase@example.test'); expect(measured.text).toContain('Nordlys Synthetic AS'); expect(measured.bold).toBeGreaterThan(0); expect(measured.italic).toBeGreaterThan(0); expect(measured.tokens.find(token => token.token === 'R99')!.page).toBe(measured.pages); for (let index = 0; index < measured.pages; index++) { if (index > 0) { await previewWorkspace.getByRole('button', { name: t.cvEditorNext, exact: true }).click(); await expect(previewWorkspace.getByText(t.letterPage.replace('{page}', String(index + 1)).replace('{count}', String(measured.pages)), { exact: true })).toBeVisible(); await expect.poll(() => frame.evaluate(element => (element as HTMLIFrameElement).contentDocument?.documentElement.dataset.cvPreviewReady)).toBe('true'); } const capture = await browser.newPage({ viewport: { width: Math.ceil(measured.width), height: Math.ceil(measured.height) } }); try { await capture.setContent(previewDocumentHtml(rendered.html, measured.height, index)); await expect.poll(() => capture.evaluate(() => document.documentElement.dataset.cvPreviewReady)).toBe('true'); await capture.screenshot({ path: path.join(directory, `preview-${index + 1}.png`) }); } finally { await capture.close(); } } const reachable = await frame.evaluate(element => { const doc = (element as HTMLIFrameElement).contentDocument!; const node = Array.from(doc.querySelectorAll('p')).find(item => item.textContent?.includes('R99'))!; const rect = node.getBoundingClientRect(); return rect.right > 0 && rect.left < Number(doc.documentElement.dataset.cvPreviewWidth) && rect.top >= 0 && rect.bottom <= Number(doc.documentElement.dataset.cvPreviewHeight); }); expect(reachable).toBe(true); const record = { fixture, previewPages: measured.pages, settings: saved.documentSettings, measured, savedRevision: saved.revision, pdfSha256: createHash('sha256').update(bytes).digest('hex'), htmlSha256: createHash('sha256').update(rendered.html).digest('hex'), browserVersion: browser.version(), language, mode }; await fs.writeFile(path.join(directory, 'measurements.json'), JSON.stringify(record, null, 2)); await page.screenshot({ path: path.join(directory, 'last-page-workspace.png') }); await info.attach(fixture, { body: JSON.stringify(record), contentType: 'application/json' }); fixtures.push(record); } expect(await (await page.request.get(`${apiUrl}/career/profile`)).json()).toEqual(sourceBefore); await fs.writeFile(path.join(output, 'measurements.json'), JSON.stringify({ browserVersion: browser.version(), fixtures }, null, 2), { flag: 'wx' }); });