Files
jobtrackingapp/job-tracker-ui/import-tests/cv-import-docling.spec.ts
T

82 lines
6.1 KiB
TypeScript

import { expect, test, type APIRequestContext } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
import path from 'node:path';
import { translations } from '../src/i18n/translations';
const url = 'http://localhost:5302/api';
const password = 'E2ePassword123!';
async function login(api: APIRequestContext, email: string) {
expect((await api.post(`${url}/auth/login`, { data: { email, password } })).status()).toBe(200);
const me = await (await api.get(`${url}/auth/me`)).json();
return { 'X-CSRF-TOKEN': (await api.storageState()).cookies.find(c => c.name === 'XSRF-TOKEN')!.value, 'X-Session-Context': me.sessionContext as string };
}
for (const scenario of [
{ language: 'en', mode: 'light', width: 1440, file: 'dev-en-simple.pdf' },
{ language: 'nb', mode: 'dark', width: 390, file: 'dev-nb-education.docx' },
] as const) {
test(`real Docling ${scenario.language}/${scenario.mode}: upload, refresh, review, correct, durable profile, editor and PDF`, async ({ page, context, playwright }, info) => {
await context.route('**/*', route => ['localhost', '127.0.0.1'].includes(new URL(route.request().url()).hostname) ? route.continue() : route.abort());
const t = translations[scenario.language];
const admin = await playwright.request.newContext();
const adminHeaders = await login(admin, 'e2e@example.test');
const email = `cv-import-${Date.now()}-${scenario.language}@example.test`;
expect((await admin.post(`${url}/users`, { headers: adminHeaders, data: { email, password, roles: ['Premium'] } })).status()).toBe(200);
await admin.dispose();
const headers = await login(page.request, email);
expect((await page.request.put(`${url}/auth/preferences/language`, { headers, data: { language: scenario.language === 'nb' ? 'nb-NO' : 'en' } })).status()).toBe(204);
await context.addInitScript(({ language, mode }) => { localStorage.setItem('uiLanguage', language); localStorage.setItem('jobtracker.themeMode', mode); }, scenario);
await page.setViewportSize({ width: scenario.width, height: 900 });
const created = await page.request.post(`${url}/cv/variants`, { headers, data: { name: 'Preserved import CV', settings: { themeId: 'minimal' } } });
expect(created.status()).toBe(200);
const cv = await created.json();
const originalVariant = await (await page.request.get(`${url}/cv/variants/${cv.id}`)).json();
await page.goto('/career?section=import');
await expect(page.getByRole('button', { name: t.profileUploadCv, exact: true })).toBeEnabled();
await page.locator('input[type=file][accept*=".pdf"]').setInputFiles(path.resolve(process.env.CV_IMPORT_FIXTURE_ROOT || '../.artifacts/cv-import/corpus-02', scenario.file));
await expect.poll(async () => (await (await page.request.get(`${url}/profile-cv/runs`)).json())[0]?.status, { timeout: 90_000 }).toBe('pending_review');
expect((await (await page.request.get(`${url}/career/profile`)).json()).revision).toBe(0);
const run = (await (await page.request.get(`${url}/profile-cv/runs`)).json())[0];
expect(run.parserVersion).toBe('docling-v1');
expect(run.operation.progressStage).toBe('cv_review_ready');
await page.reload();
const review = page.getByRole('region', { name: t.cvImportReviewPanel, exact: true });
await expect(review.getByRole('button', { name: t.cvImportApplyReviewed })).toBeDisabled();
const source = await page.request.get(`${url}/profile-cv/runs/${run.id}/source`);
expect(source.status()).toBe(200);
expect(source.headers()['cache-control']).toContain('no-store');
const stranger = await playwright.request.newContext();
await login(stranger, 'e2e@example.test');
expect((await stranger.get(`${url}/profile-cv/runs/${run.id}/source`)).status()).toBe(404);
expect((await stranger.get(`${url}/profile-cv/runs/${run.id}/review`)).status()).toBe(404);
await stranger.dispose();
const groups = review.getByRole('button', { name: t.cvImportUseGroup, exact: true });
await expect(groups.first()).toBeVisible();
for (const button of await groups.all()) await button.click();
const nameChoice = review.getByRole('combobox', { name: t.cvImportDecision.replace('{field}', t.profileCvContactFullName), exact: true });
await nameChoice.selectOption('correct');
await review.getByRole('textbox', { name: t.cvImportCorrection.replace('{field}', t.profileCvContactFullName), exact: true }).fill('Reviewed Åse Example');
const axe = await new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']).analyze();
await info.attach('review-axe', { body: JSON.stringify({ violations: axe.violations, incomplete: axe.incomplete }), contentType: 'application/json' });
expect(axe.violations).toEqual([]);
await page.screenshot({ path: info.outputPath('review.png'), fullPage: true });
await review.getByRole('button', { name: t.cvImportApplyReviewed, exact: true }).click();
await expect(page.getByText(t.cvImportSaved, { exact: true })).toBeVisible();
const profile = await (await page.request.get(`${url}/career/profile`)).json();
expect(profile.revision).toBe(1);
expect(profile.profile.contact.fullName).toBe('Reviewed Åse Example');
expect((await (await page.request.get(`${url}/career/profile/versions`)).json())).toHaveLength(1);
expect(await (await page.request.get(`${url}/cv/variants/${cv.id}`)).json()).toEqual(originalVariant);
await page.goto(`/career/builder/${cv.id}`);
await expect(page.getByLabel(t.cvEditorName, { exact: true })).toHaveValue('Preserved import CV');
if (scenario.width < 768) await page.getByRole('button', { name: t.cvEditorPreview, exact: true }).click();
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 file = await download;
expect(file.suggestedFilename()).toMatch(/\.pdf$/);
await file.saveAs(info.outputPath(`imported-${scenario.language}.pdf`));
await expect.poll(async () => (await (await page.request.get(`${url}/career/profile`)).json()).revision).toBe(1);
});
}