fix(tests): fix e2e sleep (#10061)

* fix(tests): wait for registry readiness in global setup instead of per-test sleep

The first e2e test was flaky on CI because `start-server-and-test` only
checks that the root URL (http://localhost:4000) responds before running
tests, not the /r registry endpoint. The existing 2-second hardcoded sleep
in the first test was unreliable on slower CI runners.

Move the readiness check into the vitest globalSetup so all tests wait for
the registry /r endpoint to actually be reachable before any test starts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(tests): fix race condition in global setup - poll correct URL and CLI binary

Two issues caused the previous fix to fail:

1. Was polling `http://localhost:4000/r` which is a directory → always 404.
   Now polls `{REGISTRY_URL}/index.json`, a real static file that returns 200.

2. The v4 dev script (`pnpm --filter=shadcn build && pnpm icons:dev & next dev`)
   runs the shadcn CLI build in the background while next dev starts immediately.
   On fast CI runs start-server-and-test can detect the server as ready before
   the CLI binary (packages/shadcn/dist/index.js) has been built, causing the
   first test to fail when it tries to invoke the CLI.
   Now explicitly waits for the binary to exist before any test runs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(tests): warm up /init route in global setup to prevent first-test timeout

The CLI's first request during `shadcn init` hits the dynamic Next.js /init
route. On a cold dev server this route takes ~1.8s to compile. Combined with
the rest of what init does (pnpm install, file writes), this pushes the first
test over the 30s CLI timeout on CI. Subsequent tests pass because the route
is already warm.

Polling /init in global setup ensures the route is compiled before any test
runs, making the first test's CLI invocation as fast as all subsequent ones.

Also replaced the /r/index.json poll (a static file that responds immediately
and doesn't reflect real route readiness) with the actual /init route poll,
which also naturally verifies the registry server is up.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(tests): warm up 404 route and increase default CLI timeout

Two more issues found in CI logs:

1. The CLI requests font files that don't exist (e.g. /r/styles/new-york-v4/
   font-geist.json), causing Next.js to compile /_not-found/page on the first
   404 response. That compilation takes ~4-5s on a cold dev server and is
   another hidden cost on the first test. Now triggering a 404 in global setup
   so the not-found page is compiled before any test runs.

2. The default CLI timeout of 30s is too tight for CI. Even with the /init and
   404 routes pre-warmed, pnpm install inside the fixture takes ~25s, leaving
   only ~5s of headroom. Increasing the default from 30s to 60s gives a
   comfortable buffer without masking real hangs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
shadcn
2026-03-16 16:16:16 +04:00
committed by GitHub
parent 74c4c7508b
commit dbe1fa76b3
3 changed files with 51 additions and 4 deletions

View File

@@ -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"])

View File

@@ -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

View File

@@ -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<boolean>,
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)