Skip to content

Commit 25e2d56

Browse files
committed
add tests
1 parent 893a538 commit 25e2d56

File tree

6 files changed

+726
-7
lines changed

6 files changed

+726
-7
lines changed

packages/ui-cli/package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,12 @@
2121
"scripts": {
2222
"format": "node ../../scripts/format-package.mjs",
2323
"format:check": "node ../../scripts/format-package.mjs --check",
24-
"lint": "eslint src"
24+
"lint": "eslint src",
25+
"test": "vitest run",
26+
"test:watch": "vitest"
27+
},
28+
"devDependencies": {
29+
"vitest": "^2.1.8"
2530
},
2631
"engines": {
2732
"node": ">=18.17.0"
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
import { execSync } from 'node:child_process';
2+
3+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4+
5+
import cli from '../cli.js';
6+
7+
// Mock execSync
8+
vi.mocked(execSync);
9+
10+
describe('CLI', () => {
11+
/** @type {any} */
12+
let consoleSpy;
13+
/** @type {any} */
14+
let processExitSpy;
15+
/** @type {any} */
16+
let originalArgv;
17+
18+
beforeEach(() => {
19+
// Mock console methods
20+
consoleSpy = {
21+
log: vi.spyOn(console, 'log').mockImplementation(() => {}),
22+
error: vi.spyOn(console, 'error').mockImplementation(() => {}),
23+
};
24+
25+
// Mock process.exit
26+
processExitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {
27+
throw new Error('process.exit');
28+
});
29+
30+
// Store original argv
31+
originalArgv = process.argv;
32+
33+
// Clear execSync mock
34+
vi.mocked(execSync).mockClear();
35+
});
36+
37+
afterEach(() => {
38+
// Restore all mocks
39+
consoleSpy.log.mockRestore();
40+
consoleSpy.error.mockRestore();
41+
processExitSpy.mockRestore();
42+
43+
// Restore original argv
44+
process.argv = originalArgv;
45+
});
46+
47+
describe('Invalid argument scenarios', () => {
48+
it('should show usage when no arguments provided', () => {
49+
process.argv = ['node', 'cli.js'];
50+
51+
expect(() => cli()).toThrow('process.exit');
52+
expect(consoleSpy.log).toHaveBeenCalledWith('Usage: npx @clerk/ui add [...packages]');
53+
expect(processExitSpy).toHaveBeenCalledWith(1);
54+
});
55+
56+
it('should show usage when only one argument provided', () => {
57+
process.argv = ['node', 'cli.js', 'add'];
58+
59+
expect(() => cli()).toThrow('process.exit');
60+
expect(consoleSpy.log).toHaveBeenCalledWith('Usage: npx @clerk/ui add [...packages]');
61+
expect(processExitSpy).toHaveBeenCalledWith(1);
62+
});
63+
64+
it('should show usage when wrong command provided', () => {
65+
process.argv = ['node', 'cli.js', 'install', 'button'];
66+
67+
expect(() => cli()).toThrow('process.exit');
68+
expect(consoleSpy.log).toHaveBeenCalledWith('Usage: npx @clerk/ui add [...packages]');
69+
expect(processExitSpy).toHaveBeenCalledWith(1);
70+
});
71+
72+
it('should show usage when invalid command with multiple args', () => {
73+
process.argv = ['node', 'cli.js', 'remove', 'button', 'card'];
74+
75+
expect(() => cli()).toThrow('process.exit');
76+
expect(consoleSpy.log).toHaveBeenCalledWith('Usage: npx @clerk/ui add [...packages]');
77+
expect(processExitSpy).toHaveBeenCalledWith(1);
78+
});
79+
});
80+
81+
describe('Package name validation', () => {
82+
it('should skip empty package names', () => {
83+
process.argv = ['node', 'cli.js', 'add', 'button', '', 'card'];
84+
vi.mocked(execSync).mockReturnValue('');
85+
86+
cli();
87+
88+
expect(vi.mocked(execSync)).toHaveBeenCalledTimes(2);
89+
expect(vi.mocked(execSync)).toHaveBeenCalledWith('npx -y shadcn@latest add https://clerk.com/r/button.json', {
90+
stdio: 'inherit',
91+
});
92+
expect(vi.mocked(execSync)).toHaveBeenCalledWith('npx -y shadcn@latest add https://clerk.com/r/card.json', {
93+
stdio: 'inherit',
94+
});
95+
});
96+
97+
it('should skip whitespace-only package names', () => {
98+
process.argv = ['node', 'cli.js', 'add', 'button', ' ', 'card'];
99+
vi.mocked(execSync).mockReturnValue('');
100+
101+
cli();
102+
103+
expect(vi.mocked(execSync)).toHaveBeenCalledTimes(2);
104+
expect(vi.mocked(execSync)).toHaveBeenCalledWith('npx -y shadcn@latest add https://clerk.com/r/button.json', {
105+
stdio: 'inherit',
106+
});
107+
expect(vi.mocked(execSync)).toHaveBeenCalledWith('npx -y shadcn@latest add https://clerk.com/r/card.json', {
108+
stdio: 'inherit',
109+
});
110+
});
111+
112+
it('should process valid package names with whitespace', () => {
113+
process.argv = ['node', 'cli.js', 'add', ' button '];
114+
vi.mocked(execSync).mockReturnValue('');
115+
116+
cli();
117+
118+
expect(vi.mocked(execSync)).toHaveBeenCalledTimes(1);
119+
expect(vi.mocked(execSync)).toHaveBeenCalledWith('npx -y shadcn@latest add https://clerk.com/r/button.json', {
120+
stdio: 'inherit',
121+
});
122+
});
123+
});
124+
125+
describe('URL construction correctness', () => {
126+
beforeEach(() => {
127+
vi.mocked(execSync).mockReturnValue('');
128+
});
129+
130+
it('should construct correct URL for single package', () => {
131+
process.argv = ['node', 'cli.js', 'add', 'button'];
132+
133+
cli();
134+
135+
expect(vi.mocked(execSync)).toHaveBeenCalledWith('npx -y shadcn@latest add https://clerk.com/r/button.json', {
136+
stdio: 'inherit',
137+
});
138+
});
139+
140+
it('should construct correct URLs for multiple packages', () => {
141+
process.argv = ['node', 'cli.js', 'add', 'button', 'card', 'dialog'];
142+
143+
cli();
144+
145+
expect(vi.mocked(execSync)).toHaveBeenCalledTimes(3);
146+
expect(vi.mocked(execSync)).toHaveBeenNthCalledWith(
147+
1,
148+
'npx -y shadcn@latest add https://clerk.com/r/button.json',
149+
{ stdio: 'inherit' },
150+
);
151+
expect(vi.mocked(execSync)).toHaveBeenNthCalledWith(2, 'npx -y shadcn@latest add https://clerk.com/r/card.json', {
152+
stdio: 'inherit',
153+
});
154+
expect(vi.mocked(execSync)).toHaveBeenNthCalledWith(
155+
3,
156+
'npx -y shadcn@latest add https://clerk.com/r/dialog.json',
157+
{ stdio: 'inherit' },
158+
);
159+
});
160+
161+
it('should construct correct URL for package with special characters', () => {
162+
process.argv = ['node', 'cli.js', 'add', 'data-table'];
163+
164+
cli();
165+
166+
expect(vi.mocked(execSync)).toHaveBeenCalledWith('npx -y shadcn@latest add https://clerk.com/r/data-table.json', {
167+
stdio: 'inherit',
168+
});
169+
});
170+
171+
it('should construct correct URL for package with numbers', () => {
172+
process.argv = ['node', 'cli.js', 'add', 'button2'];
173+
174+
cli();
175+
176+
expect(vi.mocked(execSync)).toHaveBeenCalledWith('npx -y shadcn@latest add https://clerk.com/r/button2.json', {
177+
stdio: 'inherit',
178+
});
179+
});
180+
});
181+
182+
describe('Command execution', () => {
183+
it('should log component being added', () => {
184+
process.argv = ['node', 'cli.js', 'add', 'button'];
185+
vi.mocked(execSync).mockReturnValue('');
186+
187+
cli();
188+
189+
expect(consoleSpy.log).toHaveBeenCalledWith('Adding button component...');
190+
});
191+
192+
it('should log multiple components being added', () => {
193+
process.argv = ['node', 'cli.js', 'add', 'button', 'card'];
194+
vi.mocked(execSync).mockReturnValue('');
195+
196+
cli();
197+
198+
expect(consoleSpy.log).toHaveBeenCalledWith('Adding button component...');
199+
expect(consoleSpy.log).toHaveBeenCalledWith('Adding card component...');
200+
});
201+
202+
it('should pass stdio: inherit option to execSync', () => {
203+
process.argv = ['node', 'cli.js', 'add', 'button'];
204+
vi.mocked(execSync).mockReturnValue('');
205+
206+
cli();
207+
208+
expect(vi.mocked(execSync)).toHaveBeenCalledWith('npx -y shadcn@latest add https://clerk.com/r/button.json', {
209+
stdio: 'inherit',
210+
});
211+
});
212+
});
213+
214+
describe('Error handling', () => {
215+
it('should handle execSync errors gracefully', () => {
216+
process.argv = ['node', 'cli.js', 'add', 'nonexistent'];
217+
vi.mocked(execSync).mockImplementation(() => {
218+
throw new Error('Command failed');
219+
});
220+
221+
expect(() => cli()).toThrow('process.exit');
222+
223+
expect(consoleSpy.error).toHaveBeenCalledWith('\nError: Failed to add component "nonexistent"');
224+
expect(consoleSpy.error).toHaveBeenCalledWith(
225+
'Could not fetch component from: https://clerk.com/r/nonexistent.json',
226+
);
227+
expect(consoleSpy.error).toHaveBeenCalledWith('Please ensure:');
228+
expect(consoleSpy.error).toHaveBeenCalledWith(' - The component name is correct');
229+
expect(consoleSpy.error).toHaveBeenCalledWith(' - You have internet connectivity');
230+
expect(consoleSpy.error).toHaveBeenCalledWith(' - The component exists at the specified URL');
231+
expect(processExitSpy).toHaveBeenCalledWith(1);
232+
});
233+
234+
it('should stop processing on first error', () => {
235+
process.argv = ['node', 'cli.js', 'add', 'button', 'nonexistent', 'card'];
236+
237+
// Mock first call to succeed, second to fail
238+
vi.mocked(execSync)
239+
.mockReturnValueOnce('')
240+
.mockImplementationOnce(() => {
241+
throw new Error('Command failed');
242+
});
243+
244+
expect(() => cli()).toThrow('process.exit');
245+
246+
expect(vi.mocked(execSync)).toHaveBeenCalledTimes(2);
247+
expect(consoleSpy.log).toHaveBeenCalledWith('Adding button component...');
248+
expect(consoleSpy.log).toHaveBeenCalledWith('Adding nonexistent component...');
249+
expect(consoleSpy.error).toHaveBeenCalledWith('\nError: Failed to add component "nonexistent"');
250+
expect(processExitSpy).toHaveBeenCalledWith(1);
251+
});
252+
});
253+
});

