@mdxui/terminal 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. package/README.md +571 -0
  2. package/dist/ansi-css-Sk5mWtdK.d.ts +119 -0
  3. package/dist/ansi-css-V6JIHGsM.d.ts +119 -0
  4. package/dist/ansi-css-_3eSEU9d.d.ts +119 -0
  5. package/dist/chunk-3EFDH7PK.js +5235 -0
  6. package/dist/chunk-3RG5ZIWI.js +10 -0
  7. package/dist/chunk-3X5IR6WE.js +884 -0
  8. package/dist/chunk-4FV5ZDCE.js +5236 -0
  9. package/dist/chunk-4OVMSF2J.js +243 -0
  10. package/dist/chunk-63FEETIS.js +4048 -0
  11. package/dist/chunk-B43KP7XJ.js +884 -0
  12. package/dist/chunk-BMTJXWUV.js +655 -0
  13. package/dist/chunk-C3SVH4N7.js +882 -0
  14. package/dist/chunk-EVWR7Y47.js +874 -0
  15. package/dist/chunk-F6A5VWUC.js +1285 -0
  16. package/dist/chunk-FD7KW7GE.js +882 -0
  17. package/dist/chunk-GBQ6UD6I.js +655 -0
  18. package/dist/chunk-GMDD3M6U.js +5227 -0
  19. package/dist/chunk-JBHRXOXM.js +1058 -0
  20. package/dist/chunk-JFOO3EYO.js +1182 -0
  21. package/dist/chunk-JQ5H3WXL.js +1291 -0
  22. package/dist/chunk-JQD5NASE.js +234 -0
  23. package/dist/chunk-KRHJP5R7.js +592 -0
  24. package/dist/chunk-KWF6WVJE.js +962 -0
  25. package/dist/chunk-LHYQVN3H.js +1038 -0
  26. package/dist/chunk-M3TLQLGC.js +1032 -0
  27. package/dist/chunk-MVW4Q5OP.js +240 -0
  28. package/dist/chunk-NXCZSWLU.js +1294 -0
  29. package/dist/chunk-O25TNRO6.js +607 -0
  30. package/dist/chunk-PNECDA2I.js +884 -0
  31. package/dist/chunk-QIHWRLJR.js +962 -0
  32. package/dist/chunk-QW5YMQ7K.js +882 -0
  33. package/dist/chunk-R5U7XKVJ.js +16 -0
  34. package/dist/chunk-RP2MVQLR.js +962 -0
  35. package/dist/chunk-TP6RXGXA.js +1087 -0
  36. package/dist/chunk-TQQSTITZ.js +655 -0
  37. package/dist/chunk-X24GWXQV.js +1281 -0
  38. package/dist/components/index.d.ts +802 -0
  39. package/dist/components/index.js +149 -0
  40. package/dist/data/index.d.ts +2554 -0
  41. package/dist/data/index.js +51 -0
  42. package/dist/forms/index.d.ts +1596 -0
  43. package/dist/forms/index.js +464 -0
  44. package/dist/index-CQRFZntR.d.ts +867 -0
  45. package/dist/index.d.ts +579 -0
  46. package/dist/index.js +786 -0
  47. package/dist/interactive-D0JkWosD.d.ts +217 -0
  48. package/dist/keyboard/index.d.ts +2 -0
  49. package/dist/keyboard/index.js +43 -0
  50. package/dist/renderers/index.d.ts +546 -0
  51. package/dist/renderers/index.js +2157 -0
  52. package/dist/storybook/index.d.ts +396 -0
  53. package/dist/storybook/index.js +641 -0
  54. package/dist/theme/index.d.ts +1339 -0
  55. package/dist/theme/index.js +123 -0
  56. package/dist/types-Bxu5PAgA.d.ts +710 -0
  57. package/dist/types-CIlop5Ji.d.ts +701 -0
  58. package/dist/types-Ca8p_p5X.d.ts +710 -0
  59. package/package.json +90 -0
  60. package/src/__tests__/components/data/card.test.ts +458 -0
  61. package/src/__tests__/components/data/list.test.ts +473 -0
  62. package/src/__tests__/components/data/metrics.test.ts +541 -0
  63. package/src/__tests__/components/data/table.test.ts +448 -0
  64. package/src/__tests__/components/input/field.test.ts +555 -0
  65. package/src/__tests__/components/input/form.test.ts +870 -0
  66. package/src/__tests__/components/input/search.test.ts +1238 -0
  67. package/src/__tests__/components/input/select.test.ts +658 -0
  68. package/src/__tests__/components/navigation/breadcrumb.test.ts +923 -0
  69. package/src/__tests__/components/navigation/command-palette.test.ts +1095 -0
  70. package/src/__tests__/components/navigation/sidebar.test.ts +1018 -0
  71. package/src/__tests__/components/navigation/tabs.test.ts +995 -0
  72. package/src/__tests__/components.test.tsx +1197 -0
  73. package/src/__tests__/core/compiler.test.ts +986 -0
  74. package/src/__tests__/core/parser.test.ts +785 -0
  75. package/src/__tests__/core/tier-switcher.test.ts +1103 -0
  76. package/src/__tests__/core/types.test.ts +1398 -0
  77. package/src/__tests__/data/collections.test.ts +1337 -0
  78. package/src/__tests__/data/db.test.ts +1265 -0
  79. package/src/__tests__/data/reactive.test.ts +1010 -0
  80. package/src/__tests__/data/sync.test.ts +1614 -0
  81. package/src/__tests__/errors.test.ts +660 -0
  82. package/src/__tests__/forms/integration.test.ts +444 -0
  83. package/src/__tests__/integration.test.ts +905 -0
  84. package/src/__tests__/keyboard.test.ts +1791 -0
  85. package/src/__tests__/renderer.test.ts +489 -0
  86. package/src/__tests__/renderers/ansi-css.test.ts +948 -0
  87. package/src/__tests__/renderers/ansi.test.ts +1366 -0
  88. package/src/__tests__/renderers/ascii.test.ts +1360 -0
  89. package/src/__tests__/renderers/interactive.test.ts +2353 -0
  90. package/src/__tests__/renderers/markdown.test.ts +1483 -0
  91. package/src/__tests__/renderers/text.test.ts +1369 -0
  92. package/src/__tests__/renderers/unicode.test.ts +1307 -0
  93. package/src/__tests__/theme.test.ts +639 -0
  94. package/src/__tests__/utils/assertions.ts +685 -0
  95. package/src/__tests__/utils/index.ts +115 -0
  96. package/src/__tests__/utils/test-renderer.ts +381 -0
  97. package/src/__tests__/utils/utils.test.ts +560 -0
  98. package/src/components/containers/card.ts +56 -0
  99. package/src/components/containers/dialog.ts +53 -0
  100. package/src/components/containers/index.ts +9 -0
  101. package/src/components/containers/panel.ts +59 -0
  102. package/src/components/feedback/badge.ts +40 -0
  103. package/src/components/feedback/index.ts +8 -0
  104. package/src/components/feedback/spinner.ts +23 -0
  105. package/src/components/helpers.ts +81 -0
  106. package/src/components/index.ts +153 -0
  107. package/src/components/layout/breadcrumb.ts +31 -0
  108. package/src/components/layout/index.ts +10 -0
  109. package/src/components/layout/list.ts +29 -0
  110. package/src/components/layout/sidebar.ts +79 -0
  111. package/src/components/layout/table.ts +62 -0
  112. package/src/components/primitives/box.ts +95 -0
  113. package/src/components/primitives/button.ts +54 -0
  114. package/src/components/primitives/index.ts +11 -0
  115. package/src/components/primitives/input.ts +88 -0
  116. package/src/components/primitives/select.ts +97 -0
  117. package/src/components/primitives/text.ts +60 -0
  118. package/src/components/render.ts +155 -0
  119. package/src/components/templates/app.ts +43 -0
  120. package/src/components/templates/index.ts +8 -0
  121. package/src/components/templates/site.ts +54 -0
  122. package/src/components/types.ts +777 -0
  123. package/src/core/compiler.ts +718 -0
  124. package/src/core/parser.ts +127 -0
  125. package/src/core/tier-switcher.ts +607 -0
  126. package/src/core/types.ts +672 -0
  127. package/src/data/collection.ts +316 -0
  128. package/src/data/collections.ts +50 -0
  129. package/src/data/context.tsx +174 -0
  130. package/src/data/db.ts +127 -0
  131. package/src/data/hooks.ts +532 -0
  132. package/src/data/index.ts +138 -0
  133. package/src/data/reactive.ts +1225 -0
  134. package/src/data/saas-collections.ts +375 -0
  135. package/src/data/sync.ts +1213 -0
  136. package/src/data/types.ts +660 -0
  137. package/src/forms/converters.ts +512 -0
  138. package/src/forms/index.ts +133 -0
  139. package/src/forms/schemas.ts +403 -0
  140. package/src/forms/types.ts +476 -0
  141. package/src/index.ts +542 -0
  142. package/src/keyboard/focus.ts +748 -0
  143. package/src/keyboard/index.ts +96 -0
  144. package/src/keyboard/integration.ts +371 -0
  145. package/src/keyboard/manager.ts +377 -0
  146. package/src/keyboard/presets.ts +90 -0
  147. package/src/renderers/ansi-css.ts +576 -0
  148. package/src/renderers/ansi.ts +802 -0
  149. package/src/renderers/ascii.ts +680 -0
  150. package/src/renderers/breadcrumb.ts +480 -0
  151. package/src/renderers/command-palette.ts +802 -0
  152. package/src/renderers/components/field.ts +210 -0
  153. package/src/renderers/components/form.ts +327 -0
  154. package/src/renderers/components/index.ts +21 -0
  155. package/src/renderers/components/search.ts +449 -0
  156. package/src/renderers/components/select.ts +222 -0
  157. package/src/renderers/index.ts +101 -0
  158. package/src/renderers/interactive/component-handlers.ts +622 -0
  159. package/src/renderers/interactive/cursor-manager.ts +147 -0
  160. package/src/renderers/interactive/focus-manager.ts +279 -0
  161. package/src/renderers/interactive/index.ts +661 -0
  162. package/src/renderers/interactive/input-handler.ts +164 -0
  163. package/src/renderers/interactive/keyboard-handler.ts +212 -0
  164. package/src/renderers/interactive/mouse-handler.ts +167 -0
  165. package/src/renderers/interactive/state-manager.ts +109 -0
  166. package/src/renderers/interactive/types.ts +338 -0
  167. package/src/renderers/interactive-string.ts +299 -0
  168. package/src/renderers/interactive.ts +59 -0
  169. package/src/renderers/markdown.ts +950 -0
  170. package/src/renderers/sidebar.ts +549 -0
  171. package/src/renderers/tabs.ts +682 -0
  172. package/src/renderers/text.ts +791 -0
  173. package/src/renderers/unicode.ts +917 -0
  174. package/src/renderers/utils.ts +942 -0
  175. package/src/router/adapters.ts +383 -0
  176. package/src/router/types.ts +140 -0
  177. package/src/router/utils.ts +452 -0
  178. package/src/schemas.ts +205 -0
  179. package/src/storybook/index.ts +91 -0
  180. package/src/storybook/interactive-decorator.tsx +659 -0
  181. package/src/storybook/keyboard-simulator.ts +501 -0
  182. package/src/theme/ansi-codes.ts +80 -0
  183. package/src/theme/box-drawing.ts +132 -0
  184. package/src/theme/color-convert.ts +254 -0
  185. package/src/theme/color-support.ts +321 -0
  186. package/src/theme/index.ts +134 -0
  187. package/src/theme/strip-ansi.ts +50 -0
  188. package/src/theme/tailwind-map.ts +469 -0
  189. package/src/theme/text-styles.ts +206 -0
  190. package/src/theme/theme-system.ts +568 -0
  191. package/src/types.ts +103 -0
