import { expect, test, type Page } from "@playwright/test"; const sourceFile = { id: "fixture-archive", tenant_id: "fixture-tenant", owner_type: "user", owner_id: "fixture-user", display_path: "small-archive.zip", filename: "small-archive.zip", size_bytes: 280, content_type: "application/zip", checksum_sha256: "a".repeat(64), version_id: "fixture-version-7", created_at: "2026-01-01T12:00:00Z", updated_at: "2026-01-01T12:00:00Z", audit_relevant: false, shares: [], metadata: {}, deleted_at: null }; type RecordedCall = { path: string; body: Record; contentType: string }; async function installFileFixtures(page: Page, options: { encrypted?: boolean; failPreviewOnce?: boolean; failConfirmation?: boolean; holdPreview?: Promise; holdConfirmation?: Promise; progress?: { current: Record | null }; staged?: boolean } = {}) { const calls: RecordedCall[] = []; const forbiddenRequests: string[] = []; const importedFiles: Record[] = []; const releasedStages: string[] = []; let previewFailed = false; // A broad **/api/** glob also matches Vite's /@fs/.../src/api/*.ts. // Restrict interception to actual API URLs so real module code is rendered. await page.route((url) => url.pathname.startsWith("/api/"), async (route) => { const request = route.request(); const url = new URL(request.url()); const path = url.pathname; if (request.method() === "DELETE" && path.startsWith("/api/v1/files/archive-staging/")) { releasedStages.push(path.split("/").slice(-1)[0]); await route.fulfill({ status: 204 }); return; } if (request.method() === "GET") { if (path.startsWith("/api/v1/files/archive-progress/")) { const operationId = path.split("/").slice(-1)[0]; const confirmation = calls.find((call) => call.path.endsWith("/archive-confirm") && call.body.operation_id === operationId); if (confirmation && options.progress?.current) await route.fulfill({ json: options.progress.current }); else await route.fulfill({ status: 404, json: { detail: "No measured progress available" } }); return; } if (path === "/api/v1/files/spaces") { await route.fulfill({ json: { spaces: [ { id: "user:fixture-user", label: "My files", owner_type: "user", owner_id: "fixture-user", space_type: "managed" }, { id: "group:fixture-team", label: "Team files", owner_type: "group", owner_id: "fixture-team", space_type: "managed" } ] } }); return; } const team = url.searchParams.get("owner_id") === "fixture-team"; if (path === "/api/v1/files/folders") { await route.fulfill({ json: { folders: [{ id: team ? "team-output" : "personal-output", path: "extracted", owner_type: team ? "group" : "user", owner_id: team ? "fixture-team" : "fixture-user", name: "extracted", created_at: sourceFile.created_at, updated_at: sourceFile.updated_at }], total: 1, next_cursor: null } }); return; } if (path === "/api/v1/files") { const files = team ? importedFiles : [sourceFile]; await route.fulfill({ json: { files, total: files.length, next_cursor: null } }); return; } } if (request.method() === "POST" && (path.endsWith("/fixture-archive/archive-preview") || path.endsWith("/fixture-archive/archive-confirm") || path === "/api/v1/files/archive-preview" || path === "/api/v1/files/archive-confirm")) { const contentType = request.headers()["content-type"] ?? ""; let body: Record; if (contentType.startsWith("application/json")) body = request.postDataJSON() as Record; else { const form = await new Request(request.url(), { method: "POST", headers: { "content-type": contentType }, body: new Uint8Array(request.postDataBuffer()!) }).formData(); body = Object.fromEntries([...form.entries()].map(([key, value]) => [key, typeof value === "string" ? value : value.name])); if (typeof body.selected_paths_json === "string") body.selected_paths = JSON.parse(body.selected_paths_json); } calls.push({ path, body, contentType: request.headers()["content-type"] ?? "" }); if (path.endsWith("/archive-preview")) { if (options.holdPreview) await options.holdPreview; if (options.failPreviewOnce && !previewFailed) { previewFailed = true; await route.fulfill({ status: 400, json: { detail: "Managed archive version changed; reload and preview again" } }); return; } await route.fulfill({ json: { preview_token: `fixture-preview-${calls.length}`, archive_format: "zip", file_count: 2, directory_count: 1, compressed_size_bytes: 280, expanded_size_bytes: 6, expires_at: "2099-01-01T12:00:00Z", requires_password: Boolean(options.encrypted), password_verified: Boolean(body.password), ...(options.staged ? { staged_upload_id: "fixture-staged-archive" } : {}), entries: [ { path: "folder", kind: "directory", size_bytes: 0, encrypted: false }, { path: "folder/one.txt", kind: "file", size_bytes: 3, compressed_size_bytes: 5, encrypted: Boolean(options.encrypted) }, { path: "two.txt", kind: "file", size_bytes: 3, compressed_size_bytes: 5, encrypted: Boolean(options.encrypted) } ] } }); return; } if (options.holdConfirmation) await options.holdConfirmation; if (options.failConfirmation) { await route.fulfill({ status: 400, json: { detail: "Target file already exists: extracted/folder/one.txt" } }); return; } for (const [index, selected] of (body.selected_paths as string[]).entries()) { importedFiles.push({ ...sourceFile, id: `imported-${index}`, owner_type: body.owner_type, owner_id: body.owner_id, display_path: `${body.path}/${selected}`, filename: selected.split("/").slice(-1)[0], version_id: `imported-version-${index}` }); } await route.fulfill({ json: { files: importedFiles } }); return; } forbiddenRequests.push(`${request.method()} ${path}`); await route.fulfill({ status: 403, json: { detail: "Unexpected fixture request; no real API access permitted" } }); }); return { calls, forbiddenRequests, importedFiles, releasedStages }; } async function openExistingArchive(page: Page, language = "en", contextMenu = false) { const name = language === "de" ? "Archiv entpacken" : "Unpack archive"; await page.goto(`/?managed-archive&language=${language}`); const row = page.locator(".file-row").filter({ hasText: "small-archive.zip" }); await expect(row).toBeVisible(); await expect(page.getByRole("button", { name, exact: true })).toHaveCount(0); await row.click({ button: contextMenu ? "right" : "left" }); if (contextMenu) await page.getByRole("menuitem", { name, exact: true }).click(); else await page.getByRole("button", { name, exact: true }).click(); const dialog = page.getByRole("dialog", { name, exact: true }); await expect(dialog).toBeVisible(); await expect(dialog).toContainText("small-archive.zip"); await expect(dialog.locator('input[type="file"]')).toHaveCount(0); await dialog.locator("select").selectOption("group:fixture-team"); await dialog.getByRole("button", { name: "extracted", exact: true }).click(); return dialog; } for (const language of ["en", "de"]) { test(`real Files page unpacks a selected managed archive without re-upload in ${language}`, async ({ page }) => { let finish!: () => void; const holdConfirmation = new Promise((resolve) => { finish = resolve; }); const fixture = await installFileFixtures(page, { holdConfirmation }); const dialog = await openExistingArchive(page, language); await dialog.getByRole("button", { name: language === "de" ? "Archivvorschau" : "Preview archive", exact: true }).click(); await expect(dialog.locator(".archive-entry-row")).toHaveCount(3); await dialog.locator(".archive-entry-row").filter({ hasText: "two.txt" }).locator('input[type="checkbox"]').uncheck(); await dialog.getByRole("button", { name: "Import selected", exact: true }).click(); await expect.poll(() => fixture.calls.filter((call) => call.path.endsWith("/archive-confirm")).length).toBe(1); await expect(dialog.getByRole("button", { name: "Import selected", exact: true, includeHidden: true })).toBeDisabled(); await expect(dialog.locator(".loading-frame-overlay")).toBeVisible(); await expect(dialog.locator(".loading-frame-overlay")).toHaveCSS("backdrop-filter", /blur/); await expect(dialog.locator(".loading-envelope")).toHaveCount(0); await expect(dialog.locator("[inert]")).toHaveCount(1); await expect(dialog.locator(".dialog-close")).toBeDisabled(); await expect(dialog.getByRole("progressbar")).not.toHaveAttribute("value"); await page.keyboard.press("Escape"); await expect(dialog).toBeVisible(); finish(); await expect(dialog).toHaveCount(0); await expect(page.locator(".file-row").filter({ hasText: "small-archive.zip" })).toBeVisible(); expect(fixture.calls).toHaveLength(2); expect(fixture.calls.every((call) => call.contentType.startsWith("application/json"))).toBe(true); expect(fixture.calls[0].body).toEqual({ source_version_id: "fixture-version-7", owner_type: "group", owner_id: "fixture-team", path: "extracted" }); expect(fixture.calls[1].body).toEqual({ ...fixture.calls[0].body, preview_token: "fixture-preview-1", selected_paths: ["folder/one.txt"], operation_id: expect.any(String) }); expect(fixture.importedFiles.map((file) => file.display_path)).toEqual(["extracted/folder/one.txt"]); expect(fixture.forbiddenRequests).toEqual([]); }); } test("managed context-menu flow verifies a ZIP password in the shared dialog", async ({ page }) => { const fixture = await installFileFixtures(page, { encrypted: true }); const dialog = await openExistingArchive(page, "en", true); await dialog.getByRole("button", { name: "Preview archive", exact: true }).click(); await expect(dialog.getByRole("button", { name: "Import selected", exact: true })).toBeDisabled(); await dialog.locator('input[type="password"]').fill("fixture-only-password"); await dialog.getByRole("button", { name: "Verify password", exact: false }).click(); await expect.poll(() => fixture.calls.length).toBe(2); await expect(dialog.getByRole("button", { name: "Import selected", exact: true })).toBeEnabled(); await dialog.getByRole("button", { name: "Import selected", exact: true }).click(); await expect(dialog).toHaveCount(0); expect(fixture.calls[1].body.password).toBe("fixture-only-password"); expect(fixture.calls[2].body.password).toBe("fixture-only-password"); expect(fixture.calls[2].body.preview_token).toBe("fixture-preview-2"); expect(fixture.calls.every((call) => call.body.source_version_id === "fixture-version-7")).toBe(true); expect(fixture.forbiddenRequests).toEqual([]); }); test("cancelling an uploaded archive releases only its temporary stage without importing", async ({ page }) => { const fixture = await installFileFixtures(page, { staged: true }); await page.goto("/?managed-archive&language=en"); await expect(page.locator(".file-row").filter({ hasText: "small-archive.zip" })).toBeVisible(); await page.getByRole("button", { name: "Upload", exact: true }).click(); const dialog = page.getByRole("dialog"); await dialog.getByRole("checkbox", { name: "Preview and unpack archive", exact: true }).focus(); await page.keyboard.press("Space"); await dialog.locator('input[type="file"]').setInputFiles({ name: "cancelled.zip", mimeType: "application/zip", buffer: Buffer.from("fixture archive") }); await expect(dialog.locator(".archive-entry-row")).toHaveCount(3); await dialog.getByRole("button", { name: "Cancel", exact: true }).click(); await expect(dialog).toHaveCount(0); await expect.poll(() => fixture.releasedStages).toEqual(["fixture-staged-archive"]); expect(fixture.calls.filter((call) => call.path.endsWith("/archive-confirm"))).toHaveLength(0); expect(fixture.importedFiles).toEqual([]); expect(fixture.forbiddenRequests).toEqual([]); }); test("managed preview and confirmation failures stay visible inside the real dialog", async ({ page }) => { const fixture = await installFileFixtures(page, { failPreviewOnce: true, failConfirmation: true }); const dialog = await openExistingArchive(page); await dialog.getByRole("button", { name: "Preview archive", exact: true }).click(); await expect(dialog.getByText(/Managed archive version changed/)).toBeVisible(); await expect(dialog).toContainText("small-archive.zip"); await dialog.getByRole("button", { name: "Preview archive", exact: true }).click(); await expect(dialog.locator(".archive-entry-row")).toHaveCount(3); await dialog.getByRole("button", { name: "Import selected", exact: true }).click(); await expect(dialog.getByText(/Target file already exists/)).toBeVisible(); await expect(dialog.locator(".loading-frame-overlay")).toHaveCount(0); await expect(dialog.locator("[inert]")).toHaveCount(0); await expect(dialog.locator(".dialog-close")).toBeEnabled(); await expect(dialog.getByRole("button", { name: "Import selected", exact: true })).toBeEnabled(); await dialog.getByRole("button", { name: "Change destination", exact: true }).click(); await expect(dialog.getByRole("button", { name: "Preview archive", exact: true })).toBeVisible(); await expect(dialog.locator(".archive-entry-row")).toHaveCount(0); expect(fixture.importedFiles).toEqual([]); expect(fixture.forbiddenRequests).toEqual([]); }); test("managed extraction is unavailable without the download permission", async ({ page }) => { const fixture = await installFileFixtures(page); await page.goto("/?managed-archive&language=en&no-download"); await page.locator(".file-row").filter({ hasText: "small-archive.zip" }).click(); await expect(page.getByRole("button", { name: "Unpack archive", exact: true })).toBeDisabled(); expect(fixture.calls).toEqual([]); expect(fixture.forbiddenRequests).toEqual([]); }); test("managed preview covers and blurs existing dialog controls without fabricating progress", async ({ page }) => { let release!: () => void; const holdPreview = new Promise((resolve) => { release = resolve; }); const fixture = await installFileFixtures(page, { holdPreview }); const dialog = await openExistingArchive(page); await dialog.getByRole("button", { name: "Preview archive", exact: true }).click(); const overlay = dialog.locator(".loading-frame-overlay"); await expect(overlay).toBeVisible(); await expect(overlay).toContainText("Inspecting the archive"); await expect(dialog.getByRole("progressbar")).not.toHaveAttribute("value"); await expect(dialog.locator(".loading-envelope")).toHaveCount(0); await expect(dialog.locator(".dialog-close")).toBeDisabled(); await page.keyboard.press("Escape"); await expect(dialog).toBeVisible(); release(); await expect(overlay).toHaveCount(0); await expect(dialog.locator(".archive-entry-row")).toHaveCount(3); expect(fixture.forbiddenRequests).toEqual([]); }); test("actual server counters update the blurred overlay and finalization never claims completion", async ({ page }) => { let release!: () => void; const holdConfirmation = new Promise((resolve) => { release = resolve; }); const progress = { current: { phase: "extracting", completed_files: 1, total_files: 2, completed_bytes: 3, total_bytes: 6, status: "running" } as Record }; const fixture = await installFileFixtures(page, { holdConfirmation, progress }); const dialog = await openExistingArchive(page); await dialog.getByRole("button", { name: "Preview archive", exact: true }).click(); await expect(dialog.locator(".archive-entry-row")).toHaveCount(3); await dialog.getByRole("button", { name: "Import selected", exact: true }).click(); await expect(dialog.getByRole("progressbar")).toHaveAttribute("value", "50"); await expect(dialog.locator(".loading-frame-overlay")).toContainText("1 of 2 files"); await expect(dialog.locator(".loading-frame-overlay")).toContainText("3 B of 6 B"); progress.current = { ...progress.current, phase: "finalizing", completed_files: 2, completed_bytes: 6 }; await expect(dialog.locator(".loading-frame-overlay")).toContainText("Finalizing and committing changes"); await expect(dialog.getByRole("progressbar")).not.toHaveAttribute("value"); await expect(dialog).toBeVisible(); release(); await expect(dialog).toHaveCount(0); expect(fixture.calls.filter((call) => call.path.endsWith("/archive-confirm"))).toHaveLength(1); expect(fixture.forbiddenRequests).toEqual([]); }); test("new archive upload preview and confirmation use the same no-envelope progress overlay", async ({ page }) => { let previewReady!: () => void; let importReady!: () => void; const holdPreview = new Promise((resolve) => { previewReady = resolve; }); const holdConfirmation = new Promise((resolve) => { importReady = resolve; }); const progress = { current: { phase: "storing", completed_files: 1, total_files: 2, completed_bytes: 3, total_bytes: 6, status: "running" } as Record }; const fixture = await installFileFixtures(page, { holdPreview, holdConfirmation, progress, staged: true }); await page.goto("/?managed-archive&language=en"); await expect(page.locator(".file-row").filter({ hasText: "small-archive.zip" })).toBeVisible(); await page.getByRole("button", { name: "Upload", exact: true }).click(); const dialog = page.getByRole("dialog"); await dialog.getByRole("checkbox", { name: "Preview and unpack archive", exact: true }).focus(); await page.keyboard.press("Space"); await dialog.locator('input[type="file"]').setInputFiles({ name: "new-archive.zip", mimeType: "application/zip", buffer: Buffer.from("fixture ZIP bytes; backend is intercepted") }); await expect(dialog.locator(".loading-frame-overlay")).toBeVisible(); await expect(dialog.locator(".loading-envelope")).toHaveCount(0); await expect(dialog.locator(".dialog-close")).toBeDisabled(); await expect(dialog.getByRole("progressbar")).not.toHaveAttribute("value", "100"); previewReady(); await expect(dialog.locator(".archive-entry-row")).toHaveCount(3); await expect(dialog.locator(".loading-frame-overlay")).toHaveCount(0); await dialog.getByRole("button", { name: "Import selected", exact: true }).click(); await expect(dialog.locator(".loading-frame-overlay")).toContainText("Storing extracted files"); await expect(dialog.getByRole("progressbar")).toHaveAttribute("value", "50"); await expect(dialog.locator(".loading-envelope")).toHaveCount(0); importReady(); await expect(dialog).toHaveCount(0); expect(fixture.calls.filter((call) => call.path === "/api/v1/files/archive-confirm")).toHaveLength(1); const confirmation = fixture.calls.find((call) => call.path === "/api/v1/files/archive-confirm")!; expect(confirmation.body.staged_upload_id).toBe("fixture-staged-archive"); expect(confirmation.body.file).toBeUndefined(); expect(fixture.forbiddenRequests).toEqual([]); });