packages/ui-cli/src/cli.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,19 @@ export default function cli() {
1111
const packageNames = args.slice(1);
1212

1313
for (const packageName of packageNames) {
14-
if (!packageName.trim()) {
14+
const trimmedPackageName = packageName.trim();
15+
if (!trimmedPackageName) {
1516
continue;
1617
}
1718

18-
console.log(`Adding ${packageName} component...`);
19+
console.log(`Adding ${trimmedPackageName} component...`);
1920

20-
const url = new URL(`r/${packageName}.json`, 'https://clerk.com');
21+
const url = new URL(`r/${trimmedPackageName}.json`, 'https://clerk.com');
2122

2223
try {
2324
execSync(`npx -y shadcn@latest add ${url.toString()}`, { stdio: 'inherit' });
2425
} catch {
25-
console.error(`\nError: Failed to add component "${packageName}"`);
26+
console.error(`\nError: Failed to add component "${trimmedPackageName}"`);
2627
console.error(`Could not fetch component from: ${url.toString()}`);
2728
console.error('Please ensure:');
2829
console.error(' - The component name is correct');

packages/ui-cli/vitest.config.mts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { defineConfig } from 'vitest/config';
2+
3+
export default defineConfig({
4+
test: {
5+
environment: 'node',
6+
include: ['**/*.spec.{js,ts}'],
7+
setupFiles: './vitest.setup.mts',
8+
},
9+
});

packages/ui-cli/vitest.setup.mts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { vi } from 'vitest';
2+
3+
// Mock execSync to prevent actual command execution during tests
4+
vi.mock('node:child_process', () => ({
5+
execSync: vi.fn(),
6+
}));

0 commit comments

Comments
 (0)