src/utils/add-package-dependency.test.ts 9.2 K raw
1
import { describe, it, expect, mock, beforeEach, afterEach } from "bun:test";
2
import path from "node:path";
3
4
// Import the function to test
5
import { addPackageDependency } from "./add-package-dependency";
6
7
// Mock execa
8
const mockExeca = mock(() => Promise.resolve({ stdout: "", stderr: "" }));
9
10
// Mock the execa module
11
mock.module("execa", () => ({
12
	execa: mockExeca,
13
}));
14
15
describe("addPackageDependency", () => {
16
	const testProjectName = "test-project";
17
	const testDependencies = ["react", "typescript"];
18
19
	beforeEach(() => {
20
		mockExeca.mockClear();
21
	});
22
23
	afterEach(() => {
24
		mock.restore();
25
	});
26
27
	describe("basic functionality", () => {
28
		it("should install dependencies in project root by default", async () => {
29
			await addPackageDependency({
30
				dependencies: testDependencies,
31
				projectName: testProjectName,
32
			});
33
34
			expect(mockExeca).toHaveBeenCalledTimes(1);
35
			expect(mockExeca).toHaveBeenCalledWith(
36
				"bun",
37
				["install", ...testDependencies],
38
				{
39
					cwd: path.resolve(process.cwd(), testProjectName),
40
				},
41
			);
42
		});
43
44
		it("should install dev dependencies when devMode is true", async () => {
45
			await addPackageDependency({
46
				dependencies: testDependencies,
47
				projectName: testProjectName,
48
				devMode: true,
49
			});
50
51
			expect(mockExeca).toHaveBeenCalledTimes(1);
52
			expect(mockExeca).toHaveBeenCalledWith(
53
				"bun",
54
				["install", "-D", ...testDependencies],
55
				{
56
					cwd: path.resolve(process.cwd(), testProjectName),
57
				},
58
			);
59
		});
60
61
		it("should handle single dependency", async () => {
62
			await addPackageDependency({
63
				dependencies: ["react"],
64
				projectName: testProjectName,
65
			});
66
67
			expect(mockExeca).toHaveBeenCalledWith("bun", ["install", "react"], {
68
				cwd: path.resolve(process.cwd(), testProjectName),
69
			});
70
		});
71
72
		it("should allow empty dependencies array", async () => {
73
			await addPackageDependency({
74
				dependencies: [],
75
				projectName: testProjectName,
76
			});
77
78
			expect(mockExeca).toHaveBeenCalledWith("bun", ["install"], {
79
				cwd: path.resolve(process.cwd(), testProjectName),
80
			});
81
		});
82
	});
83
84
	describe("target-specific installations", () => {
85
		it("should install dependencies in client directory when target is 'client'", async () => {
86
			await addPackageDependency({
87
				dependencies: testDependencies,
88
				projectName: testProjectName,
89
				target: "client",
90
			});
91
92
			expect(mockExeca).toHaveBeenCalledTimes(1);
93
			expect(mockExeca).toHaveBeenCalledWith(
94
				"bun",
95
				["install", ...testDependencies],
96
				{
97
					cwd: path.join(
98
						path.resolve(process.cwd(), testProjectName),
99
						"client",
100
					),
101
				},
102
			);
103
		});
104
105
		it("should install dependencies in server directory when target is 'server'", async () => {
106
			await addPackageDependency({
107
				dependencies: testDependencies,
108
				projectName: testProjectName,
109
				target: "server",
110
			});
111
112
			expect(mockExeca).toHaveBeenCalledTimes(1);
113
			expect(mockExeca).toHaveBeenCalledWith(
114
				"bun",
115
				["install", ...testDependencies],
116
				{
117
					cwd: path.join(
118
						path.resolve(process.cwd(), testProjectName),
119
						"server",
120
					),
121
				},
122
			);
123
		});
124
125
		it("should install dev dependencies in client directory", async () => {
126
			await addPackageDependency({
127
				dependencies: testDependencies,
128
				projectName: testProjectName,
129
				target: "client",
130
				devMode: true,
131
			});
132
133
			expect(mockExeca).toHaveBeenCalledWith(
134
				"bun",
135
				["install", "-D", ...testDependencies],
136
				{
137
					cwd: path.join(
138
						path.resolve(process.cwd(), testProjectName),
139
						"client",
140
					),
141
				},
142
			);
143
		});
144
145
		it("should install dev dependencies in server directory", async () => {
146
			await addPackageDependency({
147
				dependencies: testDependencies,
148
				projectName: testProjectName,
149
				target: "server",
150
				devMode: true,
151
			});
152
153
			expect(mockExeca).toHaveBeenCalledWith(
154
				"bun",
155
				["install", "-D", ...testDependencies],
156
				{
157
					cwd: path.join(
158
						path.resolve(process.cwd(), testProjectName),
159
						"server",
160
					),
161
				},
162
			);
163
		});
164
	});
165
166
	describe("edge cases and error scenarios", () => {
167
		it("should handle special characters in project name", async () => {
168
			const specialProjectName = "my-project_with.special-chars";
169
			await addPackageDependency({
170
				dependencies: ["react"],
171
				projectName: specialProjectName,
172
			});
173
174
			expect(mockExeca).toHaveBeenCalledWith("bun", ["install", "react"], {
175
				cwd: path.resolve(process.cwd(), specialProjectName),
176
			});
177
		});
178
179
		it("should handle dependencies with scoped packages", async () => {
180
			const scopedDependencies = ["@types/node", "@tanstack/react-query"];
181
			await addPackageDependency({
182
				dependencies: scopedDependencies,
183
				projectName: testProjectName,
184
			});
185
186
			expect(mockExeca).toHaveBeenCalledWith(
187
				"bun",
188
				["install", ...scopedDependencies],
189
				{
190
					cwd: path.resolve(process.cwd(), testProjectName),
191
				},
192
			);
193
		});
194
195
		it("should propagate execa errors", async () => {
196
			const testError = new Error("Installation failed");
197
			mockExeca.mockRejectedValueOnce(testError);
198
199
			await expect(
200
				addPackageDependency({
201
					dependencies: ["react"],
202
					projectName: testProjectName,
203
				}),
204
			).rejects.toThrow("Failed to install dependencies: Installation failed");
205
		});
206
207
		it("should handle undefined devMode (falsy)", async () => {
208
			await addPackageDependency({
209
				dependencies: testDependencies,
210
				projectName: testProjectName,
211
				devMode: undefined,
212
			});
213
214
			expect(mockExeca).toHaveBeenCalledWith(
215
				"bun",
216
				["install", ...testDependencies],
217
				{
218
					cwd: path.resolve(process.cwd(), testProjectName),
219
				},
220
			);
221
		});
222
223
		it("should handle false devMode explicitly", async () => {
224
			await addPackageDependency({
225
				dependencies: testDependencies,
226
				projectName: testProjectName,
227
				devMode: false,
228
			});
229
230
			expect(mockExeca).toHaveBeenCalledWith(
231
				"bun",
232
				["install", ...testDependencies],
233
				{
234
					cwd: path.resolve(process.cwd(), testProjectName),
235
				},
236
			);
237
		});
238
239
		it("should throw error for empty project name", async () => {
240
			await expect(
241
				addPackageDependency({
242
					dependencies: ["react"],
243
					projectName: "",
244
				}),
245
			).rejects.toThrow("Project name is required");
246
247
			expect(mockExeca).not.toHaveBeenCalled();
248
		});
249
250
		it("should throw error for whitespace-only project name", async () => {
251
			await expect(
252
				addPackageDependency({
253
					dependencies: ["react"],
254
					projectName: "   ",
255
				}),
256
			).rejects.toThrow("Project name is required");
257
258
			expect(mockExeca).not.toHaveBeenCalled();
259
		});
260
261
		it("should include target info in error messages", async () => {
262
			const testError = new Error("Installation failed");
263
			mockExeca.mockRejectedValueOnce(testError);
264
265
			await expect(
266
				addPackageDependency({
267
					dependencies: ["react"],
268
					projectName: testProjectName,
269
					target: "client",
270
				}),
271
			).rejects.toThrow(
272
				"Failed to install dependencies in client: Installation failed",
273
			);
274
		});
275
	});
276
277
	describe("path construction", () => {
278
		it("should construct correct absolute paths", async () => {
279
			const expectedPath = path.resolve(process.cwd(), testProjectName);
280
281
			await addPackageDependency({
282
				dependencies: ["react"],
283
				projectName: testProjectName,
284
			});
285
286
			const actualCall = mockExeca.mock.calls[0];
287
			expect(actualCall[2].cwd).toBe(expectedPath);
288
		});
289
290
		it("should construct correct client path", async () => {
291
			const expectedPath = path.join(
292
				path.resolve(process.cwd(), testProjectName),
293
				"client",
294
			);
295
296
			await addPackageDependency({
297
				dependencies: ["react"],
298
				projectName: testProjectName,
299
				target: "client",
300
			});
301
302
			const actualCall = mockExeca.mock.calls[0];
303
			expect(actualCall[2].cwd).toBe(expectedPath);
304
		});
305
306
		it("should construct correct server path", async () => {
307
			const expectedPath = path.join(
308
				path.resolve(process.cwd(), testProjectName),
309
				"server",
310
			);
311
312
			await addPackageDependency({
313
				dependencies: ["express"],
314
				projectName: testProjectName,
315
				target: "server",
316
			});
317
318
			const actualCall = mockExeca.mock.calls[0];
319
			expect(actualCall[2].cwd).toBe(expectedPath);
320
		});
321
	});
322
323
	describe("command structure validation", () => {
324
		it("should always use 'bun' as the command", async () => {
325
			await addPackageDependency({
326
				dependencies: ["react"],
327
				projectName: testProjectName,
328
			});
329
330
			expect(mockExeca.mock.calls[0][0]).toBe("bun");
331
		});
332
333
		it("should include 'install' as first argument", async () => {
334
			await addPackageDependency({
335
				dependencies: ["react"],
336
				projectName: testProjectName,
337
			});
338
339
			const args = mockExeca.mock.calls[0][1];
340
			expect(args[0]).toBe("install");
341
		});
342
343
		it("should preserve dependency order", async () => {
344
			const orderedDeps = ["zlib", "axios", "lodash"];
345
			await addPackageDependency({
346
				dependencies: orderedDeps,
347
				projectName: testProjectName,
348
			});
349
350
			const args = mockExeca.mock.calls[0][1];
351
			const depsInCall = args.slice(1); // Remove 'install'
352
			expect(depsInCall).toEqual(orderedDeps);
353
		});
354
355
		it("should use separate arguments for flags (not concatenated)", async () => {
356
			await addPackageDependency({
357
				dependencies: ["react"],
358
				projectName: testProjectName,
359
				devMode: true,
360
			});
361
362
			const args = mockExeca.mock.calls[0][1];
363
			expect(args).toEqual(["install", "-D", "react"]);
364
			// Ensure we're not using concatenated strings like "install -D"
365
			expect(args[0]).toBe("install");
366
			expect(args[1]).toBe("-D");
367
		});
368
	});
369
});