SPB Git

Repository Public MIT

examples.spec.ts 8.9 KB 132 lines typescript

1import AxeBuilder from "@axe-core/playwright";2import { expect, test, type APIRequestContext } from "@playwright/test";3import { readFileSync } from "node:fs";4import { MANIFEST_FILE } from "@skills/registry-core";5import { packFiles } from "@skills/registry-core/node";6import { makePng } from "../media";78const state = JSON.parse(readFileSync("test-results/e2e-state.json", "utf8")) as { namespace: string; token?: string; readonly?: boolean };9test.skip(!!state.readonly || !state.token, "publishes a fixture package; needs a disposable publisher");1011const SLUG = "examples-demo";12const PROMPT = "Use the examples-demo skill to greet me, then <b>show</b> what you did.";13const examples = [14  { title: "Greeting in Claude Code", client: "claude-code", image: "examples/claude-code.png", alt: "Fixture image: a dark terminal with lines of text", prompt: PROMPT, caption: "Fixture screenshot generated by the test suite.", capturedAt: "2026-11-10" },15  { title: "Second run in Claude Code", client: "claude-code", image: "examples/claude-code-2.png", alt: "Fixture image: a second dark terminal" },16  { title: "Same task in Codex", client: "codex", image: "examples/codex.png", alt: "Fixture image: a wide dark terminal", prompt: "Greet me with the examples-demo skill." },17];1819async function ensurePublished(request: APIRequestContext) {20  if ((await request.get(`/api/v1/artifacts/${state.namespace}/${SLUG}`)).status() === 200) return;21  const auth = { authorization: `Bearer ${state.token}` };22  const manifest = { schemaVersion: "1.0", name: `@${state.namespace}/${SLUG}`, version: "1.0.0", type: "skill", description: "Fixture skill carrying usage-example screenshots for the end-to-end suite.", license: "MIT", compatibility: ["claude-code", "codex"], examples };23  const { archive, sha256 } = packFiles([24    { path: MANIFEST_FILE, mode: 0o644, data: Buffer.from(JSON.stringify(manifest, null, 2)) },25    { path: "SKILL.md", mode: 0o644, data: Buffer.from(`---\nname: ${SLUG}\ndescription: Says hello. Use when the user asks for a greeting.\nlicense: MIT\n---\n\n# Hello\n\nReply with a friendly greeting.\n`) },26    { path: "README.md", mode: 0o644, data: Buffer.from(`# ${SLUG}\n\nFixture skill with screenshots.\n\n\`\`\`sh\necho hello\n\`\`\`\n`) },27    { path: "LICENSE", mode: 0o644, data: Buffer.from("MIT License\n") },28    { path: "examples/claude-code.png", mode: 0o644, data: makePng(1280, 800, 1) },29    { path: "examples/claude-code-2.png", mode: 0o644, data: makePng(900, 1400, 2) },30    { path: "examples/codex.png", mode: 0o644, data: makePng(1600, 700, 3) },31  ]);32  const created = await request.post("/api/v1/artifacts", { headers: auth, data: { namespace: state.namespace, slug: SLUG, type: "skill", summary: manifest.description, visibility: "public", confirmPublic: true } });33  expect(created.status()).toBe(201);34  const begin = await request.post(`/api/v1/artifacts/${(await created.json()).id}/releases`, { headers: auth, data: { version: "1.0.0", sha256, sizeBytes: archive.length } });35  expect(begin.status()).toBe(201);36  const { uploadId } = await begin.json();37  expect((await request.put(`/api/v1/uploads/${uploadId}`, { headers: { ...auth, "content-type": "application/gzip" }, data: archive })).status()).toBe(200);38  expect((await request.post(`/api/v1/uploads/${uploadId}/finalize`, { headers: auth, data: { sha256 } })).status()).toBe(201);39}4041test.describe("usage examples on the artifact page @mobile", () => {42  test.beforeEach(async ({ request }) => ensurePublished(request));43  const url = `/@${state.namespace}/${SLUG}`;4445  test("shows the screenshots grouped by client, between Install and Documentation", async ({ page, request }) => {46    const errors: string[] = [];47    page.on("console", (m) => m.type() === "error" && errors.push(m.text()));48    page.on("pageerror", (e) => errors.push(e.message));49    await page.goto(url);50    const section = page.locator("#examples");51    await expect(section.getByRole("heading", { name: "Examples", exact: true })).toBeVisible();52    await expect(page.getByRole("navigation", { name: "On this page" }).getByRole("link", { name: "Examples (3)" })).toBeVisible();53    await expect(page.getByTestId("quick-facts")).toContainText("Examples");54    await expect(section.locator('[data-client="claude-code"]')).toContainText("Claude Code");55    await expect(section.locator('[data-client="claude-code"] img')).toHaveCount(2);56    await expect(section.locator('[data-client="codex"] img')).toHaveCount(1);57    await expect(section).toContainText("Screenshots supplied by the publisher. The registry serves them from the release archive and does not verify what they show.");58    // Order in the document: Install, Examples, Documentation.59    const order = await page.evaluate(() => ["install", "examples", "readme"].map((id) => document.getElementById(id)!.getBoundingClientRect().top));60    expect(order[0]!).toBeLessThan(order[1]!);61    expect(order[1]!).toBeLessThan(order[2]!);6263    // The image is same-origin, really loads, and reserves its space from the header-declared size.64    const img = section.locator("img").first();65    await img.scrollIntoViewIfNeeded();66    await expect.poll(() => img.evaluate((el: HTMLImageElement) => el.complete && el.naturalWidth)).toBe(1280);67    expect(await img.getAttribute("src")).toBe(`/api/v1/artifacts/${state.namespace}/${SLUG}/versions/1.0.0/media/examples/claude-code.png`);68    expect(await img.getAttribute("alt")).toBe(examples[0]!.alt);69    expect(await img.getAttribute("width")).toBe("1280");70    expect(await img.getAttribute("loading")).toBe("lazy");71    const res = await request.get((await img.getAttribute("src"))!);72    expect(res.headers()["content-type"]).toBe("image/png");73    expect(res.headers()["x-content-type-options"]).toBe("nosniff");7475    // The prompt is plain text with a copy button; markup in it is shown, not rendered.76    await expect(section.locator(".code-frame").first()).toContainText(PROMPT);77    expect(await section.locator(".code-frame b").count()).toBe(0);78    await expect(section.locator(".code-frame").first().getByRole("button", { name: /copy/i })).toBeVisible();79    expect(await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth)).toBeLessThanOrEqual(1);80    expect(errors).toEqual([]);81  });8283  test("opens full size in a dialog with the keyboard and returns focus on Escape", async ({ page }) => {84    await page.goto(url);85    const opener = page.getByTestId("example-open").first();86    await opener.focus();87    await page.keyboard.press("Enter");88    const dialog = page.getByTestId("example-dialog");89    await expect(dialog).toBeVisible();90    await expect(dialog).toHaveAttribute("open", "");91    await expect(dialog.locator("img")).toHaveAttribute("alt", examples[0]!.alt);92    await expect(dialog).toContainText("Greeting in Claude Code");93    await expect(dialog.getByRole("button", { name: "Close full-size view" })).toBeFocused();94    const box = await dialog.boundingBox();95    const vp = page.viewportSize()!;96    expect(box!.width).toBeLessThanOrEqual(vp.width);97    expect(box!.height).toBeLessThanOrEqual(vp.height);98    await page.keyboard.press("Escape");99    await expect(dialog).toBeHidden();100    await expect(opener).toBeFocused();101    // Mouse path: open the Codex one, close with the button.102    await page.getByTestId("example-open").nth(2).click();103    await expect(dialog).toContainText("Same task in Codex");104    await dialog.getByRole("button", { name: "Close full-size view" }).click();105    await expect(dialog).toBeHidden();106  });107108  test("lists image files in the file browser without previewing their bytes", async ({ page }) => {109    await page.goto(url);110    const files = page.getByRole("list", { name: "Files in this release" });111    await files.getByRole("button", { name: /examples\/codex\.png/ }).click();112    await expect(page.getByTestId("binary-file")).toContainText("Binary file");113    await expect(page.locator("#files pre")).toHaveCount(0);114    await files.getByRole("button", { name: /SKILL\.md/ }).click();115    await expect(page.locator("#files pre")).toContainText("Reply with a friendly greeting");116  });117118  for (const scheme of ["light", "dark"] as const) {119    test(`${scheme} theme: examples section and open dialog pass WCAG AA`, async ({ page }) => {120      await page.emulateMedia({ colorScheme: scheme });121      await page.goto(url);122      await expect(page.locator("html")).toHaveAttribute("data-theme", scheme);123      const closed = await new AxeBuilder({ page }).withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"]).analyze();124      expect(closed.violations.map((v) => `${v.id}: ${v.nodes.length} × ${v.nodes[0]?.target}`)).toEqual([]);125      await page.getByTestId("example-open").first().click();126      await expect(page.getByTestId("example-dialog")).toBeVisible();127      const open = await new AxeBuilder({ page }).include('[data-testid="example-dialog"]').withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"]).analyze();128      expect(open.violations.map((v) => `${v.id}: ${v.nodes.length} × ${v.nodes[0]?.target}`)).toEqual([]);129    });130  }131});132
Files main
Menu

Theme