@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,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment happy-dom
|
|
3
|
+
*/
|
|
4
|
+
/* eslint-disable no-control-regex */
|
|
5
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
6
|
+
import { render } from 'ink-testing-library';
|
|
7
|
+
import React from 'react';
|
|
8
|
+
import { Select } from '../../../components/common/Select.js';
|
|
9
|
+
|
|
10
|
+
// Helper to wait for async updates
|
|
11
|
+
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
12
|
+
|
|
13
|
+
interface TestItem {
|
|
14
|
+
label: string;
|
|
15
|
+
value: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('Select', () => {
|
|
19
|
+
const mockItems: TestItem[] = [
|
|
20
|
+
{ label: 'Option 1', value: 'opt1' },
|
|
21
|
+
{ label: 'Option 2', value: 'opt2' },
|
|
22
|
+
{ label: 'Option 3', value: 'opt3' },
|
|
23
|
+
{ label: 'Option 4', value: 'opt4' },
|
|
24
|
+
{ label: 'Option 5', value: 'opt5' },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
describe('Rendering', () => {
|
|
28
|
+
it('should render all items', () => {
|
|
29
|
+
const onSelect = vi.fn();
|
|
30
|
+
const { lastFrame } = render(<Select items={mockItems} onSelect={onSelect} />);
|
|
31
|
+
|
|
32
|
+
expect(lastFrame()).toContain('Option 1');
|
|
33
|
+
expect(lastFrame()).toContain('Option 2');
|
|
34
|
+
expect(lastFrame()).toContain('Option 3');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should highlight first item by default', () => {
|
|
38
|
+
const onSelect = vi.fn();
|
|
39
|
+
const { lastFrame } = render(<Select items={mockItems} onSelect={onSelect} />);
|
|
40
|
+
|
|
41
|
+
// Cyan color code indicates selected item
|
|
42
|
+
expect(lastFrame()).toContain('›');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should highlight item at initialIndex', () => {
|
|
46
|
+
const onSelect = vi.fn();
|
|
47
|
+
const { lastFrame } = render(
|
|
48
|
+
<Select items={mockItems} onSelect={onSelect} initialIndex={2} />
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const output = lastFrame();
|
|
52
|
+
// Should have exactly one selected indicator
|
|
53
|
+
const selectedCount = (output.match(/›/g) || []).length;
|
|
54
|
+
expect(selectedCount).toBe(1);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should render with empty items array', () => {
|
|
58
|
+
const onSelect = vi.fn();
|
|
59
|
+
const { lastFrame } = render(<Select items={[]} onSelect={onSelect} />);
|
|
60
|
+
|
|
61
|
+
expect(lastFrame()).toBeDefined();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should respect limit prop for scrolling', () => {
|
|
65
|
+
const onSelect = vi.fn();
|
|
66
|
+
const { lastFrame } = render(<Select items={mockItems} onSelect={onSelect} limit={3} />);
|
|
67
|
+
|
|
68
|
+
const output = lastFrame();
|
|
69
|
+
// Should only show 3 items when limit is 3
|
|
70
|
+
expect(output).toContain('Option 1');
|
|
71
|
+
expect(output).toContain('Option 2');
|
|
72
|
+
expect(output).toContain('Option 3');
|
|
73
|
+
// Option 4 and 5 should not be visible initially
|
|
74
|
+
expect(output).not.toContain('Option 4');
|
|
75
|
+
expect(output).not.toContain('Option 5');
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('Navigation - No Looping (Critical Feature)', () => {
|
|
80
|
+
it('should implement boundary checks to prevent looping', () => {
|
|
81
|
+
// Unit test: verify the logic used in implementation
|
|
82
|
+
// Math.max(0, current - 1) - prevents going below 0
|
|
83
|
+
// Math.min(items.length - 1, current + 1) - prevents going above max
|
|
84
|
+
|
|
85
|
+
const onSelect = vi.fn();
|
|
86
|
+
const { lastFrame } = render(<Select items={mockItems} onSelect={onSelect} />);
|
|
87
|
+
|
|
88
|
+
// Verify component renders (implementation uses Math.max/min for boundaries)
|
|
89
|
+
expect(lastFrame()).toBeDefined();
|
|
90
|
+
expect(lastFrame()).toContain('Option 1');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should start at first item by default', () => {
|
|
94
|
+
const onSelect = vi.fn();
|
|
95
|
+
const { lastFrame } = render(<Select items={mockItems} onSelect={onSelect} />);
|
|
96
|
+
|
|
97
|
+
const output = lastFrame();
|
|
98
|
+
// First line should have the selection indicator
|
|
99
|
+
const lines = output.split('\n').filter((l) => l.trim());
|
|
100
|
+
expect(lines[0]).toContain('›');
|
|
101
|
+
expect(lines[0]).toContain('Option 1');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should respect initialIndex without looping', () => {
|
|
105
|
+
const onSelect = vi.fn();
|
|
106
|
+
const { lastFrame } = render(
|
|
107
|
+
<Select items={mockItems} onSelect={onSelect} initialIndex={4} />
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
// Should start at last item (index 4)
|
|
111
|
+
const output = lastFrame();
|
|
112
|
+
expect(output).toContain('Option 5');
|
|
113
|
+
expect(output).toContain('›');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should handle initialIndex at 0', () => {
|
|
117
|
+
const onSelect = vi.fn();
|
|
118
|
+
const { lastFrame } = render(
|
|
119
|
+
<Select items={mockItems} onSelect={onSelect} initialIndex={0} />
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const output = lastFrame();
|
|
123
|
+
const lines = output.split('\n').filter((l) => l.trim());
|
|
124
|
+
expect(lines[0]).toContain('Option 1');
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe('Navigation - Input Handling', () => {
|
|
129
|
+
it('should use useInput hook for keyboard handling', () => {
|
|
130
|
+
// Verify component accepts keyboard input by checking it renders properly
|
|
131
|
+
const onSelect = vi.fn();
|
|
132
|
+
const { stdin } = render(<Select items={mockItems} onSelect={onSelect} />);
|
|
133
|
+
|
|
134
|
+
// Component should handle input without errors
|
|
135
|
+
expect(() => stdin.write('\u001B[B')).not.toThrow();
|
|
136
|
+
expect(() => stdin.write('\u001B[A')).not.toThrow();
|
|
137
|
+
expect(() => stdin.write('j')).not.toThrow();
|
|
138
|
+
expect(() => stdin.write('k')).not.toThrow();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should support vim-style navigation keys (j/k)', () => {
|
|
142
|
+
const onSelect = vi.fn();
|
|
143
|
+
const { stdin } = render(<Select items={mockItems} onSelect={onSelect} />);
|
|
144
|
+
|
|
145
|
+
// Should accept j and k keys
|
|
146
|
+
expect(() => stdin.write('j')).not.toThrow();
|
|
147
|
+
expect(() => stdin.write('k')).not.toThrow();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should support arrow keys', () => {
|
|
151
|
+
const onSelect = vi.fn();
|
|
152
|
+
const { stdin } = render(<Select items={mockItems} onSelect={onSelect} />);
|
|
153
|
+
|
|
154
|
+
// Should accept arrow keys
|
|
155
|
+
expect(() => stdin.write('\u001B[A')).not.toThrow(); // Up
|
|
156
|
+
expect(() => stdin.write('\u001B[B')).not.toThrow(); // Down
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe('Selection', () => {
|
|
161
|
+
it('should call onSelect when Enter is pressed', () => {
|
|
162
|
+
const onSelect = vi.fn();
|
|
163
|
+
const { stdin } = render(<Select items={mockItems} onSelect={onSelect} />);
|
|
164
|
+
|
|
165
|
+
stdin.write('\r'); // Enter key
|
|
166
|
+
|
|
167
|
+
// Should be called at least once
|
|
168
|
+
expect(onSelect).toHaveBeenCalled();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should pass selected item to onSelect callback', () => {
|
|
172
|
+
const onSelect = vi.fn();
|
|
173
|
+
render(<Select items={mockItems} onSelect={onSelect} initialIndex={2} />);
|
|
174
|
+
|
|
175
|
+
// onSelect should be configured to receive item objects
|
|
176
|
+
// Actual keyboard testing is limited by ink-testing-library
|
|
177
|
+
expect(onSelect).toBeInstanceOf(Function);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should handle Enter key without errors', () => {
|
|
181
|
+
const onSelect = vi.fn();
|
|
182
|
+
const { stdin } = render(<Select items={mockItems} onSelect={onSelect} />);
|
|
183
|
+
|
|
184
|
+
expect(() => stdin.write('\r')).not.toThrow();
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe('Scrolling with limit', () => {
|
|
189
|
+
it('should implement offset-based scrolling logic', () => {
|
|
190
|
+
// Verify limit prop is accepted and used for slicing
|
|
191
|
+
const onSelect = vi.fn();
|
|
192
|
+
const { lastFrame } = render(<Select items={mockItems} onSelect={onSelect} limit={3} />);
|
|
193
|
+
|
|
194
|
+
const output = lastFrame();
|
|
195
|
+
// Should show limited items initially
|
|
196
|
+
expect(output).toContain('Option 1');
|
|
197
|
+
expect(output).toContain('Option 2');
|
|
198
|
+
expect(output).toContain('Option 3');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('should handle limit smaller than items length', () => {
|
|
202
|
+
const onSelect = vi.fn();
|
|
203
|
+
const { lastFrame } = render(<Select items={mockItems} onSelect={onSelect} limit={2} />);
|
|
204
|
+
|
|
205
|
+
const output = lastFrame();
|
|
206
|
+
const lines = output.split('\n').filter((l) => l.trim());
|
|
207
|
+
// Should only show 2 items
|
|
208
|
+
expect(lines.length).toBeLessThanOrEqual(2);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('should handle limit larger than items length', () => {
|
|
212
|
+
const onSelect = vi.fn();
|
|
213
|
+
const { lastFrame } = render(<Select items={mockItems} onSelect={onSelect} limit={100} />);
|
|
214
|
+
|
|
215
|
+
// Should show all items without error
|
|
216
|
+
const output = lastFrame();
|
|
217
|
+
expect(output).toContain('Option 1');
|
|
218
|
+
expect(output).toContain('Option 5');
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe('Key propagation (Critical Feature)', () => {
|
|
223
|
+
it('should not interfere with other keys like q', () => {
|
|
224
|
+
const onSelect = vi.fn();
|
|
225
|
+
const { stdin } = render(<Select items={mockItems} onSelect={onSelect} />);
|
|
226
|
+
|
|
227
|
+
// Press q key (should be ignored by Select and propagate to parent)
|
|
228
|
+
stdin.write('q');
|
|
229
|
+
|
|
230
|
+
// onSelect should not be called
|
|
231
|
+
expect(onSelect).not.toHaveBeenCalled();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('should not interfere with other keys like m, n, c', () => {
|
|
235
|
+
const onSelect = vi.fn();
|
|
236
|
+
const { stdin } = render(<Select items={mockItems} onSelect={onSelect} />);
|
|
237
|
+
|
|
238
|
+
stdin.write('m');
|
|
239
|
+
stdin.write('n');
|
|
240
|
+
stdin.write('c');
|
|
241
|
+
|
|
242
|
+
// None of these should trigger selection
|
|
243
|
+
expect(onSelect).not.toHaveBeenCalled();
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment happy-dom
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
5
|
+
import { render } from '@testing-library/react';
|
|
6
|
+
import React from 'react';
|
|
7
|
+
import { Footer } from '../../../components/parts/Footer.js';
|
|
8
|
+
import { Window } from 'happy-dom';
|
|
9
|
+
|
|
10
|
+
describe('Footer', () => {
|
|
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 mockActions = [
|
|
19
|
+
{ key: 'enter', description: 'Select' },
|
|
20
|
+
{ key: 'esc', description: 'Back' },
|
|
21
|
+
{ key: 'h', description: 'Help' },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
it('should render all actions', () => {
|
|
25
|
+
const { getByText } = render(<Footer actions={mockActions} />);
|
|
26
|
+
|
|
27
|
+
expect(getByText(/enter/)).toBeDefined();
|
|
28
|
+
expect(getByText(/Select/)).toBeDefined();
|
|
29
|
+
expect(getByText(/esc/)).toBeDefined();
|
|
30
|
+
expect(getByText(/Back/)).toBeDefined();
|
|
31
|
+
expect(getByText(/h/)).toBeDefined();
|
|
32
|
+
expect(getByText(/Help/)).toBeDefined();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should render with empty actions array', () => {
|
|
36
|
+
const { container } = render(<Footer actions={[]} />);
|
|
37
|
+
|
|
38
|
+
expect(container).toBeDefined();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should render single action', () => {
|
|
42
|
+
const singleAction = [{ key: 'esc', description: 'Exit' }];
|
|
43
|
+
const { getByText } = render(<Footer actions={singleAction} />);
|
|
44
|
+
|
|
45
|
+
expect(getByText(/esc/)).toBeDefined();
|
|
46
|
+
expect(getByText(/Exit/)).toBeDefined();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should render actions in a horizontal layout', () => {
|
|
50
|
+
const { container } = render(<Footer actions={mockActions} />);
|
|
51
|
+
|
|
52
|
+
// Verify component renders without error
|
|
53
|
+
expect(container).toBeDefined();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should accept custom separator', () => {
|
|
57
|
+
const { getAllByText } = render(<Footer actions={mockActions} separator=" | " />);
|
|
58
|
+
|
|
59
|
+
const separators = getAllByText(/\|/);
|
|
60
|
+
expect(separators.length).toBeGreaterThan(0);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment happy-dom
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
5
|
+
import { render } from '@testing-library/react';
|
|
6
|
+
import React from 'react';
|
|
7
|
+
import { Header } from '../../../components/parts/Header.js';
|
|
8
|
+
import { Window } from 'happy-dom';
|
|
9
|
+
|
|
10
|
+
describe('Header', () => {
|
|
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 title', () => {
|
|
19
|
+
const { getByText } = render(<Header title="gwt" />);
|
|
20
|
+
|
|
21
|
+
expect(getByText('gwt')).toBeDefined();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should render divider', () => {
|
|
25
|
+
const { getByText } = render(<Header title="Test" />);
|
|
26
|
+
|
|
27
|
+
// Check for a line of dashes (divider)
|
|
28
|
+
expect(getByText(/─+/)).toBeDefined();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should render title in bold and cyan by default', () => {
|
|
32
|
+
const { container } = render(<Header title="Test Title" />);
|
|
33
|
+
|
|
34
|
+
expect(container).toBeDefined();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should accept custom title color', () => {
|
|
38
|
+
const { container } = render(<Header title="Test" titleColor="green" />);
|
|
39
|
+
|
|
40
|
+
expect(container).toBeDefined();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should accept custom divider character', () => {
|
|
44
|
+
const { getByText } = render(<Header title="Test" dividerChar="=" />);
|
|
45
|
+
|
|
46
|
+
expect(getByText(/=+/)).toBeDefined();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should render without divider when showDivider is false', () => {
|
|
50
|
+
const { queryByText } = render(<Header title="Test" showDivider={false} />);
|
|
51
|
+
|
|
52
|
+
expect(queryByText(/─+/)).toBeNull();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment happy-dom
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
5
|
+
import { render } from '@testing-library/react';
|
|
6
|
+
import React from 'react';
|
|
7
|
+
import { ScrollableList } from '../../../components/parts/ScrollableList.js';
|
|
8
|
+
import { Text } from 'ink';
|
|
9
|
+
import { Window } from 'happy-dom';
|
|
10
|
+
|
|
11
|
+
describe('ScrollableList', () => {
|
|
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
|
+
it('should render children', () => {
|
|
20
|
+
const { getByText } = render(
|
|
21
|
+
<ScrollableList>
|
|
22
|
+
<Text>Item 1</Text>
|
|
23
|
+
<Text>Item 2</Text>
|
|
24
|
+
<Text>Item 3</Text>
|
|
25
|
+
</ScrollableList>
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
expect(getByText('Item 1')).toBeDefined();
|
|
29
|
+
expect(getByText('Item 2')).toBeDefined();
|
|
30
|
+
expect(getByText('Item 3')).toBeDefined();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should render with no children', () => {
|
|
34
|
+
const { container } = render(<ScrollableList>{null}</ScrollableList>);
|
|
35
|
+
|
|
36
|
+
expect(container).toBeDefined();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should accept maxHeight prop', () => {
|
|
40
|
+
const { container } = render(
|
|
41
|
+
<ScrollableList maxHeight={10}>
|
|
42
|
+
<Text>Content</Text>
|
|
43
|
+
</ScrollableList>
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
expect(container).toBeDefined();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should render in a vertical layout', () => {
|
|
50
|
+
const { container } = render(
|
|
51
|
+
<ScrollableList>
|
|
52
|
+
<Text>Content</Text>
|
|
53
|
+
</ScrollableList>
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
expect(container).toBeDefined();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should handle single child', () => {
|
|
60
|
+
const { getByText } = render(
|
|
61
|
+
<ScrollableList>
|
|
62
|
+
<Text>Single Item</Text>
|
|
63
|
+
</ScrollableList>
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
expect(getByText('Single Item')).toBeDefined();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment happy-dom
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
5
|
+
import { render } from '@testing-library/react';
|
|
6
|
+
import React from 'react';
|
|
7
|
+
import { Stats } from '../../../components/parts/Stats.js';
|
|
8
|
+
import type { Statistics } from '../../../types.js';
|
|
9
|
+
import { Window } from 'happy-dom';
|
|
10
|
+
|
|
11
|
+
describe('Stats', () => {
|
|
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 mockStats: Statistics = {
|
|
20
|
+
localCount: 10,
|
|
21
|
+
remoteCount: 8,
|
|
22
|
+
worktreeCount: 3,
|
|
23
|
+
changesCount: 2,
|
|
24
|
+
lastUpdated: new Date('2025-01-25T12:00:00Z'),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
it('should render all statistics', () => {
|
|
28
|
+
const { getByText } = render(<Stats stats={mockStats} />);
|
|
29
|
+
|
|
30
|
+
expect(getByText(/Local:/)).toBeDefined();
|
|
31
|
+
expect(getByText(/10/)).toBeDefined();
|
|
32
|
+
expect(getByText(/Remote:/)).toBeDefined();
|
|
33
|
+
expect(getByText(/8/)).toBeDefined();
|
|
34
|
+
expect(getByText(/Worktrees:/)).toBeDefined();
|
|
35
|
+
expect(getByText(/3/)).toBeDefined();
|
|
36
|
+
expect(getByText(/Changes:/)).toBeDefined();
|
|
37
|
+
expect(getByText(/2/)).toBeDefined();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should render with zero counts', () => {
|
|
41
|
+
const zeroStats: Statistics = {
|
|
42
|
+
localCount: 0,
|
|
43
|
+
remoteCount: 0,
|
|
44
|
+
worktreeCount: 0,
|
|
45
|
+
changesCount: 0,
|
|
46
|
+
lastUpdated: new Date(),
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const { getByText, getAllByText } = render(<Stats stats={zeroStats} />);
|
|
50
|
+
|
|
51
|
+
expect(getByText(/Local:/)).toBeDefined();
|
|
52
|
+
const zeros = getAllByText(/0/);
|
|
53
|
+
expect(zeros.length).toBe(4); // All 4 counts are 0
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should render in a horizontal layout', () => {
|
|
57
|
+
const { container } = render(<Stats stats={mockStats} />);
|
|
58
|
+
|
|
59
|
+
// Verify component renders without error
|
|
60
|
+
expect(container).toBeDefined();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should accept custom separator', () => {
|
|
64
|
+
const { getAllByText } = render(<Stats stats={mockStats} separator=" | " />);
|
|
65
|
+
|
|
66
|
+
const separators = getAllByText(/\|/);
|
|
67
|
+
expect(separators.length).toBeGreaterThan(0);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should handle large numbers', () => {
|
|
71
|
+
const largeStats: Statistics = {
|
|
72
|
+
localCount: 999,
|
|
73
|
+
remoteCount: 888,
|
|
74
|
+
worktreeCount: 777,
|
|
75
|
+
changesCount: 666,
|
|
76
|
+
lastUpdated: new Date(),
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const { getByText } = render(<Stats stats={largeStats} />);
|
|
80
|
+
|
|
81
|
+
expect(getByText(/999/)).toBeDefined();
|
|
82
|
+
expect(getByText(/888/)).toBeDefined();
|
|
83
|
+
expect(getByText(/777/)).toBeDefined();
|
|
84
|
+
expect(getByText(/666/)).toBeDefined();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should display lastUpdated when provided', () => {
|
|
88
|
+
const now = new Date();
|
|
89
|
+
const lastUpdated = new Date(now.getTime() - 5000); // 5 seconds ago
|
|
90
|
+
|
|
91
|
+
const { getByText } = render(<Stats stats={mockStats} lastUpdated={lastUpdated} />);
|
|
92
|
+
|
|
93
|
+
expect(getByText(/Updated:/)).toBeDefined();
|
|
94
|
+
expect(getByText(/ago/)).toBeDefined();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should not display lastUpdated when null', () => {
|
|
98
|
+
const { queryByText } = render(<Stats stats={mockStats} lastUpdated={null} />);
|
|
99
|
+
|
|
100
|
+
expect(queryByText(/Updated:/)).toBeNull();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should not display lastUpdated when not provided', () => {
|
|
104
|
+
const { queryByText } = render(<Stats stats={mockStats} />);
|
|
105
|
+
|
|
106
|
+
expect(queryByText(/Updated:/)).toBeNull();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should format relative time correctly (seconds)', () => {
|
|
110
|
+
const now = new Date();
|
|
111
|
+
const lastUpdated = new Date(now.getTime() - 30000); // 30 seconds ago
|
|
112
|
+
|
|
113
|
+
const { getByText } = render(<Stats stats={mockStats} lastUpdated={lastUpdated} />);
|
|
114
|
+
|
|
115
|
+
expect(getByText(/30s ago/)).toBeDefined();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should format relative time correctly (minutes)', () => {
|
|
119
|
+
const now = new Date();
|
|
120
|
+
const lastUpdated = new Date(now.getTime() - 120000); // 2 minutes ago
|
|
121
|
+
|
|
122
|
+
const { getByText } = render(<Stats stats={mockStats} lastUpdated={lastUpdated} />);
|
|
123
|
+
|
|
124
|
+
expect(getByText(/2m ago/)).toBeDefined();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should format relative time correctly (hours)', () => {
|
|
128
|
+
const now = new Date();
|
|
129
|
+
const lastUpdated = new Date(now.getTime() - 7200000); // 2 hours ago
|
|
130
|
+
|
|
131
|
+
const { getByText } = render(<Stats stats={mockStats} lastUpdated={lastUpdated} />);
|
|
132
|
+
|
|
133
|
+
expect(getByText(/2h ago/)).toBeDefined();
|
|
134
|
+
});
|
|
135
|
+
});
|