/** * Confluence P1 — T-139 (bulk page actions) E2E * * The bulk API (`POST /pages/bulk`) has been integration-tested since 2026-10-11 * (mixed permissions → per-item codes + audit). This spec verifies the WEB * surface that landed in this round: * * - Multi-select in the sidebar tree (per-row checkbox, revealed on hover, * scoped to the current workspace) raises the bulk action bar. * - Choosing an action opens a CONFIRM dialog (bulk ops are batch + audited, * so the dialog always asks before firing). * - Confirming a bulk trash removes every selected page from the tree * (the tree endpoint filters `trashedAt: null`); Canceling does nothing. * * Origin: `https://notion.filps.com` (Caddy single-origin — see the other * confluence-* specs). URL nav uses the workspace SLUG; API calls use the ID. */ import { test, expect } from "../fixtures"; import { createPage } from "../utils/api"; import type { PageDto } from "../fixtures"; test.describe("Confluence P1 — bulk page actions (T-139)", () => { // The per-row checkbox is `invisible group-hover/row:visible` — it only // becomes hit-testable once the row is hovered. Hover the row's title link // (a child of the `group/row` div, so the div gets :hover) to reveal it. async function selectPage(page: import("@playwright/test").Page, id: string, title: string) { const row = page.locator("li", { has: page.getByTestId(`tree-select-${id}`) }); await row.getByTitle(title, { exact: true }).hover(); const cb = row.getByTestId(`tree-select-${id}`); await expect(cb).toBeVisible(); await cb.click(); } test("multi-select 2 pages → confirm bulk trash → both leave the tree", async ({ authPage, api, freshWorkspace }) => { const ws = await freshWorkspace("Bulk Space"); const p1 = (await createPage(api, ws.id, { title: "Bulk Page A" })) as PageDto; const p2 = (await createPage(api, ws.id, { title: "Bulk Page B" })) as PageDto; await authPage.goto(`/w/${ws.slug}/page/${p1.id}`); // Both rows are in the sidebar tree before we touch anything. await selectPage(authPage, p1.id, "Bulk Page A"); await selectPage(authPage, p2.id, "Bulk Page B"); // The bulk bar appears with the live count. const bar = authPage.getByTestId("bulk-action-bar"); await expect(bar).toBeVisible(); await expect(authPage.getByTestId("bulk-count")).toHaveText("2 selected"); // Trash opens the confirm dialog (scoped by its accessible name). await authPage.getByTestId("bulk-trash").click(); const dialog = authPage.getByRole("dialog", { name: /Move to trash 2 pages/ }); await expect(dialog).toBeVisible(); await dialog.getByRole("button", { name: "Move to trash" }).click(); await expect(dialog).toBeHidden(); // The selection cleared and BOTH pages left the tree (trashedAt set → the // tree endpoint's `trashedAt: null` filter drops them). Playwright retries // until the refetch lands. await expect(authPage.getByTestId("bulk-action-bar")).toBeHidden(); await expect(authPage.getByTestId(`tree-select-${p1.id}`)).toHaveCount(0); await expect(authPage.getByTestId(`tree-select-${p2.id}`)).toHaveCount(0); // Server-side confirmation: the live (non-trash) tree no longer lists them. const res = await api.get(`/api/v1/workspaces/${ws.id}/pages`); const body = (await res.json()) as { data: Array<{ id: string }> }; const ids = (body.data ?? []).map((p) => p.id); expect(ids).not.toContain(p1.id); expect(ids).not.toContain(p2.id); }); test("the confirm dialog gates the action — Cancel leaves the pages + selection intact", async ({ authPage, api, freshWorkspace }) => { const ws = await freshWorkspace("Bulk Cancel Space"); const p1 = (await createPage(api, ws.id, { title: "Keep Me" })) as PageDto; await authPage.goto(`/w/${ws.slug}/page/${p1.id}`); await selectPage(authPage, p1.id, "Keep Me"); await authPage.getByTestId("bulk-trash").click(); const dialog = authPage.getByRole("dialog", { name: /Move to trash 1 page/ }); await expect(dialog).toBeVisible(); await dialog.getByRole("button", { name: "Cancel" }).click(); await expect(dialog).toBeHidden(); // Nothing was trashed; the selection is still live (the bar remains). await expect(authPage.getByTestId("bulk-action-bar")).toBeVisible(); await expect(authPage.getByTestId("bulk-count")).toHaveText("1 selected"); const res = await api.get(`/api/v1/workspaces/${ws.id}/pages`); const body = (await res.json()) as { data: Array<{ id: string }> }; expect((body.data ?? []).map((p) => p.id)).toContain(p1.id); }); });