import { expect, test, type Page } from "@playwright/test"; import { CHANNEL, CHANNEL_SLUG, TAG_TOPIC, VIDEO_CHAT_LARGE, VIDEO_CHAT_SMALL, VIDEO_TRANSCRIPT_ONLY, } from "./fixtures/data"; import { installRoutes, installTagRoutes, openFilters, showAll } from "./helpers"; // Release 14 (S1): a clear screen until the first Search. A plain visit shows // the search bar, the page's intro and the footer — no count, no controls, no // listing — and the bar's line says what to do. The first Search of the page // life (Enter, the button, a Filters Apply, a profile load) shows the results; // an empty one lists every video, exactly as before. A link that carries a // query (`qt=`) or a filter (`tg=`, `ch=`, a share link) is the visitor asking // and shows its results on load. The gate is once per page life: client-side // navigation keeps the listing, a reload clears it. const STORAGE_KEY = "ytdlp-tb:export-filters"; const HINT = "Press Enter or click Search to apply"; const ALPHA_TREE = { k: "g", o: "AND", c: [{ k: "l", q: "alpha", s: "transcripts" }] }; const qt = (tree: unknown) => encodeURIComponent(JSON.stringify(tree)); const leafInput = (page: Page) => page.locator('input[data-testid^="leaf-query-"]').first(); const card = (page: Page, id: string) => page.locator(`[data-result-slug="${CHANNEL_SLUG}/${id}"]`); // The bar has mounted; then give the session time to hydrate (it waits for the // manifest) and to show a listing, were it going to. async function settle(page: Page) { await page.getByTestId("query-builder").waitFor(); await page.waitForTimeout(1_000); } async function expectClearScreen(page: Page) { await expect(page.getByTestId("results-section")).toHaveCount(0); await expect(page.getByTestId("results-summary")).toHaveCount(0); await expect(page.getByTestId("view-toggle")).toHaveCount(0); await expect(page.getByTestId("selection-toolbar")).toHaveCount(0); await expect(page.getByTestId("browse-hint")).toHaveCount(0); await expect(page.locator("[data-card-header]")).toHaveCount(0); await expect(page.getByText(HINT)).toBeVisible(); } // A route outside the workspace, reached as a client navigation in the same // page life: the search session unmounts with the workspace and mounts again on // Back. Until release 16 the header's Use with AI link made that navigation; // it now leaves the site, and the footer's Changelog is a plain (a new page // life), so the spec asks the app router itself — `window.next.router`, which // Next sets for debugging in development and production alike. async function leaveForChangelog(page: Page) { await page.evaluate(() => { ( window as unknown as { next: { router: { push(href: string): void } } } ).next.router.push("/changelog/"); }); // A dev server compiles /changelog on its first visit. await expect(page).toHaveURL(/\/changelog\/?$/, { timeout: 20_000 }); await expect(page.getByRole("heading", { level: 1, name: "Changelog" })).toBeVisible(); await expect(page.getByTestId("query-builder")).toHaveCount(0); } async function expectAllVideos(page: Page) { await expect(page.getByTestId("results-summary")).toHaveText("All videos (3)"); await expect(page.getByTestId("browse-hint")).toBeVisible(); await expect(page.locator("[data-card-header]")).toHaveCount(3); } test.describe("a clear screen until the first Search", () => { test.beforeEach(async ({ page }) => { await installRoutes(page); }); for (const { width, height } of [ { width: 1280, height: 800 }, { width: 390, height: 844 }, ]) { test(`${width}×${height}: on load, the bar, the intro and the footer — no results`, async ({ page, }) => { await page.setViewportSize({ width, height }); await page.goto("/"); await settle(page); await expectClearScreen(page); // The page's intro stays: the transcript count. await expect(page.getByRole("heading", { level: 1 })).toContainText("transcripts"); // The footer is on the first screen, whole. await expect(page.getByRole("contentinfo")).toBeInViewport({ ratio: 1 }); }); } test("Enter on an empty box shows every video", async ({ page }) => { await page.goto("/"); await settle(page); await expectClearScreen(page); await leafInput(page).press("Enter"); await expectAllVideos(page); // The line has done its job. await expect(page.getByText(HINT)).toHaveCount(0); }); test("the Search button shows every video", async ({ page }) => { await page.goto("/"); await settle(page); await page.getByTestId("search-submit").click(); await expectAllVideos(page); }); test("changing a filter before the first Search does not show the listing", async ({ page, }) => { await page.goto("/"); await settle(page); await openFilters(page); await page.getByRole("checkbox", { name: "Videos" }).uncheck(); await expectClearScreen(page); await page.getByTestId("search-submit").click(); await expect(page.getByTestId("results-summary")).toHaveText("All videos (0)"); }); test("/ → /ask → / keeps the listing", async ({ page }) => { await page.goto("/"); await showAll(page); await expectAllVideos(page); const nav = page.getByTestId("workspace-nav"); await nav.getByRole("link", { name: "Chat" }).click(); // A dev server compiles /ask on its first visit. await expect(page).toHaveURL(/\/ask\/?$/, { timeout: 20_000 }); await expect(page.getByPlaceholder(/Ask about the transcripts/)).toBeVisible(); await nav.getByRole("link", { name: "Search" }).click(); await expect(page).not.toHaveURL(/\/ask/); await expectAllVideos(page); }); test("leaving the search page and coming Back keeps the listing", async ({ page }) => { await page.goto("/"); await showAll(page); await expectAllVideos(page); // A route outside the workspace: the search session unmounts with it. await leaveForChangelog(page); await page.goBack(); await expect(page).not.toHaveURL(/changelog/); await expectAllVideos(page); }); test("a reload clears it", async ({ page }) => { await page.goto("/"); await showAll(page); await expectAllVideos(page); await page.reload(); await settle(page); await expectClearScreen(page); }); test("a qt= link shows its results on load", async ({ page }) => { await page.goto(`/?qt=${qt(ALPHA_TREE)}`); await expect(page.getByTestId("results-summary")).toContainText("Matching videos"); await expect(page.locator("[data-card-header]")).toHaveCount(3); await expect(page.getByText(HINT)).toHaveCount(0); }); test("a filter-only link shows its results on load", async ({ page }) => { await installTagRoutes(page); await page.goto(`/?tg=${TAG_TOPIC}`); await expect(page.getByTestId("results-summary")).toHaveText("All videos (1)"); await expect(card(page, VIDEO_CHAT_LARGE)).toBeVisible(); await expect(card(page, VIDEO_CHAT_SMALL)).toHaveCount(0); // A legacy channel link: this one leaves the only channel out. await page.goto(`/?ch=${encodeURIComponent(CHANNEL)}`); await expect(page.getByTestId("results-summary")).toHaveText("All videos (0)"); await expect(page.getByText("No videos match the current filters.")).toBeVisible(); }); test("a restored query shows the clear screen and the filled form", async ({ page }) => { await page.goto("/"); await page.evaluate( ({ key, value }) => window.localStorage.setItem(key, value), { key: STORAGE_KEY, value: JSON.stringify({ v: 1, working: { channels: { included: [], excluded: [] }, nol: true, query: JSON.stringify(ALPHA_TREE), }, profiles: {}, activeProfileName: null, }), }, ); await page.goto("/"); await expect(leafInput(page)).toHaveValue("alpha"); await expect(page.getByTestId("search-submit")).toHaveAttribute("data-dirty", "true"); await settle(page); await expectClearScreen(page); // The visitor runs it. await leafInput(page).press("Enter"); await expect(page.getByTestId("results-summary")).toContainText("Matching videos"); await expect(card(page, VIDEO_TRANSCRIPT_ONLY)).toBeVisible(); }); });