/** * Confluence P1 — T-138 (page duplicate) + T-140 (backlinks/Related panel) E2E * * Both API surfaces existed since 2026-10-11 (`POST /pages/:id/duplicate`, * `GET /pages/:id/backlinks`); this spec verifies the WEB surfaces that * landed in this round: * * - T-138: the topbar "More" → Duplicate menu item deep-copies the page * (server title ` (copy)`), lands on the NEW page, and the new page * carries a recursive copy of the source's blocks. * - T-140: the page-view "Related" button opens the backlinks slide-over — * a page referencing this one (via a CHILD_PAGE block) is listed, and the * empty state shows when nothing references it. * * Origin: `https://notion.filps.com` (Caddy single-origin; localhost does NOT * work — see confluence-templates.spec.ts). URL navigation uses the workspace * SLUG (sidebar/topbar links); API calls use the workspace ID. */ import { test, expect } from "../fixtures"; import { createPage, createBlock, getBlocks } from "../utils/api"; import type { PageDto, BlockDto } from "../fixtures"; test.describe("Confluence P1 — page duplicate + related (T-138/T-140)", () => { test("topbar More → Duplicate deep-copies the page and its blocks", async ({ authPage, api, freshWorkspace }) => { const ws = await freshWorkspace("Duplicate Space"); const src = (await createPage(api, ws.id, { title: "Dup Source" })) as PageDto; const blocks = [ await createBlock(api, src.id, { type: "PARAGRAPH", content: { text: "dup-block-A" } }), await createBlock(api, src.id, { type: "PARAGRAPH", content: { text: "dup-block-B" } }) ]; const srcPath = `/w/${ws.slug}/page/${src.id}`; await authPage.goto(srcPath); await authPage.getByRole("button", { name: "More actions" }).click(); await authPage.getByRole("menuitem", { name: "Duplicate" }).click(); // Land on the NEW page — wait until the URL is NOT the source page's. await authPage.waitForURL( (u) => u.pathname !== srcPath, { timeout: 15_000 }, ); const copyId = new URL(authPage.url()).pathname.split("/").pop() ?? ""; expect(copyId, "the copy has its own page id").not.toBe(src.id); // GET /pages/:id returns the { page, workspace, … } ENVELOPE (tolerate a // bare page too, in case the endpoint changes shape). const copyRes = await (await api.get(`/api/v1/pages/${copyId}`)).json() as { page?: PageDto; } & PageDto; const copyPage = copyRes.page ?? copyRes; expect(copyPage.title).toBe("Dup Source (copy)"); // The recursive copy carries the source blocks' text. const copyBlocks = await getBlocks(api, copyId); const texts = copyBlocks .filter((b) => b.type === "PARAGRAPH") .map((b) => (b.content as { text?: string })?.text ?? ""); expect(texts).toEqual( expect.arrayContaining(["dup-block-A", "dup-block-B"]), ); }); test("Related panel lists a referencing page (CHILD_PAGE) and shows an empty state otherwise", async ({ authPage, api, freshWorkspace }) => { const ws = await freshWorkspace("Related Space"); const referenced = (await createPage(api, ws.id, { title: "Related Target" })) as PageDto; const referrer = (await createPage(api, ws.id, { title: "Related Source" })) as PageDto; // The referrer embeds the referenced page via a CHILD_PAGE block — the // exact block type `backlinks` scans (content.pageId). await createBlock(api, referrer.id, { type: "CHILD_PAGE", content: { pageId: referenced.id } }); await authPage.goto(`/w/${ws.slug}/page/${referenced.id}`); await authPage.getByTestId("related-button").click(); const panel = authPage.getByTestId("related-panel"); await expect(panel).toBeVisible(); // The referrer is listed by title, deep-linking to it. const row = panel.getByTestId(`related-item-${referrer.id}`); await expect(row).toHaveText("Related Source"); await expect(row).toHaveAttribute( "href", new RegExp(`/w/${ws.slug}/page/${referrer.id}$`) ); // Close (the row click navigates + closes; use the close button first). await panel.getByRole("button", { name: "Close related pages" }).click(); await expect(panel).toBeHidden(); // Empty state: a page nothing references. await authPage.goto(`/w/${ws.slug}/page/${referrer.id}`); await authPage.getByTestId("related-button").click(); const emptyPanel = authPage.getByTestId("related-panel"); await expect(emptyPanel).toBeVisible(); await expect(emptyPanel.getByText("No related pages yet")).toBeVisible(); }); });