@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,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment happy-dom
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, beforeEach, afterEach, afterAll, vi } from 'vitest';
|
|
5
|
+
import type { Mock } from 'vitest';
|
|
6
|
+
import { render, act, waitFor } from '@testing-library/react';
|
|
7
|
+
import React from 'react';
|
|
8
|
+
import type { BranchItem, CleanupTarget } from '../../types.js';
|
|
9
|
+
import { Window } from 'happy-dom';
|
|
10
|
+
import * as useGitDataModule from '../../hooks/useGitData.js';
|
|
11
|
+
import * as useScreenStateModule from '../../hooks/useScreenState.js';
|
|
12
|
+
import * as WorktreeManagerScreenModule from '../../components/screens/WorktreeManagerScreen.js';
|
|
13
|
+
import * as BranchCreatorScreenModule from '../../components/screens/BranchCreatorScreen.js';
|
|
14
|
+
import * as BranchListScreenModule from '../../components/screens/BranchListScreen.js';
|
|
15
|
+
import * as worktreeModule from '../../../../worktree.ts';
|
|
16
|
+
import * as gitModule from '../../../../git.ts';
|
|
17
|
+
import { App } from '../../components/App.js';
|
|
18
|
+
|
|
19
|
+
const navigateToMock = vi.fn();
|
|
20
|
+
const goBackMock = vi.fn();
|
|
21
|
+
const resetMock = vi.fn();
|
|
22
|
+
|
|
23
|
+
const worktreeScreenProps: any[] = [];
|
|
24
|
+
const branchCreatorProps: any[] = [];
|
|
25
|
+
const branchListProps: any[] = [];
|
|
26
|
+
|
|
27
|
+
const originalUseGitData = useGitDataModule.useGitData;
|
|
28
|
+
const originalUseScreenState = useScreenStateModule.useScreenState;
|
|
29
|
+
const originalWorktreeManagerScreen = WorktreeManagerScreenModule.WorktreeManagerScreen;
|
|
30
|
+
const originalBranchCreatorScreen = BranchCreatorScreenModule.BranchCreatorScreen;
|
|
31
|
+
const originalBranchListScreen = BranchListScreenModule.BranchListScreen;
|
|
32
|
+
const originalGetMergedPRWorktrees = worktreeModule.getMergedPRWorktrees;
|
|
33
|
+
const originalGenerateWorktreePath = worktreeModule.generateWorktreePath;
|
|
34
|
+
const originalCreateWorktree = worktreeModule.createWorktree;
|
|
35
|
+
const originalRemoveWorktree = worktreeModule.removeWorktree;
|
|
36
|
+
const originalGetRepositoryRoot = gitModule.getRepositoryRoot;
|
|
37
|
+
const originalDeleteBranch = gitModule.deleteBranch;
|
|
38
|
+
|
|
39
|
+
const useGitDataSpy = vi.spyOn(useGitDataModule, 'useGitData');
|
|
40
|
+
const useScreenStateSpy = vi.spyOn(useScreenStateModule, 'useScreenState');
|
|
41
|
+
const worktreeManagerScreenSpy = vi.spyOn(WorktreeManagerScreenModule, 'WorktreeManagerScreen');
|
|
42
|
+
const branchCreatorScreenSpy = vi.spyOn(BranchCreatorScreenModule, 'BranchCreatorScreen');
|
|
43
|
+
const branchListScreenSpy = vi.spyOn(BranchListScreenModule, 'BranchListScreen');
|
|
44
|
+
const getMergedPRWorktreesSpy = vi.spyOn(worktreeModule, 'getMergedPRWorktrees');
|
|
45
|
+
const generateWorktreePathSpy = vi.spyOn(worktreeModule, 'generateWorktreePath');
|
|
46
|
+
const createWorktreeSpy = vi.spyOn(worktreeModule, 'createWorktree');
|
|
47
|
+
const removeWorktreeSpy = vi.spyOn(worktreeModule, 'removeWorktree');
|
|
48
|
+
const getRepositoryRootSpy = vi.spyOn(gitModule, 'getRepositoryRoot');
|
|
49
|
+
const deleteBranchSpy = vi.spyOn(gitModule, 'deleteBranch');
|
|
50
|
+
|
|
51
|
+
describe('App shortcuts integration', () => {
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
if (typeof globalThis.document === 'undefined') {
|
|
54
|
+
const window = new Window();
|
|
55
|
+
globalThis.window = window as any;
|
|
56
|
+
globalThis.document = window.document as any;
|
|
57
|
+
}
|
|
58
|
+
worktreeScreenProps.length = 0;
|
|
59
|
+
branchCreatorProps.length = 0;
|
|
60
|
+
branchListProps.length = 0;
|
|
61
|
+
navigateToMock.mockClear();
|
|
62
|
+
goBackMock.mockClear();
|
|
63
|
+
resetMock.mockClear();
|
|
64
|
+
useGitDataSpy.mockImplementation(() => ({
|
|
65
|
+
branches: [],
|
|
66
|
+
worktrees: [
|
|
67
|
+
{
|
|
68
|
+
branch: 'feature/existing',
|
|
69
|
+
path: '/worktrees/feature-existing',
|
|
70
|
+
isAccessible: true,
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
loading: false,
|
|
74
|
+
error: null,
|
|
75
|
+
refresh: vi.fn(),
|
|
76
|
+
lastUpdated: null,
|
|
77
|
+
}));
|
|
78
|
+
useScreenStateSpy.mockImplementation(() => ({
|
|
79
|
+
currentScreen: 'worktree-manager',
|
|
80
|
+
navigateTo: navigateToMock as _Mock,
|
|
81
|
+
goBack: goBackMock as _Mock,
|
|
82
|
+
reset: resetMock as _Mock,
|
|
83
|
+
}));
|
|
84
|
+
worktreeManagerScreenSpy.mockImplementation((props: any) => {
|
|
85
|
+
worktreeScreenProps.push(props);
|
|
86
|
+
return React.createElement(originalWorktreeManagerScreen, props);
|
|
87
|
+
});
|
|
88
|
+
branchCreatorScreenSpy.mockImplementation((props: any) => {
|
|
89
|
+
branchCreatorProps.push(props);
|
|
90
|
+
return React.createElement(originalBranchCreatorScreen, props);
|
|
91
|
+
});
|
|
92
|
+
branchListScreenSpy.mockImplementation((props: any) => {
|
|
93
|
+
branchListProps.push(props);
|
|
94
|
+
return React.createElement(originalBranchListScreen, props);
|
|
95
|
+
});
|
|
96
|
+
getMergedPRWorktreesSpy.mockResolvedValue([
|
|
97
|
+
{
|
|
98
|
+
branch: 'feature/add-new-feature',
|
|
99
|
+
cleanupType: 'worktree-and-branch',
|
|
100
|
+
pullRequest: {
|
|
101
|
+
number: 123,
|
|
102
|
+
title: 'Add new feature',
|
|
103
|
+
branch: 'feature/add-new-feature',
|
|
104
|
+
mergedAt: '2025-01-20T10:00:00Z',
|
|
105
|
+
author: 'user1',
|
|
106
|
+
},
|
|
107
|
+
worktreePath: '/worktrees/feature-add-new-feature',
|
|
108
|
+
hasUncommittedChanges: false,
|
|
109
|
+
hasUnpushedCommits: false,
|
|
110
|
+
hasRemoteBranch: true,
|
|
111
|
+
isAccessible: true,
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
branch: 'hotfix/urgent-fix',
|
|
115
|
+
cleanupType: 'worktree-and-branch',
|
|
116
|
+
pullRequest: {
|
|
117
|
+
number: 456,
|
|
118
|
+
title: 'Urgent fix',
|
|
119
|
+
branch: 'hotfix/urgent-fix',
|
|
120
|
+
mergedAt: '2025-01-21T09:00:00Z',
|
|
121
|
+
author: 'user2',
|
|
122
|
+
},
|
|
123
|
+
worktreePath: '/worktrees/hotfix-urgent-fix',
|
|
124
|
+
hasUncommittedChanges: true,
|
|
125
|
+
hasUnpushedCommits: false,
|
|
126
|
+
hasRemoteBranch: true,
|
|
127
|
+
isAccessible: true,
|
|
128
|
+
},
|
|
129
|
+
] as CleanupTarget[]);
|
|
130
|
+
generateWorktreePathSpy.mockResolvedValue('/worktrees/new-branch');
|
|
131
|
+
createWorktreeSpy.mockResolvedValue(undefined);
|
|
132
|
+
removeWorktreeSpy.mockResolvedValue(undefined);
|
|
133
|
+
getRepositoryRootSpy.mockResolvedValue('/repo');
|
|
134
|
+
deleteBranchSpy.mockResolvedValue(undefined);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
afterEach(() => {
|
|
138
|
+
useGitDataSpy.mockReset();
|
|
139
|
+
useScreenStateSpy.mockReset();
|
|
140
|
+
worktreeManagerScreenSpy.mockReset();
|
|
141
|
+
branchCreatorScreenSpy.mockReset();
|
|
142
|
+
branchListScreenSpy.mockReset();
|
|
143
|
+
getMergedPRWorktreesSpy.mockReset();
|
|
144
|
+
generateWorktreePathSpy.mockReset();
|
|
145
|
+
createWorktreeSpy.mockReset();
|
|
146
|
+
removeWorktreeSpy.mockReset();
|
|
147
|
+
getRepositoryRootSpy.mockReset();
|
|
148
|
+
deleteBranchSpy.mockReset();
|
|
149
|
+
useGitDataSpy.mockImplementation(originalUseGitData);
|
|
150
|
+
useScreenStateSpy.mockImplementation(originalUseScreenState);
|
|
151
|
+
worktreeManagerScreenSpy.mockImplementation(originalWorktreeManagerScreen as any);
|
|
152
|
+
branchCreatorScreenSpy.mockImplementation(originalBranchCreatorScreen as any);
|
|
153
|
+
branchListScreenSpy.mockImplementation(originalBranchListScreen as any);
|
|
154
|
+
getMergedPRWorktreesSpy.mockImplementation(originalGetMergedPRWorktrees as any);
|
|
155
|
+
generateWorktreePathSpy.mockImplementation(originalGenerateWorktreePath as any);
|
|
156
|
+
createWorktreeSpy.mockImplementation(originalCreateWorktree as any);
|
|
157
|
+
removeWorktreeSpy.mockImplementation(originalRemoveWorktree as any);
|
|
158
|
+
getRepositoryRootSpy.mockImplementation(originalGetRepositoryRoot as any);
|
|
159
|
+
deleteBranchSpy.mockImplementation(originalDeleteBranch as any);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('navigates to AI tool selector when worktree is selected', () => {
|
|
163
|
+
const onExit = vi.fn();
|
|
164
|
+
render(<App onExit={onExit} />);
|
|
165
|
+
|
|
166
|
+
expect(worktreeScreenProps).not.toHaveLength(0);
|
|
167
|
+
const { onSelect, worktrees } = worktreeScreenProps[0];
|
|
168
|
+
expect(worktrees).toHaveLength(1);
|
|
169
|
+
|
|
170
|
+
onSelect(worktrees[0]);
|
|
171
|
+
|
|
172
|
+
expect(navigateToMock).toHaveBeenCalledWith('ai-tool-selector');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('creates new worktree when branch creator submits', async () => {
|
|
176
|
+
const onExit = vi.fn();
|
|
177
|
+
|
|
178
|
+
// Update screen state mock to branch-creator for this test
|
|
179
|
+
useScreenStateSpy.mockReturnValue({
|
|
180
|
+
currentScreen: 'branch-creator',
|
|
181
|
+
navigateTo: navigateToMock as _Mock,
|
|
182
|
+
goBack: goBackMock as _Mock,
|
|
183
|
+
reset: resetMock as _Mock,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
render(<App onExit={onExit} />);
|
|
187
|
+
|
|
188
|
+
expect(branchCreatorProps).not.toHaveLength(0);
|
|
189
|
+
const { onCreate } = branchCreatorProps[0];
|
|
190
|
+
|
|
191
|
+
await act(async () => {
|
|
192
|
+
await onCreate('feature/new-branch');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
expect(createWorktreeSpy).toHaveBeenCalledWith(
|
|
196
|
+
expect.objectContaining({
|
|
197
|
+
branchName: 'feature/new-branch',
|
|
198
|
+
isNewBranch: true,
|
|
199
|
+
})
|
|
200
|
+
);
|
|
201
|
+
expect(navigateToMock).toHaveBeenCalledWith('ai-tool-selector');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('displays per-branch cleanup indicators and waits before clearing results', async () => {
|
|
205
|
+
vi.useFakeTimers();
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
const onExit = vi.fn();
|
|
209
|
+
|
|
210
|
+
let resolveRemoveWorktree: (() => void) | undefined;
|
|
211
|
+
let resolveDeleteBranch: (() => void) | undefined;
|
|
212
|
+
|
|
213
|
+
removeWorktreeSpy.mockImplementationOnce(
|
|
214
|
+
() =>
|
|
215
|
+
new Promise<void>((resolve) => {
|
|
216
|
+
resolveRemoveWorktree = resolve;
|
|
217
|
+
})
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
deleteBranchSpy.mockImplementationOnce(
|
|
221
|
+
() =>
|
|
222
|
+
new Promise<void>((resolve) => {
|
|
223
|
+
resolveDeleteBranch = resolve;
|
|
224
|
+
})
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
useScreenStateSpy.mockReturnValue({
|
|
228
|
+
currentScreen: 'branch-list',
|
|
229
|
+
navigateTo: navigateToMock as _Mock,
|
|
230
|
+
goBack: goBackMock as _Mock,
|
|
231
|
+
reset: resetMock as _Mock,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
render(<App onExit={onExit} />);
|
|
235
|
+
|
|
236
|
+
expect(branchListProps).not.toHaveLength(0);
|
|
237
|
+
const initialProps = branchListProps.at(-1);
|
|
238
|
+
expect(initialProps).toBeDefined();
|
|
239
|
+
if (!initialProps) {
|
|
240
|
+
throw new Error('BranchListScreen props missing');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
act(() => {
|
|
244
|
+
initialProps.onCleanupCommand?.();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
await act(async () => {
|
|
248
|
+
await Promise.resolve();
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
let latestProps = branchListProps.at(-1);
|
|
252
|
+
expect(latestProps?.cleanupUI?.inputLocked).toBe(true);
|
|
253
|
+
expect(latestProps?.cleanupUI?.footerMessage?.text).toBeTruthy();
|
|
254
|
+
expect(latestProps?.cleanupUI?.indicators).toMatchObject({
|
|
255
|
+
'feature/add-new-feature': expect.objectContaining({ icon: expect.stringMatching(/⠋|⠙|⠹|⠸|⠼|⠴|⠦|⠧/) }),
|
|
256
|
+
'hotfix/urgent-fix': expect.objectContaining({ icon: '⏳' }),
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
resolveRemoveWorktree?.();
|
|
260
|
+
|
|
261
|
+
await act(async () => {
|
|
262
|
+
await Promise.resolve();
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
resolveDeleteBranch?.();
|
|
266
|
+
|
|
267
|
+
expect(removeWorktreeSpy).toHaveBeenCalledWith(
|
|
268
|
+
'/worktrees/feature-add-new-feature',
|
|
269
|
+
true
|
|
270
|
+
);
|
|
271
|
+
expect(deleteBranchSpy).toHaveBeenCalledWith('feature/add-new-feature', true);
|
|
272
|
+
|
|
273
|
+
// Flush state updates after processing first target
|
|
274
|
+
await act(async () => {
|
|
275
|
+
await Promise.resolve();
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
latestProps = branchListProps.at(-1);
|
|
279
|
+
expect(latestProps?.cleanupUI?.indicators).toMatchObject({
|
|
280
|
+
'feature/add-new-feature': { icon: '✅' },
|
|
281
|
+
'hotfix/urgent-fix': { icon: '⏭️' },
|
|
282
|
+
});
|
|
283
|
+
expect(latestProps?.cleanupUI?.inputLocked).toBe(false);
|
|
284
|
+
|
|
285
|
+
// Advance 3 seconds to allow UI to clear
|
|
286
|
+
await act(async () => {
|
|
287
|
+
vi.advanceTimersByTime(3000);
|
|
288
|
+
await Promise.resolve();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
latestProps = branchListProps.at(-1);
|
|
292
|
+
expect(latestProps?.cleanupUI?.indicators).toEqual({});
|
|
293
|
+
expect(latestProps?.cleanupUI?.inputLocked).toBe(false);
|
|
294
|
+
expect(latestProps?.branches?.some((branch: BranchItem) => branch.name === 'feature/add-new-feature')).toBe(false);
|
|
295
|
+
} finally {
|
|
296
|
+
vi.useRealTimers();
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
afterAll(() => {
|
|
302
|
+
useGitDataSpy.mockRestore();
|
|
303
|
+
useScreenStateSpy.mockRestore();
|
|
304
|
+
worktreeManagerScreenSpy.mockRestore();
|
|
305
|
+
branchCreatorScreenSpy.mockRestore();
|
|
306
|
+
branchListScreenSpy.mockRestore();
|
|
307
|
+
getMergedPRWorktreesSpy.mockRestore();
|
|
308
|
+
generateWorktreePathSpy.mockRestore();
|
|
309
|
+
createWorktreeSpy.mockRestore();
|
|
310
|
+
removeWorktreeSpy.mockRestore();
|
|
311
|
+
getRepositoryRootSpy.mockRestore();
|
|
312
|
+
deleteBranchSpy.mockRestore();
|
|
313
|
+
});
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment happy-dom
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, beforeEach, afterEach, afterAll, vi } from 'vitest';
|
|
5
|
+
import { act, 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
|
+
import * as useGitDataModule from '../../hooks/useGitData.js';
|
|
11
|
+
|
|
12
|
+
const mockRefresh = vi.fn();
|
|
13
|
+
const originalUseGitData = useGitDataModule.useGitData;
|
|
14
|
+
const useGitDataSpy = vi.spyOn(useGitDataModule, 'useGitData');
|
|
15
|
+
|
|
16
|
+
describe('App', () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
// Setup happy-dom
|
|
19
|
+
const window = new Window();
|
|
20
|
+
globalThis.window = window as any;
|
|
21
|
+
globalThis.document = window.document as any;
|
|
22
|
+
|
|
23
|
+
vi.clearAllMocks();
|
|
24
|
+
useGitDataSpy.mockReset();
|
|
25
|
+
useGitDataSpy.mockImplementation(originalUseGitData);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const mockBranches: BranchInfo[] = [
|
|
29
|
+
{
|
|
30
|
+
name: 'main',
|
|
31
|
+
type: 'local',
|
|
32
|
+
branchType: 'main',
|
|
33
|
+
isCurrent: true,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'feature/test',
|
|
37
|
+
type: 'local',
|
|
38
|
+
branchType: 'feature',
|
|
39
|
+
isCurrent: false,
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
it('should render BranchListScreen when data is loaded', () => {
|
|
44
|
+
useGitDataSpy.mockReturnValue({
|
|
45
|
+
branches: mockBranches,
|
|
46
|
+
loading: false,
|
|
47
|
+
error: null,
|
|
48
|
+
worktrees: [],
|
|
49
|
+
refresh: mockRefresh,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const onExit = vi.fn();
|
|
53
|
+
const { getByText } = render(<App onExit={onExit} />);
|
|
54
|
+
|
|
55
|
+
// Check for BranchListScreen elements
|
|
56
|
+
expect(getByText(/gwt - Branch Selection/i)).toBeDefined();
|
|
57
|
+
expect(getByText(/main/)).toBeDefined();
|
|
58
|
+
expect(getByText(/feature\/test/)).toBeDefined();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should show loading state initially', async () => {
|
|
62
|
+
useGitDataSpy.mockReturnValue({
|
|
63
|
+
branches: [],
|
|
64
|
+
loading: true,
|
|
65
|
+
error: null,
|
|
66
|
+
worktrees: [],
|
|
67
|
+
refresh: mockRefresh,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const onExit = vi.fn();
|
|
71
|
+
const { queryByText, getByText } = render(
|
|
72
|
+
<App onExit={onExit} loadingIndicatorDelay={10} />
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
expect(queryByText(/Loading Git information/i)).toBeNull();
|
|
76
|
+
|
|
77
|
+
await act(async () => {
|
|
78
|
+
await new Promise((resolve) => setTimeout(resolve, 15));
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
expect(getByText(/Loading Git information/i)).toBeDefined();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should show error state when Git data fails to load', () => {
|
|
85
|
+
const error = new Error('Failed to fetch branches');
|
|
86
|
+
useGitDataSpy.mockReturnValue({
|
|
87
|
+
branches: [],
|
|
88
|
+
loading: false,
|
|
89
|
+
error,
|
|
90
|
+
worktrees: [],
|
|
91
|
+
refresh: mockRefresh,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const onExit = vi.fn();
|
|
95
|
+
const { getByText } = render(<App onExit={onExit} />);
|
|
96
|
+
|
|
97
|
+
expect(getByText(/Error:/i)).toBeDefined();
|
|
98
|
+
expect(getByText(/Failed to fetch branches/i)).toBeDefined();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should calculate statistics from branches', () => {
|
|
102
|
+
const branchesWithWorktree: BranchInfo[] = [
|
|
103
|
+
{
|
|
104
|
+
name: 'main',
|
|
105
|
+
type: 'local',
|
|
106
|
+
branchType: 'main',
|
|
107
|
+
isCurrent: true,
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: 'feature/a',
|
|
111
|
+
type: 'local',
|
|
112
|
+
branchType: 'feature',
|
|
113
|
+
isCurrent: false,
|
|
114
|
+
worktree: {
|
|
115
|
+
path: '/path/a',
|
|
116
|
+
locked: false,
|
|
117
|
+
prunable: false,
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
name: 'origin/main',
|
|
122
|
+
type: 'remote',
|
|
123
|
+
branchType: 'main',
|
|
124
|
+
isCurrent: false,
|
|
125
|
+
},
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
useGitDataSpy.mockReturnValue({
|
|
129
|
+
branches: branchesWithWorktree,
|
|
130
|
+
loading: false,
|
|
131
|
+
error: null,
|
|
132
|
+
worktrees: [],
|
|
133
|
+
refresh: mockRefresh,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const onExit = vi.fn();
|
|
137
|
+
const { getByText, getAllByText } = render(<App onExit={onExit} />);
|
|
138
|
+
|
|
139
|
+
// Check for statistics
|
|
140
|
+
expect(getByText(/Local:/)).toBeDefined();
|
|
141
|
+
expect(getAllByText(/2/).length).toBeGreaterThan(0); // 2 local branches
|
|
142
|
+
expect(getByText(/Remote:/)).toBeDefined();
|
|
143
|
+
expect(getAllByText(/1/).length).toBeGreaterThan(0); // 1 remote branch + 1 worktree
|
|
144
|
+
expect(getByText(/Worktrees:/)).toBeDefined();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should call onExit when branch is selected', () => {
|
|
148
|
+
useGitDataSpy.mockReturnValue({
|
|
149
|
+
branches: mockBranches,
|
|
150
|
+
loading: false,
|
|
151
|
+
error: null,
|
|
152
|
+
worktrees: [],
|
|
153
|
+
refresh: mockRefresh,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const onExit = vi.fn();
|
|
157
|
+
const { container } = render(<App onExit={onExit} />);
|
|
158
|
+
|
|
159
|
+
expect(container).toBeDefined();
|
|
160
|
+
// Note: Testing actual selection requires simulating user input,
|
|
161
|
+
// which is covered in integration tests
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('should handle empty branch list', () => {
|
|
165
|
+
useGitDataSpy.mockReturnValue({
|
|
166
|
+
branches: [],
|
|
167
|
+
loading: false,
|
|
168
|
+
error: null,
|
|
169
|
+
worktrees: [],
|
|
170
|
+
refresh: mockRefresh,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const onExit = vi.fn();
|
|
174
|
+
const { getByText } = render(<App onExit={onExit} />);
|
|
175
|
+
|
|
176
|
+
expect(getByText(/No branches found/i)).toBeDefined();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should wrap with ErrorBoundary', () => {
|
|
180
|
+
// This test verifies ErrorBoundary is present
|
|
181
|
+
// Actual error catching is tested separately
|
|
182
|
+
useGitDataSpy.mockReturnValue({
|
|
183
|
+
branches: mockBranches,
|
|
184
|
+
loading: false,
|
|
185
|
+
error: null,
|
|
186
|
+
worktrees: [],
|
|
187
|
+
refresh: mockRefresh,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const onExit = vi.fn();
|
|
191
|
+
const { container } = render(<App onExit={onExit} />);
|
|
192
|
+
|
|
193
|
+
expect(container).toBeDefined();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('should format branch items with icons', () => {
|
|
197
|
+
useGitDataSpy.mockReturnValue({
|
|
198
|
+
branches: mockBranches,
|
|
199
|
+
loading: false,
|
|
200
|
+
error: null,
|
|
201
|
+
worktrees: [],
|
|
202
|
+
refresh: mockRefresh,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const onExit = vi.fn();
|
|
206
|
+
const { getByText } = render(<App onExit={onExit} />);
|
|
207
|
+
|
|
208
|
+
// Check for branch type icon (main = ⚡)
|
|
209
|
+
expect(getByText(/⚡/)).toBeDefined();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe('BranchActionSelectorScreen integration', () => {
|
|
213
|
+
it('should show BranchActionSelectorScreen after branch selection', () => {
|
|
214
|
+
useGitDataSpy.mockReturnValue({
|
|
215
|
+
branches: mockBranches,
|
|
216
|
+
loading: false,
|
|
217
|
+
error: null,
|
|
218
|
+
worktrees: [],
|
|
219
|
+
refresh: mockRefresh,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const onExit = vi.fn();
|
|
223
|
+
const { container } = render(<App onExit={onExit} />);
|
|
224
|
+
|
|
225
|
+
// After implementation, should verify BranchActionSelectorScreen appears
|
|
226
|
+
expect(container).toBeDefined();
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('should navigate to AI tool selector when "use existing" is selected', () => {
|
|
230
|
+
useGitDataSpy.mockReturnValue({
|
|
231
|
+
branches: mockBranches,
|
|
232
|
+
loading: false,
|
|
233
|
+
error: null,
|
|
234
|
+
worktrees: [],
|
|
235
|
+
refresh: mockRefresh,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const onExit = vi.fn();
|
|
239
|
+
const { container } = render(<App onExit={onExit} />);
|
|
240
|
+
|
|
241
|
+
// After implementation, should verify navigation to AIToolSelectorScreen
|
|
242
|
+
expect(container).toBeDefined();
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('should navigate to branch creator when "create new" is selected', () => {
|
|
246
|
+
useGitDataSpy.mockReturnValue({
|
|
247
|
+
branches: mockBranches,
|
|
248
|
+
loading: false,
|
|
249
|
+
error: null,
|
|
250
|
+
worktrees: [],
|
|
251
|
+
refresh: mockRefresh,
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const onExit = vi.fn();
|
|
255
|
+
const { container } = render(<App onExit={onExit} />);
|
|
256
|
+
|
|
257
|
+
// After implementation, should verify navigation to BranchCreatorScreen
|
|
258
|
+
expect(container).toBeDefined();
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
afterEach(() => {
|
|
263
|
+
useGitDataSpy.mockReset();
|
|
264
|
+
useGitDataSpy.mockImplementation(originalUseGitData);
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
afterAll(() => {
|
|
269
|
+
useGitDataSpy.mockRestore();
|
|
270
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment happy-dom
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
5
|
+
import { render } from '@testing-library/react';
|
|
6
|
+
import React from 'react';
|
|
7
|
+
import { Confirm } from '../../../components/common/Confirm.js';
|
|
8
|
+
import { Window } from 'happy-dom';
|
|
9
|
+
|
|
10
|
+
describe('Confirm', () => {
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
// Setup happy-dom
|
|
13
|
+
const window = new Window();
|
|
14
|
+
globalThis.window = window as any;
|
|
15
|
+
globalThis.document = window.document as any;
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should render the message', () => {
|
|
19
|
+
const onConfirm = vi.fn();
|
|
20
|
+
const { getByText } = render(<Confirm message="Are you sure?" onConfirm={onConfirm} />);
|
|
21
|
+
|
|
22
|
+
expect(getByText('Are you sure?')).toBeDefined();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should render Yes and No options', () => {
|
|
26
|
+
const onConfirm = vi.fn();
|
|
27
|
+
const { getByText } = render(<Confirm message="Continue?" onConfirm={onConfirm} />);
|
|
28
|
+
|
|
29
|
+
expect(getByText('Yes')).toBeDefined();
|
|
30
|
+
expect(getByText('No')).toBeDefined();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should render custom Yes and No labels', () => {
|
|
34
|
+
const onConfirm = vi.fn();
|
|
35
|
+
const { getByText } = render(
|
|
36
|
+
<Confirm message="Delete?" onConfirm={onConfirm} yesLabel="Confirm" noLabel="Cancel" />
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
expect(getByText('Confirm')).toBeDefined();
|
|
40
|
+
expect(getByText('Cancel')).toBeDefined();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should default to Yes option', () => {
|
|
44
|
+
const onConfirm = vi.fn();
|
|
45
|
+
const { container } = render(<Confirm message="Continue?" onConfirm={onConfirm} />);
|
|
46
|
+
|
|
47
|
+
// Verify component renders without error
|
|
48
|
+
expect(container).toBeDefined();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should accept defaultNo prop to default to No', () => {
|
|
52
|
+
const onConfirm = vi.fn();
|
|
53
|
+
const { container } = render(<Confirm message="Continue?" onConfirm={onConfirm} defaultNo />);
|
|
54
|
+
|
|
55
|
+
expect(container).toBeDefined();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should call onConfirm with false by default when rendered', () => {
|
|
59
|
+
const onConfirm = vi.fn();
|
|
60
|
+
render(<Confirm message="Continue?" onConfirm={onConfirm} />);
|
|
61
|
+
|
|
62
|
+
// Note: Simulating selection requires ink-testing-library
|
|
63
|
+
// For now, we just verify the component structure
|
|
64
|
+
expect(onConfirm).not.toHaveBeenCalled();
|
|
65
|
+
});
|
|
66
|
+
});
|