From 5c9eb84c636e73dc45e8f3a2e87dcd38bbb47d20 Mon Sep 17 00:00:00 2001 From: kurihada Date: Fri, 20 Mar 2026 10:54:12 +0800 Subject: [PATCH] test: add registry consumer smoke coverage --- .github/workflows/changeset-status.yml | 3 + .github/workflows/release-version-pr.yml | 3 + README.md | 3 + docs/registry.md | 9 + docs/releasing.md | 10 + package.json | 1 + tests/registry/consumer-smoke.mjs | 242 +++++++++++++++++++++++ 7 files changed, 271 insertions(+) create mode 100644 tests/registry/consumer-smoke.mjs diff --git a/.github/workflows/changeset-status.yml b/.github/workflows/changeset-status.yml index 324263c..7af3d4d 100644 --- a/.github/workflows/changeset-status.yml +++ b/.github/workflows/changeset-status.yml @@ -102,3 +102,6 @@ jobs: - name: Validate registry metadata run: pnpm registry:check + + - name: Run registry consumer smoke test + run: pnpm test:registry:consumer diff --git a/.github/workflows/release-version-pr.yml b/.github/workflows/release-version-pr.yml index b6a055d..5df0ae8 100644 --- a/.github/workflows/release-version-pr.yml +++ b/.github/workflows/release-version-pr.yml @@ -37,6 +37,9 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile + - name: Run registry consumer smoke test + run: pnpm test:registry:consumer + - name: Open or update version PR uses: changesets/action@v1 with: diff --git a/README.md b/README.md index c33b5e8..46b6a8f 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,7 @@ Run tests: ```bash pnpm test +pnpm test:registry:consumer pnpm test:e2e:smoke ``` @@ -111,6 +112,8 @@ copy selected items into their own codebase. - Registry metadata lives in `registry/index.json` and is generated by `pnpm registry:build`. - The installer copies components into `src/cadence-ui`, adds missing package dependencies, and writes `src/cadence-ui/.install-manifest.json` so upgrades can reuse the same item set. +- `pnpm test:registry:consumer` creates a temporary consumer app, runs the installer, and + verifies the copied source typechecks and builds. - Install and upgrade instructions live in [docs/registry.md](/Users/xd/project/cadence-ui/docs/registry.md). ## Docs and QA diff --git a/docs/registry.md b/docs/registry.md index aac7972..e253efc 100644 --- a/docs/registry.md +++ b/docs/registry.md @@ -37,6 +37,15 @@ pnpm registry:check The pull request workflow now runs `pnpm registry:check`, and the release version PR workflow refreshes the registry automatically via `pnpm release:version`. +To validate the end-to-end consumer flow locally: + +```bash +pnpm test:registry:consumer +``` + +That smoke test creates a temporary app, runs the registry installer, installs the +required dependencies, and verifies that the copied source both typechecks and builds. + ## Consumer install flow ### 1. Pin the Cadence UI source diff --git a/docs/releasing.md b/docs/releasing.md index fe717b5..d85f325 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -78,6 +78,7 @@ Use the docs and smoke checks when the change touches behavior-heavy UI: ```bash pnpm build:docs +pnpm test:registry:consumer pnpm test:e2e:smoke ``` @@ -166,6 +167,15 @@ pnpm registry:check This prevents registry metadata drift from merging unnoticed. +The repo also runs: + +```bash +pnpm test:registry:consumer +``` + +This verifies that the committed registry snapshot can still install into a fresh +consumer app, typecheck, and build. + ### Version PR workflow The `Release Version PR` workflow runs on pushes to `main` and on manual dispatch. It: diff --git a/package.json b/package.json index 41459e0..dea7e35 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "test": "pnpm --filter @ai-ui/ui test", "test:e2e": "playwright test", "test:e2e:smoke": "playwright test tests/e2e/storybook-smoke.spec.ts", + "test:registry:consumer": "node ./tests/registry/consumer-smoke.mjs", "test:watch": "pnpm --filter @ai-ui/ui test:watch", "typecheck": "pnpm -r typecheck" }, diff --git a/tests/registry/consumer-smoke.mjs b/tests/registry/consumer-smoke.mjs new file mode 100644 index 0000000..12eac8a --- /dev/null +++ b/tests/registry/consumer-smoke.mjs @@ -0,0 +1,242 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { spawn } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const registryInstallerPath = path.join(repoRoot, "scripts", "registry-install.mjs"); +const tscBinPath = path.join(repoRoot, "node_modules", "typescript", "bin", "tsc"); +const viteBinPath = path.join(repoRoot, "node_modules", "vite", "bin", "vite.js"); +const tempPrefix = path.join(os.tmpdir(), "cadence-ui-registry-consumer-"); +const keepArtifacts = process.env.CADENCE_KEEP_REGISTRY_SMOKE === "1"; +const pnpmCommand = process.platform === "win32" ? "pnpm.cmd" : "pnpm"; + +const registryItems = ["button", "dialog", "input", "form"]; + +const packageJson = { + name: "cadence-ui-registry-consumer-smoke", + private: true, + type: "module", + dependencies: { + react: "^18.3.1", + "react-dom": "^18.3.1" + }, + devDependencies: { + "@types/react": "^18.3.28", + "@types/react-dom": "^18.3.7" + } +}; + +const tsconfig = { + compilerOptions: { + target: "ES2022", + module: "ESNext", + moduleResolution: "Bundler", + jsx: "react-jsx", + strict: true, + esModuleInterop: true, + skipLibCheck: true, + noEmit: true, + types: ["react", "react-dom"] + }, + include: ["src"] +}; + +const indexHtml = ` + + + + + Cadence UI Registry Smoke + + +
+ + + +`; + +const mainSource = `import { useForm } from "react-hook-form"; +import { createRoot } from "react-dom/client"; + +import "./cadence-ui/tokens/styles.css"; +import { Button } from "./cadence-ui/components/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogTitle, + DialogTrigger +} from "./cadence-ui/components/dialog"; +import { Form, FormControl, FormItem, FormLabel, FormMessage } from "./cadence-ui/components/form"; +import { Input } from "./cadence-ui/components/input"; + +type FormValues = { + email: string; +}; + +function App() { + const form = useForm({ + defaultValues: { + email: "team@cadence.dev" + } + }); + + return ( +
+
+ + Email + + + + + +
+ + + + + + + Registry smoke + + Verifies copied components typecheck and build in a consumer app. + + + +
+ ); +} + +const root = document.getElementById("root"); + +if (!root) { + throw new Error("Missing root element."); +} + +createRoot(root).render(); +`; + +function formatCommand(command, args) { + return [command, ...args].join(" "); +} + +async function run(command, args, options = {}) { + await new Promise((resolve, reject) => { + const child = spawn(command, args, { + cwd: options.cwd ?? repoRoot, + env: { + ...process.env, + ...(options.env ?? {}) + }, + stdio: "inherit" + }); + + child.once("error", reject); + child.once("exit", (code, signal) => { + if (code === 0) { + resolve(); + return; + } + + reject( + new Error( + `${formatCommand(command, args)} failed with code ${code ?? "null"} and signal ${ + signal ?? "null" + }.` + ) + ); + }); + }); +} + +async function writeConsumerFixture(projectDir) { + await fs.mkdir(path.join(projectDir, "src"), { recursive: true }); + await fs.writeFile(path.join(projectDir, "package.json"), `${JSON.stringify(packageJson, null, 2)}\n`); + await fs.writeFile(path.join(projectDir, "tsconfig.json"), `${JSON.stringify(tsconfig, null, 2)}\n`); + await fs.writeFile(path.join(projectDir, "index.html"), indexHtml); + await fs.writeFile(path.join(projectDir, "src", "global.d.ts"), 'declare module "*.css";\n'); + await fs.writeFile(path.join(projectDir, "src", "main.tsx"), mainSource); +} + +async function assertFileExists(projectDir, relativePath) { + const absolutePath = path.join(projectDir, ...relativePath.split("/")); + + try { + await fs.stat(absolutePath); + } catch { + throw new Error(`Expected ${relativePath} to exist in the consumer fixture.`); + } +} + +async function verifyManifest(projectDir) { + const manifestPath = path.join(projectDir, "src", "cadence-ui", ".install-manifest.json"); + const manifest = JSON.parse(await fs.readFile(manifestPath, "utf8")); + + for (const item of registryItems) { + if (!manifest.items.includes(item)) { + throw new Error(`Registry manifest is missing "${item}".`); + } + } + + if (!manifest.items.includes("tokens")) { + throw new Error('Registry manifest is missing the required "tokens" item.'); + } +} + +async function main() { + const projectDir = await fs.mkdtemp(tempPrefix); + let succeeded = false; + const cleanup = async () => { + if (!succeeded || keepArtifacts) { + console.log(`Keeping registry smoke fixture at ${projectDir}`); + return; + } + + await fs.rm(projectDir, { force: true, recursive: true }); + }; + + try { + console.log(`Creating registry consumer fixture in ${projectDir}`); + await writeConsumerFixture(projectDir); + + console.log("Installing registry items into the consumer fixture"); + await run(process.execPath, [registryInstallerPath, "--project", projectDir, ...registryItems]); + await assertFileExists(projectDir, "src/cadence-ui/components/button.tsx"); + await assertFileExists(projectDir, "src/cadence-ui/components/dialog.tsx"); + await assertFileExists(projectDir, "src/cadence-ui/components/form.tsx"); + await assertFileExists(projectDir, "src/cadence-ui/tokens/styles.css"); + await verifyManifest(projectDir); + + console.log("Installing consumer dependencies"); + await run(pnpmCommand, ["install", "--ignore-workspace"], { + cwd: projectDir, + env: { + CI: "1" + } + }); + + console.log("Re-running registry install from the saved manifest"); + await run(process.execPath, [registryInstallerPath, "--project", projectDir, "--dry-run"]); + + console.log("Typechecking the consumer fixture"); + await run(process.execPath, [tscBinPath, "-p", path.join(projectDir, "tsconfig.json"), "--noEmit"]); + + console.log("Building the consumer fixture"); + await run(process.execPath, [viteBinPath, "build"], { + cwd: projectDir + }); + + succeeded = true; + console.log("Registry consumer smoke test passed."); + } catch (error) { + console.error(`Registry consumer smoke test failed. Fixture preserved at ${projectDir}`); + throw error; + } finally { + await cleanup(); + } +} + +await main();