|
1 | 1 | /** |
2 | | - * Unit tests for CLI shell completion. |
| 2 | + * Unit tests for CLI completion utilities. |
3 | 3 | * |
4 | 4 | * Purpose: |
5 | | - * Tests shell completion generation for CLI commands. Validates bash, zsh, and fish completion scripts. |
| 5 | + * Tests the bash completion script generation and configuration. |
6 | 6 | * |
7 | 7 | * Test Coverage: |
8 | | - * - Completion script generation |
9 | | - * - Command suggestion |
10 | | - * - Flag completion |
11 | | - * - Multi-shell support (bash, zsh, fish) |
12 | | - * - Dynamic completion data |
13 | | - * |
14 | | - * Testing Approach: |
15 | | - * Tests completion script output and validation logic. |
| 8 | + * - COMPLETION_CMD_PREFIX constant |
| 9 | + * - getCompletionSourcingCommand function |
| 10 | + * - getBashrcDetails function |
16 | 11 | * |
17 | 12 | * Related Files: |
18 | 13 | * - utils/cli/completion.mts (implementation) |
19 | 14 | */ |
20 | 15 |
|
21 | | -import fs from 'node:fs' |
22 | | -import path from 'node:path' |
23 | | - |
24 | 16 | import { beforeEach, describe, expect, it, vi } from 'vitest' |
25 | 17 |
|
| 18 | +// Mock fs.existsSync. |
| 19 | +const mockExistsSync = vi.hoisted(() => vi.fn()) |
| 20 | +vi.mock('node:fs', async importOriginal => { |
| 21 | + const actual = (await importOriginal()) as typeof import('node:fs') |
| 22 | + return { |
| 23 | + ...actual, |
| 24 | + default: { |
| 25 | + ...actual, |
| 26 | + existsSync: mockExistsSync, |
| 27 | + }, |
| 28 | + existsSync: mockExistsSync, |
| 29 | + } |
| 30 | +}) |
| 31 | + |
| 32 | +// Mock getSocketAppDataPath. |
| 33 | +const mockGetSocketAppDataPath = vi.hoisted(() => vi.fn()) |
| 34 | +vi.mock('../../../../src/constants/paths.mts', async importOriginal => { |
| 35 | + const actual = |
| 36 | + (await importOriginal()) as typeof import('../../../../src/constants/paths.mts') |
| 37 | + return { |
| 38 | + ...actual, |
| 39 | + getSocketAppDataPath: mockGetSocketAppDataPath, |
| 40 | + } |
| 41 | +}) |
| 42 | + |
26 | 43 | import { |
27 | 44 | COMPLETION_CMD_PREFIX, |
28 | 45 | getBashrcDetails, |
29 | 46 | getCompletionSourcingCommand, |
30 | 47 | } from '../../../../src/utils/cli/completion.mts' |
31 | 48 |
|
32 | | -// Mock node:fs. |
33 | | -const mockGetSocketAppDataPath = vi.hoisted(() => vi.fn(() => '/mock/app/data')) |
34 | | - |
35 | | -vi.mock('node:fs', () => ({ |
36 | | - default: { |
37 | | - existsSync: vi.fn(), |
38 | | - }, |
39 | | -})) |
40 | | - |
41 | | -// Mock constants/paths. |
42 | | -vi.mock('../../../../src/constants/paths.mts', () => ({ |
43 | | - rootPath: '/mock/dist/path', |
44 | | - getSocketAppDataPath: mockGetSocketAppDataPath, |
45 | | -})) |
46 | | - |
47 | | -describe('completion utilities', () => { |
| 49 | +describe('cli/completion', () => { |
48 | 50 | beforeEach(() => { |
49 | 51 | vi.clearAllMocks() |
50 | 52 | }) |
51 | 53 |
|
52 | 54 | describe('COMPLETION_CMD_PREFIX', () => { |
53 | | - it('has the expected value', () => { |
| 55 | + it('has the correct prefix', () => { |
54 | 56 | expect(COMPLETION_CMD_PREFIX).toBe('complete -F _socket_completion') |
55 | 57 | }) |
56 | 58 | }) |
57 | 59 |
|
58 | 60 | describe('getCompletionSourcingCommand', () => { |
59 | | - it('returns sourcing command when completion script exists', () => { |
60 | | - vi.mocked(fs.existsSync).mockReturnValue(true) |
| 61 | + it('returns error when completion script does not exist', () => { |
| 62 | + mockExistsSync.mockReturnValue(false) |
61 | 63 |
|
62 | 64 | const result = getCompletionSourcingCommand() |
63 | 65 |
|
64 | | - expect(result).toEqual({ |
65 | | - ok: true, |
66 | | - data: 'source /mock/dist/path/data/socket-completion.bash', |
67 | | - }) |
68 | | - |
69 | | - // Note: On Windows, path.join returns backslashes, which are then checked by existsSync. |
70 | | - // The implementation normalizes the path display but checks existence with the actual path. |
71 | | - const expectedPath = |
72 | | - path.sep === '\\' |
73 | | - ? '\\mock\\dist\\path\\data\\socket-completion.bash' |
74 | | - : '/mock/dist/path/data/socket-completion.bash' |
75 | | - expect(fs.existsSync).toHaveBeenCalledWith(expectedPath) |
| 66 | + expect(result.ok).toBe(false) |
| 67 | + if (!result.ok) { |
| 68 | + expect(result.message).toBe('Tab Completion script not found') |
| 69 | + expect(result.cause).toContain('Expected to find completion script') |
| 70 | + } |
76 | 71 | }) |
77 | 72 |
|
78 | | - it('returns error when completion script does not exist', () => { |
79 | | - vi.mocked(fs.existsSync).mockReturnValue(false) |
| 73 | + it('returns sourcing command when completion script exists', () => { |
| 74 | + mockExistsSync.mockReturnValue(true) |
80 | 75 |
|
81 | 76 | const result = getCompletionSourcingCommand() |
82 | 77 |
|
83 | | - expect(result).toEqual({ |
84 | | - ok: false, |
85 | | - message: 'Tab Completion script not found', |
86 | | - cause: |
87 | | - 'Expected to find completion script at `/mock/dist/path/data/socket-completion.bash` but it was not there', |
88 | | - }) |
| 78 | + expect(result.ok).toBe(true) |
| 79 | + if (result.ok) { |
| 80 | + expect(result.data).toContain('source') |
| 81 | + expect(result.data).toContain('socket-completion.bash') |
| 82 | + } |
89 | 83 | }) |
90 | | - }) |
91 | 84 |
|
92 | | - describe('getBashrcDetails', () => { |
93 | | - it('returns bashrc details when everything is configured', () => { |
94 | | - vi.mocked(fs.existsSync).mockReturnValue(true) |
| 85 | + it('uses forward slashes in sourcing command', () => { |
| 86 | + mockExistsSync.mockReturnValue(true) |
95 | 87 |
|
96 | | - const result = getBashrcDetails('socket') |
| 88 | + const result = getCompletionSourcingCommand() |
97 | 89 |
|
98 | 90 | expect(result.ok).toBe(true) |
99 | 91 | if (result.ok) { |
100 | | - expect(result.data.completionCommand).toBe( |
101 | | - 'complete -F _socket_completion socket', |
102 | | - ) |
103 | | - expect(result.data.sourcingCommand).toBe( |
104 | | - 'source /mock/dist/path/data/socket-completion.bash', |
105 | | - ) |
106 | | - expect(result.data.targetName).toBe('socket') |
107 | | - expect(result.data.targetPath).toBe( |
108 | | - '/mock/app/completion/socket-completion.bash', |
109 | | - ) |
110 | | - expect(result.data.toAddToBashrc).toContain( |
111 | | - '# Socket CLI completion for "socket"', |
112 | | - ) |
113 | | - expect(result.data.toAddToBashrc).toContain( |
114 | | - 'source "/mock/app/completion/socket-completion.bash"', |
115 | | - ) |
116 | | - expect(result.data.toAddToBashrc).toContain( |
117 | | - 'complete -F _socket_completion socket', |
118 | | - ) |
| 92 | + expect(result.data).not.toContain('\\') |
119 | 93 | } |
120 | 94 | }) |
| 95 | + }) |
121 | 96 |
|
122 | | - it('returns error when completion script is missing', () => { |
123 | | - vi.mocked(fs.existsSync).mockReturnValue(false) |
| 97 | + describe('getBashrcDetails', () => { |
| 98 | + it('returns error when completion script does not exist', () => { |
| 99 | + mockExistsSync.mockReturnValue(false) |
124 | 100 |
|
125 | 101 | const result = getBashrcDetails('socket') |
126 | 102 |
|
127 | | - expect(result).toEqual({ |
128 | | - ok: false, |
129 | | - message: 'Tab Completion script not found', |
130 | | - cause: |
131 | | - 'Expected to find completion script at `/mock/dist/path/data/socket-completion.bash` but it was not there', |
132 | | - }) |
| 103 | + expect(result.ok).toBe(false) |
| 104 | + if (!result.ok) { |
| 105 | + expect(result.message).toBe('Tab Completion script not found') |
| 106 | + } |
133 | 107 | }) |
134 | 108 |
|
135 | | - it('returns error when socketAppDataPath is not available', () => { |
136 | | - // This test is tricky because we need to re-mock the constants module. |
137 | | - // Since getBashrcDetails imports constants at the top level, |
138 | | - // we can't easily change it after import. Let's skip this test |
139 | | - // or mark it as todo since the logic is tested in other ways. |
| 109 | + it('returns error when config directory cannot be determined', () => { |
| 110 | + mockExistsSync.mockReturnValue(true) |
| 111 | + mockGetSocketAppDataPath.mockReturnValue(null) |
| 112 | + |
| 113 | + const result = getBashrcDetails('socket') |
| 114 | + |
| 115 | + expect(result.ok).toBe(false) |
| 116 | + if (!result.ok) { |
| 117 | + expect(result.message).toBe('Could not determine config directory') |
| 118 | + } |
140 | 119 | }) |
141 | 120 |
|
142 | | - it('handles different command names', () => { |
143 | | - vi.mocked(fs.existsSync).mockReturnValue(true) |
| 121 | + it('returns bashrc details when everything is available', () => { |
| 122 | + mockExistsSync.mockReturnValue(true) |
| 123 | + mockGetSocketAppDataPath.mockReturnValue('/home/user/.socket/config.json') |
144 | 124 |
|
145 | | - const result = getBashrcDetails('my-custom-socket') |
| 125 | + const result = getBashrcDetails('socket') |
146 | 126 |
|
147 | 127 | expect(result.ok).toBe(true) |
148 | 128 | if (result.ok) { |
| 129 | + expect(result.data.targetName).toBe('socket') |
149 | 130 | expect(result.data.completionCommand).toBe( |
150 | | - 'complete -F _socket_completion my-custom-socket', |
151 | | - ) |
152 | | - expect(result.data.targetName).toBe('my-custom-socket') |
153 | | - expect(result.data.toAddToBashrc).toContain('my-custom-socket') |
154 | | - expect(result.data.toAddToBashrc).toContain( |
155 | | - '# Socket CLI completion for "my-custom-socket"', |
| 131 | + `${COMPLETION_CMD_PREFIX} socket`, |
156 | 132 | ) |
| 133 | + expect(result.data.toAddToBashrc).toContain('Socket CLI completion') |
| 134 | + expect(result.data.toAddToBashrc).toContain('socket') |
| 135 | + expect(result.data.sourcingCommand).toContain('source') |
157 | 136 | } |
158 | 137 | }) |
159 | 138 |
|
160 | | - it('constructs correct paths using path.join', () => { |
161 | | - vi.mocked(fs.existsSync).mockReturnValue(true) |
| 139 | + it('uses forward slashes in target path', () => { |
| 140 | + mockExistsSync.mockReturnValue(true) |
| 141 | + mockGetSocketAppDataPath.mockReturnValue('/home/user/.socket/config.json') |
162 | 142 |
|
163 | 143 | const result = getBashrcDetails('socket') |
164 | 144 |
|
165 | 145 | expect(result.ok).toBe(true) |
166 | 146 | if (result.ok) { |
167 | | - // Note: The implementation normalizes paths to forward slashes for bash compatibility. |
168 | | - expect(result.data.targetPath).toBe( |
169 | | - '/mock/app/completion/socket-completion.bash', |
| 147 | + expect(result.data.targetPath).not.toContain('\\') |
| 148 | + } |
| 149 | + }) |
| 150 | + |
| 151 | + it('handles custom command names', () => { |
| 152 | + mockExistsSync.mockReturnValue(true) |
| 153 | + mockGetSocketAppDataPath.mockReturnValue('/home/user/.socket/config.json') |
| 154 | + |
| 155 | + const result = getBashrcDetails('socket-npm') |
| 156 | + |
| 157 | + expect(result.ok).toBe(true) |
| 158 | + if (result.ok) { |
| 159 | + expect(result.data.targetName).toBe('socket-npm') |
| 160 | + expect(result.data.completionCommand).toBe( |
| 161 | + `${COMPLETION_CMD_PREFIX} socket-npm`, |
170 | 162 | ) |
| 163 | + expect(result.data.toAddToBashrc).toContain('socket-npm') |
171 | 164 | } |
172 | 165 | }) |
173 | 166 | }) |
|
0 commit comments