From 5a0fc28fabaccf1c61e8f74b3fbeb4128ea48d61 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sun, 27 Sep 2026 12:01:12 -0700 Subject: [PATCH] test(e2e): add a Vite host and Playwright harness test:e2e packs the package into a throwaway Vite host built by e2e/helpers.ts and runs the Playwright specs in e2e/ against it, after the existing exports check. CI installs Chromium first. --- .github/workflows/ci.yml | 1 + .gitignore | 2 + CONTRIBUTING.md | 6 ++- bun.lock | 7 +++ e2e/fixtures.tsx | 7 +++ e2e/helpers.ts | 114 +++++++++++++++++++++++++++++++++++++++ e2e/smoke.spec.ts | 8 +++ e2e/teardown.ts | 7 +++ package.json | 3 +- playwright.config.ts | 17 ++++++ tsconfig.json | 11 +++- 11 files changed, 179 insertions(+), 4 deletions(-) create mode 100644 e2e/fixtures.tsx create mode 100644 e2e/helpers.ts create mode 100644 e2e/smoke.spec.ts create mode 100644 e2e/teardown.ts create mode 100644 playwright.config.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dc791ae7..bf5f197a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,6 +20,7 @@ jobs: - run: bun run build - run: bun run contrast-test - run: bun run dep-guard + - run: bunx playwright install --with-deps chromium - run: bun run test:e2e - name: node consumer smoke run: | diff --git a/.gitignore b/.gitignore index 2d2a0bbf..0ee26fc3 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ coverage/ .env.* .DS_Store build/ +test-results/ +playwright-report/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 20fa485e..73162046 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,8 +12,10 @@ bun run check Contributors sign the [CLA](CLA.md) on their first PR; the CLA bot explains how. `bun run test:e2e` checks the built package: every `exports` target exists and the -root entry loads under Node with every optional peer blocked. Run `bun run build` -first. CI also runs `bun run contrast-test` (theme contrast floors, reads +root entry loads under Node with every optional peer blocked, then packs the +package into a Vite host and runs the Playwright specs in `e2e/` against it. Each +scenario is a route in `e2e/fixtures.tsx`. Run `bun run build` first, and +`bunx playwright install chromium` once. CI also runs `bun run contrast-test` (theme contrast floors, reads `dist/styles.css`) and `bun run dep-guard` (forbidden imports, see `AGENTS.md`) after the build. diff --git a/bun.lock b/bun.lock index 6034c5c5..c627c396 100644 --- a/bun.lock +++ b/bun.lock @@ -12,6 +12,7 @@ "devDependencies": { "@happy-dom/global-registrator": "^20.11.2", "@ladle/react": "^5.1.1", + "@playwright/test": "^1.63.0", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-slot": "^1.2.3", @@ -300,6 +301,8 @@ "@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.1", "", { "os": "win32", "cpu": "x64" }, "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA=="], + "@playwright/test": ["@playwright/test@1.63.0", "", { "dependencies": { "playwright": "1.63.0" }, "bin": { "playwright": "cli.js" } }, "sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ=="], + "@radix-ui/primitive": ["@radix-ui/primitive@1.1.7", "", {}, "sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q=="], "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.15", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.10" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-v4zggRcjadnI+ClKDuijlQEW4tw3NoaeHc/PwpKnLoLLKNUG4InLegkstooLcRIUWCs+8L22dGURCVuFfOKfnA=="], @@ -1050,6 +1053,10 @@ "picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="], + "playwright": ["playwright@1.63.0", "", { "dependencies": { "playwright-core": "1.63.0" }, "bin": { "playwright": "cli.js" } }, "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg=="], + + "playwright-core": ["playwright-core@1.63.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg=="], + "postcss": ["postcss@8.5.26", "", { "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="], "prism-react-renderer": ["prism-react-renderer@2.4.1", "", { "dependencies": { "@types/prismjs": "^1.26.0", "clsx": "^2.0.0" }, "peerDependencies": { "react": ">=16.0.0" } }, "sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig=="], diff --git a/e2e/fixtures.tsx b/e2e/fixtures.tsx new file mode 100644 index 00000000..052d79f7 --- /dev/null +++ b/e2e/fixtures.tsx @@ -0,0 +1,7 @@ +import type { ReactNode } from "react"; + +import { Button } from "@corbits/react-ui"; + +export const scenarios: Record ReactNode> = { + smoke: () => , +}; diff --git a/e2e/helpers.ts b/e2e/helpers.ts new file mode 100644 index 00000000..95ba5f67 --- /dev/null +++ b/e2e/helpers.ts @@ -0,0 +1,114 @@ +// Bootstraps the Vite host the specs drive: packs this package, installs the +// tarball into a throwaway app in a temp directory (teardown.ts removes it), +// typechecks fixtures.tsx against the packed types and serves it on :4173. +import { execFileSync } from "node:child_process"; +import { copyFileSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const root = join(import.meta.dirname, ".."); +// Outside node_modules, where Vite would skip prebundling the host's own deps. +export const host = join(tmpdir(), "react-ui-e2e-host"); + +const packageJson = { + name: "react-ui-e2e-host", + private: true, + type: "module", + // Exact pins instead of a lockfile: bun keeps a stale copy of a rewritten local tarball. + dependencies: { + "@corbits/react-ui": "file:./react-ui.tgz", + "@radix-ui/react-dialog": "1.1.23", + "@radix-ui/react-slot": "1.3.3", + "lucide-react": "1.48.0", + react: "19.3.0", + "react-dom": "19.3.0", + }, + devDependencies: { + "@types/react": "19.3.0", + "@types/react-dom": "19.3.0", + typescript: "5.9.3", + vite: "6.4.3", + }, +}; + +const tsconfig = { + compilerOptions: { + target: "ES2022", + lib: ["dom", "dom.iterable", "ES2022"], + jsx: "react-jsx", + module: "esnext", + moduleResolution: "bundler", + strict: true, + noEmit: true, + skipLibCheck: true, + isolatedModules: true, + }, + include: ["src"], +}; + +const viteConfig = `export default { + esbuild: { jsx: "automatic" }, + server: { port: 4173, strictPort: true }, +}; +`; + +const indexHtml = ` + + + + react-ui e2e host + + +
+ + + +`; + +const mainTsx = `import "@corbits/react-ui/styles.css"; + +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; + +import { scenarios } from "./fixtures.js"; + +const name = window.location.pathname.slice(1); +const Scenario = scenarios[name]; + +createRoot(document.getElementById("root")!).render( + {Scenario ? :

Unknown scenario: {name}

}
, +); +`; + +function run(cwd: string, command: string, ...args: string[]): void { + execFileSync(command, args, { cwd, stdio: "inherit" }); +} + +if (import.meta.main) { + rmSync(host, { recursive: true, force: true }); + mkdirSync(join(host, "src"), { recursive: true }); + run( + root, + "bun", + "pm", + "pack", + "--quiet", + "--filename", + join(host, "react-ui.tgz"), + ); + writeFileSync( + join(host, "package.json"), + JSON.stringify(packageJson, null, 2), + ); + writeFileSync(join(host, "tsconfig.json"), JSON.stringify(tsconfig, null, 2)); + writeFileSync(join(host, "vite.config.mjs"), viteConfig); + writeFileSync(join(host, "index.html"), indexHtml); + writeFileSync(join(host, "src", "main.tsx"), mainTsx); + copyFileSync( + join(import.meta.dirname, "fixtures.tsx"), + join(host, "src", "fixtures.tsx"), + ); + run(host, "bun", "install"); + run(host, "bun", "x", "tsc", "--noEmit"); + run(host, "bun", "x", "vite"); +} diff --git a/e2e/smoke.spec.ts b/e2e/smoke.spec.ts new file mode 100644 index 00000000..975c1e2d --- /dev/null +++ b/e2e/smoke.spec.ts @@ -0,0 +1,8 @@ +import { expect, test } from "@playwright/test"; + +test("the host renders a component from the built package", async ({ + page, +}) => { + await page.goto("/smoke"); + await expect(page.getByRole("button", { name: "Ready" })).toBeVisible(); +}); diff --git a/e2e/teardown.ts b/e2e/teardown.ts new file mode 100644 index 00000000..1f7e3d72 --- /dev/null +++ b/e2e/teardown.ts @@ -0,0 +1,7 @@ +import { rmSync } from "node:fs"; + +import { host } from "./helpers"; + +export default function globalTeardown(): void { + rmSync(host, { recursive: true, force: true }); +} diff --git a/package.json b/package.json index 5d20af7a..17404fde 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "format": "oxfmt", "format:check": "oxfmt --check", "test": "bun test src --pass-with-no-tests", - "test:e2e": "bun test e2e", + "test:e2e": "bun test ./e2e/exports.test.ts && playwright test", "check": "bun run typecheck && bun run lint && bun run format:check && bun run test", "build:css": "tailwindcss -i src/styles.css -o dist/styles.css && cp src/theme.css dist/theme.css", "dep-guard": "node scripts/dep-guard.mjs", @@ -91,6 +91,7 @@ "devDependencies": { "@happy-dom/global-registrator": "^20.11.2", "@ladle/react": "^5.1.1", + "@playwright/test": "^1.63.0", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-slot": "^1.2.3", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 00000000..57c710fa --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,17 @@ +import { defineConfig, devices } from "@playwright/test"; + +// Serves the Vite host e2e/helpers.ts bootstraps from the packed tarball. +export default defineConfig({ + testDir: "e2e", + testMatch: "*.spec.ts", + reporter: "list", + globalTeardown: "./e2e/teardown.ts", + use: { baseURL: "http://localhost:4173" }, + projects: [{ name: "chromium", use: devices["Desktop Chrome"] }], + webServer: { + command: "bun e2e/helpers.ts", + url: "http://localhost:4173/smoke", + reuseExistingServer: false, + timeout: 180_000, + }, +}); diff --git a/tsconfig.json b/tsconfig.json index b886bbc5..5ba3c80b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,5 +18,14 @@ "lib": ["ESNext", "DOM", "DOM.Iterable"], "types": ["bun"] }, - "include": ["src", "e2e", "stories", ".ladle", "vite.ladle.config.mjs"] + "include": [ + "src", + "e2e", + "stories", + "playwright.config.ts", + ".ladle", + "vite.ladle.config.mjs" + ], + // Typechecked by the e2e host against the packed package. + "exclude": ["e2e/fixtures.tsx"] }