SPB Git

Repository Public

site.spec.ts 9.3 KB 178 lines typescript

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
Files main
Menu

Theme