@@ -0,0 +1,1095 @@
1
+ /**
2
+ * @mdxui/terminal Command Palette Component Tests
3
+ *
4
+ * TDD RED Phase: Tests for the Command Palette navigation component.
5
+ * All tests should FAIL initially because the Command Palette renderers don't exist yet.
6
+ *
7
+ * The Command Palette component provides quick command access with:
8
+ * - Search/filter input
9
+ * - Command list with fuzzy matching
10
+ * - Keyboard navigation
11
+ * - Command categories/groups
12
+ * - Shortcut key display
13
+ * - Command execution
14
+ *
15
+ * This is part of the Universal Terminal UI 6-tier rendering system.
16
+ */
17
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
18
+ import type { UINode, RenderTier, RenderContext, ThemeTokens } from '../../../core/types'
19
+
20
+ // ============================================================================
21
+ // Test Utilities
22
+ // ============================================================================
23
+
24
+ const RENDER_TIERS: RenderTier[] = ['text', 'markdown', 'ascii', 'unicode', 'ansi', 'interactive']
25
+
26
+ function createTestTheme(): ThemeTokens {
27
+ return {
28
+ primary: '\x1b[34m',
29
+ secondary: '\x1b[36m',
30
+ muted: '\x1b[90m',
31
+ foreground: '\x1b[37m',
32
+ background: '\x1b[40m',
33
+ border: '\x1b[90m',
34
+ success: '\x1b[32m',
35
+ warning: '\x1b[33m',
36
+ error: '\x1b[31m',
37
+ info: '\x1b[34m',
38
+ }
39
+ }
40
+
41
+ function createContext(tier: RenderTier): RenderContext {
42
+ return {
43
+ tier,
44
+ width: 80,
45
+ height: 24,
46
+ depth: 0,
47
+ theme: createTestTheme(),
48
+ interactive: tier === 'interactive',
49
+ }
50
+ }
51
+
52
+ // ============================================================================
53
+ // Command Palette Types
54
+ // ============================================================================
55
+
56
+ interface Command {
57
+ id: string
58
+ label: string
59
+ description?: string
60
+ shortcut?: string
61
+ icon?: string
62
+ category?: string
63
+ disabled?: boolean
64
+ keywords?: string[]
65
+ }
66
+
67
+ interface CommandGroup {
68
+ id: string
69
+ label: string
70
+ commands: Command[]
71
+ }
72
+
73
+ interface CommandPaletteProps {
74
+ commands: Command[]
75
+ groups?: CommandGroup[]
76
+ searchQuery?: string
77
+ selectedIndex?: number
78
+ placeholder?: string
79
+ maxResults?: number
80
+ showShortcuts?: boolean
81
+ onSelect?: (commandId: string) => void
82
+ onQueryChange?: (query: string) => void
83
+ onClose?: () => void
84
+ }
85
+
86
+ function createCommandPaletteNode(props: CommandPaletteProps): UINode {
87
+ return {
88
+ type: 'command-palette',
89
+ props: {
90
+ searchQuery: props.searchQuery || '',
91
+ selectedIndex: props.selectedIndex || 0,
92
+ placeholder: props.placeholder || 'Search commands...',
93
+ maxResults: props.maxResults || 10,
94
+ showShortcuts: props.showShortcuts !== false,
95
+ },
96
+ children: [
97
+ {
98
+ type: 'command-palette-input',
99
+ props: {
100
+ value: props.searchQuery || '',
101
+ placeholder: props.placeholder || 'Search commands...',
102
+ },
103
+ },
104
+ {
105
+ type: 'command-palette-list',
106
+ props: {},
107
+ children: props.groups
108
+ ? props.groups.map((group) => ({
109
+ type: 'command-group',
110
+ props: { id: group.id, label: group.label },
111
+ children: group.commands.map((cmd, idx) => ({
112
+ type: 'command-item',
113
+ props: {
114
+ ...cmd,
115
+ selected: props.selectedIndex === idx,
116
+ },
117
+ })),
118
+ }))
119
+ : props.commands.map((cmd, idx) => ({
120
+ type: 'command-item',
121
+ props: {
122
+ ...cmd,
123
+ selected: props.selectedIndex === idx,
124
+ },
125
+ })),
126
+ },
127
+ ],
128
+ }
129
+ }
130
+
131
+ // ============================================================================
132
+ // Test Suite: Basic Command Palette Rendering
133
+ // ============================================================================
134
+
135
+ describe('Command Palette Component', () => {
136
+ describe('basic rendering', () => {
137
+ it('renders command palette with commands', async () => {
138
+ const { renderCommandPalette } = await import('../../../renderers/command-palette')
139
+
140
+ const node = createCommandPaletteNode({
141
+ commands: [
142
+ { id: 'new-file', label: 'New File' },
143
+ { id: 'open-file', label: 'Open File' },
144
+ { id: 'save', label: 'Save' },
145
+ ],
146
+ })
147
+
148
+ const ctx = createContext('unicode')
149
+ const result = renderCommandPalette(node, ctx)
150
+
151
+ expect(result).toContain('New File')
152
+ expect(result).toContain('Open File')
153
+ expect(result).toContain('Save')
154
+ })
155
+
156
+ it('renders search input', async () => {
157
+ const { renderCommandPalette } = await import('../../../renderers/command-palette')
158
+
159
+ const node = createCommandPaletteNode({
160
+ commands: [{ id: 'cmd', label: 'Command' }],
161
+ placeholder: 'Type to search...',
162
+ })
163
+
164
+ const ctx = createContext('unicode')
165
+ const result = renderCommandPalette(node, ctx)
166
+
167
+ expect(result).toContain('Type to search...')
168
+ })
169
+
170
+ it('renders with search query', async () => {
171
+ const { renderCommandPalette } = await import('../../../renderers/command-palette')
172
+
173
+ const node = createCommandPaletteNode({
174
+ commands: [{ id: 'cmd', label: 'Command' }],
175
+ searchQuery: 'fil',
176
+ })
177
+
178
+ const ctx = createContext('unicode')
179
+ const result = renderCommandPalette(node, ctx)
180
+
181
+ expect(result).toContain('fil')
182
+ })
183
+
184
+ it('renders command descriptions', async () => {
185
+ const { renderCommandPalette } = await import('../../../renderers/command-palette')
186
+
187
+ const node = createCommandPaletteNode({
188
+ commands: [
189
+ { id: 'new-file', label: 'New File', description: 'Create a new file' },
190
+ { id: 'open', label: 'Open', description: 'Open an existing file' },
191
+ ],
192
+ })
193
+
194
+ const ctx = createContext('unicode')
195
+ const result = renderCommandPalette(node, ctx)
196
+
197
+ expect(result).toContain('Create a new file')
198
+ expect(result).toContain('Open an existing file')
199
+ })
200
+
201
+ it('renders command shortcuts', async () => {
202
+ const { renderCommandPalette } = await import('../../../renderers/command-palette')
203
+
204
+ const node = createCommandPaletteNode({
205
+ commands: [
206
+ { id: 'save', label: 'Save', shortcut: 'Ctrl+S' },
207
+ { id: 'quit', label: 'Quit', shortcut: 'Ctrl+Q' },
208
+ ],
209
+ showShortcuts: true,
210
+ })
211
+
212
+ const ctx = createContext('unicode')
213
+ const result = renderCommandPalette(node, ctx)
214
+
215
+ expect(result).toContain('Ctrl+S')
216
+ expect(result).toContain('Ctrl+Q')
217
+ })
218
+
219
+ it('renders command icons', async () => {
220
+ const { renderCommandPalette } = await import('../../../renderers/command-palette')
221
+
222
+ const node = createCommandPaletteNode({
223
+ commands: [
224
+ { id: 'file', label: 'File', icon: '\u2630' },
225
+ { id: 'settings', label: 'Settings', icon: '\u2699' },
226
+ ],
227
+ })
228
+
229
+ const ctx = createContext('unicode')
230
+ const result = renderCommandPalette(node, ctx)
231
+
232
+ expect(result).toContain('\u2630')
233
+ expect(result).toContain('\u2699')
234
+ })
235
+
236
+ it('returns empty for no commands', async () => {
237
+ const { renderCommandPalette } = await import('../../../renderers/command-palette')
238
+
239
+ const node = createCommandPaletteNode({ commands: [] })
240
+ const ctx = createContext('unicode')
241
+ const result = renderCommandPalette(node, ctx)
242
+
243
+ // Should still show input but no results
244
+ expect(result).toContain('Search')
245
+ })
246
+ })
247
+
248
+ // ============================================================================
249
+ // Search/Filter Tests
250
+ // ============================================================================
251
+
252
+ describe('search and filtering', () => {
253
+ it('filters commands by query', async () => {
254
+ const { filterCommands } = await import('../../../renderers/command-palette')
255
+
256
+ const commands: Command[] = [
257
+ { id: 'new-file', label: 'New File' },
258
+ { id: 'open-file', label: 'Open File' },
259
+ { id: 'save', label: 'Save' },
260
+ ]
261
+
262
+ const filtered = filterCommands(commands, 'file')
263
+
264
+ expect(filtered.length).toBe(2)
265
+ expect(filtered.map((c) => c.id)).toContain('new-file')
266
+ expect(filtered.map((c) => c.id)).toContain('open-file')
267
+ })
268
+
269
+ it('matches against label case-insensitively', async () => {
270
+ const { filterCommands } = await import('../../../renderers/command-palette')
271
+
272
+ const commands: Command[] = [
273
+ { id: 'new-file', label: 'New File' },
274
+ { id: 'open', label: 'Open' },
275
+ ]
276
+
277
+ const filtered = filterCommands(commands, 'NEW')
278
+
279
+ expect(filtered.length).toBe(1)
280
+ expect(filtered[0].id).toBe('new-file')
281
+ })
282
+
283
+ it('matches against keywords', async () => {
284
+ const { filterCommands } = await import('../../../renderers/command-palette')
285
+
286
+ const commands: Command[] = [
287
+ { id: 'new-file', label: 'New File', keywords: ['create', 'add'] },
288
+ { id: 'save', label: 'Save', keywords: ['store', 'write'] },
289
+ ]
290
+
291
+ const filtered = filterCommands(commands, 'create')
292
+
293
+ expect(filtered.length).toBe(1)
294
+ expect(filtered[0].id).toBe('new-file')
295
+ })
296
+
297
+ it('matches against description', async () => {
298
+ const { filterCommands } = await import('../../../renderers/command-palette')
299
+
300
+ const commands: Command[] = [
301
+ { id: 'export', label: 'Export', description: 'Export data to CSV' },
302
+ { id: 'import', label: 'Import', description: 'Import data from file' },
303
+ ]
304
+
305
+ const filtered = filterCommands(commands, 'csv')
306
+
307
+ expect(filtered.length).toBe(1)
308
+ expect(filtered[0].id).toBe('export')
309
+ })
310
+
311
+ it('supports fuzzy matching', async () => {
312
+ const { filterCommands } = await import('../../../renderers/command-palette')
313
+
314
+ const commands: Command[] = [
315
+ { id: 'new-file', label: 'New File' },
316
+ { id: 'navigate', label: 'Navigate' },
317
+ ]
318
+
319
+ // "nf" should fuzzy match "New File"
320
+ const filtered = filterCommands(commands, 'nf')
321
+
322
+ expect(filtered.map((c) => c.id)).toContain('new-file')
323
+ })
324
+
325
+ it('highlights matched characters in results', async () => {
326
+ const { renderCommandPalette } = await import('../../../renderers/command-palette')
327
+
328
+ const node = createCommandPaletteNode({
329
+ commands: [{ id: 'new-file', label: 'New File' }],
330
+ searchQuery: 'file',
331
+ })
332
+
333
+ const ctx = createContext('ansi')
334
+ const result = renderCommandPalette(node, ctx)
335
+
336
+ // Matched portion should be highlighted (different color/bold)
337
+ // "file" portion should have distinct styling
338
+ expect(result).toContain('\x1b[')
339
+ })
340
+
341
+ it('respects maxResults limit', async () => {
342
+ const { filterCommands } = await import('../../../renderers/command-palette')
343
+
344
+ const commands: Command[] = Array.from({ length: 20 }, (_, i) => ({
345
+ id: `cmd-${i}`,
346
+ label: `Command ${i}`,
347
+ }))
348
+
349
+ const filtered = filterCommands(commands, 'Command', { maxResults: 5 })
350
+
351
+ expect(filtered.length).toBe(5)
352
+ })
353
+
354
+ it('shows no results message when nothing matches', async () => {
355
+ const { renderCommandPalette } = await import('../../../renderers/command-palette')
356
+
357
+ const node = createCommandPaletteNode({
358
+ commands: [
359
+ { id: 'save', label: 'Save' },
360
+ { id: 'open', label: 'Open' },
361
+ ],
362
+ searchQuery: 'xyz123',
363
+ })
364
+
365
+ const ctx = createContext('unicode')
366
+ const result = renderCommandPalette(node, ctx)
367
+
368
+ expect(result).toMatch(/no results|no commands|nothing found/i)
369
+ })
370
+ })
371
+
372
+ // ============================================================================
373
+ // Command Selection Tests
374
+ // ============================================================================
375
+
376
+ describe('command selection', () => {
377
+ it('highlights selected command', async () => {
378
+ const { renderCommandPalette } = await import('../../../renderers/command-palette')
379
+
380
+ const node = createCommandPaletteNode({
381
+ commands: [
382
+ { id: 'cmd1', label: 'Command 1' },
383
+ { id: 'cmd2', label: 'Command 2' },
384
+ { id: 'cmd3', label: 'Command 3' },
385
+ ],
386
+ selectedIndex: 1,
387
+ })
388
+
389
+ const ctx = createContext('ansi')
390
+ const result = renderCommandPalette(node, ctx)
391
+
392
+ // Selected command should have highlight styling
393
+ const cmd2Index = result.indexOf('Command 2')
394
+ const beforeCmd2 = result.slice(Math.max(0, cmd2Index - 25), cmd2Index)
395
+ expect(beforeCmd2).toMatch(/\x1b\[.*m/)
396
+ })
397
+
398
+ it('text tier shows selection indicator', async () => {
399
+ const { renderCommandPalette } = await import('../../../renderers/command-palette')
400
+
401
+ const node = createCommandPaletteNode({
402
+ commands: [
403
+ { id: 'cmd1', label: 'Command 1' },
404
+ { id: 'cmd2', label: 'Command 2' },
405
+ ],
406
+ selectedIndex: 0,
407
+ })
408
+
409
+ const ctx = createContext('text')
410
+ const result = renderCommandPalette(node, ctx)
411
+
412
+ // Text tier should use > or * or [ ] for selection
413
+ expect(result).toMatch(/[>*\[\]]\s*Command 1/)
414
+ })
415
+
416
+ it('selection uses inverse video in ansi tier', async () => {
417
+ const { renderCommandPalette } = await import('../../../renderers/command-palette')
418
+
419
+ const node = createCommandPaletteNode({
420
+ commands: [{ id: 'selected', label: 'Selected' }],
421
+ selectedIndex: 0,
422
+ })
423
+
424
+ const ctx = createContext('ansi')
425
+ const result = renderCommandPalette(node, ctx)
426
+
427
+ // Inverse video code is \x1b[7m
428
+ expect(result).toContain('\x1b[7m')
429
+ })
430
+ })
431
+
432
+ // ============================================================================
433
+ // Command Groups Tests
434
+ // ============================================================================
435
+
436
+ describe('command groups', () => {
437
+ it('renders grouped commands', async () => {
438
+ const { renderCommandPalette } = await import('../../../renderers/command-palette')
439
+
440
+ const node = createCommandPaletteNode({
441
+ commands: [],
442
+ groups: [
443
+ {
444
+ id: 'file',
445
+ label: 'File',
446
+ commands: [
447
+ { id: 'new', label: 'New' },
448
+ { id: 'open', label: 'Open' },
449
+ ],
450
+ },
451
+ {
452
+ id: 'edit',
453
+ label: 'Edit',
454
+ commands: [
455
+ { id: 'cut', label: 'Cut' },
456
+ { id: 'copy', label: 'Copy' },
457
+ ],
458
+ },
459
+ ],
460
+ })
461
+
462
+ const ctx = createContext('unicode')
463
+ const result = renderCommandPalette(node, ctx)
464
+
465
+ // Group labels should be shown
466
+ expect(result).toContain('File')
467
+ expect(result).toContain('Edit')
468
+
469
+ // Commands should be shown
470
+ expect(result).toContain('New')
471
+ expect(result).toContain('Open')
472
+ expect(result).toContain('Cut')
473
+ expect(result).toContain('Copy')
474
+ })
475
+
476
+ it('groups are rendered with visual separation', async () => {
477
+ const { renderCommandPalette } = await import('../../../renderers/command-palette')
478
+
479
+ const node = createCommandPaletteNode({
480
+ commands: [],
481
+ groups: [
482
+ { id: 'g1', label: 'Group 1', commands: [{ id: 'c1', label: 'Cmd 1' }] },
483
+ { id: 'g2', label: 'Group 2', commands: [{ id: 'c2', label: 'Cmd 2' }] },
484
+ ],
485
+ })
486
+
487
+ const ctx = createContext('unicode')
488
+ const result = renderCommandPalette(node, ctx)
489
+
490
+ // Groups should have separators or distinct sections
491
+ const lines = result.split('\n')
492
+ const group1Line = lines.findIndex((l) => l.includes('Group 1'))
493
+ const group2Line = lines.findIndex((l) => l.includes('Group 2'))
494
+
495
+ expect(group1Line).toBeGreaterThanOrEqual(0)
496
+ expect(group2Line).toBeGreaterThan(group1Line)
497
+ })
498
+
499
+ it('filters commands across groups', async () => {
500
+ const { filterGroupedCommands } = await import('../../../renderers/command-palette')
501
+
502
+ const groups: CommandGroup[] = [
503
+ {
504
+ id: 'file',
505
+ label: 'File',
506
+ commands: [
507
+ { id: 'new-file', label: 'New File' },
508
+ { id: 'open', label: 'Open' },
509
+ ],
510
+ },
511
+ {
512
+ id: 'edit',
513
+ label: 'Edit',
514
+ commands: [
515
+ { id: 'copy', label: 'Copy' },
516
+ { id: 'paste', label: 'Paste' },
517
+ ],
518
+ },
519
+ ]
520
+
521
+ const filtered = filterGroupedCommands(groups, 'new')
522
+
523
+ // Should only return groups with matching commands
524
+ expect(filtered.length).toBe(1)
525
+ expect(filtered[0].id).toBe('file')
526
+ expect(filtered[0].commands.length).toBe(1)
527
+ })
528
+
529
+ it('group label styling differs from command styling', async () => {
530
+ const { renderCommandPalette } = await import('../../../renderers/command-palette')
531
+
532
+ const node = createCommandPaletteNode({
533
+ commands: [],
534
+ groups: [
535
+ { id: 'g1', label: 'Group Label', commands: [{ id: 'c1', label: 'Command' }] },
536
+ ],
537
+ })
538
+
539
+ const ctx = createContext('ansi')
540
+ const result = renderCommandPalette(node, ctx)
541
+
542
+ // Group labels should be muted/different color
543
+ expect(result).toContain('\x1b[90m') // Muted color
544
+ })
545
+ })
546
+
547
+ // ============================================================================
548
+ // Disabled Commands Tests
549
+ // ============================================================================
550
+
551
+ describe('disabled commands', () => {
552
+ it('renders disabled commands with muted styling', async () => {
553
+ const { renderCommandPalette } = await import('../../../renderers/command-palette')
554
+
555
+ const node = createCommandPaletteNode({
556
+ commands: [
557
+ { id: 'enabled', label: 'Enabled' },
558
+ { id: 'disabled', label: 'Disabled', disabled: true },
559
+ ],
560
+ })
561
+
562
+ const ctx = createContext('ansi')
563
+ const result = renderCommandPalette(node, ctx)
564
+
565
+ expect(result).toContain('Disabled')
566
+ expect(result).toContain('\x1b[90m')
567
+ })
568
+
569
+ it('skips disabled commands during navigation', async () => {
570
+ const { createCommandPaletteState, handleCommandPaletteKey } = await import('../../../renderers/command-palette')
571
+
572
+ const state = createCommandPaletteState({
573
+ commands: [
574
+ { id: 'cmd1', label: 'Command 1' },
575
+ { id: 'disabled', label: 'Disabled', disabled: true },
576
+ { id: 'cmd3', label: 'Command 3' },
577
+ ],
578
+ selectedIndex: 0,
579
+ })
580
+
581
+ // Move down should skip disabled
582
+ const afterDown = handleCommandPaletteKey(state, 'down')
583
+ expect(afterDown.selectedIndex).toBe(2)
584
+ })
585
+
586
+ it('cannot execute disabled commands', async () => {
587
+ const { createCommandPaletteState, handleCommandPaletteKey } = await import('../../../renderers/command-palette')
588
+
589
+ const onSelect = vi.fn()
590
+
591
+ const state = createCommandPaletteState({
592
+ commands: [{ id: 'disabled', label: 'Disabled', disabled: true }],
593
+ selectedIndex: 0,
594
+ onSelect,
595
+ })
596
+
597
+ handleCommandPaletteKey(state, 'enter')
598
+
599
+ expect(onSelect).not.toHaveBeenCalled()
600
+ })
601
+ })
602
+
603
+ // ============================================================================
604
+ // Tier-Specific Rendering Tests
605
+ // ============================================================================
606
+
607
+ describe('tier-specific rendering', () => {
608
+ const sampleNode = createCommandPaletteNode({
609
+ commands: [
610
+ { id: 'new', label: 'New File', shortcut: 'Ctrl+N', description: 'Create new' },
611
+ { id: 'open', label: 'Open File', shortcut: 'Ctrl+O', description: 'Open existing' },
612
+ ],
613
+ searchQuery: '',
614
+ selectedIndex: 0,
615
+ })
616
+
617
+ it('text tier renders plain text', async () => {
618
+ const { renderCommandPalette } = await import('../../../renderers/command-palette')
619
+
620
+ const ctx = createContext('text')
621
+ const result = renderCommandPalette(sampleNode, ctx)
622
+
623
+ expect(result).toContain('New File')
624
+ expect(result).toContain('Open File')
625
+ expect(result).toContain('Ctrl+N')
626
+
627
+ // No ANSI codes
628
+ expect(result).not.toContain('\x1b[')
629
+ })
630
+
631
+ it('markdown tier uses formatting', async () => {
632
+ const { renderCommandPalette } = await import('../../../renderers/command-palette')
633
+
634
+ const ctx = createContext('markdown')
635
+ const result = renderCommandPalette(sampleNode, ctx)
636
+
637
+ // Shortcuts might be in code blocks
638
+ expect(result).toMatch(/`Ctrl\+[NO]`/)
639
+ })
640
+
641
+ it('ascii tier uses ASCII borders', async () => {
642
+ const { renderCommandPalette } = await import('../../../renderers/command-palette')
643
+
644
+ const ctx = createContext('ascii')
645
+ const result = renderCommandPalette(sampleNode, ctx)
646
+
647
+ expect(result).toMatch(/[|+\-=]/)
648
+ })
649
+
650
+ it('unicode tier uses box drawing', async () => {
651
+ const { renderCommandPalette } = await import('../../../renderers/command-palette')
652
+
653
+ const ctx = createContext('unicode')
654
+ const result = renderCommandPalette(sampleNode, ctx)
655
+
656
+ expect(result).toMatch(/[\u2500-\u257F]/)
657
+ })
658
+
659
+ it('ansi tier includes colors', async () => {
660
+ const { renderCommandPalette } = await import('../../../renderers/command-palette')
661
+
662
+ const ctx = createContext('ansi')
663
+ const result = renderCommandPalette(sampleNode, ctx)
664
+
665
+ expect(result).toContain('\x1b[')
666
+ })
667
+
668
+ it('interactive tier includes focus and cursor', async () => {
669
+ const { renderCommandPalette } = await import('../../../renderers/command-palette')
670
+
671
+ const ctx = createContext('interactive')
672
+ const result = renderCommandPalette(sampleNode, ctx)
673
+
674
+ // Should have cursor indicator in input
675
+ expect(result).toMatch(/\x1b\[7m|\x1b\[4m|[|_]/)
676
+ })
677
+ })
678
+
679
+ // ============================================================================
680
+ // Keyboard Navigation Tests (INTERACTIVE Tier)
681
+ // ============================================================================
682
+
683
+ describe('keyboard navigation (interactive tier)', () => {
684
+ it('provides keyboard bindings', async () => {
685
+ const { getCommandPaletteKeyBindings } = await import('../../../renderers/command-palette')
686
+
687
+ const bindings = getCommandPaletteKeyBindings()
688
+
689
+ expect(bindings.up).toBe('select-prev')
690
+ expect(bindings.down).toBe('select-next')
691
+ expect(bindings.k).toBe('select-prev')
692
+ expect(bindings.j).toBe('select-next')
693
+ expect(bindings.enter).toBe('execute')
694
+ expect(bindings.escape).toBe('close')
695
+ expect(bindings['ctrl+n']).toBe('select-next')
696
+ expect(bindings['ctrl+p']).toBe('select-prev')
697
+ })
698
+
699
+ it('up/down or j/k navigates command list', async () => {
700
+ const { createCommandPaletteState, handleCommandPaletteKey } = await import('../../../renderers/command-palette')
701
+
702
+ const state = createCommandPaletteState({
703
+ commands: [
704
+ { id: 'cmd1', label: 'Command 1' },
705
+ { id: 'cmd2', label: 'Command 2' },
706
+ { id: 'cmd3', label: 'Command 3' },
707
+ ],
708
+ selectedIndex: 0,
709
+ })
710
+
711
+ // Move down
712
+ const afterDown = handleCommandPaletteKey(state, 'down')
713
+ expect(afterDown.selectedIndex).toBe(1)
714
+
715
+ const afterJ = handleCommandPaletteKey(afterDown, 'j')
716
+ expect(afterJ.selectedIndex).toBe(2)
717
+
718
+ // Move up
719
+ const afterUp = handleCommandPaletteKey(afterJ, 'up')
720
+ expect(afterUp.selectedIndex).toBe(1)
721
+
722
+ const afterK = handleCommandPaletteKey(afterUp, 'k')
723
+ expect(afterK.selectedIndex).toBe(0)
724
+ })
725
+
726
+ it('enter executes selected command', async () => {
727
+ const { createCommandPaletteState, handleCommandPaletteKey } = await import('../../../renderers/command-palette')
728
+
729
+ const onSelect = vi.fn()
730
+
731
+ const state = createCommandPaletteState({
732
+ commands: [
733
+ { id: 'cmd1', label: 'Command 1' },
734
+ { id: 'cmd2', label: 'Command 2' },
735
+ ],
736
+ selectedIndex: 1,
737
+ onSelect,
738
+ })
739
+
740
+ handleCommandPaletteKey(state, 'enter')
741
+
742
+ expect(onSelect).toHaveBeenCalledWith('cmd2')
743
+ })
744
+
745
+ it('escape closes palette', async () => {
746
+ const { createCommandPaletteState, handleCommandPaletteKey } = await import('../../../renderers/command-palette')
747
+
748
+ const onClose = vi.fn()
749
+
750
+ const state = createCommandPaletteState({
751
+ commands: [{ id: 'cmd', label: 'Command' }],
752
+ onClose,
753
+ })
754
+
755
+ handleCommandPaletteKey(state, 'escape')
756
+
757
+ expect(onClose).toHaveBeenCalled()
758
+ })
759
+
760
+ it('ctrl+n/ctrl+p for Emacs-style navigation', async () => {
761
+ const { createCommandPaletteState, handleCommandPaletteKey } = await import('../../../renderers/command-palette')
762
+
763
+ const state = createCommandPaletteState({
764
+ commands: [
765
+ { id: 'cmd1', label: 'Command 1' },
766
+ { id: 'cmd2', label: 'Command 2' },
767
+ ],
768
+ selectedIndex: 0,
769
+ })
770
+
771
+ // Ctrl+N moves next
772
+ const afterCtrlN = handleCommandPaletteKey(state, 'ctrl+n')
773
+ expect(afterCtrlN.selectedIndex).toBe(1)
774
+
775
+ // Ctrl+P moves prev
776
+ const afterCtrlP = handleCommandPaletteKey(afterCtrlN, 'ctrl+p')
777
+ expect(afterCtrlP.selectedIndex).toBe(0)
778
+ })
779
+
780
+ it('wraps navigation at boundaries', async () => {
781
+ const { createCommandPaletteState, handleCommandPaletteKey } = await import('../../../renderers/command-palette')
782
+
783
+ const state = createCommandPaletteState({
784
+ commands: [
785
+ { id: 'cmd1', label: 'Command 1' },
786
+ { id: 'cmd2', label: 'Command 2' },
787
+ ],
788
+ selectedIndex: 1,
789
+ wrapNavigation: true,
790
+ })
791
+
792
+ // Move down from last wraps to first
793
+ const afterDown = handleCommandPaletteKey(state, 'down')
794
+ expect(afterDown.selectedIndex).toBe(0)
795
+
796
+ // Move up from first wraps to last
797
+ const afterUp = handleCommandPaletteKey(afterDown, 'up')
798
+ expect(afterUp.selectedIndex).toBe(1)
799
+ })
800
+
801
+ it('typing updates search query', async () => {
802
+ const { createCommandPaletteState, handleCommandPaletteKey } = await import('../../../renderers/command-palette')
803
+
804
+ const onQueryChange = vi.fn()
805
+
806
+ const state = createCommandPaletteState({
807
+ commands: [{ id: 'cmd', label: 'Command' }],
808
+ searchQuery: '',
809
+ onQueryChange,
810
+ })
811
+
812
+ // Type a character
813
+ handleCommandPaletteKey(state, 'f')
814
+
815
+ expect(onQueryChange).toHaveBeenCalledWith('f')
816
+ })
817
+
818
+ it('backspace removes last character from query', async () => {
819
+ const { createCommandPaletteState, handleCommandPaletteKey } = await import('../../../renderers/command-palette')
820
+
821
+ const onQueryChange = vi.fn()
822
+
823
+ const state = createCommandPaletteState({
824
+ commands: [{ id: 'cmd', label: 'Command' }],
825
+ searchQuery: 'file',
826
+ onQueryChange,
827
+ })
828
+
829
+ handleCommandPaletteKey(state, 'backspace')
830
+
831
+ expect(onQueryChange).toHaveBeenCalledWith('fil')
832
+ })
833
+
834
+ it('ctrl+u clears search query', async () => {
835
+ const { createCommandPaletteState, handleCommandPaletteKey } = await import('../../../renderers/command-palette')
836
+
837
+ const onQueryChange = vi.fn()
838
+
839
+ const state = createCommandPaletteState({
840
+ commands: [{ id: 'cmd', label: 'Command' }],
841
+ searchQuery: 'some query',
842
+ onQueryChange,
843
+ })
844
+
845
+ handleCommandPaletteKey(state, 'ctrl+u')
846
+
847
+ expect(onQueryChange).toHaveBeenCalledWith('')
848
+ })
849
+
850
+ it('home moves to first result', async () => {
851
+ const { createCommandPaletteState, handleCommandPaletteKey } = await import('../../../renderers/command-palette')
852
+
853
+ const state = createCommandPaletteState({
854
+ commands: [
855
+ { id: 'cmd1', label: 'Command 1' },
856
+ { id: 'cmd2', label: 'Command 2' },
857
+ { id: 'cmd3', label: 'Command 3' },
858
+ ],
859
+ selectedIndex: 2,
860
+ })
861
+
862
+ const afterHome = handleCommandPaletteKey(state, 'home')
863
+ expect(afterHome.selectedIndex).toBe(0)
864
+ })
865
+
866
+ it('end moves to last result', async () => {
867
+ const { createCommandPaletteState, handleCommandPaletteKey } = await import('../../../renderers/command-palette')
868
+
869
+ const state = createCommandPaletteState({
870
+ commands: [
871
+ { id: 'cmd1', label: 'Command 1' },
872
+ { id: 'cmd2', label: 'Command 2' },
873
+ { id: 'cmd3', label: 'Command 3' },
874
+ ],
875
+ selectedIndex: 0,
876
+ })
877
+
878
+ const afterEnd = handleCommandPaletteKey(state, 'end')
879
+ expect(afterEnd.selectedIndex).toBe(2)
880
+ })
881
+
882
+ it('tab cycles through results', async () => {
883
+ const { createCommandPaletteState, handleCommandPaletteKey } = await import('../../../renderers/command-palette')
884
+
885
+ const state = createCommandPaletteState({
886
+ commands: [
887
+ { id: 'cmd1', label: 'Command 1' },
888
+ { id: 'cmd2', label: 'Command 2' },
889
+ ],
890
+ selectedIndex: 0,
891
+ })
892
+
893
+ const afterTab = handleCommandPaletteKey(state, 'tab')
894
+ expect(afterTab.selectedIndex).toBe(1)
895
+ })
896
+
897
+ it('shift+tab cycles backwards', async () => {
898
+ const { createCommandPaletteState, handleCommandPaletteKey } = await import('../../../renderers/command-palette')
899
+
900
+ const state = createCommandPaletteState({
901
+ commands: [
902
+ { id: 'cmd1', label: 'Command 1' },
903
+ { id: 'cmd2', label: 'Command 2' },
904
+ ],
905
+ selectedIndex: 1,
906
+ })
907
+
908
+ const afterShiftTab = handleCommandPaletteKey(state, 'shift+tab')
909
+ expect(afterShiftTab.selectedIndex).toBe(0)
910
+ })
911
+ })
912
+
913
+ // ============================================================================
914
+ // Search Input Focus Tests
915
+ // ============================================================================
916
+
917
+ describe('search input', () => {
918
+ it('shows cursor in search input when focused', async () => {
919
+ const { renderCommandPalette } = await import('../../../renderers/command-palette')
920
+
921
+ const node = createCommandPaletteNode({
922
+ commands: [{ id: 'cmd', label: 'Command' }],
923
+ searchQuery: 'test',
924
+ })
925
+
926
+ const ctx = createContext('interactive')
927
+ const result = renderCommandPalette(node, ctx)
928
+
929
+ // Should show cursor indicator (inverse video or cursor char)
930
+ expect(result).toMatch(/\x1b\[7m|[|_\u2588]/)
931
+ })
932
+
933
+ it('renders placeholder when query is empty', async () => {
934
+ const { renderCommandPalette } = await import('../../../renderers/command-palette')
935
+
936
+ const node = createCommandPaletteNode({
937
+ commands: [{ id: 'cmd', label: 'Command' }],
938
+ searchQuery: '',
939
+ placeholder: 'Type a command...',
940
+ })
941
+
942
+ const ctx = createContext('unicode')
943
+ const result = renderCommandPalette(node, ctx)
944
+
945
+ expect(result).toContain('Type a command...')
946
+ })
947
+
948
+ it('hides placeholder when query has text', async () => {
949
+ const { renderCommandPalette } = await import('../../../renderers/command-palette')
950
+
951
+ const node = createCommandPaletteNode({
952
+ commands: [{ id: 'cmd', label: 'Command' }],
953
+ searchQuery: 'fil',
954
+ placeholder: 'Type a command...',
955
+ })
956
+
957
+ const ctx = createContext('unicode')
958
+ const result = renderCommandPalette(node, ctx)
959
+
960
+ expect(result).not.toContain('Type a command...')
961
+ expect(result).toContain('fil')
962
+ })
963
+ })
964
+
965
+ // ============================================================================
966
+ // Modal Appearance Tests
967
+ // ============================================================================
968
+
969
+ describe('modal appearance', () => {
970
+ it('renders with border', async () => {
971
+ const { renderCommandPalette } = await import('../../../renderers/command-palette')
972
+
973
+ const node = createCommandPaletteNode({
974
+ commands: [{ id: 'cmd', label: 'Command' }],
975
+ })
976
+
977
+ const ctx = createContext('unicode')
978
+ const result = renderCommandPalette(node, ctx)
979
+
980
+ // Should have box border characters
981
+ expect(result).toMatch(/[\u250C\u2510\u2514\u2518\u2500\u2502]/)
982
+ })
983
+
984
+ it('respects context width', async () => {
985
+ const { renderCommandPalette } = await import('../../../renderers/command-palette')
986
+
987
+ const node = createCommandPaletteNode({
988
+ commands: [{ id: 'cmd', label: 'This is a very long command label' }],
989
+ })
990
+
991
+ const ctx = createContext('unicode')
992
+ ctx.width = 40
993
+
994
+ const result = renderCommandPalette(node, ctx)
995
+ const lines = result.split('\n')
996
+
997
+ for (const line of lines) {
998
+ const stripped = line.replace(/\x1b\[[\d;]*m/g, '')
999
+ expect(stripped.length).toBeLessThanOrEqual(40)
1000
+ }
1001
+ })
1002
+
1003
+ it('limits results to maxResults', async () => {
1004
+ const { renderCommandPalette } = await import('../../../renderers/command-palette')
1005
+
1006
+ const commands = Array.from({ length: 20 }, (_, i) => ({
1007
+ id: `cmd-${i}`,
1008
+ label: `Command ${i}`,
1009
+ }))
1010
+
1011
+ const node = createCommandPaletteNode({
1012
+ commands,
1013
+ maxResults: 5,
1014
+ })
1015
+
1016
+ const ctx = createContext('unicode')
1017
+ const result = renderCommandPalette(node, ctx)
1018
+
1019
+ // Should only show 5 commands
1020
+ const commandCount = (result.match(/Command \d+/g) || []).length
1021
+ expect(commandCount).toBeLessThanOrEqual(5)
1022
+ })
1023
+ })
1024
+
1025
+ // ============================================================================
1026
+ // All Tiers Rendering Tests
1027
+ // ============================================================================
1028
+
1029
+ describe('renders across all tiers', () => {
1030
+ const sampleNode = createCommandPaletteNode({
1031
+ commands: [
1032
+ { id: 'new', label: 'New', shortcut: 'Ctrl+N' },
1033
+ { id: 'open', label: 'Open', shortcut: 'Ctrl+O' },
1034
+ ],
1035
+ selectedIndex: 0,
1036
+ })
1037
+
1038
+ RENDER_TIERS.forEach((tier) => {
1039
+ it(`renders on ${tier} tier`, async () => {
1040
+ const { renderCommandPalette } = await import('../../../renderers/command-palette')
1041
+
1042
+ const ctx = createContext(tier)
1043
+ const result = renderCommandPalette(sampleNode, ctx)
1044
+
1045
+ // Should produce output
1046
+ expect(result.length).toBeGreaterThan(0)
1047
+
1048
+ // Should contain command labels
1049
+ expect(result).toContain('New')
1050
+ expect(result).toContain('Open')
1051
+ })
1052
+ })
1053
+ })
1054
+
1055
+ // ============================================================================
1056
+ // Recent Commands Tests
1057
+ // ============================================================================
1058
+
1059
+ describe('recent commands', () => {
1060
+ it('shows recently used commands first', async () => {
1061
+ const { filterCommands } = await import('../../../renderers/command-palette')
1062
+
1063
+ const commands: Command[] = [
1064
+ { id: 'cmd-a', label: 'Alpha' },
1065
+ { id: 'cmd-b', label: 'Beta' },
1066
+ { id: 'cmd-c', label: 'Charlie' },
1067
+ ]
1068
+
1069
+ const recentIds = ['cmd-c', 'cmd-a']
1070
+
1071
+ const filtered = filterCommands(commands, '', { recentIds })
1072
+
1073
+ // Recent commands should be first
1074
+ expect(filtered[0].id).toBe('cmd-c')
1075
+ expect(filtered[1].id).toBe('cmd-a')
1076
+ })
1077
+
1078
+ it('recent commands are boosted in search results', async () => {
1079
+ const { filterCommands } = await import('../../../renderers/command-palette')
1080
+
1081
+ const commands: Command[] = [
1082
+ { id: 'file-new', label: 'New File' },
1083
+ { id: 'file-open', label: 'Open File' },
1084
+ { id: 'edit-file', label: 'Edit File' },
1085
+ ]
1086
+
1087
+ const recentIds = ['edit-file']
1088
+
1089
+ // All match "file", but recent should be boosted
1090
+ const filtered = filterCommands(commands, 'file', { recentIds })
1091
+
1092
+ expect(filtered[0].id).toBe('edit-file')
1093
+ })
1094
+ })
1095
+ })