diff --git a/.changeset/tricky-terms-smell.md b/.changeset/tricky-terms-smell.md new file mode 100644 index 0000000000..9cdeba0228 --- /dev/null +++ b/.changeset/tricky-terms-smell.md @@ -0,0 +1,5 @@ +--- +"shadcn": patch +--- + +fix flacky tests diff --git a/packages/shadcn/src/utils/create-project.test.ts b/packages/shadcn/src/utils/create-project.test.ts index 6426ae7eeb..4a6a5ae8bd 100644 --- a/packages/shadcn/src/utils/create-project.test.ts +++ b/packages/shadcn/src/utils/create-project.test.ts @@ -1,8 +1,17 @@ import { fetchRegistry } from "@/src/registry/api" +import { spinner } from "@/src/utils/spinner" import { execa } from "execa" import fs from "fs-extra" import prompts from "prompts" -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" +import { + afterEach, + beforeEach, + describe, + expect, + it, + vi, + type MockInstance, +} from "vitest" import { TEMPLATES, createProject } from "./create-project" @@ -14,16 +23,85 @@ vi.mock("@/src/registry/api") vi.mock("@/src/utils/get-package-manager", () => ({ getPackageManager: vi.fn().mockResolvedValue("npm"), })) +vi.mock("@/src/utils/spinner") +vi.mock("@/src/utils/logger", () => ({ + logger: { + break: vi.fn(), + error: vi.fn(), + info: vi.fn(), + }, +})) describe("createProject", () => { + let mockExit: MockInstance + beforeEach(() => { vi.clearAllMocks() + + // Reset all fs mocks vi.mocked(fs.access).mockResolvedValue(undefined) vi.mocked(fs.existsSync).mockReturnValue(false) + vi.mocked(fs.ensureDir).mockResolvedValue(undefined) + vi.mocked(fs.writeFile).mockResolvedValue(undefined) + vi.mocked(fs.move).mockResolvedValue(undefined) + vi.mocked(fs.remove).mockResolvedValue(undefined) + + // Mock execa to resolve immediately without actual execution + vi.mocked(execa).mockResolvedValue({ + stdout: "", + stderr: "", + exitCode: 0, + signal: undefined, + signalDescription: undefined, + command: "", + escapedCommand: "", + failed: false, + timedOut: false, + isCanceled: false, + killed: false, + } as any) + + // Mock fetch for monorepo template + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + arrayBuffer: () => Promise.resolve(new ArrayBuffer(0)), + } as any) + + // Reset prompts mock + vi.mocked(prompts).mockResolvedValue({ type: "next", name: "my-app" }) + + // Reset registry mock + vi.mocked(fetchRegistry).mockResolvedValue([]) + + // Mock spinner function + const mockSpinner = { + start: vi.fn().mockReturnThis(), + succeed: vi.fn().mockReturnThis(), + fail: vi.fn().mockReturnThis(), + stop: vi.fn().mockReturnThis(), + text: "", + prefixText: "", + suffixText: "", + color: "cyan" as const, + indent: 0, + spinner: "dots" as const, + isSpinning: false, + interval: 100, + stream: process.stderr, + clear: vi.fn(), + render: vi.fn(), + frame: vi.fn(), + stopAndPersist: vi.fn(), + warn: vi.fn(), + info: vi.fn(), + } + vi.mocked(spinner).mockReturnValue(mockSpinner as any) }) afterEach(() => { vi.resetAllMocks() + mockExit?.mockRestore() + delete (global as any).fetch }) it("should create a Next.js project with default options", async () => { @@ -84,10 +162,13 @@ describe("createProject", () => { }) it("should throw error if project path already exists", async () => { - vi.mocked(fs.existsSync).mockReturnValue(true) + // Mock fs.existsSync to return true only for the specific package.json path + vi.mocked(fs.existsSync).mockImplementation((path: any) => { + return path.toString().includes("existing-app/package.json") + }) vi.mocked(prompts).mockResolvedValue({ type: "next", name: "existing-app" }) - const mockExit = vi + mockExit = vi .spyOn(process, "exit") .mockImplementation(() => undefined as never) @@ -103,7 +184,7 @@ describe("createProject", () => { vi.mocked(fs.access).mockRejectedValue(new Error("Permission denied")) vi.mocked(prompts).mockResolvedValue({ type: "next", name: "my-app" }) - const mockExit = vi + mockExit = vi .spyOn(process, "exit") .mockImplementation(() => undefined as never)