import { expect, test, type Page, type Route } from "@playwright/test"; // Self-contained fixtures exercising the channel-group filter chips // (common/components/WorkspaceSearchBar.tsx): several small groups collapse to // wrapping chips with a tri-state group checkbox; expanding breaks a group out // to a full-width card with per-channel checkboxes and All/None. type Chan = { name: string; slug: string; groupId: string }; const CHANNELS: Chan[] = [ { name: "Alpha", slug: "alpha", groupId: "g1" }, { name: "Beta", slug: "beta", groupId: "g1" }, { name: "Gamma", slug: "gamma", groupId: "g2" }, { name: "Delta", slug: "delta", groupId: "g3" }, ]; const GROUPS = [ { id: "g1", name: "Group One", description: "First fixture group", selectedByDefault: true, order: 1, }, { id: "g2", name: "Group Two", description: "Second fixture group", selectedByDefault: true, order: 2, }, { id: "g3", name: "Group Three", description: "Third fixture group", selectedByDefault: true, order: 3, }, ]; function summary(c: Chan) { return { slug: `${c.slug}/vid-${c.slug}`, id: `vid-${c.slug}`, channelSlug: c.slug, title: `Video from ${c.name}`, uploadDate: "20250101", date: "2025-01-01", duration: "5:00", channel: c.name, isLivestream: false, ageRestricted: false, isDeleted: false, isUnlisted: false, platform: "youtube" as const, webpageUrl: `https://example.com/vid-${c.slug}`, }; } async function fulfillJson(route: Route, body: unknown) { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(body), }); } async function installRoutes(page: Page) { const list = CHANNELS.map(summary); await page.route("**/summaries/manifest.json", async (route) => { await fulfillJson(route, { version: 3, totalCount: list.length, pageSize: 1000, pageCount: 1, generatedAt: new Date().toISOString(), channels: CHANNELS.map((c) => ({ name: c.name, count: 1, groupId: c.groupId, })), groups: GROUPS, defaultGroupId: "g1", }); }); await page.route("**/summaries/page-*.json", async (route) => { await fulfillJson(route, list); }); // Transcripts are never fetched by these tests, but stub the routes so // nothing falls through to the dev server. await page.route(/\/transcripts\/[^/]+\/manifest\.json$/, async (route) => { await fulfillJson(route, { version: 1, channelSlug: "unused", pageCount: 0, maxPageBytes: 8388608, generatedAt: new Date().toISOString(), slugToPage: {}, }); }); await page.route(/\/transcripts\/[^/]+\/page-\d+\.json$/, async (route) => { await fulfillJson(route, []); }); // No live chat. await page.route("**/subs/manifest.json", async (route) => { await fulfillJson(route, { version: 4, channels: [], totalCount: 0, liveChatTotalCount: 0, generatedAt: new Date().toISOString(), }); }); } async function waitForHydration(page: Page) { await page.getByTestId("query-builder").waitFor(); } // Group cards are
nested inside the outer Filters
. function groupCard(page: Page, name: string) { return page.locator("details details").filter({ hasText: name }); } function groupCheckbox(page: Page, name: string) { return page.getByRole("checkbox", { name: `Select all in ${name}` }); } // A member-channel checkbox inside its group card. Scoped + exact so the // per-result "Select "Video from Alpha" for AI" checkboxes never collide. function memberCheckbox(page: Page, group: string, channel: string) { return groupCard(page, group).getByRole("checkbox", { name: channel, exact: true, }); } // Expand/collapse by clicking the group's name inside the chip summary — // the tri-state checkbox and All/None isolate their own clicks. async function clickGroupName(page: Page, name: string) { await groupCard(page, name) .locator("summary") .getByText(name, { exact: true }) .click(); } test.describe("channel-group filter chips", () => { test.beforeEach(async ({ page }) => { await installRoutes(page); await page.goto("/"); await waitForHydration(page); }); test("groups start collapsed as chips that wrap onto one row", async ({ page, }) => { // All three chips render with their selected counts. await expect(groupCard(page, "Group One")).toContainText("2/2"); await expect(groupCard(page, "Group Two")).toContainText("1/1"); await expect(groupCard(page, "Group Three")).toContainText("1/1"); // Collapsed by default on a multi-group site: member checkboxes hidden. await expect(memberCheckbox(page, "Group One", "Alpha")).toBeHidden(); await expect(memberCheckbox(page, "Group Two", "Gamma")).toBeHidden(); // Uniform min-width chips pack onto a shared row. const one = await groupCard(page, "Group One") .locator("summary") .boundingBox(); const two = await groupCard(page, "Group Two") .locator("summary") .boundingBox(); expect(one).not.toBeNull(); expect(two).not.toBeNull(); expect(one!.y).toBe(two!.y); }); test("chip checkbox toggles the whole group without expanding", async ({ page, }) => { const box = groupCheckbox(page, "Group One"); await expect(box).toHaveAttribute("aria-checked", "true"); // Fully selected → click deselects the whole group. await box.click(); await expect(groupCard(page, "Group One")).toContainText("0/2"); await expect(box).toHaveAttribute("aria-checked", "false"); // ...and the chip did not expand. await expect(groupCard(page, "Group One")).not.toHaveAttribute("open"); await expect(memberCheckbox(page, "Group One", "Alpha")).toBeHidden(); // Unselected → click selects the whole group. await box.click(); await expect(groupCard(page, "Group One")).toContainText("2/2"); await expect(box).toHaveAttribute("aria-checked", "true"); await expect(groupCard(page, "Group One")).not.toHaveAttribute("open"); }); test("partial selection shows indeterminate; clicking selects all", async ({ page, }) => { // Expand Group One and deselect one member. await clickGroupName(page, "Group One"); await memberCheckbox(page, "Group One", "Alpha").click(); await expect(groupCard(page, "Group One")).toContainText("1/2"); // Collapse — the chip checkbox reads mixed. await clickGroupName(page, "Group One"); await expect(memberCheckbox(page, "Group One", "Alpha")).toBeHidden(); const box = groupCheckbox(page, "Group One"); await expect(box).toHaveAttribute("aria-checked", "mixed"); // Radix cycles indeterminate → checked, i.e. "select all". await box.click(); await expect(groupCard(page, "Group One")).toContainText("2/2"); await expect(box).toHaveAttribute("aria-checked", "true"); }); test("expanding shows member checkboxes and All/None at full width", async ({ page, }) => { await clickGroupName(page, "Group One"); const card = groupCard(page, "Group One"); await expect(card).toHaveAttribute("open", ""); await expect(memberCheckbox(page, "Group One", "Alpha")).toBeVisible(); await expect(memberCheckbox(page, "Group One", "Beta")).toBeVisible(); await expect(card.getByRole("button", { name: "All" })).toBeVisible(); await expect(card.getByRole("button", { name: "None" })).toBeVisible(); // The expanded card breaks out to (roughly) the container's full width. const containerBox = await card.locator("xpath=..").boundingBox(); const cardBox = await card.boundingBox(); expect(containerBox).not.toBeNull(); expect(cardBox).not.toBeNull(); expect(cardBox!.width).toBeGreaterThanOrEqual(containerBox!.width * 0.9); // Other groups stay collapsed chips. await expect(memberCheckbox(page, "Group Two", "Gamma")).toBeHidden(); }); test("expanded/collapsed state persists across a reload", async ({ page, }) => { await clickGroupName(page, "Group Two"); await expect(groupCard(page, "Group Two")).toHaveAttribute("open", ""); await page.reload(); await waitForHydration(page); await expect(groupCard(page, "Group Two")).toHaveAttribute("open", ""); await expect(memberCheckbox(page, "Group Two", "Gamma")).toBeVisible(); await expect(groupCard(page, "Group One")).not.toHaveAttribute("open"); await expect(groupCard(page, "Group Three")).not.toHaveAttribute("open"); }); });