@akiojin/gwt 2.2.0 → 2.3.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 (149) hide show
  1. package/README.ja.md +4 -4
  2. package/README.md +4 -4
  3. package/dist/cli/ui/components/App.d.ts +4 -4
  4. package/dist/cli/ui/components/App.d.ts.map +1 -1
  5. package/dist/cli/ui/components/App.js +144 -105
  6. package/dist/cli/ui/components/App.js.map +1 -1
  7. package/dist/cli/ui/components/common/Confirm.d.ts +1 -1
  8. package/dist/cli/ui/components/common/Confirm.d.ts.map +1 -1
  9. package/dist/cli/ui/components/common/Confirm.js +7 -7
  10. package/dist/cli/ui/components/common/Confirm.js.map +1 -1
  11. package/dist/cli/ui/components/common/ErrorBoundary.d.ts +1 -1
  12. package/dist/cli/ui/components/common/ErrorBoundary.d.ts.map +1 -1
  13. package/dist/cli/ui/components/common/ErrorBoundary.js +4 -4
  14. package/dist/cli/ui/components/common/ErrorBoundary.js.map +1 -1
  15. package/dist/cli/ui/components/common/Input.d.ts +2 -2
  16. package/dist/cli/ui/components/common/Input.d.ts.map +1 -1
  17. package/dist/cli/ui/components/common/Input.js +4 -4
  18. package/dist/cli/ui/components/common/Input.js.map +1 -1
  19. package/dist/cli/ui/components/common/LoadingIndicator.d.ts +1 -1
  20. package/dist/cli/ui/components/common/LoadingIndicator.d.ts.map +1 -1
  21. package/dist/cli/ui/components/common/LoadingIndicator.js +4 -4
  22. package/dist/cli/ui/components/common/LoadingIndicator.js.map +1 -1
  23. package/dist/cli/ui/components/common/Select.d.ts +1 -1
  24. package/dist/cli/ui/components/common/Select.d.ts.map +1 -1
  25. package/dist/cli/ui/components/common/Select.js +11 -12
  26. package/dist/cli/ui/components/common/Select.js.map +1 -1
  27. package/dist/cli/ui/components/screens/AIToolSelectorScreen.d.ts +2 -2
  28. package/dist/cli/ui/components/screens/AIToolSelectorScreen.d.ts.map +1 -1
  29. package/dist/cli/ui/components/screens/AIToolSelectorScreen.js +11 -11
  30. package/dist/cli/ui/components/screens/AIToolSelectorScreen.js.map +1 -1
  31. package/dist/cli/ui/components/screens/BranchCreatorScreen.d.ts +1 -1
  32. package/dist/cli/ui/components/screens/BranchCreatorScreen.d.ts.map +1 -1
  33. package/dist/cli/ui/components/screens/BranchCreatorScreen.js +39 -36
  34. package/dist/cli/ui/components/screens/BranchCreatorScreen.js.map +1 -1
  35. package/dist/cli/ui/components/screens/BranchListScreen.d.ts +3 -3
  36. package/dist/cli/ui/components/screens/BranchListScreen.d.ts.map +1 -1
  37. package/dist/cli/ui/components/screens/BranchListScreen.js +55 -50
  38. package/dist/cli/ui/components/screens/BranchListScreen.js.map +1 -1
  39. package/dist/cli/ui/components/screens/ExecutionModeSelectorScreen.d.ts +2 -2
  40. package/dist/cli/ui/components/screens/ExecutionModeSelectorScreen.d.ts.map +1 -1
  41. package/dist/cli/ui/components/screens/ExecutionModeSelectorScreen.js +25 -25
  42. package/dist/cli/ui/components/screens/ExecutionModeSelectorScreen.js.map +1 -1
  43. package/dist/cli/ui/components/screens/PRCleanupScreen.d.ts +2 -2
  44. package/dist/cli/ui/components/screens/PRCleanupScreen.js +21 -21
  45. package/dist/cli/ui/components/screens/SessionSelectorScreen.d.ts +1 -1
  46. package/dist/cli/ui/components/screens/SessionSelectorScreen.js +8 -8
  47. package/dist/cli/ui/components/screens/WorktreeManagerScreen.d.ts +1 -1
  48. package/dist/cli/ui/components/screens/WorktreeManagerScreen.js +8 -8
  49. package/dist/cli/ui/screens/BranchActionSelectorScreen.d.ts.map +1 -1
  50. package/dist/cli/ui/screens/BranchActionSelectorScreen.js +7 -4
  51. package/dist/cli/ui/screens/BranchActionSelectorScreen.js.map +1 -1
  52. package/dist/cli/ui/types.d.ts.map +1 -1
  53. package/dist/client/assets/{index-V6hDu9KS.js → index-Difv1Hwu.js} +2 -2
  54. package/dist/client/index.html +1 -1
  55. package/dist/config/builtin-tools.d.ts +10 -2
  56. package/dist/config/builtin-tools.d.ts.map +1 -1
  57. package/dist/config/builtin-tools.js +40 -4
  58. package/dist/config/builtin-tools.js.map +1 -1
  59. package/dist/config/index.d.ts.map +1 -1
  60. package/dist/config/index.js.map +1 -1
  61. package/dist/config/tools.d.ts.map +1 -1
  62. package/dist/config/tools.js +4 -3
  63. package/dist/config/tools.js.map +1 -1
  64. package/dist/gemini.d.ts +12 -0
  65. package/dist/gemini.d.ts.map +1 -0
  66. package/dist/gemini.js +154 -0
  67. package/dist/gemini.js.map +1 -0
  68. package/dist/git.d.ts.map +1 -1
  69. package/dist/git.js.map +1 -1
  70. package/dist/index.d.ts.map +1 -1
  71. package/dist/index.js +30 -0
  72. package/dist/index.js.map +1 -1
  73. package/dist/qwen.d.ts +12 -0
  74. package/dist/qwen.d.ts.map +1 -0
  75. package/dist/qwen.js +154 -0
  76. package/dist/qwen.js.map +1 -0
  77. package/dist/services/git.service.d.ts.map +1 -1
  78. package/dist/services/git.service.js.map +1 -1
  79. package/dist/web/client/src/components/BranchGraph.d.ts.map +1 -1
  80. package/dist/web/client/src/components/BranchGraph.js +1 -1
  81. package/dist/web/client/src/components/BranchGraph.js.map +1 -1
  82. package/dist/web/client/src/components/EnvEditor.d.ts.map +1 -1
  83. package/dist/web/client/src/components/EnvEditor.js +7 -4
  84. package/dist/web/client/src/components/EnvEditor.js.map +1 -1
  85. package/dist/web/client/src/pages/BranchDetailPage.d.ts.map +1 -1
  86. package/dist/web/client/src/pages/BranchDetailPage.js +55 -18
  87. package/dist/web/client/src/pages/BranchDetailPage.js.map +1 -1
  88. package/dist/web/client/src/pages/BranchListPage.d.ts.map +1 -1
  89. package/dist/web/client/src/pages/BranchListPage.js +10 -4
  90. package/dist/web/client/src/pages/BranchListPage.js.map +1 -1
  91. package/dist/web/client/src/pages/ConfigManagementPage.d.ts.map +1 -1
  92. package/dist/web/client/src/pages/ConfigManagementPage.js +4 -2
  93. package/dist/web/client/src/pages/ConfigManagementPage.js.map +1 -1
  94. package/package.json +2 -1
  95. package/src/cli/ui/__tests__/acceptance/navigation.acceptance.test.tsx +69 -50
  96. package/src/cli/ui/__tests__/components/App.protected-branch.test.tsx +67 -45
  97. package/src/cli/ui/__tests__/components/App.shortcuts.test.tsx +117 -75
  98. package/src/cli/ui/__tests__/components/App.test.tsx +45 -37
  99. package/src/cli/ui/__tests__/components/common/Confirm.test.tsx +35 -22
  100. package/src/cli/ui/__tests__/components/common/ErrorBoundary.test.tsx +22 -22
  101. package/src/cli/ui/__tests__/components/common/Input.test.tsx +29 -22
  102. package/src/cli/ui/__tests__/components/common/LoadingIndicator.test.tsx +40 -34
  103. package/src/cli/ui/__tests__/components/common/Select.memo.test.tsx +57 -66
  104. package/src/cli/ui/__tests__/components/common/Select.test.tsx +121 -91
  105. package/src/cli/ui/__tests__/components/parts/Footer.test.tsx +18 -16
  106. package/src/cli/ui/__tests__/components/parts/Header.test.tsx +13 -13
  107. package/src/cli/ui/__tests__/components/parts/ScrollableList.test.tsx +20 -20
  108. package/src/cli/ui/__tests__/components/parts/Stats.test.tsx +38 -26
  109. package/src/cli/ui/__tests__/components/screens/AIToolSelectorScreen.test.tsx +31 -31
  110. package/src/cli/ui/__tests__/components/screens/BranchCreatorScreen.test.tsx +73 -37
  111. package/src/cli/ui/__tests__/components/screens/BranchListScreen.test.tsx +261 -153
  112. package/src/cli/ui/__tests__/components/screens/ExecutionModeSelectorScreen.test.tsx +38 -32
  113. package/src/cli/ui/__tests__/components/screens/PRCleanupScreen.test.tsx +39 -39
  114. package/src/cli/ui/__tests__/components/screens/SessionSelectorScreen.test.tsx +49 -21
  115. package/src/cli/ui/__tests__/components/screens/WorktreeManagerScreen.test.tsx +52 -28
  116. package/src/cli/ui/__tests__/integration/edgeCases.test.tsx +84 -48
  117. package/src/cli/ui/__tests__/integration/navigation.test.tsx +111 -83
  118. package/src/cli/ui/__tests__/integration/realtimeUpdate.test.tsx +111 -108
  119. package/src/cli/ui/__tests__/performance/branchList.performance.test.tsx +50 -37
  120. package/src/cli/ui/__tests__/performance/useMemoOptimization.test.tsx +75 -76
  121. package/src/cli/ui/components/App.tsx +247 -150
  122. package/src/cli/ui/components/common/Confirm.tsx +13 -9
  123. package/src/cli/ui/components/common/ErrorBoundary.tsx +8 -5
  124. package/src/cli/ui/components/common/Input.tsx +12 -4
  125. package/src/cli/ui/components/common/LoadingIndicator.tsx +8 -5
  126. package/src/cli/ui/components/common/Select.tsx +28 -17
  127. package/src/cli/ui/components/parts/Header.test.tsx +5 -15
  128. package/src/cli/ui/components/screens/AIToolSelectorScreen.tsx +19 -13
  129. package/src/cli/ui/components/screens/BranchCreatorScreen.tsx +74 -54
  130. package/src/cli/ui/components/screens/BranchListScreen.tsx +92 -75
  131. package/src/cli/ui/components/screens/ExecutionModeSelectorScreen.tsx +35 -28
  132. package/src/cli/ui/components/screens/PRCleanupScreen.tsx +22 -22
  133. package/src/cli/ui/components/screens/SessionSelectorScreen.tsx +8 -8
  134. package/src/cli/ui/components/screens/WorktreeManagerScreen.tsx +8 -8
  135. package/src/cli/ui/screens/BranchActionSelectorScreen.tsx +9 -4
  136. package/src/cli/ui/types.ts +8 -1
  137. package/src/config/builtin-tools.ts +42 -4
  138. package/src/config/index.ts +2 -12
  139. package/src/config/tools.ts +16 -6
  140. package/src/gemini.ts +202 -0
  141. package/src/git.ts +2 -1
  142. package/src/index.ts +30 -0
  143. package/src/qwen.ts +208 -0
  144. package/src/services/git.service.ts +2 -1
  145. package/src/web/client/src/components/BranchGraph.tsx +3 -2
  146. package/src/web/client/src/components/EnvEditor.tsx +44 -11
  147. package/src/web/client/src/pages/BranchDetailPage.tsx +165 -54
  148. package/src/web/client/src/pages/BranchListPage.tsx +37 -13
  149. package/src/web/client/src/pages/ConfigManagementPage.tsx +28 -9
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * @vitest-environment happy-dom
3
3
  */
