Files
siftlode/frontend/e2e/bookmark.spec.ts
T
peter 7b4e007680 feat(nav): the live YouTube search and Home's budget are named in the address
A YouTube search is `?yt=<term>` (+ `ytn` for a non-default count), so a reload, a link
or Back/Forward reopens it. It runs by itself only where it costs no quota: the free page
scrape, a search asked for in this document, or results still cached. An API search
reopened any other way shows its term and waits for "Search YouTube again".

Home's time budget is `hb`; the question's draw stays on the history entry.

An e2e spec opens every page's bookmark in a fresh browser context; CLAUDE.md and
docs/development.md name the module URL codec as the place for view state.
2026-10-27 11:51:29 +01:00

147 lines
6.4 KiB
TypeScript

import { test, expect, type Browser, type Page } from "@playwright/test";
import {
clickVisibleChannelLink,
expectContentType,
gotoPage,
openApp,
openFirstVideo,
toggleContentType,
} from "./helpers";
// The address is the view: a view set through the UI, its address opened as a bookmark — in a NEW
// browser context, so nothing this tab remembered (its history entries, sessionStorage, the view
// storage it wrote) can stand in for what the address says. One round trip per page, plus the
// overlays the shell opens over any page (the channel page, the player) and the live search.
const STORAGE_STATE = "e2e/.auth/state.json";
/** The page's address, opened as a bookmark in a fresh context of the same account. */
async function bookmarked(browser: Browser, page: Page, prepare?: (p: Page) => Promise<void>) {
const url = page.url();
const ctx = await browser.newContext({ storageState: STORAGE_STATE, locale: "en-US" });
const fresh = await ctx.newPage();
await prepare?.(fresh);
await openApp(fresh, url);
return fresh;
}
const tab = (p: Page, name: string) => p.getByRole("tab", { name, exact: true });
test.describe("a bookmark reopens the view it was taken of", () => {
test("the feed's filters", async ({ page, browser }) => {
await openApp(page, "/?page=feed");
await toggleContentType(page, "includeLive");
await expect(page).toHaveURL(/[?&]live=/);
const fresh = await bookmarked(browser, page);
await expectContentType(fresh, "includeLive", true);
await fresh.context().close();
});
test("Home's time budget", async ({ page, browser }) => {
await openApp(page, "/?page=home");
await page.getByRole("button", { name: "An evening", exact: true }).click();
await expect(page).toHaveURL(/[?&]hb=long/);
const fresh = await bookmarked(browser, page);
await expect(fresh.getByRole("button", { name: "An evening", exact: true })).toHaveAttribute(
"aria-pressed",
"true",
);
await fresh.context().close();
});
test("a Settings tab", async ({ page, browser }) => {
await openApp(page, "/?page=feed");
await gotoPage(page, "settings");
await tab(page, "Notifications").click();
const fresh = await bookmarked(browser, page);
await expect(tab(fresh, "Notifications")).toHaveAttribute("aria-selected", "true");
await fresh.context().close();
});
test("an admin page's tab", async ({ page, browser }) => {
await openApp(page, "/?page=feed");
await page.getByTestId("nav-admin").click();
await page.getByTestId("nav-admin-users").click();
await tab(page, "Access requests").click();
const fresh = await bookmarked(browser, page);
await expect(tab(fresh, "Access requests")).toHaveAttribute("aria-selected", "true");
await fresh.context().close();
});
test("the Channels manager's tab and name search", async ({ page, browser }) => {
await openApp(page, "/?page=feed");
await gotoPage(page, "channels");
await tab(page, "Hidden").click();
await page.getByPlaceholder("Search channels…").fill("zz");
await expect(page).toHaveURL(/zz/); // written once the typing settles
const fresh = await bookmarked(browser, page);
await expect(tab(fresh, "Hidden")).toHaveAttribute("aria-selected", "true");
await expect(fresh.getByPlaceholder("Search channels…")).toHaveValue("zz");
await fresh.context().close();
});
test("the Files search", async ({ page, browser }) => {
await openApp(page, "/?page=feed");
await gotoPage(page, "files");
await page.getByPlaceholder("Search files…").fill("zz");
await expect(page).toHaveURL(/zz/);
const fresh = await bookmarked(browser, page);
await expect(fresh.getByPlaceholder("Search files…")).toHaveValue("zz");
await fresh.context().close();
});
test("a channel page's own tab, over the page it was opened from", async ({ page, browser }) => {
await openApp(page, "/?page=feed");
await expect(page.getByTestId("video-card").first()).toBeVisible();
expect(await clickVisibleChannelLink(page)).toBe(true);
const title = await page.getByTestId("channel-title").textContent();
await tab(page, "About").click();
const fresh = await bookmarked(browser, page);
await expect(fresh.getByTestId("channel-title")).toHaveText(title ?? "");
await expect(tab(fresh, "About")).toHaveAttribute("aria-selected", "true");
await fresh.context().close();
});
test("the player and the video it plays", async ({ page, browser }) => {
await openApp(page, "/?page=feed");
await openFirstVideo(page);
await expect(page).toHaveURL(/[?&]watch=/);
const watch = new URL(page.url()).searchParams.get("watch");
const fresh = await bookmarked(browser, page);
await expect(fresh.getByTestId("player-modal")).toBeVisible();
expect(new URL(fresh.url()).searchParams.get("watch")).toBe(watch);
await fresh.context().close();
});
// The search itself never reaches YouTube here: the answer is the server's shape, served by the
// test, and the request it answers is what the address named.
test("a live YouTube search, its term and its count", async ({ page, browser }) => {
const answer = async (p: Page, seen: string[]) => {
await p.route("**/api/search/youtube?**", (route) => {
seen.push(route.request().url());
return route.fulfill({ json: { items: [], next_cursor: null, source: "scrape" } });
});
};
await answer(page, []);
await openApp(page, "/?page=feed");
await page.getByPlaceholder("Search your subscriptions…").fill("lofi");
await page.getByRole("button", { name: "Search YouTube", exact: true }).click();
await expect(page.getByText("YouTube results for “lofi”")).toBeVisible();
await expect(page).toHaveURL(/[?&]yt=lofi/); // named as it opens, not only once the count is set
await page.getByRole("button", { name: "40", exact: true }).click();
await expect(page).toHaveURL(/[?&]ytn=40/);
const seen: string[] = [];
const fresh = await bookmarked(browser, page, (p) => answer(p, seen));
await expect(fresh.getByText("YouTube results for “lofi”")).toBeVisible();
await expect(fresh.getByRole("button", { name: "40", exact: true })).toHaveAttribute(
"aria-pressed",
"true",
);
await expect.poll(() => seen.length).toBeGreaterThan(0);
const asked = new URL(seen[0]!).searchParams;
expect([asked.get("q"), asked.get("limit")]).toEqual(["lofi", "40"]);
await fresh.context().close();
});
});