@akiojin/gwt 2.0.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/README.ja.md +323 -0
- package/README.md +347 -0
- package/bin/gwt.js +5 -0
- package/package.json +125 -0
- package/src/claude-history.ts +717 -0
- package/src/claude.ts +292 -0
- package/src/cli/ui/__tests__/SKIPPED_TESTS.md +119 -0
- package/src/cli/ui/__tests__/acceptance/branchList.acceptance.test.tsx.skip +239 -0
- package/src/cli/ui/__tests__/acceptance/navigation.acceptance.test.tsx +214 -0
- package/src/cli/ui/__tests__/acceptance/realtimeUpdate.acceptance.test.tsx.skip +219 -0
- package/src/cli/ui/__tests__/components/App.protected-branch.test.tsx +183 -0
- package/src/cli/ui/__tests__/components/App.shortcuts.test.tsx +313 -0
- package/src/cli/ui/__tests__/components/App.test.tsx +270 -0
- package/src/cli/ui/__tests__/components/common/Confirm.test.tsx +66 -0
- package/src/cli/ui/__tests__/components/common/ErrorBoundary.test.tsx +103 -0
- package/src/cli/ui/__tests__/components/common/Input.test.tsx +92 -0
- package/src/cli/ui/__tests__/components/common/LoadingIndicator.test.tsx +127 -0
- package/src/cli/ui/__tests__/components/common/Select.memo.test.tsx +264 -0
- package/src/cli/ui/__tests__/components/common/Select.test.tsx +246 -0
- package/src/cli/ui/__tests__/components/parts/Footer.test.tsx +62 -0
- package/src/cli/ui/__tests__/components/parts/Header.test.tsx +54 -0
- package/src/cli/ui/__tests__/components/parts/ScrollableList.test.tsx +68 -0
- package/src/cli/ui/__tests__/components/parts/Stats.test.tsx +135 -0
- package/src/cli/ui/__tests__/components/screens/AIToolSelectorScreen.test.tsx +153 -0
- package/src/cli/ui/__tests__/components/screens/BranchCreatorScreen.test.tsx +215 -0
- package/src/cli/ui/__tests__/components/screens/BranchListScreen.test.tsx +293 -0
- package/src/cli/ui/__tests__/components/screens/ExecutionModeSelectorScreen.test.tsx +161 -0
- package/src/cli/ui/__tests__/components/screens/PRCleanupScreen.test.tsx +215 -0
- package/src/cli/ui/__tests__/components/screens/SessionSelectorScreen.test.tsx +99 -0
- package/src/cli/ui/__tests__/components/screens/WorktreeManagerScreen.test.tsx +127 -0
- package/src/cli/ui/__tests__/hooks/useGitData.test.ts.skip +228 -0
- package/src/cli/ui/__tests__/hooks/useScreenState.test.ts +146 -0
- package/src/cli/ui/__tests__/hooks/useTerminalSize.test.ts +98 -0
- package/src/cli/ui/__tests__/integration/branchList.test.tsx.skip +253 -0
- package/src/cli/ui/__tests__/integration/edgeCases.test.tsx +306 -0
- package/src/cli/ui/__tests__/integration/navigation.test.tsx +405 -0
- package/src/cli/ui/__tests__/integration/realtimeUpdate.test.tsx +505 -0
- package/src/cli/ui/__tests__/integration/realtimeUpdate.test.tsx.skip +216 -0
- package/src/cli/ui/__tests__/performance/branchList.performance.test.tsx +180 -0
- package/src/cli/ui/__tests__/performance/useMemoOptimization.test.tsx +237 -0
- package/src/cli/ui/__tests__/utils/branchFormatter.test.ts +775 -0
- package/src/cli/ui/__tests__/utils/statisticsCalculator.test.ts +243 -0
- package/src/cli/ui/components/App.tsx +793 -0
- package/src/cli/ui/components/common/Confirm.tsx +40 -0
- package/src/cli/ui/components/common/ErrorBoundary.tsx +57 -0
- package/src/cli/ui/components/common/Input.tsx +36 -0
- package/src/cli/ui/components/common/LoadingIndicator.tsx +95 -0
- package/src/cli/ui/components/common/Select.tsx +216 -0
- package/src/cli/ui/components/parts/Footer.tsx +41 -0
- package/src/cli/ui/components/parts/Header.test.tsx +85 -0
- package/src/cli/ui/components/parts/Header.tsx +63 -0
- package/src/cli/ui/components/parts/MergeStatusList.tsx +75 -0
- package/src/cli/ui/components/parts/ProgressBar.tsx +73 -0
- package/src/cli/ui/components/parts/ScrollableList.tsx +24 -0
- package/src/cli/ui/components/parts/Stats.tsx +67 -0
- package/src/cli/ui/components/screens/AIToolSelectorScreen.tsx +116 -0
- package/src/cli/ui/components/screens/BatchMergeProgressScreen.tsx +70 -0
- package/src/cli/ui/components/screens/BatchMergeResultScreen.tsx +104 -0
- package/src/cli/ui/components/screens/BranchCreatorScreen.tsx +213 -0
- package/src/cli/ui/components/screens/BranchListScreen.tsx +299 -0
- package/src/cli/ui/components/screens/ExecutionModeSelectorScreen.tsx +149 -0
- package/src/cli/ui/components/screens/PRCleanupScreen.tsx +167 -0
- package/src/cli/ui/components/screens/SessionSelectorScreen.tsx +100 -0
- package/src/cli/ui/components/screens/WorktreeManagerScreen.tsx +117 -0
- package/src/cli/ui/hooks/useBatchMerge.ts +96 -0
- package/src/cli/ui/hooks/useGitData.ts +157 -0
- package/src/cli/ui/hooks/useScreenState.ts +44 -0
- package/src/cli/ui/hooks/useTerminalSize.ts +33 -0
- package/src/cli/ui/screens/BranchActionSelectorScreen.tsx +102 -0
- package/src/cli/ui/screens/__tests__/BranchActionSelectorScreen.test.tsx +151 -0
- package/src/cli/ui/types.ts +295 -0
- package/src/cli/ui/utils/baseBranch.ts +34 -0
- package/src/cli/ui/utils/branchFormatter.ts +222 -0
- package/src/cli/ui/utils/statisticsCalculator.ts +44 -0
- package/src/codex.ts +139 -0
- package/src/config/builtin-tools.ts +44 -0
- package/src/config/constants.ts +100 -0
- package/src/config/env-history.ts +45 -0
- package/src/config/index.ts +204 -0
- package/src/config/tools.ts +293 -0
- package/src/git.ts +1102 -0
- package/src/github.ts +158 -0
- package/src/index.test.ts +87 -0
- package/src/index.ts +684 -0
- package/src/index.ts.backup +1543 -0
- package/src/launcher.ts +142 -0
- package/src/repositories/git.repository.ts +129 -0
- package/src/repositories/github.repository.ts +83 -0
- package/src/repositories/worktree.repository.ts +69 -0
- package/src/services/BatchMergeService.ts +251 -0
- package/src/services/WorktreeOrchestrator.ts +115 -0
- package/src/services/__tests__/BatchMergeService.test.ts +518 -0
- package/src/services/__tests__/WorktreeOrchestrator.test.ts +258 -0
- package/src/services/dependency-installer.ts +199 -0
- package/src/services/git.service.ts +113 -0
- package/src/services/github.service.ts +61 -0
- package/src/services/worktree.service.ts +66 -0
- package/src/types/api.ts +241 -0
- package/src/types/tools.ts +235 -0
- package/src/utils/spinner.ts +54 -0
- package/src/utils/terminal.ts +272 -0
- package/src/utils.test.ts +43 -0
- package/src/utils.ts +60 -0
- package/src/web/client/index.html +12 -0
- package/src/web/client/src/components/BranchGraph.tsx +231 -0
- package/src/web/client/src/components/EnvEditor.tsx +145 -0
- package/src/web/client/src/components/Terminal.tsx +137 -0
- package/src/web/client/src/hooks/useBranches.ts +41 -0
- package/src/web/client/src/hooks/useConfig.ts +31 -0
- package/src/web/client/src/hooks/useSessions.ts +59 -0
- package/src/web/client/src/hooks/useWorktrees.ts +47 -0
- package/src/web/client/src/index.css +834 -0
- package/src/web/client/src/lib/api.ts +184 -0
- package/src/web/client/src/lib/websocket.ts +174 -0
- package/src/web/client/src/main.tsx +29 -0
- package/src/web/client/src/pages/BranchDetailPage.tsx +847 -0
- package/src/web/client/src/pages/BranchListPage.tsx +264 -0
- package/src/web/client/src/pages/ConfigManagementPage.tsx +203 -0
- package/src/web/client/src/router.tsx +27 -0
- package/src/web/client/vite.config.ts +21 -0
- package/src/web/server/env/importer.ts +54 -0
- package/src/web/server/index.ts +74 -0
- package/src/web/server/pty/manager.ts +189 -0
- package/src/web/server/routes/branches.ts +126 -0
- package/src/web/server/routes/config.ts +220 -0
- package/src/web/server/routes/index.ts +37 -0
- package/src/web/server/routes/sessions.ts +130 -0
- package/src/web/server/routes/worktrees.ts +108 -0
- package/src/web/server/services/branches.ts +368 -0
- package/src/web/server/services/worktrees.ts +85 -0
- package/src/web/server/websocket/handler.ts +180 -0
- package/src/worktree.ts +703 -0
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment happy-dom
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
5
|
+
import { render } from '@testing-library/react';
|
|
6
|
+
import React from 'react';
|
|
7
|
+
import { App } from '../../components/App.js';
|
|
8
|
+
import { Window } from 'happy-dom';
|
|
9
|
+
import type { BranchInfo } from '../../types.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Real-time update integration tests
|
|
13
|
+
* Tests auto-refresh functionality and lastUpdated display
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// Mock useGitData hook
|
|
17
|
+
const mockRefresh = vi.fn();
|
|
18
|
+
vi.mock('../../hooks/useGitData.js', () => ({
|
|
19
|
+
useGitData: vi.fn(),
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
import { useGitData } from '../../hooks/useGitData.js';
|
|
23
|
+
const mockUseGitData = useGitData as ReturnType<typeof vi.fn>;
|
|
24
|
+
|
|
25
|
+
describe('Real-time Update Integration', () => {
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
// Setup happy-dom
|
|
28
|
+
const window = new Window();
|
|
29
|
+
globalThis.window = window as any;
|
|
30
|
+
globalThis.document = window.document as any;
|
|
31
|
+
|
|
32
|
+
// Reset mocks
|
|
33
|
+
vi.clearAllMocks();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
vi.restoreAllMocks();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('T084: should disable auto-refresh (manual refresh with r key)', () => {
|
|
41
|
+
const mockBranches: BranchInfo[] = [
|
|
42
|
+
{
|
|
43
|
+
name: 'main',
|
|
44
|
+
branchType: 'main',
|
|
45
|
+
type: 'local',
|
|
46
|
+
isCurrent: true,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: 'feature/test-1',
|
|
50
|
+
branchType: 'feature',
|
|
51
|
+
type: 'local',
|
|
52
|
+
isCurrent: false,
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
mockUseGitData.mockReturnValue({
|
|
57
|
+
branches: mockBranches,
|
|
58
|
+
worktrees: [],
|
|
59
|
+
loading: false,
|
|
60
|
+
error: null,
|
|
61
|
+
refresh: mockRefresh,
|
|
62
|
+
lastUpdated: new Date(),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const onExit = vi.fn();
|
|
66
|
+
render(<App onExit={onExit} />);
|
|
67
|
+
|
|
68
|
+
// Verify useGitData was called with auto-refresh disabled (manual refresh with r key)
|
|
69
|
+
expect(mockUseGitData).toHaveBeenCalledWith({
|
|
70
|
+
enableAutoRefresh: false,
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('T085: should display updated statistics', () => {
|
|
75
|
+
const mockBranches: BranchInfo[] = [
|
|
76
|
+
{
|
|
77
|
+
name: 'main',
|
|
78
|
+
branchType: 'main',
|
|
79
|
+
type: 'local',
|
|
80
|
+
isCurrent: true,
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: 'feature/test-1',
|
|
84
|
+
branchType: 'feature',
|
|
85
|
+
type: 'local',
|
|
86
|
+
isCurrent: false,
|
|
87
|
+
},
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
mockUseGitData.mockReturnValue({
|
|
91
|
+
branches: mockBranches,
|
|
92
|
+
worktrees: [],
|
|
93
|
+
loading: false,
|
|
94
|
+
error: null,
|
|
95
|
+
refresh: mockRefresh,
|
|
96
|
+
lastUpdated: new Date(),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const onExit = vi.fn();
|
|
100
|
+
const { getByText, rerender } = render(<App onExit={onExit} />);
|
|
101
|
+
|
|
102
|
+
// Initial state should show "Local: 2"
|
|
103
|
+
expect(getByText(/Local:/i)).toBeDefined();
|
|
104
|
+
expect(getByText('2')).toBeDefined();
|
|
105
|
+
|
|
106
|
+
// Simulate Git operation: add a new branch
|
|
107
|
+
const updatedBranches: BranchInfo[] = [
|
|
108
|
+
...mockBranches,
|
|
109
|
+
{
|
|
110
|
+
name: 'feature/test-2',
|
|
111
|
+
branchType: 'feature',
|
|
112
|
+
type: 'local',
|
|
113
|
+
isCurrent: false,
|
|
114
|
+
},
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
mockUseGitData.mockReturnValue({
|
|
118
|
+
branches: updatedBranches,
|
|
119
|
+
worktrees: [],
|
|
120
|
+
loading: false,
|
|
121
|
+
error: null,
|
|
122
|
+
refresh: mockRefresh,
|
|
123
|
+
lastUpdated: new Date(),
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Re-render to simulate update
|
|
127
|
+
rerender(<App onExit={onExit} />);
|
|
128
|
+
|
|
129
|
+
// Should now show "Local: 3"
|
|
130
|
+
expect(getByText('3')).toBeDefined();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('T086: should update statistics after Worktree creation', () => {
|
|
134
|
+
const mockBranches: BranchInfo[] = [
|
|
135
|
+
{
|
|
136
|
+
name: 'main',
|
|
137
|
+
branchType: 'main',
|
|
138
|
+
type: 'local',
|
|
139
|
+
isCurrent: true,
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: 'feature/test-1',
|
|
143
|
+
branchType: 'feature',
|
|
144
|
+
type: 'local',
|
|
145
|
+
isCurrent: false,
|
|
146
|
+
},
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
mockUseGitData.mockReturnValue({
|
|
150
|
+
branches: mockBranches,
|
|
151
|
+
worktrees: [],
|
|
152
|
+
loading: false,
|
|
153
|
+
error: null,
|
|
154
|
+
refresh: mockRefresh,
|
|
155
|
+
lastUpdated: new Date(),
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const onExit = vi.fn();
|
|
159
|
+
const { container, getByText, rerender } = render(<App onExit={onExit} />);
|
|
160
|
+
|
|
161
|
+
// Initial state should show "Worktrees: 0"
|
|
162
|
+
expect(getByText(/Worktrees:/i)).toBeDefined();
|
|
163
|
+
// Verify the content contains Worktrees: 0
|
|
164
|
+
expect(container.textContent).toContain('Worktrees');
|
|
165
|
+
|
|
166
|
+
// Simulate Worktree creation
|
|
167
|
+
const branchesWithWorktree: BranchInfo[] = [
|
|
168
|
+
{
|
|
169
|
+
name: 'main',
|
|
170
|
+
branchType: 'main',
|
|
171
|
+
type: 'local',
|
|
172
|
+
isCurrent: true,
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
name: 'feature/test-1',
|
|
176
|
+
branchType: 'feature',
|
|
177
|
+
type: 'local',
|
|
178
|
+
isCurrent: false,
|
|
179
|
+
worktree: {
|
|
180
|
+
path: '/mock/worktree/feature-test-1',
|
|
181
|
+
branch: 'feature/test-1',
|
|
182
|
+
isAccessible: true,
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
mockUseGitData.mockReturnValue({
|
|
188
|
+
branches: branchesWithWorktree,
|
|
189
|
+
worktrees: [
|
|
190
|
+
{
|
|
191
|
+
path: '/mock/worktree/feature-test-1',
|
|
192
|
+
branch: 'feature/test-1',
|
|
193
|
+
isAccessible: true,
|
|
194
|
+
},
|
|
195
|
+
],
|
|
196
|
+
loading: false,
|
|
197
|
+
error: null,
|
|
198
|
+
refresh: mockRefresh,
|
|
199
|
+
lastUpdated: new Date(),
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// Re-render to simulate update
|
|
203
|
+
rerender(<App onExit={onExit} />);
|
|
204
|
+
|
|
205
|
+
// Should now show "Worktrees: 1"
|
|
206
|
+
expect(getByText(/Worktrees:/i)).toBeDefined();
|
|
207
|
+
// Verify worktree count increased by checking container content
|
|
208
|
+
expect(container.textContent).toContain('Worktrees');
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('should display lastUpdated timestamp', () => {
|
|
212
|
+
const mockBranches: BranchInfo[] = [
|
|
213
|
+
{
|
|
214
|
+
name: 'main',
|
|
215
|
+
branchType: 'main',
|
|
216
|
+
type: 'local',
|
|
217
|
+
isCurrent: true,
|
|
218
|
+
},
|
|
219
|
+
];
|
|
220
|
+
|
|
221
|
+
const lastUpdated = new Date();
|
|
222
|
+
mockUseGitData.mockReturnValue({
|
|
223
|
+
branches: mockBranches,
|
|
224
|
+
worktrees: [],
|
|
225
|
+
loading: false,
|
|
226
|
+
error: null,
|
|
227
|
+
refresh: mockRefresh,
|
|
228
|
+
lastUpdated,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const onExit = vi.fn();
|
|
232
|
+
const { getByText } = render(<App onExit={onExit} />);
|
|
233
|
+
|
|
234
|
+
// Should display "Updated:" text
|
|
235
|
+
expect(getByText(/Updated:/i)).toBeDefined();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('should handle refresh errors gracefully', () => {
|
|
239
|
+
const error = new Error('Git command failed');
|
|
240
|
+
mockUseGitData.mockReturnValue({
|
|
241
|
+
branches: [],
|
|
242
|
+
worktrees: [],
|
|
243
|
+
loading: false,
|
|
244
|
+
error,
|
|
245
|
+
refresh: mockRefresh,
|
|
246
|
+
lastUpdated: new Date(),
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const onExit = vi.fn();
|
|
250
|
+
const { getByText } = render(<App onExit={onExit} />);
|
|
251
|
+
|
|
252
|
+
// Should display error message
|
|
253
|
+
expect(getByText(/Error:/i)).toBeDefined();
|
|
254
|
+
expect(getByText(/Git command failed/i)).toBeDefined();
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* T082-3: Cursor position retention during auto-refresh
|
|
259
|
+
* Tests that cursor position is maintained when data is auto-refreshed
|
|
260
|
+
*/
|
|
261
|
+
describe('Cursor Position Retention (T082-3)', () => {
|
|
262
|
+
it('should maintain cursor position when branches data is refreshed with same content', () => {
|
|
263
|
+
const mockBranches: BranchInfo[] = [
|
|
264
|
+
{
|
|
265
|
+
name: 'main',
|
|
266
|
+
branchType: 'main',
|
|
267
|
+
type: 'local',
|
|
268
|
+
isCurrent: true,
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
name: 'feature/test-1',
|
|
272
|
+
branchType: 'feature',
|
|
273
|
+
type: 'local',
|
|
274
|
+
isCurrent: false,
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
name: 'feature/test-2',
|
|
278
|
+
branchType: 'feature',
|
|
279
|
+
type: 'local',
|
|
280
|
+
isCurrent: false,
|
|
281
|
+
},
|
|
282
|
+
];
|
|
283
|
+
|
|
284
|
+
mockUseGitData.mockReturnValue({
|
|
285
|
+
branches: mockBranches,
|
|
286
|
+
worktrees: [],
|
|
287
|
+
loading: false,
|
|
288
|
+
error: null,
|
|
289
|
+
refresh: mockRefresh,
|
|
290
|
+
lastUpdated: new Date(),
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
const onExit = vi.fn();
|
|
294
|
+
const { rerender } = render(<App onExit={onExit} />);
|
|
295
|
+
|
|
296
|
+
// Simulate user moving cursor down (this would be done via keyboard in real app)
|
|
297
|
+
// For now, we just verify that the component renders
|
|
298
|
+
|
|
299
|
+
// Create new array with same content (simulating auto-refresh)
|
|
300
|
+
const refreshedBranches: BranchInfo[] = [
|
|
301
|
+
{
|
|
302
|
+
name: 'main',
|
|
303
|
+
branchType: 'main',
|
|
304
|
+
type: 'local',
|
|
305
|
+
isCurrent: true,
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
name: 'feature/test-1',
|
|
309
|
+
branchType: 'feature',
|
|
310
|
+
type: 'local',
|
|
311
|
+
isCurrent: false,
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
name: 'feature/test-2',
|
|
315
|
+
branchType: 'feature',
|
|
316
|
+
type: 'local',
|
|
317
|
+
isCurrent: false,
|
|
318
|
+
},
|
|
319
|
+
];
|
|
320
|
+
|
|
321
|
+
mockUseGitData.mockReturnValue({
|
|
322
|
+
branches: refreshedBranches,
|
|
323
|
+
worktrees: [],
|
|
324
|
+
loading: false,
|
|
325
|
+
error: null,
|
|
326
|
+
refresh: mockRefresh,
|
|
327
|
+
lastUpdated: new Date(),
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// Re-render to simulate auto-refresh
|
|
331
|
+
rerender(<App onExit={onExit} />);
|
|
332
|
+
|
|
333
|
+
// With proper optimization:
|
|
334
|
+
// 1. useMemo should not regenerate branchItems (content is the same)
|
|
335
|
+
// 2. Select should not re-render (items prop hasn't changed)
|
|
336
|
+
// 3. Cursor position should be maintained
|
|
337
|
+
|
|
338
|
+
// Without optimization:
|
|
339
|
+
// - branchItems would be regenerated
|
|
340
|
+
// - Select would re-render
|
|
341
|
+
// - Cursor position might be reset
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('should maintain cursor position when a branch is added at the end', () => {
|
|
345
|
+
const initialBranches: BranchInfo[] = [
|
|
346
|
+
{
|
|
347
|
+
name: 'main',
|
|
348
|
+
branchType: 'main',
|
|
349
|
+
type: 'local',
|
|
350
|
+
isCurrent: true,
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
name: 'feature/test-1',
|
|
354
|
+
branchType: 'feature',
|
|
355
|
+
type: 'local',
|
|
356
|
+
isCurrent: false,
|
|
357
|
+
},
|
|
358
|
+
];
|
|
359
|
+
|
|
360
|
+
mockUseGitData.mockReturnValue({
|
|
361
|
+
branches: initialBranches,
|
|
362
|
+
worktrees: [],
|
|
363
|
+
loading: false,
|
|
364
|
+
error: null,
|
|
365
|
+
refresh: mockRefresh,
|
|
366
|
+
lastUpdated: new Date(),
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
const onExit = vi.fn();
|
|
370
|
+
const { rerender } = render(<App onExit={onExit} />);
|
|
371
|
+
|
|
372
|
+
// Add a branch at the end (cursor should stay on current item)
|
|
373
|
+
const updatedBranches: BranchInfo[] = [
|
|
374
|
+
...initialBranches,
|
|
375
|
+
{
|
|
376
|
+
name: 'feature/test-2',
|
|
377
|
+
branchType: 'feature',
|
|
378
|
+
type: 'local',
|
|
379
|
+
isCurrent: false,
|
|
380
|
+
},
|
|
381
|
+
];
|
|
382
|
+
|
|
383
|
+
mockUseGitData.mockReturnValue({
|
|
384
|
+
branches: updatedBranches,
|
|
385
|
+
worktrees: [],
|
|
386
|
+
loading: false,
|
|
387
|
+
error: null,
|
|
388
|
+
refresh: mockRefresh,
|
|
389
|
+
lastUpdated: new Date(),
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
rerender(<App onExit={onExit} />);
|
|
393
|
+
|
|
394
|
+
// Cursor should remain on the same item (e.g., index 1 should still point to 'feature/test-1')
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it('should adjust cursor position when current selected branch is deleted', () => {
|
|
398
|
+
const initialBranches: BranchInfo[] = [
|
|
399
|
+
{
|
|
400
|
+
name: 'main',
|
|
401
|
+
branchType: 'main',
|
|
402
|
+
type: 'local',
|
|
403
|
+
isCurrent: true,
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
name: 'feature/test-1',
|
|
407
|
+
branchType: 'feature',
|
|
408
|
+
type: 'local',
|
|
409
|
+
isCurrent: false,
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
name: 'feature/test-2',
|
|
413
|
+
branchType: 'feature',
|
|
414
|
+
type: 'local',
|
|
415
|
+
isCurrent: false,
|
|
416
|
+
},
|
|
417
|
+
];
|
|
418
|
+
|
|
419
|
+
mockUseGitData.mockReturnValue({
|
|
420
|
+
branches: initialBranches,
|
|
421
|
+
worktrees: [],
|
|
422
|
+
loading: false,
|
|
423
|
+
error: null,
|
|
424
|
+
refresh: mockRefresh,
|
|
425
|
+
lastUpdated: new Date(),
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
const onExit = vi.fn();
|
|
429
|
+
const { rerender } = render(<App onExit={onExit} />);
|
|
430
|
+
|
|
431
|
+
// Remove middle branch (cursor was on index 1, which is now deleted)
|
|
432
|
+
const updatedBranches: BranchInfo[] = [
|
|
433
|
+
{
|
|
434
|
+
name: 'main',
|
|
435
|
+
branchType: 'main',
|
|
436
|
+
type: 'local',
|
|
437
|
+
isCurrent: true,
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
name: 'feature/test-2',
|
|
441
|
+
branchType: 'feature',
|
|
442
|
+
type: 'local',
|
|
443
|
+
isCurrent: false,
|
|
444
|
+
},
|
|
445
|
+
];
|
|
446
|
+
|
|
447
|
+
mockUseGitData.mockReturnValue({
|
|
448
|
+
branches: updatedBranches,
|
|
449
|
+
worktrees: [],
|
|
450
|
+
loading: false,
|
|
451
|
+
error: null,
|
|
452
|
+
refresh: mockRefresh,
|
|
453
|
+
lastUpdated: new Date(),
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
rerender(<App onExit={onExit} />);
|
|
457
|
+
|
|
458
|
+
// Cursor should be clamped to valid index (e.g., moved to index 1, which is now 'feature/test-2')
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it('should maintain scroll offset during auto-refresh', () => {
|
|
462
|
+
// Create many branches to test scrolling
|
|
463
|
+
const manyBranches: BranchInfo[] = Array.from({ length: 20 }, (_, i) => ({
|
|
464
|
+
name: `feature/test-${i + 1}`,
|
|
465
|
+
branchType: 'feature' as const,
|
|
466
|
+
type: 'local' as const,
|
|
467
|
+
isCurrent: false,
|
|
468
|
+
}));
|
|
469
|
+
|
|
470
|
+
mockUseGitData.mockReturnValue({
|
|
471
|
+
branches: manyBranches,
|
|
472
|
+
worktrees: [],
|
|
473
|
+
loading: false,
|
|
474
|
+
error: null,
|
|
475
|
+
refresh: mockRefresh,
|
|
476
|
+
lastUpdated: new Date(),
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
const onExit = vi.fn();
|
|
480
|
+
const { rerender } = render(<App onExit={onExit} />);
|
|
481
|
+
|
|
482
|
+
// Simulate auto-refresh with same content
|
|
483
|
+
const refreshedBranches: BranchInfo[] = Array.from({ length: 20 }, (_, i) => ({
|
|
484
|
+
name: `feature/test-${i + 1}`,
|
|
485
|
+
branchType: 'feature' as const,
|
|
486
|
+
type: 'local' as const,
|
|
487
|
+
isCurrent: false,
|
|
488
|
+
}));
|
|
489
|
+
|
|
490
|
+
mockUseGitData.mockReturnValue({
|
|
491
|
+
branches: refreshedBranches,
|
|
492
|
+
worktrees: [],
|
|
493
|
+
loading: false,
|
|
494
|
+
error: null,
|
|
495
|
+
refresh: mockRefresh,
|
|
496
|
+
lastUpdated: new Date(),
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
rerender(<App onExit={onExit} />);
|
|
500
|
+
|
|
501
|
+
// Scroll offset should be maintained
|
|
502
|
+
// (in real app, user might be viewing items 10-20, and auto-refresh shouldn't reset to top)
|
|
503
|
+
});
|
|
504
|
+
});
|
|
505
|
+
});
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment happy-dom
|
|
3
|
+
* Integration tests for realtime update functionality
|
|
4
|
+
*/
|
|
5
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
6
|
+
import { renderHook, waitFor } from '@testing-library/react';
|
|
7
|
+
import { useGitData } from '../../hooks/useGitData.js';
|
|
8
|
+
import { Window } from 'happy-dom';
|
|
9
|
+
import type { BranchInfo } from '../../types.js';
|
|
10
|
+
|
|
11
|
+
// Mock git.js and worktree.js
|
|
12
|
+
vi.mock('../../../git.js', () => ({
|
|
13
|
+
getAllBranches: vi.fn(),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
vi.mock('../../../worktree.js', () => ({
|
|
17
|
+
listAdditionalWorktrees: vi.fn(),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
import { getAllBranches } from '../../../git.js';
|
|
21
|
+
import { listAdditionalWorktrees } from '../../../worktree.js';
|
|
22
|
+
|
|
23
|
+
describe('Realtime Update Integration Tests', () => {
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
// Setup happy-dom
|
|
26
|
+
const window = new Window();
|
|
27
|
+
globalThis.window = window as any;
|
|
28
|
+
globalThis.document = window.document as any;
|
|
29
|
+
|
|
30
|
+
// Reset mocks
|
|
31
|
+
(getAllBranches as ReturnType<typeof vi.fn>).mockReset();
|
|
32
|
+
(listAdditionalWorktrees as ReturnType<typeof vi.fn>).mockReset();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const mockBranches: BranchInfo[] = [
|
|
36
|
+
{
|
|
37
|
+
name: 'main',
|
|
38
|
+
type: 'local',
|
|
39
|
+
branchType: 'main',
|
|
40
|
+
isCurrent: true,
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
it('should update lastUpdated timestamp after data refresh', async () => {
|
|
45
|
+
(getAllBranches as ReturnType<typeof vi.fn>).mockResolvedValue(mockBranches);
|
|
46
|
+
(listAdditionalWorktrees as ReturnType<typeof vi.fn>).mockResolvedValue([]);
|
|
47
|
+
|
|
48
|
+
const { result } = renderHook(() => useGitData());
|
|
49
|
+
|
|
50
|
+
// Wait for initial load
|
|
51
|
+
await waitFor(() => {
|
|
52
|
+
expect(result.current.loading).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const firstUpdated = result.current.lastUpdated;
|
|
56
|
+
expect(firstUpdated).toBeInstanceOf(Date);
|
|
57
|
+
|
|
58
|
+
// Wait to ensure timestamp difference (increased from 50ms to 100ms)
|
|
59
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
60
|
+
|
|
61
|
+
// Trigger manual refresh
|
|
62
|
+
result.current.refresh();
|
|
63
|
+
|
|
64
|
+
await waitFor(() => {
|
|
65
|
+
expect(result.current.loading).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const secondUpdated = result.current.lastUpdated;
|
|
69
|
+
expect(secondUpdated).toBeInstanceOf(Date);
|
|
70
|
+
// Use greaterThanOrEqual to handle rare cases where timestamps are identical
|
|
71
|
+
expect(secondUpdated!.getTime()).toBeGreaterThanOrEqual(firstUpdated!.getTime());
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should maintain data consistency during auto-refresh', async () => {
|
|
75
|
+
let callCount = 0;
|
|
76
|
+
(getAllBranches as ReturnType<typeof vi.fn>).mockImplementation(async () => {
|
|
77
|
+
callCount++;
|
|
78
|
+
if (callCount === 1) {
|
|
79
|
+
return mockBranches;
|
|
80
|
+
}
|
|
81
|
+
// Return updated branches on subsequent calls
|
|
82
|
+
return [
|
|
83
|
+
...mockBranches,
|
|
84
|
+
{
|
|
85
|
+
name: 'feature/new',
|
|
86
|
+
type: 'local',
|
|
87
|
+
branchType: 'feature',
|
|
88
|
+
isCurrent: false,
|
|
89
|
+
},
|
|
90
|
+
];
|
|
91
|
+
});
|
|
92
|
+
(listAdditionalWorktrees as ReturnType<typeof vi.fn>).mockResolvedValue([]);
|
|
93
|
+
|
|
94
|
+
const { result } = renderHook(() =>
|
|
95
|
+
useGitData({ enableAutoRefresh: true, refreshInterval: 100 })
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
// Wait for initial load
|
|
99
|
+
await waitFor(() => {
|
|
100
|
+
expect(result.current.loading).toBe(false);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
expect(result.current.branches).toHaveLength(1);
|
|
104
|
+
|
|
105
|
+
// Wait for auto-refresh to trigger
|
|
106
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
107
|
+
|
|
108
|
+
await waitFor(() => {
|
|
109
|
+
expect(result.current.branches).toHaveLength(2);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Verify data integrity
|
|
113
|
+
expect(result.current.branches[0].name).toBe('main');
|
|
114
|
+
expect(result.current.branches[1].name).toBe('feature/new');
|
|
115
|
+
expect(result.current.error).toBeNull();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should handle errors during auto-refresh gracefully', async () => {
|
|
119
|
+
let callCount = 0;
|
|
120
|
+
(getAllBranches as ReturnType<typeof vi.fn>).mockImplementation(async () => {
|
|
121
|
+
callCount++;
|
|
122
|
+
if (callCount === 1) {
|
|
123
|
+
return mockBranches;
|
|
124
|
+
}
|
|
125
|
+
// Simulate error on second call
|
|
126
|
+
throw new Error('Network error');
|
|
127
|
+
});
|
|
128
|
+
(listAdditionalWorktrees as ReturnType<typeof vi.fn>).mockResolvedValue([]);
|
|
129
|
+
|
|
130
|
+
const { result } = renderHook(() =>
|
|
131
|
+
useGitData({ enableAutoRefresh: true, refreshInterval: 100 })
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
// Wait for initial load
|
|
135
|
+
await waitFor(() => {
|
|
136
|
+
expect(result.current.loading).toBe(false);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
expect(result.current.branches).toHaveLength(1);
|
|
140
|
+
expect(result.current.error).toBeNull();
|
|
141
|
+
|
|
142
|
+
// Wait for auto-refresh to trigger and fail
|
|
143
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
144
|
+
|
|
145
|
+
await waitFor(() => {
|
|
146
|
+
expect(result.current.error).not.toBeNull();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
expect(result.current.error?.message).toBe('Network error');
|
|
150
|
+
// Data should be cleared on error
|
|
151
|
+
expect(result.current.branches).toEqual([]);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should stop auto-refresh when component unmounts', async () => {
|
|
155
|
+
(getAllBranches as ReturnType<typeof vi.fn>).mockResolvedValue(mockBranches);
|
|
156
|
+
(listAdditionalWorktrees as ReturnType<typeof vi.fn>).mockResolvedValue([]);
|
|
157
|
+
|
|
158
|
+
const { result, unmount } = renderHook(() =>
|
|
159
|
+
useGitData({ enableAutoRefresh: true, refreshInterval: 100 })
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
// Wait for initial load
|
|
163
|
+
await waitFor(() => {
|
|
164
|
+
expect(result.current.loading).toBe(false);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const initialCallCount = (getAllBranches as ReturnType<typeof vi.fn>).mock.calls.length;
|
|
168
|
+
|
|
169
|
+
// Unmount the hook
|
|
170
|
+
unmount();
|
|
171
|
+
|
|
172
|
+
// Wait longer than refresh interval
|
|
173
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
174
|
+
|
|
175
|
+
// Call count should not increase after unmount
|
|
176
|
+
const finalCallCount = (getAllBranches as ReturnType<typeof vi.fn>).mock.calls.length;
|
|
177
|
+
expect(finalCallCount).toBe(initialCallCount);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should update statistics in real-time', async () => {
|
|
181
|
+
let callCount = 0;
|
|
182
|
+
(getAllBranches as ReturnType<typeof vi.fn>).mockImplementation(async () => {
|
|
183
|
+
callCount++;
|
|
184
|
+
if (callCount === 1) {
|
|
185
|
+
return [mockBranches[0]];
|
|
186
|
+
}
|
|
187
|
+
// Return more branches on subsequent calls
|
|
188
|
+
return [
|
|
189
|
+
mockBranches[0],
|
|
190
|
+
{ name: 'feature/a', type: 'local', branchType: 'feature', isCurrent: false },
|
|
191
|
+
{ name: 'feature/b', type: 'local', branchType: 'feature', isCurrent: false },
|
|
192
|
+
];
|
|
193
|
+
});
|
|
194
|
+
(listAdditionalWorktrees as ReturnType<typeof vi.fn>).mockResolvedValue([]);
|
|
195
|
+
|
|
196
|
+
const { result } = renderHook(() =>
|
|
197
|
+
useGitData({ enableAutoRefresh: true, refreshInterval: 100 })
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
// Wait for initial load
|
|
201
|
+
await waitFor(() => {
|
|
202
|
+
expect(result.current.loading).toBe(false);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
expect(result.current.branches).toHaveLength(1);
|
|
206
|
+
|
|
207
|
+
// Wait for auto-refresh
|
|
208
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
209
|
+
|
|
210
|
+
await waitFor(() => {
|
|
211
|
+
expect(result.current.branches).toHaveLength(3);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
expect(result.current.lastUpdated).toBeInstanceOf(Date);
|
|
215
|
+
});
|
|
216
|
+
});
|