Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ coverage/
.env.*
.DS_Store
build/
test-results/
playwright-report/
6 changes: 4 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
7 changes: 7 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions e2e/fixtures.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { ReactNode } from "react";

import { Button } from "@corbits/react-ui";

export const scenarios: Record<string, () => ReactNode> = {
smoke: () => <Button>Ready</Button>,
};
114 changes: 114 additions & 0 deletions e2e/helpers.ts
Original file line number Diff line number Diff line change
@@ -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 = `<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>react-ui e2e host</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
`;

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(
<StrictMode>{Scenario ? <Scenario /> : <p>Unknown scenario: {name}</p>}</StrictMode>,
);
`;

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");
}
8 changes: 8 additions & 0 deletions e2e/smoke.spec.ts
Original file line number Diff line number Diff line change
@@ -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();
});
7 changes: 7 additions & 0 deletions e2e/teardown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { rmSync } from "node:fs";

import { host } from "./helpers";

export default function globalTeardown(): void {
rmSync(host, { recursive: true, force: true });
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
17 changes: 17 additions & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
@@ -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,
},
});
11 changes: 10 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
Loading