T46 E2E: Playwright walks the built site, desktop and phone, live stories included
This commit is contained in:
6 files changed
+203
-1
No files matched your search
@@ -2,3 +2,5 @@ node_modules
|
||||
dist
|
||||
public/gallery
|
||||
.wrangler
|
||||
test-results
|
||||
playwright-report
|
||||
@@ -0,0 +1,70 @@
|
||||
import { expect, test, type Page } from "@playwright/test";
|
||||
import manifest from "../../examples/gallery/stories.json" with { type: "json" };
|
||||
|
||||
const STARTS = 45_000;
|
||||
|
||||
async function live(page: Page, path: string) {
|
||||
await page.goto(path);
|
||||
const frame = page.frameLocator(".stage iframe");
|
||||
// The host hides its state once the gallery says it is ready.
|
||||
await expect(page.locator(".stage .state")).toBeHidden({ timeout: STARTS });
|
||||
return frame;
|
||||
}
|
||||
|
||||
test("a story runs live and follows the site's mode", async ({ page }) => {
|
||||
await page.emulateMedia({ colorScheme: "light" });
|
||||
const frame = await live(page, "/components/buttons/button/");
|
||||
await expect(frame.locator("canvas")).toBeVisible();
|
||||
await expect(frame.locator("html")).not.toHaveClass(/dark/);
|
||||
await page.locator("[data-mode]").click();
|
||||
await expect(frame.locator("html")).toHaveClass(/dark/);
|
||||
});
|
||||
|
||||
test("the live story leaves focus with the host until it is pressed", async ({ page, isMobile }) => {
|
||||
test.skip(isMobile, "no hardware keyboard");
|
||||
await live(page, "/components/forms/input-textfield-clearableinput/");
|
||||
await page.waitForTimeout(1000);
|
||||
expect(await page.evaluate(() => document.activeElement?.tagName)).not.toBe("IFRAME");
|
||||
await page.keyboard.press("/");
|
||||
await expect(page.getByRole("dialog", { name: "Search components" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("a skip link passes the live example", async ({ page, isMobile }) => {
|
||||
test.skip(isMobile, "no hardware keyboard");
|
||||
await page.goto("/components/buttons/button/");
|
||||
const skip = page.getByRole("link", { name: "Skip the live example" });
|
||||
await skip.focus();
|
||||
await expect(skip).toBeVisible();
|
||||
await page.keyboard.press("Enter");
|
||||
await expect(page.locator("#after-stage")).toBeFocused();
|
||||
});
|
||||
|
||||
test("typing reaches a live field after a press", async ({ page, isMobile }) => {
|
||||
test.skip(isMobile, "no hardware keyboard");
|
||||
const frame = await live(page, "/components/forms/input-textfield-clearableinput/");
|
||||
const box = (await page.locator(".stage iframe").boundingBox())!;
|
||||
// The first field sits under the section's note, as the gallery lays it.
|
||||
await page.mouse.click(box.x + 120, box.y + 72);
|
||||
const before = await frame.locator("canvas").screenshot();
|
||||
await page.keyboard.type("Ada");
|
||||
await expect(frame.locator("textarea")).toBeFocused();
|
||||
await expect.poll(async () => (await frame.locator("canvas").screenshot()).equals(before)).toBe(false);
|
||||
});
|
||||
|
||||
test("full screen covers the page and Escape leaves it", async ({ page }) => {
|
||||
await live(page, "/components/buttons/button/");
|
||||
await page.locator(".stage").hover();
|
||||
await page.getByRole("button", { name: "Full screen" }).click();
|
||||
await expect(page.locator(".stage")).toHaveClass(/full/);
|
||||
await page.keyboard.press("Escape");
|
||||
await expect(page.locator(".stage")).not.toHaveClass(/full/);
|
||||
});
|
||||
|
||||
// One story from every fifth chapter, each drawn in its frame.
|
||||
for (const chapter of manifest.pages.filter((_, i) => i % 5 === 0)) {
|
||||
test(`the first ${chapter.title} story draws`, async ({ page }) => {
|
||||
const frame = await live(page, `/components/${chapter.slug}/`);
|
||||
const shot = await frame.locator("canvas").screenshot();
|
||||
expect(shot.length).toBeGreaterThan(4000);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import { expect, test, type Page } from "@playwright/test";
|
||||
import manifest from "../../examples/gallery/stories.json" with { type: "json" };
|
||||
|
||||
/** Fails the test on any console error or uncaught exception. */
|
||||
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 home page shows the pitch, the scene and the credits", async ({ page }) => {
|
||||
const errors = watch(page);
|
||||
await page.goto("/");
|
||||
await expect(page.getByRole("heading", { level: 1 })).toContainText("component library for GPUI");
|
||||
await expect(page.locator(".hero canvas")).toBeVisible();
|
||||
await expect(page.locator("#foot")).toContainText("Solar Icons by 480 Design");
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
|
||||
test("the mode toggles, and a reload keeps it", async ({ page }) => {
|
||||
await page.emulateMedia({ colorScheme: "light" });
|
||||
await page.goto("/components/");
|
||||
const html = page.locator("html");
|
||||
await expect(html).not.toHaveClass(/dark/);
|
||||
await page.getByRole("button", { name: "Dark mode" }).click();
|
||||
await expect(html).toHaveClass(/dark/);
|
||||
await page.reload();
|
||||
await expect(html).toHaveClass(/dark/);
|
||||
});
|
||||
|
||||
test("the index lists every chapter with its story count", async ({ page }) => {
|
||||
await page.goto("/components/");
|
||||
const rows = page.locator(".grid a");
|
||||
await expect(rows).toHaveCount(manifest.pages.length);
|
||||
const first = manifest.pages[0];
|
||||
await expect(rows.first()).toContainText(`${first.stories.length} stories`);
|
||||
});
|
||||
|
||||
test("a chapter opens its first story, and paging walks on", async ({ page }) => {
|
||||
const [chapter] = manifest.pages;
|
||||
await page.goto(`/components/${chapter.slug}/`);
|
||||
await expect(page.getByRole("heading", { level: 1 })).toHaveText(chapter.stories[0].title);
|
||||
await page.locator(".pager a.next").click();
|
||||
await expect(page).toHaveURL(`/components/${chapter.slug}/${chapter.stories[1].slug}/`);
|
||||
await expect(page.getByRole("heading", { level: 1 })).toHaveText(chapter.stories[1].title);
|
||||
});
|
||||
|
||||
test("the sidebar filters by story and component name", async ({ page }) => {
|
||||
await page.goto("/components/");
|
||||
await page.getByPlaceholder("Filter components").fill("Toast");
|
||||
await expect(page.locator(".side li a").first()).toContainText("Toast");
|
||||
await page.getByPlaceholder("Filter components").fill("zzzz");
|
||||
await expect(page.locator(".side .none")).toContainText("Nothing matches");
|
||||
});
|
||||
|
||||
test("search opens with the keyboard and goes where Enter points", async ({ page, isMobile }) => {
|
||||
test.skip(isMobile, "no hardware keyboard");
|
||||
await page.goto("/components/");
|
||||
await page.keyboard.press("/");
|
||||
const dialog = page.getByRole("dialog", { name: "Search components" });
|
||||
await expect(dialog).toBeVisible();
|
||||
await page.keyboard.type("breadcrumb");
|
||||
await page.keyboard.press("Enter");
|
||||
await expect(dialog).toBeHidden();
|
||||
await expect(page).toHaveURL(/\/components\/navigation\/.*breadcrumb/);
|
||||
});
|
||||
|
||||
test("the Browse menu opens a chapter's stories beside it", async ({ page, isMobile }) => {
|
||||
test.skip(isMobile, "hover menu");
|
||||
await page.goto("/components/");
|
||||
await page.getByRole("button", { name: "Browse" }).click();
|
||||
await page.getByRole("menuitem", { name: "Charts" }).hover();
|
||||
const story = page.getByRole("menuitem", { name: /^RadarChart/ });
|
||||
await expect(story).toBeVisible();
|
||||
await story.click();
|
||||
await expect(page).toHaveURL(/\/components\/charts\/radarchart/);
|
||||
});
|
||||
|
||||
test("an unknown chapter says so", async ({ page }) => {
|
||||
const errors = watch(page);
|
||||
await page.goto("/components/nowhere/");
|
||||
await expect(page.getByRole("heading", { level: 1 })).toHaveText("Not found");
|
||||
expect(errors.join()).toContain("No chapter");
|
||||
});
|
||||
@@ -7,9 +7,11 @@
|
||||
"dev": "vite",
|
||||
"build": "tsc --noEmit && vite build",
|
||||
"preview": "vite preview",
|
||||
"deploy": "wrangler deploy"
|
||||
"deploy": "wrangler deploy",
|
||||
"e2e": "playwright test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.63.0",
|
||||
"@types/three": "0.186.0",
|
||||
"typescript": "^7.0.2",
|
||||
"vite": "8.3.1",
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
// Runs against the built site; the gallery must be built for this origin.
|
||||
export default defineConfig({
|
||||
testDir: "e2e",
|
||||
timeout: 60_000,
|
||||
fullyParallel: true,
|
||||
reporter: [["list"]],
|
||||
use: { baseURL: "http://localhost:4173", trace: "retain-on-failure" },
|
||||
webServer: { command: "pnpm preview --port 4173 --strictPort", url: "http://localhost:4173", reuseExistingServer: true },
|
||||
projects: [
|
||||
{ name: "desktop", use: { ...devices["Desktop Chrome"] } },
|
||||
{ name: "phone", use: { ...devices["Pixel 7"] } },
|
||||
],
|
||||
});
|
||||
Generated
+28
@@ -24,6 +24,9 @@ importers:
|
||||
specifier: 0.186.1
|
||||
version: 0.186.1
|
||||
devDependencies:
|
||||
'@playwright/test':
|
||||
specifier: 1.63.0
|
||||
version: 1.63.0
|
||||
'@types/three':
|
||||
specifier: 0.186.0
|
||||
version: 0.186.0
|
||||
@@ -429,6 +432,11 @@ packages:
|
||||
'@oxc-project/types@0.151.0':
|
||||
resolution: {integrity: sha512-J1yXrIlNDZVzE3ada310xeAw7nH8yCAyLPuUIsjKatFPmfn5bS1oW+cM+QsGOtVWd5nhSpbwZWx/rue+r5Z+PA==}
|
||||
|
||||
'@playwright/test@1.63.0':
|
||||
resolution: {integrity: sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==}
|
||||
engines: {node: '>=20'}
|
||||
hasBin: true
|
||||
|
||||
'@poppinss/colors@4.1.6':
|
||||
resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==}
|
||||
|
||||
@@ -829,6 +837,16 @@ packages:
|
||||
resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
playwright-core@1.63.0:
|
||||
resolution: {integrity: sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==}
|
||||
engines: {node: '>=20'}
|
||||
hasBin: true
|
||||
|
||||
playwright@1.63.0:
|
||||
resolution: {integrity: sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==}
|
||||
engines: {node: '>=20'}
|
||||
hasBin: true
|
||||
|
||||
postcss@8.5.28:
|
||||
resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
@@ -1196,6 +1214,10 @@ snapshots:
|
||||
|
||||
'@oxc-project/types@0.151.0': {}
|
||||
|
||||
'@playwright/test@1.63.0':
|
||||
dependencies:
|
||||
playwright: 1.63.0
|
||||
|
||||
'@poppinss/colors@4.1.6':
|
||||
dependencies:
|
||||
kleur: 4.1.5
|
||||
@@ -1458,6 +1480,12 @@ snapshots:
|
||||
|
||||
picomatch@4.0.7: {}
|
||||
|
||||
playwright-core@1.63.0: {}
|
||||
|
||||
playwright@1.63.0:
|
||||
dependencies:
|
||||
playwright-core: 1.63.0
|
||||
|
||||
postcss@8.5.28:
|
||||
dependencies:
|
||||
nanoid: 3.3.19
|
||||
|
||||
Reference in new issue
Block a user