4
- import { describe, it, expect, vi, beforeEach } from 'vitest';
5
- import { render } from '@testing-library/react';
6
- import React, { useState } from 'react';
7
- import { Window } from 'happy-dom';
8
- import { Select, type SelectItem } from '../../../components/common/Select.js';
4
+ import { describe, it, expect, vi, beforeEach } from "vitest";
5
+ import { render } from "@testing-library/react";
6
+ import React, { useState } from "react";
7
+ import { Window } from "happy-dom";
8
+ import { Select, type SelectItem } from "../../../components/common/Select.js";
9
9
 
10
10
  /**
11
11
  * T082-2: React.memo optimization tests
@@ -16,7 +16,7 @@ import { Select, type SelectItem } from '../../../components/common/Select.js';
16
16
  * The actual functionality works correctly in production.
17
17
  */
18
18
 
19
- describe.skip('Select Component React.memo (T082-2)', () => {
19
+ describe.skip("Select Component React.memo (T082-2)", () => {
20
20
  beforeEach(() => {
21
21
  const window = new Window();
22
22
  globalThis.window = window as any;
@@ -24,15 +24,15 @@ describe.skip('Select Component React.memo (T082-2)', () => {
24
24
  vi.clearAllMocks();
25
25
  });
26
26
 
27
- it('should not re-render when items array reference changes but content is the same', () => {
27
+ it("should not re-render when items array reference changes but content is the same", () => {
28
28
  const onSelect = vi.fn();
29
29
  const renderCount = 0;
30
30
 
31
31
  // Wrapper component to track renders
32
32
  function TestWrapper() {
33
33
  const [items, setItems] = useState<SelectItem[]>([
34
- { label: 'Item 1', value: '1' },
35
- { label: 'Item 2', value: '2' },
34
+ { label: "Item 1", value: "1" },
35
+ { label: "Item 2", value: "2" },
36
36
  ]);
37
37
 
38
38
  const [counter, setCounter] = useState(0);
@@ -40,23 +40,20 @@ describe.skip('Select Component React.memo (T082-2)', () => {
40
40
  return (
41
41
  <div>
42
42
  <div data-testid="counter">{counter}</div>
43
- <Select
44
- items={items}
45
- onSelect={onSelect}
46
- />
43
+ <Select items={items} onSelect={onSelect} />
47
44
  <button
48
45
  data-testid="same-content"
49
46
  onClick={() => {
50
47
  // Create new array with same content
51
48
  setItems([
52
- { label: 'Item 1', value: '1' },
53
- { label: 'Item 2', value: '2' },
49
+ { label: "Item 1", value: "1" },
50
+ { label: "Item 2", value: "2" },
54
51
  ]);
55
52
  }}
56
53
  />
57
54
  <button
58
55
  data-testid="increment"
59
- onClick={() => setCounter(c => c + 1)}
56
+ onClick={() => setCounter((c) => c + 1)}
60
57
  />
61
58
  </div>
62
59
  );
@@ -65,19 +62,19 @@ describe.skip('Select Component React.memo (T082-2)', () => {
65
62
  const { getByTestId } = render(<TestWrapper />);
66
63
 
67
64
  // Click "same-content" button to trigger re-render with same items
68
- const sameContentButton = getByTestId('same-content') as HTMLButtonElement;
65
+ const sameContentButton = getByTestId("same-content") as HTMLButtonElement;
69
66
  sameContentButton.click();
70
67
 
71
68
  // With React.memo, Select should not re-render if items content is the same
72
69
  // Without React.memo, this test would show that Select re-renders unnecessarily
73
70
  });
74
71
 
75
- it('should re-render when items content actually changes', () => {
72
+ it("should re-render when items content actually changes", () => {
76
73
  const onSelect = vi.fn();
77
74
 
78
75
  function TestWrapper() {
79
76
  const [items, setItems] = useState<SelectItem[]>([
80
- { label: 'Item 1', value: '1' },
77
+ { label: "Item 1", value: "1" },
81
78
  ]);
82
79
 
83
80
  return (
@@ -86,10 +83,7 @@ describe.skip('Select Component React.memo (T082-2)', () => {
86
83
  <button
87
84
  data-testid="add-item"
88
85
  onClick={() => {
89
- setItems([
90
- ...items,
91
- { label: 'Item 2', value: '2' },
92
- ]);
86
+ setItems([...items, { label: "Item 2", value: "2" }]);
93
87
  }}
94
88
  />
95
89
  </div>
@@ -99,35 +93,40 @@ describe.skip('Select Component React.memo (T082-2)', () => {
99
93
  const { getByTestId, container } = render(<TestWrapper />);
100
94
 
101
95
  // Initially should have 1 item
102
- expect(container.textContent).toContain('Item 1');
103
- expect(container.textContent).not.toContain('Item 2');
96
+ expect(container.textContent).toContain("Item 1");
97
+ expect(container.textContent).not.toContain("Item 2");
104
98
 
105
99
  // Click "add-item" button
106
- const addButton = getByTestId('add-item') as HTMLButtonElement;
100
+ const addButton = getByTestId("add-item") as HTMLButtonElement;
107
101
  addButton.click();
108
102
 
109
103
  // Should now have 2 items (Select should re-render)
110
- expect(container.textContent).toContain('Item 1');
111
- expect(container.textContent).toContain('Item 2');
104
+ expect(container.textContent).toContain("Item 1");
105
+ expect(container.textContent).toContain("Item 2");
112
106
  });
113
107
 
114
- it('should not re-render when other props are the same', () => {
108
+ it("should not re-render when other props are the same", () => {
115
109
  const onSelect = vi.fn();
116
110
 
117
111
  function TestWrapper() {
118
112
  const [items] = useState<SelectItem[]>([
119
- { label: 'Item 1', value: '1' },
120
- { label: 'Item 2', value: '2' },
113
+ { label: "Item 1", value: "1" },
114
+ { label: "Item 2", value: "2" },
121
115
  ]);
122
116
  const [unrelatedState, setUnrelatedState] = useState(0);
123
117
 
124
118
  return (
125
119
  <div>
126
120
  <div data-testid="unrelated">{unrelatedState}</div>
127
- <Select items={items} onSelect={onSelect} limit={10} disabled={false} />
121
+ <Select
122
+ items={items}
123
+ onSelect={onSelect}
124
+ limit={10}
125
+ disabled={false}
126
+ />
128
127
  <button
129
128
  data-testid="update-unrelated"
130
- onClick={() => setUnrelatedState(s => s + 1)}
129
+ onClick={() => setUnrelatedState((s) => s + 1)}
131
130
  />
132
131
  </div>
133
132
  );
@@ -136,22 +135,22 @@ describe.skip('Select Component React.memo (T082-2)', () => {
136
135
  const { getByTestId } = render(<TestWrapper />);
137
136
 
138
137
  // Update unrelated state
139
- const updateButton = getByTestId('update-unrelated') as HTMLButtonElement;
138
+ const updateButton = getByTestId("update-unrelated") as HTMLButtonElement;
140
139
  updateButton.click();
141
140
 
142
141
  // Verify unrelated state changed
143
- expect(getByTestId('unrelated').textContent).toBe('1');
142
+ expect(getByTestId("unrelated").textContent).toBe("1");
144
143
 
145
144
  // With React.memo, Select should not re-render because its props haven't changed
146
145
  });
147
146
 
148
- it('should re-render when limit prop changes', () => {
147
+ it("should re-render when limit prop changes", () => {
149
148
  const onSelect = vi.fn();
150
149
  const items: SelectItem[] = [
151
- { label: 'Item 1', value: '1' },
152
- { label: 'Item 2', value: '2' },
153
- { label: 'Item 3', value: '3' },
154
- { label: 'Item 4', value: '4' },
150
+ { label: "Item 1", value: "1" },
151
+ { label: "Item 2", value: "2" },
152
+ { label: "Item 3", value: "3" },
153
+ { label: "Item 4", value: "4" },
155
154
  ];
156
155
 
157
156
  function TestWrapper() {
@@ -160,10 +159,7 @@ describe.skip('Select Component React.memo (T082-2)', () => {
160
159
  return (
161
160
  <div>
162
161
  <Select items={items} onSelect={onSelect} limit={limit} />
163
- <button
164
- data-testid="change-limit"
165
- onClick={() => setLimit(3)}
166
- />
162
+ <button data-testid="change-limit" onClick={() => setLimit(3)} />
167
163
  </div>
168
164
  );
169
165
  }
@@ -172,25 +168,23 @@ describe.skip('Select Component React.memo (T082-2)', () => {
172
168
 
173
169
  // Initially should show 2 items (limit=2)
174
170
  const initialText = container.textContent;
175
- expect(initialText).toContain('Item 1');
176
- expect(initialText).toContain('Item 2');
171
+ expect(initialText).toContain("Item 1");
172
+ expect(initialText).toContain("Item 2");
177
173
 
178
174
  // Change limit
179
- const changeLimitButton = getByTestId('change-limit') as HTMLButtonElement;
175
+ const changeLimitButton = getByTestId("change-limit") as HTMLButtonElement;
180
176
  changeLimitButton.click();
181
177
 
182
178
  // Should now show 3 items (Select should re-render)
183
179
  const updatedText = container.textContent;
184
- expect(updatedText).toContain('Item 1');
185
- expect(updatedText).toContain('Item 2');
186
- expect(updatedText).toContain('Item 3');
180
+ expect(updatedText).toContain("Item 1");
181
+ expect(updatedText).toContain("Item 2");
182
+ expect(updatedText).toContain("Item 3");
187
183
  });
188
184
 
189
- it('should re-render when disabled prop changes', () => {
185
+ it("should re-render when disabled prop changes", () => {
190
186
  const onSelect = vi.fn();
191
- const items: SelectItem[] = [
192
- { label: 'Item 1', value: '1' },
193
- ];
187
+ const items: SelectItem[] = [{ label: "Item 1", value: "1" }];
194
188
 
195
189
  function TestWrapper() {
196
190
  const [disabled, setDisabled] = useState(false);
@@ -200,7 +194,7 @@ describe.skip('Select Component React.memo (T082-2)', () => {
200
194
  <Select items={items} onSelect={onSelect} disabled={disabled} />
201
195
  <button
202
196
  data-testid="toggle-disabled"
203
- onClick={() => setDisabled(d => !d)}
197
+ onClick={() => setDisabled((d) => !d)}
204
198
  />
205
199
  </div>
206
200
  );
@@ -209,24 +203,24 @@ describe.skip('Select Component React.memo (T082-2)', () => {
209
203
  const { getByTestId } = render(<TestWrapper />);
210
204
 
211
205
  // Toggle disabled
212
- const toggleButton = getByTestId('toggle-disabled') as HTMLButtonElement;
206
+ const toggleButton = getByTestId("toggle-disabled") as HTMLButtonElement;
213
207
  toggleButton.click();
214
208
 
215
209
  // Select should re-render with new disabled prop
216
210
  });
217
211
 
218
- it('should use custom comparison for items array', () => {
212
+ it("should use custom comparison for items array", () => {
219
213
  const onSelect = vi.fn();
220
214
 
221
215
  // Two arrays with same content but different references
222
216
  const items1: SelectItem[] = [
223
- { label: 'Item 1', value: '1' },
224
- { label: 'Item 2', value: '2' },
217
+ { label: "Item 1", value: "1" },
218
+ { label: "Item 2", value: "2" },
225
219
  ];
226
220
 
227
221
  const items2: SelectItem[] = [
228
- { label: 'Item 1', value: '1' },
229
- { label: 'Item 2', value: '2' },
222
+ { label: "Item 1", value: "1" },
223
+ { label: "Item 2", value: "2" },
230
224
  ];
231
225
 
232
226
  // Verify they're different references
@@ -245,10 +239,7 @@ describe.skip('Select Component React.memo (T082-2)', () => {
245
239
  return (
246
240
  <div>
247
241
  <Select items={items} onSelect={onSelect} />
248
- <button
249
- data-testid="swap-items"
250
- onClick={() => setItems(items2)}
251
- />
242
+ <button data-testid="swap-items" onClick={() => setItems(items2)} />
252
243
  </div>
253
244
  );
254
245
  }
@@ -256,7 +247,7 @@ describe.skip('Select Component React.memo (T082-2)', () => {
256
247
  const { getByTestId } = render(<TestWrapper />);
257
248
 
258
249
  // Swap to items2 (same content, different reference)
259
- const swapButton = getByTestId('swap-items') as HTMLButtonElement;
250
+ const swapButton = getByTestId("swap-items") as HTMLButtonElement;
260
251
  swapButton.click();
261
252
 
262
253
  // With custom comparison in React.memo, Select should not re-render
@@ -2,10 +2,10 @@
2
2
  * @vitest-environment happy-dom
3
3
  */
4
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';
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
9
 
10
10
  // Helper to wait for async updates
11
11
  const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
@@ -15,37 +15,41 @@ interface TestItem {
15
15
  value: string;
16
16
  }
17
17
 
18
- describe('Select', () => {
18
+ describe("Select", () => {
19
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' },
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
25
  ];
26
26
 
27
- describe('Rendering', () => {
28
- it('should render all items', () => {
27
+ describe("Rendering", () => {
28
+ it("should render all items", () => {
29
29
  const onSelect = vi.fn();
30
- const { lastFrame } = render(<Select items={mockItems} onSelect={onSelect} />);
30
+ const { lastFrame } = render(
31
+ <Select items={mockItems} onSelect={onSelect} />,
32
+ );
31
33
 
32
- expect(lastFrame()).toContain('Option 1');
33
- expect(lastFrame()).toContain('Option 2');
34
- expect(lastFrame()).toContain('Option 3');
34
+ expect(lastFrame()).toContain("Option 1");
35
+ expect(lastFrame()).toContain("Option 2");
36
+ expect(lastFrame()).toContain("Option 3");
35
37
  });
36
38
 
37
- it('should highlight first item by default', () => {
39
+ it("should highlight first item by default", () => {
38
40
  const onSelect = vi.fn();
39
- const { lastFrame } = render(<Select items={mockItems} onSelect={onSelect} />);
41
+ const { lastFrame } = render(
42
+ <Select items={mockItems} onSelect={onSelect} />,
43
+ );
40
44
 
41
45
  // Cyan color code indicates selected item
42
- expect(lastFrame()).toContain('');
46
+ expect(lastFrame()).toContain("");
43
47
  });
44
48
 
45
- it('should highlight item at initialIndex', () => {
49
+ it("should highlight item at initialIndex", () => {
46
50
  const onSelect = vi.fn();
47
51
  const { lastFrame } = render(
48
- <Select items={mockItems} onSelect={onSelect} initialIndex={2} />
52
+ <Select items={mockItems} onSelect={onSelect} initialIndex={2} />,
49
53
  );
50
54
 
51
55
  const output = lastFrame();
@@ -54,121 +58,135 @@ describe('Select', () => {
54
58
  expect(selectedCount).toBe(1);
55
59
  });
56
60
 
57
- it('should render with empty items array', () => {
61
+ it("should render with empty items array", () => {
58
62
  const onSelect = vi.fn();
59
63
  const { lastFrame } = render(<Select items={[]} onSelect={onSelect} />);
60
64
 
61
65
  expect(lastFrame()).toBeDefined();
62
66
  });
63
67
 
64
- it('should respect limit prop for scrolling', () => {
68
+ it("should respect limit prop for scrolling", () => {
65
69
  const onSelect = vi.fn();
66
- const { lastFrame } = render(<Select items={mockItems} onSelect={onSelect} limit={3} />);
70
+ const { lastFrame } = render(
71
+ <Select items={mockItems} onSelect={onSelect} limit={3} />,
72
+ );
67
73
 
68
74
  const output = lastFrame();
69
75
  // 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');
76
+ expect(output).toContain("Option 1");
77
+ expect(output).toContain("Option 2");
78
+ expect(output).toContain("Option 3");
73
79
  // Option 4 and 5 should not be visible initially
74
- expect(output).not.toContain('Option 4');
75
- expect(output).not.toContain('Option 5');
80
+ expect(output).not.toContain("Option 4");
81
+ expect(output).not.toContain("Option 5");
76
82
  });
77
83
  });
78
84
 
79
- describe('Navigation - No Looping (Critical Feature)', () => {
80
- it('should implement boundary checks to prevent looping', () => {
85
+ describe("Navigation - No Looping (Critical Feature)", () => {
86
+ it("should implement boundary checks to prevent looping", () => {
81
87
  // Unit test: verify the logic used in implementation
82
88
  // Math.max(0, current - 1) - prevents going below 0
83
89
  // Math.min(items.length - 1, current + 1) - prevents going above max
84
90
 
85
91
  const onSelect = vi.fn();
86
- const { lastFrame } = render(<Select items={mockItems} onSelect={onSelect} />);
92
+ const { lastFrame } = render(
93
+ <Select items={mockItems} onSelect={onSelect} />,
94
+ );
87
95
 
88
96
  // Verify component renders (implementation uses Math.max/min for boundaries)
89
97
  expect(lastFrame()).toBeDefined();
90
- expect(lastFrame()).toContain('Option 1');
98
+ expect(lastFrame()).toContain("Option 1");
91
99
  });
92
100
 
93
- it('should start at first item by default', () => {
101
+ it("should start at first item by default", () => {
94
102
  const onSelect = vi.fn();
95
- const { lastFrame } = render(<Select items={mockItems} onSelect={onSelect} />);
103
+ const { lastFrame } = render(
104
+ <Select items={mockItems} onSelect={onSelect} />,
105
+ );
96
106
 
97
107
  const output = lastFrame();
98
108
  // 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');
109
+ const lines = output.split("\n").filter((l) => l.trim());
110
+ expect(lines[0]).toContain("");
111
+ expect(lines[0]).toContain("Option 1");
102
112
  });
103
113
 
104
- it('should respect initialIndex without looping', () => {
114
+ it("should respect initialIndex without looping", () => {
105
115
  const onSelect = vi.fn();
106
116
  const { lastFrame } = render(
107
- <Select items={mockItems} onSelect={onSelect} initialIndex={4} />
117
+ <Select items={mockItems} onSelect={onSelect} initialIndex={4} />,
108
118
  );
109
119
 
110
120
  // Should start at last item (index 4)
111
121
  const output = lastFrame();
112
- expect(output).toContain('Option 5');
113
- expect(output).toContain('');
122
+ expect(output).toContain("Option 5");
123
+ expect(output).toContain("");
114
124
  });
115
125
 
116
- it('should handle initialIndex at 0', () => {
126
+ it("should handle initialIndex at 0", () => {
117
127
  const onSelect = vi.fn();
118
128
  const { lastFrame } = render(
119
- <Select items={mockItems} onSelect={onSelect} initialIndex={0} />
129
+ <Select items={mockItems} onSelect={onSelect} initialIndex={0} />,
120
130
  );
121
131
 
122
132
  const output = lastFrame();
123
- const lines = output.split('\n').filter((l) => l.trim());
124
- expect(lines[0]).toContain('Option 1');
133
+ const lines = output.split("\n").filter((l) => l.trim());
134
+ expect(lines[0]).toContain("Option 1");
125
135
  });
126
136
  });
127
137
 
128
- describe('Navigation - Input Handling', () => {
129
- it('should use useInput hook for keyboard handling', () => {
138
+ describe("Navigation - Input Handling", () => {
139
+ it("should use useInput hook for keyboard handling", () => {
130
140
  // Verify component accepts keyboard input by checking it renders properly
131
141
  const onSelect = vi.fn();
132
- const { stdin } = render(<Select items={mockItems} onSelect={onSelect} />);
142
+ const { stdin } = render(
143
+ <Select items={mockItems} onSelect={onSelect} />,
144
+ );
133
145
 
134
146
  // 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();
147
+ expect(() => stdin.write("\u001B[B")).not.toThrow();
148
+ expect(() => stdin.write("\u001B[A")).not.toThrow();
149
+ expect(() => stdin.write("j")).not.toThrow();
150
+ expect(() => stdin.write("k")).not.toThrow();
139
151
  });
140
152
 
141
- it('should support vim-style navigation keys (j/k)', () => {
153
+ it("should support vim-style navigation keys (j/k)", () => {
142
154
  const onSelect = vi.fn();
143
- const { stdin } = render(<Select items={mockItems} onSelect={onSelect} />);
155
+ const { stdin } = render(
156
+ <Select items={mockItems} onSelect={onSelect} />,
157
+ );
144
158
 
145
159
  // Should accept j and k keys
146
- expect(() => stdin.write('j')).not.toThrow();
147
- expect(() => stdin.write('k')).not.toThrow();
160
+ expect(() => stdin.write("j")).not.toThrow();
161
+ expect(() => stdin.write("k")).not.toThrow();
148
162
  });
149
163
 
150
- it('should support arrow keys', () => {
164
+ it("should support arrow keys", () => {
151
165
  const onSelect = vi.fn();
152
- const { stdin } = render(<Select items={mockItems} onSelect={onSelect} />);
166
+ const { stdin } = render(
167
+ <Select items={mockItems} onSelect={onSelect} />,
168
+ );
153
169
 
154
170
  // Should accept arrow keys
155
- expect(() => stdin.write('\u001B[A')).not.toThrow(); // Up
156
- expect(() => stdin.write('\u001B[B')).not.toThrow(); // Down
171
+ expect(() => stdin.write("\u001B[A")).not.toThrow(); // Up
172
+ expect(() => stdin.write("\u001B[B")).not.toThrow(); // Down
157
173
  });
158
174
  });
159
175
 
160
- describe('Selection', () => {
161
- it('should call onSelect when Enter is pressed', () => {
176
+ describe("Selection", () => {
177
+ it("should call onSelect when Enter is pressed", () => {
162
178
  const onSelect = vi.fn();
163
- const { stdin } = render(<Select items={mockItems} onSelect={onSelect} />);
179
+ const { stdin } = render(
180
+ <Select items={mockItems} onSelect={onSelect} />,
181
+ );
164
182
 
165
- stdin.write('\r'); // Enter key
183
+ stdin.write("\r"); // Enter key
166
184
 
167
185
  // Should be called at least once
168
186
  expect(onSelect).toHaveBeenCalled();
169
187
  });
170
188
 
171
- it('should pass selected item to onSelect callback', () => {
189
+ it("should pass selected item to onSelect callback", () => {
172
190
  const onSelect = vi.fn();
173
191
  render(<Select items={mockItems} onSelect={onSelect} initialIndex={2} />);
174
192
 
@@ -177,67 +195,79 @@ describe('Select', () => {
177
195
  expect(onSelect).toBeInstanceOf(Function);
178
196
  });
179
197
 
180
- it('should handle Enter key without errors', () => {
198
+ it("should handle Enter key without errors", () => {
181
199
  const onSelect = vi.fn();
182
- const { stdin } = render(<Select items={mockItems} onSelect={onSelect} />);
200
+ const { stdin } = render(
201
+ <Select items={mockItems} onSelect={onSelect} />,
202
+ );
183
203
 
184
- expect(() => stdin.write('\r')).not.toThrow();
204
+ expect(() => stdin.write("\r")).not.toThrow();
185
205
  });
186
206
  });
187
207
 
188
- describe('Scrolling with limit', () => {
189
- it('should implement offset-based scrolling logic', () => {
208
+ describe("Scrolling with limit", () => {
209
+ it("should implement offset-based scrolling logic", () => {
190
210
  // Verify limit prop is accepted and used for slicing
191
211
  const onSelect = vi.fn();
192
- const { lastFrame } = render(<Select items={mockItems} onSelect={onSelect} limit={3} />);
212
+ const { lastFrame } = render(
213
+ <Select items={mockItems} onSelect={onSelect} limit={3} />,
214
+ );
193
215
 
194
216
  const output = lastFrame();
195
217
  // Should show limited items initially
196
- expect(output).toContain('Option 1');
197
- expect(output).toContain('Option 2');
198
- expect(output).toContain('Option 3');
218
+ expect(output).toContain("Option 1");
219
+ expect(output).toContain("Option 2");
220
+ expect(output).toContain("Option 3");
199
221
  });
200
222
 
201
- it('should handle limit smaller than items length', () => {
223
+ it("should handle limit smaller than items length", () => {
202
224
  const onSelect = vi.fn();
203
- const { lastFrame } = render(<Select items={mockItems} onSelect={onSelect} limit={2} />);
225
+ const { lastFrame } = render(
226
+ <Select items={mockItems} onSelect={onSelect} limit={2} />,
227
+ );
204
228
 
205
229
  const output = lastFrame();
206
- const lines = output.split('\n').filter((l) => l.trim());
230
+ const lines = output.split("\n").filter((l) => l.trim());
207
231
  // Should only show 2 items
208
232
  expect(lines.length).toBeLessThanOrEqual(2);
209
233
  });
210
234
 
211
- it('should handle limit larger than items length', () => {
235
+ it("should handle limit larger than items length", () => {
212
236
  const onSelect = vi.fn();
213
- const { lastFrame } = render(<Select items={mockItems} onSelect={onSelect} limit={100} />);
237
+ const { lastFrame } = render(
238
+ <Select items={mockItems} onSelect={onSelect} limit={100} />,
239
+ );
214
240
 
215
241
  // Should show all items without error
216
242
  const output = lastFrame();
217
- expect(output).toContain('Option 1');
218
- expect(output).toContain('Option 5');
243
+ expect(output).toContain("Option 1");
244
+ expect(output).toContain("Option 5");
219
245
  });
220
246
  });
221
247
 
222
- describe('Key propagation (Critical Feature)', () => {
223
- it('should not interfere with other keys like q', () => {
248
+ describe("Key propagation (Critical Feature)", () => {
249
+ it("should not interfere with other keys like q", () => {
224
250
  const onSelect = vi.fn();
225
- const { stdin } = render(<Select items={mockItems} onSelect={onSelect} />);
251
+ const { stdin } = render(
252
+ <Select items={mockItems} onSelect={onSelect} />,
253
+ );
226
254
 
227
255
  // Press q key (should be ignored by Select and propagate to parent)
228
- stdin.write('q');
256
+ stdin.write("q");
229
257
 
230
258
  // onSelect should not be called
231
259
  expect(onSelect).not.toHaveBeenCalled();
232
260
  });
233
261
 
234
- it('should not interfere with other keys like m, n, c', () => {
262
+ it("should not interfere with other keys like m, n, c", () => {
235
263
  const onSelect = vi.fn();
236
- const { stdin } = render(<Select items={mockItems} onSelect={onSelect} />);
264
+ const { stdin } = render(
265
+ <Select items={mockItems} onSelect={onSelect} />,
266
+ );
237
267
 
238
- stdin.write('m');
239
- stdin.write('n');
240
- stdin.write('c');
268
+ stdin.write("m");
269
+ stdin.write("n");
270
+ stdin.write("c");
241
271
 
242
272
  // None of these should trigger selection
243
273
  expect(onSelect).not.toHaveBeenCalled();