Archilyzer · Source

archilyzer

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

commit 56e8d5b6b0b03c5e05e7d683e08ee7c4350712bf
parent 292de396ded34fb74230b48caef0e33126008101
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Sat, 29 Aug 2026 21:08:14 -0400

e2e: one spec that looks at the site on a phone

Every other spec runs at 1440x1200, which is how a header that wrapped
out of its own background and a modal with a hundred pixels of cue list
both shipped. This one runs at 390: no route scrolls sideways, the menu
sheet carries the nav, the filters sheet applies and closes, the modal
is a full-screen player with a 300px cue list and a reachable Close,
and the composer is still on screen after an answer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Diffstat:
Aexport/e2e/responsive.spec.ts | 153+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 153 insertions(+), 0 deletions(-)

diff --git a/export/e2e/responsive.spec.ts b/export/e2e/responsive.spec.ts @@ -0,0 +1,153 @@ +import { expect, test, type Page } from "@playwright/test"; +import { CHANNEL_SLUG, VIDEO_TRANSCRIPT_ONLY } from "./fixtures/data"; +import { expectModalOpen, installRoutes } from "./helpers"; + +// The phone. Every other spec in this suite runs at the project's 1440×1200 +// desktop viewport, which is exactly why the export site could ship a header +// that wrapped out of its own background, a footer that overflowed at 360px and +// a transcript modal with ~100px of cue list: nothing was ever looking. +// +// A file-level `test.use` overrides the Desktop Chrome project for this file +// only, so these assertions are about the narrow layout and nothing else. +test.use({ + viewport: { width: 390, height: 844 }, + isMobile: true, + hasTouch: true, +}); + +// The one invariant worth asserting on every route: the page must never scroll +// sideways. `scrollWidth > clientWidth` is what a reader feels as the layout +// being slightly wider than their phone. +async function expectNoHorizontalOverflow(page: Page) { + const over = await page.evaluate( + () => + document.documentElement.scrollWidth - + document.documentElement.clientWidth, + ); + expect(over).toBeLessThanOrEqual(0); +} + +const CORS = { + "access-control-allow-origin": "*", + "access-control-allow-headers": "*", + "access-control-allow-methods": "*", +}; + +function sse(text: string): string { + return ( + `data: ${JSON.stringify({ + type: "content_block_delta", + delta: { type: "text_delta", text }, + })}\n\n` + `data: ${JSON.stringify({ type: "message_stop" })}\n\n` + ); +} + +// A one-turn provider: no retrieval round trip (the gather step is told DONE +// immediately), then a streamed answer. Enough to put a message on screen and +// see where the composer ends up. +async function mockAI(page: Page) { + await page.route("https://api.anthropic.com/**", async (route) => { + if (route.request().method() === "OPTIONS") { + await route.fulfill({ status: 204, headers: CORS }); + return; + } + const body = route.request().postDataJSON() as { system?: string }; + const answering = (body.system ?? "").includes("Markdown"); + await route.fulfill({ + status: 200, + headers: { ...CORS, "content-type": "text/event-stream" }, + body: sse(answering ? "A short answer about alpha." : "DONE"), + }); + }); +} + +test.describe("phone layout", () => { + test.beforeEach(async ({ page }) => { + await installRoutes(page); + }); + + // /duplicates.json is 404-mocked by installRoutes, so the duplicates route + // renders its empty state — which is still a full page of chrome to overflow. + for (const route of [ + "/", + "/ask/", + "/downloads/", + "/duplicates/", + "/use-with-ai/", + ]) { + test(`no horizontal overflow on ${route}`, async ({ page }) => { + await page.goto(route); + await page.waitForLoadState("networkidle"); + await expectNoHorizontalOverflow(page); + }); + } + + test("the header menu carries the nav that the wide header shows inline", async ({ + page, + }) => { + await page.goto("/"); + await page.getByRole("button", { name: "Open menu" }).click(); + const menu = page.getByRole("dialog"); + await expect(menu.getByRole("link", { name: "Ask AI" })).toBeVisible(); + await expect(menu.getByRole("link", { name: "Search" })).toBeVisible(); + // The theme picker is a dropdown in the wide header; here it is a plain + // radio list, because a popover inside a dialog is a focus-trap fight. + await expect(menu.getByRole("radio", { name: "Archive" })).toBeVisible(); + }); + + test("the filters sheet applies a filter and the results change", async ({ + page, + }) => { + await page.goto("/"); + const summary = page.getByTestId("results-summary"); + await expect(summary).toContainText("All videos (3)"); + + await page.getByTestId("filters-trigger").click(); + const sheet = page.getByRole("dialog"); + await sheet.getByRole("checkbox", { name: "Videos" }).uncheck(); + await sheet.getByRole("button", { name: "Apply filters" }).click(); + + // Apply commits and closes; nothing is left covering the results. + await expect(sheet).toBeHidden(); + await expect(summary).toContainText("All videos (0)"); + }); + + test("the transcript modal is a full-screen player view", async ({ page }) => { + await page.goto(`/?v=${CHANNEL_SLUG}/${VIDEO_TRANSCRIPT_ONLY}`); + await expectModalOpen(page); + + // The cue list is the point of the modal. It used to get ~100px at this + // width, under a player slot sized for a desktop card. + const box = await page.getByTestId("cue-list").boundingBox(); + expect(box).not.toBeNull(); + expect(box!.height).toBeGreaterThanOrEqual(300); + + // Every control is a real button with a visible label or an icon, and the + // three that close the thing never scroll out of reach. + await expect( + page.getByRole("button", { name: "Close player" }), + ).toBeInViewport(); + await expect( + page.getByRole("button", { name: "Show live chat" }), + ).toBeInViewport(); + }); + + test("the composer stays on screen once the chat has an answer", async ({ + page, + }) => { + await mockAI(page); + await page.goto("/ask/"); + await page.getByRole("button", { name: "Scripted" }).click(); + await page.locator('input[placeholder^="sk-ant"]').fill("sk-ant-test"); + + const box = page.getByPlaceholder(/Ask about the transcripts/); + await box.fill("what is alpha"); + await page.getByRole("button", { name: "Ask", exact: true }).click(); + await expect(page.getByText("A short answer about alpha.")).toBeVisible(); + + // The whole point of the sticky composer: after an answer arrives, the box + // you type the next question into has not scrolled away. + await expect(box).toBeInViewport(); + await expectNoHorizontalOverflow(page); + }); +});