@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,253 @@
1
+ /**
2
+ * @vitest-environment happy-dom
3
+ */
4
+ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
5
+ import { render, waitFor } 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
+ // 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('Branch List Integration', () => {
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
+ afterEach(() => {
36
+ vi.clearAllMocks();
37
+ });
38
+
39
+ it('should render full application with branch list', async () => {
40
+ const mockBranches: BranchInfo[] = [
41
+ {
42
+ name: 'main',
43
+ type: 'local',
44
+ branchType: 'main',
45
+ isCurrent: true,
46
+ },
47
+ {
48
+ name: 'feature/test',
49
+ type: 'local',
50
+ branchType: 'feature',
51
+ isCurrent: false,
52
+ },
53
+ ];
54
+
55
+ (getAllBranches as ReturnType<typeof vi.fn>).mockResolvedValue(mockBranches);
56
+ (listAdditionalWorktrees as ReturnType<typeof vi.fn>).mockResolvedValue([]);
57
+
58
+ const onExit = vi.fn();
59
+ const { getByText } = render(<App onExit={onExit} />);
60
+
61
+ // Wait for async data loading
62
+ await waitFor(() => {
63
+ expect(getByText(/Claude Worktree/i)).toBeDefined();
64
+ });
65
+
66
+ // Verify header
67
+ expect(getByText(/Claude Worktree/i)).toBeDefined();
68
+
69
+ // Verify stats
70
+ expect(getByText(/Local:/)).toBeDefined();
71
+
72
+ // Verify branches are displayed
73
+ expect(getByText(/main/)).toBeDefined();
74
+ expect(getByText(/feature\/test/)).toBeDefined();
75
+
76
+ // Verify footer
77
+ expect(getByText(/Quit/i)).toBeDefined();
78
+ });
79
+
80
+ it('should display statistics correctly', async () => {
81
+ const mockBranches: BranchInfo[] = [
82
+ {
83
+ name: 'main',
84
+ type: 'local',
85
+ branchType: 'main',
86
+ isCurrent: true,
87
+ },
88
+ {
89
+ name: 'feature/a',
90
+ type: 'local',
91
+ branchType: 'feature',
92
+ isCurrent: false,
93
+ worktree: {
94
+ path: '/path/a',
95
+ locked: false,
96
+ prunable: false,
97
+ },
98
+ },
99
+ {
100
+ name: 'origin/main',
101
+ type: 'remote',
102
+ branchType: 'main',
103
+ isCurrent: false,
104
+ },
105
+ ];
106
+
107
+ (getAllBranches as ReturnType<typeof vi.fn>).mockResolvedValue(mockBranches);
108
+ (listAdditionalWorktrees as ReturnType<typeof vi.fn>).mockResolvedValue([]);
109
+
110
+ const onExit = vi.fn();
111
+ const { getByText, getAllByText } = render(<App onExit={onExit} />);
112
+
113
+ // Wait for async data loading
114
+ await waitFor(() => {
115
+ expect(getByText(/Local:/)).toBeDefined();
116
+ });
117
+
118
+ // Verify statistics
119
+ expect(getByText(/Local:/)).toBeDefined();
120
+ expect(getAllByText(/2/).length).toBeGreaterThan(0); // 2 local branches
121
+
122
+ expect(getByText(/Remote:/)).toBeDefined();
123
+ expect(getAllByText(/1/).length).toBeGreaterThan(0); // 1 remote branch + 1 worktree
124
+
125
+ expect(getByText(/Worktrees:/)).toBeDefined();
126
+ });
127
+
128
+ it('should handle empty branch list', async () => {
129
+ (getAllBranches as ReturnType<typeof vi.fn>).mockResolvedValue([]);
130
+ (listAdditionalWorktrees as ReturnType<typeof vi.fn>).mockResolvedValue([]);
131
+
132
+ const onExit = vi.fn();
133
+ const { getByText } = render(<App onExit={onExit} />);
134
+
135
+ // Wait for async data loading
136
+ await waitFor(() => {
137
+ expect(getByText(/No branches found/i)).toBeDefined();
138
+ });
139
+
140
+ expect(getByText(/No branches found/i)).toBeDefined();
141
+ });
142
+
143
+ it('should handle loading state', async () => {
144
+ // Mock a slow response
145
+ (getAllBranches as ReturnType<typeof vi.fn>).mockImplementation(
146
+ () =>
147
+ new Promise((resolve) =>
148
+ setTimeout(() => resolve([]), 100)
149
+ )
150
+ );
151
+ (listAdditionalWorktrees as ReturnType<typeof vi.fn>).mockResolvedValue([]);
152
+
153
+ const onExit = vi.fn();
154
+ const { getByText, queryByText } = render(<App onExit={onExit} />);
155
+
156
+ // Initially should show loading
157
+ // Note: In happy-dom, useEffect may run synchronously, so we check for either loading or loaded state
158
+ const hasLoading = queryByText(/Loading/i);
159
+ if (hasLoading) {
160
+ expect(hasLoading).toBeDefined();
161
+ }
162
+
163
+ // Wait for data to load
164
+ await waitFor(() => {
165
+ expect(queryByText(/Loading/i)).toBeNull();
166
+ });
167
+ });
168
+
169
+ it('should handle error state', async () => {
170
+ const error = new Error('Failed to fetch branches');
171
+ (getAllBranches as ReturnType<typeof vi.fn>).mockRejectedValue(error);
172
+ (listAdditionalWorktrees as ReturnType<typeof vi.fn>).mockResolvedValue([]);
173
+
174
+ const onExit = vi.fn();
175
+ const { getByText } = render(<App onExit={onExit} />);
176
+
177
+ // Wait for error to appear
178
+ await waitFor(() => {
179
+ expect(getByText(/Error:/i)).toBeDefined();
180
+ });
181
+
182
+ expect(getByText(/Error:/i)).toBeDefined();
183
+ expect(getByText(/Failed to fetch branches/i)).toBeDefined();
184
+ });
185
+
186
+ it('should display branch icons correctly', async () => {
187
+ const mockBranches: BranchInfo[] = [
188
+ {
189
+ name: 'main',
190
+ type: 'local',
191
+ branchType: 'main',
192
+ isCurrent: true,
193
+ },
194
+ {
195
+ name: 'feature/test',
196
+ type: 'local',
197
+ branchType: 'feature',
198
+ isCurrent: false,
199
+ },
200
+ {
201
+ name: 'hotfix/urgent',
202
+ type: 'local',
203
+ branchType: 'hotfix',
204
+ isCurrent: false,
205
+ },
206
+ ];
207
+
208
+ (getAllBranches as ReturnType<typeof vi.fn>).mockResolvedValue(mockBranches);
209
+ (listAdditionalWorktrees as ReturnType<typeof vi.fn>).mockResolvedValue([]);
210
+
211
+ const onExit = vi.fn();
212
+ const { getByText } = render(<App onExit={onExit} />);
213
+
214
+ // Wait for async data loading
215
+ await waitFor(() => {
216
+ expect(getByText(/⚡/)).toBeDefined();
217
+ });
218
+
219
+ // Check for branch type icons
220
+ expect(getByText(/⚡/)).toBeDefined(); // main icon
221
+ expect(getByText(/⭐/)).toBeDefined(); // current branch icon
222
+ expect(getByText(/✨/)).toBeDefined(); // feature icon
223
+ expect(getByText(/🔥/)).toBeDefined(); // hotfix icon
224
+ });
225
+
226
+ it('should integrate all components correctly', async () => {
227
+ const mockBranches: BranchInfo[] = [
228
+ {
229
+ name: 'main',
230
+ type: 'local',
231
+ branchType: 'main',
232
+ isCurrent: true,
233
+ },
234
+ ];
235
+
236
+ (getAllBranches as ReturnType<typeof vi.fn>).mockResolvedValue(mockBranches);
237
+ (listAdditionalWorktrees as ReturnType<typeof vi.fn>).mockResolvedValue([]);
238
+
239
+ const onExit = vi.fn();
240
+ const { container } = render(<App onExit={onExit} />);
241
+
242
+ // Wait for rendering
243
+ await waitFor(() => {
244
+ expect(container.textContent).toContain('Claude Worktree');
245
+ });
246
+
247
+ // Verify all major sections are present
248
+ expect(container.textContent).toContain('Claude Worktree'); // Header
249
+ expect(container.textContent).toContain('Local:'); // Stats
250
+ expect(container.textContent).toContain('main'); // Branch list
251
+ expect(container.textContent).toContain('Quit'); // Footer
252
+ });
253
+ });
@@ -0,0 +1,306 @@
1
+ /**
2
+ * @vitest-environment happy-dom
3
+ * Edge case tests for UI components
4
+ */
5
+ import { describe, it, expect, beforeEach, afterEach, afterAll, vi } from 'vitest';
6
+ import { render } from '@testing-library/react';
7
+ import React from 'react';
8
+ import { App } from '../../components/App.js';
9
+ import { BranchListScreen } from '../../components/screens/BranchListScreen.js';
10
+ import { Window } from 'happy-dom';
11
+ import type { BranchInfo, BranchItem, Statistics } from '../../types.js';
12
+ import * as useGitDataModule from '../../hooks/useGitData.js';
13
+
14
+ const mockRefresh = vi.fn();
15
+ const originalUseGitData = useGitDataModule.useGitData;
16
+ const useGitDataSpy = vi.spyOn(useGitDataModule, 'useGitData');
17
+
18
+ describe('Edge Cases Integration Tests', () => {
19
+ beforeEach(() => {
20
+ // Setup happy-dom
21
+ const window = new Window();
22
+ globalThis.window = window as any;
23
+ globalThis.document = window.document as any;
24
+
25
+ // Reset mocks
26
+ vi.clearAllMocks();
27
+ useGitDataSpy.mockReset();
28
+ useGitDataSpy.mockImplementation(originalUseGitData);
29
+ });
30
+
31
+ /**
32
+ * T091: Terminal size極小(10行以下)の動作確認
33
+ */
34
+ it('[T091] should handle minimal terminal size (10 rows)', () => {
35
+ // Save original rows
36
+ const originalRows = process.stdout.rows;
37
+
38
+ // Set minimal terminal size
39
+ process.stdout.rows = 10;
40
+
41
+ const mockBranches: BranchItem[] = [
42
+ { name: 'main', label: 'main', value: 'main' },
43
+ { name: 'feature/a', label: 'feature/a', value: 'feature/a' },
44
+ { name: 'feature/b', label: 'feature/b', value: 'feature/b' },
45
+ ];
46
+
47
+ const mockStats: Statistics = {
48
+ localCount: 3,
49
+ remoteCount: 0,
50
+ worktreeCount: 0,
51
+ changesCount: 0,
52
+ lastUpdated: new Date(),
53
+ };
54
+
55
+ const onSelect = vi.fn();
56
+ const { container } = render(
57
+ <BranchListScreen branches={mockBranches} stats={mockStats} onSelect={onSelect} />
58
+ );
59
+
60
+ // Should render without crashing
61
+ expect(container).toBeDefined();
62
+
63
+ // Restore original rows
64
+ process.stdout.rows = originalRows;
65
+ });
66
+
67
+ it('[T091] should handle extremely small terminal (5 rows)', () => {
68
+ const originalRows = process.stdout.rows;
69
+ process.stdout.rows = 5;
70
+
71
+ const mockBranches: BranchItem[] = [
72
+ { name: 'main', label: 'main', value: 'main' },
73
+ ];
74
+
75
+ const mockStats: Statistics = {
76
+ localCount: 1,
77
+ remoteCount: 0,
78
+ worktreeCount: 0,
79
+ changesCount: 0,
80
+ lastUpdated: new Date(),
81
+ };
82
+
83
+ const onSelect = vi.fn();
84
+ const { getByText } = render(
85
+ <BranchListScreen branches={mockBranches} stats={mockStats} onSelect={onSelect} />
86
+ );
87
+
88
+ // Header should still be visible
89
+ expect(getByText(/gwt - Branch Selection/i)).toBeDefined();
90
+
91
+ process.stdout.rows = originalRows;
92
+ });
93
+
94
+ /**
95
+ * T092: 非常に長いブランチ名の表示確認
96
+ */
97
+ it('[T092] should handle very long branch names', () => {
98
+ const longBranchName =
99
+ 'feature/very-long-branch-name-that-exceeds-normal-terminal-width-and-should-be-handled-gracefully';
100
+
101
+ const mockBranches: BranchItem[] = [
102
+ {
103
+ name: 'main',
104
+ label: 'main',
105
+ value: 'main',
106
+ latestCommitTimestamp: 1_700_000_000,
107
+ },
108
+ {
109
+ name: longBranchName,
110
+ label: longBranchName,
111
+ value: longBranchName,
112
+ latestCommitTimestamp: 1_700_000_600,
113
+ },
114
+ ];
115
+
116
+ const mockStats: Statistics = {
117
+ localCount: 2,
118
+ remoteCount: 0,
119
+ worktreeCount: 0,
120
+ changesCount: 0,
121
+ lastUpdated: new Date(),
122
+ };
123
+
124
+ const onSelect = vi.fn();
125
+ const { container } = render(
126
+ <BranchListScreen branches={mockBranches} stats={mockStats} onSelect={onSelect} />
127
+ );
128
+
129
+ // Long branch name should be displayed (Ink will handle wrapping/truncation)
130
+ expect(container.textContent).toMatch(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}/);
131
+ });
132
+
133
+ it('[T092] should handle branch names with special characters', () => {
134
+ const specialBranchNames = [
135
+ 'feature/bug-fix-#123',
136
+ 'hotfix/issue@456',
137
+ 'release/v1.0.0-beta.1',
138
+ 'feature/改善-日本語',
139
+ ];
140
+
141
+ const mockBranches: BranchItem[] = specialBranchNames.map((name, index) => ({
142
+ name,
143
+ label: name,
144
+ value: name,
145
+ latestCommitTimestamp: 1_700_001_000 + index * 60,
146
+ }));
147
+
148
+ const mockStats: Statistics = {
149
+ localCount: mockBranches.length,
150
+ remoteCount: 0,
151
+ worktreeCount: 0,
152
+ changesCount: 0,
153
+ lastUpdated: new Date(),
154
+ };
155
+
156
+ const onSelect = vi.fn();
157
+ const { container } = render(
158
+ <BranchListScreen branches={mockBranches} stats={mockStats} onSelect={onSelect} />
159
+ );
160
+
161
+ // All special branch names should be displayed
162
+ specialBranchNames.forEach((name) => {
163
+ expect(container.textContent).toContain(name);
164
+ });
165
+ });
166
+
167
+ /**
168
+ * T093: Error Boundary動作確認
169
+ */
170
+ it('[T093] should catch errors in App component', async () => {
171
+ // Mock useGitData to throw an error after initial render
172
+ let callCount = 0;
173
+ useGitDataSpy.mockImplementation(() => {
174
+ callCount++;
175
+ if (callCount > 1) {
176
+ throw new Error('Simulated error');
177
+ }
178
+ return {
179
+ branches: [],
180
+ worktrees: [],
181
+ loading: false,
182
+ error: null,
183
+ refresh: mockRefresh,
184
+ lastUpdated: null,
185
+ };
186
+ });
187
+
188
+ const onExit = vi.fn();
189
+ const { container } = render(<App onExit={onExit} />);
190
+
191
+ // Initial render should work
192
+ expect(container).toBeDefined();
193
+ });
194
+
195
+ it('[T093] should display error message when data loading fails', () => {
196
+ const testError = new Error('Test error: Failed to load Git data');
197
+ useGitDataSpy.mockReturnValue({
198
+ branches: [],
199
+ worktrees: [],
200
+ loading: false,
201
+ error: testError,
202
+ refresh: mockRefresh,
203
+ lastUpdated: null,
204
+ });
205
+
206
+ const onExit = vi.fn();
207
+ const { getByText } = render(<App onExit={onExit} />);
208
+
209
+ // Error should be displayed
210
+ expect(getByText(/Error:/i)).toBeDefined();
211
+ expect(getByText(/Failed to load Git data/i)).toBeDefined();
212
+ });
213
+
214
+ it('[T093] should handle empty branches list gracefully', () => {
215
+ useGitDataSpy.mockReturnValue({
216
+ branches: [],
217
+ worktrees: [],
218
+ loading: false,
219
+ error: null,
220
+ refresh: mockRefresh,
221
+ lastUpdated: null,
222
+ });
223
+
224
+ const onExit = vi.fn();
225
+ const { container } = render(<App onExit={onExit} />);
226
+
227
+ // Should render without error even with no branches
228
+ expect(container).toBeDefined();
229
+ });
230
+
231
+ /**
232
+ * Additional edge cases
233
+ */
234
+ it('should handle large number of worktrees', () => {
235
+ const mockBranches: BranchInfo[] = Array.from({ length: 50 }, (_, i) => ({
236
+ name: `feature/branch-${i}`,
237
+ type: 'local' as const,
238
+ branchType: 'feature' as const,
239
+ isCurrent: false,
240
+ }));
241
+
242
+ useGitDataSpy.mockReturnValue({
243
+ branches: mockBranches,
244
+ worktrees: Array.from({ length: 30 }, (_, i) => ({
245
+ branch: `feature/branch-${i}`,
246
+ path: `/path/to/worktree-${i}`,
247
+ head: `commit-${i}`,
248
+ isAccessible: true,
249
+ })),
250
+ loading: false,
251
+ error: null,
252
+ refresh: mockRefresh,
253
+ lastUpdated: new Date(),
254
+ });
255
+
256
+ const onExit = vi.fn();
257
+ const { container } = render(<App onExit={onExit} />);
258
+
259
+ expect(container).toBeDefined();
260
+ });
261
+
262
+ it('should handle terminal resize gracefully', () => {
263
+ const originalRows = process.stdout.rows;
264
+
265
+ // Start with normal size
266
+ process.stdout.rows = 30;
267
+
268
+ const mockBranches: BranchItem[] = [
269
+ { name: 'main', label: 'main', value: 'main' },
270
+ ];
271
+
272
+ const mockStats: Statistics = {
273
+ localCount: 1,
274
+ remoteCount: 0,
275
+ worktreeCount: 0,
276
+ changesCount: 0,
277
+ lastUpdated: new Date(),
278
+ };
279
+
280
+ const onSelect = vi.fn();
281
+ const { container, rerender } = render(
282
+ <BranchListScreen branches={mockBranches} stats={mockStats} onSelect={onSelect} />
283
+ );
284
+
285
+ expect(container).toBeDefined();
286
+
287
+ // Simulate terminal resize
288
+ process.stdout.rows = 15;
289
+
290
+ // Re-render
291
+ rerender(<BranchListScreen branches={mockBranches} stats={mockStats} onSelect={onSelect} />);
292
+
293
+ expect(container).toBeDefined();
294
+
295
+ process.stdout.rows = originalRows;
296
+ });
297
+
298
+ afterEach(() => {
299
+ useGitDataSpy.mockReset();
300
+ useGitDataSpy.mockImplementation(originalUseGitData);
301
+ });
302
+ });
303
+
304
+ afterAll(() => {
305
+ useGitDataSpy.mockRestore();
306
+ });