test(cv-qa): qualify long imported PDFs and cross-platform fidelity

This commit is contained in:
cesnimda
2026-10-27 05:14:23 +01:00
parent be6786095a
commit e416420e79
4 changed files with 198 additions and 0 deletions
@@ -0,0 +1,98 @@
import { expect, test, type APIRequestContext } from '@playwright/test';
import { createHash } from 'node:crypto';
import { readFile } from 'node:fs/promises';
import path from 'node:path';
import type { StructuredCvProfile } from '../src/profileCv';
import type { CvVariant } from '../src/cvBuilder';
import { translations } from '../src/i18n/translations';
const api = 'http://localhost:5302/api';
const password = 'E2ePassword123!';
const t = translations.en;
async function login(request: APIRequestContext, email: string) {
expect((await request.post(`${api}/auth/login`, { data: { email, password } })).status()).toBe(200);
const me = await (await request.get(`${api}/auth/me`)).json();
return { 'X-CSRF-TOKEN': (await request.storageState()).cookies.find(cookie => cookie.name === 'XSRF-TOKEN')!.value,
'X-Session-Context': me.sessionContext as string };
}
test('real long Docling import preserves twelve jobs, dense skills and qualifications across three PDF templates', async ({ page, context, playwright }, info) => {
const root = path.resolve(process.env.CV_QA_LONG_FIXTURE_ROOT!);
const expected = JSON.parse(await readFile(path.join(root, 'expected.json'), 'utf8')) as {
name: string; employers: string[]; titles: string[]; details: string[]; qualifications: string[]; skills: string[]; markers: string[];
};
const admin = await playwright.request.newContext();
const adminHeaders = await login(admin, 'e2e@example.test');
const email = `long-import-${Date.now()}@example.test`;
expect((await admin.post(`${api}/users`, { headers: adminHeaders, data: { email, password, roles: ['Premium'] } })).status()).toBe(200);
await admin.dispose();
const headers = await login(page.request, email);
await context.addInitScript(() => { localStorage.setItem('uiLanguage', 'en'); localStorage.setItem('jobtracker.themeMode', 'light'); });
await context.route('**/*', route => {
const target = new URL(route.request().url());
return ['localhost', '127.0.0.1'].includes(target.hostname) || ['data:', 'blob:', 'about:'].includes(target.protocol) ? route.continue() : route.abort();
});
await page.setViewportSize({ width: 1440, height: 1000 });
await page.emulateMedia({ reducedMotion: 'reduce' });
await page.goto('/career?section=import');
const file = path.join(root, 'long-labelled-integration.docx');
const sourceHash = createHash('sha256').update(await readFile(file)).digest('hex');
await page.locator('input[type=file][accept*=".pdf"]').setInputFiles(file);
await expect.poll(async () => (await (await page.request.get(`${api}/profile-cv/runs`)).json())[0]?.status, { timeout: 90_000 }).toBe('pending_review');
const run = (await (await page.request.get(`${api}/profile-cv/runs`)).json())[0];
expect(run.parserVersion).toBe('docling-v1');
expect(run.operation.progressStage).toBe('cv_review_ready');
expect((await (await page.request.get(`${api}/career/profile`)).json()).revision).toBe(0);
const source = await page.request.get(`${api}/profile-cv/runs/${run.id}/source`);
expect(source.status()).toBe(200);
expect(createHash('sha256').update(await source.body()).digest('hex')).toBe(sourceHash);
await page.reload();
const review = page.getByRole('region', { name: t.cvImportReviewPanel, exact: true });
await expect(review.getByRole('button', { name: t.cvImportApplyReviewed, exact: true })).toBeDisabled();
const groups = review.getByRole('button', { name: t.cvImportUseGroup, exact: true });
await expect(groups.first()).toBeVisible();
for (const group of await groups.all()) await group.click();
await review.getByRole('button', { name: t.cvImportApplyReviewed, exact: true }).click();
await expect(page.getByText(t.cvImportSaved, { exact: true })).toBeVisible();
const snapshot = await (await page.request.get(`${api}/career/profile`)).json() as { revision: number; profile: StructuredCvProfile };
expect(snapshot.revision).toBe(1);
expect(snapshot.profile.contact.fullName).toBe(expected.name);
expect(snapshot.profile.jobs).toHaveLength(12);
expect(snapshot.profile.jobs.map(job => job.company)).toEqual(expected.employers);
expect(snapshot.profile.jobs.map(job => job.title)).toEqual(expected.titles);
expect(snapshot.profile.jobs.map(job => job.start)).toEqual(expected.employers.map((_, index) => String(2011 + index)));
expect(snapshot.profile.jobs.slice(0, 11).map(job => job.end)).toEqual(expected.employers.slice(0, 11).map((_, index) => String(2012 + index)));
expect(snapshot.profile.jobs[11].isCurrent).toBe(true);
expect(snapshot.profile.jobs.flatMap(job => job.bullets)).toEqual(expected.details);
snapshot.profile.jobs.forEach((job, index) => expect(job.bullets).toEqual(expected.details.slice(index * 3, index * 3 + 3)));
expect(snapshot.profile.education.map(education => education.qualification)).toEqual(expected.qualifications);
expect(snapshot.profile.skills).toEqual(expected.skills);
expect((await (await page.request.get(`${api}/career/profile/versions`)).json())).toHaveLength(1);
const preserved: CvVariant[] = [];
for (const [theme, pageSize] of [['minimal', 'a4'], ['modern', 'letter'], ['technical', 'a4']] as const) {
const response = await page.request.post(`${api}/cv/variants`, { headers, data: { name: `Long import ${theme}`, settings: { themeId: theme, pageSize } } });
expect(response.status()).toBe(200);
const cv = await response.json() as CvVariant;
expect(cv.settings.themeId).toBe(theme);
expect(cv.settings.pageSize).toBe(pageSize);
await page.goto(`/career/builder/${cv.id}`);
await expect(page.getByLabel(t.cvEditorName, { exact: true })).toHaveValue(cv.name);
await expect(page.getByTitle(t.cvEditorPreviewPage.replace('{page}', '1'), { exact: true })).toBeVisible();
const download = page.waitForEvent('download');
await page.getByRole('button', { name: t.cvEditorDownloadPdf, exact: true }).click();
const pdf = await download;
const pdfPath = info.outputPath(`long-import-${theme}-${pageSize}.pdf`);
await pdf.saveAs(pdfPath);
expect(await pdf.failure()).toBeNull();
const bytes = await readFile(pdfPath);
expect(bytes.subarray(0, 5).toString()).toBe('%PDF-');
await info.attach('pdf-semantic-oracle-input', { body: JSON.stringify({ pdfPath, sha256: createHash('sha256').update(bytes).digest('hex').toUpperCase(),
expectedLiteralMarkers: [...expected.markers, ...expected.employers.map((_, index) => String(2011 + index)), 'Present'], theme, pageSize, minimumPages: 2, sourceHash,
boundary: 'Real source import, explicit automated fixture approval and downloaded bytes. Root independently checks every marker, page bounds and rendered output.' }), contentType: 'application/json' });
await info.attach(`long-${theme}-pdf`, { path: pdfPath, contentType: 'application/pdf' });
await page.screenshot({ path: info.outputPath(`long-${theme}-preview.png`), fullPage: true, animations: 'disabled' });
expect(await (await page.request.get(`${api}/career/profile`)).json()).toEqual(snapshot);
for (const prior of preserved) expect(await (await page.request.get(`${api}/cv/variants/${prior.id}`)).json()).toEqual(prior);
preserved.push(await (await page.request.get(`${api}/cv/variants/${cv.id}`)).json() as CvVariant);
}
});
@@ -0,0 +1,4 @@
import { defineConfig } from '@playwright/test';
import imports from './playwright.cv-import.config';
if (!process.env.CV_QA_LONG_FIXTURE_ROOT) throw new Error('Set CV_QA_LONG_FIXTURE_ROOT to the generated synthetic long document.');
export default defineConfig({ ...imports, testMatch: 'cv-import-long.spec.ts', timeout: 240_000 });