ccmanager 3.9.0 → 3.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/App.js +159 -44
- package/dist/components/App.test.js +96 -5
- package/dist/components/Dashboard.d.ts +12 -0
- package/dist/components/Dashboard.js +443 -0
- package/dist/components/Dashboard.test.js +348 -0
- package/dist/components/Menu.recent-projects.test.js +19 -19
- package/dist/components/NewWorktree.d.ts +20 -1
- package/dist/components/NewWorktree.js +103 -56
- package/dist/components/NewWorktree.test.js +17 -4
- package/dist/services/globalSessionOrchestrator.d.ts +1 -0
- package/dist/services/globalSessionOrchestrator.js +3 -0
- package/dist/services/projectManager.d.ts +7 -1
- package/dist/services/projectManager.js +26 -10
- package/dist/services/sessionManager.d.ts +3 -2
- package/dist/services/sessionManager.js +37 -40
- package/dist/services/sessionManager.test.js +38 -0
- package/dist/services/worktreeNameGenerator.d.ts +8 -0
- package/dist/services/worktreeNameGenerator.js +184 -0
- package/dist/services/worktreeNameGenerator.test.js +35 -0
- package/dist/utils/presetPrompt.d.ts +11 -0
- package/dist/utils/presetPrompt.js +71 -0
- package/dist/utils/presetPrompt.test.d.ts +1 -0
- package/dist/utils/presetPrompt.test.js +167 -0
- package/dist/utils/worktreeUtils.d.ts +1 -2
- package/package.json +6 -6
- package/dist/components/ProjectList.d.ts +0 -10
- package/dist/components/ProjectList.js +0 -233
- package/dist/components/ProjectList.recent-projects.test.js +0 -193
- package/dist/components/ProjectList.test.js +0 -620
- /package/dist/components/{ProjectList.recent-projects.test.d.ts → Dashboard.test.d.ts} +0 -0
- /package/dist/{components/ProjectList.test.d.ts → services/worktreeNameGenerator.test.d.ts} +0 -0
|
@@ -1,620 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { render } from 'ink-testing-library';
|
|
3
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
4
|
-
// Mock bunTerminal to avoid native module loading issues
|
|
5
|
-
vi.mock('../services/bunTerminal.js', () => ({
|
|
6
|
-
spawn: vi.fn(function () {
|
|
7
|
-
return null;
|
|
8
|
-
}),
|
|
9
|
-
}));
|
|
10
|
-
// Import the actual component code but skip the useInput hook
|
|
11
|
-
vi.mock('ink', async () => {
|
|
12
|
-
const actual = await vi.importActual('ink');
|
|
13
|
-
return {
|
|
14
|
-
...actual,
|
|
15
|
-
useInput: vi.fn(),
|
|
16
|
-
};
|
|
17
|
-
});
|
|
18
|
-
// Mock SelectInput to render items as simple text
|
|
19
|
-
vi.mock('ink-select-input', async () => {
|
|
20
|
-
const React = await vi.importActual('react');
|
|
21
|
-
const { Text, Box } = await vi.importActual('ink');
|
|
22
|
-
return {
|
|
23
|
-
default: ({ items }) => {
|
|
24
|
-
return React.createElement(Box, { flexDirection: 'column' }, items.map((item, index) => React.createElement(Text, { key: index }, item.label)));
|
|
25
|
-
},
|
|
26
|
-
};
|
|
27
|
-
});
|
|
28
|
-
// Mock TextInputWrapper to render as simple text
|
|
29
|
-
vi.mock('./TextInputWrapper.js', async () => {
|
|
30
|
-
const React = await vi.importActual('react');
|
|
31
|
-
const { Text } = await vi.importActual('ink');
|
|
32
|
-
return {
|
|
33
|
-
default: ({ value, placeholder }) => {
|
|
34
|
-
return React.createElement(Text, {}, value || placeholder || '');
|
|
35
|
-
},
|
|
36
|
-
};
|
|
37
|
-
});
|
|
38
|
-
// Mock Effect for testing
|
|
39
|
-
vi.mock('effect', async () => {
|
|
40
|
-
const actual = await vi.importActual('effect');
|
|
41
|
-
return actual;
|
|
42
|
-
});
|
|
43
|
-
// Mock the projectManager
|
|
44
|
-
vi.mock('../services/projectManager.js', () => ({
|
|
45
|
-
projectManager: {
|
|
46
|
-
instance: {
|
|
47
|
-
discoverProjectsEffect: vi.fn(),
|
|
48
|
-
},
|
|
49
|
-
getRecentProjects: vi.fn().mockReturnValue([]),
|
|
50
|
-
},
|
|
51
|
-
}));
|
|
52
|
-
// Now import after mocking
|
|
53
|
-
const { default: ProjectList } = await import('./ProjectList.js');
|
|
54
|
-
const { projectManager } = await import('../services/projectManager.js');
|
|
55
|
-
const { Effect } = await import('effect');
|
|
56
|
-
describe('ProjectList', () => {
|
|
57
|
-
const mockOnSelectProject = vi.fn();
|
|
58
|
-
const mockOnDismissError = vi.fn();
|
|
59
|
-
const mockProjects = [
|
|
60
|
-
{
|
|
61
|
-
name: 'project1',
|
|
62
|
-
path: '/projects/project1',
|
|
63
|
-
relativePath: 'project1',
|
|
64
|
-
isValid: true,
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
name: 'project2',
|
|
68
|
-
path: '/projects/project2',
|
|
69
|
-
relativePath: 'project2',
|
|
70
|
-
isValid: true,
|
|
71
|
-
},
|
|
72
|
-
{
|
|
73
|
-
name: 'project3',
|
|
74
|
-
path: '/projects/project3',
|
|
75
|
-
relativePath: 'project3',
|
|
76
|
-
isValid: true,
|
|
77
|
-
},
|
|
78
|
-
];
|
|
79
|
-
beforeEach(() => {
|
|
80
|
-
vi.clearAllMocks();
|
|
81
|
-
vi.mocked(projectManager.instance.discoverProjectsEffect).mockReturnValue(Effect.succeed(mockProjects));
|
|
82
|
-
});
|
|
83
|
-
it('should render project list with correct title', () => {
|
|
84
|
-
const { lastFrame } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
|
|
85
|
-
expect(lastFrame()).toContain('CCManager - Multi-Project Mode');
|
|
86
|
-
expect(lastFrame()).toContain('Select a project:');
|
|
87
|
-
});
|
|
88
|
-
it('should display loading state initially', () => {
|
|
89
|
-
// Create an Effect that never completes to keep loading state
|
|
90
|
-
vi.mocked(projectManager.instance.discoverProjectsEffect).mockReturnValue(Effect.async(() => { }));
|
|
91
|
-
const { lastFrame } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
|
|
92
|
-
expect(lastFrame()).toContain('Loading projects...');
|
|
93
|
-
});
|
|
94
|
-
it('should display projects after loading', async () => {
|
|
95
|
-
const { lastFrame, rerender } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
|
|
96
|
-
// Wait a bit for async operations
|
|
97
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
98
|
-
// Force rerender
|
|
99
|
-
rerender(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
|
|
100
|
-
// Wait for SelectInput to render with our mock
|
|
101
|
-
await vi.waitFor(() => {
|
|
102
|
-
const frame = lastFrame();
|
|
103
|
-
return frame && !frame.includes('Loading projects...');
|
|
104
|
-
}, { timeout: 2000 });
|
|
105
|
-
const frame = lastFrame();
|
|
106
|
-
expect(frame).toContain('0 ❯ project1');
|
|
107
|
-
expect(frame).toContain('1 ❯ project2');
|
|
108
|
-
expect(frame).toContain('2 ❯ project3');
|
|
109
|
-
});
|
|
110
|
-
it('should display error when provided', () => {
|
|
111
|
-
const { lastFrame } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: "Failed to load projects", onDismissError: mockOnDismissError }));
|
|
112
|
-
expect(lastFrame()).toContain('Error: Failed to load projects');
|
|
113
|
-
expect(lastFrame()).toContain('Press any key to dismiss');
|
|
114
|
-
});
|
|
115
|
-
it('should handle project selection via menu', async () => {
|
|
116
|
-
const { lastFrame, rerender } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
|
|
117
|
-
// Wait a bit for async operations
|
|
118
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
119
|
-
// Force rerender
|
|
120
|
-
rerender(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
|
|
121
|
-
// Wait for component to update after async loading
|
|
122
|
-
await vi.waitFor(() => {
|
|
123
|
-
const frame = lastFrame();
|
|
124
|
-
return frame && !frame.includes('Loading projects...');
|
|
125
|
-
});
|
|
126
|
-
// Verify menu structure
|
|
127
|
-
const frame = lastFrame();
|
|
128
|
-
expect(frame).toContain('0 ❯ project1');
|
|
129
|
-
expect(frame).toContain('R');
|
|
130
|
-
expect(frame).toContain('Refresh');
|
|
131
|
-
expect(frame).toContain('Q');
|
|
132
|
-
expect(frame).toContain('Exit');
|
|
133
|
-
});
|
|
134
|
-
it('should display number shortcuts for first 10 projects', async () => {
|
|
135
|
-
const { lastFrame, rerender } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
|
|
136
|
-
// Wait a bit for async operations
|
|
137
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
138
|
-
// Force rerender
|
|
139
|
-
rerender(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
|
|
140
|
-
// Wait for projects to load
|
|
141
|
-
await vi.waitFor(() => {
|
|
142
|
-
const frame = lastFrame();
|
|
143
|
-
return frame && !frame.includes('Loading projects...');
|
|
144
|
-
});
|
|
145
|
-
// Verify number prefixes are shown
|
|
146
|
-
const frame = lastFrame();
|
|
147
|
-
expect(frame).toContain('0 ❯ project1');
|
|
148
|
-
expect(frame).toContain('1 ❯ project2');
|
|
149
|
-
expect(frame).toContain('2 ❯ project3');
|
|
150
|
-
});
|
|
151
|
-
it('should display exit option in menu', async () => {
|
|
152
|
-
const { lastFrame, rerender } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
|
|
153
|
-
// Wait a bit for async operations
|
|
154
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
155
|
-
// Force rerender
|
|
156
|
-
rerender(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
|
|
157
|
-
// Wait for projects to load
|
|
158
|
-
await vi.waitFor(() => {
|
|
159
|
-
const frame = lastFrame();
|
|
160
|
-
return frame && !frame.includes('Loading projects...');
|
|
161
|
-
});
|
|
162
|
-
// Verify exit option is shown
|
|
163
|
-
const frame = lastFrame();
|
|
164
|
-
expect(frame).toContain('Q');
|
|
165
|
-
expect(frame).toContain('Exit');
|
|
166
|
-
});
|
|
167
|
-
it('should display refresh option in menu', async () => {
|
|
168
|
-
const { lastFrame } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
|
|
169
|
-
// Wait for projects to load
|
|
170
|
-
await vi.waitFor(() => {
|
|
171
|
-
return lastFrame()?.includes('project1') ?? false;
|
|
172
|
-
});
|
|
173
|
-
// Verify refresh option is shown
|
|
174
|
-
const frame = lastFrame();
|
|
175
|
-
expect(frame).toContain('R');
|
|
176
|
-
expect(frame).toContain('Refresh');
|
|
177
|
-
});
|
|
178
|
-
it('should show empty state when no projects found', async () => {
|
|
179
|
-
vi.mocked(projectManager.instance.discoverProjectsEffect).mockReturnValue(Effect.succeed([]));
|
|
180
|
-
const { lastFrame, rerender } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
|
|
181
|
-
// Wait for loading to finish
|
|
182
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
183
|
-
// Force rerender
|
|
184
|
-
rerender(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
|
|
185
|
-
// Wait for projects to load
|
|
186
|
-
await vi.waitFor(() => {
|
|
187
|
-
const frame = lastFrame();
|
|
188
|
-
return frame && !frame.includes('Loading projects...');
|
|
189
|
-
}, { timeout: 2000 });
|
|
190
|
-
expect(lastFrame()).toContain('No git repositories found in /projects');
|
|
191
|
-
});
|
|
192
|
-
it('should display error message when error prop is provided', () => {
|
|
193
|
-
const { lastFrame } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: "Test error", onDismissError: mockOnDismissError }));
|
|
194
|
-
expect(lastFrame()).toContain('Error: Test error');
|
|
195
|
-
expect(lastFrame()).toContain('Press any key to dismiss');
|
|
196
|
-
});
|
|
197
|
-
describe('search functionality', () => {
|
|
198
|
-
it.skip('should enter search mode when "/" key is pressed', async () => {
|
|
199
|
-
const mockUseInput = vi.mocked(await import('ink')).useInput;
|
|
200
|
-
const inputHandlers = [];
|
|
201
|
-
mockUseInput.mockImplementation(handler => {
|
|
202
|
-
inputHandlers.push(handler);
|
|
203
|
-
});
|
|
204
|
-
// Need to set up stdin.setRawMode for the test
|
|
205
|
-
const originalSetRawMode = process.stdin.setRawMode;
|
|
206
|
-
process.stdin.setRawMode = vi.fn();
|
|
207
|
-
const { lastFrame, rerender } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
|
|
208
|
-
// Wait for projects to load
|
|
209
|
-
await vi.waitFor(() => {
|
|
210
|
-
return lastFrame()?.includes('project1') ?? false;
|
|
211
|
-
});
|
|
212
|
-
// Simulate pressing "/" key on all handlers (both from useSearchMode and ProjectList)
|
|
213
|
-
inputHandlers.forEach(handler => {
|
|
214
|
-
handler('/', {
|
|
215
|
-
escape: false,
|
|
216
|
-
return: false,
|
|
217
|
-
leftArrow: false,
|
|
218
|
-
rightArrow: false,
|
|
219
|
-
upArrow: false,
|
|
220
|
-
downArrow: false,
|
|
221
|
-
pageDown: false,
|
|
222
|
-
pageUp: false,
|
|
223
|
-
ctrl: false,
|
|
224
|
-
shift: false,
|
|
225
|
-
tab: false,
|
|
226
|
-
backspace: false,
|
|
227
|
-
delete: false,
|
|
228
|
-
meta: false,
|
|
229
|
-
home: false,
|
|
230
|
-
end: false,
|
|
231
|
-
});
|
|
232
|
-
});
|
|
233
|
-
// Wait a bit for state update
|
|
234
|
-
await new Promise(resolve => setTimeout(resolve, 50));
|
|
235
|
-
// Force rerender to see updated state
|
|
236
|
-
rerender(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
|
|
237
|
-
// Should show search input
|
|
238
|
-
expect(lastFrame()).toContain('Search:');
|
|
239
|
-
// Restore original
|
|
240
|
-
process.stdin.setRawMode = originalSetRawMode;
|
|
241
|
-
});
|
|
242
|
-
it('should filter projects based on search query', async () => {
|
|
243
|
-
const mockUseInput = vi.mocked(await import('ink')).useInput;
|
|
244
|
-
let inputHandler = () => { };
|
|
245
|
-
mockUseInput.mockImplementation(handler => {
|
|
246
|
-
inputHandler = handler;
|
|
247
|
-
});
|
|
248
|
-
const { lastFrame, rerender } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
|
|
249
|
-
// Wait for projects to load
|
|
250
|
-
await vi.waitFor(() => {
|
|
251
|
-
return lastFrame()?.includes('project1') ?? false;
|
|
252
|
-
});
|
|
253
|
-
// Enter search mode
|
|
254
|
-
inputHandler('/', {
|
|
255
|
-
escape: false,
|
|
256
|
-
return: false,
|
|
257
|
-
leftArrow: false,
|
|
258
|
-
rightArrow: false,
|
|
259
|
-
upArrow: false,
|
|
260
|
-
downArrow: false,
|
|
261
|
-
pageDown: false,
|
|
262
|
-
pageUp: false,
|
|
263
|
-
ctrl: false,
|
|
264
|
-
shift: false,
|
|
265
|
-
tab: false,
|
|
266
|
-
backspace: false,
|
|
267
|
-
delete: false,
|
|
268
|
-
meta: false,
|
|
269
|
-
home: false,
|
|
270
|
-
end: false,
|
|
271
|
-
});
|
|
272
|
-
// Force rerender with search active and query
|
|
273
|
-
rerender(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
|
|
274
|
-
// Simulate typing "project2" in search
|
|
275
|
-
// This would be handled by the TextInput component
|
|
276
|
-
// We'll test the filtering logic separately
|
|
277
|
-
});
|
|
278
|
-
it.skip('should exit search mode but keep filter when ESC is pressed in search mode', async () => {
|
|
279
|
-
const mockUseInput = vi.mocked(await import('ink')).useInput;
|
|
280
|
-
let inputHandler = () => { };
|
|
281
|
-
mockUseInput.mockImplementation(handler => {
|
|
282
|
-
inputHandler = handler;
|
|
283
|
-
});
|
|
284
|
-
// Need to set up stdin.setRawMode for the test
|
|
285
|
-
const originalSetRawMode = process.stdin.setRawMode;
|
|
286
|
-
process.stdin.setRawMode = vi.fn();
|
|
287
|
-
const { lastFrame, rerender } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
|
|
288
|
-
// Wait for projects to load
|
|
289
|
-
await vi.waitFor(() => {
|
|
290
|
-
return lastFrame()?.includes('project1') ?? false;
|
|
291
|
-
});
|
|
292
|
-
// Enter search mode
|
|
293
|
-
inputHandler('/', {
|
|
294
|
-
escape: false,
|
|
295
|
-
return: false,
|
|
296
|
-
leftArrow: false,
|
|
297
|
-
rightArrow: false,
|
|
298
|
-
upArrow: false,
|
|
299
|
-
downArrow: false,
|
|
300
|
-
pageDown: false,
|
|
301
|
-
pageUp: false,
|
|
302
|
-
ctrl: false,
|
|
303
|
-
shift: false,
|
|
304
|
-
tab: false,
|
|
305
|
-
backspace: false,
|
|
306
|
-
delete: false,
|
|
307
|
-
meta: false,
|
|
308
|
-
home: false,
|
|
309
|
-
end: false,
|
|
310
|
-
});
|
|
311
|
-
// Wait a bit for state update
|
|
312
|
-
await new Promise(resolve => setTimeout(resolve, 50));
|
|
313
|
-
// Force rerender
|
|
314
|
-
rerender(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
|
|
315
|
-
// Should be in search mode
|
|
316
|
-
expect(lastFrame()).toContain('Search:');
|
|
317
|
-
// Press ESC
|
|
318
|
-
inputHandler('', {
|
|
319
|
-
escape: true,
|
|
320
|
-
return: false,
|
|
321
|
-
leftArrow: false,
|
|
322
|
-
rightArrow: false,
|
|
323
|
-
upArrow: false,
|
|
324
|
-
downArrow: false,
|
|
325
|
-
pageDown: false,
|
|
326
|
-
pageUp: false,
|
|
327
|
-
ctrl: false,
|
|
328
|
-
shift: false,
|
|
329
|
-
tab: false,
|
|
330
|
-
backspace: false,
|
|
331
|
-
delete: false,
|
|
332
|
-
meta: false,
|
|
333
|
-
home: false,
|
|
334
|
-
end: false,
|
|
335
|
-
});
|
|
336
|
-
// Wait a bit for state update
|
|
337
|
-
await new Promise(resolve => setTimeout(resolve, 50));
|
|
338
|
-
// Force rerender
|
|
339
|
-
rerender(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
|
|
340
|
-
// Should exit search mode
|
|
341
|
-
expect(lastFrame()).not.toContain('Search:');
|
|
342
|
-
// Restore original
|
|
343
|
-
process.stdin.setRawMode = originalSetRawMode;
|
|
344
|
-
});
|
|
345
|
-
it('should not enter search mode when "/" is pressed during error display', async () => {
|
|
346
|
-
const mockUseInput = vi.mocked(await import('ink')).useInput;
|
|
347
|
-
let inputHandler = () => { };
|
|
348
|
-
mockUseInput.mockImplementation(handler => {
|
|
349
|
-
inputHandler = handler;
|
|
350
|
-
});
|
|
351
|
-
// Need to set up stdin.setRawMode for the test
|
|
352
|
-
const originalSetRawMode = process.stdin.setRawMode;
|
|
353
|
-
process.stdin.setRawMode = vi.fn();
|
|
354
|
-
const { lastFrame, rerender } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: "Test error", onDismissError: mockOnDismissError }));
|
|
355
|
-
// Press "/" key
|
|
356
|
-
inputHandler('/', {
|
|
357
|
-
escape: false,
|
|
358
|
-
return: false,
|
|
359
|
-
leftArrow: false,
|
|
360
|
-
rightArrow: false,
|
|
361
|
-
upArrow: false,
|
|
362
|
-
downArrow: false,
|
|
363
|
-
pageDown: false,
|
|
364
|
-
pageUp: false,
|
|
365
|
-
ctrl: false,
|
|
366
|
-
shift: false,
|
|
367
|
-
tab: false,
|
|
368
|
-
backspace: false,
|
|
369
|
-
delete: false,
|
|
370
|
-
meta: false,
|
|
371
|
-
home: false,
|
|
372
|
-
end: false,
|
|
373
|
-
});
|
|
374
|
-
// Wait a bit for state update
|
|
375
|
-
await new Promise(resolve => setTimeout(resolve, 50));
|
|
376
|
-
// Force rerender
|
|
377
|
-
rerender(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: "Test error", onDismissError: mockOnDismissError }));
|
|
378
|
-
// Should not show search input, should dismiss error instead
|
|
379
|
-
expect(lastFrame()).not.toContain('Search:');
|
|
380
|
-
expect(mockOnDismissError).toHaveBeenCalled();
|
|
381
|
-
// Restore original
|
|
382
|
-
process.stdin.setRawMode = originalSetRawMode;
|
|
383
|
-
});
|
|
384
|
-
it.skip('should exit search mode when Enter is pressed but keep filter', async () => {
|
|
385
|
-
const mockUseInput = vi.mocked(await import('ink')).useInput;
|
|
386
|
-
let inputHandler = () => { };
|
|
387
|
-
mockUseInput.mockImplementation(handler => {
|
|
388
|
-
inputHandler = handler;
|
|
389
|
-
});
|
|
390
|
-
// Need to set up stdin.setRawMode for the test
|
|
391
|
-
const originalSetRawMode = process.stdin.setRawMode;
|
|
392
|
-
process.stdin.setRawMode = vi.fn();
|
|
393
|
-
const { lastFrame, rerender } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
|
|
394
|
-
// Wait for projects to load
|
|
395
|
-
await vi.waitFor(() => {
|
|
396
|
-
return lastFrame()?.includes('project1') ?? false;
|
|
397
|
-
});
|
|
398
|
-
// Enter search mode
|
|
399
|
-
inputHandler('/', {
|
|
400
|
-
escape: false,
|
|
401
|
-
return: false,
|
|
402
|
-
leftArrow: false,
|
|
403
|
-
rightArrow: false,
|
|
404
|
-
upArrow: false,
|
|
405
|
-
downArrow: false,
|
|
406
|
-
pageDown: false,
|
|
407
|
-
pageUp: false,
|
|
408
|
-
ctrl: false,
|
|
409
|
-
shift: false,
|
|
410
|
-
tab: false,
|
|
411
|
-
backspace: false,
|
|
412
|
-
delete: false,
|
|
413
|
-
meta: false,
|
|
414
|
-
home: false,
|
|
415
|
-
end: false,
|
|
416
|
-
});
|
|
417
|
-
// Wait a bit for state update
|
|
418
|
-
await new Promise(resolve => setTimeout(resolve, 50));
|
|
419
|
-
// Force rerender
|
|
420
|
-
rerender(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
|
|
421
|
-
// Should be in search mode
|
|
422
|
-
expect(lastFrame()).toContain('Search:');
|
|
423
|
-
// Press Enter
|
|
424
|
-
inputHandler('', {
|
|
425
|
-
escape: false,
|
|
426
|
-
return: true,
|
|
427
|
-
leftArrow: false,
|
|
428
|
-
rightArrow: false,
|
|
429
|
-
upArrow: false,
|
|
430
|
-
downArrow: false,
|
|
431
|
-
pageDown: false,
|
|
432
|
-
pageUp: false,
|
|
433
|
-
ctrl: false,
|
|
434
|
-
shift: false,
|
|
435
|
-
tab: false,
|
|
436
|
-
backspace: false,
|
|
437
|
-
delete: false,
|
|
438
|
-
meta: false,
|
|
439
|
-
home: false,
|
|
440
|
-
end: false,
|
|
441
|
-
});
|
|
442
|
-
// Wait a bit for state update
|
|
443
|
-
await new Promise(resolve => setTimeout(resolve, 50));
|
|
444
|
-
// Force rerender
|
|
445
|
-
rerender(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
|
|
446
|
-
// Should exit search mode
|
|
447
|
-
expect(lastFrame()).not.toContain('Search:');
|
|
448
|
-
// Should not have called onSelectProject
|
|
449
|
-
expect(mockOnSelectProject).not.toHaveBeenCalled();
|
|
450
|
-
// Restore original
|
|
451
|
-
process.stdin.setRawMode = originalSetRawMode;
|
|
452
|
-
});
|
|
453
|
-
it('should clear filter when ESC is pressed outside search mode', async () => {
|
|
454
|
-
const mockUseInput = vi.mocked(await import('ink')).useInput;
|
|
455
|
-
let inputHandler = () => { };
|
|
456
|
-
mockUseInput.mockImplementation(handler => {
|
|
457
|
-
inputHandler = handler;
|
|
458
|
-
});
|
|
459
|
-
// Need to set up stdin.setRawMode for the test
|
|
460
|
-
const originalSetRawMode = process.stdin.setRawMode;
|
|
461
|
-
process.stdin.setRawMode = vi.fn();
|
|
462
|
-
const { lastFrame, rerender } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
|
|
463
|
-
// Wait for projects to load
|
|
464
|
-
await vi.waitFor(() => {
|
|
465
|
-
return lastFrame()?.includes('project1') ?? false;
|
|
466
|
-
});
|
|
467
|
-
// Enter search mode
|
|
468
|
-
inputHandler('/', {
|
|
469
|
-
escape: false,
|
|
470
|
-
return: false,
|
|
471
|
-
leftArrow: false,
|
|
472
|
-
rightArrow: false,
|
|
473
|
-
upArrow: false,
|
|
474
|
-
downArrow: false,
|
|
475
|
-
pageDown: false,
|
|
476
|
-
pageUp: false,
|
|
477
|
-
ctrl: false,
|
|
478
|
-
shift: false,
|
|
479
|
-
tab: false,
|
|
480
|
-
backspace: false,
|
|
481
|
-
delete: false,
|
|
482
|
-
meta: false,
|
|
483
|
-
home: false,
|
|
484
|
-
end: false,
|
|
485
|
-
});
|
|
486
|
-
await new Promise(resolve => setTimeout(resolve, 50));
|
|
487
|
-
// Exit search mode with Enter (keeping filter)
|
|
488
|
-
inputHandler('', {
|
|
489
|
-
escape: false,
|
|
490
|
-
return: true,
|
|
491
|
-
leftArrow: false,
|
|
492
|
-
rightArrow: false,
|
|
493
|
-
upArrow: false,
|
|
494
|
-
downArrow: false,
|
|
495
|
-
pageDown: false,
|
|
496
|
-
pageUp: false,
|
|
497
|
-
ctrl: false,
|
|
498
|
-
shift: false,
|
|
499
|
-
tab: false,
|
|
500
|
-
backspace: false,
|
|
501
|
-
delete: false,
|
|
502
|
-
meta: false,
|
|
503
|
-
home: false,
|
|
504
|
-
end: false,
|
|
505
|
-
});
|
|
506
|
-
await new Promise(resolve => setTimeout(resolve, 50));
|
|
507
|
-
// Now press ESC outside search mode to clear filter
|
|
508
|
-
inputHandler('', {
|
|
509
|
-
escape: true,
|
|
510
|
-
return: false,
|
|
511
|
-
leftArrow: false,
|
|
512
|
-
rightArrow: false,
|
|
513
|
-
upArrow: false,
|
|
514
|
-
downArrow: false,
|
|
515
|
-
pageDown: false,
|
|
516
|
-
pageUp: false,
|
|
517
|
-
ctrl: false,
|
|
518
|
-
shift: false,
|
|
519
|
-
tab: false,
|
|
520
|
-
backspace: false,
|
|
521
|
-
delete: false,
|
|
522
|
-
meta: false,
|
|
523
|
-
home: false,
|
|
524
|
-
end: false,
|
|
525
|
-
});
|
|
526
|
-
await new Promise(resolve => setTimeout(resolve, 50));
|
|
527
|
-
// Force rerender
|
|
528
|
-
rerender(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
|
|
529
|
-
// Should display all projects (filter cleared)
|
|
530
|
-
expect(lastFrame()).toContain('project1');
|
|
531
|
-
expect(lastFrame()).toContain('project2');
|
|
532
|
-
expect(lastFrame()).toContain('project3');
|
|
533
|
-
// Restore original
|
|
534
|
-
process.stdin.setRawMode = originalSetRawMode;
|
|
535
|
-
});
|
|
536
|
-
});
|
|
537
|
-
describe('Effect-based Project Discovery Error Handling', () => {
|
|
538
|
-
it('should handle FileSystemError from discoverProjectsEffect gracefully', async () => {
|
|
539
|
-
const { FileSystemError } = await import('../types/errors.js');
|
|
540
|
-
// Mock discoverProjectsEffect to return a failed Effect with FileSystemError
|
|
541
|
-
const fileSystemError = new FileSystemError({
|
|
542
|
-
operation: 'read',
|
|
543
|
-
path: '/projects',
|
|
544
|
-
cause: 'Directory not accessible',
|
|
545
|
-
});
|
|
546
|
-
vi.mocked(projectManager.instance.discoverProjectsEffect).mockReturnValue(Effect.fail(fileSystemError));
|
|
547
|
-
const { lastFrame, rerender } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
|
|
548
|
-
// Wait for loading to finish
|
|
549
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
550
|
-
// Force rerender
|
|
551
|
-
rerender(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
|
|
552
|
-
// Wait for projects to attempt loading
|
|
553
|
-
await vi.waitFor(() => {
|
|
554
|
-
const frame = lastFrame();
|
|
555
|
-
return frame && !frame.includes('Loading projects...');
|
|
556
|
-
}, { timeout: 2000 });
|
|
557
|
-
// Should display error message with FileSystemError details
|
|
558
|
-
const frame = lastFrame();
|
|
559
|
-
expect(frame).toContain('Error:');
|
|
560
|
-
});
|
|
561
|
-
it.skip('should handle GitError from project validation failures', async () => {
|
|
562
|
-
const { GitError } = await import('../types/errors.js');
|
|
563
|
-
// Mock discoverProjectsEffect to return a failed Effect with GitError
|
|
564
|
-
const gitError = new GitError({
|
|
565
|
-
command: 'git rev-parse --show-toplevel',
|
|
566
|
-
exitCode: 128,
|
|
567
|
-
stderr: 'Not a git repository',
|
|
568
|
-
});
|
|
569
|
-
vi.mocked(projectManager.instance.discoverProjectsEffect).mockReturnValue(
|
|
570
|
-
// @ts-expect-error - Test uses wrong error type (should be FileSystemError)
|
|
571
|
-
Effect.fail(gitError));
|
|
572
|
-
const { lastFrame, rerender } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
|
|
573
|
-
// Wait for projects to attempt loading
|
|
574
|
-
await vi.waitFor(() => {
|
|
575
|
-
rerender(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
|
|
576
|
-
return !lastFrame()?.includes('Loading projects...');
|
|
577
|
-
});
|
|
578
|
-
// Should display error message
|
|
579
|
-
const frame = lastFrame();
|
|
580
|
-
expect(frame).toContain('Error:');
|
|
581
|
-
});
|
|
582
|
-
it('should implement cancellation flag for cleanup on unmount', async () => {
|
|
583
|
-
vi.mocked(projectManager.instance.discoverProjectsEffect).mockReturnValue(Effect.async(emit => {
|
|
584
|
-
const timeout = setTimeout(() => {
|
|
585
|
-
emit(Effect.succeed(mockProjects));
|
|
586
|
-
}, 500);
|
|
587
|
-
return Effect.sync(() => clearTimeout(timeout));
|
|
588
|
-
}));
|
|
589
|
-
const { unmount, lastFrame } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
|
|
590
|
-
// Wait a bit to ensure promise is pending
|
|
591
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
592
|
-
// Component should still be loading
|
|
593
|
-
expect(lastFrame()).toContain('Loading projects...');
|
|
594
|
-
// Unmount before promise resolves
|
|
595
|
-
unmount();
|
|
596
|
-
// Wait for promise to potentially resolve
|
|
597
|
-
await new Promise(resolve => setTimeout(resolve, 500));
|
|
598
|
-
// Component is unmounted, no state updates should occur
|
|
599
|
-
// This test verifies the cancellation flag prevents state updates after unmount
|
|
600
|
-
});
|
|
601
|
-
it('should successfully load projects using Effect execution', async () => {
|
|
602
|
-
vi.mocked(projectManager.instance.discoverProjectsEffect).mockReturnValue(Effect.succeed(mockProjects));
|
|
603
|
-
const { lastFrame, rerender } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
|
|
604
|
-
// Wait for loading to finish
|
|
605
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
606
|
-
// Force rerender
|
|
607
|
-
rerender(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
|
|
608
|
-
// Wait for projects to load
|
|
609
|
-
await vi.waitFor(() => {
|
|
610
|
-
const frame = lastFrame();
|
|
611
|
-
return frame && frame.includes('project1');
|
|
612
|
-
}, { timeout: 2000 });
|
|
613
|
-
// Should display loaded projects
|
|
614
|
-
const frame = lastFrame();
|
|
615
|
-
expect(frame).toContain('0 ❯ project1');
|
|
616
|
-
expect(frame).toContain('1 ❯ project2');
|
|
617
|
-
expect(frame).toContain('2 ❯ project3');
|
|
618
|
-
});
|
|
619
|
-
});
|
|
620
|
-
});
|
|
File without changes
|
|
File without changes
|