import { expect, test, type Page, type Route } from "@playwright/test"; import { openFilters } from "./helpers"; // Self-contained fixtures exercising inline channel groups // (common/components/FiltersPanel.tsx): a group flagged `inline: true` // renders its member channels as loose individual chips — checkbox + name, // no collapsible box — flowing among the normal group chips. type Chan = { name: string; slug: string; groupId: string }; const LONG_NAME = "Extremely Long Channel Name That Certainly Overflows A Uniform Chip"; const CHANNELS: Chan[] = [ { name: "Epsilon", slug: "epsilon", groupId: "solo" }, { name: LONG_NAME, slug: "longname", groupId: "solo" }, { name: "Alpha", slug: "alpha", groupId: "g1" }, { name: "Beta", slug: "beta", groupId: "g1" }, { name: "Gamma", slug: "gamma", groupId: "g2" }, ]; const GROUPS = [ { id: "solo", name: "Solo", selectedByDefault: true, order: 0, inline: true, }, { 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, }, ]; 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: "solo", }); }); 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(), }); }); // No curated tags: this fixture is about channel chips, and the tag groups // are
too — a real /tags.json from the dev server's public/ would // put open tag disclosures inside the same "details details" the // expand-nothing assertions below count. await page.route("**/tags.json", async (route) => { await route.fulfill({ status: 404, contentType: "application/json", body: "{}", }); }); } 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 }); } // A member-channel checkbox inside its group card. function memberCheckbox(page: Page, group: string, channel: string) { return groupCard(page, group).getByRole("checkbox", { name: channel, exact: true, }); } // A loose per-channel chip from an inline group. function inlineChip(page: Page, channel: string) { return page .getByTestId("inline-channel-chip") .filter({ hasText: channel }) .first(); } test.describe("inline channel chips", () => { test.beforeEach(async ({ page }) => { await installRoutes(page); await page.goto("/"); await waitForHydration(page); }); test("inline group renders loose chips instead of a group chip", async ({ page, }) => { // Two loose chips, one per Solo member — no
card for "Solo" // and no group bulk-toggle checkbox. await expect(page.getByTestId("inline-channel-chip")).toHaveCount(2); await expect(groupCard(page, "Solo")).toHaveCount(0); await expect( page.getByRole("checkbox", { name: "Select all in Solo" }), ).toHaveCount(0); // Inline members are visible on first load: the default-collapse // heuristic only counts collapsible groups (still >1 here, so g1/g2 // members stay hidden behind collapsed chips). await expect( inlineChip(page, "Epsilon").getByRole("checkbox"), ).toBeVisible(); await expect(memberCheckbox(page, "Group One", "Alpha")).toBeHidden(); await expect(memberCheckbox(page, "Group Two", "Gamma")).toBeHidden(); }); test("inline chip toggles its channel without expanding anything", async ({ page, }) => { await expect(page.getByText("5 of 5 channels")).toBeVisible(); const box = inlineChip(page, "Epsilon").getByRole("checkbox"); await expect(box).toHaveAttribute("aria-checked", "true"); await box.click(); await expect(page.getByText("4 of 5 channels")).toBeVisible(); await expect(box).toHaveAttribute("aria-checked", "false"); // No chip expanded as a side effect of the toggle. await expect(page.locator("details details[open]")).toHaveCount(0); await box.click(); await expect(page.getByText("5 of 5 channels")).toBeVisible(); await expect(box).toHaveAttribute("aria-checked", "true"); await expect(page.locator("details details[open]")).toHaveCount(0); }); test("uniform chip widths with long names ellipsized", async ({ page }) => { // Short inline chip and short collapsed group chip share the uniform // min width. const epsilon = await inlineChip(page, "Epsilon").boundingBox(); const g2 = await groupCard(page, "Group Two").boundingBox(); expect(epsilon).not.toBeNull(); expect(g2).not.toBeNull(); expect(epsilon!.width).toBe(g2!.width); // Long-named chip carries the full name as a tooltip, never wraps the // text (same height as a short chip), and stays inside the container. const long = inlineChip(page, LONG_NAME); await expect(long).toHaveAttribute("title", LONG_NAME); const longBox = await long.boundingBox(); const containerBox = await long.locator("xpath=..").boundingBox(); expect(longBox).not.toBeNull(); expect(containerBox).not.toBeNull(); expect(longBox!.height).toBe(epsilon!.height); expect(longBox!.width).toBeLessThanOrEqual(containerBox!.width); }); }); test.describe("inline channel chips on mobile", () => { test.use({ viewport: { width: 390, height: 844 } }); test.beforeEach(async ({ page }) => { await installRoutes(page); await page.goto("/"); await waitForHydration(page); // Below xl the filters live in a sheet behind the "Filters" chip, so the // chips have to be opened before they can be measured. await openFilters(page); }); test("chips stack one per line at phone widths", async ({ page }) => { const epsilon = await inlineChip(page, "Epsilon").boundingBox(); const long = await inlineChip(page, LONG_NAME).boundingBox(); const containerBox = await inlineChip(page, "Epsilon") .locator("xpath=..") .boundingBox(); expect(epsilon).not.toBeNull(); expect(long).not.toBeNull(); expect(containerBox).not.toBeNull(); // Stacked: same left edge, different rows, each near full width. expect(epsilon!.x).toBe(long!.x); expect(epsilon!.y).not.toBe(long!.y); expect(epsilon!.width).toBeGreaterThanOrEqual(containerBox!.width * 0.9); expect(long!.width).toBeGreaterThanOrEqual(containerBox!.width * 0.9); }); });