SPB Git

Repository Public MIT

public.spec.ts 10.3 KB 187 lines typescript

1import AxeBuilder from "@axe-core/playwright";2import { expect, test, type Page } from "@playwright/test";3import { createHash } from "node:crypto";45const NS = "spboucher";67function watch(page: Page): string[] {8  const errors: string[] = [];9  page.on("console", (m) => m.type() === "error" && errors.push(m.text()));10  page.on("pageerror", (e) => errors.push(e.message));11  return errors;12}13async function noOverflow(page: Page) {14  expect(await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth)).toBeLessThanOrEqual(1);15}1617test.describe("visitor, no account @public", () => {18  test("home shows live counts, types and recent artifacts without script or CSP errors", async ({ page, request }) => {19    const errors = watch(page);20    const res = await page.goto("/");21    const csp = res!.headers()["content-security-policy"]!;22    expect(csp).toMatch(/script-src 'self' 'nonce-[A-Za-z0-9+/=]+' 'strict-dynamic'/);23    expect(csp).not.toContain("'unsafe-inline' 'unsafe-eval'");24    expect(csp).toContain("frame-ancestors 'none'");25    expect(res!.headers()["x-content-type-options"]).toBe("nosniff");26    await expect(page.getByRole("heading", { level: 1 })).toContainText("Discover. Build. Publish.");27    const api = await (await request.get("/api/v1/categories")).json();28    await expect(page.getByTestId("count-public artifact")).toHaveText(String(api.totals.artifacts));29    await expect(page.locator('main a[href^="/@"]').first()).toBeVisible(); // whichever artifacts are most recent30    await noOverflow(page);31    expect(errors).toEqual([]);32  });3334  test("search palette opens with / and navigates by keyboard", async ({ page, isMobile }) => {35    test.skip(!!isMobile, "keyboard shortcut is a desktop affordance; mobile uses the search button");36    await page.goto("/");37    await page.keyboard.press("/");38    const box = page.getByRole("combobox");39    await expect(box).toBeFocused();40    await box.fill("citation-check");41    await expect(page.getByRole("option").first()).toContainText("@spboucher/citation-check");42    await page.keyboard.press("Enter");43    await expect(page).toHaveURL(/\/@spboucher\/citation-check$/);44  });4546  test("explore filters by type and client, and has an honest empty state", async ({ page }) => {47    await page.goto("/explore");48    await page.getByLabel("Type").selectOption("mcp-server");49    await page.getByRole("button", { name: "Apply" }).click();50    await expect(page).toHaveURL(/type=mcp-server/);51    await expect(page.getByRole("link", { name: /echo-mcp-server/ })).toBeVisible();52    await expect(page.getByRole("link", { name: /hello-skill/ })).toHaveCount(0);53    await page.goto("/explore?q=zzzz-no-such-thing-qqq");54    await expect(page.getByText("No artifact matches this search.")).toBeVisible();55    await page.goto("/skills?compatibility=codex");56    await expect(page.getByRole("link", { name: /hello-skill/ })).toBeVisible();57    await noOverflow(page);58  });5960  test("artifact page: metadata, files, install tabs and a download that matches the published checksum", async ({ page, request }) => {61    const errors = watch(page);62    await page.goto(`/@${NS}/hello-skill`);63    await expect(page.getByRole("heading", { level: 1 })).toContainText("hello-skill");64    const shown = (await page.getByTestId("sha256").first().innerText()).trim();65    expect(shown).toMatch(/^[0-9a-f]{64}$/);6667    const meta = await (await request.get(`/api/v1/artifacts/${NS}/hello-skill/versions/latest`)).json();68    const dl = await request.get(`/api/v1/artifacts/${NS}/hello-skill/versions/${meta.version}/download`);69    expect(dl.status()).toBe(200);70    expect(createHash("sha256").update(await dl.body()).digest("hex")).toBe(shown);71    expect(dl.headers()["x-checksum-sha256"]).toBe(shown);7273    await expect(page.getByRole("tabpanel")).toContainText("~/.claude/skills/hello-skill");74    await page.getByRole("tab", { name: "OpenAI Codex" }).click();75    await expect(page.getByRole("tabpanel")).toContainText("~/.agents/skills/hello-skill");76    await expect(page.getByRole("tabpanel")).toContainText("shasum -a 256 -c");7778    await page.getByRole("button", { name: /SKILL\.md/ }).click();79    await expect(page.locator("#files pre")).toContainText("name: hello-skill");80    await expect(page.getByText("Static checks are advisory").first()).toBeVisible();81    await expect(page.locator("#history")).toContainText("release.publish");82    await expect(page.getByText("not provided").first()).toBeVisible(); // missing metadata is shown honestly83    await noOverflow(page);84    expect(errors).toEqual([]);85  });8687  test("MCP listing explains local execution and gives per-client configuration", async ({ page }) => {88    await page.goto(`/@${NS}/echo-mcp-server`);89    await expect(page.getByRole("tabpanel")).toContainText("claude mcp add --transport stdio echo-mcp-server -- node server.mjs");90    await expect(page.getByRole("tabpanel")).toContainText("runs locally on your machine");91    await page.getByRole("tab", { name: "Cursor" }).click();92    await expect(page.getByRole("tabpanel")).toContainText(".cursor/mcp.json");93    const html = await page.request.get("/mcp", { headers: { accept: "text/html" } });94    expect(html.status()).toBe(200);95    expect(await html.text()).toContain("MCP Servers");96  });9798  test("version page, publisher profile, collections, docs and legal pages render", async ({ page }) => {99    await page.goto(`/@${NS}/hello-skill/versions/1.0.0`);100    await expect(page.getByRole("heading", { level: 1 })).toContainText("v1.0.0");101    await page.goto(`/@${NS}`);102    await expect(page.getByRole("heading", { level: 1 })).toHaveText(`@${NS}`);103    await page.goto("/collections/academic-research");104    await expect(page.getByRole("heading", { level: 1 })).toHaveText("Academic Research");105    for (const [url, text] of [["/docs", "Getting started"], ["/docs/cli", "skills publish"], ["/docs/mcp", "search_artifacts"], ["/docs/security", "advisory"], ["/legal/privacy", "Privacy"], ["/legal/terms", "Terms"], ["/legal/acceptable-use", "Acceptable"], ["/about", "About"], ["/contact", "contact@spboucher.ai"], ["/publish", "Immutable releases"]] as const) {106      const r = await page.goto(url);107      expect(r!.status(), url).toBe(200);108      await expect(page.locator("main"), url).toContainText(text);109      await noOverflow(page);110    }111  });112113  test("unknown and private addresses return a real 404", async ({ page }) => {114    const r = await page.goto("/@spboucher/does-not-exist");115    expect(r!.status()).toBe(404);116    await expect(page.getByRole("heading", { level: 1 })).toHaveText("This page does not exist.");117    expect((await page.request.get("/dashboard", { maxRedirects: 0 })).status()).toBe(307);118  });119120  test("abuse report can be filed without an account", async ({ page }) => {121    test.skip(process.env.E2E_READONLY === "1", "mutating");122    await page.goto(`/report?artifact=@${NS}/hello-skill`);123    await page.getByLabel("Category").selectOption("broken");124    await page.getByLabel("What is wrong?").fill("End-to-end test report: the example does not greet in French.");125    await page.getByRole("button", { name: "Send report" }).click();126    await expect(page.getByRole("status")).toContainText("was recorded");127  });128129  test("mobile navigation menu works @mobile", async ({ page, isMobile }) => {130    test.skip(!isMobile, "mobile only");131    await page.goto("/");132    await page.getByLabel("Menu").click();133    await page.getByRole("navigation", { name: "Mobile" }).getByRole("link", { name: "Docs" }).click();134    await expect(page).toHaveURL(/\/docs$/);135    await page.getByText("Documentation menu").click();136    await expect(page.getByRole("navigation", { name: "Documentation (mobile)" })).toBeVisible();137  });138139  for (const url of ["/", "/explore", `/@${NS}/citation-check`, "/docs/publishing", "/publish", "/login", "/report", "/collections"]) {140    test(`accessibility (WCAG 2.1 A/AA) ${url}`, async ({ page }) => {141      await page.goto(url);142      const results = await new AxeBuilder({ page }).withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"]).analyze();143      expect(results.violations.map((v) => `${v.id}: ${v.nodes.length} × ${v.nodes[0]?.target}`)).toEqual([]);144    });145  }146});147148test.describe("themes and presentation @public", () => {149  test("theme toggle switches light and dark, persists, and paints before hydration", async ({ page }) => {150    await page.emulateMedia({ colorScheme: "light" });151    await page.goto("/");152    const html = page.locator("html");153    await expect(html).toHaveAttribute("data-theme", "light");154    const toggle = page.getByTestId("theme-toggle");155    await toggle.click(); // system -> light156    await toggle.click(); // light -> dark157    await expect(html).toHaveAttribute("data-theme", "dark");158    await page.reload();159    await expect(html).toHaveAttribute("data-theme", "dark");160    const bgDark = await page.evaluate(() => getComputedStyle(document.documentElement).getPropertyValue("--color-ink").trim());161    await page.getByTestId("theme-toggle").click(); // dark -> system (light here)162    await expect(html).toHaveAttribute("data-theme", "light");163    expect(await page.evaluate(() => getComputedStyle(document.documentElement).getPropertyValue("--color-ink").trim())).not.toBe(bgDark);164  });165166  for (const scheme of ["light", "dark"] as const) {167    test(`${scheme} theme passes WCAG AA contrast on home, listing and docs`, async ({ page }) => {168      await page.emulateMedia({ colorScheme: scheme });169      for (const path of ["/", `/@${NS}/echo-mcp-server`, "/docs/publishing", "/explore"]) {170        await page.goto(path);171        await expect(page.locator("html")).toHaveAttribute("data-theme", scheme);172        const results = await new AxeBuilder({ page }).withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"]).analyze();173        expect(results.violations.map((v) => `${path} ${v.id}: ${v.nodes.length} × ${v.nodes[0]?.target}`)).toEqual([]);174        await noOverflow(page);175      }176    });177  }178179  test("MCP listing shows quick facts, a tools table and copyable commands", async ({ page }) => {180    await page.goto(`/@${NS}/echo-mcp-server`);181    await expect(page.getByTestId("quick-facts")).toContainText("stdio");182    await expect(page.getByTestId("tools-table")).toContainText("echo");183    await expect(page.locator("#install").getByRole("button", { name: /copy/i }).first()).toBeVisible();184    await expect(page.locator("#readme .code-frame").first().getByRole("button", { name: /copy/i })).toBeVisible();185  });186});187
Files main
Menu

Theme