import { expect, test, type Page } from "@playwright/test"; const archive = { 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 }; async function fixtures(page: Page, connector = false) { const reads: string[] = []; const forbidden: string[] = []; const state = { fail: false }; await page.route((url) => url.pathname.startsWith("/api/"), async (route) => { const request = route.request(); const url = new URL(request.url()); const path = url.pathname; const readOnlyPattern = request.method() === "POST" && path === "/api/v1/files/resolve-patterns"; if (request.method() !== "GET" && !readOnlyPattern) { forbidden.push(`${request.method()} ${path}`); await route.fulfill({ status: 403, json: { detail: "Fixture refuses all writes" } }); return; } reads.push(`${request.method()} ${path}${url.search}`); if (state.fail && (path === "/api/v1/files" || path.endsWith("/browse"))) { await route.fulfill({ status: 503, json: { detail: "Fixture listing temporarily unavailable" } }); 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" }, ...(connector ? [{ id: "connector:fixture-remote", label: "Remote archive", owner_type: "user", owner_id: "fixture-user", space_type: "connector", connector_space_id: "fixture-remote", connector_profile_id: "fixture-profile", remote_path: "source", provider: "s3", read_only: true, sync_mode: "manual" }] : []) ] } }); } else if (path === "/api/v1/files/folders") { await route.fulfill({ json: { folders: [], total: 0, next_cursor: null } }); } else if (path === "/api/v1/files") { await route.fulfill({ json: { files: [archive], total: 1, next_cursor: null } }); } else if (readOnlyPattern) { await route.fulfill({ json: { patterns: [{ pattern: "*.zip", matches: [archive] }], unmatched: [] } }); } else if (path === "/api/v1/files/connectors/profiles") { await route.fulfill({ json: { profiles: [] } }); } else if (path.endsWith("/fixture-profile/browse")) { const folder = url.searchParams.get("path") ?? "source"; await route.fulfill({ json: { profile_id: "fixture-profile", provider: "s3", path: folder, library_id: null, read_only: true, has_more: false, decision: { allowed: true }, items: folder.endsWith("/nested") ? [{ kind: "file", name: "remote-letter.pdf", path: "source/nested/remote-letter.pdf", metadata: {}, size_bytes: 42 }] : [{ kind: "folder", name: "nested", path: "source/nested", metadata: {} }] } }); } else { forbidden.push(`${request.method()} ${path}`); await route.fulfill({ status: 404, json: { detail: "Unconfigured fixture read" } }); } }); return { reads, forbidden, state }; } const header = (page: Page) => page.locator('[data-interface-id="files.workspace.actions"]'); const archiveRow = (page: Page) => page.locator(".file-row").filter({ hasText: "small-archive.zip" }); test("Files global Reload, Create folder and primary Upload remain above both workspace panes", async ({ page }) => { await page.setViewportSize({ width: 1280, height: 900 }); const fixture = await fixtures(page); await page.goto("/?files-toolbar&language=en"); await expect(archiveRow(page)).toBeVisible(); const toolbar = header(page); await expect(toolbar).toHaveAttribute("data-workspace-action-scope", "workspace"); await expect(toolbar.getByRole("link", { name: "Open user documentation", exact: true })).toBeVisible(); await expect(toolbar.locator('[data-page-action-group="trailing"] button')).toHaveText(["Reload", /Create Folder/, /Upload/]); const [bar, reload, create, upload, tree] = await Promise.all([ toolbar.boundingBox(), toolbar.getByRole("button", { name: "Reload", exact: true }).boundingBox(), toolbar.getByRole("button", { name: "Create Folder", exact: true }).boundingBox(), toolbar.getByRole("button", { name: "Upload", exact: true }).boundingBox(), page.locator(".file-tree-panel").boundingBox() ]); expect(bar && reload && create && upload && tree).toBeTruthy(); expect(reload!.x + reload!.width).toBeLessThanOrEqual(create!.x); expect(create!.x + create!.width).toBeLessThanOrEqual(upload!.x); expect(upload!.x + upload!.width).toBeGreaterThan(bar!.x + bar!.width - 24); expect(bar!.y + bar!.height).toBeLessThanOrEqual(tree!.y + 1); await expect(toolbar.getByRole("button", { name: "Upload", exact: true })).toHaveClass(/primary/); await expect(page.locator('.file-list-sticky [data-workspace-action-scope="workspace"]')).toHaveCount(0); expect(fixture.forbidden).toEqual([]); }); test("Files selection tools preserve organization, access and confirmed destructive actions", async ({ page }) => { const fixture = await fixtures(page); await page.goto("/?files-toolbar&language=en"); await archiveRow(page).click(); await expect(page.getByRole("button", { name: "Unpack archive", exact: true })).toBeEnabled(); await page.getByRole("button", { name: "Manage selection", exact: true }).click(); const dialog = page.getByRole("dialog", { name: "Manage selection", exact: true }); await expect(dialog).toContainText("1 file"); for (const name of ["Move", "Copy", "Rename", "Manage shares", "Explain access"]) { await expect(dialog.getByRole("button", { name, exact: true })).toBeEnabled(); } const remove = dialog.locator('[data-page-action-separation="destructive"]').getByRole("button", { name: "Delete", exact: true }); await remove.click(); await expect(dialog).toHaveCount(0); await expect(page.getByRole("alertdialog")).toContainText(/delete|Delete/); await page.getByRole("alertdialog").getByRole("button", { name: "Cancel", exact: true }).click(); await archiveRow(page).click({ button: "right" }); await expect(page.getByRole("menuitem", { name: "Unpack archive", exact: true })).toBeVisible(); expect(fixture.forbidden).toEqual([]); }); test("Files Connections and imports exposes explicit tools without starting synchronization", async ({ page }) => { const fixture = await fixtures(page); await page.goto("/?files-toolbar&language=en"); await expect(archiveRow(page)).toBeVisible(); const initialReads = fixture.reads.length; await header(page).getByRole("button", { name: "Connections and imports", exact: true }).click(); const dialog = page.getByRole("dialog", { name: "Connections and imports", exact: true }); await expect(dialog).toContainText("never synchronizes or imports"); await expect(dialog.getByRole("button", { name: "Sync", exact: true })).toBeEnabled(); await dialog.getByRole("button", { name: "Add Space", exact: true }).click(); await expect(page.getByRole("dialog", { name: "Add connector space", exact: true })).toBeVisible(); expect(fixture.reads.slice(initialReads).every((read) => read.startsWith("GET "))).toBe(true); expect(fixture.forbidden).toEqual([]); }); test("Files Reload preserves active pattern and property filters and retains loaded data after failure", async ({ page }) => { const fixture = await fixtures(page); await page.goto("/?files-toolbar&language=en"); await expect(archiveRow(page)).toBeVisible(); await page.locator(".file-search-row input:not([type=checkbox])").fill("*.zip"); await page.locator(".file-search-row").getByRole("button", { name: "Search", exact: true }).click(); await page.getByRole("combobox", { name: "Campaign use", exact: true }).selectOption("linked"); await page.getByRole("button", { name: "Apply filters", exact: true }).click(); await expect(page.getByRole("button", { name: "Clear filters", exact: true })).toBeVisible(); const before = fixture.reads.length; await header(page).getByRole("button", { name: "Reload", exact: true }).click(); await expect(header(page).getByRole("button", { name: "Reload", exact: true })).toBeEnabled(); await expect.poll(() => fixture.reads.slice(before).some((read) => read.startsWith("POST /api/v1/files/resolve-patterns"))).toBe(true); await expect.poll(() => fixture.reads.slice(before).some((read) => read.includes("campaign_usage=linked"))).toBe(true); await expect(header(page).getByRole("button", { name: "Reload", exact: true })).toBeEnabled(); await expect(page.locator(".file-search-row input:not([type=checkbox])")).toHaveValue("*.zip"); await expect(page.getByRole("combobox", { name: "Campaign use", exact: true })).toHaveValue("linked"); fixture.state.fail = true; await header(page).getByRole("button", { name: "Reload", exact: true }).click(); await expect(page.getByText(/Fixture listing temporarily unavailable/)).toBeVisible(); await expect(archiveRow(page)).toBeVisible(); await expect(page.getByRole("button", { name: "Clear filters", exact: true })).toBeVisible(); expect(fixture.forbidden).toEqual([]); }); test("Files reader keeps creation visible with permission reasons and cannot use selected write tools", async ({ page }) => { const fixture = await fixtures(page); await page.goto("/?files-toolbar&read-only&language=en"); await expect(archiveRow(page)).toBeVisible(); const upload = header(page).getByRole("button", { name: "Upload", exact: true }); await expect(upload).toBeDisabled(); await header(page).locator(".disabled-action-tooltip").filter({ has: page.getByRole("button", { name: "Upload", exact: true }) }).focus(); await expect(page.getByRole("tooltip")).toContainText(/upload permission/i); await expect(header(page).getByRole("button", { name: "Create Folder", exact: true })).toBeDisabled(); await archiveRow(page).click(); await expect(page.getByRole("button", { name: "Unpack archive", exact: true })).toBeDisabled(); await page.getByRole("button", { name: "Manage selection", exact: true }).click(); const dialog = page.getByRole("dialog", { name: "Manage selection", exact: true }); for (const name of ["Move", "Copy", "Rename", "Manage shares", "Explain access", "Delete"]) { await expect(dialog.getByRole("button", { name, exact: true })).toBeDisabled(); } expect(fixture.forbidden).toEqual([]); }); test("Files connector Reload only re-browses the selected remote folder and retains it on failure", async ({ page }) => { const fixture = await fixtures(page, true); await page.goto("/?files-toolbar&language=en"); await expect(archiveRow(page)).toBeVisible(); await page.locator(".file-tree-root").filter({ hasText: "Remote archive" }).click(); await page.locator(".file-list-panel").getByText("nested", { exact: true }).dblclick(); await expect(page.getByText("remote-letter.pdf", { exact: true })).toBeVisible(); const before = fixture.reads.length; await header(page).getByRole("button", { name: "Reload", exact: true }).click(); await expect(header(page).getByRole("button", { name: "Reload", exact: true })).toBeEnabled(); await expect.poll(() => fixture.reads.slice(before)).toEqual(["GET /api/v1/files/connectors/profiles/fixture-profile/browse?path=source%2Fnested"]); await expect(header(page).getByRole("button", { name: "Reload", exact: true })).toBeEnabled(); fixture.state.fail = true; await header(page).getByRole("button", { name: "Reload", exact: true }).click(); await expect(page.getByText(/Fixture listing temporarily unavailable/)).toBeVisible(); await expect(page.getByText("remote-letter.pdf", { exact: true })).toBeVisible(); expect(fixture.forbidden).toEqual([]); }); test("Files grouped tools and creation remain reachable without horizontal overflow on narrow German screens", async ({ page }) => { await page.setViewportSize({ width: 390, height: 844 }); const fixture = await fixtures(page); await page.goto("/?files-toolbar&language=de"); await expect(archiveRow(page)).toBeVisible(); await expect(header(page).getByRole("button", { name: "Hochladen", exact: true })).toBeVisible(); await header(page).getByRole("button", { name: "Verbindungen und Importe", exact: true }).click(); const dialog = page.getByRole("dialog", { name: "Verbindungen und Importe", exact: true }); await expect(dialog).toBeVisible(); const overflow = await dialog.evaluate((element) => element.scrollWidth - element.clientWidth); expect(overflow).toBeLessThanOrEqual(1); expect(await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth)).toBeLessThanOrEqual(1); await page.keyboard.press("Escape"); await expect(dialog).toHaveCount(0); expect(fixture.forbidden).toEqual([]); });