diff --git a/packages/tests/src/tests/init.test.ts b/packages/tests/src/tests/init.test.ts index 358063700b..8433838949 100644 --- a/packages/tests/src/tests/init.test.ts +++ b/packages/tests/src/tests/init.test.ts @@ -12,9 +12,6 @@ import { createRegistryServer } from "../utils/registry" 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"]) diff --git a/packages/tests/src/utils/helpers.ts b/packages/tests/src/utils/helpers.ts index 31b0a10397..b6437e2dc5 100644 --- a/packages/tests/src/utils/helpers.ts +++ b/packages/tests/src/utils/helpers.ts @@ -47,7 +47,7 @@ export async function runCommand( }, input: options?.input, reject: false, - timeout: options?.timeout ?? 30000, + timeout: options?.timeout ?? 60000, }) const result = await childProcess diff --git a/packages/tests/src/utils/setup.ts b/packages/tests/src/utils/setup.ts index 98b3acf24c..6b14b85fb5 100644 --- a/packages/tests/src/utils/setup.ts +++ b/packages/tests/src/utils/setup.ts @@ -4,9 +4,59 @@ import { rimraf } from "rimraf" export const TEMP_DIR = path.join(__dirname, "../../temp") +const SHADCN_CLI_PATH = path.join(__dirname, "../../../shadcn/dist/index.js") + +async function waitForCondition( + label: string, + check: () => Promise, + timeoutMs = 60000 +) { + const deadline = Date.now() + timeoutMs + while (Date.now() < deadline) { + if (await check()) return + await new Promise((resolve) => setTimeout(resolve, 500)) + } + throw new Error(`Timed out waiting for: ${label} (${timeoutMs}ms)`) +} + export default async function setup() { await fs.ensureDir(TEMP_DIR) + // The v4 dev script runs `pnpm --filter=shadcn build` in the background + // while `next dev` starts immediately. On fast CI runs the server can be + // ready before the CLI binary is built, so we wait for it explicitly. + await waitForCondition("shadcn CLI binary", () => + fs.pathExists(SHADCN_CLI_PATH) + ) + + // The CLI's first request goes to the dynamic /init route. On a cold Next.js + // dev server, this route needs to be compiled on first access (~1.8s). That + // compilation time, on top of everything else init does, pushes the first + // test over the 30s CLI timeout. Warming up the route here ensures it is + // already compiled before any test starts. + const registryUrl = process.env.REGISTRY_URL || "http://localhost:4000/r" + const shadcnUrl = registryUrl.replace(/\/r\/?$/, "") + const initWarmupUrl = `${shadcnUrl}/init?base=base&style=nova&baseColor=neutral&theme=neutral&iconLibrary=lucide&font=geist&rtl=false&menuAccent=subtle&menuColor=default&radius=default&template=next` + + await waitForCondition("init route warm-up", async () => { + try { + const res = await fetch(initWarmupUrl) + return res.ok + } catch { + return false + } + }) + + // The CLI fetches registry paths that may not exist (e.g. font files), + // causing Next.js to compile the /_not-found/page route on first 404. + // That compilation takes ~4-5s and contributes to the first test timing + // out. Trigger one 404 here so the not-found page is pre-compiled. + try { + await fetch(`${shadcnUrl}/r/styles/new-york-v4/font-geist.json`) + } catch { + // Best effort — don't block setup if this fails. + } + return async () => { try { await rimraf(TEMP_DIR)