456 lines
27 KiB
TypeScript
456 lines
27 KiB
TypeScript
import AxeBuilder from "@axe-core/playwright";
|
|
import { expect, test as base, type APIRequestContext, type APIResponse, type Locator, type Page, type TestInfo } from "@playwright/test";
|
|
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(c => c.name === "XSRF-TOKEN")!.value;
|
|
return { "X-CSRF-TOKEN": csrf, "X-Session-Context": me.sessionContext as string };
|
|
}
|
|
|
|
const test = base.extend<{ owner: { headers: Record<string, string>; jobId: number; companyId: number; cvId: number } }>({
|
|
owner: async ({ playwright, page, context }, provide) => {
|
|
const admin = await playwright.request.newContext();
|
|
try {
|
|
const adminHeaders = await login(admin, "e2e@example.test");
|
|
const email = `p500-${Date.now()}-${Math.random().toString(36).slice(2)}@example.test`;
|
|
expect((await admin.post(`${apiUrl}/users`, { headers: adminHeaders, data: { email, password, roles: ["Admin"] } })).status()).toBe(200);
|
|
const headers = await login(page.request, email);
|
|
await context.route("**/*", route => {
|
|
const url = new URL(route.request().url());
|
|
return ["localhost", "127.0.0.1"].includes(url.hostname) || ["data:", "blob:", "about:"].includes(url.protocol) ? route.continue() : route.abort();
|
|
});
|
|
const company = await page.request.post(`${apiUrl}/companies`, { headers, data: { name: "Nordlys beredskap æøå" } });
|
|
expect(company.status()).toBe(201);
|
|
const companyId = (await company.json()).id;
|
|
const job = await page.request.post(`${apiUrl}/jobapplications`, { headers, data: { companyId, jobTitle: "Tilgjengelig plattformutvikler", status: "Saved" } });
|
|
expect(job.ok()).toBe(true);
|
|
expect((await page.request.put(`${apiUrl}/career/profile`, { headers, data: { profile: { version: "1", contact: { fullName: "Åse Eksempel" }, skills: ["Norsk", "C#"] } } })).status()).toBe(200);
|
|
const cv = await page.request.post(`${apiUrl}/cv/variants`, { headers, data: { name: "Readiness CV" } });
|
|
expect(cv.status()).toBe(200);
|
|
await provide({ headers, jobId: (await job.json()).id, companyId, cvId: (await cv.json()).id });
|
|
} finally { await admin.dispose(); }
|
|
},
|
|
});
|
|
|
|
async function settled(page: Page) {
|
|
await page.evaluate(async () => {
|
|
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
await Promise.all(document.getAnimations().filter(a => a.playState === "running" && Number.isFinite(a.effect?.getComputedTiming().endTime)).map(a => a.finished.catch(() => undefined)));
|
|
});
|
|
}
|
|
async function capture(page: Page, info: TestInfo, state: string) {
|
|
await settled(page);
|
|
const path = info.outputPath(`${state}.png`);
|
|
await page.screenshot({ path });
|
|
await info.attach(state, { path, contentType: "image/png" });
|
|
}
|
|
async function scan(page: Page, info: TestInfo, state: string, selector?: string) {
|
|
await settled(page);
|
|
let axe = new AxeBuilder({ page }).withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"]);
|
|
if (selector) axe = axe.include(selector);
|
|
const result = await axe.analyze();
|
|
await info.attach(`${state}-axe`, { body: JSON.stringify({ violations: result.violations, incomplete: result.incomplete }), contentType: "application/json" });
|
|
expect(result.violations).toEqual([]);
|
|
}
|
|
async function exposed(target: Locator) {
|
|
await expect.poll(() => target.evaluate(node => {
|
|
const r = node.getBoundingClientRect();
|
|
return r.left >= 0 && r.right <= innerWidth + 1 && r.top >= 0 && r.bottom <= innerHeight + 1 && node.contains(document.elementFromPoint(r.x + r.width / 2, r.y + r.height / 2));
|
|
})).toBe(true);
|
|
}
|
|
async function ring(target: Locator) {
|
|
await expect.poll(() => target.evaluate(node => ({ width: getComputedStyle(node).outlineWidth, style: getComputedStyle(node).outlineStyle }))).toEqual({ width: "3px", style: "solid" });
|
|
}
|
|
|
|
for (const mode of ["light", "dark"] as const) for (const language of ["en", "nb"] as const) {
|
|
const suffix = `${mode}-${language}`;
|
|
test.describe(suffix, () => {
|
|
test.beforeEach(async ({ page, owner }) => {
|
|
await page.addInitScript(({ mode, language }) => { localStorage.setItem("jobtracker.themeMode", mode); localStorage.setItem("uiLanguage", language); }, { mode, language });
|
|
expect((await page.request.put(`${apiUrl}/auth/preferences/language`, { headers: owner.headers, data: { language: language === "nb" ? "nb-NO" : "en" } })).ok()).toBe(true);
|
|
});
|
|
|
|
test(`[P5-00 PRES-E07 E08 A03] ${suffix} mobile labels, focus and admin reflow`, async ({ page }, info) => {
|
|
test.setTimeout(120_000);
|
|
const t = translations[language];
|
|
await page.setViewportSize({ width: 1440, height: 900 });
|
|
await page.goto("/settings");
|
|
await expect(page.getByRole("heading", { level: 1, name: t.settings })).toBeVisible();
|
|
await page.getByRole("button", { name: t.collapseSidebar }).click();
|
|
await page.reload();
|
|
await expect(page.getByRole("button", { name: t.expandSidebar })).toBeVisible();
|
|
for (const width of [1440, 1024, 1023, 900, 768, 767, 390, 320]) {
|
|
await page.setViewportSize({ width, height: width < 360 ? 740 : width < 768 ? 844 : 900 });
|
|
await settled(page);
|
|
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1)).toBe(true);
|
|
const header = page.getByRole("banner");
|
|
await exposed(page.getByRole("heading", { level: 1, name: t.settings }));
|
|
const controls = header.locator("button");
|
|
for (const control of await controls.all()) if (await control.isVisible()) await exposed(control);
|
|
if (width >= 1024) {
|
|
await expect(header.getByRole("button", { name: t.expandSidebar, exact: true })).toHaveCount(1);
|
|
await expect(header.getByRole("button", { name: t.openNavigation, exact: true })).toHaveCount(0);
|
|
await expect(page.getByRole("button", { name: t.dashboard, exact: true })).toHaveCount(1);
|
|
await exposed(page.getByRole("button", { name: t.dashboard, exact: true }));
|
|
await exposed(page.getByTestId("admin-version-badge"));
|
|
} else {
|
|
await expect(header.getByRole("button", { name: t.openNavigation, exact: true })).toHaveCount(1);
|
|
await expect(header.getByRole("button", { name: t.expandSidebar, exact: true })).toHaveCount(0);
|
|
await expect(page.getByRole("button", { name: t.dashboard, exact: true })).toHaveCount(0);
|
|
await expect(page.getByTestId("admin-version-badge")).toHaveCount(0);
|
|
const user = page.getByRole("button", { name: t.user, exact: true });
|
|
await user.click();
|
|
await expect(page.getByRole("menu")).toContainText(t.applicationVersion.replace("{version}", "e2e-verification"));
|
|
await expect(page.getByRole("menu")).toContainText("e2e1234");
|
|
if (width === 320) {
|
|
await expect(page.getByRole("menuitem", { name: t.profileMenu, exact: true })).toBeFocused();
|
|
await capture(page, info, `account-metadata-${suffix}`);
|
|
await scan(page, info, `account-metadata-${suffix}`, '[role="menu"]');
|
|
}
|
|
await page.keyboard.press("Escape");
|
|
await expect(user).toBeFocused();
|
|
if ([768, 900, 1023].includes(width)) {
|
|
const navigation = header.getByRole("button", { name: t.openNavigation, exact: true });
|
|
await navigation.focus();
|
|
await navigation.press("Enter");
|
|
const first = page.getByRole("dialog", { name: t.navigation }).getByRole("button", { name: t.dashboard, exact: true });
|
|
await expect(first).toBeFocused();
|
|
await exposed(first);
|
|
await page.keyboard.press("Escape");
|
|
await expect(navigation).toBeFocused();
|
|
}
|
|
}
|
|
if ([1440, 900, 768, 320].includes(width)) await capture(page, info, `admin-${width}-${suffix}`);
|
|
}
|
|
const trigger = page.getByRole("button", { name: t.openNavigation });
|
|
await trigger.focus();
|
|
await trigger.press("Enter");
|
|
const drawer = page.getByRole("dialog");
|
|
await expect(drawer).toHaveAccessibleName(/.+/);
|
|
const dashboard = drawer.getByRole("button", { name: t.dashboard, exact: true });
|
|
await expect(dashboard.getByText(t.dashboard, { exact: true })).toBeVisible();
|
|
// Opening the modal sets a meaningful entry; Tab traverses its controls.
|
|
await expect(dashboard).toBeFocused();
|
|
await exposed(dashboard);
|
|
await ring(dashboard);
|
|
await expect(drawer.getByRole("button", { name: t.settings, exact: true })).toHaveAttribute("aria-current", "page");
|
|
const links = drawer.getByRole("button");
|
|
for (let i = 0; i < await links.count(); i++) {
|
|
await page.keyboard.press("Tab");
|
|
expect(await drawer.evaluate(node => node.contains(document.activeElement))).toBe(true);
|
|
}
|
|
await links.last().focus();
|
|
await links.last().scrollIntoViewIfNeeded();
|
|
await exposed(links.last());
|
|
await dashboard.focus();
|
|
await dashboard.scrollIntoViewIfNeeded();
|
|
await capture(page, info, `drawer-${suffix}`);
|
|
await scan(page, info, `drawer-${suffix}`, '[role="dialog"]');
|
|
await page.keyboard.press("Escape");
|
|
await expect(trigger).toBeFocused();
|
|
await exposed(trigger);
|
|
await trigger.press("Enter");
|
|
await dashboard.press("Enter");
|
|
await expect(page).toHaveURL(/\/dashboard$/);
|
|
await expect(drawer).not.toBeVisible();
|
|
await page.setViewportSize({ width: 1440, height: 900 });
|
|
await expect(page.getByRole("button", { name: t.expandSidebar })).toBeVisible();
|
|
await page.getByRole("button", { name: t.expandSidebar }).click();
|
|
await page.setViewportSize({ width: 390, height: 844 });
|
|
await trigger.click();
|
|
await expect(dashboard).toBeFocused();
|
|
await exposed(dashboard);
|
|
// A hidden temporary drawer must not keep desktop content aria-hidden or trap focus.
|
|
await page.setViewportSize({ width: 1024, height: 900 });
|
|
await expect(drawer).toHaveCount(0);
|
|
const desktopToggle = page.getByRole("button", { name: t.collapseSidebar, exact: true });
|
|
await desktopToggle.press("Tab");
|
|
await page.keyboard.press("Shift+Tab");
|
|
await expect(desktopToggle).toBeFocused();
|
|
await ring(desktopToggle);
|
|
await expect(page.getByRole("button", { name: t.dashboard, exact: true })).toHaveCount(1);
|
|
await page.setViewportSize({ width: 390, height: 844 });
|
|
await expect(drawer).toHaveCount(0);
|
|
await trigger.press("Enter");
|
|
await expect(dashboard).toBeFocused();
|
|
await page.keyboard.press("Escape");
|
|
await expect(trigger).toBeFocused();
|
|
});
|
|
|
|
test(`[P5-00 PRES-E07 J03 J08] ${suffix} command list, keyboard and no-write actions`, async ({ page, owner }, info) => {
|
|
test.setTimeout(90_000);
|
|
const t = translations[language];
|
|
await page.setViewportSize({ width: language === "nb" ? 390 : 1440, height: 900 });
|
|
await page.goto("/settings");
|
|
await expect(page.getByRole("heading", { level: 1, name: t.settings })).toBeVisible();
|
|
const trigger = page.getByRole("button", { name: new RegExp(t.quickSearch) });
|
|
await trigger.focus();
|
|
await trigger.press("Enter");
|
|
const dialog = page.getByRole("dialog", { name: t.quickSearch });
|
|
const input = dialog.getByRole("textbox", { name: t.quickSearch });
|
|
await expect(input).toBeFocused();
|
|
await capture(page, info, `commands-${suffix}`);
|
|
await scan(page, info, `commands-${suffix}`, '[role="dialog"]');
|
|
await page.keyboard.press("Tab");
|
|
await expect(dialog.getByRole("button", { name: new RegExp(t.goToDashboard) })).toBeFocused();
|
|
await page.keyboard.press("Escape");
|
|
await expect(trigger).toBeFocused();
|
|
await page.keyboard.press("Control+k");
|
|
await input.fill("Nordlys");
|
|
const job = dialog.getByRole("button", { name: /Nordlys.*Tilgjengelig/ });
|
|
await expect(job).toBeVisible();
|
|
await job.focus();
|
|
await job.press("Enter");
|
|
await expect(page).toHaveURL(new RegExp(`/jobs/${owner.jobId}$`));
|
|
await page.keyboard.press("Control+k");
|
|
await input.fill("Nordlys");
|
|
const company = dialog.getByRole("button", { name: new RegExp(`^Nordlys beredskap æøå ${t.openCompanies}$`) });
|
|
await expect(company).toBeVisible();
|
|
await company.press("Enter");
|
|
await expect(page).toHaveURL(new RegExp(`/companies\\?edit=${owner.companyId}$`));
|
|
// Wait for the route's asynchronous record read before dismissing its modal.
|
|
await expect(dialog).not.toBeVisible();
|
|
await expect(page.getByRole("dialog", { name: t.companiesEdit })).toBeVisible();
|
|
await page.keyboard.press("Escape");
|
|
await expect(page.getByRole("dialog")).not.toBeVisible();
|
|
await page.keyboard.press("Control+k");
|
|
await input.fill("no-such-readiness-command");
|
|
await expect(dialog.getByText(t.noMatchingCommands)).toBeVisible();
|
|
await scan(page, info, `commands-empty-${suffix}`, '[role="dialog"]');
|
|
await input.fill("");
|
|
await dialog.getByRole("button", { name: new RegExp(t.addNewJob) }).press("Enter");
|
|
await expect(dialog).not.toBeVisible();
|
|
await expect(page.getByRole("dialog")).toBeVisible();
|
|
expect((await (await page.request.get(`${apiUrl}/jobapplications`)).json()).total).toBe(1);
|
|
});
|
|
|
|
test(`[P5-02 PRES-E07 E08 E09 S02] ${suffix} utility search and notification states`, async ({ page }, info) => {
|
|
test.setTimeout(90_000);
|
|
const t = translations[language];
|
|
await page.setViewportSize({ width: language === "nb" ? 390 : 1440, height: language === "nb" ? 844 : 900 });
|
|
await page.goto("/settings");
|
|
await expect(page.getByRole("heading", { level: 1, name: t.settings })).toBeVisible();
|
|
|
|
await page.route("**/api/jobapplications?**", async route => {
|
|
if (new URL(route.request().url()).searchParams.get("q") === "outage") {
|
|
await route.fulfill({ status: 503, contentType: "application/json", body: '{"message":"Search is temporarily unavailable"}' });
|
|
} else await route.continue();
|
|
});
|
|
const trigger = page.getByRole("button", { name: new RegExp(t.quickSearch) });
|
|
await trigger.focus();
|
|
await trigger.press("Enter");
|
|
const dialog = page.getByRole("dialog", { name: t.quickSearch });
|
|
const input = dialog.getByRole("textbox", { name: t.quickSearch });
|
|
await input.fill("outage");
|
|
await expect(dialog.getByRole("alert")).toBeVisible();
|
|
await expect(dialog.getByText(t.noMatchingCommands)).toHaveCount(0);
|
|
await capture(page, info, `utility-error-${suffix}`);
|
|
await scan(page, info, `utility-error-${suffix}`, '[role="dialog"]');
|
|
await input.fill("");
|
|
await input.press("End");
|
|
await input.press("Home");
|
|
await input.press("ArrowDown");
|
|
await input.press("Enter");
|
|
await expect(page).toHaveURL(/\/jobs$/);
|
|
|
|
const notification = {
|
|
id: "22222222-2222-2222-2222-222222222222", operationId: null, kind: "operation_failed",
|
|
title: "CV export failed", message: "Review the operation and try again.", linkPath: "/operations",
|
|
createdAtUtc: "2026-08-15T12:00:00Z", readAtUtc: null,
|
|
};
|
|
await page.route("**/api/notifications?limit=10", route => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify([notification]) }));
|
|
await page.route("**/api/notifications/*/read", route => route.fulfill({ status: 200, body: "{}" }));
|
|
await page.route("**/api/notifications/*", route => route.request().method() === "DELETE"
|
|
? route.fulfill({ status: 204, body: "" }) : route.continue());
|
|
await page.getByRole("button", { name: t.notifications, exact: true }).click();
|
|
await expect(page.getByText("CV export failed")).toBeVisible();
|
|
await expect(page.getByText(t.notificationsUnread)).toBeVisible();
|
|
await capture(page, info, `utility-notifications-${suffix}`);
|
|
await scan(page, info, `utility-notifications-${suffix}`, ".MuiPopover-paper");
|
|
await page.getByRole("button", { name: t.notificationsMarkRead }).click();
|
|
await expect(page.getByRole("button", { name: t.notificationsMarkRead })).toHaveCount(0);
|
|
await page.getByRole("button", { name: t.notificationsDismiss }).click();
|
|
await expect(page.getByText("CV export failed")).toHaveCount(0);
|
|
});
|
|
|
|
test(`[P5-00 PRES-E08 C06 S05] ${suffix} real Tooltip contrast and open-toast action access`, async ({ page, owner }, info) => {
|
|
test.setTimeout(120_000);
|
|
const t = translations[language];
|
|
await page.setViewportSize({ width: 1440, height: 900 });
|
|
await page.goto(`/career/builder/${owner.cvId}`);
|
|
await page.getByRole("tab", { name: t.cvEditorContent, exact: true }).click();
|
|
const headline = page.getByRole("textbox", { name: t.cvEditorHeadlineOverride, exact: true });
|
|
await headline.fill("Edited readiness headline");
|
|
await expect(page.getByText(t.cvEditorSaved, { exact: true })).toBeVisible();
|
|
await page.getByRole("button", { name: t.cvEditorUndo, exact: true }).click();
|
|
const redo = page.getByRole("button", { name: t.cvEditorRedo, exact: true });
|
|
await expect(redo).toBeEnabled();
|
|
await expect(redo).toHaveAccessibleName(t.cvEditorRedo);
|
|
await redo.hover();
|
|
const tooltip = page.getByRole("tooltip", { name: t.cvEditorRedo, exact: true });
|
|
await expect(tooltip).toBeVisible();
|
|
await capture(page, info, `redo-tooltip-${suffix}`);
|
|
await scan(page, info, `redo-tooltip-${suffix}`, '[role="tooltip"]');
|
|
await page.mouse.move(0, 0);
|
|
await redo.focus();
|
|
await page.keyboard.press("Shift+Tab");
|
|
await page.keyboard.press("Tab");
|
|
await expect(redo).toBeFocused();
|
|
await expect(tooltip).toBeVisible();
|
|
await ring(redo);
|
|
|
|
await page.goto("/settings");
|
|
await page.getByRole("button", { name: t.notifications, exact: true }).click();
|
|
const notificationClose = page.getByRole("button", { name: t.close, exact: true });
|
|
await expect(notificationClose).toHaveAccessibleName(t.close);
|
|
await notificationClose.hover();
|
|
await expect(page.getByRole("tooltip", { name: t.close, exact: true })).toBeVisible();
|
|
await scan(page, info, `shell-tooltip-${suffix}`, '[role="tooltip"]');
|
|
await notificationClose.click();
|
|
const main = page.getByRole("main");
|
|
let exported: APIResponse | undefined;
|
|
for (const width of [1440, 390, 320]) {
|
|
await page.setViewportSize({ width, height: width < 360 ? 740 : width < 768 ? 844 : 900 });
|
|
const choose = async (label: string) => {
|
|
if (width >= 768) await main.getByRole("tab", { name: label, exact: true }).click();
|
|
else {
|
|
await main.getByRole("combobox", { name: t.settingsNavigation, exact: true }).click();
|
|
await page.getByRole("option", { name: label, exact: true }).click();
|
|
}
|
|
};
|
|
await choose(t.settingsTabBackup);
|
|
// Complete the real export after navigating back to the bottom action.
|
|
// Otherwise clicking General correctly dismisses the toast via click-away.
|
|
let release!: () => void;
|
|
const held = new Promise<void>(resolve => { release = resolve; });
|
|
await page.route(`${apiUrl}/export/account`, async route => {
|
|
// One native ZIP, replayed for viewport checks to respect the 2/minute owner limit.
|
|
exported ??= await route.fetch();
|
|
expect(exported.status()).toBe(200);
|
|
await held;
|
|
await route.fulfill({ response: exported });
|
|
}, { times: 1 });
|
|
const download = page.waitForEvent("download");
|
|
await main.getByRole("button", { name: t.accountExportDownload, exact: true }).click();
|
|
await choose(t.settingsTabGeneral);
|
|
const manage = main.getByRole("button", { name: t.settingsManageConnectedAccounts, exact: true });
|
|
await manage.focus();
|
|
release();
|
|
expect((await download).suggestedFilename()).toMatch(/\.zip$/);
|
|
const toast = page.getByRole("alert").filter({ hasText: t.accountExportDownloaded });
|
|
await expect(toast).toBeVisible();
|
|
// Deliberately align at the viewport end: reserved page space alone does
|
|
// not stop native scrolling from placing an action underneath the toast.
|
|
await manage.evaluate(node => node.scrollIntoView({ block: "end" }));
|
|
// Hover uses Snackbar's normal pause behavior; timing is not shortened or mocked.
|
|
await toast.hover();
|
|
await toast.getByRole("button").focus();
|
|
await exposed(manage);
|
|
await capture(page, info, `open-toast-${width}-${suffix}`);
|
|
await expect(toast).toBeVisible();
|
|
await exposed(toast.getByRole("button"));
|
|
await toast.getByRole("button").click();
|
|
await expect(toast).not.toBeVisible();
|
|
}
|
|
});
|
|
|
|
test(`[P5-01 PRES-E07 E08 A03] ${suffix} grouped shell, rail menu and route focus`, async ({ page }, info) => {
|
|
test.setTimeout(90_000);
|
|
const t = translations[language];
|
|
await page.setViewportSize({ width: 1440, height: 900 });
|
|
await page.goto("/kanban");
|
|
const navigation = page.getByRole("navigation", { name: t.navigation });
|
|
await expect(navigation.getByText(t.jobSearch, { exact: true })).toBeVisible();
|
|
await expect(navigation.getByText(t.career, { exact: true })).toBeVisible();
|
|
await expect(navigation.getByRole("button", { name: t.jobApplications, exact: true })).toHaveAttribute("aria-current", "page");
|
|
await expect(page.getByRole("link", { name: t.jobsBoardView, exact: true })).toHaveAttribute("aria-current", "page");
|
|
await expect(navigation.getByRole("button", { name: t.connectedAccounts, exact: true })).toBeInViewport({ ratio: 1 });
|
|
await expect(page.getByRole("link", { name: t.skipToMainContent, exact: true })).toHaveAttribute("href", "#app-main-content");
|
|
await capture(page, info, `grouped-shell-${suffix}`);
|
|
await scan(page, info, `grouped-shell-${suffix}`, "nav");
|
|
|
|
await page.getByRole("button", { name: t.collapseSidebar, exact: true }).click();
|
|
const jobsRail = page.getByRole("button", { name: t.jobApplications, exact: true });
|
|
await jobsRail.focus();
|
|
await jobsRail.press("Enter");
|
|
const railMenu = page.getByRole("menu");
|
|
await expect(railMenu.getByRole("menuitem", { name: t.trash, exact: true })).toBeVisible();
|
|
await capture(page, info, `grouped-rail-${suffix}`);
|
|
await railMenu.getByRole("menuitem", { name: t.trash, exact: true }).click();
|
|
await expect(page).toHaveURL(/\/trash$/);
|
|
await expect(page.getByRole("heading", { level: 1, name: t.trash, exact: true })).toBeFocused();
|
|
|
|
await page.setViewportSize({ width: 390, height: 844 });
|
|
const trigger = page.getByRole("button", { name: t.openNavigation, exact: true });
|
|
await trigger.click();
|
|
const drawer = page.getByRole("dialog", { name: t.navigation });
|
|
await expect(drawer.getByText(t.jobSearch, { exact: true })).toBeVisible();
|
|
await expect(drawer.getByRole("button", { name: t.trash, exact: true })).toHaveAttribute("aria-current", "page");
|
|
await capture(page, info, `grouped-mobile-${suffix}`);
|
|
await scan(page, info, `grouped-mobile-${suffix}`, '[role="dialog"]');
|
|
await drawer.getByRole("button", { name: t.jobApplications, exact: true }).click();
|
|
await expect(page).toHaveURL(/\/jobs$/);
|
|
await expect(page.getByRole("heading", { level: 1, name: t.jobApplications, exact: true })).toBeFocused();
|
|
await expect(drawer).not.toBeVisible();
|
|
});
|
|
|
|
if ((mode === "light" && language === "en") || (mode === "dark" && language === "nb")) {
|
|
test(`[P5-00 PRES-E07 E09 C06 S02 S03] ${suffix} dirty editor survives shell utilities and cancel`, async ({ page, context, owner }, info) => {
|
|
test.setTimeout(90_000);
|
|
const t = translations[language];
|
|
await page.setViewportSize({ width: language === "nb" ? 320 : 1440, height: 900 });
|
|
await page.goto(`/career/builder/${owner.cvId}`);
|
|
await page.getByRole("tab", { name: t.cvEditorContent, exact: true }).click();
|
|
// A deterministic failed save keeps the real editor dirty; all reads stay native.
|
|
await page.route(`${apiUrl}/cv/variants/${owner.cvId}`, route => route.request().method() === "PUT"
|
|
? route.fulfill({ status: 503, json: { message: "Readiness save unavailable" } }) : route.continue());
|
|
const headline = () => page.getByRole("textbox", { name: translations[language].cvEditorHeadlineOverride, exact: true });
|
|
await headline().fill("Unsaved readiness draft æøå");
|
|
await expect(page.getByText(t.cvEditorSaveFailed, { exact: true })).toBeVisible();
|
|
const other = language === "en" ? "nb" : "en";
|
|
await page.locator("header").getByRole("button", { name: other === "nb" ? "Norsk" : "English", exact: true }).click();
|
|
await expect(page.getByRole("textbox", { name: translations[other].cvEditorHeadlineOverride, exact: true })).toHaveValue("Unsaved readiness draft æøå");
|
|
await page.locator("header").getByRole("button", { name: language === "nb" ? "Norsk" : "English", exact: true }).click();
|
|
await expect(headline()).toHaveValue("Unsaved readiness draft æøå");
|
|
|
|
// Change the existing browser preference through Settings in a second tab.
|
|
const settings = await context.newPage();
|
|
await settings.goto("/settings");
|
|
await settings.getByRole("combobox", { name: t.settingsTheme, exact: true }).click();
|
|
await settings.getByRole("option", { name: mode === "light" ? t.settingsThemeDark : t.settingsThemeLight, exact: true }).click();
|
|
await expect(page.locator("html")).toHaveAttribute("data-color-scheme", mode === "light" ? "dark" : "light");
|
|
await settings.close();
|
|
await expect(headline()).toHaveValue("Unsaved readiness draft æøå");
|
|
|
|
if (language === "nb") {
|
|
const trigger = page.getByRole("button", { name: t.openNavigation });
|
|
await trigger.click();
|
|
await page.keyboard.press("Escape");
|
|
await expect(trigger).toBeFocused();
|
|
await expect(headline()).toHaveValue("Unsaved readiness draft æøå");
|
|
await trigger.click();
|
|
await page.getByRole("dialog").getByRole("button", { name: t.dashboard, exact: true }).click();
|
|
} else {
|
|
await page.keyboard.press("Control+k");
|
|
await page.getByRole("dialog").getByRole("button", { name: new RegExp(t.goToDashboard) }).click();
|
|
}
|
|
const confirm = page.getByRole("dialog", { name: t.cvEditorDiscardTitle });
|
|
await expect(confirm).toBeVisible();
|
|
await capture(page, info, `dirty-cancel-${suffix}`);
|
|
await confirm.getByRole("button", { name: t.cancel, exact: true }).click();
|
|
await expect(headline()).toHaveValue("Unsaved readiness draft æøå");
|
|
await expect(page).toHaveURL(new RegExp(`/career/builder/${owner.cvId}$`));
|
|
await page.keyboard.press("Control+k");
|
|
await page.getByRole("dialog").getByRole("button", { name: new RegExp(t.goToDashboard) }).click();
|
|
await confirm.getByRole("button", { name: t.cvEditorDiscardLeave, exact: true }).click();
|
|
await expect(page).toHaveURL(/\/dashboard$/);
|
|
expect(JSON.stringify(await (await page.request.get(`${apiUrl}/cv/variants/${owner.cvId}`)).json())).not.toContain("Unsaved readiness draft");
|
|
});
|
|
}
|
|
});
|
|
}
|