Repository Public
1import AxeBuilder from "@axe-core/playwright";2import { expect, test } from "@playwright/test";34const ROUTES = ["", "/live", "/nodes", "/nodes/node-01", "/nodes/node-09", "/architecture", "/research", "/benchmarks", "/gallery", "/access"];5const PRIVATE = /\b(?:192\.168|10\.67|100\.\d{2,3})\.\d{1,3}\.\d{1,3}\b|maclustr\.io|51\.161\.\d+\.\d+|wireguard|tailscale/i;67for (const locale of ["en", "fr"] as const) {8 for (const route of ROUTES) {9 test(`${locale}${route || "/"} renders, is localized, accessible and leaks nothing`, async ({ page }) => {10 const res = await page.goto(`/${locale}${route}`);11 expect(res?.status()).toBe(200);12 await expect(page.locator("html")).toHaveAttribute("lang", locale === "fr" ? "fr-CA" : "en");13 await expect(page.locator("h1")).toHaveCount(1);14 await expect(page.locator(`link[rel="alternate"][hreflang="fr-CA"]`)).toHaveAttribute("href", new RegExp(`/fr${route}$`));15 await expect(page.locator(`link[rel="canonical"]`)).toHaveAttribute("href", new RegExp(`/${locale}${route}$`));16 expect(await page.content()).not.toMatch(PRIVATE);17 const axe = await new AxeBuilder({ page }).withTags(["wcag2a", "wcag2aa", "wcag21aa", "wcag22aa"]).analyze();18 expect(axe.violations.map((v) => `${v.id}: ${v.nodes.map((n) => n.target.join(" ")).slice(0, 3).join(" | ")}`)).toEqual([]);19 });20 }21}2223test("live dashboard shows current readings or says plainly that telemetry is unavailable", async ({ page, request }) => {24 const api = await (await request.get("/api/live")).json();25 expect(JSON.stringify(api)).not.toMatch(PRIVATE);26 await page.goto("/en/live");27 await expect(page.getByTestId("live-tiles").locator("li")).toHaveCount(29);28 const fresh = page.getByTestId("live-freshness");29 await expect(fresh).not.toHaveAttribute("data-state", "loading");30 if (api.available) {31 expect(api.nodes).toHaveLength(29);32 expect(Object.keys(api.nodes[0]).sort()).toEqual(["battery", "cpu", "gpu", "id", "load1", "memPct", "memTotalGB", "memUsedGB", "netInKBs", "netOutKBs", "online", "thermal", "uptimeS"]);33 await expect(fresh).toHaveAttribute("data-state", "live");34 await expect(page.getByTestId("live-kpis")).toContainText("/ 29");35 await expect(page.getByTestId("live-tiles").locator('a[data-online="true"]').first()).toBeVisible();36 await expect(page.getByTestId("history-chart")).toHaveAttribute("data-state", "ready");37 await page.goto("/en/nodes/node-01");38 await expect(page.getByTestId("node-live")).not.toHaveAttribute("data-state", "loading");39 } else {40 await expect(page.getByTestId("live-unavailable")).toBeVisible();41 }42});4344test("pages never scroll sideways on a phone", async ({ page }) => {45 for (const route of ["/en", "/en/live", "/en/nodes", "/en/nodes/node-01", "/fr/architecture"]) {46 await page.goto(route);47 const overflow = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth);48 expect(overflow, route).toBeLessThanOrEqual(0);49 }50});5152test("root redirects by browser language", async ({ browser }) => {53 for (const [lang, expected] of [["fr-CA", "/fr"], ["en-US", "/en"]] as const) {54 const ctx = await browser.newContext({ locale: lang });55 const page = await ctx.newPage();56 await page.goto("/");57 await expect(page).toHaveURL(new RegExp(`${expected}$`));58 await ctx.close();59 }60});6162test("language switch keeps the current page", async ({ page }) => {63 await page.goto("/en/nodes/node-03");64 await page.locator('header a[hreflang="fr"]').click();65 await expect(page).toHaveURL(/\/fr\/nodes\/node-03$/);66 await expect(page.locator("html")).toHaveAttribute("lang", "fr-CA");67});6869test("unknown page returns 404", async ({ page }) => {70 const res = await page.goto("/en/does-not-exist");71 expect(res?.status()).toBe(404);72});7374test("home shows computed metrics and a real photograph", async ({ page }) => {75 await page.goto("/en");76 await expect(page.getByTestId("metrics")).toContainText("29");77 await expect(page.getByTestId("metrics")).toContainText("1,068");78 await expect(page.getByTestId("metrics")).toContainText("1 GbE");79 const hero = page.locator("main img").first();80 await expect(hero).toHaveAttribute("alt", /Mac Studio/);81 expect(await hero.evaluate((img: HTMLImageElement) => img.complete && img.naturalWidth > 0)).toBe(true);82});8384test("topology blocks are operable from the keyboard", async ({ page }) => {85 await page.goto("/en");86 const gateway = page.getByRole("button", { name: "Gateway node" });87 await gateway.focus();88 await expect(gateway).toHaveAttribute("aria-pressed", "true");89 await expect(page.getByRole("heading", { name: "Gateway node" })).toBeVisible();90});9192test("inventory filters, sorts and keeps its state in the URL", async ({ page }) => {93 await page.goto("/en/nodes");94 await expect(page.getByTestId("result-count")).toHaveText("29 of 29 machines");95 await page.getByLabel("Family").selectOption("Mac mini");96 await expect(page).toHaveURL(/family=Mac(\+|%20)mini/);97 await expect(page.getByTestId("result-count")).toHaveText("13 of 29 machines");98 await page.getByRole("button", { name: "Table" }).click();99 await expect(page).toHaveURL(/view=table/);100 await expect(page.getByTestId("node-table").locator("tbody tr")).toHaveCount(13);101 await page.getByTestId("node-table").getByRole("button", { name: "Memory" }).click();102 await page.getByTestId("node-table").getByRole("button", { name: "Memory" }).click();103 await expect(page).toHaveURL(/sort=memory/);104 await expect(page).toHaveURL(/dir=desc/);105 await expect(page.getByTestId("node-table").locator("tbody tr").first()).toContainText("48 GB");106 await page.reload();107 await expect(page.getByTestId("node-table").locator("tbody tr")).toHaveCount(13);108 await page.getByRole("button", { name: "Reset filters" }).click();109 await expect(page.getByTestId("result-count")).toHaveText("29 of 29 machines");110});111112test("a deep link restores a filtered view", async ({ page }) => {113 await page.goto("/fr/nodes?memory=64&family=Mac+Studio");114 await expect(page.getByTestId("result-count")).toHaveText("6 machines sur 29");115});116117test("node record shows measured facts and marks missing ones", async ({ page }) => {118 await page.goto("/en/nodes/node-01");119 await expect(page.locator("h1")).toHaveText("M3U96a");120 await expect(page.getByText("20P + 8E")).toBeVisible();121 await page.goto("/en/nodes/node-09");122 await expect(page.getByText("Offline").first()).toBeVisible();123 await expect(page.getByText("Not yet documented").first()).toBeVisible();124});125126test("benchmarks page shows an honest empty state", async ({ page }) => {127 await page.goto("/en/benchmarks");128 await expect(page.getByTestId("benchmarks-empty")).toContainText("No benchmark observation has been published yet");129 await expect(page.getByTestId("benchmarks-results")).toHaveCount(0);130});131132test("gallery shows the four photographs and opens a lightbox", async ({ page }) => {133 await page.goto("/en/gallery");134 await expect(page.locator("main figure img")).toHaveCount(4);135 await page.getByRole("button", { name: /Enlarge photograph/ }).first().click();136 const dialog = page.locator("dialog[open]");137 await expect(dialog).toBeVisible();138 await expect(dialog).toContainText("1 of 4");139 await page.keyboard.press("ArrowRight");140 await expect(dialog).toContainText("2 of 4");141 await page.keyboard.press("Escape");142 await expect(page.locator("dialog[open]")).toHaveCount(0);143});144145test("access form validates, then prepares an email without claiming it was sent", async ({ page }) => {146 await page.goto("/en/access");147 await expect(page.getByTestId("access-form")).toHaveAttribute("data-mode", "mailto");148 await expect(page.getByTestId("access-mode")).toContainText("does not send anything");149 await page.getByRole("button", { name: "Prepare the email" }).click();150 await expect(page.getByTestId("access-error")).toBeVisible();151 await expect(page.getByLabel("Name")).toHaveAttribute("aria-invalid", "true");152 await page.getByLabel("Name").fill("Ada Lovelace");153 await page.getByLabel("Email").fill("ada@example.org");154 await page.getByLabel("Project title").fill("Reproducing a published result");155 await page.getByLabel("Research question").fill("Does the published speed-up hold on a different chip generation?");156 await page.getByLabel("Compute requirements").fill("One node with 64 GB, MLX, two days");157 await page.getByRole("button", { name: "Prepare the email" }).click();158 await expect(page.getByTestId("access-mail-ready")).toContainText("has not been sent");159 await expect(page.locator("#mail-body")).toHaveValue(/Ada Lovelace/);160 await expect(page.getByTestId("access-sent")).toHaveCount(0);161});162163test("api: health is minimal and the mail endpoint refuses when no provider is configured", async ({ request }) => {164 const health = await request.get("/api/health");165 expect(await health.json()).toEqual({ status: "ok", records: 29 });166 const post = await request.post("/api/access", { data: { name: "x" } });167 expect(post.status()).toBe(501);168});169170test("security headers, robots and sitemap", async ({ request }) => {171 const res = await request.get("/en");172 expect(res.headers()["content-security-policy"]).toContain("frame-ancestors 'none'");173 expect(res.headers()["x-content-type-options"]).toBe("nosniff");174 expect(await (await request.get("/robots.txt")).text()).toContain("Sitemap:");175 const sitemap = await (await request.get("/sitemap.xml")).text();176 expect(sitemap).toContain("/fr/nodes/node-29");177});178