@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,1238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mdxui/terminal Search Component Tests (RED phase)
|
|
3
|
+
*
|
|
4
|
+
* TDD RED Phase: These tests define the contract for the Search component
|
|
5
|
+
* that provides search input with suggestions and filtering capabilities.
|
|
6
|
+
*
|
|
7
|
+
* Search component responsibilities:
|
|
8
|
+
* - Display search input field
|
|
9
|
+
* - Show search suggestions/autocomplete
|
|
10
|
+
* - Filter and display results
|
|
11
|
+
* - Support keyboard navigation of suggestions
|
|
12
|
+
* - Handle search submission
|
|
13
|
+
*
|
|
14
|
+
* Rendering across tiers:
|
|
15
|
+
* - TEXT: Plain input with results list
|
|
16
|
+
* - MARKDOWN: Formatted search results
|
|
17
|
+
* - ASCII: ASCII box around search and results
|
|
18
|
+
* - UNICODE: Unicode decorations for search
|
|
19
|
+
* - ANSI: Colors for matching text highlight
|
|
20
|
+
* - INTERACTIVE: Real-time suggestions, keyboard nav
|
|
21
|
+
*
|
|
22
|
+
* NOTE: These tests are expected to FAIL until implementation is complete.
|
|
23
|
+
* Run: pnpm --filter @mdxui/terminal test -- --run src/__tests__/components/input/search.test.ts
|
|
24
|
+
*/
|
|
25
|
+
import { describe, it, expect } from 'vitest'
|
|
26
|
+
|
|
27
|
+
import type { UINode, RenderTier, RenderContext, ThemeTokens } from '../../../core/types'
|
|
28
|
+
|
|
29
|
+
// ============================================================================
|
|
30
|
+
// Test Utilities
|
|
31
|
+
// ============================================================================
|
|
32
|
+
|
|
33
|
+
const RENDER_TIERS: RenderTier[] = ['text', 'markdown', 'ascii', 'unicode', 'ansi', 'interactive']
|
|
34
|
+
|
|
35
|
+
const mockTheme: ThemeTokens = {
|
|
36
|
+
primary: '\x1b[34m',
|
|
37
|
+
secondary: '\x1b[36m',
|
|
38
|
+
muted: '\x1b[90m',
|
|
39
|
+
foreground: '\x1b[37m',
|
|
40
|
+
background: '\x1b[40m',
|
|
41
|
+
border: '\x1b[90m',
|
|
42
|
+
success: '\x1b[32m',
|
|
43
|
+
warning: '\x1b[33m',
|
|
44
|
+
error: '\x1b[31m',
|
|
45
|
+
info: '\x1b[34m',
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function createContext(tier: RenderTier, overrides: Partial<RenderContext> = {}): RenderContext {
|
|
49
|
+
return {
|
|
50
|
+
tier,
|
|
51
|
+
width: 80,
|
|
52
|
+
height: 24,
|
|
53
|
+
depth: 0,
|
|
54
|
+
theme: mockTheme,
|
|
55
|
+
interactive: tier === 'interactive',
|
|
56
|
+
...overrides,
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function createNode(
|
|
61
|
+
type: string,
|
|
62
|
+
props: Record<string, unknown> = {},
|
|
63
|
+
children?: UINode[]
|
|
64
|
+
): UINode {
|
|
65
|
+
return { type, props, children }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function createSearchNode(props: Record<string, unknown>): UINode {
|
|
69
|
+
return createNode('search', props)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ============================================================================
|
|
73
|
+
// Basic Rendering Tests
|
|
74
|
+
// ============================================================================
|
|
75
|
+
|
|
76
|
+
describe('Search Component', () => {
|
|
77
|
+
describe('function signature', () => {
|
|
78
|
+
it('exports renderSearch function', async () => {
|
|
79
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
80
|
+
expect(typeof renderSearch).toBe('function')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('accepts UINode and RenderContext and returns string', async () => {
|
|
84
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
85
|
+
const node = createSearchNode({ value: '' })
|
|
86
|
+
const ctx = createContext('text')
|
|
87
|
+
const result = renderSearch(node, ctx)
|
|
88
|
+
expect(typeof result).toBe('string')
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
// ============================================================================
|
|
93
|
+
// Input Rendering Tests
|
|
94
|
+
// ============================================================================
|
|
95
|
+
|
|
96
|
+
describe('input rendering', () => {
|
|
97
|
+
RENDER_TIERS.forEach((tier) => {
|
|
98
|
+
describe(`[${tier}] tier`, () => {
|
|
99
|
+
it('renders search input field', async () => {
|
|
100
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
101
|
+
const node = createSearchNode({
|
|
102
|
+
value: '',
|
|
103
|
+
placeholder: 'Search...',
|
|
104
|
+
})
|
|
105
|
+
const ctx = createContext(tier)
|
|
106
|
+
const result = renderSearch(node, ctx)
|
|
107
|
+
|
|
108
|
+
expect(result).toContain('Search')
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('renders current search value', async () => {
|
|
112
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
113
|
+
const node = createSearchNode({
|
|
114
|
+
value: 'hello world',
|
|
115
|
+
})
|
|
116
|
+
const ctx = createContext(tier)
|
|
117
|
+
const result = renderSearch(node, ctx)
|
|
118
|
+
|
|
119
|
+
expect(result).toContain('hello world')
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('renders placeholder when empty', async () => {
|
|
123
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
124
|
+
const node = createSearchNode({
|
|
125
|
+
value: '',
|
|
126
|
+
placeholder: 'Type to search...',
|
|
127
|
+
})
|
|
128
|
+
const ctx = createContext(tier)
|
|
129
|
+
const result = renderSearch(node, ctx)
|
|
130
|
+
|
|
131
|
+
expect(result).toContain('Type to search')
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('renders search icon/indicator', async () => {
|
|
135
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
136
|
+
const node = createSearchNode({ value: '' })
|
|
137
|
+
const ctx = createContext(tier)
|
|
138
|
+
const result = renderSearch(node, ctx)
|
|
139
|
+
|
|
140
|
+
// Should have search icon or label
|
|
141
|
+
expect(result).toMatch(/[🔍🔎⌕]|search/i)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('renders with label', async () => {
|
|
145
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
146
|
+
const node = createSearchNode({
|
|
147
|
+
label: 'Search Users',
|
|
148
|
+
value: '',
|
|
149
|
+
})
|
|
150
|
+
const ctx = createContext(tier)
|
|
151
|
+
const result = renderSearch(node, ctx)
|
|
152
|
+
|
|
153
|
+
expect(result).toContain('Search Users')
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('renders disabled state', async () => {
|
|
157
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
158
|
+
const node = createSearchNode({
|
|
159
|
+
value: 'disabled',
|
|
160
|
+
disabled: true,
|
|
161
|
+
})
|
|
162
|
+
const ctx = createContext(tier)
|
|
163
|
+
const result = renderSearch(node, ctx)
|
|
164
|
+
|
|
165
|
+
expect(result).toContain('disabled')
|
|
166
|
+
})
|
|
167
|
+
})
|
|
168
|
+
})
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
// ============================================================================
|
|
172
|
+
// Suggestions Rendering Tests
|
|
173
|
+
// ============================================================================
|
|
174
|
+
|
|
175
|
+
describe('suggestions rendering', () => {
|
|
176
|
+
RENDER_TIERS.forEach((tier) => {
|
|
177
|
+
describe(`[${tier}] tier`, () => {
|
|
178
|
+
it('renders suggestions list', async () => {
|
|
179
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
180
|
+
const node = createSearchNode({
|
|
181
|
+
value: 'app',
|
|
182
|
+
suggestions: [
|
|
183
|
+
{ label: 'Apple', value: 'apple' },
|
|
184
|
+
{ label: 'Application', value: 'application' },
|
|
185
|
+
{ label: 'Appetizer', value: 'appetizer' },
|
|
186
|
+
],
|
|
187
|
+
showSuggestions: true,
|
|
188
|
+
})
|
|
189
|
+
const ctx = createContext(tier)
|
|
190
|
+
const result = renderSearch(node, ctx)
|
|
191
|
+
|
|
192
|
+
expect(result).toContain('Apple')
|
|
193
|
+
expect(result).toContain('Application')
|
|
194
|
+
expect(result).toContain('Appetizer')
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('preserves suggestion order', async () => {
|
|
198
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
199
|
+
const node = createSearchNode({
|
|
200
|
+
value: 'test',
|
|
201
|
+
suggestions: [
|
|
202
|
+
{ label: 'First', value: 'first' },
|
|
203
|
+
{ label: 'Second', value: 'second' },
|
|
204
|
+
{ label: 'Third', value: 'third' },
|
|
205
|
+
],
|
|
206
|
+
showSuggestions: true,
|
|
207
|
+
})
|
|
208
|
+
const ctx = createContext(tier)
|
|
209
|
+
const result = renderSearch(node, ctx)
|
|
210
|
+
|
|
211
|
+
const firstPos = result.indexOf('First')
|
|
212
|
+
const secondPos = result.indexOf('Second')
|
|
213
|
+
const thirdPos = result.indexOf('Third')
|
|
214
|
+
|
|
215
|
+
expect(firstPos).toBeLessThan(secondPos)
|
|
216
|
+
expect(secondPos).toBeLessThan(thirdPos)
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
it('hides suggestions when not focused', async () => {
|
|
220
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
221
|
+
const node = createSearchNode({
|
|
222
|
+
value: 'app',
|
|
223
|
+
suggestions: [{ label: 'Apple', value: 'apple' }],
|
|
224
|
+
showSuggestions: false,
|
|
225
|
+
})
|
|
226
|
+
const ctx = createContext(tier)
|
|
227
|
+
const result = renderSearch(node, ctx)
|
|
228
|
+
|
|
229
|
+
// Should not show suggestion
|
|
230
|
+
expect(result).not.toContain('Apple')
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
it('shows no suggestions message', async () => {
|
|
234
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
235
|
+
const node = createSearchNode({
|
|
236
|
+
value: 'xyz',
|
|
237
|
+
suggestions: [],
|
|
238
|
+
showSuggestions: true,
|
|
239
|
+
})
|
|
240
|
+
const ctx = createContext(tier)
|
|
241
|
+
const result = renderSearch(node, ctx)
|
|
242
|
+
|
|
243
|
+
expect(result).toMatch(/no.*results|no.*suggestions|not found/i)
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('limits visible suggestions', async () => {
|
|
247
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
248
|
+
const suggestions = Array.from({ length: 20 }, (_, i) => ({
|
|
249
|
+
label: `Suggestion ${i + 1}`,
|
|
250
|
+
value: `sug${i + 1}`,
|
|
251
|
+
}))
|
|
252
|
+
const node = createSearchNode({
|
|
253
|
+
value: 'test',
|
|
254
|
+
suggestions,
|
|
255
|
+
showSuggestions: true,
|
|
256
|
+
maxSuggestions: 5,
|
|
257
|
+
})
|
|
258
|
+
const ctx = createContext(tier)
|
|
259
|
+
const result = renderSearch(node, ctx)
|
|
260
|
+
|
|
261
|
+
// Should contain first suggestions
|
|
262
|
+
expect(result).toContain('Suggestion 1')
|
|
263
|
+
// May or may not contain 5th depending on limit display
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
it('renders suggestion with description', async () => {
|
|
267
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
268
|
+
const node = createSearchNode({
|
|
269
|
+
value: 'user',
|
|
270
|
+
suggestions: [
|
|
271
|
+
{ label: 'Users', value: 'users', description: 'Manage user accounts' },
|
|
272
|
+
],
|
|
273
|
+
showSuggestions: true,
|
|
274
|
+
})
|
|
275
|
+
const ctx = createContext(tier)
|
|
276
|
+
const result = renderSearch(node, ctx)
|
|
277
|
+
|
|
278
|
+
expect(result).toContain('Users')
|
|
279
|
+
expect(result).toContain('Manage user accounts')
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
it('renders suggestion with icon', async () => {
|
|
283
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
284
|
+
const node = createSearchNode({
|
|
285
|
+
value: 'set',
|
|
286
|
+
suggestions: [
|
|
287
|
+
{ label: 'Settings', value: 'settings', icon: 'gear' },
|
|
288
|
+
],
|
|
289
|
+
showSuggestions: true,
|
|
290
|
+
})
|
|
291
|
+
const ctx = createContext(tier)
|
|
292
|
+
const result = renderSearch(node, ctx)
|
|
293
|
+
|
|
294
|
+
expect(result).toContain('Settings')
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
it('renders grouped suggestions', async () => {
|
|
298
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
299
|
+
const node = createSearchNode({
|
|
300
|
+
value: 'a',
|
|
301
|
+
suggestionGroups: [
|
|
302
|
+
{
|
|
303
|
+
label: 'Recent',
|
|
304
|
+
suggestions: [
|
|
305
|
+
{ label: 'Apple', value: 'apple' },
|
|
306
|
+
],
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
label: 'Popular',
|
|
310
|
+
suggestions: [
|
|
311
|
+
{ label: 'Amazon', value: 'amazon' },
|
|
312
|
+
],
|
|
313
|
+
},
|
|
314
|
+
],
|
|
315
|
+
showSuggestions: true,
|
|
316
|
+
})
|
|
317
|
+
const ctx = createContext(tier)
|
|
318
|
+
const result = renderSearch(node, ctx)
|
|
319
|
+
|
|
320
|
+
expect(result).toContain('Recent')
|
|
321
|
+
expect(result).toContain('Apple')
|
|
322
|
+
expect(result).toContain('Popular')
|
|
323
|
+
expect(result).toContain('Amazon')
|
|
324
|
+
})
|
|
325
|
+
})
|
|
326
|
+
})
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
// ============================================================================
|
|
330
|
+
// Filtering Tests
|
|
331
|
+
// ============================================================================
|
|
332
|
+
|
|
333
|
+
describe('filtering', () => {
|
|
334
|
+
RENDER_TIERS.forEach((tier) => {
|
|
335
|
+
describe(`[${tier}] tier`, () => {
|
|
336
|
+
it('renders filtered results', async () => {
|
|
337
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
338
|
+
const node = createSearchNode({
|
|
339
|
+
value: 'apple',
|
|
340
|
+
results: [
|
|
341
|
+
{ id: '1', title: 'Apple iPhone', type: 'product' },
|
|
342
|
+
{ id: '2', title: 'Apple MacBook', type: 'product' },
|
|
343
|
+
],
|
|
344
|
+
showResults: true,
|
|
345
|
+
})
|
|
346
|
+
const ctx = createContext(tier)
|
|
347
|
+
const result = renderSearch(node, ctx)
|
|
348
|
+
|
|
349
|
+
expect(result).toContain('Apple iPhone')
|
|
350
|
+
expect(result).toContain('Apple MacBook')
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
it('shows result count', async () => {
|
|
354
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
355
|
+
const node = createSearchNode({
|
|
356
|
+
value: 'test',
|
|
357
|
+
results: [
|
|
358
|
+
{ id: '1', title: 'Result 1' },
|
|
359
|
+
{ id: '2', title: 'Result 2' },
|
|
360
|
+
{ id: '3', title: 'Result 3' },
|
|
361
|
+
],
|
|
362
|
+
showResults: true,
|
|
363
|
+
totalResults: 42,
|
|
364
|
+
})
|
|
365
|
+
const ctx = createContext(tier)
|
|
366
|
+
const result = renderSearch(node, ctx)
|
|
367
|
+
|
|
368
|
+
expect(result).toMatch(/42|results|found/i)
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
it('shows no results found message', async () => {
|
|
372
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
373
|
+
const node = createSearchNode({
|
|
374
|
+
value: 'nonexistent',
|
|
375
|
+
results: [],
|
|
376
|
+
showResults: true,
|
|
377
|
+
})
|
|
378
|
+
const ctx = createContext(tier)
|
|
379
|
+
const result = renderSearch(node, ctx)
|
|
380
|
+
|
|
381
|
+
expect(result).toMatch(/no.*results|not found|empty/i)
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
it('renders result with subtitle/description', async () => {
|
|
385
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
386
|
+
const node = createSearchNode({
|
|
387
|
+
value: 'user',
|
|
388
|
+
results: [
|
|
389
|
+
{
|
|
390
|
+
id: '1',
|
|
391
|
+
title: 'John Doe',
|
|
392
|
+
subtitle: 'john@example.com',
|
|
393
|
+
},
|
|
394
|
+
],
|
|
395
|
+
showResults: true,
|
|
396
|
+
})
|
|
397
|
+
const ctx = createContext(tier)
|
|
398
|
+
const result = renderSearch(node, ctx)
|
|
399
|
+
|
|
400
|
+
expect(result).toContain('John Doe')
|
|
401
|
+
expect(result).toContain('john@example.com')
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
it('renders result with type indicator', async () => {
|
|
405
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
406
|
+
const node = createSearchNode({
|
|
407
|
+
value: 'doc',
|
|
408
|
+
results: [
|
|
409
|
+
{ id: '1', title: 'Documentation', type: 'page' },
|
|
410
|
+
{ id: '2', title: 'Dockerfile', type: 'file' },
|
|
411
|
+
],
|
|
412
|
+
showResults: true,
|
|
413
|
+
})
|
|
414
|
+
const ctx = createContext(tier)
|
|
415
|
+
const result = renderSearch(node, ctx)
|
|
416
|
+
|
|
417
|
+
expect(result).toContain('Documentation')
|
|
418
|
+
expect(result).toContain('Dockerfile')
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
it('renders result categories/filters', async () => {
|
|
422
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
423
|
+
const node = createSearchNode({
|
|
424
|
+
value: 'test',
|
|
425
|
+
categories: [
|
|
426
|
+
{ label: 'All', count: 100 },
|
|
427
|
+
{ label: 'Pages', count: 50 },
|
|
428
|
+
{ label: 'Files', count: 30 },
|
|
429
|
+
{ label: 'Users', count: 20 },
|
|
430
|
+
],
|
|
431
|
+
activeCategory: 'All',
|
|
432
|
+
showResults: true,
|
|
433
|
+
})
|
|
434
|
+
const ctx = createContext(tier)
|
|
435
|
+
const result = renderSearch(node, ctx)
|
|
436
|
+
|
|
437
|
+
expect(result).toContain('All')
|
|
438
|
+
expect(result).toContain('Pages')
|
|
439
|
+
expect(result).toContain('Files')
|
|
440
|
+
})
|
|
441
|
+
})
|
|
442
|
+
})
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
// ============================================================================
|
|
446
|
+
// Highlight Matching Text Tests
|
|
447
|
+
// ============================================================================
|
|
448
|
+
|
|
449
|
+
describe('highlight matching text', () => {
|
|
450
|
+
describe('[ansi] tier', () => {
|
|
451
|
+
it('highlights matching text in suggestions', async () => {
|
|
452
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
453
|
+
const node = createSearchNode({
|
|
454
|
+
value: 'app',
|
|
455
|
+
suggestions: [
|
|
456
|
+
{ label: 'Application', value: 'application' },
|
|
457
|
+
],
|
|
458
|
+
showSuggestions: true,
|
|
459
|
+
highlightMatches: true,
|
|
460
|
+
})
|
|
461
|
+
const ctx = createContext('ansi')
|
|
462
|
+
const result = renderSearch(node, ctx)
|
|
463
|
+
|
|
464
|
+
// Should have bold or color for "app" portion
|
|
465
|
+
expect(result).toMatch(/\x1b\[(1|34)m.*app/i)
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
it('highlights matching text in results', async () => {
|
|
469
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
470
|
+
const node = createSearchNode({
|
|
471
|
+
value: 'test',
|
|
472
|
+
results: [
|
|
473
|
+
{ id: '1', title: 'Testing Guide' },
|
|
474
|
+
],
|
|
475
|
+
showResults: true,
|
|
476
|
+
highlightMatches: true,
|
|
477
|
+
})
|
|
478
|
+
const ctx = createContext('ansi')
|
|
479
|
+
const result = renderSearch(node, ctx)
|
|
480
|
+
|
|
481
|
+
// Should have highlight for "test" portion
|
|
482
|
+
expect(result).toMatch(/\x1b\[(1|34)m.*test/i)
|
|
483
|
+
})
|
|
484
|
+
})
|
|
485
|
+
|
|
486
|
+
describe('[markdown] tier', () => {
|
|
487
|
+
it('highlights matching text with markdown bold', async () => {
|
|
488
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
489
|
+
const node = createSearchNode({
|
|
490
|
+
value: 'app',
|
|
491
|
+
suggestions: [
|
|
492
|
+
{ label: 'Application', value: 'application' },
|
|
493
|
+
],
|
|
494
|
+
showSuggestions: true,
|
|
495
|
+
highlightMatches: true,
|
|
496
|
+
})
|
|
497
|
+
const ctx = createContext('markdown')
|
|
498
|
+
const result = renderSearch(node, ctx)
|
|
499
|
+
|
|
500
|
+
// Should have bold markdown for match
|
|
501
|
+
expect(result).toMatch(/\*\*app\*\*/i)
|
|
502
|
+
})
|
|
503
|
+
})
|
|
504
|
+
})
|
|
505
|
+
|
|
506
|
+
// ============================================================================
|
|
507
|
+
// Keyboard Navigation Tests (Interactive Tier)
|
|
508
|
+
// ============================================================================
|
|
509
|
+
|
|
510
|
+
describe('[interactive] tier keyboard navigation', () => {
|
|
511
|
+
it('shows cursor in input', async () => {
|
|
512
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
513
|
+
const node = createSearchNode({
|
|
514
|
+
value: 'hello',
|
|
515
|
+
focused: true,
|
|
516
|
+
cursorPosition: 5,
|
|
517
|
+
})
|
|
518
|
+
const ctx = createContext('interactive')
|
|
519
|
+
const result = renderSearch(node, ctx)
|
|
520
|
+
|
|
521
|
+
// Should show cursor indicator
|
|
522
|
+
expect(result).toMatch(/\x1b\[(7|4)m|[|_▌█]/)
|
|
523
|
+
})
|
|
524
|
+
|
|
525
|
+
it('shows highlighted suggestion', async () => {
|
|
526
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
527
|
+
const node = createSearchNode({
|
|
528
|
+
value: 'app',
|
|
529
|
+
suggestions: [
|
|
530
|
+
{ label: 'Apple', value: 'apple' },
|
|
531
|
+
{ label: 'Application', value: 'application' },
|
|
532
|
+
{ label: 'Appetizer', value: 'appetizer' },
|
|
533
|
+
],
|
|
534
|
+
showSuggestions: true,
|
|
535
|
+
highlightedIndex: 1,
|
|
536
|
+
})
|
|
537
|
+
const ctx = createContext('interactive')
|
|
538
|
+
const result = renderSearch(node, ctx)
|
|
539
|
+
|
|
540
|
+
// Application should be highlighted
|
|
541
|
+
expect(result).toContain('Application')
|
|
542
|
+
})
|
|
543
|
+
|
|
544
|
+
it('shows keyboard navigation hints', async () => {
|
|
545
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
546
|
+
const node = createSearchNode({
|
|
547
|
+
value: '',
|
|
548
|
+
showSuggestions: true,
|
|
549
|
+
suggestions: [{ label: 'Test', value: 'test' }],
|
|
550
|
+
})
|
|
551
|
+
const ctx = createContext('interactive')
|
|
552
|
+
const result = renderSearch(node, ctx)
|
|
553
|
+
|
|
554
|
+
expect(result).toMatch(/↑|↓|Enter|Esc|Tab|Arrow/i)
|
|
555
|
+
})
|
|
556
|
+
|
|
557
|
+
it('shows clear button hint', async () => {
|
|
558
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
559
|
+
const node = createSearchNode({
|
|
560
|
+
value: 'some text',
|
|
561
|
+
})
|
|
562
|
+
const ctx = createContext('interactive')
|
|
563
|
+
const result = renderSearch(node, ctx)
|
|
564
|
+
|
|
565
|
+
// Should show clear hint
|
|
566
|
+
expect(result).toMatch(/clear|×|Esc|Ctrl\+/i)
|
|
567
|
+
})
|
|
568
|
+
|
|
569
|
+
it('shows submit hint', async () => {
|
|
570
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
571
|
+
const node = createSearchNode({
|
|
572
|
+
value: 'search query',
|
|
573
|
+
})
|
|
574
|
+
const ctx = createContext('interactive')
|
|
575
|
+
const result = renderSearch(node, ctx)
|
|
576
|
+
|
|
577
|
+
// Should show submit/enter hint
|
|
578
|
+
expect(result).toMatch(/Enter|search|submit/i)
|
|
579
|
+
})
|
|
580
|
+
|
|
581
|
+
it('shows scroll indicator for long suggestions', async () => {
|
|
582
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
583
|
+
const suggestions = Array.from({ length: 50 }, (_, i) => ({
|
|
584
|
+
label: `Suggestion ${i + 1}`,
|
|
585
|
+
value: `sug${i + 1}`,
|
|
586
|
+
}))
|
|
587
|
+
const node = createSearchNode({
|
|
588
|
+
value: 'test',
|
|
589
|
+
suggestions,
|
|
590
|
+
showSuggestions: true,
|
|
591
|
+
maxVisibleSuggestions: 5,
|
|
592
|
+
})
|
|
593
|
+
const ctx = createContext('interactive', { height: 10 })
|
|
594
|
+
const result = renderSearch(node, ctx)
|
|
595
|
+
|
|
596
|
+
// Should show scroll indicator
|
|
597
|
+
expect(result).toMatch(/[▼▲↓↑…]|more|scroll/i)
|
|
598
|
+
})
|
|
599
|
+
|
|
600
|
+
it('shows text selection', async () => {
|
|
601
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
602
|
+
const node = createSearchNode({
|
|
603
|
+
value: 'hello world',
|
|
604
|
+
focused: true,
|
|
605
|
+
selectionStart: 0,
|
|
606
|
+
selectionEnd: 5,
|
|
607
|
+
})
|
|
608
|
+
const ctx = createContext('interactive')
|
|
609
|
+
const result = renderSearch(node, ctx)
|
|
610
|
+
|
|
611
|
+
// Selected text should be highlighted
|
|
612
|
+
expect(result).toMatch(/\x1b\[7m/)
|
|
613
|
+
})
|
|
614
|
+
})
|
|
615
|
+
|
|
616
|
+
// ============================================================================
|
|
617
|
+
// Loading State Tests
|
|
618
|
+
// ============================================================================
|
|
619
|
+
|
|
620
|
+
describe('loading state', () => {
|
|
621
|
+
RENDER_TIERS.forEach((tier) => {
|
|
622
|
+
describe(`[${tier}] tier`, () => {
|
|
623
|
+
it('shows loading indicator while searching', async () => {
|
|
624
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
625
|
+
const node = createSearchNode({
|
|
626
|
+
value: 'test',
|
|
627
|
+
loading: true,
|
|
628
|
+
})
|
|
629
|
+
const ctx = createContext(tier)
|
|
630
|
+
const result = renderSearch(node, ctx)
|
|
631
|
+
|
|
632
|
+
expect(result).toMatch(/loading|searching|\.{3}|⠋|spinner/i)
|
|
633
|
+
})
|
|
634
|
+
|
|
635
|
+
it('shows loading in suggestions area', async () => {
|
|
636
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
637
|
+
const node = createSearchNode({
|
|
638
|
+
value: 'app',
|
|
639
|
+
loading: true,
|
|
640
|
+
showSuggestions: true,
|
|
641
|
+
suggestions: [],
|
|
642
|
+
})
|
|
643
|
+
const ctx = createContext(tier)
|
|
644
|
+
const result = renderSearch(node, ctx)
|
|
645
|
+
|
|
646
|
+
expect(result).toMatch(/loading|searching|fetching/i)
|
|
647
|
+
})
|
|
648
|
+
})
|
|
649
|
+
})
|
|
650
|
+
})
|
|
651
|
+
|
|
652
|
+
// ============================================================================
|
|
653
|
+
// Border and Layout Tests
|
|
654
|
+
// ============================================================================
|
|
655
|
+
|
|
656
|
+
describe('border and layout', () => {
|
|
657
|
+
describe('[ascii] tier', () => {
|
|
658
|
+
it('renders ASCII border around search', async () => {
|
|
659
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
660
|
+
const node = createSearchNode({
|
|
661
|
+
value: '',
|
|
662
|
+
border: true,
|
|
663
|
+
})
|
|
664
|
+
const ctx = createContext('ascii')
|
|
665
|
+
const result = renderSearch(node, ctx)
|
|
666
|
+
|
|
667
|
+
expect(result).toMatch(/[+\-|]/)
|
|
668
|
+
})
|
|
669
|
+
|
|
670
|
+
it('renders ASCII suggestions box', async () => {
|
|
671
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
672
|
+
const node = createSearchNode({
|
|
673
|
+
value: 'test',
|
|
674
|
+
suggestions: [{ label: 'Test', value: 'test' }],
|
|
675
|
+
showSuggestions: true,
|
|
676
|
+
})
|
|
677
|
+
const ctx = createContext('ascii')
|
|
678
|
+
const result = renderSearch(node, ctx)
|
|
679
|
+
|
|
680
|
+
expect(result).toMatch(/[+\-|]/)
|
|
681
|
+
})
|
|
682
|
+
})
|
|
683
|
+
|
|
684
|
+
describe('[unicode] tier', () => {
|
|
685
|
+
it('renders Unicode border around search', async () => {
|
|
686
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
687
|
+
const node = createSearchNode({
|
|
688
|
+
value: '',
|
|
689
|
+
border: true,
|
|
690
|
+
})
|
|
691
|
+
const ctx = createContext('unicode')
|
|
692
|
+
const result = renderSearch(node, ctx)
|
|
693
|
+
|
|
694
|
+
expect(result).toMatch(/[┌┐└┘─│]/)
|
|
695
|
+
})
|
|
696
|
+
|
|
697
|
+
it('renders Unicode search icon', async () => {
|
|
698
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
699
|
+
const node = createSearchNode({
|
|
700
|
+
value: '',
|
|
701
|
+
})
|
|
702
|
+
const ctx = createContext('unicode')
|
|
703
|
+
const result = renderSearch(node, ctx)
|
|
704
|
+
|
|
705
|
+
expect(result).toMatch(/[🔍🔎⌕]/)
|
|
706
|
+
})
|
|
707
|
+
})
|
|
708
|
+
})
|
|
709
|
+
|
|
710
|
+
// ============================================================================
|
|
711
|
+
// Recent Searches Tests
|
|
712
|
+
// ============================================================================
|
|
713
|
+
|
|
714
|
+
describe('recent searches', () => {
|
|
715
|
+
RENDER_TIERS.forEach((tier) => {
|
|
716
|
+
describe(`[${tier}] tier`, () => {
|
|
717
|
+
it('renders recent searches', async () => {
|
|
718
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
719
|
+
const node = createSearchNode({
|
|
720
|
+
value: '',
|
|
721
|
+
recentSearches: [
|
|
722
|
+
'previous search 1',
|
|
723
|
+
'previous search 2',
|
|
724
|
+
'previous search 3',
|
|
725
|
+
],
|
|
726
|
+
showRecentSearches: true,
|
|
727
|
+
})
|
|
728
|
+
const ctx = createContext(tier)
|
|
729
|
+
const result = renderSearch(node, ctx)
|
|
730
|
+
|
|
731
|
+
expect(result).toContain('previous search 1')
|
|
732
|
+
expect(result).toContain('previous search 2')
|
|
733
|
+
})
|
|
734
|
+
|
|
735
|
+
it('shows recent header', async () => {
|
|
736
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
737
|
+
const node = createSearchNode({
|
|
738
|
+
value: '',
|
|
739
|
+
recentSearches: ['search 1'],
|
|
740
|
+
showRecentSearches: true,
|
|
741
|
+
})
|
|
742
|
+
const ctx = createContext(tier)
|
|
743
|
+
const result = renderSearch(node, ctx)
|
|
744
|
+
|
|
745
|
+
expect(result).toMatch(/recent|history|previous/i)
|
|
746
|
+
})
|
|
747
|
+
|
|
748
|
+
it('shows clear recent searches option', async () => {
|
|
749
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
750
|
+
const node = createSearchNode({
|
|
751
|
+
value: '',
|
|
752
|
+
recentSearches: ['search 1', 'search 2'],
|
|
753
|
+
showRecentSearches: true,
|
|
754
|
+
})
|
|
755
|
+
const ctx = createContext(tier)
|
|
756
|
+
const result = renderSearch(node, ctx)
|
|
757
|
+
|
|
758
|
+
expect(result).toMatch(/clear|remove/i)
|
|
759
|
+
})
|
|
760
|
+
})
|
|
761
|
+
})
|
|
762
|
+
})
|
|
763
|
+
|
|
764
|
+
// ============================================================================
|
|
765
|
+
// Error State Tests
|
|
766
|
+
// ============================================================================
|
|
767
|
+
|
|
768
|
+
describe('error state', () => {
|
|
769
|
+
RENDER_TIERS.forEach((tier) => {
|
|
770
|
+
describe(`[${tier}] tier`, () => {
|
|
771
|
+
it('renders error message', async () => {
|
|
772
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
773
|
+
const node = createSearchNode({
|
|
774
|
+
value: 'test',
|
|
775
|
+
error: 'Search failed. Please try again.',
|
|
776
|
+
})
|
|
777
|
+
const ctx = createContext(tier)
|
|
778
|
+
const result = renderSearch(node, ctx)
|
|
779
|
+
|
|
780
|
+
expect(result).toContain('Search failed')
|
|
781
|
+
})
|
|
782
|
+
|
|
783
|
+
it('shows error state visually', async () => {
|
|
784
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
785
|
+
const node = createSearchNode({
|
|
786
|
+
value: 'test',
|
|
787
|
+
error: 'Error',
|
|
788
|
+
})
|
|
789
|
+
const ctx = createContext(tier)
|
|
790
|
+
const result = renderSearch(node, ctx)
|
|
791
|
+
|
|
792
|
+
expect(result).toContain('Error')
|
|
793
|
+
})
|
|
794
|
+
})
|
|
795
|
+
})
|
|
796
|
+
|
|
797
|
+
describe('[ansi] tier error styling', () => {
|
|
798
|
+
it('renders error in red', async () => {
|
|
799
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
800
|
+
const node = createSearchNode({
|
|
801
|
+
value: 'test',
|
|
802
|
+
error: 'Error message',
|
|
803
|
+
})
|
|
804
|
+
const ctx = createContext('ansi')
|
|
805
|
+
const result = renderSearch(node, ctx)
|
|
806
|
+
|
|
807
|
+
expect(result).toContain('\x1b[31m')
|
|
808
|
+
})
|
|
809
|
+
})
|
|
810
|
+
})
|
|
811
|
+
|
|
812
|
+
// ============================================================================
|
|
813
|
+
// Quick Actions Tests
|
|
814
|
+
// ============================================================================
|
|
815
|
+
|
|
816
|
+
describe('quick actions', () => {
|
|
817
|
+
RENDER_TIERS.forEach((tier) => {
|
|
818
|
+
describe(`[${tier}] tier`, () => {
|
|
819
|
+
it('renders quick action suggestions', async () => {
|
|
820
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
821
|
+
const node = createSearchNode({
|
|
822
|
+
value: '>',
|
|
823
|
+
quickActions: [
|
|
824
|
+
{ label: 'Create New', value: 'create', shortcut: 'Ctrl+N' },
|
|
825
|
+
{ label: 'Settings', value: 'settings', shortcut: 'Ctrl+,' },
|
|
826
|
+
],
|
|
827
|
+
showQuickActions: true,
|
|
828
|
+
})
|
|
829
|
+
const ctx = createContext(tier)
|
|
830
|
+
const result = renderSearch(node, ctx)
|
|
831
|
+
|
|
832
|
+
expect(result).toContain('Create New')
|
|
833
|
+
expect(result).toContain('Settings')
|
|
834
|
+
})
|
|
835
|
+
|
|
836
|
+
it('shows keyboard shortcuts for actions', async () => {
|
|
837
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
838
|
+
const node = createSearchNode({
|
|
839
|
+
value: '>',
|
|
840
|
+
quickActions: [
|
|
841
|
+
{ label: 'Save', value: 'save', shortcut: 'Ctrl+S' },
|
|
842
|
+
],
|
|
843
|
+
showQuickActions: true,
|
|
844
|
+
})
|
|
845
|
+
const ctx = createContext(tier)
|
|
846
|
+
const result = renderSearch(node, ctx)
|
|
847
|
+
|
|
848
|
+
expect(result).toContain('Ctrl+S')
|
|
849
|
+
})
|
|
850
|
+
})
|
|
851
|
+
})
|
|
852
|
+
})
|
|
853
|
+
|
|
854
|
+
// ============================================================================
|
|
855
|
+
// Voice Search Tests
|
|
856
|
+
// ============================================================================
|
|
857
|
+
|
|
858
|
+
describe('voice search', () => {
|
|
859
|
+
describe('[interactive] tier', () => {
|
|
860
|
+
it('shows voice search button', async () => {
|
|
861
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
862
|
+
const node = createSearchNode({
|
|
863
|
+
value: '',
|
|
864
|
+
voiceEnabled: true,
|
|
865
|
+
})
|
|
866
|
+
const ctx = createContext('interactive')
|
|
867
|
+
const result = renderSearch(node, ctx)
|
|
868
|
+
|
|
869
|
+
expect(result).toMatch(/[🎤🎙]|voice|mic/i)
|
|
870
|
+
})
|
|
871
|
+
|
|
872
|
+
it('shows listening indicator', async () => {
|
|
873
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
874
|
+
const node = createSearchNode({
|
|
875
|
+
value: '',
|
|
876
|
+
voiceEnabled: true,
|
|
877
|
+
voiceListening: true,
|
|
878
|
+
})
|
|
879
|
+
const ctx = createContext('interactive')
|
|
880
|
+
const result = renderSearch(node, ctx)
|
|
881
|
+
|
|
882
|
+
expect(result).toMatch(/listening|recording|speak/i)
|
|
883
|
+
})
|
|
884
|
+
})
|
|
885
|
+
})
|
|
886
|
+
|
|
887
|
+
// ============================================================================
|
|
888
|
+
// Search Scope/Context Tests
|
|
889
|
+
// ============================================================================
|
|
890
|
+
|
|
891
|
+
describe('search scope', () => {
|
|
892
|
+
RENDER_TIERS.forEach((tier) => {
|
|
893
|
+
describe(`[${tier}] tier`, () => {
|
|
894
|
+
it('renders search scope indicator', async () => {
|
|
895
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
896
|
+
const node = createSearchNode({
|
|
897
|
+
value: '',
|
|
898
|
+
scope: 'current-folder',
|
|
899
|
+
scopeLabel: 'Current Folder',
|
|
900
|
+
})
|
|
901
|
+
const ctx = createContext(tier)
|
|
902
|
+
const result = renderSearch(node, ctx)
|
|
903
|
+
|
|
904
|
+
expect(result).toContain('Current Folder')
|
|
905
|
+
})
|
|
906
|
+
|
|
907
|
+
it('renders scope options', async () => {
|
|
908
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
909
|
+
const node = createSearchNode({
|
|
910
|
+
value: '',
|
|
911
|
+
scopes: [
|
|
912
|
+
{ label: 'Everywhere', value: 'all' },
|
|
913
|
+
{ label: 'Current Project', value: 'project' },
|
|
914
|
+
{ label: 'Open Files', value: 'open' },
|
|
915
|
+
],
|
|
916
|
+
showScopes: true,
|
|
917
|
+
})
|
|
918
|
+
const ctx = createContext(tier)
|
|
919
|
+
const result = renderSearch(node, ctx)
|
|
920
|
+
|
|
921
|
+
expect(result).toContain('Everywhere')
|
|
922
|
+
expect(result).toContain('Current Project')
|
|
923
|
+
})
|
|
924
|
+
})
|
|
925
|
+
})
|
|
926
|
+
})
|
|
927
|
+
|
|
928
|
+
// ============================================================================
|
|
929
|
+
// Inline vs Full Screen Modes
|
|
930
|
+
// ============================================================================
|
|
931
|
+
|
|
932
|
+
describe('display modes', () => {
|
|
933
|
+
RENDER_TIERS.forEach((tier) => {
|
|
934
|
+
describe(`[${tier}] tier`, () => {
|
|
935
|
+
it('renders inline mode', async () => {
|
|
936
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
937
|
+
const node = createSearchNode({
|
|
938
|
+
value: 'test',
|
|
939
|
+
mode: 'inline',
|
|
940
|
+
suggestions: [{ label: 'Test', value: 'test' }],
|
|
941
|
+
showSuggestions: true,
|
|
942
|
+
})
|
|
943
|
+
const ctx = createContext(tier)
|
|
944
|
+
const result = renderSearch(node, ctx)
|
|
945
|
+
|
|
946
|
+
expect(result).toContain('test')
|
|
947
|
+
})
|
|
948
|
+
|
|
949
|
+
it('renders expanded mode', async () => {
|
|
950
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
951
|
+
const node = createSearchNode({
|
|
952
|
+
value: 'test',
|
|
953
|
+
mode: 'expanded',
|
|
954
|
+
results: [
|
|
955
|
+
{ id: '1', title: 'Result 1' },
|
|
956
|
+
{ id: '2', title: 'Result 2' },
|
|
957
|
+
],
|
|
958
|
+
showResults: true,
|
|
959
|
+
})
|
|
960
|
+
const ctx = createContext(tier)
|
|
961
|
+
const result = renderSearch(node, ctx)
|
|
962
|
+
|
|
963
|
+
expect(result).toContain('Result 1')
|
|
964
|
+
expect(result).toContain('Result 2')
|
|
965
|
+
})
|
|
966
|
+
})
|
|
967
|
+
})
|
|
968
|
+
})
|
|
969
|
+
|
|
970
|
+
// ============================================================================
|
|
971
|
+
// Command Palette Mode Tests
|
|
972
|
+
// ============================================================================
|
|
973
|
+
|
|
974
|
+
describe('command palette mode', () => {
|
|
975
|
+
describe('[interactive] tier', () => {
|
|
976
|
+
it('renders command palette style', async () => {
|
|
977
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
978
|
+
const node = createSearchNode({
|
|
979
|
+
value: '',
|
|
980
|
+
mode: 'command-palette',
|
|
981
|
+
commands: [
|
|
982
|
+
{ label: 'Toggle Theme', value: 'theme', shortcut: 'Ctrl+T' },
|
|
983
|
+
{ label: 'Open File', value: 'open', shortcut: 'Ctrl+O' },
|
|
984
|
+
],
|
|
985
|
+
showCommands: true,
|
|
986
|
+
})
|
|
987
|
+
const ctx = createContext('interactive')
|
|
988
|
+
const result = renderSearch(node, ctx)
|
|
989
|
+
|
|
990
|
+
expect(result).toContain('Toggle Theme')
|
|
991
|
+
expect(result).toContain('Ctrl+T')
|
|
992
|
+
})
|
|
993
|
+
|
|
994
|
+
it('filters commands by input', async () => {
|
|
995
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
996
|
+
const node = createSearchNode({
|
|
997
|
+
value: 'open',
|
|
998
|
+
mode: 'command-palette',
|
|
999
|
+
commands: [
|
|
1000
|
+
{ label: 'Toggle Theme', value: 'theme' },
|
|
1001
|
+
{ label: 'Open File', value: 'open' },
|
|
1002
|
+
{ label: 'Open Recent', value: 'recent' },
|
|
1003
|
+
],
|
|
1004
|
+
filteredCommands: [
|
|
1005
|
+
{ label: 'Open File', value: 'open' },
|
|
1006
|
+
{ label: 'Open Recent', value: 'recent' },
|
|
1007
|
+
],
|
|
1008
|
+
showCommands: true,
|
|
1009
|
+
})
|
|
1010
|
+
const ctx = createContext('interactive')
|
|
1011
|
+
const result = renderSearch(node, ctx)
|
|
1012
|
+
|
|
1013
|
+
expect(result).toContain('Open File')
|
|
1014
|
+
expect(result).toContain('Open Recent')
|
|
1015
|
+
expect(result).not.toContain('Toggle Theme')
|
|
1016
|
+
})
|
|
1017
|
+
|
|
1018
|
+
it('shows command categories', async () => {
|
|
1019
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
1020
|
+
const node = createSearchNode({
|
|
1021
|
+
value: '',
|
|
1022
|
+
mode: 'command-palette',
|
|
1023
|
+
commandGroups: [
|
|
1024
|
+
{
|
|
1025
|
+
label: 'File',
|
|
1026
|
+
commands: [
|
|
1027
|
+
{ label: 'New File', value: 'new' },
|
|
1028
|
+
{ label: 'Save', value: 'save' },
|
|
1029
|
+
],
|
|
1030
|
+
},
|
|
1031
|
+
{
|
|
1032
|
+
label: 'Edit',
|
|
1033
|
+
commands: [
|
|
1034
|
+
{ label: 'Undo', value: 'undo' },
|
|
1035
|
+
{ label: 'Redo', value: 'redo' },
|
|
1036
|
+
],
|
|
1037
|
+
},
|
|
1038
|
+
],
|
|
1039
|
+
showCommands: true,
|
|
1040
|
+
})
|
|
1041
|
+
const ctx = createContext('interactive')
|
|
1042
|
+
const result = renderSearch(node, ctx)
|
|
1043
|
+
|
|
1044
|
+
expect(result).toContain('File')
|
|
1045
|
+
expect(result).toContain('Edit')
|
|
1046
|
+
})
|
|
1047
|
+
})
|
|
1048
|
+
})
|
|
1049
|
+
|
|
1050
|
+
// ============================================================================
|
|
1051
|
+
// Edge Cases
|
|
1052
|
+
// ============================================================================
|
|
1053
|
+
|
|
1054
|
+
describe('edge cases', () => {
|
|
1055
|
+
it('handles empty search value', async () => {
|
|
1056
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
1057
|
+
const node = createSearchNode({ value: '' })
|
|
1058
|
+
const ctx = createContext('text')
|
|
1059
|
+
const result = renderSearch(node, ctx)
|
|
1060
|
+
|
|
1061
|
+
expect(typeof result).toBe('string')
|
|
1062
|
+
})
|
|
1063
|
+
|
|
1064
|
+
it('handles null suggestions', async () => {
|
|
1065
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
1066
|
+
const node = createSearchNode({
|
|
1067
|
+
value: 'test',
|
|
1068
|
+
suggestions: null,
|
|
1069
|
+
})
|
|
1070
|
+
const ctx = createContext('text')
|
|
1071
|
+
const result = renderSearch(node, ctx)
|
|
1072
|
+
|
|
1073
|
+
expect(typeof result).toBe('string')
|
|
1074
|
+
})
|
|
1075
|
+
|
|
1076
|
+
it('handles very long search query', async () => {
|
|
1077
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
1078
|
+
const longQuery = 'A'.repeat(200)
|
|
1079
|
+
const node = createSearchNode({
|
|
1080
|
+
value: longQuery,
|
|
1081
|
+
})
|
|
1082
|
+
const ctx = createContext('text', { width: 40 })
|
|
1083
|
+
const result = renderSearch(node, ctx)
|
|
1084
|
+
|
|
1085
|
+
expect(typeof result).toBe('string')
|
|
1086
|
+
})
|
|
1087
|
+
|
|
1088
|
+
it('handles special characters in query', async () => {
|
|
1089
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
1090
|
+
const node = createSearchNode({
|
|
1091
|
+
value: '<script>alert("xss")</script>',
|
|
1092
|
+
})
|
|
1093
|
+
const ctx = createContext('text')
|
|
1094
|
+
const result = renderSearch(node, ctx)
|
|
1095
|
+
|
|
1096
|
+
expect(result).toContain('script')
|
|
1097
|
+
})
|
|
1098
|
+
|
|
1099
|
+
it('handles unicode in query', async () => {
|
|
1100
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
1101
|
+
const node = createSearchNode({
|
|
1102
|
+
value: '日本語検索',
|
|
1103
|
+
})
|
|
1104
|
+
const ctx = createContext('text')
|
|
1105
|
+
const result = renderSearch(node, ctx)
|
|
1106
|
+
|
|
1107
|
+
expect(result).toContain('日本語検索')
|
|
1108
|
+
})
|
|
1109
|
+
|
|
1110
|
+
it('handles emoji in query', async () => {
|
|
1111
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
1112
|
+
const node = createSearchNode({
|
|
1113
|
+
value: '🔍 search emoji',
|
|
1114
|
+
})
|
|
1115
|
+
const ctx = createContext('text')
|
|
1116
|
+
const result = renderSearch(node, ctx)
|
|
1117
|
+
|
|
1118
|
+
expect(result).toContain('search emoji')
|
|
1119
|
+
})
|
|
1120
|
+
|
|
1121
|
+
it('handles RTL text in query', async () => {
|
|
1122
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
1123
|
+
const node = createSearchNode({
|
|
1124
|
+
value: 'مرحبا',
|
|
1125
|
+
})
|
|
1126
|
+
const ctx = createContext('text')
|
|
1127
|
+
const result = renderSearch(node, ctx)
|
|
1128
|
+
|
|
1129
|
+
expect(result).toContain('مرحبا')
|
|
1130
|
+
})
|
|
1131
|
+
|
|
1132
|
+
it('handles suggestion with missing label', async () => {
|
|
1133
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
1134
|
+
const node = createSearchNode({
|
|
1135
|
+
value: 'test',
|
|
1136
|
+
suggestions: [
|
|
1137
|
+
{ label: '', value: 'empty' },
|
|
1138
|
+
{ label: 'Normal', value: 'normal' },
|
|
1139
|
+
],
|
|
1140
|
+
showSuggestions: true,
|
|
1141
|
+
})
|
|
1142
|
+
const ctx = createContext('text')
|
|
1143
|
+
const result = renderSearch(node, ctx)
|
|
1144
|
+
|
|
1145
|
+
expect(result).toContain('Normal')
|
|
1146
|
+
})
|
|
1147
|
+
|
|
1148
|
+
it('handles narrow terminal width', async () => {
|
|
1149
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
1150
|
+
const node = createSearchNode({
|
|
1151
|
+
value: 'test',
|
|
1152
|
+
suggestions: [
|
|
1153
|
+
{ label: 'A Long Suggestion Label', value: 'long' },
|
|
1154
|
+
],
|
|
1155
|
+
showSuggestions: true,
|
|
1156
|
+
})
|
|
1157
|
+
const ctx = createContext('text', { width: 20 })
|
|
1158
|
+
const result = renderSearch(node, ctx)
|
|
1159
|
+
|
|
1160
|
+
expect(result.length).toBeGreaterThan(0)
|
|
1161
|
+
})
|
|
1162
|
+
|
|
1163
|
+
it('handles result with very long title', async () => {
|
|
1164
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
1165
|
+
const longTitle = 'Result Title ' + 'A'.repeat(200)
|
|
1166
|
+
const node = createSearchNode({
|
|
1167
|
+
value: 'test',
|
|
1168
|
+
results: [{ id: '1', title: longTitle }],
|
|
1169
|
+
showResults: true,
|
|
1170
|
+
})
|
|
1171
|
+
const ctx = createContext('text', { width: 40 })
|
|
1172
|
+
const result = renderSearch(node, ctx)
|
|
1173
|
+
|
|
1174
|
+
expect(typeof result).toBe('string')
|
|
1175
|
+
})
|
|
1176
|
+
})
|
|
1177
|
+
|
|
1178
|
+
// ============================================================================
|
|
1179
|
+
// Accessibility Tests
|
|
1180
|
+
// ============================================================================
|
|
1181
|
+
|
|
1182
|
+
describe('accessibility', () => {
|
|
1183
|
+
describe('[interactive] tier', () => {
|
|
1184
|
+
it('includes ARIA-like label for search input', async () => {
|
|
1185
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
1186
|
+
const node = createSearchNode({
|
|
1187
|
+
value: '',
|
|
1188
|
+
ariaLabel: 'Search documentation',
|
|
1189
|
+
})
|
|
1190
|
+
const ctx = createContext('interactive')
|
|
1191
|
+
const result = renderSearch(node, ctx)
|
|
1192
|
+
|
|
1193
|
+
// Should contain accessible label
|
|
1194
|
+
expect(result).toMatch(/search|documentation/i)
|
|
1195
|
+
})
|
|
1196
|
+
|
|
1197
|
+
it('shows selection index for screen reader', async () => {
|
|
1198
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
1199
|
+
const node = createSearchNode({
|
|
1200
|
+
value: 'test',
|
|
1201
|
+
suggestions: [
|
|
1202
|
+
{ label: 'Option 1', value: '1' },
|
|
1203
|
+
{ label: 'Option 2', value: '2' },
|
|
1204
|
+
{ label: 'Option 3', value: '3' },
|
|
1205
|
+
],
|
|
1206
|
+
showSuggestions: true,
|
|
1207
|
+
highlightedIndex: 1,
|
|
1208
|
+
})
|
|
1209
|
+
const ctx = createContext('interactive')
|
|
1210
|
+
const result = renderSearch(node, ctx)
|
|
1211
|
+
|
|
1212
|
+
// Should indicate position
|
|
1213
|
+
expect(result).toMatch(/2.*of.*3|2\/3/i)
|
|
1214
|
+
})
|
|
1215
|
+
})
|
|
1216
|
+
})
|
|
1217
|
+
|
|
1218
|
+
// ============================================================================
|
|
1219
|
+
// Debounce Indicator Tests
|
|
1220
|
+
// ============================================================================
|
|
1221
|
+
|
|
1222
|
+
describe('debounce indicator', () => {
|
|
1223
|
+
describe('[interactive] tier', () => {
|
|
1224
|
+
it('shows typing indicator during debounce', async () => {
|
|
1225
|
+
const { renderSearch } = await import('../../../renderers/components/search')
|
|
1226
|
+
const node = createSearchNode({
|
|
1227
|
+
value: 'typ',
|
|
1228
|
+
isTyping: true,
|
|
1229
|
+
})
|
|
1230
|
+
const ctx = createContext('interactive')
|
|
1231
|
+
const result = renderSearch(node, ctx)
|
|
1232
|
+
|
|
1233
|
+
// Should show some typing indicator
|
|
1234
|
+
expect(result).toMatch(/typing|\.{3}|waiting/i)
|
|
1235
|
+
})
|
|
1236
|
+
})
|
|
1237
|
+
})
|
|
1238
|
+
})
|