The suite ran only as an admin, so nothing end to end held what a member sees. A second seeded account, a member with its own session, now walks every rail page (no admin entry, no admin call, no error state) and follows links to the admin pages. It found one: a member's link to an admin page showed the feed with no rail row current, and the address bar kept naming the admin page — only the content fell back. The navigation state now derives the page on screen through each module's gate (reachablePage), so the rail, the content, the tab title and the address bar agree, on a link, a Back, and a role taken away mid-session.
79 lines
3.0 KiB
TypeScript
79 lines
3.0 KiB
TypeScript
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",
|
|
];
|
|
|
|
/** Collect every admin call and every refused response the page makes while `run` goes. */
|
|
async function watchRequests(page: Page, run: () => Promise<void>): Promise<string[]> {
|
|
const bad: string[] = [];
|
|
const onRequest = (r: Request) => {
|
|
if (new URL(r.url()).pathname.startsWith("/api/admin")) bad.push(`admin call ${r.url()}`);
|
|
};
|
|
const onResponse = (r: Response) => {
|
|
if (r.status() === 401 || r.status() === 403) bad.push(`${r.status()} ${r.url()}`);
|
|
};
|
|
page.on("request", onRequest);
|
|
page.on("response", onResponse);
|
|
try {
|
|
await run();
|
|
} finally {
|
|
page.off("request", onRequest);
|
|
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 () => {
|
|
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");
|
|
// 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 one of their own pages instead.
|
|
await expect(page.locator('[data-testid^="nav-"][aria-current="page"]')).toHaveCount(1);
|
|
}
|
|
});
|
|
});
|