@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,161 @@
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 { ExecutionModeSelectorScreen } from '../../../components/screens/ExecutionModeSelectorScreen.js';
8
+ import { Window } from 'happy-dom';
9
+
10
+ describe('ExecutionModeSelectorScreen', () => {
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 header with title', () => {
19
+ const onBack = vi.fn();
20
+ const onSelect = vi.fn();
21
+ const { container } = render(
22
+ <ExecutionModeSelectorScreen onBack={onBack} onSelect={onSelect} />
23
+ );
24
+
25
+ expect(container).toBeDefined();
26
+ });
27
+
28
+ it('should render execution mode options', () => {
29
+ const onBack = vi.fn();
30
+ const onSelect = vi.fn();
31
+ const { getByText } = render(
32
+ <ExecutionModeSelectorScreen onBack={onBack} onSelect={onSelect} />
33
+ );
34
+
35
+ expect(getByText(/Normal/i)).toBeDefined();
36
+ expect(getByText(/Continue/i)).toBeDefined();
37
+ expect(getByText(/Resume/i)).toBeDefined();
38
+ });
39
+
40
+ it('should render footer with actions', () => {
41
+ const onBack = vi.fn();
42
+ const onSelect = vi.fn();
43
+ const { getAllByText } = render(
44
+ <ExecutionModeSelectorScreen onBack={onBack} onSelect={onSelect} />
45
+ );
46
+
47
+ expect(getAllByText(/enter/i).length).toBeGreaterThan(0);
48
+ expect(getAllByText(/esc/i).length).toBeGreaterThan(0);
49
+ });
50
+
51
+ it('should use terminal height for layout calculation', () => {
52
+ const originalRows = process.stdout.rows;
53
+ process.stdout.rows = 30;
54
+
55
+ const onBack = vi.fn();
56
+ const onSelect = vi.fn();
57
+ const { container } = render(
58
+ <ExecutionModeSelectorScreen onBack={onBack} onSelect={onSelect} />
59
+ );
60
+
61
+ expect(container).toBeDefined();
62
+
63
+ process.stdout.rows = originalRows;
64
+ });
65
+
66
+ it('should handle back navigation with ESC key', () => {
67
+ const onBack = vi.fn();
68
+ const onSelect = vi.fn();
69
+ const { container } = render(
70
+ <ExecutionModeSelectorScreen onBack={onBack} onSelect={onSelect} />
71
+ );
72
+
73
+ // Test will verify onBack is called when ESC is pressed
74
+ expect(container).toBeDefined();
75
+ });
76
+
77
+ it('should handle mode selection', () => {
78
+ const onBack = vi.fn();
79
+ const onSelect = vi.fn();
80
+ const { container } = render(
81
+ <ExecutionModeSelectorScreen onBack={onBack} onSelect={onSelect} />
82
+ );
83
+
84
+ // Test will verify onSelect is called with correct mode
85
+ expect(container).toBeDefined();
86
+ });
87
+
88
+ // TDD: Tests for 2-step selection (mode + skipPermissions)
89
+ // TODO: Implement integration tests with user interaction simulation
90
+ describe.skip('Skip Permissions Selection', () => {
91
+ it('should render skip permissions prompt after mode selection', () => {
92
+ const onBack = vi.fn();
93
+ const onSelect = vi.fn();
94
+ const { getByText } = render(
95
+ <ExecutionModeSelectorScreen onBack={onBack} onSelect={onSelect} />
96
+ );
97
+
98
+ // After selecting a mode, should show skip permissions prompt
99
+ // This test will fail until we implement the 2-step UI
100
+ expect(getByText(/Skip permission checks/i)).toBeDefined();
101
+ });
102
+
103
+ it('should show correct flag hints for skipPermissions prompt', () => {
104
+ const onBack = vi.fn();
105
+ const onSelect = vi.fn();
106
+ const { getByText } = render(
107
+ <ExecutionModeSelectorScreen onBack={onBack} onSelect={onSelect} />
108
+ );
109
+
110
+ // Should show both --dangerously-skip-permissions and --yolo hints
111
+ expect(getByText(/--dangerously-skip-permissions/i)).toBeDefined();
112
+ expect(getByText(/--yolo/i)).toBeDefined();
113
+ });
114
+
115
+ it('should call onSelect with mode and skipPermissions=true when Yes is selected', () => {
116
+ const onBack = vi.fn();
117
+ const onSelect = vi.fn();
118
+ render(<ExecutionModeSelectorScreen onBack={onBack} onSelect={onSelect} />);
119
+
120
+ // After selecting mode and Yes for skipPermissions
121
+ // onSelect should be called with { mode: 'normal', skipPermissions: true }
122
+ // This test will fail until implementation
123
+ expect(onSelect).toHaveBeenCalledWith(
124
+ expect.objectContaining({
125
+ mode: expect.any(String),
126
+ skipPermissions: true,
127
+ })
128
+ );
129
+ });
130
+
131
+ it('should call onSelect with mode and skipPermissions=false when No is selected', () => {
132
+ const onBack = vi.fn();
133
+ const onSelect = vi.fn();
134
+ render(<ExecutionModeSelectorScreen onBack={onBack} onSelect={onSelect} />);
135
+
136
+ // After selecting mode and No for skipPermissions
137
+ // onSelect should be called with { mode: 'normal', skipPermissions: false }
138
+ // This test will fail until implementation
139
+ expect(onSelect).toHaveBeenCalledWith(
140
+ expect.objectContaining({
141
+ mode: expect.any(String),
142
+ skipPermissions: false,
143
+ })
144
+ );
145
+ });
146
+
147
+ it('should default skipPermissions to false', () => {
148
+ const onBack = vi.fn();
149
+ const onSelect = vi.fn();
150
+ render(<ExecutionModeSelectorScreen onBack={onBack} onSelect={onSelect} />);
151
+
152
+ // Default should be No (skipPermissions: false)
153
+ // This test will fail until implementation
154
+ expect(onSelect).toHaveBeenCalledWith(
155
+ expect.objectContaining({
156
+ skipPermissions: false,
157
+ })
158
+ );
159
+ });
160
+ });
161
+ });
@@ -0,0 +1,215 @@
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 { PRCleanupScreen } from '../../../components/screens/PRCleanupScreen.js';
8
+ import { Window } from 'happy-dom';
9
+ import type { CleanupTarget } from '../../../types.js';
10
+
11
+ describe('PRCleanupScreen', () => {
12
+ beforeEach(() => {
13
+ // Setup happy-dom
14
+ const window = new Window();
15
+ globalThis.window = window as any;
16
+ globalThis.document = window.document as any;
17
+ });
18
+
19
+ const mockTargets: CleanupTarget[] = [
20
+ {
21
+ branch: 'feature/add-new-feature',
22
+ cleanupType: 'worktree-and-branch',
23
+ pullRequest: {
24
+ number: 123,
25
+ title: 'Add new feature',
26
+ branch: 'feature/add-new-feature',
27
+ mergedAt: '2025-01-20T10:00:00Z',
28
+ author: 'user1',
29
+ },
30
+ worktreePath: '/workspace/feature-add-new-feature',
31
+ hasUncommittedChanges: false,
32
+ hasUnpushedCommits: false,
33
+ hasRemoteBranch: true,
34
+ isAccessible: true,
35
+ },
36
+ {
37
+ branch: 'hotfix/fix-bug',
38
+ cleanupType: 'branch-only',
39
+ pullRequest: {
40
+ number: 124,
41
+ title: 'Fix bug',
42
+ branch: 'hotfix/fix-bug',
43
+ mergedAt: '2025-01-21T15:30:00Z',
44
+ author: 'user2',
45
+ },
46
+ worktreePath: null,
47
+ hasUncommittedChanges: false,
48
+ hasUnpushedCommits: false,
49
+ hasRemoteBranch: false,
50
+ },
51
+ ];
52
+
53
+ it('should render header with title', () => {
54
+ const onBack = vi.fn();
55
+ const onCleanup = vi.fn();
56
+ const { getByText } = render(
57
+ <PRCleanupScreen
58
+ targets={mockTargets}
59
+ loading={false}
60
+ error={null}
61
+ onBack={onBack}
62
+ onRefresh={vi.fn()}
63
+ onCleanup={onCleanup}
64
+ />
65
+ );
66
+
67
+ expect(getByText(/Branch Cleanup/i)).toBeDefined();
68
+ });
69
+
70
+ it('should render PR list', () => {
71
+ const onBack = vi.fn();
72
+ const onCleanup = vi.fn();
73
+ const { getByText } = render(
74
+ <PRCleanupScreen
75
+ targets={mockTargets}
76
+ loading={false}
77
+ error={null}
78
+ onBack={onBack}
79
+ onRefresh={vi.fn()}
80
+ onCleanup={onCleanup}
81
+ />
82
+ );
83
+
84
+ expect(getByText(/feature\/add-new-feature/i)).toBeDefined();
85
+ expect(getByText(/hotfix\/fix-bug/i)).toBeDefined();
86
+ });
87
+
88
+ it('should render footer with actions', () => {
89
+ const onBack = vi.fn();
90
+ const onCleanup = vi.fn();
91
+ const { getAllByText } = render(
92
+ <PRCleanupScreen
93
+ targets={mockTargets}
94
+ loading={false}
95
+ error={null}
96
+ onBack={onBack}
97
+ onRefresh={vi.fn()}
98
+ onCleanup={onCleanup}
99
+ />
100
+ );
101
+
102
+ expect(getAllByText(/enter/i).length).toBeGreaterThan(0);
103
+ expect(getAllByText(/esc/i).length).toBeGreaterThan(0);
104
+ });
105
+
106
+ it('should handle empty PR list', () => {
107
+ const onBack = vi.fn();
108
+ const onCleanup = vi.fn();
109
+ const { getByText } = render(
110
+ <PRCleanupScreen
111
+ targets={[]}
112
+ loading={false}
113
+ error={null}
114
+ onBack={onBack}
115
+ onRefresh={vi.fn()}
116
+ onCleanup={onCleanup}
117
+ />
118
+ );
119
+
120
+ expect(getByText(/No cleanup targets found/i)).toBeDefined();
121
+ });
122
+
123
+ it('should display PR count in stats', () => {
124
+ const onBack = vi.fn();
125
+ const onCleanup = vi.fn();
126
+ const { getByText, getAllByText } = render(
127
+ <PRCleanupScreen
128
+ targets={mockTargets}
129
+ loading={false}
130
+ error={null}
131
+ onBack={onBack}
132
+ onRefresh={vi.fn()}
133
+ onCleanup={onCleanup}
134
+ />
135
+ );
136
+
137
+ expect(getByText(/Total:/i)).toBeDefined();
138
+ expect(getAllByText(/^2$/).length).toBeGreaterThan(0);
139
+ });
140
+
141
+ it('should use terminal height for layout calculation', () => {
142
+ const originalRows = process.stdout.rows;
143
+ process.stdout.rows = 30;
144
+
145
+ const onBack = vi.fn();
146
+ const onCleanup = vi.fn();
147
+ const { container } = render(
148
+ <PRCleanupScreen
149
+ targets={mockTargets}
150
+ loading={false}
151
+ error={null}
152
+ onBack={onBack}
153
+ onRefresh={vi.fn()}
154
+ onCleanup={onCleanup}
155
+ />
156
+ );
157
+
158
+ expect(container).toBeDefined();
159
+
160
+ process.stdout.rows = originalRows;
161
+ });
162
+
163
+ it('should handle back navigation with ESC key', () => {
164
+ const onBack = vi.fn();
165
+ const onCleanup = vi.fn();
166
+ const { container } = render(
167
+ <PRCleanupScreen
168
+ targets={mockTargets}
169
+ loading={false}
170
+ error={null}
171
+ onBack={onBack}
172
+ onRefresh={vi.fn()}
173
+ onCleanup={onCleanup}
174
+ />
175
+ );
176
+
177
+ // Test will verify onBack is called when ESC is pressed
178
+ expect(container).toBeDefined();
179
+ });
180
+
181
+ it('should render status message when provided', () => {
182
+ const onBack = vi.fn();
183
+ const onCleanup = vi.fn();
184
+ const { getByText } = render(
185
+ <PRCleanupScreen
186
+ targets={mockTargets}
187
+ loading={false}
188
+ error={null}
189
+ statusMessage="Cleanup completed"
190
+ onBack={onBack}
191
+ onRefresh={vi.fn()}
192
+ onCleanup={onCleanup}
193
+ />
194
+ );
195
+
196
+ expect(getByText(/Cleanup completed/i)).toBeDefined();
197
+ });
198
+
199
+ it('should render loading message when loading', () => {
200
+ const onBack = vi.fn();
201
+ const onCleanup = vi.fn();
202
+ const { getByText } = render(
203
+ <PRCleanupScreen
204
+ targets={[]}
205
+ loading
206
+ error={null}
207
+ onBack={onBack}
208
+ onRefresh={vi.fn()}
209
+ onCleanup={onCleanup}
210
+ />
211
+ );
212
+
213
+ expect(getByText(/Loading cleanup targets/i)).toBeDefined();
214
+ });
215
+ });
@@ -0,0 +1,99 @@
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 { SessionSelectorScreen } from '../../../components/screens/SessionSelectorScreen.js';
8
+ import { Window } from 'happy-dom';
9
+
10
+ describe('SessionSelectorScreen', () => {
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
+ const mockSessions = ['session-1', 'session-2', 'session-3'];
19
+
20
+ it('should render header with title', () => {
21
+ const onBack = vi.fn();
22
+ const onSelect = vi.fn();
23
+ const { getByText } = render(
24
+ <SessionSelectorScreen sessions={mockSessions} onBack={onBack} onSelect={onSelect} />
25
+ );
26
+
27
+ expect(getByText(/Session Selection/i)).toBeDefined();
28
+ });
29
+
30
+ it('should render session list', () => {
31
+ const onBack = vi.fn();
32
+ const onSelect = vi.fn();
33
+ const { getByText } = render(
34
+ <SessionSelectorScreen sessions={mockSessions} onBack={onBack} onSelect={onSelect} />
35
+ );
36
+
37
+ expect(getByText(/session-1/i)).toBeDefined();
38
+ expect(getByText(/session-2/i)).toBeDefined();
39
+ expect(getByText(/session-3/i)).toBeDefined();
40
+ });
41
+
42
+ it('should render footer with actions', () => {
43
+ const onBack = vi.fn();
44
+ const onSelect = vi.fn();
45
+ const { getAllByText } = render(
46
+ <SessionSelectorScreen sessions={mockSessions} onBack={onBack} onSelect={onSelect} />
47
+ );
48
+
49
+ expect(getAllByText(/enter/i).length).toBeGreaterThan(0);
50
+ expect(getAllByText(/esc/i).length).toBeGreaterThan(0);
51
+ });
52
+
53
+ it('should handle empty session list', () => {
54
+ const onBack = vi.fn();
55
+ const onSelect = vi.fn();
56
+ const { getByText } = render(
57
+ <SessionSelectorScreen sessions={[]} onBack={onBack} onSelect={onSelect} />
58
+ );
59
+
60
+ expect(getByText(/No sessions found/i)).toBeDefined();
61
+ });
62
+
63
+ it('should display session count in stats', () => {
64
+ const onBack = vi.fn();
65
+ const onSelect = vi.fn();
66
+ const { getByText, getAllByText } = render(
67
+ <SessionSelectorScreen sessions={mockSessions} onBack={onBack} onSelect={onSelect} />
68
+ );
69
+
70
+ expect(getByText(/Total:/i)).toBeDefined();
71
+ expect(getAllByText(/3/).length).toBeGreaterThan(0);
72
+ });
73
+
74
+ it('should use terminal height for layout calculation', () => {
75
+ const originalRows = process.stdout.rows;
76
+ process.stdout.rows = 30;
77
+
78
+ const onBack = vi.fn();
79
+ const onSelect = vi.fn();
80
+ const { container } = render(
81
+ <SessionSelectorScreen sessions={mockSessions} onBack={onBack} onSelect={onSelect} />
82
+ );
83
+
84
+ expect(container).toBeDefined();
85
+
86
+ process.stdout.rows = originalRows;
87
+ });
88
+
89
+ it('should handle back navigation with ESC key', () => {
90
+ const onBack = vi.fn();
91
+ const onSelect = vi.fn();
92
+ const { container } = render(
93
+ <SessionSelectorScreen sessions={mockSessions} onBack={onBack} onSelect={onSelect} />
94
+ );
95
+
96
+ // Test will verify onBack is called when ESC is pressed
97
+ expect(container).toBeDefined();
98
+ });
99
+ });
@@ -0,0 +1,127 @@
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 { WorktreeManagerScreen } from '../../../components/screens/WorktreeManagerScreen.js';
8
+ import { Window } from 'happy-dom';
9
+
10
+ describe('WorktreeManagerScreen', () => {
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
+ const mockWorktrees = [
19
+ {
20
+ branch: 'feature/test-1',
21
+ path: '/path/to/worktree-1',
22
+ isAccessible: true,
23
+ },
24
+ {
25
+ branch: 'feature/test-2',
26
+ path: '/path/to/worktree-2',
27
+ isAccessible: true,
28
+ },
29
+ ];
30
+
31
+ it('should render header with title', () => {
32
+ const onBack = vi.fn();
33
+ const onSelect = vi.fn();
34
+ const { getByText } = render(
35
+ <WorktreeManagerScreen worktrees={mockWorktrees} onBack={onBack} onSelect={onSelect} />
36
+ );
37
+
38
+ expect(getByText(/Worktree Manager/i)).toBeDefined();
39
+ });
40
+
41
+ it('should render worktree list', () => {
42
+ const onBack = vi.fn();
43
+ const onSelect = vi.fn();
44
+ const { getByText } = render(
45
+ <WorktreeManagerScreen worktrees={mockWorktrees} onBack={onBack} onSelect={onSelect} />
46
+ );
47
+
48
+ expect(getByText(/feature\/test-1/)).toBeDefined();
49
+ expect(getByText(/feature\/test-2/)).toBeDefined();
50
+ });
51
+
52
+ it('should render footer with actions', () => {
53
+ const onBack = vi.fn();
54
+ const onSelect = vi.fn();
55
+ const { getAllByText } = render(
56
+ <WorktreeManagerScreen worktrees={mockWorktrees} onBack={onBack} onSelect={onSelect} />
57
+ );
58
+
59
+ expect(getAllByText(/enter/i).length).toBeGreaterThan(0);
60
+ expect(getAllByText(/esc/i).length).toBeGreaterThan(0);
61
+ });
62
+
63
+ it('should handle empty worktree list', () => {
64
+ const onBack = vi.fn();
65
+ const onSelect = vi.fn();
66
+ const { getByText } = render(
67
+ <WorktreeManagerScreen worktrees={[]} onBack={onBack} onSelect={onSelect} />
68
+ );
69
+
70
+ expect(getByText(/No worktrees found/i)).toBeDefined();
71
+ });
72
+
73
+ it('should display inaccessible worktrees differently', () => {
74
+ const worktreesWithInaccessible = [
75
+ {
76
+ branch: 'feature/accessible',
77
+ path: '/path/accessible',
78
+ isAccessible: true,
79
+ },
80
+ {
81
+ branch: 'feature/inaccessible',
82
+ path: '/path/inaccessible',
83
+ isAccessible: false,
84
+ },
85
+ ];
86
+
87
+ const onBack = vi.fn();
88
+ const onSelect = vi.fn();
89
+ const { getByText } = render(
90
+ <WorktreeManagerScreen
91
+ worktrees={worktreesWithInaccessible}
92
+ onBack={onBack}
93
+ onSelect={onSelect}
94
+ />
95
+ );
96
+
97
+ expect(getByText(/feature\/accessible/)).toBeDefined();
98
+ expect(getByText(/feature\/inaccessible/)).toBeDefined();
99
+ });
100
+
101
+ it('should use terminal height for layout calculation', () => {
102
+ const originalRows = process.stdout.rows;
103
+ process.stdout.rows = 30;
104
+
105
+ const onBack = vi.fn();
106
+ const onSelect = vi.fn();
107
+ const { container } = render(
108
+ <WorktreeManagerScreen worktrees={mockWorktrees} onBack={onBack} onSelect={onSelect} />
109
+ );
110
+
111
+ expect(container).toBeDefined();
112
+
113
+ process.stdout.rows = originalRows;
114
+ });
115
+
116
+ it('should display worktree count in stats', () => {
117
+ const onBack = vi.fn();
118
+ const onSelect = vi.fn();
119
+ const { getByText, getAllByText } = render(
120
+ <WorktreeManagerScreen worktrees={mockWorktrees} onBack={onBack} onSelect={onSelect} />
121
+ );
122
+
123
+ // Check for worktree count
124
+ expect(getByText(/Total:/i)).toBeDefined();
125
+ expect(getAllByText(/2/).length).toBeGreaterThan(0);
126
+ });
127
+ });