spb/autoscholar
Public
1import AxeBuilder from "@axe-core/playwright";2import { expect, test } from "@playwright/test";34const PAGES = [5 "/",6 "/research",7 "/topics",8 "/series",9 "/about",10 "/methodology",11 "/infrastructure",12 "/transparency",13 "/legal/privacy",14 "/legal/terms",15 "/legal/research-disclaimer",16];1718test.describe("public site", () => {19 for (const path of PAGES) {20 test(`renders ${path} with no serious a11y violations`, async ({ page }) => {21 const res = await page.goto(path);22 expect(res?.status()).toBe(200);23 await expect(page.locator("main")).toBeVisible();24 await expect(page.getByText(/MacLustr/).first()).toBeVisible();25 const axe = await new AxeBuilder({ page }).withTags(["wcag2a", "wcag2aa"]).analyze();26 const serious = axe.violations.filter((v) => v.impact === "serious" || v.impact === "critical");27 expect(28 serious,29 JSON.stringify(30 serious.map((v) => ({ id: v.id, nodes: v.nodes.length })),31 null,32 1,33 ),34 ).toEqual([]);35 });36 }3738 test("responsive screenshots", async ({ page }) => {39 for (const [name, w] of [40 ["mobile", 390],41 ["tablet", 820],42 ["desktop", 1440],43 ] as const) {44 await page.setViewportSize({ width: w, height: 900 });45 for (const [slug, path] of [46 ["home", "/"],47 ["research", "/research"],48 ["topics", "/topics"],49 ["paper", "/research/as-wp-2026-0000-staging-sample"],50 ] as const) {51 await page.goto(path);52 await page.screenshot({ path: `tests/e2e/screenshots/${slug}-${name}.png`, fullPage: true });53 }54 }55 });5657 test("dark mode toggle persists", async ({ page }) => {58 await page.goto("/");59 await page.getByRole("button", { name: /colour scheme/i }).click();60 await expect(page.locator("html")).toHaveClass(/dark/);61 await page.reload();62 await expect(page.locator("html")).toHaveClass(/dark/);63 });6465 test("keyboard: skip link and header nav are reachable", async ({ page }) => {66 await page.goto("/");67 await page.keyboard.press("Tab");68 await expect(page.locator(".skip-link")).toBeFocused();69 await page.keyboard.press("Enter");70 await expect(page.locator("#main")).toBeVisible();71 });7273 test("search, filters, pagination and empty state", async ({ page }) => {74 await page.goto("/research?q=zzzz-no-such-paper-zzzz");75 await expect(page.getByText(/No papers match these filters yet/)).toBeVisible();76 await page.goto("/research?discipline=interdisciplinary&mode=theoretical");77 await expect(page.locator("main")).toContainText(/paper/);78 await page.goto("/research?page=abc&pageSize=9999");79 await expect(page.locator('p[role="alert"]')).toContainText(/invalid/i);80 const r = await page.request.get("/api/public/papers?q=staging&sort=relevance");81 expect(r.status()).toBe(200);82 const j = (await r.json()) as { items: Array<{ publicId: string }>; total: number };83 expect(j.total).toBeGreaterThanOrEqual(1);84 expect(j.items[0]?.publicId).toBe("AS-WP-2026-0000");85 });8687 test("paper page, PDF route, citation exports and JSON-LD", async ({ page, request }) => {88 await page.goto("/research/as-wp-2026-0000-staging-sample");89 await expect(page.getByRole("heading", { level: 1 })).toContainText("STAGING SAMPLE");90 await expect(page.getByText("AutoScholar — AI-generated research").first()).toBeVisible();91 await expect(page.getByText(/not formally peer reviewed/).first()).toBeVisible();92 const ld = await page.locator('script[type="application/ld+json"]').textContent();93 expect(ld).toContain('"ScholarlyArticle"');94 expect(ld).not.toContain("doi");95 const pdf = await request.get("/papers/AS-WP-2026-0000/v1/manuscript/main.pdf");96 expect(pdf.status()).toBe(200);97 expect(pdf.headers()["content-type"]).toContain("application/pdf");98 expect(pdf.headers()["x-artifact-sha256"]).toHaveLength(64);99 const notListed = await request.get("/papers/AS-WP-2026-0000/v1/manuscript/secret.txt");100 expect(notListed.status()).toBe(404);101 const traversal = await request.get("/papers/AS-WP-2026-0000/v1/..%2F..%2Fetc%2Fpasswd");102 expect([400, 404]).toContain(traversal.status());103 for (const ext of ["bib", "ris", "txt"]) {104 const c = await request.get(`/research/as-wp-2026-0000-staging-sample/cite.${ext}`);105 expect(c.status()).toBe(200);106 expect(await c.text()).toContain("AS-WP-2026-0000");107 }108 await page.getByRole("tab", { name: "RIS" }).click();109 await expect(page.getByText("TY - RPRT")).toBeVisible();110 await expect(page.getByText(/Version history/)).toBeVisible();111 const api = await request.get("/api/public/papers/as-wp-2026-0000-staging-sample");112 const body = await api.text();113 expect(api.status()).toBe(200);114 expect(body).not.toMatch(/tokenHash|storageKey|instructionText|fencing/);115 expect(await (await request.get("/research/does-not-exist")).status()).toBe(404);116 });117118 test("feeds, sitemap, robots and security headers", async ({ request }) => {119 const rss = await request.get("/feed.xml");120 expect(rss.status()).toBe(200);121 expect(rss.headers()["content-type"]).toContain("rss");122 expect(await rss.text()).toContain("<rss");123 const atom = await request.get("/feed.atom");124 expect(atom.status()).toBe(200);125 expect(await atom.text()).toContain("<feed");126 const robots = await request.get("/robots.txt");127 expect(robots.status()).toBe(200);128 expect(await robots.text()).toMatch(/Disallow/);129 const sm = await request.get("/sitemap.xml");130 expect(sm.status()).toBe(200);131 const home = await request.get("/");132 const h = home.headers();133 expect(h["content-security-policy"]).toContain("default-src 'self'");134 expect(h["x-content-type-options"]).toBe("nosniff");135 expect(h["x-powered-by"]).toBeUndefined();136 });137});138