import { clickProjectAction, showProjectAction, navigateWorkspace } from "./workspace-controls"; import { expect, test, type Page } from "@playwright/test"; const project = { id: "project-removal-e2e", name: "Retained Archive E2E", description: "A project kept for removal-flow coverage.", status: "archived", currentRevisionId: "revision-removal-e2e", updatedAt: "2026-09-01T10:00:00.000Z", createdAt: "2026-09-01T09:00:00.000Z", version: 4, workItems: [{ id: "work-removal-e2e", projectId: "project-removal-e2e", name: "Removal fixture", kind: "project", description: "Retained history fixture." }], currentRevision: { id: "revision-removal-e2e", projectId: "project-removal-e2e", number: 1, name: "Initial", status: "concept", createdAt: "2026-09-01T09:00:00.000Z", version: 1, bom: [], artifacts: [] } }; async function mockRemovalWorkspace(page: Page): Promise<{ readonly deleteRequest: () => { readonly body: unknown; readonly ifMatch: string | undefined; readonly idempotencyKey: string | undefined } | undefined }> { let deleted: { readonly body: unknown; readonly ifMatch: string | undefined; readonly idempotencyKey: string | undefined } | undefined; await page.route("**/api/v1/auth/access", async (route) => { if (route.request().method() !== "GET") return route.continue(); await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ mode: "lan_open", passwordConfigured: false, version: 1 }) }); }); await page.route("**/api/v1/auth/lan-session", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ authenticated: true, actor: "e2e", csrfToken: "csrf-removal-e2e", expiresAt: "2026-09-01T18:00:00.000Z" }) }); }); await page.route("**/api/v1/auth/session", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ authenticated: true, actor: "e2e", source: "ui", scopes: ["read", "write"] }) }); }); await page.route("**/api/v1/health", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ status: "ok", service: "benchledger", version: "e2e", demo: false, now: "2026-09-01T10:00:00.000Z" }) }); }); await page.route("**/api/v1/workspace", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ source: "api", fetchedAt: "2026-09-01T10:00:00.000Z", inventory: [], projects: [], offers: [] }) }); }); await page.route("**/api/v1/projects**", async (route) => { if (route.request().method() === "DELETE" && new URL(route.request().url()).pathname.endsWith("/projects/project-removal-e2e")) { deleted = { body: route.request().postDataJSON(), ifMatch: route.request().headers()["if-match"], idempotencyKey: route.request().headers()["idempotency-key"] }; await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ data: { id: project.id, name: project.name, removedAt: "2026-09-01T10:01:00.000Z", removedBy: "e2e", lastLifecycleStatus: "archived", releasedReservationIds: ["reservation-e2e"], version: 5, auditId: "audit-removal-e2e" }, audit: { id: "audit-removal-e2e" }, correlationId: "correlation-removal-e2e", replayed: false }) }); return; } if (route.request().method() !== "GET") return route.continue(); const requestUrl = new URL(route.request().url()); const archived = requestUrl.searchParams.get("status") === "archived" && !deleted; await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ data: archived ? [project] : [], limit: 200, total: archived ? 1 : 0 }) }); }); await page.route("**/api/v1/inventory/categories*", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ data: [], limit: 200, total: 0 }) }); }); await page.route("**/api/v1/project-library?**", route => route.fulfill({ json: { data: new URL(route.request().url()).searchParams.get("status") === "archived" && !deleted ? [project] : [] } })); return { deleteRequest: () => deleted }; } async function mockRestoreWorkspace(page: Page): Promise<{ readonly restoreRequests: () => number }> { let restoreRequests = 0; await page.route("**/api/v1/auth/access", async (route) => { if (route.request().method() !== "GET") return route.continue(); await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ mode: "lan_open", passwordConfigured: false, version: 1 }) }); }); await page.route("**/api/v1/auth/lan-session", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ authenticated: true, actor: "e2e", csrfToken: "csrf-restore-e2e", expiresAt: "2026-09-01T18:00:00.000Z" }) }); }); await page.route("**/api/v1/auth/session", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ authenticated: true, actor: "e2e", source: "ui", scopes: ["read", "write"] }) }); }); await page.route("**/api/v1/health", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ status: "ok", service: "benchledger", version: "e2e", demo: false, now: "2026-09-01T10:00:00.000Z" }) }); }); await page.route("**/api/v1/workspace", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ source: "api", fetchedAt: "2026-09-01T10:00:00.000Z", inventory: [], projects: [], offers: [] }) }); }); await page.route("**/api/v1/projects**", async (route) => { if (route.request().method() === "POST" && new URL(route.request().url()).pathname.endsWith("/projects/project-removal-e2e/restore")) { restoreRequests += 1; await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ data: { ...project, status: "idea", version: 5, updatedAt: "2026-09-01T10:01:00.000Z" }, audit: { id: "audit-restore-e2e" }, correlationId: "correlation-restore-e2e", replayed: false }) }); return; } if (route.request().method() !== "GET") return route.continue(); const requestUrl = new URL(route.request().url()); const archived = requestUrl.searchParams.get("status") === "archived"; await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ data: archived ? [project] : [], limit: 200, total: archived ? 1 : 0 }) }); }); await page.route("**/api/v1/inventory/categories*", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ data: [], limit: 200, total: 0 }) }); }); await page.route("**/api/v1/project-library?**", route => route.fulfill({ json: { data: new URL(route.request().url()).searchParams.get("status") === "archived" ? (restoreRequests ? [] : [project]) : (restoreRequests ? [{ ...project, status: "idea", version: 5 }] : []) } })); return { restoreRequests: () => restoreRequests }; } test.describe("restore confirmation", () => { test.use({ viewport: { width: 390, height: 844 } }); test("restores an archived project only after confirmation", async ({ page }) => { const harness = await mockRestoreWorkspace(page); await page.goto("/"); await navigateWorkspace(page, "Projects"); await expect(page.getByRole("heading", { name: "Projects", exact: true })).toBeVisible(); await page.getByRole("button", { name: "Archived", exact: true }).click(); await page.getByRole("button", { name: "Open project Retained Archive E2E", exact: true }).click(); await expect(page.getByRole("heading", { name: "Retained Archive E2E", exact: true })).toBeVisible(); await expect(page.locator("#main-content")).not.toContainText(/reservation|tombstone|audit/iu); const restoreButton = page.getByRole("button", { name: "Restore project", exact: true }); await restoreButton.click(); expect(harness.restoreRequests()).toBe(0); const dialog = page.getByRole("alertdialog", { name: "Restore Retained Archive E2E?" }); await expect(dialog).toContainText("This moves the project to Idea. Stock released when it was archived will not be set aside again."); await expect(dialog).not.toContainText(/reservation|tombstone|audit/iu); await expect(dialog.getByRole("button", { name: "Close dialog" })).toBeFocused(); await expect(dialog.getByRole("button", { name: "Cancel", exact: true })).toBeVisible(); await expect(dialog.getByRole("button", { name: "Restore project", exact: true })).toBeVisible(); await page.keyboard.press("Escape"); await expect(dialog).toHaveCount(0); await expect(restoreButton).toBeFocused(); expect(harness.restoreRequests()).toBe(0); await restoreButton.click(); await page.getByRole("alertdialog", { name: "Restore Retained Archive E2E?" }).getByRole("button", { name: "Restore project", exact: true }).click(); expect(harness.restoreRequests()).toBe(1); await expect(page.getByText("Retained Archive E2E was restored to Idea. Previously released stock was not set aside again.", { exact: true })).toBeVisible(); await expect(await showProjectAction(page, "Archive project")).toBeVisible(); }); }); test("deletes an archived project after a single confirmation and hides it from Archived", async ({ page }) => { const harness = await mockRemovalWorkspace(page); await page.goto("/"); await navigateWorkspace(page, "Projects"); await expect(page.getByRole("heading", { name: "Projects", exact: true })).toBeVisible(); await page.getByRole("button", { name: "Archived", exact: true }).click(); await page.getByRole("button", { name: "Open project Retained Archive E2E", exact: true }).click(); await expect(page.getByRole("heading", { name: "Retained Archive E2E", exact: true })).toBeVisible(); await clickProjectAction(page, "Delete project"); await page.getByRole("button", { name: "Delete project permanently", exact: true }).click(); let dialog = page.getByRole("alertdialog", { name: "Delete Retained Archive E2E?" }); await expect(dialog).toContainText("This action is irreversible."); await expect(dialog).not.toContainText(/reservation|tombstone|audit/iu); await dialog.getByRole("button", { name: "Cancel", exact: true }).click(); expect(harness.deleteRequest()).toBeUndefined(); await clickProjectAction(page, "Delete project"); await page.getByRole("button", { name: "Delete project permanently", exact: true }).click(); dialog = page.getByRole("alertdialog", { name: "Delete Retained Archive E2E?" }); const removeButton = dialog.getByRole("button", { name: "Delete project", exact: true }); await expect(removeButton).toBeEnabled(); await removeButton.click(); expect(harness.deleteRequest()).toMatchObject({ body: { name: "Retained Archive E2E" }, ifMatch: "4" }); expect(harness.deleteRequest()?.idempotencyKey).toBeTruthy(); await expect(page.getByRole("heading", { name: "Projects", exact: true })).toBeVisible(); await expect.poll(() => new URL(page.url()).hash).toBe("#/"); await expect(page.getByRole("region", { name: "Getting started", exact: true })).toBeVisible(); await expect(page.getByRole("region", { name: "Project navigator", exact: true })).toHaveCount(0); await expect(page.getByRole("button", { name: "View archived projects", exact: true })).toHaveCount(0); await expect(page.getByRole("button", { name: "Open project Retained Archive E2E", exact: true })).toHaveCount(0); await navigateWorkspace(page, "Settings"); await page.goBack(); await expect(page.getByRole("heading", { name: "Projects", exact: true })).toBeVisible(); await expect(page.getByRole("region", { name: "Getting started", exact: true })).toBeVisible(); await expect(page.getByRole("button", { name: "Open project Retained Archive E2E", exact: true })).toHaveCount(0); await page.goForward(); await expect.poll(() => new URL(page.url()).hash).toBe("#/settings"); await expect(page.getByRole("region", { name: "Project navigator", exact: true })).toHaveCount(0); }); test("restores the active or archived project named by browser history", async ({ page }) => { const activeProject = { ...project, id: "project-active-e2e", name: "Active Project E2E", status: "idea", currentRevisionId: "revision-active-e2e", workItems: [], currentRevision: { ...project.currentRevision, id: "revision-active-e2e", projectId: "project-active-e2e", bom: [], artifacts: [], }, }; await page.route("**/api/v1/auth/access", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ mode: "lan_open", passwordConfigured: false, version: 1 }) }); }); await page.route("**/api/v1/auth/lan-session", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ authenticated: true, actor: "e2e", csrfToken: "csrf-history-e2e", expiresAt: "2026-09-01T18:00:00.000Z" }) }); }); await page.route("**/api/v1/auth/session", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ authenticated: true, actor: "e2e", source: "ui", scopes: ["read", "write"] }) }); }); await page.route("**/api/v1/health", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ status: "ok", service: "benchledger", version: "e2e", demo: false, now: "2026-09-01T10:00:00.000Z" }) }); }); await page.route("**/api/v1/workspace", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ source: "api", fetchedAt: "2026-09-01T10:00:00.000Z", inventory: [], projects: [activeProject], offers: [] }) }); }); await page.route("**/api/v1/projects**", async (route) => { const archived = new URL(route.request().url()).searchParams.get("status") === "archived"; await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ data: archived ? [project] : [activeProject], limit: 200, total: 1 }) }); }); await page.route("**/api/v1/inventory/categories*", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ data: [], limit: 200, total: 0 }) }); }); await page.route("**/api/v1/project-library?**", route => route.fulfill({ json: { data: new URL(route.request().url()).searchParams.get("status") === "archived" ? [project] : [activeProject] } })); await page.goto(`/#/projects/${project.id}/files`); await expect(page.getByRole("heading", { name: project.name, exact: true })).toBeVisible(); await navigateWorkspace(page, "Projects"); await page.getByRole("button", { name: "Active", exact: true }).click(); await page.getByRole("button", { name: `Open project ${activeProject.name}`, exact: true }).click(); await expect(page.getByRole("heading", { name: activeProject.name, exact: true })).toBeVisible(); await navigateWorkspace(page, "Projects"); await page.getByRole("button", { name: "Archived", exact: true }).click(); await page.getByRole("button", { name: `Open project ${project.name}`, exact: true }).click(); await expect(page.getByRole("heading", { name: project.name, exact: true })).toBeVisible(); await page.goBack(); await expect(page.getByRole("heading", { name: "Projects", exact: true })).toBeVisible(); await page.goBack(); await expect(page.getByRole("heading", { name: activeProject.name, exact: true })).toBeVisible(); await expect(page.locator(".archive-notice")).toHaveCount(0); await page.goBack(); await expect(page.getByRole("heading", { name: "Projects", exact: true })).toBeVisible(); await page.goBack(); await expect(page.getByRole("heading", { name: project.name, exact: true })).toBeVisible(); await expect(page.locator(".archive-notice")).toBeVisible(); await expect(page.getByRole("tab", { name: "Files", exact: true })).toHaveAttribute("aria-selected", "true"); });