/** * T-141 — the /templates DIRECTORY UI E2E (search + category filter + * description), on top of the snapshot→list→apply loop covered by * confluence-templates.spec.ts. * * The filters are SERVER-side: `GET /workspaces/:id/templates?search=&category=` * (search = case-insensitive contains on name+description, category = exact). * This spec snapshots two templates into DIFFERENT categories (via the API, * which carries category/description) and verifies the web directory: * - the category dropdown lists the categories present, * - selecting a category narrows the list to that category's templates, * - search narrows by name (case-insensitive), * - each row shows its description, * - "Clear" restores the unfiltered union. * * Origin: `https://notion.filps.com` (Caddy single-origin — see the other * confluence-* specs). */ import { test, expect } from "../fixtures"; import { createPage } from "../utils/api"; import type { PageDto } from "../fixtures"; interface TemplateDto { id: string; name: string; category: string | null; description: string | null; } async function makeTemplate( api: import("@playwright/test").APIRequestContext, wsId: string, name: string, category: string, description: string ): Promise { const src = (await createPage(api, wsId, { title: name })) as PageDto; const res = await api.post(`/api/v1/pages/${src.id}/templates`, { data: { name, category, description } }); expect(res.status(), await res.text()).toBe(201); return (await res.json()) as TemplateDto; } test.describe("Templates directory — T-141 (search + category)", () => { test("category filter, name search, description, and Clear", async ({ authPage, api, freshWorkspace }) => { const ws = await freshWorkspace("Template Dir Space"); const meeting = await makeTemplate(api, ws.id, "Standup Notes", "meetings", "Daily team standup"); const incident = await makeTemplate(api, ws.id, "Incident Report", "engineering", "Post-incident writeup"); await authPage.goto(`/w/${ws.slug}/templates`); await expect(authPage.getByTestId(`template-item-${meeting.id}`)).toBeVisible({ timeout: 15_000 }); // Both rows show their description (the directory's human-readable line). await expect(authPage.getByTestId(`template-desc-${meeting.id}`)).toHaveText("Daily team standup"); await expect(authPage.getByTestId(`template-desc-${incident.id}`)).toHaveText("Post-incident writeup"); // The category dropdown offers the categories present in the directory // (the list is a cross-workspace union — built-in templates from other // workspaces the user belongs to contribute categories — so assert the // two we created are present, not an exact set). const select = authPage.getByTestId("template-category"); const options = await select.locator("option").allInnerTexts(); expect(options).toEqual(expect.arrayContaining(["All categories", "engineering", "meetings"])); // Category filter → only the matching template remains. await select.selectOption("meetings"); await expect(authPage.getByTestId(`template-item-${meeting.id}`)).toBeVisible(); await expect(authPage.getByTestId(`template-item-${incident.id}`)).toHaveCount(0); // The row also carries the category badge. await expect(authPage.getByTestId(`template-category-badge-${meeting.id}`)).toHaveText("meetings"); // Reset the category (search and category COMBINE server-side — an // "incident" search under category=meetings would be empty by design), // then name search (case-insensitive) → only the matching template. await select.selectOption(""); const search = authPage.getByTestId("template-search"); await search.fill("incident"); await expect(authPage.getByTestId(`template-item-${incident.id}`)).toBeVisible(); await expect(authPage.getByTestId(`template-item-${meeting.id}`)).toHaveCount(0); // Clear → the unfiltered union is back. await authPage.getByTestId("template-clear-filters").click(); await expect(search).toHaveValue(""); await expect(select).toHaveValue(""); await expect(authPage.getByTestId(`template-item-${meeting.id}`)).toBeVisible(); await expect(authPage.getByTestId(`template-item-${incident.id}`)).toBeVisible(); }); });