@google/gemini-cli 0.7.0-preview.1 → 0.7.1-reland

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 (193) hide show
  1. package/dist/package.json +3 -4
  2. package/dist/src/commands/extensions/install.js +1 -1
  3. package/dist/src/commands/extensions/install.js.map +1 -1
  4. package/dist/src/commands/extensions/install.test.js +2 -8
  5. package/dist/src/commands/extensions/install.test.js.map +1 -1
  6. package/dist/src/commands/extensions/new.test.js +2 -2
  7. package/dist/src/commands/extensions/new.test.js.map +1 -1
  8. package/dist/src/commands/extensions/uninstall.test.js +1 -4
  9. package/dist/src/commands/extensions/uninstall.test.js.map +1 -1
  10. package/dist/src/commands/extensions/update.js +16 -17
  11. package/dist/src/commands/extensions/update.js.map +1 -1
  12. package/dist/src/commands/mcp/add.js +1 -6
  13. package/dist/src/commands/mcp/add.js.map +1 -1
  14. package/dist/src/config/config.d.ts +0 -2
  15. package/dist/src/config/config.js +21 -38
  16. package/dist/src/config/config.js.map +1 -1
  17. package/dist/src/config/extension.d.ts +1 -1
  18. package/dist/src/config/extension.js +59 -71
  19. package/dist/src/config/extension.js.map +1 -1
  20. package/dist/src/config/extensions/github.d.ts +2 -6
  21. package/dist/src/config/extensions/github.js +32 -59
  22. package/dist/src/config/extensions/github.js.map +1 -1
  23. package/dist/src/config/extensions/github.test.js +1 -5
  24. package/dist/src/config/extensions/github.test.js.map +1 -1
  25. package/dist/src/config/extensions/update.d.ts +1 -1
  26. package/dist/src/config/extensions/update.js +2 -2
  27. package/dist/src/config/extensions/update.js.map +1 -1
  28. package/dist/src/config/extensions/update.test.js +15 -50
  29. package/dist/src/config/extensions/update.test.js.map +1 -1
  30. package/dist/src/config/settings.js +3 -18
  31. package/dist/src/config/settings.js.map +1 -1
  32. package/dist/src/config/settingsSchema.d.ts +2 -20
  33. package/dist/src/config/settingsSchema.js +2 -20
  34. package/dist/src/config/settingsSchema.js.map +1 -1
  35. package/dist/src/config/settingsSchema.test.js +1 -1
  36. package/dist/src/config/settingsSchema.test.js.map +1 -1
  37. package/dist/src/config/trustedFolders.d.ts +1 -10
  38. package/dist/src/config/trustedFolders.js +14 -40
  39. package/dist/src/config/trustedFolders.js.map +1 -1
  40. package/dist/src/config/trustedFolders.test.js +14 -95
  41. package/dist/src/config/trustedFolders.test.js.map +1 -1
  42. package/dist/src/gemini.js +126 -100
  43. package/dist/src/gemini.js.map +1 -1
  44. package/dist/src/gemini.test.js +5 -74
  45. package/dist/src/gemini.test.js.map +1 -1
  46. package/dist/src/generated/git-commit.d.ts +2 -2
  47. package/dist/src/generated/git-commit.js +2 -2
  48. package/dist/src/generated/git-commit.js.map +1 -1
  49. package/dist/src/services/BuiltinCommandLoader.js +0 -4
  50. package/dist/src/services/BuiltinCommandLoader.js.map +1 -1
  51. package/dist/src/services/BuiltinCommandLoader.test.js +1 -50
  52. package/dist/src/services/BuiltinCommandLoader.test.js.map +1 -1
  53. package/dist/src/test-utils/render.d.ts +1 -3
  54. package/dist/src/test-utils/render.js +1 -2
  55. package/dist/src/test-utils/render.js.map +1 -1
  56. package/dist/src/ui/App.js +1 -1
  57. package/dist/src/ui/App.js.map +1 -1
  58. package/dist/src/ui/AppContainer.js +12 -32
  59. package/dist/src/ui/AppContainer.js.map +1 -1
  60. package/dist/src/ui/AppContainer.test.js +0 -31
  61. package/dist/src/ui/AppContainer.test.js.map +1 -1
  62. package/dist/src/ui/commands/chatCommand.js +3 -14
  63. package/dist/src/ui/commands/chatCommand.js.map +1 -1
  64. package/dist/src/ui/commands/types.d.ts +1 -1
  65. package/dist/src/ui/commands/types.js.map +1 -1
  66. package/dist/src/ui/components/AppHeader.js +5 -2
  67. package/dist/src/ui/components/AppHeader.js.map +1 -1
  68. package/dist/src/ui/components/Composer.js +4 -2
  69. package/dist/src/ui/components/Composer.js.map +1 -1
  70. package/dist/src/ui/components/DialogManager.d.ts +1 -6
  71. package/dist/src/ui/components/DialogManager.js +1 -10
  72. package/dist/src/ui/components/DialogManager.js.map +1 -1
  73. package/dist/src/ui/components/HistoryItemDisplay.d.ts +1 -1
  74. package/dist/src/ui/components/HistoryItemDisplay.js +1 -1
  75. package/dist/src/ui/components/HistoryItemDisplay.js.map +1 -1
  76. package/dist/src/ui/components/InputPrompt.d.ts +1 -1
  77. package/dist/src/ui/components/InputPrompt.js +8 -17
  78. package/dist/src/ui/components/InputPrompt.js.map +1 -1
  79. package/dist/src/ui/components/MainContent.js +1 -1
  80. package/dist/src/ui/components/MainContent.js.map +1 -1
  81. package/dist/src/ui/components/SettingsDialog.js +1 -1
  82. package/dist/src/ui/components/SettingsDialog.js.map +1 -1
  83. package/dist/src/ui/components/SettingsDialog.test.js +13 -47
  84. package/dist/src/ui/components/SettingsDialog.test.js.map +1 -1
  85. package/dist/src/ui/components/messages/ToolGroupMessage.d.ts +1 -1
  86. package/dist/src/ui/components/messages/ToolGroupMessage.js +5 -5
  87. package/dist/src/ui/components/messages/ToolGroupMessage.js.map +1 -1
  88. package/dist/src/ui/components/messages/ToolMessage.d.ts +1 -1
  89. package/dist/src/ui/components/messages/ToolMessage.js +3 -3
  90. package/dist/src/ui/components/messages/ToolMessage.js.map +1 -1
  91. package/dist/src/ui/components/shared/RadioButtonSelect.js +104 -9
  92. package/dist/src/ui/components/shared/RadioButtonSelect.js.map +1 -1
  93. package/dist/src/ui/components/shared/RadioButtonSelect.test.js +92 -113
  94. package/dist/src/ui/components/shared/RadioButtonSelect.test.js.map +1 -1
  95. package/dist/src/ui/contexts/FocusContext.d.ts +7 -0
  96. package/dist/src/ui/contexts/FocusContext.js +9 -0
  97. package/dist/src/ui/contexts/FocusContext.js.map +1 -0
  98. package/dist/src/ui/contexts/KeypressContext.js +0 -3
  99. package/dist/src/ui/contexts/KeypressContext.js.map +1 -1
  100. package/dist/src/ui/contexts/UIActionsContext.d.ts +0 -2
  101. package/dist/src/ui/contexts/UIActionsContext.js.map +1 -1
  102. package/dist/src/ui/contexts/UIStateContext.d.ts +1 -5
  103. package/dist/src/ui/contexts/UIStateContext.js +0 -1
  104. package/dist/src/ui/contexts/UIStateContext.js.map +1 -1
  105. package/dist/src/ui/hooks/slashCommandProcessor.d.ts +0 -2
  106. package/dist/src/ui/hooks/slashCommandProcessor.js +0 -6
  107. package/dist/src/ui/hooks/slashCommandProcessor.js.map +1 -1
  108. package/dist/src/ui/hooks/useExtensionUpdates.test.js +10 -8
  109. package/dist/src/ui/hooks/useExtensionUpdates.test.js.map +1 -1
  110. package/dist/src/ui/hooks/useFocus.js +0 -10
  111. package/dist/src/ui/hooks/useFocus.js.map +1 -1
  112. package/dist/src/ui/hooks/useFolderTrust.d.ts +1 -1
  113. package/dist/src/ui/hooks/useFolderTrust.js +7 -2
  114. package/dist/src/ui/hooks/useFolderTrust.js.map +1 -1
  115. package/dist/src/ui/hooks/useGeminiStream.js +1 -5
  116. package/dist/src/ui/hooks/useGeminiStream.js.map +1 -1
  117. package/dist/src/ui/hooks/useGitBranchName.test.js.map +1 -1
  118. package/dist/src/ui/hooks/useSlashCompletion.js +2 -7
  119. package/dist/src/ui/hooks/useSlashCompletion.js.map +1 -1
  120. package/dist/src/ui/hooks/useSlashCompletion.test.js +0 -33
  121. package/dist/src/ui/hooks/useSlashCompletion.test.js.map +1 -1
  122. package/dist/src/ui/privacy/CloudFreePrivacyNotice.js +3 -3
  123. package/dist/src/ui/privacy/CloudFreePrivacyNotice.js.map +1 -1
  124. package/dist/src/utils/deepMerge.d.ts +3 -2
  125. package/dist/src/zed-integration/schema.d.ts +22 -22
  126. package/dist/src/zed-integration/zedIntegration.d.ts +0 -7
  127. package/dist/src/zed-integration/zedIntegration.js +2 -14
  128. package/dist/src/zed-integration/zedIntegration.js.map +1 -1
  129. package/dist/tsconfig.tsbuildinfo +1 -1
  130. package/package.json +4 -5
  131. package/dist/src/ui/commands/modelCommand.d.ts +0 -7
  132. package/dist/src/ui/commands/modelCommand.js +0 -16
  133. package/dist/src/ui/commands/modelCommand.js.map +0 -1
  134. package/dist/src/ui/commands/modelCommand.test.d.ts +0 -6
  135. package/dist/src/ui/commands/modelCommand.test.js +0 -30
  136. package/dist/src/ui/commands/modelCommand.test.js.map +0 -1
  137. package/dist/src/ui/commands/permissionsCommand.d.ts +0 -7
  138. package/dist/src/ui/commands/permissionsCommand.js +0 -16
  139. package/dist/src/ui/commands/permissionsCommand.js.map +0 -1
  140. package/dist/src/ui/commands/permissionsCommand.test.d.ts +0 -6
  141. package/dist/src/ui/commands/permissionsCommand.test.js +0 -30
  142. package/dist/src/ui/commands/permissionsCommand.test.js.map +0 -1
  143. package/dist/src/ui/components/ModelDialog.d.ts +0 -11
  144. package/dist/src/ui/components/ModelDialog.js +0 -53
  145. package/dist/src/ui/components/ModelDialog.js.map +0 -1
  146. package/dist/src/ui/components/ModelDialog.test.d.ts +0 -6
  147. package/dist/src/ui/components/ModelDialog.test.js +0 -153
  148. package/dist/src/ui/components/ModelDialog.test.js.map +0 -1
  149. package/dist/src/ui/components/PermissionsModifyTrustDialog.d.ts +0 -13
  150. package/dist/src/ui/components/PermissionsModifyTrustDialog.js +0 -45
  151. package/dist/src/ui/components/PermissionsModifyTrustDialog.js.map +0 -1
  152. package/dist/src/ui/components/PermissionsModifyTrustDialog.test.d.ts +0 -6
  153. package/dist/src/ui/components/PermissionsModifyTrustDialog.test.js +0 -158
  154. package/dist/src/ui/components/PermissionsModifyTrustDialog.test.js.map +0 -1
  155. package/dist/src/ui/components/shared/BaseSelectionList.d.ts +0 -43
  156. package/dist/src/ui/components/shared/BaseSelectionList.js +0 -72
  157. package/dist/src/ui/components/shared/BaseSelectionList.js.map +0 -1
  158. package/dist/src/ui/components/shared/BaseSelectionList.test.d.ts +0 -6
  159. package/dist/src/ui/components/shared/BaseSelectionList.test.js +0 -374
  160. package/dist/src/ui/components/shared/BaseSelectionList.test.js.map +0 -1
  161. package/dist/src/ui/components/shared/DescriptiveRadioButtonSelect.d.ts +0 -36
  162. package/dist/src/ui/components/shared/DescriptiveRadioButtonSelect.js +0 -13
  163. package/dist/src/ui/components/shared/DescriptiveRadioButtonSelect.js.map +0 -1
  164. package/dist/src/ui/components/shared/DescriptiveRadioButtonSelect.test.d.ts +0 -6
  165. package/dist/src/ui/components/shared/DescriptiveRadioButtonSelect.test.js +0 -68
  166. package/dist/src/ui/components/shared/DescriptiveRadioButtonSelect.test.js.map +0 -1
  167. package/dist/src/ui/contexts/ShellFocusContext.d.ts +0 -7
  168. package/dist/src/ui/contexts/ShellFocusContext.js +0 -9
  169. package/dist/src/ui/contexts/ShellFocusContext.js.map +0 -1
  170. package/dist/src/ui/hooks/useModelCommand.d.ts +0 -12
  171. package/dist/src/ui/hooks/useModelCommand.js +0 -21
  172. package/dist/src/ui/hooks/useModelCommand.js.map +0 -1
  173. package/dist/src/ui/hooks/useModelCommand.test.d.ts +0 -6
  174. package/dist/src/ui/hooks/useModelCommand.test.js +0 -35
  175. package/dist/src/ui/hooks/useModelCommand.test.js.map +0 -1
  176. package/dist/src/ui/hooks/usePermissionsModifyTrust.d.ts +0 -17
  177. package/dist/src/ui/hooks/usePermissionsModifyTrust.js +0 -78
  178. package/dist/src/ui/hooks/usePermissionsModifyTrust.js.map +0 -1
  179. package/dist/src/ui/hooks/usePermissionsModifyTrust.test.d.ts +0 -6
  180. package/dist/src/ui/hooks/usePermissionsModifyTrust.test.js +0 -182
  181. package/dist/src/ui/hooks/usePermissionsModifyTrust.test.js.map +0 -1
  182. package/dist/src/ui/hooks/useSelectionList.d.ts +0 -33
  183. package/dist/src/ui/hooks/useSelectionList.js +0 -252
  184. package/dist/src/ui/hooks/useSelectionList.js.map +0 -1
  185. package/dist/src/ui/hooks/useSelectionList.test.d.ts +0 -6
  186. package/dist/src/ui/hooks/useSelectionList.test.js +0 -651
  187. package/dist/src/ui/hooks/useSelectionList.test.js.map +0 -1
  188. package/dist/src/utils/relaunch.d.ts +0 -7
  189. package/dist/src/utils/relaunch.js +0 -57
  190. package/dist/src/utils/relaunch.js.map +0 -1
  191. package/dist/src/utils/relaunch.test.d.ts +0 -6
  192. package/dist/src/utils/relaunch.test.js +0 -273
  193. package/dist/src/utils/relaunch.test.js.map +0 -1
