const { test, expect } = require("@playwright/test"); const AxeBuilder = require("@axe-core/playwright").default; async function loginAsAdmin(page) { await page.goto("/auth/localhost"); await page.getByLabel(/username/i).fill("admin"); await page.getByLabel(/password/i).fill("admin"); await Promise.all([ page.waitForURL(/\/$|\/admin/), page.getByRole("button", { name: /login locally/i }).click() ]); } async function expectNoSeriousAxeViolations(page) { const result = await new AxeBuilder({ page }).analyze(); const blocking = result.violations.filter((item) => ["serious", "critical"].includes(item.impact)); expect(blocking, blocking.map((item) => `${item.id}: ${item.help}`).join("\n")).toEqual([]); } async function clickSidebarLink(page, href) { const link = page.locator(`.nav-link[href="${href}"]`); if (!(await link.isVisible())) { await link.locator("xpath=ancestor::details/summary").click(); } await Promise.all([ page.waitForNavigation({ waitUntil: "load" }), link.click() ]); } test("public shell renders without page errors or serious accessibility violations", async ({ page }, testInfo) => { const errors = []; page.on("pageerror", (error) => errors.push(error.message)); await page.goto("/"); await expect(page.locator("main.content")).toBeVisible(); await expect(page.getByRole("heading", { level: 1 })).toBeVisible(); await expectNoSeriousAxeViolations(page); await page.screenshot({ path: testInfo.outputPath("home-baseline.png"), fullPage: true }); expect(errors).toEqual([]); }); test("mobile navigation is a focus-contained drawer and restores its opener", async ({ page }) => { test.skip((page.viewportSize()?.width || 999) > 900, "Mobile drawer contract applies below 900px."); await page.goto("/"); const opener = page.locator(".mobile-topbar [data-sidebar-toggle]"); await opener.click(); await expect(opener).toHaveAttribute("aria-expanded", "true"); await expect(page.locator("#lumi-sidebar")).toHaveAttribute("aria-modal", "true"); await page.keyboard.press("Escape"); await expect(opener).toHaveAttribute("aria-expanded", "false"); await expect(opener).toBeFocused(); }); test("live events use one connection and release it across repeated navigation", async ({ page }) => { test.skip((page.viewportSize()?.width || 0) <= 900, "Desktop navigation stress coverage exercises the same event lifecycle."); await page.addInitScript(() => { const NativeEventSource = window.EventSource; window.__lumiEventSourceUrls = []; window.EventSource = new Proxy(NativeEventSource, { construct(Target, args) { window.__lumiEventSourceUrls.push(new URL(args[0], window.location.href).pathname); return Reflect.construct(Target, args); } }); }); await loginAsAdmin(page); await clickSidebarLink(page, "/admin/updates"); await expect.poll(() => page.evaluate(() => window.__lumiEventSourceUrls)).toEqual(["/api/events"]); const routes = ["/commands", "/feedback", "/admin", "/admin/navigation", "/admin/settings", "/"]; for (let cycle = 0; cycle < 3; cycle += 1) { for (const route of routes) { await clickSidebarLink(page, route); await expect.poll(() => page.evaluate(() => window.__lumiEventSourceUrls)).toEqual(["/api/events"]); } } }); test("updates reconcile checks and cleanup settings without document reloads", async ({ page }) => { await loginAsAdmin(page); let snapshotSaveAttempts = 0; await page.route("**/admin/updates/core/check", async (route) => { if (route.request().method() !== "POST") return route.continue(); const core = { current_version: "0.2.24", safe_target_version: "0.2.25", latest_available_version: "0.2.25", source_branch: "main", update_available: true, blocked: false, version_description: "A targeted browser-test update is available." }; await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ ok: true, message: "Core version 0.2.25 is available.", status: { core }, data: { status: { core } }, patch: {}, reloadRequired: false }) }); }); await page.route("**/admin/updates/snapshots/settings", async (route) => { if (route.request().method() !== "POST" || snapshotSaveAttempts++ > 0) return route.continue(); await route.fulfill({ status: 422, contentType: "application/json", body: JSON.stringify({ ok: false, message: "Correct the snapshot cleanup limits and try again.", fieldErrors: { max_age_days: "Enter a whole number from 1 to 3650." }, data: {}, patch: {}, reloadRequired: false }) }); }); await page.goto("/admin/updates"); await expect(page.locator("[data-update-management]")).toBeVisible(); const deployment = page.locator("[data-deployment-panel]"); await expect(deployment.getByRole("heading", { name: "Running code branch" })).toBeVisible(); await expect(deployment.locator("select[name='branch']")).toContainText("Stable ยท main"); await expect(deployment.getByRole("button", { name: "Deploy selected branch" })).toBeVisible(); const navigationCount = await page.evaluate(() => performance.getEntriesByType("navigation").length); const core = page.locator('[data-update-key="core"]'); await core.locator("summary").first().click(); const [checkResponse] = await Promise.all([ page.waitForResponse((response) => new URL(response.url()).pathname === "/admin/updates/core/check"), core.getByRole("button", { name: "Check for updates" }).click() ]); expect(checkResponse.headers()["content-type"]).toContain("application/json"); await expect(core.locator("[data-update-summary]")).toContainText("0.2.25"); await expect(core.locator("[data-update-badge]")).toHaveText("Update available"); await expect(core.locator("[data-update-inline-result]")).toContainText("0.2.25"); expect(await page.evaluate(() => performance.getEntriesByType("navigation").length)).toBe(navigationCount); const settings = page.locator("[data-update-settings-form]"); const age = settings.locator("input[name='max_age_days']"); const count = settings.locator("input[name='max_per_target']"); const originalAge = await age.inputValue(); const originalCount = await count.inputValue(); const [invalidResponse] = await Promise.all([ page.waitForResponse((response) => new URL(response.url()).pathname === "/admin/updates/snapshots/settings"), settings.getByRole("button", { name: "Save cleanup policy" }).click() ]); expect(invalidResponse.status()).toBe(422); await expect(age).toHaveAttribute("aria-invalid", "true"); await expect(settings.locator("[data-update-inline-result]")).toContainText("Correct the snapshot cleanup limits"); await age.fill(originalAge); await count.fill(originalCount); const [saveResponse] = await Promise.all([ page.waitForResponse((response) => new URL(response.url()).pathname === "/admin/updates/snapshots/settings" && response.status() === 200), settings.getByRole("button", { name: "Save cleanup policy" }).click() ]); expect(saveResponse.headers()["content-type"]).toContain("application/json"); await expect(settings.locator("[data-update-inline-result]")).toContainText("Snapshot cleanup now keeps"); expect(await page.evaluate(() => performance.getEntriesByType("navigation").length)).toBe(navigationCount); const reflow = await page.evaluate(() => ({ viewport: document.documentElement.clientWidth, documentWidth: document.documentElement.scrollWidth })); expect(reflow.documentWidth).toBeLessThanOrEqual(reflow.viewport + 1); await expectNoSeriousAxeViolations(page); }); test("custom commands and policies save targeted state without document reloads", async ({ page }) => { await loginAsAdmin(page); await page.goto("/commands"); const triggerHeader = page.locator("th[data-sort='trigger']"); await triggerHeader.getByRole("button", { name: "Trigger" }).focus(); await page.keyboard.press("Enter"); await expect(triggerHeader).toHaveAttribute("aria-sort", "ascending"); await expectNoSeriousAxeViolations(page); await page.goto("/admin/commands"); const createForm = page.locator("[data-command-create-form]"); const mode = createForm.locator("select[name='mode']"); await mode.selectOption("random"); await expect(createForm.locator(".js-field-random")).toBeVisible(); await expect(createForm.locator(".js-field-response").locator("xpath=ancestor::div[contains(@class,'field')]")).toBeHidden(); await mode.selectOption("plain"); const trigger = `uxcommand${Date.now()}`; await createForm.locator("input[name='trigger']").fill(trigger); await createForm.locator("input[name='response']").fill("A targeted command response"); const commandNavigationCount = await page.evaluate(() => performance.getEntriesByType("navigation").length); const [createResponse] = await Promise.all([ page.waitForResponse((response) => new URL(response.url()).pathname === "/admin/commands" && response.request().method() === "POST"), createForm.getByRole("button", { name: "Create command" }).click({ timeout: 5000 }) ]); expect(createResponse.headers()["content-type"]).toContain("application/json"); await expect(page.locator("[data-command-list-section]")).toContainText(trigger); await expect(page.locator("[data-command-management-status]")).toContainText("Command created"); expect(await page.evaluate(() => performance.getEntriesByType("navigation").length)).toBe(commandNavigationCount); await expect(createForm.locator("input[name='trigger']")).toHaveValue(""); expect(await createForm.evaluate((form) => Array.from(form.elements) .filter((field) => typeof field.checkValidity === "function" && !field.checkValidity()) .map((field) => ({ name: field.name, message: field.validationMessage })))).toEqual([]); const [invalidResponse] = await Promise.all([ page.waitForResponse((response) => new URL(response.url()).pathname === "/admin/commands" && response.request().method() === "POST"), createForm.evaluate((form) => form.requestSubmit(form.querySelector("button[type='submit']"))) ]); expect(invalidResponse.status()).toBe(422); await expect(createForm.locator("input[name='trigger']")).toHaveAttribute("aria-invalid", "true"); await expect(createForm.locator("[data-lumi-form-status]")).toContainText("Trigger is required"); expect(await page.evaluate(() => performance.getEntriesByType("navigation").length)).toBe(commandNavigationCount); const commandReflow = await page.evaluate(() => ({ viewport: document.documentElement.clientWidth, documentWidth: document.documentElement.scrollWidth })); expect(commandReflow.documentWidth).toBeLessThanOrEqual(commandReflow.viewport + 1); await page.goto("/admin/command-policies"); const commandItem = page.locator('[data-policy-search-scope="commands"] [data-policy-item]', { hasText: `!${trigger}` }); await expect(commandItem).toHaveCount(1); await commandItem.locator("summary").click(); await commandItem.locator("select[name$='window_mode']").selectOption("custom"); await commandItem.locator("input[name$='window_uses']").fill("2"); const policyNavigationCount = await page.evaluate(() => performance.getEntriesByType("navigation").length); const [policyResponse] = await Promise.all([ page.waitForResponse((response) => new URL(response.url()).pathname === "/admin/command-policies/commands"), page.locator(".command-policy-savebar").first().getByRole("button", { name: "Save all command settings" }).click() ]); expect(policyResponse.headers()["content-type"]).toContain("application/json"); await expect(page.locator("[data-command-save-state]")).toContainText("saved"); expect(await page.evaluate(() => performance.getEntriesByType("navigation").length)).toBe(policyNavigationCount); const reflow = await page.evaluate(() => ({ viewport: document.documentElement.clientWidth, documentWidth: document.documentElement.scrollWidth })); expect(reflow.documentWidth).toBeLessThanOrEqual(reflow.viewport + 1); await expectNoSeriousAxeViolations(page); }); test("enhanced settings save preserves the document, focus, and viewport", async ({ page }) => { await loginAsAdmin(page); await page.goto("/admin/settings"); const title = page.locator("input[name='site_title']"); await title.focus(); const navigationCount = await page.evaluate(() => performance.getEntriesByType("navigation").length); const scrollBefore = await page.evaluate(() => window.scrollY); await title.fill(`Lumi UI Test ${Date.now()}`); await page.getByRole("button", { name: "Save changes" }).click(); await expect(page.getByText(/Section saved|Settings saved/).first()).toBeVisible(); expect(await page.evaluate(() => performance.getEntriesByType("navigation").length)).toBe(navigationCount); expect(Math.abs((await page.evaluate(() => window.scrollY)) - scrollBefore)).toBeLessThan(3); await expect(title).toBeFocused(); await expectNoSeriousAxeViolations(page); }); test("overlay editor keeps settings scrollable and synchronizes interactive previews", async ({ page }) => { await loginAsAdmin(page); await page.goto("/admin/overlays"); await page.getByRole("button", { name: "Create overlay" }).click(); const createName = page.locator("[data-overlay-create-modal] input[name='name']"); const overlayName = `UX overlay ${Date.now()}`; await expect(createName).toBeFocused(); await createName.fill(overlayName); await Promise.all([ page.waitForURL(/\/admin\/overlays/), page.locator("[data-overlay-create-modal]").getByRole("button", { name: /create|add/i }).click() ]); await page.locator("tbody tr", { hasText: overlayName }).getByRole("link", { name: "Open editor" }).click(); const viewportWidth = page.viewportSize()?.width || 0; const preview = page.locator("[data-overlay-editor]"); const previewLauncher = page.locator("[data-preview-drawer-toggle]"); if (viewportWidth >= 1180) { await expect(preview).toBeVisible(); } else { await expect(preview).toBeHidden(); await page.mouse.move((page.viewportSize()?.width || 320) / 2, Math.min(420, (page.viewportSize()?.height || 720) / 2)); await page.mouse.wheel(0, 480); await expect.poll(() => page.evaluate(() => window.scrollY)).toBeGreaterThan(0); await page.evaluate(() => window.scrollTo(0, 0)); const launcherGeometry = await previewLauncher.evaluate((button) => { const rect = button.getBoundingClientRect(); return { top: rect.top, bottom: rect.bottom, viewportHeight: window.innerHeight }; }); expect(launcherGeometry.top).toBeGreaterThanOrEqual(0); expect(launcherGeometry.bottom).toBeLessThanOrEqual(launcherGeometry.viewportHeight); await previewLauncher.click(); await expect(preview).toBeVisible(); await expect(previewLauncher).toHaveAttribute("aria-expanded", "true"); await expect(preview).toHaveCSS("overflow-y", "auto"); } const previewGeometry = await preview.evaluate((panel) => { const panelRect = panel.getBoundingClientRect(); const frameRect = panel.querySelector("[data-preview-frame]").getBoundingClientRect(); return { panelTop: panelRect.top, panelBottom: panelRect.bottom, viewportHeight: window.innerHeight, frameRatio: frameRect.width / frameRect.height, stageHeight: panel.querySelector("[data-preview-stage]").getBoundingClientRect().height, frameHeight: frameRect.height, panelHeight: panelRect.height }; }); expect(previewGeometry.panelTop).toBeGreaterThanOrEqual(-1); expect(previewGeometry.panelBottom).toBeLessThanOrEqual(previewGeometry.viewportHeight + 1); expect(previewGeometry.frameRatio).toBeCloseTo(16 / 9, 2); expect(Math.abs(previewGeometry.stageHeight - previewGeometry.frameHeight)).toBeLessThanOrEqual(2); if (viewportWidth >= 1180) expect(previewGeometry.panelHeight).toBeLessThan(previewGeometry.viewportHeight * 0.9); if (viewportWidth < 1180) { await preview.getByRole("button", { name: "Close scene preview" }).click(); await expect(preview).toBeHidden(); await expect(previewLauncher).toBeFocused(); } const settingsColumn = page.locator(".overlay-settings-column"); const finalSettingsCard = page.locator(".overlay-settings-column > .card").last(); const finalDisclosure = finalSettingsCard.locator("details").first(); if (viewportWidth < 1180) { await finalDisclosure.locator("summary").evaluate((item) => item.scrollIntoView({ block: "center", behavior: "instant" })); } await finalDisclosure.locator("summary").click(); const scrollContract = await settingsColumn.evaluate((item) => ({ overflowY: getComputedStyle(item).overflowY, maxHeight: getComputedStyle(item).maxHeight, legacyHeight: item.style.getPropertyValue("--overlay-settings-max-height"), clientHeight: item.clientHeight, scrollHeight: item.scrollHeight })); expect(scrollContract.legacyHeight).toBe(""); if (viewportWidth >= 1180) { expect(scrollContract.overflowY).toBe("auto"); expect(scrollContract.scrollHeight).toBeGreaterThan(scrollContract.clientHeight); await settingsColumn.evaluate((item) => { item.scrollTop = item.scrollHeight; }); await expect.poll(() => settingsColumn.evaluate((item) => item.scrollTop)).toBeGreaterThan(0); } else { expect(["visible", "clip"]).toContain(scrollContract.overflowY); } const finalSettingsAction = finalSettingsCard.locator("button").last(); await finalSettingsAction.scrollIntoViewIfNeeded(); await expect(finalSettingsAction).toBeVisible(); const finalActionGeometry = await finalSettingsAction.evaluate((button) => { const rect = button.getBoundingClientRect(); return { top: rect.top, bottom: rect.bottom, viewportHeight: window.innerHeight }; }); expect(finalActionGeometry.top).toBeGreaterThanOrEqual(-1); expect(finalActionGeometry.bottom).toBeLessThanOrEqual(finalActionGeometry.viewportHeight + 1); if (viewportWidth >= 1180) { const stickyGeometry = await preview.evaluate((panel) => { const rect = panel.getBoundingClientRect(); return { top: rect.top, bottom: rect.bottom, viewportHeight: window.innerHeight }; }); expect(stickyGeometry.top).toBeGreaterThanOrEqual(-1); expect(stickyGeometry.bottom).toBeLessThanOrEqual(stickyGeometry.viewportHeight + 1); const addSource = page.locator(".overlay-add-source").first(); await addSource.scrollIntoViewIfNeeded(); await addSource.locator("summary").click(); await addSource.locator("input[name='name']").fill("Pop-out adjustment source"); await Promise.all([ page.waitForNavigation({ waitUntil: "load" }), addSource.getByRole("button", { name: "Add source" }).click() ]); await expect(page.locator("[data-overlay-editor]")).toBeVisible(); const [popup] = await Promise.all([ page.waitForEvent("popup"), preview.getByRole("button", { name: "Pop out" }).click() ]); await popup.waitForLoadState("domcontentloaded"); await popup.setViewportSize({ width: 900, height: 500 }); const popoutFrame = popup.locator("[data-preview-window-frame]"); await expect(popoutFrame).toBeVisible(); await expect.poll(() => popoutFrame.evaluate((frame) => { const rect = frame.getBoundingClientRect(); return rect.right <= window.innerWidth + 1 && rect.bottom <= window.innerHeight + 1; })).toBe(true); const popoutGeometry = await popoutFrame.evaluate((frame) => { const rect = frame.getBoundingClientRect(); return { top: rect.top, right: rect.right, bottom: rect.bottom, left: rect.left, ratio: rect.width / rect.height, viewportWidth: window.innerWidth, viewportHeight: window.innerHeight }; }); expect(popoutGeometry.top).toBeGreaterThanOrEqual(-1); expect(popoutGeometry.left).toBeGreaterThanOrEqual(-1); expect(popoutGeometry.right).toBeLessThanOrEqual(popoutGeometry.viewportWidth + 1); expect(popoutGeometry.bottom).toBeLessThanOrEqual(popoutGeometry.viewportHeight + 1); expect(popoutGeometry.ratio).toBeCloseTo(16 / 9, 2); await expect(popup.locator("[data-popout-source]")).toHaveValue(/.+/); await expect(popup.locator(".lumi-overlay-module.is-selected")).toHaveCount(1); const mainX = page.locator("[data-module-editor][data-module-id] input[name='x']").first(); await expect(mainX).toHaveValue("0"); const transformResponse = popup.waitForResponse((response) => /\/api\/admin\/overlays\/[^/]+\/modules\/[^/]+\/transform$/.test(new URL(response.url()).pathname)); await popup.locator("[data-preview-window-canvas]").focus(); await popup.keyboard.press("ArrowRight"); expect((await transformResponse).ok()).toBe(true); await expect(popup.locator("[data-preview-window-status]")).toContainText("Saved"); await expect(mainX).toHaveValue("0.1"); await popup.close(); } const navigationCount = await page.evaluate(() => performance.getEntriesByType("navigation").length); await page.getByText("Overlay name and canvas size", { exact: true }).click(); const name = page.locator("form[data-lumi-enhanced-form] input[name='name']").first(); await name.fill(`${await name.inputValue()} edited`); await name.locator("xpath=ancestor::form").getByRole("button", { name: /save overlay settings/i }).click(); await expect(name.locator("xpath=ancestor::form").locator("[data-lumi-form-status]")).toContainText("Overlay saved"); expect(await page.evaluate(() => performance.getEntriesByType("navigation").length)).toBe(navigationCount); await expectNoSeriousAxeViolations(page); }); test("resource storage limits save, reconcile immediately, and persist", async ({ page }) => { await loginAsAdmin(page); await page.goto("/admin/resources"); const settings = page.locator(".resource-settings-panel"); await settings.locator("summary").click(); const form = settings.locator("[data-resource-settings-form]"); await form.locator("[name='limit_gib']").fill("3.5"); await form.locator("[name='reserve_gib']").fill("0.3"); await form.locator("[name='max_file_mib']").fill("24"); await form.locator("[name='max_files']").fill("6"); const [response] = await Promise.all([ page.waitForResponse((candidate) => new URL(candidate.url()).pathname === "/admin/resources/settings"), form.getByRole("button", { name: "Save storage settings" }).click() ]); expect(response.ok()).toBe(true); const payload = await response.json(); expect(payload.settings).toEqual({ limit_gib: 3.5, reserve_gib: 0.3, max_file_mib: 24, max_files: 6 }); await expect(form.locator("[data-resource-settings-status]")).toContainText("saved"); await expect(page.locator("[data-storage-limit]")).toContainText("3.50 GB"); await expect(page.locator("[data-storage-max-file]")).toContainText("24.0 MB"); const fileInput = page.locator("[data-resource-file-input]"); await expect(fileInput).toHaveAttribute("data-max-files", "6"); await expect(fileInput).toHaveAttribute("data-max-file-bytes", String(24 * 1024 * 1024)); await page.reload(); await page.locator(".resource-settings-panel summary").click(); await expect(page.locator("[data-resource-settings-form] [name='limit_gib']")).toHaveValue("3.5"); await expect(page.locator("[data-resource-settings-form] [name='reserve_gib']")).toHaveValue("0.3"); await expect(page.locator("[data-resource-settings-form] [name='max_file_mib']")).toHaveValue("24"); await expect(page.locator("[data-resource-settings-form] [name='max_files']")).toHaveValue("6"); await expectNoSeriousAxeViolations(page); }); test("navigation builder supports keyboard movement and saves without navigation", async ({ page }) => { await loginAsAdmin(page); await page.goto("/admin/navigation"); await expect(page.locator("[data-navigation-builder]" )).toBeVisible(); const navigationCount = await page.evaluate(() => performance.getEntriesByType("navigation").length); const firstItem = page.locator("[data-nav-section-card] .nav-dd-item").first(); const navId = await firstItem.getAttribute("data-nav-id"); const movedItem = page.locator(`.nav-dd-item[data-nav-id="${navId}"]`); const originalGroup = await movedItem.locator("xpath=parent::*").getAttribute("data-group-label"); await movedItem.getByRole("button", { name: /to the next group/i }).click(); const movedGroup = await movedItem.locator("xpath=parent::*").getAttribute("data-group-label"); expect(movedGroup).not.toBe(originalGroup); await page.getByRole("button", { name: "Add section" }).click(); const newLabel = page.locator("[data-nav-section-card] .nav-section-label").last(); await expect(newLabel).toBeFocused(); await newLabel.fill("Automation"); await page.getByRole("button", { name: "Save navigation" }).click(); await expect(page.locator("[data-nav-builder-status]")).toContainText("Navigation saved"); expect(await page.evaluate(() => performance.getEntriesByType("navigation").length)).toBe(navigationCount); await expect(page.locator("[data-nav-dirty-badge]")).toBeHidden(); const reflow = await page.evaluate(() => ({ viewport: document.documentElement.clientWidth, documentWidth: document.documentElement.scrollWidth })); expect(reflow.documentWidth).toBeLessThanOrEqual(reflow.viewport + 1); await expectNoSeriousAxeViolations(page); }); test("theme draft preview survives a targeted custom-theme save", async ({ page }) => { await loginAsAdmin(page); await page.goto("/admin/theming"); const sourceTheme = page.locator(".theme-card").first(); await sourceTheme.locator("summary", { hasText: "More actions" }).click(); const copyName = `UX Theme ${Date.now()}`; await sourceTheme.locator("form[action='/admin/theming/duplicate'] input[name='name']").fill(copyName); await Promise.all([ page.waitForURL(/\/admin\/theming\?edit=.*#theme-editor/), sourceTheme.getByRole("button", { name: "Duplicate" }).click() ]); const form = page.locator("[data-theme-form]"); await expect(form).toBeVisible(); const accent = form.locator("input[name='light_accent']"); const nextAccent = (await accent.inputValue()).toLowerCase() === "#176b76" ? "#176b75" : "#176b76"; await accent.fill(nextAccent); const navigationCount = await page.evaluate(() => performance.getEntriesByType("navigation").length); const [saveResponse] = await Promise.all([ page.waitForResponse((response) => /\/admin\/theming\/custom\/[^/]+\/save$/.test(new URL(response.url()).pathname)), form.getByRole("button", { name: "Save theme" }).click() ]); expect(saveResponse.headers()["content-type"]).toContain("application/json"); expect((await saveResponse.json()).message).toContain(`${copyName} saved`); await expect(form.locator("[data-lumi-form-status]")).toContainText(`${copyName} saved`); expect(await page.evaluate(() => performance.getEntriesByType("navigation").length)).toBe(navigationCount); await expect(form.getByRole("button", { name: "Saved", exact: true })).toBeVisible(); await expectNoSeriousAxeViolations(page); });