From 91e86edac775d7d8c01f734db7a5dc2d406beee1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Thu, 1 Oct 2026 13:41:18 -0400 Subject: [PATCH] =?UTF-8?q?T48m=20Docs=20=C2=B7=20E2E:=20the=20Docs=20link?= =?UTF-8?q?,=20every=20guide,=20the=20rail,=20previous=20and=20next,=20sec?= =?UTF-8?q?tion=20links,=20the=20compiled=20sample=20and=20Copy,=20an=20un?= =?UTF-8?q?known=20guide;=20the=20rail=20marks=20only=20the=20open=20guide?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/e2e/docs.spec.ts | 76 +++++++++++++++++++++++++++++++++++++ frontend/src/pages/Docs.tsx | 2 +- 2 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 frontend/e2e/docs.spec.ts diff --git a/frontend/e2e/docs.spec.ts b/frontend/e2e/docs.spec.ts new file mode 100644 index 0000000..36b1501 --- /dev/null +++ b/frontend/e2e/docs.spec.ts @@ -0,0 +1,76 @@ +import { readFileSync } from "node:fs"; +import { expect, test, type Page } from "@playwright/test"; + +/** The guides in reading order, as `src/docs/guides.ts` lists them. */ +const GUIDES = ["", "installation", "getting-started", "theme", "focus", "motion", "icons", "overlays", "i18n", "webassembly", "testing"]; + +const path = (slug: string) => (slug ? `/docs/${slug}/` : "/docs/"); + +/** Collects console errors and uncaught exceptions. */ +function watch(page: Page): string[] { + const errors: string[] = []; + page.on("console", (m) => m.type() === "error" && errors.push(m.text())); + page.on("pageerror", (e) => errors.push(e.message)); + return errors; +} + +test("the header's Docs link opens the introduction", async ({ page }) => { + await page.goto("/"); + await page.getByRole("link", { name: "Docs" }).click(); + await expect(page).toHaveURL("/docs/"); + await expect(page.getByRole("heading", { level: 1 })).toHaveText("Introduction"); +}); + +test("every guide loads with its title, list and no errors", async ({ page }) => { + const errors = watch(page); + for (const slug of GUIDES) { + await page.goto(path(slug)); + await expect(page.locator(".doc h1")).not.toBeEmpty(); + await expect(page.locator(".prose h2").first()).toBeVisible(); + } + expect(errors).toEqual([]); +}); + +test("the rail lists the guides in order and marks the open one", async ({ page }) => { + await page.goto("/docs/theme/"); + const links = page.locator(".docs-rail a"); + await expect(links).toHaveCount(GUIDES.length); + for (const [i, slug] of GUIDES.entries()) await expect(links.nth(i)).toHaveAttribute("href", path(slug)); + await expect(page.locator('.docs-rail a[aria-current="page"]')).toHaveText("Theme"); +}); + +test("previous and next walk the guides", async ({ page }) => { + await page.goto("/docs/"); + await expect(page.locator(".pager a")).toHaveCount(1); + await page.locator(".pager a.next").click(); + await expect(page).toHaveURL("/docs/installation/"); + await page.goto(path(GUIDES[GUIDES.length - 1])); + await expect(page.locator(".pager a.next")).toHaveCount(0); +}); + +test("an on-this-page link moves to its section", async ({ page, isMobile }) => { + test.skip(isMobile, "the list hides on narrow screens"); + await page.goto("/docs/installation/"); + await page.locator(".doc-toc a", { hasText: "When it fails" }).click(); + await expect(page).toHaveURL("/docs/installation/#when-it-fails"); + await expect(page.locator("#when-it-fails")).toBeInViewport(); +}); + +test("a code sample is the compiled example, and Copy copies it", async ({ page, context, browserName, isMobile }) => { + test.skip(browserName !== "chromium" || isMobile, "clipboard reads need Chromium's permission"); + await context.grantPermissions(["clipboard-read", "clipboard-write"]); + const file = readFileSync(new URL("../../examples/docs/hello.rs", import.meta.url), "utf8").trimEnd(); + await page.goto("/docs/"); + const block = page.locator(".code", { hasText: "examples/docs/hello.rs" }); + await expect(block.locator("code")).toHaveText(file, { useInnerText: true }); + await block.getByRole("button", { name: "Copy" }).click(); + await expect(block.getByRole("button", { name: "Copied" })).toBeVisible(); + expect(await page.evaluate(() => navigator.clipboard.readText())).toBe(file); +}); + +test("an unknown guide says so", async ({ page }) => { + const errors = watch(page); + await page.goto("/docs/nowhere/"); + await expect(page.getByRole("heading", { level: 1 })).toHaveText("Not found"); + expect(errors.join()).toContain("No guide at nowhere"); +}); diff --git a/frontend/src/pages/Docs.tsx b/frontend/src/pages/Docs.tsx index bec9f38..178095b 100644 --- a/frontend/src/pages/Docs.tsx +++ b/frontend/src/pages/Docs.tsx @@ -48,7 +48,7 @@ export function Docs() {
    {guides.map((g) => (
  1. - + {g.title}