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; 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(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"); }); } }); }