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 that names its // view, tagged with the page (moduleUrl.contract.test holds the tags to the module definitions), // plus the overlays the shell opens over any page (the channel page, the player) and the live // search. Not Plex: what it shows is the instance's own library, which a test stack may not have. 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) { 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 }); const column = (p: Page, name: string) => p.getByRole("columnheader", { name: new RegExp(`^${name}`) }); async function gotoAdminPage(page: Page, id: string): Promise { await page.getByTestId("nav-admin").click(); await page.getByTestId(`nav-admin-${id}`).click(); } test.describe("a bookmark reopens the view it was taken of", () => { test("the feed's filters", { tag: "@feed" }, 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", { tag: "@home" }, 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", { tag: "@settings" }, 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", { tag: "@users" }, async ({ page, browser }) => { await openApp(page, "/?page=feed"); await gotoAdminPage(page, "users"); 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("a Configuration group", { tag: "@config" }, async ({ page, browser }) => { await openApp(page, "/?page=feed"); await gotoAdminPage(page, "config"); const second = page.getByRole("tab").nth(1); const name = (await second.textContent()) ?? ""; await second.click(); await expect(page).toHaveURL(/[?&]cfgtab=/); const fresh = await bookmarked(browser, page); await expect(fresh.getByRole("tab", { name, exact: true })).toHaveAttribute( "aria-selected", "true", ); await fresh.context().close(); }); test("the Activity tab and its time window", { tag: "@activity" }, async ({ page, browser }) => { await openApp(page, "/?page=feed"); await gotoAdminPage(page, "activity"); await tab(page, "Server log").click(); await page.getByRole("button", { name: "30 days", exact: true }).click(); await expect(page).toHaveURL(/[?&]aw=30d/); const fresh = await bookmarked(browser, page); await expect(tab(fresh, "Server log")).toHaveAttribute("aria-selected", "true"); await expect(fresh.getByRole("button", { name: "30 days", exact: true })).toHaveAttribute( "aria-pressed", "true", ); await fresh.context().close(); }); test("a playlist and its sort", { tag: "@playlists" }, async ({ page, browser }) => { await openApp(page, "/?page=feed"); await gotoPage(page, "playlists"); await expect(column(page, "Title")).toBeVisible(); await column(page, "Title").getByRole("button").first().click(); await expect(page).toHaveURL(/[?&]plsort=/); const sorted = await column(page, "Title").getAttribute("aria-sort"); const fresh = await bookmarked(browser, page); await expect(column(fresh, "Title")).toHaveAttribute("aria-sort", sorted ?? ""); expect(new URL(fresh.url()).searchParams.get("pl")).toBe( new URL(page.url()).searchParams.get("pl"), ); await fresh.context().close(); }); test( "the Channels manager's tab and name search", { tag: "@channels" }, 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", { tag: "@files" }, 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(); }); // A file of the test's own, uploaded for it and deleted after, and the account's sort put back: // the account is left as it was found. test( "the Files sort, and the file open in the viewer", { tag: "@files" }, async ({ page, browser }) => { await openApp(page, "/?page=feed"); const NAME = "bookmark-note"; // One a run cut short left behind goes first. const left = (await (await page.request.get("/api/files")).json()) as Record< string, unknown >[]; for (const f of left.filter((f) => JSON.stringify(f).includes(`"${NAME}`))) await page.request.delete(`/api/files/${String(f.id)}`); const up = await page.request.post("/api/files/upload", { headers: { "X-Upload-Filename": `${NAME}.txt` }, data: Buffer.from("a note the bookmark reopens\n"), }); expect(up.ok(), await up.text()).toBeTruthy(); const { id } = (await up.json()) as { id: number }; // The account's own sort, put back after: a header press cycles it (asc, desc, none), so the // case starts from none, as the address names it, whatever an earlier run left. const me = (await (await page.request.get("/api/me")).json()) as { preferences?: Record; }; const ownSort = me.preferences?.filesSort ?? null; try { await openApp(page, "/?page=files&fsort=none"); await column(page, "Name").getByRole("button").first().click(); await expect(page).toHaveURL(/[?&]fsort=name/); const sorted = await column(page, "Name").getAttribute("aria-sort"); await page.getByText(NAME, { exact: true }).first().click(); await expect(page).toHaveURL(new RegExp(`[?&]fview=${id}`)); const fresh = await bookmarked(browser, page); await expect(fresh.getByRole("dialog", { name: NAME })).toBeVisible(); await fresh.keyboard.press("Escape"); await expect(column(fresh, "Name")).toHaveAttribute("aria-sort", sorted ?? ""); await fresh.context().close(); } finally { await page.request.delete(`/api/files/${id}`); await page.request.put("/api/me/preferences", { data: { filesSort: ownSort } }); } }, ); 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(); await clickVisibleChannelLink(page); 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. The instance's search settings are // pinned too (the free source, switched on): a stack left on the API source reopens the search // waiting for its press, by design, and one with search off offers none. test( "a live YouTube search, its term and its count", { tag: "@feed" }, async ({ page, browser }) => { const answer = async (p: Page, seen: string[]) => { await p.route("**/api/me", async (route) => { const me = (await (await route.fetch()).json()) as Record; return route.fulfill({ json: { ...me, search_source: "scrape", search_enabled: true } }); }); 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(); }, ); });