import { expect, test as base, type Locator, type Page, type TestInfo } from "@playwright/test"; import { createHash, randomUUID } from "node:crypto"; import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { createApp } from "../apps/server/dist/app.js"; import { navigateWorkspace, openProject, openProjectSection } from "./workspace-controls"; import AxeBuilder from "@axe-core/playwright"; const originalName = "Synthetic control board"; const changedName = "Synthetic control board for front panel"; const itemName = "Synthetic bench controller"; const spareName = "Synthetic optional spare board"; const originalNote = "Verify voltage and pinout before using this board."; const changedNote = "Front panel needs the keyed connector. Verify voltage and pinout before assembly."; const compatibility = { compatible: "conditional", reason: "Verify voltage and pinout" }; const instructionsFile = "synthetic-control-board-notes.md"; const instructions = "# Synthetic control board notes\n\nCheck the keyed connector and voltage before assembly.\n\nA recorded count does not confirm compatibility.\n"; type BomRecord = { id: string; name: string; requiredQuantity: number; notes?: string; version: number; alternatives: Array<{ itemId: string; compatible: string; reason?: string }> }; type StockRecord = { quantity: number; version: number; evidence: { state: string } }; type GapRecord = { lineId: string; decision: string; status: string; suppliedQuantity: number; candidates: Array<{ itemId: string; compatibility: string }> }; type PartWorkspace = { projectName: string; itemId: string; lineId: string; countUrl: string; readStock(): Promise; readBom(): Promise; readGap(): Promise; }; // A temporary, empty workspace makes these real writes independent of the demo // server and of any user inventory. Both widths exercise the same actual API. const test = base.extend<{ partWorkspace: PartWorkspace }>({ partWorkspace: async ({ page }, use) => { const directory = await mkdtemp(join(tmpdir(), "benchledger-part-stock-")); const app = await createApp({ demo: false, dataDir: directory, publicBaseUrl: "http://127.0.0.1", logger: false, auth: { sessionSecret: randomUUID().repeat(2) } }); try { const origin = await app.listen({ host: "127.0.0.1", port: 0 }); await page.goto(origin); await expect(page.getByRole("heading", { name: "Projects", exact: true })).toBeVisible(); const post = async (path: string, data: object): Promise => { const csrf = (await page.context().cookies()).find(cookie => cookie.name === "forge_csrf")?.value; expect(csrf).toBeTruthy(); const response = await page.request.post(`${origin}/api/v1${path}`, { headers: { "x-csrf-token": csrf!, "idempotency-key": randomUUID() }, data }); expect(response.ok(), await response.text()).toBe(true); return response.json(); }; const read = async (path: string): Promise => { const response = await page.request.get(`${origin}/api/v1${path}`); expect(response.ok(), await response.text()).toBe(true); return response.json(); }; const id = `part-stock-${randomUUID()}`, revisionId = `${id}-r1`, itemId = `${id}-item`; const projectName = "Synthetic panel controller"; await post("/projects/with-initial-revision", { project: { id, name: projectName, status: "building" }, revision: { id: revisionId, name: "Initial", status: "concept", fabricationRoute: "none" } }); await post("/inventory", { id: itemId, name: itemName, kind: "electronic", quantity: 8, unit: "each", location: "Synthetic drawer A", tags: [], links: [], evidence: { state: "delivered_uncounted", source: "Synthetic test fixture" } }); const { data: line } = await post<{ data: BomRecord }>(`/project-revisions/${revisionId}/bom`, { name: originalName, requiredQuantity: 2, unit: "each", role: "consumed", itemId, notes: originalNote, optional: false, constraints: {}, alternatives: [{ itemId, ...compatibility }] }); // A second linked part proves Needed by opens the requested requirement, // rather than whichever line happens to be first in the project. await post(`/project-revisions/${revisionId}/bom`, { name: spareName, requiredQuantity: 1, unit: "each", role: "consumed", itemId, optional: true, constraints: {}, alternatives: [{ itemId, ...compatibility }] }); const bytes = Buffer.from(instructions); const sha256 = createHash("sha256").update(bytes).digest("hex"); const { data: upload } = await post<{ data: { id: string } }>("/artifacts/uploads", { projectId: id, projectRevisionId: revisionId, filename: instructionsFile, role: "text", mediaType: "text/markdown", byteSize: bytes.length, sha256 }); const csrf = (await page.context().cookies()).find(cookie => cookie.name === "forge_csrf")!.value; const uploaded = await page.request.put(`${origin}/api/v1/artifacts/uploads/${upload.id}`, { headers: { "x-csrf-token": csrf, "content-type": "application/octet-stream" }, data: bytes }); expect(uploaded.ok(), await uploaded.text()).toBe(true); const { data: artifact } = await post<{ data: { sha256: string } }>(`/artifacts/uploads/${upload.id}/finalize`, {}); expect(artifact.sha256).toBe(sha256); await page.reload(); await use({ projectName, itemId, lineId: line.id, countUrl: `${origin}/api/v1/inventory/${itemId}/count`, readStock: () => read(`/inventory/${itemId}`), readBom: () => read(`/project-revisions/${revisionId}/bom`), readGap: async () => (await read<{ lines: GapRecord[] }>(`/project-revisions/${revisionId}/gaps`)).lines.find(gap => gap.lineId === line.id), }); } finally { const closed = app.close(); app.server.closeAllConnections(); await closed; await rm(directory, { recursive: true, force: true }); } }, }); async function editFromOverview(page: Page, workspace: PartWorkspace, changeDraft = true) { await openProject(page, workspace.projectName); await expect(page.getByRole("tab", { name: "Overview", exact: true })).toHaveAttribute("aria-selected", "true"); const next = page.getByRole("region", { name: "Next project action", exact: true }); await expect(next.getByRole("heading", { name: `Check ${originalName}`, exact: true })).toBeVisible(); await next.getByRole("button", { name: "Review this part", exact: true }).click(); const editor = page.getByRole("dialog", { name: "Part details", exact: true }); await expect(editor.getByLabel("Requirement name", { exact: true })).toHaveValue(originalName); await expect(editor.getByRole("textbox", { name: "Specification and notes", exact: true })).toHaveValue(originalNote); if (!changeDraft) return editor; await editor.getByLabel("Requirement name", { exact: true }).fill(changedName); await editor.getByLabel("Required quantity", { exact: true }).fill("3"); await editor.getByRole("textbox", { name: "Specification and notes", exact: true }).fill(changedNote); return editor; } async function expectPartDraft(editor: Locator) { await expect(editor.getByLabel("Requirement name", { exact: true })).toHaveValue(changedName); await expect(editor.getByLabel("Required quantity", { exact: true })).toHaveValue("3"); await expect(editor.getByRole("textbox", { name: "Specification and notes", exact: true })).toHaveValue(changedNote); } async function expectConditionalGap(workspace: PartWorkspace) { const gap = await workspace.readGap(); expect(gap).toMatchObject({ decision: "check", status: "inspect_first", suppliedQuantity: 0 }); expect(gap?.candidates).toEqual(expect.arrayContaining([expect.objectContaining({ itemId: workspace.itemId, compatibility: "conditional" })])); } async function capture(page: Page, info: TestInfo, name: string) { await page.evaluate(() => document.fonts.ready); await expect.poll(() => page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true); const viewport = page.viewportSize()!; for (const modal of await page.locator('[role="dialog"][aria-modal="true"]:visible, [role="alertdialog"]:visible').all()) { const bounds = (await modal.boundingBox())!; expect(bounds.x).toBeGreaterThanOrEqual(0); expect(bounds.y).toBeGreaterThanOrEqual(0); expect(bounds.x + bounds.width).toBeLessThanOrEqual(viewport.width + 1); expect(bounds.y + bounds.height).toBeLessThanOrEqual(viewport.height + 1); } await page.screenshot({ path: info.outputPath(`${name}.png`), fullPage: false, animations: "disabled" }); } test("resizing during a stock detour keeps the active task reachable and retains the part draft", async ({ page, partWorkspace: workspace }, info) => { await page.setViewportSize({ width: 1536, height: 1024 }); const beforeStock = await workspace.readStock(); const beforeBom = await workspace.readBom(); const editor = await editFromOverview(page, workspace); await editor.getByRole("button", { name: "Check this stock", exact: true }).click(); const stock = page.getByRole("dialog", { name: itemName, exact: true }); const quantity = stock.getByLabel("Counted quantity", { exact: true }); await quantity.fill("7"); await page.setViewportSize({ width: 390, height: 844 }); await expect(stock).toBeVisible(); await expect(editor).toHaveCount(0); await expect(quantity).toBeFocused(); await expect(quantity).toHaveValue("7"); // A real pointer action catches a suspended parent becoming the top modal. await stock.getByRole("button", { name: "Review physical count", exact: true }).click(); const review = page.getByRole("alertdialog", { name: "Review physical count", exact: true }); await expect(review).toContainText("7 pieces"); await review.getByRole("button", { name: "Confirm physical count", exact: true }).focus(); await page.setViewportSize({ width: 1536, height: 1024 }); await expect(review.getByRole("button", { name: "Confirm physical count", exact: true })).toBeFocused(); await review.getByRole("button", { name: "Confirm physical count", exact: true }).click(); await expect(stock.getByRole("status", { name: "Physical count saved", exact: true })).toContainText("7 pieces"); await page.setViewportSize({ width: 390, height: 844 }); await stock.getByRole("button", { name: `Return to ${originalName}`, exact: true }).click(); await expectPartDraft(editor); await expect(editor).toHaveAttribute("aria-modal", "true"); expect(await workspace.readStock()).toMatchObject({ quantity: 7, version: beforeStock.version + 1 }); expect(await workspace.readBom()).toEqual(beforeBom); await expectConditionalGap(workspace); await capture(page, info, "part-draft-after-breakpoint-stock-detour"); }); for (const width of [1536, 390]) { test.describe(`approved part stock flow at ${width}px`, () => { test.use({ viewport: { width, height: width === 390 ? 844 : 1024 } }); test("count review returns to the exact edited part without confirming compatibility", async ({ page, partWorkspace: workspace }, info) => { test.setTimeout(60_000); await page.emulateMedia({ reducedMotion: "reduce" }); const beforeStock = await workspace.readStock(); const beforeBom = await workspace.readBom(); await expectConditionalGap(workspace); const editor = await editFromOverview(page, workspace); if (width === 1536) { await page.locator(".bom-row").getByRole("button", { name: spareName, exact: true }).click(); const leave = page.getByRole("alertdialog", { name: "Leave without saving?", exact: true }); await expect(leave).toBeVisible(); await leave.getByRole("button", { name: "Keep editing", exact: true }).click(); await expectPartDraft(editor); } const countWrites: string[] = []; page.on("request", request => { if (request.url() === workspace.countUrl && request.method() === "POST") countWrites.push(request.postData() ?? ""); }); await editor.getByRole("button", { name: "Check this stock", exact: true }).click(); const stock = page.getByRole("dialog", { name: itemName, exact: true }); await expect(stock.getByLabel("Counted quantity", { exact: true })).toHaveValue(""); await stock.getByLabel("Counted quantity", { exact: true }).fill("7"); await stock.getByRole("button", { name: "Review physical count", exact: true }).click(); const review = page.getByRole("alertdialog", { name: "Review physical count", exact: true }); await expect(review).toContainText("This count does not confirm that the item fits a project."); await expect(review).toContainText(itemName); expect(countWrites).toHaveLength(0); expect(await workspace.readStock()).toEqual(beforeStock); await capture(page, info, "count-review"); await review.getByRole("button", { name: "Back to item", exact: true }).click(); await expect(stock.getByLabel("Counted quantity", { exact: true })).toHaveValue("7"); expect(countWrites).toHaveLength(0); await stock.getByRole("button", { name: "Review physical count", exact: true }).click(); await review.getByRole("button", { name: "Confirm physical count", exact: true }).click(); await expect(review).toHaveCount(0); await expect(stock.getByText("Physical count saved", { exact: true })).toBeVisible(); await expect(stock).toContainText("Compatibility with a project is checked separately."); expect(countWrites).toHaveLength(1); expect(JSON.parse(countWrites[0]!)).toEqual({ quantity: 7 }); expect(await workspace.readStock()).toMatchObject({ quantity: 7, evidence: { state: "physically_counted" }, version: beforeStock.version + 1 }); expect(await workspace.readBom()).toEqual(beforeBom); await expectConditionalGap(workspace); const returnToPart = stock.getByRole("button", { name: `Return to ${originalName}`, exact: true }); await expect(returnToPart).toBeVisible(); await returnToPart.scrollIntoViewIfNeeded(); await capture(page, info, "count-receipt-return-to-exact-part"); await returnToPart.click(); await expectPartDraft(editor); await expect(editor.getByRole("button", { name: "Check this stock", exact: true })).toBeEnabled(); await editor.getByRole("button", { name: "Save requirement", exact: true }).click(); await expect(editor).toHaveCount(0); const saved = (await workspace.readBom()).find(line => line.id === workspace.lineId); expect(saved).toMatchObject({ name: changedName, requiredQuantity: 3, notes: changedNote, alternatives: [{ itemId: workspace.itemId, ...compatibility }] }); await expectConditionalGap(workspace); await openProjectSection(page, "Parts"); const row = page.locator(".bom-row").filter({ has: page.getByRole("button", { name: changedName, exact: true }) }); await expect(row.locator(".status-pill")).toHaveText(/Check/u); await row.getByRole("button", { name: changedName, exact: true }).click(); await expectPartDraft(editor); await expect(editor.getByRole("button", { name: "Check this stock", exact: true })).toBeVisible(); await expect(editor).toContainText(itemName); await expect(editor.getByText("Searching all inventory…", { exact: true })).toHaveCount(0); const savedNotice = page.getByRole("button", { name: "Dismiss notification", exact: true }); if (await savedNotice.isVisible()) await savedNotice.click(); if (width === 1536) { await page.locator(".content").evaluate(element => element.scrollTo(0, 0)); await expect(editor).toHaveAttribute("aria-modal", "false"); await expect(page.locator('[data-slot="dialog-overlay"]:visible')).toHaveCount(0); const bounds = (await editor.boundingBox())!; const list = (await page.locator(".project-parts-register").boundingBox())!; expect(list.x + list.width).toBeLessThanOrEqual(bounds.x); for (const cell of [".bom-quantity", ".bom-available", ".bom-status"]) await expect(row.locator(cell)).toBeInViewport({ ratio: 1 }); await expect(editor.getByLabel("Required quantity", { exact: true })).toBeInViewport({ ratio: 1 }); await expect(editor.getByRole("region", { name: "Selected stock candidate", exact: true })).toBeInViewport({ ratio: 1 }); await expect(editor.getByRole("button", { name: "Check this stock", exact: true })).toBeInViewport({ ratio: 1 }); await expect(editor.getByText("You’ll return to this part with your edits kept. A count alone does not confirm fit.", { exact: true })).toBeInViewport({ ratio: 1 }); const check = editor.getByRole("button", { name: "Check this stock", exact: true }); expect(await check.evaluate(element => { const box = element.getBoundingClientRect(); return element.contains(document.elementFromPoint(box.x + box.width / 2, box.y + box.height / 2)); })).toBe(true); const a11y = await new AxeBuilder({ page }).include(".project-parts-workspace").withTags(["wcag2a", "wcag2aa", "wcag21aa"]).analyze(); expect(a11y.violations).toEqual([]); } await capture(page, info, "parts-selected-detail"); await editor.getByRole("button", { name: "Cancel", exact: true }).click(); await expect(editor).toHaveCount(0); await navigateWorkspace(page, "Inventory"); await page.getByRole("textbox", { name: "Search inventory", exact: true }).fill(itemName); await page.locator(".inventory-table tbody tr").filter({ hasText: itemName }).locator(".row-open").click(); const needs = stock.getByRole("region", { name: "Needed by projects", exact: true }); await expect(needs.getByRole("button", { name: /^Open .* in /u })).toHaveCount(2); await needs.getByRole("button", { name: `Open ${spareName} in ${workspace.projectName}`, exact: true }).click(); await expect(stock).toHaveCount(0); await expect(editor.getByLabel("Requirement name", { exact: true })).toHaveValue(spareName); await expect(editor.getByLabel("Required quantity", { exact: true })).toHaveValue("1"); await expect(editor.getByRole("textbox", { name: "Specification and notes", exact: true })).toHaveValue(""); await editor.getByRole("button", { name: "Cancel", exact: true }).click(); await expect(editor).toHaveCount(0); await openProjectSection(page, "Files"); for (const name of ["3D print", "Electronics", "CAD & firmware", "Instructions"]) await expect(page.getByRole("region", { name, exact: true })).toBeVisible(); await page.locator(".content").evaluate(element => element.scrollTo(0, 0)); await expect(page.getByRole("button", { name: `Details for ${instructionsFile}`, exact: true })).toBeInViewport(); await page.getByRole("button", { name: `Details for ${instructionsFile}`, exact: true }).click(); const details = page.getByRole("complementary", { name: `File details: ${instructionsFile}`, exact: true }); await expect(details).toContainText("Candidate file"); if (width === 1536) { await page.locator(".content").evaluate(element => element.scrollTo(0, 0)); await expect.poll(() => page.locator(".content").evaluate(element => element.scrollTop)).toBe(0); for (const name of ["3D print", "Electronics", "CAD & firmware", "Instructions"]) await expect(page.getByRole("heading", { name, exact: true })).toBeInViewport(); await expect(details.getByRole("button", { name: "Preview file", exact: true })).toBeInViewport(); await expect(details.getByRole("button", { name: "Download file", exact: true })).toBeInViewport(); } else { await details.scrollIntoViewIfNeeded(); } await capture(page, info, "files-current-selected-detail"); await details.getByRole("button", { name: "Preview file", exact: true }).click(); const preview = page.getByRole("dialog", { name: instructionsFile, exact: true }); await expect(preview.getByRole("heading", { name: "Synthetic control board notes", exact: true })).toBeVisible(); await expect(preview).toContainText("A recorded count does not confirm compatibility."); await capture(page, info, "files-current-loaded-preview"); await preview.getByRole("button", { name: "Close preview", exact: true }).click(); await openProjectSection(page, "Parts"); await page.getByRole("button", { name: "Add part", exact: true }).click(); const add = page.getByRole("dialog", { name: "Add a part", exact: true }); const draftName = "Synthetic panel mounting clips"; const draftNote = "Check clip width against the panel before sourcing."; await add.getByLabel("Part name", { exact: true }).fill(draftName); await add.getByLabel("Amount needed", { exact: true }).fill("4"); await add.getByRole("textbox", { name: "Specification (optional)", exact: true }).fill(draftNote); const rejectedCreate = "**/project-revisions/*/bom"; await page.route(rejectedCreate, route => route.request().method() === "POST" ? route.fulfill({ status: 409, contentType: "application/json", json: { error: { code: "version_conflict", message: "The project changed. Your part has not been saved." } } }) : route.continue()); await add.getByRole("button", { name: "Add part", exact: true }).click(); await expect(add.getByRole("alert")).toContainText("Your entries are kept."); await expect(add.getByLabel("Part name", { exact: true })).toHaveValue(draftName); await expect(add.getByLabel("Amount needed", { exact: true })).toHaveValue("4"); await expect(add.getByRole("textbox", { name: "Specification (optional)", exact: true })).toHaveValue(draftNote); await expect(add.getByRole("button", { name: "Try saving again", exact: true })).toBeInViewport(); await capture(page, info, "add-part-error-draft-preserved"); await page.unroute(rejectedCreate); await add.getByRole("button", { name: "Cancel", exact: true }).click(); await page.getByRole("alertdialog", { name: "Leave without saving?", exact: true }).getByRole("button", { name: "Discard changes and leave", exact: true }).click(); await expect(add).toHaveCount(0); }); test("discarding a nested stock draft preserves the unsaved part draft", async ({ page, partWorkspace: workspace }) => { const beforeStock = await workspace.readStock(); const beforeBom = await workspace.readBom(); const editor = await editFromOverview(page, workspace); let countWrites = 0; page.on("request", request => { if (request.url() === workspace.countUrl && request.method() === "POST") countWrites++; }); await editor.getByRole("button", { name: "Check this stock", exact: true }).click(); const stock = page.getByRole("dialog", { name: itemName, exact: true }); await stock.getByLabel("Counted quantity", { exact: true }).fill("6"); await stock.getByRole("button", { name: `Return to ${originalName}`, exact: true }).click(); const discard = page.getByRole("alertdialog", { name: "Discard this item draft?", exact: true }); await expect(discard).toContainText("Discard only the unsaved item changes to return to your requirement."); await discard.getByRole("button", { name: "Keep editing", exact: true }).click(); await expect(stock.getByLabel("Counted quantity", { exact: true })).toHaveValue("6"); await stock.getByRole("button", { name: `Return to ${originalName}`, exact: true }).click(); await discard.getByRole("button", { name: "Discard item changes", exact: true }).click(); await expect(stock).toHaveCount(0); await expect(page.getByRole("alertdialog")).toHaveCount(0); await expectPartDraft(editor); expect(countWrites).toBe(0); expect(await workspace.readStock()).toEqual(beforeStock); expect(await workspace.readBom()).toEqual(beforeBom); await editor.getByRole("button", { name: "Check this stock", exact: true }).click(); await expect(stock.getByLabel("Counted quantity", { exact: true })).toHaveValue(""); await stock.getByRole("button", { name: `Return to ${originalName}`, exact: true }).click(); await expect(page.getByRole("alertdialog")).toHaveCount(0); await expectPartDraft(editor); }); test("browser Back retains a nested count and cannot leave an unconfirmed save", async ({ page, partWorkspace: workspace }) => { const beforeStock = await workspace.readStock(); const beforeBom = await workspace.readBom(); // Keep the requirement unchanged: only the supporting stock task is dirty. const editor = await editFromOverview(page, workspace, false); const originalUrl = page.url(); await editor.getByRole("button", { name: "Check this stock", exact: true }).click(); const stock = page.getByRole("dialog", { name: itemName, exact: true }); await stock.getByLabel("Counted quantity", { exact: true }).fill("7"); await page.goBack(); const leave = page.getByRole("alertdialog", { name: "Leave without saving?", exact: true }); await expect(leave).toContainText("unsaved changes in stock observation."); await expect(page).toHaveURL(originalUrl); await leave.getByRole("button", { name: "Keep editing", exact: true }).click(); await expect(stock.getByLabel("Counted quantity", { exact: true })).toHaveValue("7"); await expect(page).toHaveURL(originalUrl); expect(await workspace.readStock()).toEqual(beforeStock); expect(await workspace.readBom()).toEqual(beforeBom); const countRequests: Array<{ key: string | undefined; body: string | null }> = []; await page.route(workspace.countUrl, async route => { countRequests.push({ key: route.request().headers()["idempotency-key"], body: route.request().postData() }); if (countRequests.length === 1) { // The service records the observation, but its acknowledgement is lost. const response = await route.fetch(); expect(response.ok()).toBe(true); await route.abort("failed"); } else await route.continue(); }); await stock.getByRole("button", { name: "Review physical count", exact: true }).click(); const review = page.getByRole("alertdialog", { name: "Review physical count", exact: true }); await review.getByRole("button", { name: "Confirm physical count", exact: true }).click(); await expect(review.getByRole("button", { name: "Retry unchanged observation", exact: true })).toBeVisible(); await page.goBack(); const pending = page.getByRole("alertdialog", { name: "Finish the pending save", exact: true }); await expect(pending).toContainText("has not been confirmed"); await expect(pending.getByRole("button", { name: /Discard/u })).toHaveCount(0); await expect(page).toHaveURL(originalUrl); await pending.getByRole("button", { name: "Keep editing", exact: true }).click(); await expect(review).toContainText("7 pieces"); await expect(review.getByRole("button", { name: "Back to item", exact: true })).toBeDisabled(); await review.getByRole("button", { name: "Retry unchanged observation", exact: true }).click(); await expect(stock.getByText("Physical count saved", { exact: true })).toBeVisible(); expect(countRequests).toHaveLength(2); expect(countRequests[0]!.key).toBeTruthy(); expect(countRequests[1]).toEqual(countRequests[0]); expect(await workspace.readStock()).toMatchObject({ quantity: 7, version: beforeStock.version + 1 }); expect(await workspace.readBom()).toEqual(beforeBom); await expectConditionalGap(workspace); await stock.getByRole("button", { name: `Return to ${originalName}`, exact: true }).click(); await expect(editor.getByLabel("Requirement name", { exact: true })).toHaveValue(originalName); await expect(editor.getByLabel("Required quantity", { exact: true })).toHaveValue("2"); await editor.getByRole("button", { name: "Cancel", exact: true }).click(); await expect(page.getByRole("alertdialog")).toHaveCount(0); // The resolved child entry must not leave a stale navigation blocker. await page.goBack(); await expect(page).not.toHaveURL(originalUrl); await expect(page.getByRole("alertdialog")).toHaveCount(0); }); }); }