From a6f3ef591f72cdcaee872a0aeb9d979255effdc6 Mon Sep 17 00:00:00 2001 From: shadcn Date: Tue, 17 Feb 2026 12:34:45 +0400 Subject: [PATCH] test: update to use mock server --- packages/tests/src/tests/init.test.ts | 109 +++++++++++------ packages/tests/src/utils/helpers.ts | 3 + packages/tests/src/utils/registry.ts | 163 +++++++++++++++++++++++++- 3 files changed, 238 insertions(+), 37 deletions(-) diff --git a/packages/tests/src/tests/init.test.ts b/packages/tests/src/tests/init.test.ts index 6a0a0d60c4..e32cf4c934 100644 --- a/packages/tests/src/tests/init.test.ts +++ b/packages/tests/src/tests/init.test.ts @@ -1,5 +1,6 @@ import os from "os" import path from "path" +import { fileURLToPath } from "url" import fs from "fs-extra" import { afterAll, beforeAll, describe, expect, it } from "vitest" @@ -10,13 +11,29 @@ import { } from "../utils/helpers" import { createRegistryServer } from "../utils/registry" +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const PUBLIC_R_DIR = path.join(__dirname, "../../../../apps/v4/public/r") +const MOCK_PORT = 4001 +const MOCK_ENV = { REGISTRY_URL: `http://localhost:${MOCK_PORT}/r` } + +// Mock registry server that handles /init requests and serves static files. +const mockRegistry = await createRegistryServer([], { + port: MOCK_PORT, + publicDir: PUBLIC_R_DIR, +}) + +beforeAll(async () => { + await mockRegistry.start() +}) + +afterAll(async () => { + await mockRegistry.stop() +}) + describe("shadcn init - next-app", () => { it("should init with default configuration", async () => { - // Sleep for 1 second to avoid race condition with the registry server. - await new Promise((resolve) => setTimeout(resolve, 2000)) - const fixturePath = await createFixtureTestDirectory("next-app") - await npxShadcn(fixturePath, ["init", "--defaults"]) + await npxShadcn(fixturePath, ["init", "--defaults"], { env: MOCK_ENV }) const componentsJsonPath = path.join(fixturePath, "components.json") expect(await fs.pathExists(componentsJsonPath)).toBe(true) @@ -57,7 +74,9 @@ describe("shadcn init - next-app", () => { it("should init without CSS variables", async () => { const fixturePath = await createFixtureTestDirectory("next-app") - await npxShadcn(fixturePath, ["init", "--defaults", "--no-css-variables"]) + await npxShadcn(fixturePath, ["init", "--defaults", "--no-css-variables"], { + env: MOCK_ENV, + }) const componentsJson = await fs.readJson( path.join(fixturePath, "components.json") @@ -67,7 +86,9 @@ describe("shadcn init - next-app", () => { it("should init with components", async () => { const fixturePath = await createFixtureTestDirectory("next-app") - await npxShadcn(fixturePath, ["init", "--defaults", "button"]) + await npxShadcn(fixturePath, ["init", "--defaults", "button"], { + env: MOCK_ENV, + }) expect( await fs.pathExists(path.join(fixturePath, "components/ui/button.tsx")) @@ -78,7 +99,9 @@ describe("shadcn init - next-app", () => { describe("shadcn init - vite-app", () => { it("should init with custom alias and src", async () => { const fixturePath = await createFixtureTestDirectory("vite-app") - await npxShadcn(fixturePath, ["init", "--defaults", "alert-dialog"]) + await npxShadcn(fixturePath, ["init", "--defaults", "alert-dialog"], { + env: MOCK_ENV, + }) const componentsJson = await fs.readJson( path.join(fixturePath, "components.json") @@ -212,7 +235,13 @@ describe("shadcn init - custom style", async () => { it("should init with style that extends shadcn", async () => { const fixturePath = await createFixtureTestDirectory("next-app") - await npxShadcn(fixturePath, ["init", "http://localhost:4445/r/style.json"]) + await npxShadcn( + fixturePath, + ["init", "http://localhost:4445/r/style.json"], + { + env: MOCK_ENV, + } + ) const componentsJson = await fs.readJson( path.join(fixturePath, "components.json") @@ -273,10 +302,11 @@ describe("shadcn init - custom style", async () => { it("should init with style that extends another style", async () => { const fixturePath = await createFixtureTestDirectory("next-app") - await npxShadcn(fixturePath, [ - "init", - "http://localhost:4445/r/style-extended.json", - ]) + await npxShadcn( + fixturePath, + ["init", "http://localhost:4445/r/style-extended.json"], + { env: MOCK_ENV } + ) const componentsJson = await fs.readJson( path.join(fixturePath, "components.json") @@ -341,10 +371,11 @@ describe("shadcn init - custom style", async () => { it("should init with custom style extended none", async () => { const fixturePath = await createFixtureTestDirectory("next-app") - await npxShadcn(fixturePath, [ - "init", - "http://localhost:4445/r/style-extend-none.json", - ]) + await npxShadcn( + fixturePath, + ["init", "http://localhost:4445/r/style-extend-none.json"], + { env: MOCK_ENV } + ) // We still expect components.json to be created. // With some defaults. @@ -398,7 +429,7 @@ describe("shadcn init - custom style", async () => { describe("shadcn init - unsupported framework", () => { it("should init with --defaults on unsupported framework", async () => { const fixturePath = await createFixtureTestDirectory("remix-app") - await npxShadcn(fixturePath, ["init", "--defaults"]) + await npxShadcn(fixturePath, ["init", "--defaults"], { env: MOCK_ENV }) const componentsJsonPath = path.join(fixturePath, "components.json") expect(await fs.pathExists(componentsJsonPath)).toBe(true) @@ -419,7 +450,9 @@ describe("shadcn init - unsupported framework", () => { it("should init with --defaults and components on unsupported framework", async () => { const fixturePath = await createFixtureTestDirectory("remix-app") - await npxShadcn(fixturePath, ["init", "--defaults", "button"]) + await npxShadcn(fixturePath, ["init", "--defaults", "button"], { + env: MOCK_ENV, + }) expect( await fs.pathExists(path.join(fixturePath, "components/ui/button.tsx")) @@ -444,7 +477,9 @@ describe("shadcn init - template flag", () => { it("should accept valid template with --defaults", async () => { const fixturePath = await createFixtureTestDirectory("next-app") - await npxShadcn(fixturePath, ["init", "-t", "next", "--defaults"]) + await npxShadcn(fixturePath, ["init", "-t", "next", "--defaults"], { + env: MOCK_ENV, + }) const componentsJsonPath = path.join(fixturePath, "components.json") expect(await fs.pathExists(componentsJsonPath)).toBe(true) @@ -476,6 +511,7 @@ describe("shadcn init - --name flag", () => { await npxShadcn(emptyDir, ["init", "--defaults", "--name", projectName], { timeout: 120000, + env: MOCK_ENV, }) const projectPath = path.join(emptyDir, projectName) @@ -506,7 +542,7 @@ describe("shadcn init - --name flag", () => { await npxShadcn( emptyDir, ["init", "--defaults", "--name", projectName, "-t", "vite"], - { timeout: 120000 } + { timeout: 120000, env: MOCK_ENV } ) const projectPath = path.join(emptyDir, projectName) @@ -551,7 +587,7 @@ describe("shadcn init - next-monorepo", () => { "--preset", "radix-nova", ], - { timeout: 300000 } + { timeout: 300000, env: MOCK_ENV } ) expect(result.exitCode).toBe(0) @@ -599,10 +635,9 @@ describe("shadcn init - next-monorepo", () => { it("should create a monorepo with custom preset url", async () => { const projectName = `test-monorepo-url-${process.pid}` - // Build a custom init URL with specific options. - const registryUrl = process.env.REGISTRY_URL || "http://localhost:4000/r" - const baseUrl = registryUrl.replace(/\/r\/?$/, "") - const initUrl = `${baseUrl}/init?base=radix&style=nova&baseColor=zinc&theme=zinc&iconLibrary=lucide&font=inter&rtl=false&menuAccent=subtle&menuColor=default&radius=default&template=next` + // Build a custom init URL with specific options using the mock server. + const mockBaseUrl = `http://localhost:${MOCK_PORT}` + const initUrl = `${mockBaseUrl}/init?base=radix&style=nova&baseColor=zinc&theme=zinc&iconLibrary=lucide&font=inter&rtl=false&menuAccent=subtle&menuColor=default&radius=default&template=next` const result = await npxShadcn( testBaseDir, @@ -615,7 +650,7 @@ describe("shadcn init - next-monorepo", () => { "--preset", initUrl, ], - { timeout: 300000 } + { timeout: 300000, env: MOCK_ENV } ) expect(result.exitCode).toBe(0) @@ -659,11 +694,11 @@ describe("shadcn init - next-monorepo", () => { describe("shadcn init - deprecated --src-dir", () => { it("should reject --src-dir as unknown option", async () => { const fixturePath = await createFixtureTestDirectory("next-app") - const result = await npxShadcn(fixturePath, [ - "init", - "--defaults", - "--src-dir", - ]) + const result = await npxShadcn( + fixturePath, + ["init", "--defaults", "--src-dir"], + { env: MOCK_ENV } + ) expect(result.exitCode).toBe(1) }) @@ -676,7 +711,7 @@ describe("shadcn init - existing components.json", () => { const fixturePath = await createFixtureTestDirectory("next-app") // Run init with default configuration. - await npxShadcn(fixturePath, ["init", "--defaults"]) + await npxShadcn(fixturePath, ["init", "--defaults"], { env: MOCK_ENV }) // Override style in components.json. const componentsJsonPath = path.join(fixturePath, "components.json") @@ -685,7 +720,9 @@ describe("shadcn init - existing components.json", () => { await fs.writeJson(componentsJsonPath, config) // Reinit with --force. - await npxShadcn(fixturePath, ["init", "--force", "--defaults"]) + await npxShadcn(fixturePath, ["init", "--force", "--defaults"], { + env: MOCK_ENV, + }) const newConfig = await fs.readJson(componentsJsonPath) expect(newConfig.style).toBe("new-york") @@ -730,7 +767,7 @@ describe("shadcn init - existing components.json", () => { const fixturePath = await createFixtureTestDirectory("next-app") // Run init with default configuration. - await npxShadcn(fixturePath, ["init", "--defaults"]) + await npxShadcn(fixturePath, ["init", "--defaults"], { env: MOCK_ENV }) // Inject a custom registries object into components.json. const componentsJsonPath = path.join(fixturePath, "components.json") @@ -741,7 +778,9 @@ describe("shadcn init - existing components.json", () => { await fs.writeJson(componentsJsonPath, config) // Reinit with --force. - await npxShadcn(fixturePath, ["init", "--force", "--defaults"]) + await npxShadcn(fixturePath, ["init", "--force", "--defaults"], { + env: MOCK_ENV, + }) // components.json should exist with no .bak leftover. expect(await fs.pathExists(componentsJsonPath)).toBe(true) diff --git a/packages/tests/src/utils/helpers.ts b/packages/tests/src/utils/helpers.ts index 31b0a10397..89ba66f5c5 100644 --- a/packages/tests/src/utils/helpers.ts +++ b/packages/tests/src/utils/helpers.ts @@ -72,15 +72,18 @@ export async function npxShadcn( { debug = false, timeout, + env, }: { debug?: boolean timeout?: number + env?: Record } = {} ) { const result = await runCommand(cwd, args, { env: { REGISTRY_URL: getRegistryUrl(), SHADCN_TEMPLATE_DIR: TEMPLATES_DIR, + ...env, }, timeout, }) diff --git a/packages/tests/src/utils/registry.ts b/packages/tests/src/utils/registry.ts index 44d4e330f4..7f03c58100 100644 --- a/packages/tests/src/utils/registry.ts +++ b/packages/tests/src/utils/registry.ts @@ -2,19 +2,178 @@ import { createServer } from "http" import path from "path" import fs from "fs-extra" +// Neutral theme CSS vars used by the mock /init handler. +const NEUTRAL_THEME_LIGHT: Record = { + background: "oklch(1 0 0)", + foreground: "oklch(0.145 0 0)", + card: "oklch(1 0 0)", + "card-foreground": "oklch(0.145 0 0)", + popover: "oklch(1 0 0)", + "popover-foreground": "oklch(0.145 0 0)", + primary: "oklch(0.205 0 0)", + "primary-foreground": "oklch(0.985 0 0)", + secondary: "oklch(0.97 0 0)", + "secondary-foreground": "oklch(0.205 0 0)", + muted: "oklch(0.97 0 0)", + "muted-foreground": "oklch(0.556 0 0)", + accent: "oklch(0.97 0 0)", + "accent-foreground": "oklch(0.205 0 0)", + destructive: "oklch(0.58 0.22 27)", + border: "oklch(0.922 0 0)", + input: "oklch(0.922 0 0)", + ring: "oklch(0.708 0 0)", + "chart-1": "oklch(0.809 0.105 251.813)", + "chart-2": "oklch(0.623 0.214 259.815)", + "chart-3": "oklch(0.546 0.245 262.881)", + "chart-4": "oklch(0.488 0.243 264.376)", + "chart-5": "oklch(0.424 0.199 265.638)", + radius: "0.625rem", + sidebar: "oklch(0.985 0 0)", + "sidebar-foreground": "oklch(0.145 0 0)", + "sidebar-primary": "oklch(0.205 0 0)", + "sidebar-primary-foreground": "oklch(0.985 0 0)", + "sidebar-accent": "oklch(0.97 0 0)", + "sidebar-accent-foreground": "oklch(0.205 0 0)", + "sidebar-border": "oklch(0.922 0 0)", + "sidebar-ring": "oklch(0.708 0 0)", +} + +const NEUTRAL_THEME_DARK: Record = { + background: "oklch(0.145 0 0)", + foreground: "oklch(0.985 0 0)", + card: "oklch(0.205 0 0)", + "card-foreground": "oklch(0.985 0 0)", + popover: "oklch(0.205 0 0)", + "popover-foreground": "oklch(0.985 0 0)", + primary: "oklch(0.87 0.00 0)", + "primary-foreground": "oklch(0.205 0 0)", + secondary: "oklch(0.269 0 0)", + "secondary-foreground": "oklch(0.985 0 0)", + muted: "oklch(0.269 0 0)", + "muted-foreground": "oklch(0.708 0 0)", + accent: "oklch(0.371 0 0)", + "accent-foreground": "oklch(0.985 0 0)", + destructive: "oklch(0.704 0.191 22.216)", + border: "oklch(1 0 0 / 10%)", + input: "oklch(1 0 0 / 15%)", + ring: "oklch(0.556 0 0)", + "chart-1": "oklch(0.809 0.105 251.813)", + "chart-2": "oklch(0.623 0.214 259.815)", + "chart-3": "oklch(0.546 0.245 262.881)", + "chart-4": "oklch(0.488 0.243 264.376)", + "chart-5": "oklch(0.424 0.199 265.638)", + sidebar: "oklch(0.205 0 0)", + "sidebar-foreground": "oklch(0.985 0 0)", + "sidebar-primary": "oklch(0.488 0.243 264.376)", + "sidebar-primary-foreground": "oklch(0.985 0 0)", + "sidebar-accent": "oklch(0.269 0 0)", + "sidebar-accent-foreground": "oklch(0.985 0 0)", + "sidebar-border": "oklch(1 0 0 / 10%)", + "sidebar-ring": "oklch(0.556 0 0)", +} + +// Base dependencies by base name. +const BASE_DEPENDENCIES: Record = { + radix: ["radix-ui"], + base: ["@base-ui/react"], +} + +// Build a mock /init response matching buildRegistryBase() output. +function buildMockInitResponse(params: URLSearchParams) { + const base = params.get("base") ?? "base" + const style = params.get("style") ?? "nova" + const iconLibrary = params.get("iconLibrary") ?? "lucide" + const baseColor = params.get("baseColor") ?? "neutral" + const font = params.get("font") ?? "geist" + const rtl = params.get("rtl") === "true" + const menuAccent = params.get("menuAccent") ?? "subtle" + const menuColor = params.get("menuColor") ?? "default" + const template = params.get("template") ?? undefined + + const dependencies = [ + "shadcn@latest", + "class-variance-authority", + "tw-animate-css", + ...(BASE_DEPENDENCIES[base] ?? []), + "lucide-react", + ] + + const registryDependencies = ["utils"] + if (font) { + registryDependencies.push(`font-${font}`) + } + + return { + name: `${base}-${style}`, + extends: "none", + type: "registry:base", + config: { + style: `${base}-${style}`, + iconLibrary, + rtl, + menuColor, + menuAccent, + tailwind: { + baseColor, + }, + }, + dependencies, + registryDependencies, + cssVars: { + light: { ...NEUTRAL_THEME_LIGHT }, + dark: { ...NEUTRAL_THEME_DARK }, + }, + css: { + '@import "tw-animate-css"': {}, + '@import "shadcn/tailwind.css"': {}, + "@layer base": { + "*": { "@apply border-border outline-ring/50": {} }, + body: { "@apply bg-background text-foreground": {} }, + }, + }, + ...(rtl && { + docs: `To learn how to set up the RTL provider and fonts for your app, see https://ui.shadcn.com/docs/rtl/${ + template === "next-monorepo" ? "next" : template ?? "next" + }`, + }), + } +} + export async function createRegistryServer( items: Array<{ name: string; type: string } & Record>, { port = 4444, - path = "/r", + path: basePath = "/r", + publicDir, }: { port?: number path?: string + publicDir?: string } ) { const server = createServer((request, response) => { const urlRaw = request.url?.split("?")[0] + // Handle /init endpoint. + if (request.url?.startsWith("/init")) { + const params = new URLSearchParams(request.url.split("?")[1] ?? "") + const initResponse = buildMockInitResponse(params) + response.writeHead(200, { "Content-Type": "application/json" }) + response.end(JSON.stringify(initResponse)) + return + } + + // When publicDir is set, try serving static JSON files first. + if (publicDir && urlRaw?.startsWith(basePath + "/")) { + const relativePath = urlRaw.slice(basePath.length + 1) + const filePath = path.join(publicDir, relativePath) + if (fs.pathExistsSync(filePath)) { + response.writeHead(200, { "Content-Type": "application/json" }) + response.end(fs.readFileSync(filePath, "utf-8")) + return + } + } + // Handle registries.json endpoint (don't strip .json for this one). if (urlRaw?.endsWith("/registries.json")) { response.writeHead(200, { "Content-Type": "application/json" }) @@ -177,7 +336,7 @@ export async function createRegistryServer( } const match = urlWithoutQuery?.match( - new RegExp(`^${path}/(?:.*/)?([^/]+)$`) + new RegExp(`^${basePath}/(?:.*/)?([^/]+)$`) ) const itemName = match?.[1] const item = itemName