/** * T-142 — template VARIABLES web E2E (the RHF apply-with-values dialog). * * The server-side substitution (create-with-variables + apply `values` + * plain-text token replacement) is integration-tested in * `templates.integration.test.ts`. This spec verifies the WEB surface: * * - "Use" on a template WITH variables opens the variable FORM (not the * one-click apply), * - an empty required field blocks submit client-side ("Required" error, * no apply fired, no navigation), * - a valid submit calls `POST /templates/:id/apply` with `values` and the * new page's blocks show the tokens SUBSTITUTED. * * Origin: `https://notion.filps.com` (Caddy single-origin — see the other * confluence-* specs). */ import { test, expect } from "../fixtures"; import { createPage, createBlock, getBlocks } from "../utils/api"; import type { PageDto, BlockDto } from "../fixtures"; test.describe("Templates variables — T-142 (RHF apply-with-values)", () => { test("Use → variable form → required blocks submit; valid values substitute the tokens", async ({ authPage, api, freshWorkspace }) => { const ws = await freshWorkspace("Var Template Space"); const src = (await createPage(api, ws.id, { title: "Plan Draft" })) as PageDto; await createBlock(api, src.id, { type: "PARAGRAPH", content: { text: "Project {{project}} is owned by {{owner}}." } }); // Snapshot into a template WITH typed variables (the server stores them). const tRes = await api.post(`/api/v1/pages/${src.id}/templates`, { data: { name: "Plan Template", variables: [ { name: "project", label: "Project name", type: "text", required: true }, { name: "owner", label: "Owner", type: "text" } ] } }); expect(tRes.status(), await tRes.text()).toBe(201); const tpl = (await tRes.json()) as { id: string; variables: unknown }; expect(Array.isArray(tpl.variables)).toBe(true); await authPage.goto(`/w/${ws.slug}/templates`); const item = authPage.getByTestId(`template-item-${tpl.id}`); await expect(item).toBeVisible({ timeout: 15_000 }); // "Use" on a variable template opens the FORM dialog (not one-click apply). await item.getByTestId(`template-apply-${tpl.id}`).click(); const projectInput = authPage.getByLabel("Project name"); await expect(projectInput).toBeVisible(); await expect(authPage.getByLabel("Owner")).toBeVisible(); await expect(authPage.getByRole("button", { name: "Create page" })).toBeVisible(); // 1) Empty required field → client-side "Required", no navigation. await authPage.getByRole("button", { name: "Create page" }).click(); await expect(authPage.getByTestId("tpl-var-error-project")).toHaveText("Required"); // Still on the templates page (no apply fired for an invalid form). await expect(authPage).toHaveURL(/\/templates$/); // 2) Valid values → apply(values) → navigate to the new page. await projectInput.fill("Apollo"); await authPage.getByLabel("Owner").fill("Ada"); await authPage.getByRole("button", { name: "Create page" }).click(); await authPage.waitForURL(/\/w\/[^/]+\/page\/[^/]+$/, { timeout: 20_000 }); const newId = new URL(authPage.url()).pathname.split("/").pop() ?? ""; expect(newId).toBeTruthy(); expect(newId).not.toBe(src.id); // The applied page's block has the tokens SUBSTITUTED (plain text). The // deep-copy may also carry a title block ("Plan Draft") — assert the // substituted content is present and NO {{tokens}} remain. const blocks = await getBlocks(api, newId); const texts = blocks .filter((b) => b.type === "PARAGRAPH") .map((b) => (b.content as { text?: string })?.text ?? ""); expect(texts).toContain("Project Apollo is owned by Ada."); expect(texts.join(" ")).not.toContain("{{"); }); });