@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.
Files changed (132) hide show
  1. package/README.ja.md +323 -0
  2. package/README.md +347 -0
  3. package/bin/gwt.js +5 -0
  4. package/package.json +125 -0
  5. package/src/claude-history.ts +717 -0
  6. package/src/claude.ts +292 -0
  7. package/src/cli/ui/__tests__/SKIPPED_TESTS.md +119 -0
  8. package/src/cli/ui/__tests__/acceptance/branchList.acceptance.test.tsx.skip +239 -0
  9. package/src/cli/ui/__tests__/acceptance/navigation.acceptance.test.tsx +214 -0
  10. package/src/cli/ui/__tests__/acceptance/realtimeUpdate.acceptance.test.tsx.skip +219 -0
  11. package/src/cli/ui/__tests__/components/App.protected-branch.test.tsx +183 -0
  12. package/src/cli/ui/__tests__/components/App.shortcuts.test.tsx +313 -0
  13. package/src/cli/ui/__tests__/components/App.test.tsx +270 -0
  14. package/src/cli/ui/__tests__/components/common/Confirm.test.tsx +66 -0
  15. package/src/cli/ui/__tests__/components/common/ErrorBoundary.test.tsx +103 -0
  16. package/src/cli/ui/__tests__/components/common/Input.test.tsx +92 -0
  17. package/src/cli/ui/__tests__/components/common/LoadingIndicator.test.tsx +127 -0
  18. package/src/cli/ui/__tests__/components/common/Select.memo.test.tsx +264 -0
  19. package/src/cli/ui/__tests__/components/common/Select.test.tsx +246 -0
  20. package/src/cli/ui/__tests__/components/parts/Footer.test.tsx +62 -0
  21. package/src/cli/ui/__tests__/components/parts/Header.test.tsx +54 -0
  22. package/src/cli/ui/__tests__/components/parts/ScrollableList.test.tsx +68 -0
  23. package/src/cli/ui/__tests__/components/parts/Stats.test.tsx +135 -0
  24. package/src/cli/ui/__tests__/components/screens/AIToolSelectorScreen.test.tsx +153 -0
  25. package/src/cli/ui/__tests__/components/screens/BranchCreatorScreen.test.tsx +215 -0
  26. package/src/cli/ui/__tests__/components/screens/BranchListScreen.test.tsx +293 -0
  27. package/src/cli/ui/__tests__/components/screens/ExecutionModeSelectorScreen.test.tsx +161 -0
  28. package/src/cli/ui/__tests__/components/screens/PRCleanupScreen.test.tsx +215 -0
  29. package/src/cli/ui/__tests__/components/screens/SessionSelectorScreen.test.tsx +99 -0
  30. package/src/cli/ui/__tests__/components/screens/WorktreeManagerScreen.test.tsx +127 -0
  31. package/src/cli/ui/__tests__/hooks/useGitData.test.ts.skip +228 -0
  32. package/src/cli/ui/__tests__/hooks/useScreenState.test.ts +146 -0
  33. package/src/cli/ui/__tests__/hooks/useTerminalSize.test.ts +98 -0
  34. package/src/cli/ui/__tests__/integration/branchList.test.tsx.skip +253 -0
  35. package/src/cli/ui/__tests__/integration/edgeCases.test.tsx +306 -0
  36. package/src/cli/ui/__tests__/integration/navigation.test.tsx +405 -0
  37. package/src/cli/ui/__tests__/integration/realtimeUpdate.test.tsx +505 -0
  38. package/src/cli/ui/__tests__/integration/realtimeUpdate.test.tsx.skip +216 -0
  39. package/src/cli/ui/__tests__/performance/branchList.performance.test.tsx +180 -0
  40. package/src/cli/ui/__tests__/performance/useMemoOptimization.test.tsx +237 -0
  41. package/src/cli/ui/__tests__/utils/branchFormatter.test.ts +775 -0
  42. package/src/cli/ui/__tests__/utils/statisticsCalculator.test.ts +243 -0
  43. package/src/cli/ui/components/App.tsx +793 -0
  44. package/src/cli/ui/components/common/Confirm.tsx +40 -0
  45. package/src/cli/ui/components/common/ErrorBoundary.tsx +57 -0
  46. package/src/cli/ui/components/common/Input.tsx +36 -0
  47. package/src/cli/ui/components/common/LoadingIndicator.tsx +95 -0
  48. package/src/cli/ui/components/common/Select.tsx +216 -0
  49. package/src/cli/ui/components/parts/Footer.tsx +41 -0
  50. package/src/cli/ui/components/parts/Header.test.tsx +85 -0
  51. package/src/cli/ui/components/parts/Header.tsx +63 -0
  52. package/src/cli/ui/components/parts/MergeStatusList.tsx +75 -0
  53. package/src/cli/ui/components/parts/ProgressBar.tsx +73 -0
  54. package/src/cli/ui/components/parts/ScrollableList.tsx +24 -0
  55. package/src/cli/ui/components/parts/Stats.tsx +67 -0
  56. package/src/cli/ui/components/screens/AIToolSelectorScreen.tsx +116 -0
  57. package/src/cli/ui/components/screens/BatchMergeProgressScreen.tsx +70 -0
  58. package/src/cli/ui/components/screens/BatchMergeResultScreen.tsx +104 -0
  59. package/src/cli/ui/components/screens/BranchCreatorScreen.tsx +213 -0
  60. package/src/cli/ui/components/screens/BranchListScreen.tsx +299 -0
  61. package/src/cli/ui/components/screens/ExecutionModeSelectorScreen.tsx +149 -0
  62. package/src/cli/ui/components/screens/PRCleanupScreen.tsx +167 -0
  63. package/src/cli/ui/components/screens/SessionSelectorScreen.tsx +100 -0
  64. package/src/cli/ui/components/screens/WorktreeManagerScreen.tsx +117 -0
  65. package/src/cli/ui/hooks/useBatchMerge.ts +96 -0
  66. package/src/cli/ui/hooks/useGitData.ts +157 -0
  67. package/src/cli/ui/hooks/useScreenState.ts +44 -0
  68. package/src/cli/ui/hooks/useTerminalSize.ts +33 -0
  69. package/src/cli/ui/screens/BranchActionSelectorScreen.tsx +102 -0
  70. package/src/cli/ui/screens/__tests__/BranchActionSelectorScreen.test.tsx +151 -0
  71. package/src/cli/ui/types.ts +295 -0
  72. package/src/cli/ui/utils/baseBranch.ts +34 -0
  73. package/src/cli/ui/utils/branchFormatter.ts +222 -0
  74. package/src/cli/ui/utils/statisticsCalculator.ts +44 -0
  75. package/src/codex.ts +139 -0
  76. package/src/config/builtin-tools.ts +44 -0
  77. package/src/config/constants.ts +100 -0
  78. package/src/config/env-history.ts +45 -0
  79. package/src/config/index.ts +204 -0
  80. package/src/config/tools.ts +293 -0
  81. package/src/git.ts +1102 -0
  82. package/src/github.ts +158 -0
  83. package/src/index.test.ts +87 -0
  84. package/src/index.ts +684 -0
  85. package/src/index.ts.backup +1543 -0
  86. package/src/launcher.ts +142 -0
  87. package/src/repositories/git.repository.ts +129 -0
  88. package/src/repositories/github.repository.ts +83 -0
  89. package/src/repositories/worktree.repository.ts +69 -0
  90. package/src/services/BatchMergeService.ts +251 -0
  91. package/src/services/WorktreeOrchestrator.ts +115 -0
  92. package/src/services/__tests__/BatchMergeService.test.ts +518 -0
  93. package/src/services/__tests__/WorktreeOrchestrator.test.ts +258 -0
  94. package/src/services/dependency-installer.ts +199 -0
  95. package/src/services/git.service.ts +113 -0
  96. package/src/services/github.service.ts +61 -0
  97. package/src/services/worktree.service.ts +66 -0
  98. package/src/types/api.ts +241 -0
  99. package/src/types/tools.ts +235 -0
  100. package/src/utils/spinner.ts +54 -0
  101. package/src/utils/terminal.ts +272 -0
  102. package/src/utils.test.ts +43 -0
  103. package/src/utils.ts +60 -0
  104. package/src/web/client/index.html +12 -0
  105. package/src/web/client/src/components/BranchGraph.tsx +231 -0
  106. package/src/web/client/src/components/EnvEditor.tsx +145 -0
  107. package/src/web/client/src/components/Terminal.tsx +137 -0
  108. package/src/web/client/src/hooks/useBranches.ts +41 -0
  109. package/src/web/client/src/hooks/useConfig.ts +31 -0
  110. package/src/web/client/src/hooks/useSessions.ts +59 -0
  111. package/src/web/client/src/hooks/useWorktrees.ts +47 -0
  112. package/src/web/client/src/index.css +834 -0
  113. package/src/web/client/src/lib/api.ts +184 -0
  114. package/src/web/client/src/lib/websocket.ts +174 -0
  115. package/src/web/client/src/main.tsx +29 -0
  116. package/src/web/client/src/pages/BranchDetailPage.tsx +847 -0
  117. package/src/web/client/src/pages/BranchListPage.tsx +264 -0
  118. package/src/web/client/src/pages/ConfigManagementPage.tsx +203 -0
  119. package/src/web/client/src/router.tsx +27 -0
  120. package/src/web/client/vite.config.ts +21 -0
  121. package/src/web/server/env/importer.ts +54 -0
  122. package/src/web/server/index.ts +74 -0
  123. package/src/web/server/pty/manager.ts +189 -0
  124. package/src/web/server/routes/branches.ts +126 -0
  125. package/src/web/server/routes/config.ts +220 -0
  126. package/src/web/server/routes/index.ts +37 -0
  127. package/src/web/server/routes/sessions.ts +130 -0
  128. package/src/web/server/routes/worktrees.ts +108 -0
  129. package/src/web/server/services/branches.ts +368 -0
  130. package/src/web/server/services/worktrees.ts +85 -0
  131. package/src/web/server/websocket/handler.ts +180 -0
  132. 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
+ });