import { test, expect, type APIRequestContext, type Page } from "@playwright/test"; const BASE = "http://localhost:8765"; const STAMP = Date.now(); const EMAIL = `e2e-${STAMP}@example.com`; const USERNAME = `e2e-player-${STAMP}`; const PASSWORD = "E2e-Playwright-2026!"; let token = ""; // Collect uncaught page errors per page; every test asserts none of them // mention the functions this cleanup cycle removed or fixed. const consoleFailures: string[] = []; function watchPage(page: Page, label: string): void { page.on("pageerror", (err) => { consoleFailures.push(`[${label}] pageerror: ${err.message}`); }); page.on("console", (msg) => { if (msg.type() === "error") { consoleFailures.push(`[${label}] console.error: ${msg.text()}`); } }); } function expectNoRemovedFunctionErrors(): void { const bad = consoleFailures.filter((f) => /(clearSyncQueue|addBookToSelection|removeBookFromSelection|initDashboard|initStatsPage|selectTab|selectRange|selectLibrary|logout|changeTheme|showErrorToast|setupProfileModal|is not defined|is not a function)/i.test( f, ), ); expect(bad, `uncaught errors referencing removed/fixed functions:\n${bad.join("\n")}`).toEqual([]); } async function apiLogin(request: APIRequestContext): Promise { const res = await request.post("/api/auth/login", { data: { login: EMAIL, password: PASSWORD }, }); expect(res.status(), "login should succeed").toBe(200); const body = await res.json(); return body.access_token as string; } test.describe.configure({ mode: "serial" }); test.describe("Bookhoard system smoke + regression", () => { let page: Page; test.beforeAll(async ({ browser, request }) => { const reg = await request.post("/api/auth/register", { data: { email: EMAIL, username: USERNAME, password: PASSWORD }, }); // 201 created OR 409 already-registered (rerun tolerance) expect([201, 200, 409]).toContain(reg.status()); token = await apiLogin(request); expect(token).toBeTruthy(); page = await browser.newPage(); // Auth for server-rendered pages travels in the `token` cookie. await page.context().addCookies([ { name: "token", value: token, url: BASE }, ]); }); test.afterAll(async () => { expectNoRemovedFunctionErrors(); await page?.close(); }); test("health endpoint reports healthy + db connected", async ({ request }) => { const res = await request.get("/health"); expect(res.status()).toBe(200); const body = await res.json(); expect(body.status).toBe("healthy"); expect(body.database).toBe("connected"); }); test("unauthenticated API is guarded", async ({ request }) => { const res = await request.get("/api/auth/profile"); expect([401, 403]).toContain(res.status()); }); test("login API returns a bearer token + user", async ({ request }) => { const res = await request.post("/api/auth/login", { data: { login: EMAIL, password: PASSWORD }, }); expect(res.status()).toBe(200); const body = await res.json(); expect(body.access_token).toBeTruthy(); expect(body.user).toBeTruthy(); }); test("authenticated profile works with bearer token", async ({ request }) => { const res = await request.get("/api/auth/profile", { headers: { Authorization: `Bearer ${token}` }, }); expect(res.status()).toBe(200); const body = await res.json(); const data = body.data ?? body; expect(String(data.username ?? data.Username ?? "")).toContain("e2e-player"); }); test("removed queue endpoints stay gone", async ({ request }) => { const res = await request.get("/api/queue/all", { headers: { Authorization: `Bearer ${token}` }, }); expect(res.status()).toBe(404); }); test("login page renders", async () => { watchPage(page, "login"); const res = await page.goto("/login"); expect(res?.status()).toBe(200); await expect(page.locator("body")).toContainText(/log ?in|sign in/i); }); const protectedPages: Array<[string, RegExp]> = [ ["/dashboard", /dashboard|continue|library|section/i], ["/bookshelf", /bookshelf|book|filter|library/i], ["/collections", /collection/i], ["/series", /series/i], ["/devices", /device/i], ["/stats", /stat|reading|summary|minutes/i], ]; for (const [path, bodyRe] of protectedPages) { test(`page ${path} renders authenticated with no uncaught errors`, async () => { watchPage(page, path); const res = await page.goto(path); expect(res?.status(), `${path} should render`).toBe(200); await expect(page.locator("body")).toContainText(bodyRe); // the theme flash guard must have painted a theme class await expect(page.locator("body")).toHaveClass(/theme-/); }); } test("devices page: add-device modal opens; Clear Queue button is gone", async () => { watchPage(page, "devices-modal"); await page.goto("/devices"); const clearQueue = page.getByRole("button", { name: /clear queue/i }); await expect(clearQueue).toHaveCount(0); // open the add-device modal to prove the page's Alpine wiring works const addBtn = page.getByRole("button", { name: /add device|add new device/i }).first(); if (await addBtn.count()) { await addBtn.click(); await expect(page.locator("#add-device-modal")).toBeVisible(); await expect(page.locator("#device-type")).toBeVisible(); } }); test("stats page: tabs switch with no errors (selectTab restored)", async () => { watchPage(page, "stats-tabs"); await page.goto("/stats"); const tabs = page.locator("[data-tab], .tab-btn, [role=tab]"); const count = await tabs.count(); console.log("stats tab buttons found:", count); if (count > 1) { await tabs.nth(1).click(); await page.waitForTimeout(300); await tabs.nth(0).click(); } }); test("docs: overview and an endpoint page render", async () => { watchPage(page, "docs"); const res = await page.goto("/docs"); expect(res?.status()).toBe(200); await expect(page.locator("body")).toContainText(/api|documentation/i); // endpoint docs render from the split markdown pages const res2 = await page.request.get("/docs/developer/api/api-reference"); expect([200, 301, 302]).toContain(res2.status()); }); test("theme switch persists via the theme module (unexport-only, still wired)", async () => { watchPage(page, "theme"); await page.goto("/dashboard"); const before = await page.evaluate(() => document.body.className); await page.evaluate(() => { localStorage.setItem("theme", "light"); }); await page.reload(); const after = await page.evaluate(() => document.body.className); expect(after).toContain("theme-"); // restore the original theme so the dev environment is untouched await page.evaluate((b) => { const m = /\btheme-([\w-]+)/.exec(b); localStorage.setItem("theme", m ? m[1] : "tokyo-night"); }, before); }); test("bookshelf exposes a reader link for real library content", async () => { watchPage(page, "reader-link"); await page.goto("/bookshelf"); const readerLink = page.locator('a[href*="/readers/"]').first(); if (await readerLink.count()) { const href = await readerLink.getAttribute("href"); console.log("reader href:", href); const res = await page.request.get(href ?? "", { headers: { Authorization: `Bearer ${token}` }, }); // the reader page itself is an SSR page: 200 expected when the item exists expect([200]).toContain(res.status()); } else { console.log("no reader links on bookshelf — skipped"); } }); test("registration page renders with complexity-validated form", async () => { watchPage(page, "register"); const res = await page.goto("/register"); expect(res?.status()).toBe(200); await expect(page.locator("body")).toContainText(/register|create/i); }); test("security headers are on every response", async ({ request }) => { const res = await request.get("/login"); const h = res.headers(); expect(h["x-content-type-options"]).toBe("nosniff"); expect(h["x-frame-options"]).toBe("DENY"); expect(h["content-security-policy"]).toContain("default-src 'self'"); expect(h["referrer-policy"]).toBe("strict-origin-when-cross-origin"); }); test("no console failures were collected anywhere", async () => { expectNoRemovedFunctionErrors(); // strict: any pageerror at all is a failure; console.error entries that are // network 4xx noise from intentional 401/404 probes are tolerated. const real = consoleFailures.filter((f) => f.startsWith("[") && f.includes("pageerror")); expect(real, `uncaught page errors:\n${real.join("\n")}`).toEqual([]); }); });