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) => { 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); }); }