import { expect, test, type Page, type Route } from "@playwright/test"; // Release 8 slice E (review fix R1). A search restored from localStorage is // HELD on a page life's first load — but the hub mounts a separate search // session per route (HubHome on `/`, AskHub on `/ask`), so `/` -> `/ask` by the // per-card Ask re-hydrates from storage. The hold must not apply there: the // visitor has just run that search, and hub `/ask` has no search bar to run // it again. The chat must still ground in it. // // Origin B is route-mocked exactly as in federation.spec.ts. const ORIGIN_B = "http://localhost:4599"; const CHANNEL_B = "Channel B"; const CHANNEL_B_SLUG = "chan-b"; const CORS = { "access-control-allow-origin": "*" }; async function fulfillJson(route: Route, body: unknown, cors = true) { await route.fulfill({ status: 200, contentType: "application/json", headers: cors ? CORS : {}, body: JSON.stringify(body), }); } function descriptorB() { return { contract: 1, siteId: "originb", siteTitle: "Origin B", siteDescription: "A federated archive.", headerTitle: "Origin B", homeTagline: "", accent: "#c026d3", siteUrl: ORIGIN_B, pwa: false, socialLinks: [], groups: [{ id: "gb", name: "Origin B", selectedByDefault: true }], defaultGroupId: "gb", channels: [ { slug: CHANNEL_B_SLUG, name: CHANNEL_B, count: 1, groupId: "gb" }, ], generatedAt: "2026-01-01T00:00:00.000Z", summariesVersion: 3, }; } function manifestB() { return { version: 3, totalCount: 1, pageSize: 1000, pageCount: 1, generatedAt: "2026-01-01T00:00:00.000Z", channels: [ { name: CHANNEL_B, count: 1, slug: CHANNEL_B_SLUG, groupId: "gb" }, ], groups: [{ id: "gb", name: "Origin B", selectedByDefault: true }], defaultGroupId: "gb", siteId: "originb", }; } function summariesB() { return [ { slug: `${CHANNEL_B_SLUG}/vidb1`, id: "vidb1", channelSlug: CHANNEL_B_SLUG, title: "Bravo Video", uploadDate: "20260101", date: "2026-01-01", duration: "5:00", channel: CHANNEL_B, isLivestream: false, ageRestricted: false, isDeleted: false, isUnlisted: false, platform: "youtube" as const, webpageUrl: `${ORIGIN_B}/vidb1`, }, ]; } // Route-mock Origin B's federation contract. `cors` toggles the ACAO header on // the descriptor to simulate a site that isn't sharing its data. async function mockOriginB(page: Page, { cors = true }: { cors?: boolean } = {}) { await page.route(`${ORIGIN_B}/site.json`, (r) => fulfillJson(r, descriptorB(), cors), ); await page.route(`${ORIGIN_B}/summaries/manifest.json`, (r) => fulfillJson(r, manifestB()), ); await page.route(`${ORIGIN_B}/summaries/page-*.json`, (r) => fulfillJson(r, summariesB()), ); // No subs on Origin B. await page.route(`${ORIGIN_B}/subs/manifest.json`, (r) => fulfillJson(r, { version: 4, channels: [], totalCount: 0, liveChatTotalCount: 0, generatedAt: "2026-01-01T00:00:00.000Z", }), ); } // The hub has no built-in pool in these tests — keep it empty so only the // added origin is exercised (and avoid a 404 on the missing file). async function stubBuiltins(page: Page) { await page.route("**/hub-sites.json", (r) => fulfillJson(r, [])); } async function addArchive(page: Page, url: string) { await page.getByLabel("Archive URL").fill(url); await page.getByRole("button", { name: "Add", exact: true }).click(); } test("hub: a search run on / still grounds the chat after the per-card Ask", async ({ page, }) => { await stubBuiltins(page); await mockOriginB(page); await page.goto("/"); await expect(page.getByRole("heading", { level: 1, name: /searchable at once\./ })).toBeVisible(); await addArchive(page, ORIGIN_B); await expect( page.getByTestId("shelf-spine").filter({ hasText: "Origin B" }), ).toBeVisible(); // Run a metadata search for "bravo" (Origin B serves no transcript shards). await page.getByTestId("query-builder").waitFor(); await page .locator('[data-testid^="leaf-scope-"]') .first() .selectOption({ label: "Title / channel" }); const input = page.locator('[data-testid^="leaf-query-"]').first(); await input.click(); await input.fill("bravo"); await input.press("Enter"); await page.waitForURL(/[?&]qt=/); const card = page.locator("[data-result-slug]").first(); await expect(card).toBeVisible(); // The per-card Ask: selects the video and client-navigates to /ask/. await card.getByRole("button", { name: "Ask", exact: true }).click(); await page.waitForURL(/\/ask\/?$/); const summary = page.getByText("Grounding: 1 selected video"); await expect(summary).toBeVisible(); const whole = page.getByRole("tab", { name: "Whole search" }); if (!(await whole.isVisible())) await summary.click(); // Switch to the whole search. Held, the session would have no active query // here and no search grounding at all. await whole.click(); await expect( page.getByText("Grounded in 1 result from your current search"), ).toBeVisible(); });