feat: consolidate shared UI and harden browser authority for release
This commit is contained in:
@@ -0,0 +1,296 @@
|
||||
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<string, unknown>; contentType: string };
|
||||
|
||||
async function installFileFixtures(page: Page, options: { encrypted?: boolean; failPreviewOnce?: boolean; failConfirmation?: boolean; holdPreview?: Promise<void>; holdConfirmation?: Promise<void>; progress?: { current: Record<string, unknown> | null }; staged?: boolean } = {}) {
|
||||
const calls: RecordedCall[] = [];
|
||||
const forbiddenRequests: string[] = [];
|
||||
const importedFiles: Record<string, unknown>[] = [];
|
||||
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<string, unknown>;
|
||||
if (contentType.startsWith("application/json")) body = request.postDataJSON() as Record<string, unknown>;
|
||||
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<void>((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<void>((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<void>((resolve) => { release = resolve; });
|
||||
const progress = { current: { phase: "extracting", completed_files: 1, total_files: 2, completed_bytes: 3, total_bytes: 6, status: "running" } as Record<string, unknown> };
|
||||
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<void>((resolve) => { previewReady = resolve; });
|
||||
const holdConfirmation = new Promise<void>((resolve) => { importReady = resolve; });
|
||||
const progress = { current: { phase: "storing", completed_files: 1, total_files: 2, completed_bytes: 3, total_bytes: 6, status: "running" } as Record<string, unknown> };
|
||||
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([]);
|
||||
});
|
||||
Reference in New Issue
Block a user