import { expect, test, type Page } from "@playwright/test"; import { TAG_TOPIC } from "./fixtures/data"; import { installRoutes, installTagRoutes } from "./helpers"; // Release 8 slice E. The search session restored from localStorage loads the // query and the filters but does NOT run the search on first load: a restored // query used to re-run the pipeline on every visit, refetching transcript // shards (up to 8 MB each on a cold device) for a search the visitor had not // asked for this time. The visitor runs it — Search / Enter. A query on the // URL (`qt=`) is a shared link, i.e. the visitor asking, and still runs. // Release 14: until then the page shows no results area at all — the held // query no longer sits over a browse listing. A link that carries only a // filter (`tg=`, `ch=`) shows its results but runs no query, so a stored one // stays held on that load AND on every later mount in the same visit. const STORAGE_KEY = "ytdlp-tb:export-filters"; const SHARD = /\/transcripts\/[^/]+\/page-\d+\.json$/; function countShards(page: Page): { n: number } { const seen = { n: 0 }; page.on("request", (req) => { if (SHARD.test(new URL(req.url()).pathname)) seen.n += 1; }); return seen; } const leafInput = (page: Page) => page.locator('input[data-testid^="leaf-query-"]').first(); // A query that hits every fixture transcript (each has an "alpha line" cue), // in the app's own serialization (`stringifyRoot`: the qt= JSON). const ALPHA_TREE = { k: "g", o: "AND", c: [{ k: "l", q: "alpha", s: "transcripts" }] }; // Pre-seed the session a previous visit would have left: the query plus a // filter (Livestreams off). Written via page.evaluate on a throwaway first load // — not addInitScript, which would re-fire on every navigation. Nothing has // searched in this browser context, so the IndexedDB layer cache is empty and a // search that runs MUST fetch shards. async function seedStoredSearch(page: 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, }), }, ); } // A route outside the workspace unmounts the search session; coming back // mounts it again in the same page life. Until release 16 the header's Use // with AI link made that client 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/"); }); 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 expectHeld(page: Page) { await expect(leafInput(page)).toHaveValue("alpha"); await expect(page.getByTestId("search-submit")).toHaveAttribute( "data-dirty", "true", ); } test.describe("restored search waits for the visitor", () => { test.beforeEach(async ({ page }) => { await installRoutes(page); }); test("a restored query loads into the form, fetches no shard, and runs on submit", async ({ page, }) => { const shards = countShards(page); await seedStoredSearch(page); // A fresh visit with no query on the URL: the session comes from storage. shards.n = 0; await page.goto("/"); // The settings load: the query is in the input, the filter is restored… await expect(leafInput(page)).toHaveValue("alpha"); await expect( page.getByRole("checkbox", { name: "Livestreams" }), ).not.toBeChecked(); // …and the bar says what the visitor has to do, as for any unapplied edit. await expect(page.getByTestId("search-submit")).toHaveAttribute( "data-dirty", "true", ); await expect( page.getByText("Press Enter or click Search to apply"), ).toBeVisible(); // Give a would-be pipeline every chance to start. await page.waitForTimeout(1_500); expect(shards.n).toBe(0); // Nothing has been asked in this page life, so there is no results area // at all: no count, no listing under the restored filters. await expect(page.getByTestId("results-summary")).toHaveCount(0); await expect(page.getByTestId("browse-hint")).toHaveCount(0); await expect(page.locator("[data-card-header]")).toHaveCount(0); // The visitor runs it. const fetched = page.waitForRequest( (req) => SHARD.test(new URL(req.url()).pathname), { timeout: 10_000 }, ); await leafInput(page).press("Enter"); await fetched; await expect(page.getByTestId("results-summary")).toContainText( "Matching videos", ); await expect(page.getByTestId("search-submit")).toHaveAttribute( "data-dirty", "false", ); }); test("a query on the URL (a shared link) still runs on load", async ({ page, }) => { const shards = countShards(page); // A stored session is present too: the URL query is not held, it runs. await seedStoredSearch(page); const link = `/?qt=${encodeURIComponent(JSON.stringify(ALPHA_TREE))}`; shards.n = 0; await page.goto(link); await expect(page.getByTestId("results-summary")).toContainText( "Matching videos", ); await expect.poll(() => shards.n).toBeGreaterThan(0); await expect(page.getByTestId("search-submit")).toHaveAttribute( "data-dirty", "false", ); }); test("a filter-only link shows its results and leaves a stored query held, then and after Back", async ({ page, }) => { await installTagRoutes(page); const shards = countShards(page); await seedStoredSearch(page); shards.n = 0; await page.goto(`/?tg=${TAG_TOPIC}`); // The link asked: its results show, under its filter… await expect(page.getByTestId("results-summary")).toHaveText("All videos (1)"); // …and the stored query is in the box, held. await expectHeld(page); await leaveForChangelog(page); await page.goBack(); await expect(page).not.toHaveURL(/changelog/); // A second mount in the same visit: still held, still the listing. await expectHeld(page); await expect(page.getByTestId("results-summary")).toHaveText("All videos (1)"); await page.waitForTimeout(1_500); expect(shards.n).toBe(0); }); test("a filter-only link, then the header's Search link: the stored query stays held", async ({ page, }) => { const shards = countShards(page); await seedStoredSearch(page); shards.n = 0; // Under a filter link the stored session is not read at all. await page.goto(`/?ch=${encodeURIComponent("Nobody")}`); await expect(page.getByTestId("results-summary")).toContainText("All videos"); await expect(leafInput(page)).toHaveValue(""); await leaveForChangelog(page); await page .getByRole("banner") .getByRole("link", { name: "Search", exact: true }) .click(); await expect(page).not.toHaveURL(/changelog/, { timeout: 20_000 }); // The plain search page reads the stored session: the query is held, and // the listing shows because the visitor asked earlier in this visit. await expectHeld(page); await expect(page.getByTestId("results-summary")).toHaveText("All videos (3)"); await page.waitForTimeout(1_500); expect(shards.n).toBe(0); }); });