Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

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:
Mexport/e2e-hub/federated-search.spec.ts | 21+++++++++++++++++++++
Mexport/e2e/browse-all.spec.ts | 23+++++++++++++++++++----
Mexport/e2e/charts.spec.ts | 8+++++++-
Aexport/e2e/first-search.spec.ts | 194+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mexport/e2e/helpers.ts | 11+++++++++++
Mexport/e2e/responsive.spec.ts | 4+++-
Mexport/e2e/restore-no-refire.spec.ts | 12+++++++-----
Mexport/e2e/tag-chips.spec.ts | 5++++-
Mexport/e2e/workspace-shell.spec.ts | 9+++++----
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();