@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.
- package/README.md +571 -0
- package/dist/ansi-css-Sk5mWtdK.d.ts +119 -0
- package/dist/ansi-css-V6JIHGsM.d.ts +119 -0
- package/dist/ansi-css-_3eSEU9d.d.ts +119 -0
- package/dist/chunk-3EFDH7PK.js +5235 -0
- package/dist/chunk-3RG5ZIWI.js +10 -0
- package/dist/chunk-3X5IR6WE.js +884 -0
- package/dist/chunk-4FV5ZDCE.js +5236 -0
- package/dist/chunk-4OVMSF2J.js +243 -0
- package/dist/chunk-63FEETIS.js +4048 -0
- package/dist/chunk-B43KP7XJ.js +884 -0
- package/dist/chunk-BMTJXWUV.js +655 -0
- package/dist/chunk-C3SVH4N7.js +882 -0
- package/dist/chunk-EVWR7Y47.js +874 -0
- package/dist/chunk-F6A5VWUC.js +1285 -0
- package/dist/chunk-FD7KW7GE.js +882 -0
- package/dist/chunk-GBQ6UD6I.js +655 -0
- package/dist/chunk-GMDD3M6U.js +5227 -0
- package/dist/chunk-JBHRXOXM.js +1058 -0
- package/dist/chunk-JFOO3EYO.js +1182 -0
- package/dist/chunk-JQ5H3WXL.js +1291 -0
- package/dist/chunk-JQD5NASE.js +234 -0
- package/dist/chunk-KRHJP5R7.js +592 -0
- package/dist/chunk-KWF6WVJE.js +962 -0
- package/dist/chunk-LHYQVN3H.js +1038 -0
- package/dist/chunk-M3TLQLGC.js +1032 -0
- package/dist/chunk-MVW4Q5OP.js +240 -0
- package/dist/chunk-NXCZSWLU.js +1294 -0
- package/dist/chunk-O25TNRO6.js +607 -0
- package/dist/chunk-PNECDA2I.js +884 -0
- package/dist/chunk-QIHWRLJR.js +962 -0
- package/dist/chunk-QW5YMQ7K.js +882 -0
- package/dist/chunk-R5U7XKVJ.js +16 -0
- package/dist/chunk-RP2MVQLR.js +962 -0
- package/dist/chunk-TP6RXGXA.js +1087 -0
- package/dist/chunk-TQQSTITZ.js +655 -0
- package/dist/chunk-X24GWXQV.js +1281 -0
- package/dist/components/index.d.ts +802 -0
- package/dist/components/index.js +149 -0
- package/dist/data/index.d.ts +2554 -0
- package/dist/data/index.js +51 -0
- package/dist/forms/index.d.ts +1596 -0
- package/dist/forms/index.js +464 -0
- package/dist/index-CQRFZntR.d.ts +867 -0
- package/dist/index.d.ts +579 -0
- package/dist/index.js +786 -0
- package/dist/interactive-D0JkWosD.d.ts +217 -0
- package/dist/keyboard/index.d.ts +2 -0
- package/dist/keyboard/index.js +43 -0
- package/dist/renderers/index.d.ts +546 -0
- package/dist/renderers/index.js +2157 -0
- package/dist/storybook/index.d.ts +396 -0
- package/dist/storybook/index.js +641 -0
- package/dist/theme/index.d.ts +1339 -0
- package/dist/theme/index.js +123 -0
- package/dist/types-Bxu5PAgA.d.ts +710 -0
- package/dist/types-CIlop5Ji.d.ts +701 -0
- package/dist/types-Ca8p_p5X.d.ts +710 -0
- package/package.json +90 -0
- package/src/__tests__/components/data/card.test.ts +458 -0
- package/src/__tests__/components/data/list.test.ts +473 -0
- package/src/__tests__/components/data/metrics.test.ts +541 -0
- package/src/__tests__/components/data/table.test.ts +448 -0
- package/src/__tests__/components/input/field.test.ts +555 -0
- package/src/__tests__/components/input/form.test.ts +870 -0
- package/src/__tests__/components/input/search.test.ts +1238 -0
- package/src/__tests__/components/input/select.test.ts +658 -0
- package/src/__tests__/components/navigation/breadcrumb.test.ts +923 -0
- package/src/__tests__/components/navigation/command-palette.test.ts +1095 -0
- package/src/__tests__/components/navigation/sidebar.test.ts +1018 -0
- package/src/__tests__/components/navigation/tabs.test.ts +995 -0
- package/src/__tests__/components.test.tsx +1197 -0
- package/src/__tests__/core/compiler.test.ts +986 -0
- package/src/__tests__/core/parser.test.ts +785 -0
- package/src/__tests__/core/tier-switcher.test.ts +1103 -0
- package/src/__tests__/core/types.test.ts +1398 -0
- package/src/__tests__/data/collections.test.ts +1337 -0
- package/src/__tests__/data/db.test.ts +1265 -0
- package/src/__tests__/data/reactive.test.ts +1010 -0
- package/src/__tests__/data/sync.test.ts +1614 -0
- package/src/__tests__/errors.test.ts +660 -0
- package/src/__tests__/forms/integration.test.ts +444 -0
- package/src/__tests__/integration.test.ts +905 -0
- package/src/__tests__/keyboard.test.ts +1791 -0
- package/src/__tests__/renderer.test.ts +489 -0
- package/src/__tests__/renderers/ansi-css.test.ts +948 -0
- package/src/__tests__/renderers/ansi.test.ts +1366 -0
- package/src/__tests__/renderers/ascii.test.ts +1360 -0
- package/src/__tests__/renderers/interactive.test.ts +2353 -0
- package/src/__tests__/renderers/markdown.test.ts +1483 -0
- package/src/__tests__/renderers/text.test.ts +1369 -0
- package/src/__tests__/renderers/unicode.test.ts +1307 -0
- package/src/__tests__/theme.test.ts +639 -0
- package/src/__tests__/utils/assertions.ts +685 -0
- package/src/__tests__/utils/index.ts +115 -0
- package/src/__tests__/utils/test-renderer.ts +381 -0
- package/src/__tests__/utils/utils.test.ts +560 -0
- package/src/components/containers/card.ts +56 -0
- package/src/components/containers/dialog.ts +53 -0
- package/src/components/containers/index.ts +9 -0
- package/src/components/containers/panel.ts +59 -0
- package/src/components/feedback/badge.ts +40 -0
- package/src/components/feedback/index.ts +8 -0
- package/src/components/feedback/spinner.ts +23 -0
- package/src/components/helpers.ts +81 -0
- package/src/components/index.ts +153 -0
- package/src/components/layout/breadcrumb.ts +31 -0
- package/src/components/layout/index.ts +10 -0
- package/src/components/layout/list.ts +29 -0
- package/src/components/layout/sidebar.ts +79 -0
- package/src/components/layout/table.ts +62 -0
- package/src/components/primitives/box.ts +95 -0
- package/src/components/primitives/button.ts +54 -0
- package/src/components/primitives/index.ts +11 -0
- package/src/components/primitives/input.ts +88 -0
- package/src/components/primitives/select.ts +97 -0
- package/src/components/primitives/text.ts +60 -0
- package/src/components/render.ts +155 -0
- package/src/components/templates/app.ts +43 -0
- package/src/components/templates/index.ts +8 -0
- package/src/components/templates/site.ts +54 -0
- package/src/components/types.ts +777 -0
- package/src/core/compiler.ts +718 -0
- package/src/core/parser.ts +127 -0
- package/src/core/tier-switcher.ts +607 -0
- package/src/core/types.ts +672 -0
- package/src/data/collection.ts +316 -0
- package/src/data/collections.ts +50 -0
- package/src/data/context.tsx +174 -0
- package/src/data/db.ts +127 -0
- package/src/data/hooks.ts +532 -0
- package/src/data/index.ts +138 -0
- package/src/data/reactive.ts +1225 -0
- package/src/data/saas-collections.ts +375 -0
- package/src/data/sync.ts +1213 -0
- package/src/data/types.ts +660 -0
- package/src/forms/converters.ts +512 -0
- package/src/forms/index.ts +133 -0
- package/src/forms/schemas.ts +403 -0
- package/src/forms/types.ts +476 -0
- package/src/index.ts +542 -0
- package/src/keyboard/focus.ts +748 -0
- package/src/keyboard/index.ts +96 -0
- package/src/keyboard/integration.ts +371 -0
- package/src/keyboard/manager.ts +377 -0
- package/src/keyboard/presets.ts +90 -0
- package/src/renderers/ansi-css.ts +576 -0
- package/src/renderers/ansi.ts +802 -0
- package/src/renderers/ascii.ts +680 -0
- package/src/renderers/breadcrumb.ts +480 -0
- package/src/renderers/command-palette.ts +802 -0
- package/src/renderers/components/field.ts +210 -0
- package/src/renderers/components/form.ts +327 -0
- package/src/renderers/components/index.ts +21 -0
- package/src/renderers/components/search.ts +449 -0
- package/src/renderers/components/select.ts +222 -0
- package/src/renderers/index.ts +101 -0
- package/src/renderers/interactive/component-handlers.ts +622 -0
- package/src/renderers/interactive/cursor-manager.ts +147 -0
- package/src/renderers/interactive/focus-manager.ts +279 -0
- package/src/renderers/interactive/index.ts +661 -0
- package/src/renderers/interactive/input-handler.ts +164 -0
- package/src/renderers/interactive/keyboard-handler.ts +212 -0
- package/src/renderers/interactive/mouse-handler.ts +167 -0
- package/src/renderers/interactive/state-manager.ts +109 -0
- package/src/renderers/interactive/types.ts +338 -0
- package/src/renderers/interactive-string.ts +299 -0
- package/src/renderers/interactive.ts +59 -0
- package/src/renderers/markdown.ts +950 -0
- package/src/renderers/sidebar.ts +549 -0
- package/src/renderers/tabs.ts +682 -0
- package/src/renderers/text.ts +791 -0
- package/src/renderers/unicode.ts +917 -0
- package/src/renderers/utils.ts +942 -0
- package/src/router/adapters.ts +383 -0
- package/src/router/types.ts +140 -0
- package/src/router/utils.ts +452 -0
- package/src/schemas.ts +205 -0
- package/src/storybook/index.ts +91 -0
- package/src/storybook/interactive-decorator.tsx +659 -0
- package/src/storybook/keyboard-simulator.ts +501 -0
- package/src/theme/ansi-codes.ts +80 -0
- package/src/theme/box-drawing.ts +132 -0
- package/src/theme/color-convert.ts +254 -0
- package/src/theme/color-support.ts +321 -0
- package/src/theme/index.ts +134 -0
- package/src/theme/strip-ansi.ts +50 -0
- package/src/theme/tailwind-map.ts +469 -0
- package/src/theme/text-styles.ts +206 -0
- package/src/theme/theme-system.ts +568 -0
- 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
|
+
})
|