import { expect, test, type APIRequestContext, type Page, type TestInfo } from '@playwright/test'; import AxeBuilder from '@axe-core/playwright'; import { translations } from '../src/i18n/translations'; const apiUrl = 'http://localhost:5302/api'; 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(); const csrf = (await api.storageState()).cookies.find(cookie => cookie.name === 'XSRF-TOKEN')!.value; return { me, headers: { 'X-CSRF-TOKEN': csrf, 'X-Session-Context': me.sessionContext as string } }; } async function capture(page: Page, info: TestInfo, name: string) { const path = info.outputPath(`${name}.png`); await page.screenshot({ path, fullPage: true }); await info.attach(name, { path, contentType: 'image/png' }); } test('[PRES-W01 W02 W03 J05 E08 E09] native Job header/Overview retains actions, custom status, aliases and mobile progress keyboard access', async ({ page, playwright }, info) => { test.setTimeout(240000); const admin = await playwright.request.newContext(); const { headers: adminHeaders } = await login(admin, 'e2e@example.test'); const email = `job-overview-${Date.now()}@example.test`; expect((await admin.post(`${apiUrl}/users`, { headers: adminHeaders, data: { email, password, roles: [] } })).status()).toBe(200); const { headers } = await login(page.request, email); await page.route('**/*', route => ['localhost', '127.0.0.1'].includes(new URL(route.request().url()).hostname) ? route.continue() : route.abort()); const companyResponse = await page.request.post(`${apiUrl}/companies`, { headers, data: { name: 'Nordlys karrieremuligheter og langsiktig plattformutvikling æøå', source: 'direct' } }); expect(companyResponse.status()).toBe(201); const companyId = (await companyResponse.json()).id; const title = 'Senior utvikler og faglig rådgiver for samfunnskritiske plattformer og inkluderende digitale tjenester'; const jobData = { companyId, jobTitle: title, status: 'Interview', location: 'Tromsø · Oslo og fjernarbeid', salary: '700 000–900 000 NOK', deadline: '2026-12-14T12:00:00Z', dateApplied: '2026-10-02T12:00:00Z', followUpAt: '2026-10-12T12:00:00Z', jobUrl: 'https://example.test/jobs/platform', notes: 'Ask about ownership and sustainable delivery.', description: 'Build reliable services with .NET, SQL and Azure.', tags: 'Azure, .NET', source: 'direct', countryCode: 'NO' }; const created = await page.request.post(`${apiUrl}/jobapplications`, { headers, data: jobData }); expect(created.status()).toBe(201); const id = (await created.json()).id; const sparseResponse = await page.request.post(`${apiUrl}/jobapplications`, { headers, data: { companyId, jobTitle: 'Sparse application', status: 'Saved' } }); expect(sparseResponse.status()).toBe(201); const sparseId = (await sparseResponse.json()).id; const writes: string[] = []; page.on('request', request => { if (request.url().startsWith(apiUrl) && !['GET', 'OPTIONS'].includes(request.method())) writes.push(`${request.method()} ${new URL(request.url()).pathname}`); }); await page.goto(`/jobs/${id}`); await expect(page.getByRole('heading', { name: title, level: 2 })).toBeVisible(); await expect(page.getByRole('tablist', { name: translations.en.workspaceSections }).getByRole('tab')).toHaveCount(7); await expect(page.getByRole('link', { name: translations.en.workspaceOpenAdvert })).toHaveAttribute('href', jobData.jobUrl); await expect(page.getByRole('link', { name: translations.en.workspaceOpenAdvert })).toHaveAttribute('rel', 'noopener noreferrer'); expect(writes).toEqual([]); const edit = page.getByRole('button', { name: translations.en.workspaceEditApplication }); await edit.focus(); await page.keyboard.press('Enter'); await expect(page.getByRole('dialog')).toBeVisible(); await page.keyboard.press('Escape'); await expect(edit).toBeFocused(); for (const language of ['en', 'nb'] as const) for (const mode of ['light', 'dark'] as const) { const t = translations[language]; await page.evaluate(({ mode, language }) => { localStorage.setItem('jobtracker.themeMode', mode); localStorage.setItem('uiLanguage', language); }, { mode, language }); // Persist language while no app reads are in flight; theme uses its existing local owner. await page.goto('about:blank'); expect((await page.request.put(`${apiUrl}/auth/preferences/language`, { headers, data: { language: language === 'nb' ? 'nb-NO' : 'en' } })).status()).toBe(204); await page.goto(`/jobs/${id}?section=overview`); await expect(page.getByRole('heading', { name: title, level: 2 })).toBeVisible(); await expect(page.locator('html')).toHaveAttribute('data-color-scheme', mode); const summary = page.getByRole('region', { name: t.workspaceApplicationInfo, exact: true }); await expect(summary).toContainText(new Date(jobData.dateApplied).toLocaleDateString(language === 'nb' ? 'nb-NO' : 'en')); const progress = page.getByRole('region', { name: `${t.workspaceProgress}: ${t.statusInterview}` }); await expect(progress.locator('[aria-current=step]')).toHaveText(t.statusInterview); for (const [width, height, name] of [[1440, 900, 'desktop'], [768, 1024, 'tablet'], [390, 844, 'phone']] as const) { await page.setViewportSize({ width, height }); await expect.poll(() => page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1)).toBe(true); await capture(page, info, `job-overview-${name}-${mode}-${language}`); } await progress.focus(); expect(await progress.evaluate(element => parseFloat(getComputedStyle(element).outlineWidth))).toBeGreaterThanOrEqual(3); await progress.evaluate(element => { element.scrollLeft = 0; }); await page.keyboard.press('ArrowRight'); await expect.poll(() => progress.evaluate(element => element.scrollLeft)).toBeGreaterThan(0); await capture(page, info, `job-progress-focus-${mode}-${language}`); if (language === 'en') { const scan = await new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']).analyze(); await info.attach(`job-overview-${mode}-axe`, { body: JSON.stringify({ violations: scan.violations, incomplete: scan.incomplete }), contentType: 'application/json' }); expect(scan.violations).toEqual([]); } } for (const width of [320, 1920]) { await page.setViewportSize({ width, height: 900 }); await expect.poll(() => page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1)).toBe(true); await capture(page, info, `job-overview-${width}-dark-nb`); } // Existing native status write, not a new header status API or inferred progression. await page.goto('about:blank'); expect((await page.request.put(`${apiUrl}/jobapplications/${id}`, { headers, data: { ...jobData, status: 'Faglig vurdering' } })).status()).toBe(204); await page.goto(`/applications/${id}?section=overview`); const custom = page.getByRole('region', { name: `${translations.nb.workspaceProgress}: Faglig vurdering` }); await expect(custom).toContainText(translations.nb.workspaceCustomProgress); await expect(custom.locator('[aria-current]')).toHaveCount(0); await capture(page, info, 'job-custom-status-dark-nb'); await page.goto(`/jobs/${sparseId}`); await expect(page.getByRole('heading', { name: 'Sparse application' })).toBeVisible(); await expect(page.getByRole('region', { name: translations.nb.workspaceApplicationInfo }).getByText('—', { exact: true })).toHaveCount(3); await expect(page.getByRole('link', { name: translations.nb.workspaceOpenAdvert })).toHaveCount(0); await capture(page, info, 'job-sparse-dark-nb'); const foreignEmail = `job-overview-foreign-${Date.now()}@example.test`; expect((await admin.post(`${apiUrl}/users`, { headers: adminHeaders, data: { email: foreignEmail, password, roles: [] } })).status()).toBe(200); const foreign = await playwright.request.newContext(); await login(foreign, foreignEmail); expect((await foreign.get(`${apiUrl}/jobapplications/${id}/workspace`)).status()).toBe(404); await foreign.dispose(); await admin.dispose(); }); test('[PRES-W01 W02 E08] Job Overview loading, failed read, retry and unavailable resource remain explicit', async ({ page }, info) => { await login(page.request, 'e2e@example.test'); let release: (() => void) | undefined; let failed = true; const hold = new Promise(resolve => { release = resolve; }); await page.route(`${apiUrl}/jobapplications/2147483646/workspace`, async route => { await hold; return route.fulfill({ status: failed ? 503 : 404, json: { message: failed ? 'Temporary workspace failure' : 'Not Found' } }); }); await page.setViewportSize({ width: 390, height: 844 }); await page.goto('/jobs/2147483646'); await expect(page.getByRole('progressbar').first()).toBeVisible(); await capture(page, info, 'job-loading'); release!(); await expect(page.getByRole('alert').filter({ hasText: 'Could not open this application.' })).toBeVisible(); await capture(page, info, 'job-load-error'); failed = false; await page.getByRole('button', { name: 'Retry', exact: true }).click(); await expect(page.getByRole('alert').filter({ hasText: 'Not Found' })).toBeVisible(); await expect(page.getByRole('button', { name: 'Retry', exact: true })).toHaveCount(0); await expect(page.getByRole('button', { name: 'Back to applications' })).toBeVisible(); await capture(page, info, 'job-unavailable'); });