import type { Page, Route } from "@playwright/test"; import { channelSubsManifest, channelTranscriptsManifest, chartTemplates, statsManifest, statsPage, subsManifest, subsPage, postsManifest, channelPostsManifest, postsPage, summaries, summariesManifest, tagsJson, transcriptPage, } from "./fixtures/data"; async function fulfillJson(route: Route, body: unknown) { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(body), }); } export async function installRoutes(page: Page) { // Neutralise the client-side AI rate limiter for the whole suite: a "*" RPM // override raises every model's ceiling so the per-request pacing gate never // adds real waits (which would slow every AI test). Throttle-specific tests opt // back in by driving 429s + Retry-After headers, which exercise the RETRY path // regardless of this ceiling. await page.addInitScript(() => { localStorage.setItem("ytdlp-tb:ai:rpm", JSON.stringify({ "*": 100000 })); }); await page.route("**/summaries/manifest.json", async (route) => { await fulfillJson(route, summariesManifest()); }); await page.route("**/summaries/page-*.json", async (route) => { await fulfillJson(route, summaries()); }); // Per-channel transcripts manifest — match `/transcripts//manifest.json`. await page.route( /\/transcripts\/[^/]+\/manifest\.json$/, async (route) => { await fulfillJson(route, channelTranscriptsManifest()); }, ); await page.route(/\/transcripts\/[^/]+\/page-\d+\.json$/, async (route) => { await fulfillJson(route, transcriptPage()); }); await page.route("**/subs/manifest.json", async (route) => { await fulfillJson(route, subsManifest()); }); await page.route(/\/subs\/[^/]+\/manifest\.json$/, async (route) => { await fulfillJson(route, channelSubsManifest()); }); await page.route(/\/subs\/[^/]+\/page-\d+\.json$/, async (route) => { await fulfillJson(route, subsPage()); }); await page.route("**/posts/manifest.json", async (route) => { await fulfillJson(route, postsManifest()); }); await page.route(/\/posts\/[^/]+\/manifest\.json$/, async (route) => { await fulfillJson(route, channelPostsManifest()); }); await page.route(/\/posts\/[^/]+\/page-\d+\.json$/, async (route) => { await fulfillJson(route, postsPage()); }); // AI digests — absent by default, which is also the real default: only ~0.1% // of the corpus is digested, so most channels ship no digests tree at all and // the manifest 404s. Tests that want the Digest control register their own // route afterwards, which takes precedence. await page.route(/\/digests\/[^/]+\/manifest\.json$/, async (route) => { await route.fulfill({ status: 404, contentType: "application/json", body: "{}", }); }); // Search-alias dictionary — empty by default; alias-suggestion.spec overrides // this with a populated list. Kept here so other search tests get a clean // intercept instead of a real 404. await page.route("**/search-aliases.json", async (route) => { await fulfillJson(route, { aliases: [] }); }); // Duplicate clusters — absent by default, which is also the real default: // compose-site writes /duplicates.json only when a site has at least one // shippable cluster. Tests that want the search-result dupe badge register // their own route afterwards, which takes precedence. await page.route("**/duplicates.json", async (route) => { await route.fulfill({ status: 404, contentType: "application/json", body: "{}", }); }); // Curated tags — absent by default, which is also the real default: // compose-site writes /tags.json only for a site with at least one visible, // non-zero-count tag, so a 404 is a legitimate empty state (exactly like // duplicates.json). A spec that wants the tag chips calls installTagRoutes // afterwards, which takes precedence. await page.route("**/tags.json", async (route) => { await route.fulfill({ status: 404, contentType: "application/json", body: "{}", }); }); } // Serve the populated /tags.json fixture — two curated tags in one group, with // counts that agree with the `curatedTags` on summaries(). Call AFTER // installRoutes (a later route wins) in any spec that asserts on tag chips or // tag filtering. export async function installTagRoutes(page: Page) { await page.route("**/tags.json", async (route) => { await fulfillJson(route, tagsJson()); }); } // Charts page fetches: stats dataset + baked templates. Also installs the // summaries/transcripts routes so search-derived charts can evaluate. export async function installChartRoutes(page: Page) { await installRoutes(page); await page.route("**/stats/manifest.json", async (route) => { await fulfillJson(route, statsManifest()); }); await page.route("**/stats/page-*.json", async (route) => { await fulfillJson(route, statsPage()); }); await page.route("**/chart-templates.json", async (route) => { await fulfillJson(route, chartTemplates()); }); } // Make the filters reachable. From xl they are an inline block under the search // bar and this is a no-op; below xl they live in a sheet behind the "Filters" // chip, so a spec that asserts on a filter control has to open it first. export async function openFilters(page: Page) { // The bar only decides where the panel goes once it has hydrated and read // the viewport, so wait for the builder before asking which shape it took. await page.getByTestId("query-builder").waitFor(); const panel = page.getByTestId("filters-panel"); const trigger = page.getByTestId("filters-trigger"); await panel.or(trigger).first().waitFor(); if ((await panel.count()) === 0) { await trigger.click(); await panel.waitFor(); } // Inline placement: the panel is a
and may be collapsed. const profiles = panel.getByTestId("profiles-row"); if (!(await profiles.isVisible())) { await panel.locator("summary").first().click(); } await profiles.waitFor(); } // The search page shows nothing under the bar until the first Search of the // page life (a link that carries a query or a filter shows its results on // load). A spec that wants the listing of every video presses Search, as a // visitor does. It waits for the query builder first, which renders only once // the bar has mounted, so the click is never lost to the pre-hydration window. export async function showAll(page: Page) { await page.getByTestId("query-builder").waitFor(); await page.getByTestId("search-submit").click(); await page.getByTestId("results-summary").waitFor(); } export async function urlParams(page: Page): Promise { const search = await page.evaluate(() => window.location.search); return new URLSearchParams(search); } export async function expectModalOpen(page: Page) { await page .getByRole("button", { name: "Mark clip start at current time" }) .waitFor(); } export async function closeModal(page: Page) { await page.getByRole("button", { name: "Close player" }).click(); await page .getByRole("button", { name: "Mark clip start at current time" }) .waitFor({ state: "detached" }); }