Files
jobtrackingapp/job-tracker-ui/e2e/job-overview-presentation.spec.ts
T
2026-10-07 01:58:31 +02:00

128 lines
9.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<void>(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');
});