import { test, expect, type Page, type Request, type Response } from "@playwright/test"; import { MEMBER_STORAGE, openApp } from "./helpers"; // What a plain member (not an admin) sees. The rest of the suite runs as the admin, so without this // spec nothing end to end holds the member's shell: the backend refuses the admin routes (pytest), // but a member shown an admin entry, sent to an admin page or met by a refused request would pass. test.use({ storageState: MEMBER_STORAGE }); const ADMIN_PAGES = ["scheduler", "config", "users", "activity"]; // The pages every member has; Plex joins them only where the instance enables it. const CORE = [ "home", "feed", "channels", "playlists", "notifications", "files", "stats", "settings", ]; const isApi = (url: string) => new URL(url).pathname.startsWith("/api/"); /** Collect every admin call and every failed API answer the page gets while `run` goes. `run` is * handed `settled`, which waits until no API request of the page is in flight: an error state * renders only once its request has answered, and a check made before that passes on a page that * is still loading. */ async function watchRequests( page: Page, run: (settled: () => Promise) => Promise, ): Promise { const bad: string[] = []; const pending = new Set(); const onRequest = (r: Request) => { if (!isApi(r.url())) return; pending.add(r); if (new URL(r.url()).pathname.startsWith("/api/admin")) bad.push(`admin call ${r.url()}`); }; const onDone = (r: Request) => pending.delete(r); const onResponse = (r: Response) => { if (isApi(r.url()) && r.status() >= 400) bad.push(`${r.status()} ${r.url()}`); }; const settled = () => expect.poll(() => pending.size, { timeout: 15_000 }).toBe(0); page.on("request", onRequest); page.on("requestfinished", onDone); page.on("requestfailed", onDone); page.on("response", onResponse); try { await run(settled); await settled(); } finally { page.off("request", onRequest); page.off("requestfinished", onDone); page.off("requestfailed", onDone); page.off("response", onResponse); } return bad; } test.describe("a member", () => { test("gets no admin entry, and every page of the rail opens without an error", async ({ page, }) => { const bad = await watchRequests(page, async (settled) => { await openApp(page); await expect(page.getByTestId("nav-admin")).toHaveCount(0); const rail = page .getByRole("navigation", { name: "Primary navigation" }) .locator('[data-testid^="nav-"]'); const ids = (await rail.evaluateAll((els) => els.map((el) => el.getAttribute("data-testid")))) .filter((id): id is string => id !== null) .map((id) => id.replace(/^nav-/, "")); expect(ids).toEqual(expect.arrayContaining(CORE)); expect(ids.filter((id) => ADMIN_PAGES.includes(id))).toEqual([]); for (const id of ids) { await page.getByTestId(`nav-${id}`).click(); await expect(page.getByTestId(`nav-${id}`)).toHaveAttribute("aria-current", "page"); await settled(); // An error state is a `role=alert` StateMessage; a page that failed to load shows one. await expect(page.getByRole("alert"), `error state on ${id}`).toHaveCount(0); } }); expect(bad).toEqual([]); }); test("a link to an admin page opens no admin page", async ({ page }) => { for (const id of ADMIN_PAGES) { const bad = await watchRequests(page, () => openApp(page, `/?page=${id}`)); expect(bad, id).toEqual([]); await expect(page.getByTestId("nav-admin")).toHaveCount(0); // The member lands on the feed instead, and the rail says so. await expect(page.getByTestId("nav-feed")).toHaveAttribute("aria-current", "page"); } }); });