fix(cv): bind letter proposals to current sources and preserve document choices

This commit is contained in:
cesnimda
2026-10-26 03:44:37 +01:00
parent 8948c988e2
commit 8979244576
21 changed files with 816 additions and 32 deletions
@@ -1,4 +1,5 @@
import { expect, test, type APIRequestContext } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
import fs from 'node:fs/promises';
import path from 'node:path';
import { createHash } from 'node:crypto';
@@ -35,6 +36,7 @@ test('cover letter native preview/PDF fidelity records four independent physical
const email = `letter-fidelity-${Date.now()}@example.test`;
try {
expect((await admin.post(`${apiUrl}/users`, { headers: adminHeaders, data: { email, password, roles: [] } })).status()).toBe(200);
expect((await admin.post(`${apiUrl}/jobapplications/2147483647/ai/history/2147483647/validate-source`, { headers: adminHeaders })).status()).toBe(404);
} finally { await admin.dispose(); }
const headers = await login(page.request, email);
const sourceBefore = await (await page.request.get(`${apiUrl}/career/profile`)).json();
@@ -51,7 +53,7 @@ test('cover letter native preview/PDF fidelity records four independent physical
: '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',
const documentSettings = { schemaVersion: 1, language: language === 'nb' ? 'nb-NO' : 'en', 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',
@@ -71,6 +73,7 @@ test('cover letter native preview/PDF fidelity records four independent physical
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(`<html lang="${documentSettings.language}">`);
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);
@@ -89,6 +92,7 @@ test('cover letter native preview/PDF fidelity records four independent physical
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');
expect(await frame.evaluate(element => (element as HTMLIFrameElement).contentDocument?.documentElement.lang)).toBe(documentSettings.language);
const measured = await frame.evaluate(async element => {
const doc = (element as HTMLIFrameElement).contentDocument!;
const win = (element as HTMLIFrameElement).contentWindow!;
@@ -171,6 +175,7 @@ test('cover letter workspace preserves keyboard drafts and localised controls ac
const created = await context.request.post(`${apiUrl}/jobapplications`, { headers, data: { companyId: company.id, jobTitle: 'Responsive reviewed letter', status: 'Saved' } });
expect(created.status()).toBe(201);
const job = await created.json();
expect((await context.request.post(`${apiUrl}/jobapplications/${job.id}/ai/history/2147483647/validate-source`, { headers })).status()).toBe(403);
const letterUrl = `${apiUrl}/jobapplications/${job.id}/cover-letter`;
const original = await (await context.request.get(letterUrl)).json();
const savedResponse = await context.request.put(letterUrl, { headers, data: { text: 'Original reviewed letter — æøå.', source: 'manual', expectedVersion: original.revision } });
@@ -199,6 +204,10 @@ test('cover letter workspace preserves keyboard drafts and localised controls ac
const workspace = page.getByRole('group', { name: t.workspaceCoverLetter, exact: true });
const body = workspace.getByRole('textbox', { name: t.jobDetailsCoverLetter, exact: true });
await expect(body).toHaveValue(saved.text);
const documentLanguage = workspace.getByRole('combobox', { name: t.letterDocumentLanguage });
await expect(documentLanguage).toBeVisible();
await documentLanguage.click();
await page.getByRole('option', { name: language === 'nb' ? t.languageNorwegianBokmal : t.languageEnglish, exact: true }).click();
const draft = `${width} ${language === 'nb' ? 'Ulagret søknadsbrev med gjennomgåtte fakta' : 'Unsaved letter with reviewed facts'} — æøå.`;
await body.focus(); await body.press('ControlOrMeta+A'); await body.pressSequentially(draft);
await expect(body).toHaveValue(draft);
@@ -220,6 +229,15 @@ test('cover letter workspace preserves keyboard drafts and localised controls ac
expect(geometry.root).toBeLessThanOrEqual(1); expect(geometry.app).toBeLessThanOrEqual(1);
const writePane = workspace.locator('[id$="-write-panel"]');
const previewPane = workspace.locator('[id$="-preview-panel"]');
const audit = async (state: string) => {
await workspace.evaluate(node => node.setAttribute('data-cv-ux-axe', 'letter'));
const result = await new AxeBuilder({ page }).include('[data-cv-ux-axe="letter"]').withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']).analyze();
const reportPath = info.outputPath(`letter-${width}-${state}-axe.json`);
await fs.writeFile(reportPath, JSON.stringify({ violations: result.violations, incomplete: result.incomplete }, null, 2));
await info.attach(`letter-${width}-${state}-axe`, { path: reportPath, contentType: 'application/json' });
expect(result.violations).toEqual([]);
};
if (width === 320 || width === 390 || width === 1440) await audit('write');
if (width < 1024) {
const tabs = workspace.getByRole('tablist', { name: t.assetsLetterWorkspace, exact: true });
const write = tabs.getByRole('tab', { name: t.assetsLetterWrite, exact: true });
@@ -228,6 +246,7 @@ test('cover letter workspace preserves keyboard drafts and localised controls ac
await write.focus(); await page.keyboard.press('ArrowRight'); await expect(preview).toBeFocused(); await page.keyboard.press('Enter');
await expect(preview).toHaveAttribute('aria-selected', 'true');
await expect(writePane).not.toBeVisible(); await expect(previewPane).toBeVisible();
if (width === 320 || width === 390) await audit('preview');
await expect(workspace.getByRole('textbox', { name: t.jobDetailsCoverLetter, exact: true })).toHaveCount(0);
for (let step = 0; step < 8; step++) {
await page.keyboard.press('Tab');