@@ -1,651 +0,0 @@
1
- /**
2
- * @license
3
- * Copyright 2025 Google LLC
4
- * SPDX-License-Identifier: Apache-2.0
5
- */
6
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
7
- import { renderHook, act } from '@testing-library/react';
8
- import { useSelectionList, } from './useSelectionList.js';
9
- import { useKeypress } from './useKeypress.js';
10
- vi.mock('./useKeypress.js');
11
- let activeKeypressHandler = null;
12
- describe('useSelectionList', () => {
13
- const mockOnSelect = vi.fn();
14
- const mockOnHighlight = vi.fn();
15
- const items = [
16
- { value: 'A' },
17
- { value: 'B', disabled: true },
18
- { value: 'C' },
19
- { value: 'D' },
20
- ];
21
- beforeEach(() => {
22
- activeKeypressHandler = null;
23
- vi.mocked(useKeypress).mockImplementation((handler, options) => {
24
- if (options?.isActive) {
25
- activeKeypressHandler = handler;
26
- }
27
- else {
28
- activeKeypressHandler = null;
29
- }
30
- });
31
- mockOnSelect.mockClear();
32
- mockOnHighlight.mockClear();
33
- });
34
- const pressKey = (name, sequence = name) => {
35
- act(() => {
36
- if (activeKeypressHandler) {
37
- const key = {
38
- name,
39
- sequence,
40
- ctrl: false,
41
- meta: false,
42
- shift: false,
43
- paste: false,
44
- };
45
- activeKeypressHandler(key);
46
- }
47
- else {
48
- throw new Error(`Test attempted to press key (${name}) but the keypress handler is not active. Ensure the hook is focused (isFocused=true) and the list is not empty.`);
49
- }
50
- });
51
- };
52
- describe('Initialization', () => {
53
- it('should initialize with the default index (0) if enabled', () => {
54
- const { result } = renderHook(() => useSelectionList({ items, onSelect: mockOnSelect }));
55
- expect(result.current.activeIndex).toBe(0);
56
- });
57
- it('should initialize with the provided initialIndex if enabled', () => {
58
- const { result } = renderHook(() => useSelectionList({
59
- items,
60
- initialIndex: 2,
61
- onSelect: mockOnSelect,
62
- }));
63
- expect(result.current.activeIndex).toBe(2);
64
- });
65
- it('should handle an empty list gracefully', () => {
66
- const { result } = renderHook(() => useSelectionList({ items: [], onSelect: mockOnSelect }));
67
- expect(result.current.activeIndex).toBe(0);
68
- });
69
- it('should find the next enabled item (downwards) if initialIndex is disabled', () => {
70
- const { result } = renderHook(() => useSelectionList({
71
- items,
72
- initialIndex: 1,
73
- onSelect: mockOnSelect,
74
- }));
75
- expect(result.current.activeIndex).toBe(2);
76
- });
77
- it('should wrap around to find the next enabled item if initialIndex is disabled', () => {
78
- const wrappingItems = [
79
- { value: 'A' },
80
- { value: 'B', disabled: true },
81
- { value: 'C', disabled: true },
82
- ];
83
- const { result } = renderHook(() => useSelectionList({
84
- items: wrappingItems,
85
- initialIndex: 2,
86
- onSelect: mockOnSelect,
87
- }));
88
- expect(result.current.activeIndex).toBe(0);
89
- });
90
- it('should default to 0 if initialIndex is out of bounds', () => {
91
- const { result } = renderHook(() => useSelectionList({
92
- items,
93
- initialIndex: 10,
94
- onSelect: mockOnSelect,
95
- }));
96
- expect(result.current.activeIndex).toBe(0);
97
- const { result: resultNeg } = renderHook(() => useSelectionList({
98
- items,
99
- initialIndex: -1,
100
- onSelect: mockOnSelect,
101
- }));
102
- expect(resultNeg.current.activeIndex).toBe(0);
103
- });
104
- it('should stick to the initial index if all items are disabled', () => {
105
- const allDisabled = [
106
- { value: 'A', disabled: true },
107
- { value: 'B', disabled: true },
108
- ];
109
- const { result } = renderHook(() => useSelectionList({
110
- items: allDisabled,
111
- initialIndex: 1,
112
- onSelect: mockOnSelect,
113
- }));
114
- expect(result.current.activeIndex).toBe(1);
115
- });
116
- });
117
- describe('Keyboard Navigation (Up/Down/J/K)', () => {
118
- it('should move down with "j" and "down" keys, skipping disabled items', () => {
119
- const { result } = renderHook(() => useSelectionList({ items, onSelect: mockOnSelect }));
120
- expect(result.current.activeIndex).toBe(0);
121
- pressKey('j');
122
- expect(result.current.activeIndex).toBe(2);
123
- pressKey('down');
124
- expect(result.current.activeIndex).toBe(3);
125
- });
126
- it('should move up with "k" and "up" keys, skipping disabled items', () => {
127
- const { result } = renderHook(() => useSelectionList({ items, initialIndex: 3, onSelect: mockOnSelect }));
128
- expect(result.current.activeIndex).toBe(3);
129
- pressKey('k');
130
- expect(result.current.activeIndex).toBe(2);
131
- pressKey('up');
132
- expect(result.current.activeIndex).toBe(0);
133
- });
134
- it('should wrap navigation correctly', () => {
135
- const { result } = renderHook(() => useSelectionList({
136
- items,
137
- initialIndex: items.length - 1,
138
- onSelect: mockOnSelect,
139
- }));
140
- expect(result.current.activeIndex).toBe(3);
141
- pressKey('down');
142
- expect(result.current.activeIndex).toBe(0);
143
- pressKey('up');
144
- expect(result.current.activeIndex).toBe(3);
145
- });
146
- it('should call onHighlight when index changes', () => {
147
- renderHook(() => useSelectionList({
148
- items,
149
- onSelect: mockOnSelect,
150
- onHighlight: mockOnHighlight,
151
- }));
152
- pressKey('down');
153
- expect(mockOnHighlight).toHaveBeenCalledTimes(1);
154
- expect(mockOnHighlight).toHaveBeenCalledWith('C');
155
- });
156
- it('should not move or call onHighlight if navigation results in the same index (e.g., single item)', () => {
157
- const singleItem = [{ value: 'A' }];
158
- const { result } = renderHook(() => useSelectionList({
159
- items: singleItem,
160
- onSelect: mockOnSelect,
161
- onHighlight: mockOnHighlight,
162
- }));
163
- pressKey('down');
164
- expect(result.current.activeIndex).toBe(0);
165
- expect(mockOnHighlight).not.toHaveBeenCalled();
166
- });
167
- it('should not move or call onHighlight if all items are disabled', () => {
168
- const allDisabled = [
169
- { value: 'A', disabled: true },
170
- { value: 'B', disabled: true },
171
- ];
172
- const { result } = renderHook(() => useSelectionList({
173
- items: allDisabled,
174
- onSelect: mockOnSelect,
175
- onHighlight: mockOnHighlight,
176
- }));
177
- const initialIndex = result.current.activeIndex;
178
- pressKey('down');
179
- expect(result.current.activeIndex).toBe(initialIndex);
180
- expect(mockOnHighlight).not.toHaveBeenCalled();
181
- });
182
- });
183
- describe('Selection (Enter)', () => {
184
- it('should call onSelect when "return" is pressed on enabled item', () => {
185
- renderHook(() => useSelectionList({
186
- items,
187
- initialIndex: 2,
188
- onSelect: mockOnSelect,
189
- }));
190
- pressKey('return');
191
- expect(mockOnSelect).toHaveBeenCalledTimes(1);
192
- expect(mockOnSelect).toHaveBeenCalledWith('C');
193
- });
194
- it('should not call onSelect if the active item is disabled', () => {
195
- const { result } = renderHook(() => useSelectionList({
196
- items,
197
- onSelect: mockOnSelect,
198
- }));
199
- act(() => result.current.setActiveIndex(1));
200
- pressKey('return');
201
- expect(mockOnSelect).not.toHaveBeenCalled();
202
- });
203
- });
204
- describe('Keyboard Navigation Robustness (Rapid Input)', () => {
205
- it('should handle rapid navigation and selection robustly (avoiding stale state)', () => {
206
- const { result } = renderHook(() => useSelectionList({
207
- items, // A, B(disabled), C, D. Initial index 0 (A).
208
- onSelect: mockOnSelect,
209
- onHighlight: mockOnHighlight,
210
- }));
211
- // Simulate rapid inputs with separate act blocks to allow effects to run
212
- if (!activeKeypressHandler)
213
- throw new Error('Handler not active');
214
- const handler = activeKeypressHandler;
215
- const press = (name) => {
216
- const key = {
217
- name,
218
- sequence: name,
219
- ctrl: false,
220
- meta: false,
221
- shift: false,
222
- paste: false,
223
- };
224
- handler(key);
225
- };
226
- // 1. Press Down. Should move 0 (A) -> 2 (C).
227
- act(() => {
228
- press('down');
229
- });
230
- // 2. Press Down again. Should move 2 (C) -> 3 (D).
231
- act(() => {
232
- press('down');
233
- });
234
- // 3. Press Enter. Should select D.
235
- act(() => {
236
- press('return');
237
- });
238
- expect(result.current.activeIndex).toBe(3);
239
- expect(mockOnHighlight).toHaveBeenCalledTimes(2);
240
- expect(mockOnHighlight).toHaveBeenNthCalledWith(1, 'C');
241
- expect(mockOnHighlight).toHaveBeenNthCalledWith(2, 'D');
242
- expect(mockOnSelect).toHaveBeenCalledTimes(1);
243
- expect(mockOnSelect).toHaveBeenCalledWith('D');
244
- expect(mockOnSelect).not.toHaveBeenCalledWith('A');
245
- });
246
- it('should handle ultra-rapid input (multiple presses in single act) without stale state', () => {
247
- const { result } = renderHook(() => useSelectionList({
248
- items, // A, B(disabled), C, D. Initial index 0 (A).
249
- onSelect: mockOnSelect,
250
- onHighlight: mockOnHighlight,
251
- }));
252
- // Simulate ultra-rapid inputs where all keypresses happen faster than React can re-render
253
- act(() => {
254
- if (!activeKeypressHandler)
255
- throw new Error('Handler not active');
256
- const handler = activeKeypressHandler;
257
- const press = (name) => {
258
- const key = {
259
- name,
260
- sequence: name,
261
- ctrl: false,
262
- meta: false,
263
- shift: false,
264
- paste: false,
265
- };
266
- handler(key);
267
- };
268
- // All presses happen in same render cycle - React batches the state updates
269
- press('down'); // Should move 0 (A) -> 2 (C)
270
- press('down'); // Should move 2 (C) -> 3 (D)
271
- press('return'); // Should select D
272
- });
273
- expect(result.current.activeIndex).toBe(3);
274
- expect(mockOnHighlight).toHaveBeenCalledWith('D');
275
- expect(mockOnSelect).toHaveBeenCalledTimes(1);
276
- expect(mockOnSelect).toHaveBeenCalledWith('D');
277
- });
278
- });
279
- describe('Focus Management (isFocused)', () => {
280
- it('should activate the keypress handler when focused (default) and items exist', () => {
281
- const { result } = renderHook(() => useSelectionList({ items, onSelect: mockOnSelect }));
282
- expect(activeKeypressHandler).not.toBeNull();
283
- pressKey('down');
284
- expect(result.current.activeIndex).toBe(2);
285
- });
286
- it('should not activate the keypress handler when isFocused is false', () => {
287
- renderHook(() => useSelectionList({ items, onSelect: mockOnSelect, isFocused: false }));
288
- expect(activeKeypressHandler).toBeNull();
289
- expect(() => pressKey('down')).toThrow(/keypress handler is not active/);
290
- });
291
- it('should not activate the keypress handler when items list is empty', () => {
292
- renderHook(() => useSelectionList({
293
- items: [],
294
- onSelect: mockOnSelect,
295
- isFocused: true,
296
- }));
297
- expect(activeKeypressHandler).toBeNull();
298
- expect(() => pressKey('down')).toThrow(/keypress handler is not active/);
299
- });
300
- it('should activate/deactivate when isFocused prop changes', () => {
301
- const { result, rerender } = renderHook((props) => useSelectionList({ items, onSelect: mockOnSelect, ...props }), { initialProps: { isFocused: false } });
302
- expect(activeKeypressHandler).toBeNull();
303
- rerender({ isFocused: true });
304
- expect(activeKeypressHandler).not.toBeNull();
305
- pressKey('down');
306
- expect(result.current.activeIndex).toBe(2);
307
- rerender({ isFocused: false });
308
- expect(activeKeypressHandler).toBeNull();
309
- expect(() => pressKey('down')).toThrow(/keypress handler is not active/);
310
- });
311
- });
312
- describe('Numeric Quick Selection (showNumbers=true)', () => {
313
- beforeEach(() => {
314
- vi.useFakeTimers();
315
- });
316
- afterEach(() => {
317
- vi.useRealTimers();
318
- });
319
- const shortList = items;
320
- const longList = Array.from({ length: 15 }, (_, i) => ({ value: `Item ${i + 1}` }));
321
- const pressNumber = (num) => pressKey(num, num);
322
- it('should not respond to numbers if showNumbers is false (default)', () => {
323
- const { result } = renderHook(() => useSelectionList({ items: shortList, onSelect: mockOnSelect }));
324
- pressNumber('1');
325
- expect(result.current.activeIndex).toBe(0);
326
- expect(mockOnSelect).not.toHaveBeenCalled();
327
- });
328
- it('should select item immediately if the number cannot be extended (unambiguous)', () => {
329
- const { result } = renderHook(() => useSelectionList({
330
- items: shortList,
331
- onSelect: mockOnSelect,
332
- onHighlight: mockOnHighlight,
333
- showNumbers: true,
334
- }));
335
- pressNumber('3');
336
- expect(result.current.activeIndex).toBe(2);
337
- expect(mockOnHighlight).toHaveBeenCalledWith('C');
338
- expect(mockOnSelect).toHaveBeenCalledTimes(1);
339
- expect(mockOnSelect).toHaveBeenCalledWith('C');
340
- expect(vi.getTimerCount()).toBe(0);
341
- });
342
- it('should highlight and wait for timeout if the number can be extended (ambiguous)', () => {
343
- const { result } = renderHook(() => useSelectionList({
344
- items: longList,
345
- initialIndex: 1, // Start at index 1 so pressing "1" (index 0) causes a change
346
- onSelect: mockOnSelect,
347
- onHighlight: mockOnHighlight,
348
- showNumbers: true,
349
- }));
350
- pressNumber('1');
351
- expect(result.current.activeIndex).toBe(0);
352
- expect(mockOnHighlight).toHaveBeenCalledWith('Item 1');
353
- expect(mockOnSelect).not.toHaveBeenCalled();
354
- expect(vi.getTimerCount()).toBe(1);
355
- act(() => {
356
- vi.advanceTimersByTime(1000);
357
- });
358
- expect(mockOnSelect).toHaveBeenCalledTimes(1);
359
- expect(mockOnSelect).toHaveBeenCalledWith('Item 1');
360
- });
361
- it('should handle multi-digit input correctly', () => {
362
- const { result } = renderHook(() => useSelectionList({
363
- items: longList,
364
- onSelect: mockOnSelect,
365
- showNumbers: true,
366
- }));
367
- pressNumber('1');
368
- expect(mockOnSelect).not.toHaveBeenCalled();
369
- pressNumber('2');
370
- expect(result.current.activeIndex).toBe(11);
371
- expect(mockOnSelect).toHaveBeenCalledTimes(1);
372
- expect(mockOnSelect).toHaveBeenCalledWith('Item 12');
373
- });
374
- it('should reset buffer if input becomes invalid (out of bounds)', () => {
375
- const { result } = renderHook(() => useSelectionList({
376
- items: shortList,
377
- onSelect: mockOnSelect,
378
- showNumbers: true,
379
- }));
380
- pressNumber('5');
381
- expect(result.current.activeIndex).toBe(0);
382
- expect(mockOnSelect).not.toHaveBeenCalled();
383
- pressNumber('3');
384
- expect(result.current.activeIndex).toBe(2);
385
- expect(mockOnSelect).toHaveBeenCalledWith('C');
386
- });
387
- it('should allow "0" as subsequent digit, but ignore as first digit', () => {
388
- const { result } = renderHook(() => useSelectionList({
389
- items: longList,
390
- onSelect: mockOnSelect,
391
- showNumbers: true,
392
- }));
393
- pressNumber('0');
394
- expect(result.current.activeIndex).toBe(0);
395
- expect(mockOnSelect).not.toHaveBeenCalled();
396
- // Timer should be running to clear the '0' input buffer
397
- expect(vi.getTimerCount()).toBe(1);
398
- // Press '1', then '0' (Item 10, index 9)
399
- pressNumber('1');
400
- pressNumber('0');
401
- expect(result.current.activeIndex).toBe(9);
402
- expect(mockOnSelect).toHaveBeenCalledWith('Item 10');
403
- });
404
- it('should clear the initial "0" input after timeout', () => {
405
- renderHook(() => useSelectionList({
406
- items: longList,
407
- onSelect: mockOnSelect,
408
- showNumbers: true,
409
- }));
410
- pressNumber('0');
411
- act(() => vi.advanceTimersByTime(1000)); // Timeout the '0' input
412
- pressNumber('1');
413
- expect(mockOnSelect).not.toHaveBeenCalled(); // Should be waiting for second digit
414
- act(() => vi.advanceTimersByTime(1000)); // Timeout '1'
415
- expect(mockOnSelect).toHaveBeenCalledWith('Item 1');
416
- });
417
- it('should highlight but not select a disabled item (immediate selection case)', () => {
418
- const { result } = renderHook(() => useSelectionList({
419
- items: shortList, // B (index 1, number 2) is disabled
420
- onSelect: mockOnSelect,
421
- onHighlight: mockOnHighlight,
422
- showNumbers: true,
423
- }));
424
- pressNumber('2');
425
- expect(result.current.activeIndex).toBe(1);
426
- expect(mockOnHighlight).toHaveBeenCalledWith('B');
427
- // Should not select immediately, even though 20 > 4
428
- expect(mockOnSelect).not.toHaveBeenCalled();
429
- });
430
- it('should highlight but not select a disabled item (timeout case)', () => {
431
- // Create a list where the ambiguous prefix points to a disabled item
432
- const disabledAmbiguousList = [
433
- { value: 'Item 1 Disabled', disabled: true },
434
- ...longList.slice(1),
435
- ];
436
- const { result } = renderHook(() => useSelectionList({
437
- items: disabledAmbiguousList,
438
- onSelect: mockOnSelect,
439
- showNumbers: true,
440
- }));
441
- pressNumber('1');
442
- expect(result.current.activeIndex).toBe(0);
443
- expect(vi.getTimerCount()).toBe(1);
444
- act(() => {
445
- vi.advanceTimersByTime(1000);
446
- });
447
- // Should not select after timeout
448
- expect(mockOnSelect).not.toHaveBeenCalled();
449
- });
450
- it('should clear the number buffer if a non-numeric key (e.g., navigation) is pressed', () => {
451
- const { result } = renderHook(() => useSelectionList({
452
- items: longList,
453
- onSelect: mockOnSelect,
454
- showNumbers: true,
455
- }));
456
- pressNumber('1');
457
- expect(vi.getTimerCount()).toBe(1);
458
- pressKey('down');
459
- expect(result.current.activeIndex).toBe(1);
460
- expect(vi.getTimerCount()).toBe(0);
461
- pressNumber('3');
462
- // Should select '3', not '13'
463
- expect(result.current.activeIndex).toBe(2);
464
- });
465
- it('should clear the number buffer if "return" is pressed', () => {
466
- renderHook(() => useSelectionList({
467
- items: longList,
468
- onSelect: mockOnSelect,
469
- showNumbers: true,
470
- }));
471
- pressNumber('1');
472
- pressKey('return');
473
- expect(mockOnSelect).toHaveBeenCalledTimes(1);
474
- expect(vi.getTimerCount()).toBe(0);
475
- act(() => {
476
- vi.advanceTimersByTime(1000);
477
- });
478
- expect(mockOnSelect).toHaveBeenCalledTimes(1);
479
- });
480
- });
481
- describe('Reactivity (Dynamic Updates)', () => {
482
- it('should update activeIndex when initialIndex prop changes', () => {
483
- const { result, rerender } = renderHook(({ initialIndex }) => useSelectionList({
484
- items,
485
- onSelect: mockOnSelect,
486
- initialIndex,
487
- }), { initialProps: { initialIndex: 0 } });
488
- rerender({ initialIndex: 2 });
489
- expect(result.current.activeIndex).toBe(2);
490
- });
491
- it('should validate index when initialIndex prop changes to a disabled item', () => {
492
- const { result, rerender } = renderHook(({ initialIndex }) => useSelectionList({
493
- items,
494
- onSelect: mockOnSelect,
495
- initialIndex,
496
- }), { initialProps: { initialIndex: 0 } });
497
- rerender({ initialIndex: 1 });
498
- expect(result.current.activeIndex).toBe(2);
499
- });
500
- it('should adjust activeIndex if items change and the initialIndex is now out of bounds', () => {
501
- const { result, rerender } = renderHook(({ items: testItems }) => useSelectionList({
502
- onSelect: mockOnSelect,
503
- initialIndex: 3,
504
- items: testItems,
505
- }), { initialProps: { items } });
506
- expect(result.current.activeIndex).toBe(3);
507
- const shorterItems = [{ value: 'X' }, { value: 'Y' }];
508
- rerender({ items: shorterItems }); // Length 2
509
- // The useEffect syncs based on the initialIndex (3) which is now out of bounds. It defaults to 0.
510
- expect(result.current.activeIndex).toBe(0);
511
- });
512
- it('should adjust activeIndex if items change and the initialIndex becomes disabled', () => {
513
- const initialItems = [{ value: 'A' }, { value: 'B' }, { value: 'C' }];
514
- const { result, rerender } = renderHook(({ items: testItems }) => useSelectionList({
515
- onSelect: mockOnSelect,
516
- initialIndex: 1,
517
- items: testItems,
518
- }), { initialProps: { items: initialItems } });
519
- expect(result.current.activeIndex).toBe(1);
520
- const newItems = [
521
- { value: 'A' },
522
- { value: 'B', disabled: true },
523
- { value: 'C' },
524
- ];
525
- rerender({ items: newItems });
526
- expect(result.current.activeIndex).toBe(2);
527
- });
528
- it('should reset to 0 if items change to an empty list', () => {
529
- const { result, rerender } = renderHook(({ items: testItems }) => useSelectionList({
530
- onSelect: mockOnSelect,
531
- initialIndex: 2,
532
- items: testItems,
533
- }), { initialProps: { items } });
534
- rerender({ items: [] });
535
- expect(result.current.activeIndex).toBe(0);
536
- });
537
- it('should not reset activeIndex when items are deeply equal', () => {
538
- const initialItems = [
539
- { value: 'A' },
540
- { value: 'B', disabled: true },
541
- { value: 'C' },
542
- { value: 'D' },
543
- ];
544
- const { result, rerender } = renderHook(({ items: testItems }) => useSelectionList({
545
- onSelect: mockOnSelect,
546
- onHighlight: mockOnHighlight,
547
- initialIndex: 2,
548
- items: testItems,
549
- }), { initialProps: { items: initialItems } });
550
- expect(result.current.activeIndex).toBe(2);
551
- act(() => {
552
- result.current.setActiveIndex(3);
553
- });
554
- expect(result.current.activeIndex).toBe(3);
555
- mockOnHighlight.mockClear();
556
- // Create new array with same content (deeply equal but not identical)
557
- const newItems = [
558
- { value: 'A' },
559
- { value: 'B', disabled: true },
560
- { value: 'C' },
561
- { value: 'D' },
562
- ];
563
- rerender({ items: newItems });
564
- // Active index should remain the same since items are deeply equal
565
- expect(result.current.activeIndex).toBe(3);
566
- // onHighlight should NOT be called since the index didn't change
567
- expect(mockOnHighlight).not.toHaveBeenCalled();
568
- });
569
- it('should update activeIndex when items change structurally', () => {
570
- const initialItems = [
571
- { value: 'A' },
572
- { value: 'B', disabled: true },
573
- { value: 'C' },
574
- { value: 'D' },
575
- ];
576
- const { result, rerender } = renderHook(({ items: testItems }) => useSelectionList({
577
- onSelect: mockOnSelect,
578
- onHighlight: mockOnHighlight,
579
- initialIndex: 3,
580
- items: testItems,
581
- }), { initialProps: { items: initialItems } });
582
- expect(result.current.activeIndex).toBe(3);
583
- mockOnHighlight.mockClear();
584
- // Change item values (not deeply equal)
585
- const newItems = [{ value: 'X' }, { value: 'Y' }, { value: 'Z' }];
586
- rerender({ items: newItems });
587
- // Active index should update based on initialIndex and new items
588
- expect(result.current.activeIndex).toBe(0);
589
- });
590
- it('should handle partial changes in items array', () => {
591
- const initialItems = [{ value: 'A' }, { value: 'B' }, { value: 'C' }];
592
- const { result, rerender } = renderHook(({ items: testItems }) => useSelectionList({
593
- onSelect: mockOnSelect,
594
- initialIndex: 1,
595
- items: testItems,
596
- }), { initialProps: { items: initialItems } });
597
- expect(result.current.activeIndex).toBe(1);
598
- // Change only one item's disabled status
599
- const newItems = [
600
- { value: 'A' },
601
- { value: 'B', disabled: true },
602
- { value: 'C' },
603
- ];
604
- rerender({ items: newItems });
605
- // Should find next valid index since current became disabled
606
- expect(result.current.activeIndex).toBe(2);
607
- });
608
- });
609
- describe('Manual Control', () => {
610
- it('should allow manual setting of active index via setActiveIndex', () => {
611
- const { result } = renderHook(() => useSelectionList({ items, onSelect: mockOnSelect }));
612
- act(() => {
613
- result.current.setActiveIndex(3);
614
- });
615
- expect(result.current.activeIndex).toBe(3);
616
- act(() => {
617
- result.current.setActiveIndex(1);
618
- });
619
- expect(result.current.activeIndex).toBe(1);
620
- });
621
- });
622
- describe('Cleanup', () => {
623
- beforeEach(() => {
624
- vi.useFakeTimers();
625
- });
626
- afterEach(() => {
627
- vi.useRealTimers();
628
- });
629
- it('should clear timeout on unmount when timer is active', () => {
630
- const longList = Array.from({ length: 15 }, (_, i) => ({ value: `Item ${i + 1}` }));
631
- const { unmount } = renderHook(() => useSelectionList({
632
- items: longList,
633
- onSelect: mockOnSelect,
634
- showNumbers: true,
635
- }));
636
- pressKey('1', '1');
637
- expect(vi.getTimerCount()).toBe(1);
638
- act(() => {
639
- vi.advanceTimersByTime(500);
640
- });
641
- expect(mockOnSelect).not.toHaveBeenCalled();
642
- unmount();
643
- expect(vi.getTimerCount()).toBe(0);
644
- act(() => {
645
- vi.advanceTimersByTime(1000);
646
- });
647
- expect(mockOnSelect).not.toHaveBeenCalled();
648
- });
649
- });
650
- });
651
- //# sourceMappingURL=useSelectionList.test.js.map