import AxeBuilder from "@axe-core/playwright"; import { expect, test } from "@playwright/test"; const PAGES = [ "/", "/research", "/topics", "/series", "/about", "/methodology", "/infrastructure", "/transparency", "/legal/privacy", "/legal/terms", "/legal/research-disclaimer", ]; test.describe("public site", () => { for (const path of PAGES) { test(`renders ${path} with no serious a11y violations`, async ({ page }) => { const res = await page.goto(path); expect(res?.status()).toBe(200); await expect(page.locator("main")).toBeVisible(); await expect(page.getByText(/MacLustr/).first()).toBeVisible(); const axe = await new AxeBuilder({ page }).withTags(["wcag2a", "wcag2aa"]).analyze(); const serious = axe.violations.filter((v) => v.impact === "serious" || v.impact === "critical"); expect( serious, JSON.stringify( serious.map((v) => ({ id: v.id, nodes: v.nodes.length })), null, 1, ), ).toEqual([]); }); } test("responsive screenshots", async ({ page }) => { for (const [name, w] of [ ["mobile", 390], ["tablet", 820], ["desktop", 1440], ] as const) { await page.setViewportSize({ width: w, height: 900 }); for (const [slug, path] of [ ["home", "/"], ["research", "/research"], ["topics", "/topics"], ["paper", "/research/as-wp-2026-0000-staging-sample"], ] as const) { await page.goto(path); await page.screenshot({ path: `tests/e2e/screenshots/${slug}-${name}.png`, fullPage: true }); } } }); test("dark mode toggle persists", async ({ page }) => { await page.goto("/"); await page.getByRole("button", { name: /colour scheme/i }).click(); await expect(page.locator("html")).toHaveClass(/dark/); await page.reload(); await expect(page.locator("html")).toHaveClass(/dark/); }); test("keyboard: skip link and header nav are reachable", async ({ page }) => { await page.goto("/"); await page.keyboard.press("Tab"); await expect(page.locator(".skip-link")).toBeFocused(); await page.keyboard.press("Enter"); await expect(page.locator("#main")).toBeVisible(); }); test("search, filters, pagination and empty state", async ({ page }) => { await page.goto("/research?q=zzzz-no-such-paper-zzzz"); await expect(page.getByText(/No papers match these filters yet/)).toBeVisible(); await page.goto("/research?discipline=interdisciplinary&mode=theoretical"); await expect(page.locator("main")).toContainText(/paper/); await page.goto("/research?page=abc&pageSize=9999"); await expect(page.locator('p[role="alert"]')).toContainText(/invalid/i); const r = await page.request.get("/api/public/papers?q=staging&sort=relevance"); expect(r.status()).toBe(200); const j = (await r.json()) as { items: Array<{ publicId: string }>; total: number }; expect(j.total).toBeGreaterThanOrEqual(1); expect(j.items[0]?.publicId).toBe("AS-WP-2026-0000"); }); test("paper page, PDF route, citation exports and JSON-LD", async ({ page, request }) => { await page.goto("/research/as-wp-2026-0000-staging-sample"); await expect(page.getByRole("heading", { level: 1 })).toContainText("STAGING SAMPLE"); await expect(page.getByText("AutoScholar — AI-generated research").first()).toBeVisible(); await expect(page.getByText(/not formally peer reviewed/).first()).toBeVisible(); const ld = await page.locator('script[type="application/ld+json"]').textContent(); expect(ld).toContain('"ScholarlyArticle"'); expect(ld).not.toContain("doi"); const pdf = await request.get("/papers/AS-WP-2026-0000/v1/manuscript/main.pdf"); expect(pdf.status()).toBe(200); expect(pdf.headers()["content-type"]).toContain("application/pdf"); expect(pdf.headers()["x-artifact-sha256"]).toHaveLength(64); const notListed = await request.get("/papers/AS-WP-2026-0000/v1/manuscript/secret.txt"); expect(notListed.status()).toBe(404); const traversal = await request.get("/papers/AS-WP-2026-0000/v1/..%2F..%2Fetc%2Fpasswd"); expect([400, 404]).toContain(traversal.status()); for (const ext of ["bib", "ris", "txt"]) { const c = await request.get(`/research/as-wp-2026-0000-staging-sample/cite.${ext}`); expect(c.status()).toBe(200); expect(await c.text()).toContain("AS-WP-2026-0000"); } await page.getByRole("tab", { name: "RIS" }).click(); await expect(page.getByText("TY - RPRT")).toBeVisible(); await expect(page.getByText(/Version history/)).toBeVisible(); const api = await request.get("/api/public/papers/as-wp-2026-0000-staging-sample"); const body = await api.text(); expect(api.status()).toBe(200); expect(body).not.toMatch(/tokenHash|storageKey|instructionText|fencing/); expect(await (await request.get("/research/does-not-exist")).status()).toBe(404); }); test("feeds, sitemap, robots and security headers", async ({ request }) => { const rss = await request.get("/feed.xml"); expect(rss.status()).toBe(200); expect(rss.headers()["content-type"]).toContain("rss"); expect(await rss.text()).toContain("