@mdxui/terminal 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. package/README.md +571 -0
  2. package/dist/ansi-css-Sk5mWtdK.d.ts +119 -0
  3. package/dist/ansi-css-V6JIHGsM.d.ts +119 -0
  4. package/dist/ansi-css-_3eSEU9d.d.ts +119 -0
  5. package/dist/chunk-3EFDH7PK.js +5235 -0
  6. package/dist/chunk-3RG5ZIWI.js +10 -0
  7. package/dist/chunk-3X5IR6WE.js +884 -0
  8. package/dist/chunk-4FV5ZDCE.js +5236 -0
  9. package/dist/chunk-4OVMSF2J.js +243 -0
  10. package/dist/chunk-63FEETIS.js +4048 -0
  11. package/dist/chunk-B43KP7XJ.js +884 -0
  12. package/dist/chunk-BMTJXWUV.js +655 -0
  13. package/dist/chunk-C3SVH4N7.js +882 -0
  14. package/dist/chunk-EVWR7Y47.js +874 -0
  15. package/dist/chunk-F6A5VWUC.js +1285 -0
  16. package/dist/chunk-FD7KW7GE.js +882 -0
  17. package/dist/chunk-GBQ6UD6I.js +655 -0
  18. package/dist/chunk-GMDD3M6U.js +5227 -0
  19. package/dist/chunk-JBHRXOXM.js +1058 -0
  20. package/dist/chunk-JFOO3EYO.js +1182 -0
  21. package/dist/chunk-JQ5H3WXL.js +1291 -0
  22. package/dist/chunk-JQD5NASE.js +234 -0
  23. package/dist/chunk-KRHJP5R7.js +592 -0
  24. package/dist/chunk-KWF6WVJE.js +962 -0
  25. package/dist/chunk-LHYQVN3H.js +1038 -0
  26. package/dist/chunk-M3TLQLGC.js +1032 -0
  27. package/dist/chunk-MVW4Q5OP.js +240 -0
  28. package/dist/chunk-NXCZSWLU.js +1294 -0
  29. package/dist/chunk-O25TNRO6.js +607 -0
  30. package/dist/chunk-PNECDA2I.js +884 -0
  31. package/dist/chunk-QIHWRLJR.js +962 -0
  32. package/dist/chunk-QW5YMQ7K.js +882 -0
  33. package/dist/chunk-R5U7XKVJ.js +16 -0
  34. package/dist/chunk-RP2MVQLR.js +962 -0
  35. package/dist/chunk-TP6RXGXA.js +1087 -0
  36. package/dist/chunk-TQQSTITZ.js +655 -0
  37. package/dist/chunk-X24GWXQV.js +1281 -0
  38. package/dist/components/index.d.ts +802 -0
  39. package/dist/components/index.js +149 -0
  40. package/dist/data/index.d.ts +2554 -0
  41. package/dist/data/index.js +51 -0
  42. package/dist/forms/index.d.ts +1596 -0
  43. package/dist/forms/index.js +464 -0
  44. package/dist/index-CQRFZntR.d.ts +867 -0
  45. package/dist/index.d.ts +579 -0
  46. package/dist/index.js +786 -0
  47. package/dist/interactive-D0JkWosD.d.ts +217 -0
  48. package/dist/keyboard/index.d.ts +2 -0
  49. package/dist/keyboard/index.js +43 -0
  50. package/dist/renderers/index.d.ts +546 -0
  51. package/dist/renderers/index.js +2157 -0
  52. package/dist/storybook/index.d.ts +396 -0
  53. package/dist/storybook/index.js +641 -0
  54. package/dist/theme/index.d.ts +1339 -0
  55. package/dist/theme/index.js +123 -0
  56. package/dist/types-Bxu5PAgA.d.ts +710 -0
  57. package/dist/types-CIlop5Ji.d.ts +701 -0
  58. package/dist/types-Ca8p_p5X.d.ts +710 -0
  59. package/package.json +90 -0
  60. package/src/__tests__/components/data/card.test.ts +458 -0
  61. package/src/__tests__/components/data/list.test.ts +473 -0
  62. package/src/__tests__/components/data/metrics.test.ts +541 -0
  63. package/src/__tests__/components/data/table.test.ts +448 -0
  64. package/src/__tests__/components/input/field.test.ts +555 -0
  65. package/src/__tests__/components/input/form.test.ts +870 -0
  66. package/src/__tests__/components/input/search.test.ts +1238 -0
  67. package/src/__tests__/components/input/select.test.ts +658 -0
  68. package/src/__tests__/components/navigation/breadcrumb.test.ts +923 -0
  69. package/src/__tests__/components/navigation/command-palette.test.ts +1095 -0
  70. package/src/__tests__/components/navigation/sidebar.test.ts +1018 -0
  71. package/src/__tests__/components/navigation/tabs.test.ts +995 -0
  72. package/src/__tests__/components.test.tsx +1197 -0
  73. package/src/__tests__/core/compiler.test.ts +986 -0
  74. package/src/__tests__/core/parser.test.ts +785 -0
  75. package/src/__tests__/core/tier-switcher.test.ts +1103 -0
  76. package/src/__tests__/core/types.test.ts +1398 -0
  77. package/src/__tests__/data/collections.test.ts +1337 -0
  78. package/src/__tests__/data/db.test.ts +1265 -0
  79. package/src/__tests__/data/reactive.test.ts +1010 -0
  80. package/src/__tests__/data/sync.test.ts +1614 -0
  81. package/src/__tests__/errors.test.ts +660 -0
  82. package/src/__tests__/forms/integration.test.ts +444 -0
  83. package/src/__tests__/integration.test.ts +905 -0
  84. package/src/__tests__/keyboard.test.ts +1791 -0
  85. package/src/__tests__/renderer.test.ts +489 -0
  86. package/src/__tests__/renderers/ansi-css.test.ts +948 -0
  87. package/src/__tests__/renderers/ansi.test.ts +1366 -0
  88. package/src/__tests__/renderers/ascii.test.ts +1360 -0
  89. package/src/__tests__/renderers/interactive.test.ts +2353 -0
  90. package/src/__tests__/renderers/markdown.test.ts +1483 -0
  91. package/src/__tests__/renderers/text.test.ts +1369 -0
  92. package/src/__tests__/renderers/unicode.test.ts +1307 -0
  93. package/src/__tests__/theme.test.ts +639 -0
  94. package/src/__tests__/utils/assertions.ts +685 -0
  95. package/src/__tests__/utils/index.ts +115 -0
  96. package/src/__tests__/utils/test-renderer.ts +381 -0
  97. package/src/__tests__/utils/utils.test.ts +560 -0
  98. package/src/components/containers/card.ts +56 -0
  99. package/src/components/containers/dialog.ts +53 -0
  100. package/src/components/containers/index.ts +9 -0
  101. package/src/components/containers/panel.ts +59 -0
  102. package/src/components/feedback/badge.ts +40 -0
  103. package/src/components/feedback/index.ts +8 -0
  104. package/src/components/feedback/spinner.ts +23 -0
  105. package/src/components/helpers.ts +81 -0
  106. package/src/components/index.ts +153 -0
  107. package/src/components/layout/breadcrumb.ts +31 -0
  108. package/src/components/layout/index.ts +10 -0
  109. package/src/components/layout/list.ts +29 -0
  110. package/src/components/layout/sidebar.ts +79 -0
  111. package/src/components/layout/table.ts +62 -0
  112. package/src/components/primitives/box.ts +95 -0
  113. package/src/components/primitives/button.ts +54 -0
  114. package/src/components/primitives/index.ts +11 -0
  115. package/src/components/primitives/input.ts +88 -0
  116. package/src/components/primitives/select.ts +97 -0
  117. package/src/components/primitives/text.ts +60 -0
  118. package/src/components/render.ts +155 -0
  119. package/src/components/templates/app.ts +43 -0
  120. package/src/components/templates/index.ts +8 -0
  121. package/src/components/templates/site.ts +54 -0
  122. package/src/components/types.ts +777 -0
  123. package/src/core/compiler.ts +718 -0
  124. package/src/core/parser.ts +127 -0
  125. package/src/core/tier-switcher.ts +607 -0
  126. package/src/core/types.ts +672 -0
  127. package/src/data/collection.ts +316 -0
  128. package/src/data/collections.ts +50 -0
  129. package/src/data/context.tsx +174 -0
  130. package/src/data/db.ts +127 -0
  131. package/src/data/hooks.ts +532 -0
  132. package/src/data/index.ts +138 -0
  133. package/src/data/reactive.ts +1225 -0
  134. package/src/data/saas-collections.ts +375 -0
  135. package/src/data/sync.ts +1213 -0
  136. package/src/data/types.ts +660 -0
  137. package/src/forms/converters.ts +512 -0
  138. package/src/forms/index.ts +133 -0
  139. package/src/forms/schemas.ts +403 -0
  140. package/src/forms/types.ts +476 -0
  141. package/src/index.ts +542 -0
  142. package/src/keyboard/focus.ts +748 -0
  143. package/src/keyboard/index.ts +96 -0
  144. package/src/keyboard/integration.ts +371 -0
  145. package/src/keyboard/manager.ts +377 -0
  146. package/src/keyboard/presets.ts +90 -0
  147. package/src/renderers/ansi-css.ts +576 -0
  148. package/src/renderers/ansi.ts +802 -0
  149. package/src/renderers/ascii.ts +680 -0
  150. package/src/renderers/breadcrumb.ts +480 -0
  151. package/src/renderers/command-palette.ts +802 -0
  152. package/src/renderers/components/field.ts +210 -0
  153. package/src/renderers/components/form.ts +327 -0
  154. package/src/renderers/components/index.ts +21 -0
  155. package/src/renderers/components/search.ts +449 -0
  156. package/src/renderers/components/select.ts +222 -0
  157. package/src/renderers/index.ts +101 -0
  158. package/src/renderers/interactive/component-handlers.ts +622 -0
  159. package/src/renderers/interactive/cursor-manager.ts +147 -0
  160. package/src/renderers/interactive/focus-manager.ts +279 -0
  161. package/src/renderers/interactive/index.ts +661 -0
  162. package/src/renderers/interactive/input-handler.ts +164 -0
  163. package/src/renderers/interactive/keyboard-handler.ts +212 -0
  164. package/src/renderers/interactive/mouse-handler.ts +167 -0
  165. package/src/renderers/interactive/state-manager.ts +109 -0
  166. package/src/renderers/interactive/types.ts +338 -0
  167. package/src/renderers/interactive-string.ts +299 -0
  168. package/src/renderers/interactive.ts +59 -0
  169. package/src/renderers/markdown.ts +950 -0
  170. package/src/renderers/sidebar.ts +549 -0
  171. package/src/renderers/tabs.ts +682 -0
  172. package/src/renderers/text.ts +791 -0
  173. package/src/renderers/unicode.ts +917 -0
  174. package/src/renderers/utils.ts +942 -0
  175. package/src/router/adapters.ts +383 -0
  176. package/src/router/types.ts +140 -0
  177. package/src/router/utils.ts +452 -0
  178. package/src/schemas.ts +205 -0
  179. package/src/storybook/index.ts +91 -0
  180. package/src/storybook/interactive-decorator.tsx +659 -0
  181. package/src/storybook/keyboard-simulator.ts +501 -0
  182. package/src/theme/ansi-codes.ts +80 -0
  183. package/src/theme/box-drawing.ts +132 -0
  184. package/src/theme/color-convert.ts +254 -0
  185. package/src/theme/color-support.ts +321 -0
  186. package/src/theme/index.ts +134 -0
  187. package/src/theme/strip-ansi.ts +50 -0
  188. package/src/theme/tailwind-map.ts +469 -0
  189. package/src/theme/text-styles.ts +206 -0
  190. package/src/theme/theme-system.ts +568 -0
  191. package/src/types.ts +103 -0
@@ -0,0 +1,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
+ })