commit fbfd4009c1e1cce259a401456fb472348f516777
parent 695a44ef43a277814b263206f3495d520eefd251
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Tue, 29 Sep 2026 20:54:11 -0400
export: e2e for the clear screen — first-search.spec (no results on load, the footer whole on the first screen at 1280×800 and 390×844, Enter and the button show every video, a filter change alone shows nothing, / → /ask → / and Back keep the listing, a reload clears it, qt= and filter-only links show results on load, a restored query shows the clear screen and the filled form); one showAll helper, pressed where specs read the listing at load (export and hub)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat:
9 files changed, 271 insertions(+), 16 deletions(-)
diff --git a/export/e2e-hub/federated-search.spec.ts b/export/e2e-hub/federated-search.spec.ts
@@ -4,6 +4,7 @@ import {
LIVE_CHAT_MISSING,
NO_ARCHIVES_IN_SCOPE,
} from "../app/ask/hubScopeCopy";
+import { showAll } from "../e2e/helpers";
// The hub's federated search, per archive: two official members (hub-sites.json)
// served by route mocks WITH CORS, each with one video. Proves the scope chips
@@ -26,6 +27,10 @@ import {
// later; a member whose live chat cannot be read says so on its chip
// (LIVE_CHAT_MISSING), with a Retry for the live chat alone that keeps its
// videos in the search while it runs.
+// Release 14 (S1): the results — the listing and the "N of M archives
+// answered" line with it — show only after the first Search of the page life,
+// so a spec that reads them presses Search first (`showAll`); the chips carry
+// each archive's state before that.
const ORIGIN_A = "http://localhost:4598";
const ORIGIN_B = "http://localhost:4599";
@@ -199,6 +204,14 @@ test.describe("hub federated search — scope, per-archive state, attribution",
"aria-pressed",
"true",
);
+ // Both archives are in, and nothing has been asked: no results area yet,
+ // and the bar says what to do.
+ await expect(page.getByTestId("results-summary")).toHaveCount(0);
+ await expect(resultFrom(page, A)).toHaveCount(0);
+ await expect(resultFrom(page, B)).toHaveCount(0);
+ await expect(page.getByText("Press Enter or click Search to apply")).toBeVisible();
+ await showAll(page);
+ await expect(page.getByTestId("results-summary")).toHaveText("All videos (2)");
// Browse listing: one card per archive, each naming its source in text.
const b = resultFrom(page, B);
await expect(b).toHaveCount(1);
@@ -216,6 +229,7 @@ test.describe("hub federated search — scope, per-archive state, attribution",
}) => {
const mocks = await setup(page);
await page.goto("/");
+ await showAll(page);
await expect(resultFrom(page, B)).toHaveCount(1);
await expect(resultFrom(page, A)).toHaveCount(1);
@@ -234,6 +248,7 @@ test.describe("hub federated search — scope, per-archive state, attribution",
await page.reload();
await expect(chip(page, A)).toHaveAttribute("data-status", "ready");
await expect(chip(page, B)).toHaveAttribute("data-status", "off");
+ await showAll(page);
await expect(resultFrom(page, A)).toHaveCount(1);
await expect(resultFrom(page, B)).toHaveCount(0);
expect(mocks.b.requests).toEqual([]);
@@ -251,6 +266,7 @@ test.describe("hub federated search — scope, per-archive state, attribution",
const mocks = await setup(page);
mocks.b.pages = "abort";
await page.goto("/");
+ await showAll(page);
await expect(chip(page, B)).toHaveAttribute("data-status", "failed");
await expect(chip(page, B)).toContainText("failed");
@@ -314,6 +330,7 @@ test.describe("hub federated search — scope, per-archive state, attribution",
mocks.b.pages = "abort";
mocks.c!.pages = "hold";
await page.goto("/");
+ await showAll(page);
await expect(chip(page, B)).toHaveAttribute("data-status", "failed");
await expect(chip(page, C)).toHaveAttribute("data-status", "loading");
@@ -335,6 +352,7 @@ test.describe("hub federated search — scope, per-archive state, attribution",
const mocks = await setup(page);
mocks.b.subs = "missing";
await page.goto("/");
+ await showAll(page);
// Ready means every feed of the archive is in, subs included. A 404 is an
// empty subs manifest, at once. It used to be an error, retried after ~1 s,
@@ -354,6 +372,7 @@ test.describe("hub federated search — scope, per-archive state, attribution",
// transcripts first) lists B then A. Neither sets an accent.
await setup(page, [A, B], { summary: [B, A] });
await page.goto("/");
+ await showAll(page);
const cards = page.getByTestId("shelf-spine");
await expect(cards).toHaveCount(2);
@@ -521,6 +540,7 @@ test.describe("hub federated search — scope, per-archive state, attribution",
const mocks = await setup(page);
mocks.b.subs = "error";
await page.goto("/");
+ await showAll(page);
// Live chat is the auxiliary layer: after one retry the error counts as
// settled, so Origin B is ready — its videos searched, without live chat —
@@ -544,6 +564,7 @@ test.describe("hub federated search — scope, per-archive state, attribution",
const mocks = await setup(page);
mocks.b.subs = "error";
await page.goto("/");
+ await showAll(page);
await expect(chip(page, B)).toHaveAttribute("data-status", "ready");
await expect(resultFrom(page, B)).toHaveCount(1);
diff --git a/export/e2e/browse-all.spec.ts b/export/e2e/browse-all.spec.ts
@@ -5,11 +5,12 @@ import {
VIDEO_CHAT_SMALL,
VIDEO_TRANSCRIPT_ONLY,
} from "./fixtures/data";
-import { installRoutes } from "./helpers";
+import { installRoutes, showAll } from "./helpers";
// Browse-all e2e: with no search query active the results section lists every
-// video passing the committed filters (no hits). Typing a query narrows it;
-// clearing it returns to the full browse list.
+// video passing the committed filters (no hits) — once the visitor has pressed
+// Search; until then the page shows no results area at all (first-search.spec).
+// Typing a query narrows it; clearing it returns to the full browse list.
const TRANSCRIPT_ONLY_SLUG = `${CHANNEL_SLUG}/${VIDEO_TRANSCRIPT_ONLY}`;
const CHAT_SMALL_SLUG = `${CHANNEL_SLUG}/${VIDEO_CHAT_SMALL}`;
@@ -31,8 +32,20 @@ test.describe("browse all (no query)", () => {
await installRoutes(page);
});
- test("lists every video on load with no query", async ({ page }) => {
+ test("a clear screen on load, then every video on an empty Search", async ({
+ page,
+ }) => {
await page.goto("/");
+ await page.getByTestId("query-builder").waitFor();
+ // Give the session time to hydrate (it waits for the manifest) and to
+ // show a listing if it were going to.
+ await page.waitForTimeout(1_000);
+ await expect(page.getByTestId("results-section")).toHaveCount(0);
+ 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);
+
+ await page.getByTestId("search-submit").click();
await expect(page.getByTestId("results-section")).toBeVisible();
await expect(page.getByTestId("results-summary")).toHaveText(
"All videos (3)",
@@ -43,6 +56,7 @@ test.describe("browse all (no query)", () => {
test("filters apply to the browse list on Search", async ({ page }) => {
await page.goto("/");
+ await showAll(page);
await expectResultSlugs(page, ALL_SLUGS);
// All fixture videos are non-livestream, so unchecking "Videos" excludes
@@ -62,6 +76,7 @@ test.describe("browse all (no query)", () => {
page,
}) => {
await page.goto("/");
+ await showAll(page);
await expectResultSlugs(page, ALL_SLUGS);
const leafInput = page.locator('input[data-testid^="leaf-query-"]').first();
diff --git a/export/e2e/charts.spec.ts b/export/e2e/charts.spec.ts
@@ -1,5 +1,5 @@
import { expect, test, type Page } from "@playwright/test";
-import { installChartRoutes, urlParams } from "./helpers";
+import { installChartRoutes, showAll, urlParams } from "./helpers";
import { over, painted, rgbOf } from "../../common/testing/chartPixels";
// Charts are now a VIEW MODE of the search page: a "Results | Chart" toggle
@@ -74,6 +74,7 @@ test.describe("charts (search view mode)", () => {
test("Chart toggle with no query plots metadata", async ({ page }) => {
await page.goto("/");
+ await showAll(page);
await expect(page.getByTestId("results-summary")).toContainText("All videos");
await openChart(page);
await expect(page.locator(".recharts-surface")).toBeVisible({
@@ -106,6 +107,7 @@ test.describe("charts (search view mode)", () => {
page,
}) => {
await page.goto("/");
+ await showAll(page);
await expect(page.getByTestId("results-summary")).toContainText("All videos");
await openChart(page);
const opts = page.getByTestId("chart-options");
@@ -122,6 +124,7 @@ test.describe("charts (search view mode)", () => {
page,
}) => {
await page.goto("/");
+ await showAll(page);
await expect(page.getByTestId("results-summary")).toContainText("All videos");
await openChart(page);
const opts = page.getByTestId("chart-options");
@@ -150,6 +153,7 @@ test.describe("charts (search view mode)", () => {
page,
}) => {
await page.goto("/");
+ await showAll(page);
await expect(page.getByTestId("results-summary")).toContainText("All videos");
await openChart(page);
await expect(page.locator(".recharts-surface")).toBeVisible({
@@ -231,6 +235,7 @@ test.describe("charts (search view mode)", () => {
page,
}) => {
await page.goto("/");
+ await showAll(page);
await expect(page.getByTestId("results-summary")).toContainText("All videos");
await openChart(page);
const opts = page.getByTestId("chart-options");
@@ -272,6 +277,7 @@ test.describe("charts (search view mode)", () => {
test(`${width} px: a stacked area paints its true total and every band`, async ({ page }) => {
await page.setViewportSize({ width, height: 1000 });
await page.goto("/");
+ await showAll(page);
await expect(page.getByTestId("results-summary")).toContainText("All videos");
await openChart(page);
const opts = page.getByTestId("chart-options");
diff --git a/export/e2e/first-search.spec.ts b/export/e2e/first-search.spec.ts
@@ -0,0 +1,194 @@
+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();
+}
+
+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 page
+ .getByRole("banner")
+ .getByRole("link", { name: "Use with AI", exact: true })
+ .click();
+ await expect(page).toHaveURL(/\/use-with-ai\/?$/);
+ await page.goBack();
+ await expect(page).not.toHaveURL(/use-with-ai/);
+ 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();
+ });
+});
diff --git a/export/e2e/helpers.ts b/export/e2e/helpers.ts
@@ -156,6 +156,17 @@ export async function openFilters(page: Page) {
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<URLSearchParams> {
const search = await page.evaluate(() => window.location.search);
return new URLSearchParams(search);
diff --git a/export/e2e/responsive.spec.ts b/export/e2e/responsive.spec.ts
@@ -1,6 +1,6 @@
import { expect, test, type Page } from "@playwright/test";
import { CHANNEL_SLUG, VIDEO_TRANSCRIPT_ONLY } from "./fixtures/data";
-import { expectModalOpen, installRoutes } from "./helpers";
+import { expectModalOpen, installRoutes, showAll } from "./helpers";
import { INSTANCES_URL } from "../../common/lib/project";
// The phone. Every other spec in this suite runs at the project's 1440×1200
@@ -109,6 +109,7 @@ test.describe("phone layout", () => {
page,
}) => {
await page.goto("/");
+ await showAll(page);
const summary = page.getByTestId("results-summary");
await expect(summary).toContainText("All videos (3)");
@@ -146,6 +147,7 @@ test.describe("phone layout", () => {
page,
}) => {
await page.goto("/");
+ await showAll(page);
const toolbar = page.getByTestId("selection-toolbar");
// Nothing selected: it is an ordinary row above the cards, as the 1440
// specs see it.
diff --git a/export/e2e/restore-no-refire.spec.ts b/export/e2e/restore-no-refire.spec.ts
@@ -7,6 +7,8 @@ import { installRoutes } from "./helpers";
// 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.
const STORAGE_KEY = "ytdlp-tb:export-filters";
const SHARD = /\/transcripts\/[^/]+\/page-\d+\.json$/;
@@ -78,14 +80,14 @@ test.describe("restored search waits for the visitor", () => {
await expect(
page.getByText("Press Enter or click Search to apply"),
).toBeVisible();
- // The results area is the browse listing under the restored filters.
- await expect(page.getByTestId("results-summary")).toHaveText(
- "All videos (3)",
- );
- await expect(page.getByTestId("browse-hint")).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(
diff --git a/export/e2e/tag-chips.spec.ts b/export/e2e/tag-chips.spec.ts
@@ -1,5 +1,5 @@
import { expect, test, type Page } from "@playwright/test";
-import { installRoutes, installTagRoutes } from "./helpers";
+import { installRoutes, installTagRoutes, showAll } from "./helpers";
import {
CHANNEL_SLUG,
TAG_COLLAB,
@@ -146,6 +146,7 @@ test.describe("curated tag chips", () => {
test("a tagged card shows its tags; an untagged one shows none", async ({
page,
}) => {
+ await showAll(page);
await expect(card(page, VIDEO_CHAT_LARGE).getByTestId("card-tag")).toHaveCount(2);
await expect(
card(page, VIDEO_CHAT_LARGE).locator('[data-testid="card-tag"][data-tag-id="' + TAG_TOPIC + '"]'),
@@ -161,6 +162,7 @@ test.describe("curated tag chips", () => {
page,
}) => {
// All three videos before any tag filter.
+ await showAll(page);
await expect(card(page, VIDEO_TRANSCRIPT_ONLY)).toBeVisible();
await chip(page, TAG_TOPIC).click();
@@ -378,6 +380,7 @@ test.describe("curated tag chips", () => {
await installRoutes(page);
await page.goto("/");
await waitForHydration(page);
+ await showAll(page);
await expect(card(page, VIDEO_TRANSCRIPT_ONLY)).toBeVisible();
await expect(chipRow(page)).toHaveCount(0);
diff --git a/export/e2e/workspace-shell.spec.ts b/export/e2e/workspace-shell.spec.ts
@@ -5,7 +5,7 @@ import {
VIDEO_CHAT_SMALL,
VIDEO_TRANSCRIPT_ONLY,
} from "./fixtures/data";
-import { installRoutes } from "./helpers";
+import { installRoutes, showAll } from "./helpers";
// The unified workspace shell: `/` (results) and `/ask` (chat) share one layout,
// so the persistent search bar and its committed search survive navigation
@@ -61,9 +61,10 @@ test.describe("workspace shell", () => {
"aria-current",
"page",
);
- // Wait for the results pane to hydrate (browse mode lists every video) before
- // clicking — a client Link click lost to a pre-hydration window would leave us
- // stranded on `/`.
+ // Wait for the results pane to hydrate (an empty Search lists every video)
+ // before clicking — a client Link click lost to a pre-hydration window would
+ // leave us stranded on `/`.
+ await showAll(page);
await expect(page.locator("[data-card-header]").first()).toBeVisible();
await nav.getByRole("link", { name: "Chat" }).click();
await expect(page.getByPlaceholder(/Ask about the transcripts/)).toBeVisible();