@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,658 @@
1
+ /**
2
+ * @mdxui/terminal Select Component Tests (RED phase)
3
+ *
4
+ * TDD RED Phase: These tests define the contract for the Select component
5
+ * that renders dropdown/list selection with options and keyboard navigation.
6
+ *
7
+ * Rendering across tiers:
8
+ * - TEXT: Plain list with selected indicator
9
+ * - MARKDOWN: Markdown list with selection marker
10
+ * - ASCII: ASCII box with scrollable list
11
+ * - UNICODE: Unicode box with scroll indicators
12
+ * - ANSI: Colors for selection and hover states
13
+ * - INTERACTIVE: Full keyboard navigation, type-to-search
14
+ *
15
+ * NOTE: These tests are expected to FAIL until implementation is complete.
16
+ */
17
+ import { describe, it, expect } from 'vitest'
18
+ import type { UINode, RenderTier, RenderContext, ThemeTokens } from '../../../core/types'
19
+
20
+ // ============================================================================
21
+ // Test Utilities
22
+ // ============================================================================
23
+
24
+ const RENDER_TIERS: RenderTier[] = ['text', 'markdown', 'ascii', 'unicode', 'ansi', 'interactive']
25
+
26
+ const mockTheme: ThemeTokens = {
27
+ primary: '\x1b[34m',
28
+ secondary: '\x1b[36m',
29
+ muted: '\x1b[90m',
30
+ foreground: '\x1b[37m',
31
+ background: '\x1b[40m',
32
+ border: '\x1b[90m',
33
+ success: '\x1b[32m',
34
+ warning: '\x1b[33m',
35
+ error: '\x1b[31m',
36
+ info: '\x1b[34m',
37
+ }
38
+
39
+ function createContext(tier: RenderTier, overrides: Partial<RenderContext> = {}): RenderContext {
40
+ return {
41
+ tier,
42
+ width: 80,
43
+ height: 24,
44
+ depth: 0,
45
+ theme: mockTheme,
46
+ interactive: tier === 'interactive',
47
+ ...overrides,
48
+ }
49
+ }
50
+
51
+ function createNode(
52
+ type: string,
53
+ props: Record<string, unknown> = {},
54
+ children?: UINode[]
55
+ ): UINode {
56
+ return { type, props, children }
57
+ }
58
+
59
+ function createSelectNode(props: Record<string, unknown>): UINode {
60
+ return createNode('select', props)
61
+ }
62
+
63
+ // ============================================================================
64
+ // Basic Rendering Tests
65
+ // ============================================================================
66
+
67
+ describe('Select Component', () => {
68
+ describe('function signature', () => {
69
+ it('exports renderSelect function', async () => {
70
+ const { renderSelect } = await import('../../../renderers/components/select')
71
+ expect(typeof renderSelect).toBe('function')
72
+ })
73
+
74
+ it('accepts UINode and RenderContext and returns string', async () => {
75
+ const { renderSelect } = await import('../../../renderers/components/select')
76
+ const node = createSelectNode({
77
+ options: [{ label: 'Option 1', value: 'opt1' }],
78
+ })
79
+ const ctx = createContext('text')
80
+ const result = renderSelect(node, ctx)
81
+ expect(typeof result).toBe('string')
82
+ })
83
+ })
84
+
85
+ // ============================================================================
86
+ // Options Rendering Tests
87
+ // ============================================================================
88
+
89
+ describe('options rendering', () => {
90
+ RENDER_TIERS.forEach((tier) => {
91
+ describe(`[${tier}] tier`, () => {
92
+ it('renders single option', async () => {
93
+ const { renderSelect } = await import('../../../renderers/components/select')
94
+ const node = createSelectNode({
95
+ options: [{ label: 'First Option', value: 'first' }],
96
+ })
97
+ const ctx = createContext(tier)
98
+ const result = renderSelect(node, ctx)
99
+
100
+ expect(result).toContain('First Option')
101
+ })
102
+
103
+ it('renders multiple options', async () => {
104
+ const { renderSelect } = await import('../../../renderers/components/select')
105
+ const node = createSelectNode({
106
+ options: [
107
+ { label: 'Option A', value: 'a' },
108
+ { label: 'Option B', value: 'b' },
109
+ { label: 'Option C', value: 'c' },
110
+ ],
111
+ })
112
+ const ctx = createContext(tier)
113
+ const result = renderSelect(node, ctx)
114
+
115
+ expect(result).toContain('Option A')
116
+ expect(result).toContain('Option B')
117
+ expect(result).toContain('Option C')
118
+ })
119
+
120
+ it('preserves option order', async () => {
121
+ const { renderSelect } = await import('../../../renderers/components/select')
122
+ const node = createSelectNode({
123
+ options: [
124
+ { label: 'First', value: '1' },
125
+ { label: 'Second', value: '2' },
126
+ { label: 'Third', value: '3' },
127
+ ],
128
+ })
129
+ const ctx = createContext(tier)
130
+ const result = renderSelect(node, ctx)
131
+
132
+ const firstPos = result.indexOf('First')
133
+ const secondPos = result.indexOf('Second')
134
+ const thirdPos = result.indexOf('Third')
135
+
136
+ expect(firstPos).toBeLessThan(secondPos)
137
+ expect(secondPos).toBeLessThan(thirdPos)
138
+ })
139
+
140
+ it('renders empty select gracefully', async () => {
141
+ const { renderSelect } = await import('../../../renderers/components/select')
142
+ const node = createSelectNode({ options: [] })
143
+ const ctx = createContext(tier)
144
+ const result = renderSelect(node, ctx)
145
+
146
+ expect(typeof result).toBe('string')
147
+ })
148
+
149
+ it('renders select with label', async () => {
150
+ const { renderSelect } = await import('../../../renderers/components/select')
151
+ const node = createSelectNode({
152
+ label: 'Choose Country',
153
+ options: [{ label: 'USA', value: 'us' }],
154
+ })
155
+ const ctx = createContext(tier)
156
+ const result = renderSelect(node, ctx)
157
+
158
+ expect(result).toContain('Choose Country')
159
+ })
160
+
161
+ it('renders placeholder when no selection', async () => {
162
+ const { renderSelect } = await import('../../../renderers/components/select')
163
+ const node = createSelectNode({
164
+ placeholder: 'Select an option',
165
+ options: [{ label: 'Option 1', value: '1' }],
166
+ })
167
+ const ctx = createContext(tier)
168
+ const result = renderSelect(node, ctx)
169
+
170
+ expect(result).toContain('Select an option')
171
+ })
172
+ })
173
+ })
174
+ })
175
+
176
+ // ============================================================================
177
+ // Selection Indicator Tests
178
+ // ============================================================================
179
+
180
+ describe('selection indicator', () => {
181
+ RENDER_TIERS.forEach((tier) => {
182
+ describe(`[${tier}] tier`, () => {
183
+ it('shows selected option', async () => {
184
+ const { renderSelect } = await import('../../../renderers/components/select')
185
+ const node = createSelectNode({
186
+ value: 'b',
187
+ options: [
188
+ { label: 'Option A', value: 'a' },
189
+ { label: 'Option B', value: 'b' },
190
+ { label: 'Option C', value: 'c' },
191
+ ],
192
+ })
193
+ const ctx = createContext(tier)
194
+ const result = renderSelect(node, ctx)
195
+
196
+ expect(result).toContain('Option B')
197
+ })
198
+
199
+ it('distinguishes selected from unselected', async () => {
200
+ const { renderSelect } = await import('../../../renderers/components/select')
201
+ const node = createSelectNode({
202
+ value: 'selected',
203
+ options: [
204
+ { label: 'Selected Item', value: 'selected' },
205
+ { label: 'Other Item', value: 'other' },
206
+ ],
207
+ })
208
+ const ctx = createContext(tier)
209
+ const result = renderSelect(node, ctx)
210
+
211
+ expect(result).toMatch(/[✓✔●◉>*\[\*\]]/i)
212
+ })
213
+ })
214
+ })
215
+
216
+ describe('[ansi] tier selection styling', () => {
217
+ it('highlights selected option', async () => {
218
+ const { renderSelect } = await import('../../../renderers/components/select')
219
+ const node = createSelectNode({
220
+ value: 'selected',
221
+ options: [
222
+ { label: 'Selected', value: 'selected' },
223
+ { label: 'Other', value: 'other' },
224
+ ],
225
+ })
226
+ const ctx = createContext('ansi')
227
+ const result = renderSelect(node, ctx)
228
+
229
+ expect(result).toMatch(/\x1b\[(34|32)m/)
230
+ })
231
+ })
232
+ })
233
+
234
+ // ============================================================================
235
+ // Dropdown State Tests
236
+ // ============================================================================
237
+
238
+ describe('dropdown state', () => {
239
+ RENDER_TIERS.forEach((tier) => {
240
+ describe(`[${tier}] tier`, () => {
241
+ it('renders collapsed state showing only selected', async () => {
242
+ const { renderSelect } = await import('../../../renderers/components/select')
243
+ const node = createSelectNode({
244
+ open: false,
245
+ value: 'selected',
246
+ options: [
247
+ { label: 'Selected Option', value: 'selected' },
248
+ { label: 'Other Option', value: 'other' },
249
+ ],
250
+ })
251
+ const ctx = createContext(tier)
252
+ const result = renderSelect(node, ctx)
253
+
254
+ expect(result).toContain('Selected Option')
255
+ })
256
+
257
+ it('renders expanded state showing all options', async () => {
258
+ const { renderSelect } = await import('../../../renderers/components/select')
259
+ const node = createSelectNode({
260
+ open: true,
261
+ value: 'b',
262
+ options: [
263
+ { label: 'Option A', value: 'a' },
264
+ { label: 'Option B', value: 'b' },
265
+ { label: 'Option C', value: 'c' },
266
+ ],
267
+ })
268
+ const ctx = createContext(tier)
269
+ const result = renderSelect(node, ctx)
270
+
271
+ expect(result).toContain('Option A')
272
+ expect(result).toContain('Option B')
273
+ expect(result).toContain('Option C')
274
+ })
275
+
276
+ it('shows dropdown indicator', async () => {
277
+ const { renderSelect } = await import('../../../renderers/components/select')
278
+ const node = createSelectNode({
279
+ open: false,
280
+ options: [{ label: 'Option', value: 'opt' }],
281
+ })
282
+ const ctx = createContext(tier)
283
+ const result = renderSelect(node, ctx)
284
+
285
+ expect(result).toMatch(/[▼▾↓v>]/i)
286
+ })
287
+ })
288
+ })
289
+ })
290
+
291
+ // ============================================================================
292
+ // Disabled Options Tests
293
+ // ============================================================================
294
+
295
+ describe('disabled options', () => {
296
+ RENDER_TIERS.forEach((tier) => {
297
+ describe(`[${tier}] tier`, () => {
298
+ it('renders disabled option', async () => {
299
+ const { renderSelect } = await import('../../../renderers/components/select')
300
+ const node = createSelectNode({
301
+ options: [
302
+ { label: 'Available', value: 'available' },
303
+ { label: 'Unavailable', value: 'unavailable', disabled: true },
304
+ ],
305
+ })
306
+ const ctx = createContext(tier)
307
+ const result = renderSelect(node, ctx)
308
+
309
+ expect(result).toContain('Available')
310
+ expect(result).toContain('Unavailable')
311
+ })
312
+ })
313
+ })
314
+
315
+ describe('[ansi] tier disabled styling', () => {
316
+ it('renders disabled option as dimmed', async () => {
317
+ const { renderSelect } = await import('../../../renderers/components/select')
318
+ const node = createSelectNode({
319
+ options: [
320
+ { label: 'Disabled', value: 'disabled', disabled: true },
321
+ ],
322
+ })
323
+ const ctx = createContext('ansi')
324
+ const result = renderSelect(node, ctx)
325
+
326
+ expect(result).toContain('\x1b[2m')
327
+ })
328
+ })
329
+ })
330
+
331
+ // ============================================================================
332
+ // Keyboard Navigation Tests (Interactive Tier)
333
+ // ============================================================================
334
+
335
+ describe('[interactive] tier keyboard navigation', () => {
336
+ it('shows currently highlighted option', async () => {
337
+ const { renderSelect } = await import('../../../renderers/components/select')
338
+ const node = createSelectNode({
339
+ open: true,
340
+ highlightedIndex: 1,
341
+ options: [
342
+ { label: 'Option A', value: 'a' },
343
+ { label: 'Option B', value: 'b' },
344
+ { label: 'Option C', value: 'c' },
345
+ ],
346
+ })
347
+ const ctx = createContext('interactive')
348
+ const result = renderSelect(node, ctx)
349
+
350
+ expect(result).toContain('Option B')
351
+ })
352
+
353
+ it('shows keyboard navigation hints', async () => {
354
+ const { renderSelect } = await import('../../../renderers/components/select')
355
+ const node = createSelectNode({
356
+ open: true,
357
+ options: [{ label: 'Option', value: 'opt' }],
358
+ })
359
+ const ctx = createContext('interactive')
360
+ const result = renderSelect(node, ctx)
361
+
362
+ expect(result).toMatch(/↑|↓|Enter|Esc|Arrow/i)
363
+ })
364
+
365
+ it('renders search input when searchable', async () => {
366
+ const { renderSelect } = await import('../../../renderers/components/select')
367
+ const node = createSelectNode({
368
+ open: true,
369
+ searchable: true,
370
+ searchValue: 'app',
371
+ options: [
372
+ { label: 'Apple', value: 'apple' },
373
+ { label: 'Banana', value: 'banana' },
374
+ ],
375
+ })
376
+ const ctx = createContext('interactive')
377
+ const result = renderSelect(node, ctx)
378
+
379
+ expect(result).toContain('app')
380
+ })
381
+
382
+ it('shows no results message when filter has no matches', async () => {
383
+ const { renderSelect } = await import('../../../renderers/components/select')
384
+ const node = createSelectNode({
385
+ open: true,
386
+ searchable: true,
387
+ searchValue: 'xyz',
388
+ filteredOptions: [],
389
+ options: [
390
+ { label: 'Apple', value: 'apple' },
391
+ ],
392
+ })
393
+ const ctx = createContext('interactive')
394
+ const result = renderSelect(node, ctx)
395
+
396
+ expect(result).toMatch(/no.*results|no.*matches|not found/i)
397
+ })
398
+ })
399
+
400
+ // ============================================================================
401
+ // ASCII/Unicode Border Tests
402
+ // ============================================================================
403
+
404
+ describe('border rendering', () => {
405
+ describe('[ascii] tier', () => {
406
+ it('renders ASCII box when open', async () => {
407
+ const { renderSelect } = await import('../../../renderers/components/select')
408
+ const node = createSelectNode({
409
+ open: true,
410
+ options: [
411
+ { label: 'Option 1', value: '1' },
412
+ { label: 'Option 2', value: '2' },
413
+ ],
414
+ })
415
+ const ctx = createContext('ascii')
416
+ const result = renderSelect(node, ctx)
417
+
418
+ expect(result).toMatch(/[+\-|]/)
419
+ })
420
+ })
421
+
422
+ describe('[unicode] tier', () => {
423
+ it('renders Unicode box when open', async () => {
424
+ const { renderSelect } = await import('../../../renderers/components/select')
425
+ const node = createSelectNode({
426
+ open: true,
427
+ options: [
428
+ { label: 'Option 1', value: '1' },
429
+ { label: 'Option 2', value: '2' },
430
+ ],
431
+ })
432
+ const ctx = createContext('unicode')
433
+ const result = renderSelect(node, ctx)
434
+
435
+ expect(result).toMatch(/[┌┐└┘─│]/)
436
+ })
437
+ })
438
+ })
439
+
440
+ // ============================================================================
441
+ // Multi-Select Tests
442
+ // ============================================================================
443
+
444
+ describe('multi-select mode', () => {
445
+ RENDER_TIERS.forEach((tier) => {
446
+ describe(`[${tier}] tier`, () => {
447
+ it('renders checkboxes for multi-select', async () => {
448
+ const { renderSelect } = await import('../../../renderers/components/select')
449
+ const node = createSelectNode({
450
+ multiple: true,
451
+ options: [
452
+ { label: 'Option A', value: 'a' },
453
+ { label: 'Option B', value: 'b' },
454
+ ],
455
+ })
456
+ const ctx = createContext(tier)
457
+ const result = renderSelect(node, ctx)
458
+
459
+ expect(result).toMatch(/[☐☑✓✔\[\s\]\[x\]]/i)
460
+ })
461
+
462
+ it('shows multiple selected values', async () => {
463
+ const { renderSelect } = await import('../../../renderers/components/select')
464
+ const node = createSelectNode({
465
+ multiple: true,
466
+ value: ['a', 'c'],
467
+ options: [
468
+ { label: 'Option A', value: 'a' },
469
+ { label: 'Option B', value: 'b' },
470
+ { label: 'Option C', value: 'c' },
471
+ ],
472
+ })
473
+ const ctx = createContext(tier)
474
+ const result = renderSelect(node, ctx)
475
+
476
+ expect(result).toContain('Option A')
477
+ expect(result).toContain('Option C')
478
+ })
479
+ })
480
+ })
481
+ })
482
+
483
+ // ============================================================================
484
+ // Validation Tests
485
+ // ============================================================================
486
+
487
+ describe('validation', () => {
488
+ RENDER_TIERS.forEach((tier) => {
489
+ describe(`[${tier}] tier`, () => {
490
+ it('renders required indicator', async () => {
491
+ const { renderSelect } = await import('../../../renderers/components/select')
492
+ const node = createSelectNode({
493
+ label: 'Country',
494
+ required: true,
495
+ options: [{ label: 'USA', value: 'us' }],
496
+ })
497
+ const ctx = createContext(tier)
498
+ const result = renderSelect(node, ctx)
499
+
500
+ expect(result).toMatch(/\*|required/i)
501
+ })
502
+
503
+ it('renders error message', async () => {
504
+ const { renderSelect } = await import('../../../renderers/components/select')
505
+ const node = createSelectNode({
506
+ options: [{ label: 'Option', value: 'opt' }],
507
+ error: 'Please select an option',
508
+ })
509
+ const ctx = createContext(tier)
510
+ const result = renderSelect(node, ctx)
511
+
512
+ expect(result).toContain('Please select an option')
513
+ })
514
+ })
515
+ })
516
+
517
+ describe('[ansi] tier validation styling', () => {
518
+ it('renders error in red', async () => {
519
+ const { renderSelect } = await import('../../../renderers/components/select')
520
+ const node = createSelectNode({
521
+ options: [{ label: 'Option', value: 'opt' }],
522
+ error: 'Error message',
523
+ })
524
+ const ctx = createContext('ansi')
525
+ const result = renderSelect(node, ctx)
526
+
527
+ expect(result).toContain('\x1b[31m')
528
+ })
529
+
530
+ it('renders valid state in green', async () => {
531
+ const { renderSelect } = await import('../../../renderers/components/select')
532
+ const node = createSelectNode({
533
+ value: 'opt',
534
+ valid: true,
535
+ options: [{ label: 'Option', value: 'opt' }],
536
+ })
537
+ const ctx = createContext('ansi')
538
+ const result = renderSelect(node, ctx)
539
+
540
+ expect(result).toContain('\x1b[32m')
541
+ })
542
+ })
543
+ })
544
+
545
+ // ============================================================================
546
+ // Disabled State Tests
547
+ // ============================================================================
548
+
549
+ describe('disabled state', () => {
550
+ RENDER_TIERS.forEach((tier) => {
551
+ it(`[${tier}] renders disabled select`, async () => {
552
+ const { renderSelect } = await import('../../../renderers/components/select')
553
+ const node = createSelectNode({
554
+ disabled: true,
555
+ value: 'selected',
556
+ options: [
557
+ { label: 'Selected Option', value: 'selected' },
558
+ { label: 'Other', value: 'other' },
559
+ ],
560
+ })
561
+ const ctx = createContext(tier)
562
+ const result = renderSelect(node, ctx)
563
+
564
+ expect(result).toContain('Selected Option')
565
+ })
566
+ })
567
+
568
+ describe('[ansi] tier disabled styling', () => {
569
+ it('renders disabled select as dimmed', async () => {
570
+ const { renderSelect } = await import('../../../renderers/components/select')
571
+ const node = createSelectNode({
572
+ disabled: true,
573
+ options: [{ label: 'Option', value: 'opt' }],
574
+ })
575
+ const ctx = createContext('ansi')
576
+ const result = renderSelect(node, ctx)
577
+
578
+ expect(result).toContain('\x1b[2m')
579
+ })
580
+ })
581
+ })
582
+
583
+ // ============================================================================
584
+ // Loading State Tests
585
+ // ============================================================================
586
+
587
+ describe('loading state', () => {
588
+ RENDER_TIERS.forEach((tier) => {
589
+ it(`[${tier}] renders loading indicator`, async () => {
590
+ const { renderSelect } = await import('../../../renderers/components/select')
591
+ const node = createSelectNode({
592
+ loading: true,
593
+ options: [],
594
+ })
595
+ const ctx = createContext(tier)
596
+ const result = renderSelect(node, ctx)
597
+
598
+ expect(result).toMatch(/loading|\.{3}|⠋|spinner/i)
599
+ })
600
+ })
601
+ })
602
+
603
+ // ============================================================================
604
+ // Edge Cases
605
+ // ============================================================================
606
+
607
+ describe('edge cases', () => {
608
+ it('handles missing options array', async () => {
609
+ const { renderSelect } = await import('../../../renderers/components/select')
610
+ const node = createSelectNode({})
611
+ const ctx = createContext('text')
612
+ const result = renderSelect(node, ctx)
613
+
614
+ expect(typeof result).toBe('string')
615
+ })
616
+
617
+ it('handles null value', async () => {
618
+ const { renderSelect } = await import('../../../renderers/components/select')
619
+ const node = createSelectNode({
620
+ value: null,
621
+ options: [{ label: 'Option', value: 'opt' }],
622
+ })
623
+ const ctx = createContext('text')
624
+ const result = renderSelect(node, ctx)
625
+
626
+ expect(result).toContain('Option')
627
+ })
628
+
629
+ it('handles unicode in options', async () => {
630
+ const { renderSelect } = await import('../../../renderers/components/select')
631
+ const node = createSelectNode({
632
+ options: [
633
+ { label: '日本語', value: 'ja' },
634
+ { label: 'العربية', value: 'ar' },
635
+ ],
636
+ })
637
+ const ctx = createContext('text')
638
+ const result = renderSelect(node, ctx)
639
+
640
+ expect(result).toContain('日本語')
641
+ expect(result).toContain('العربية')
642
+ })
643
+
644
+ it('handles emoji in option labels', async () => {
645
+ const { renderSelect } = await import('../../../renderers/components/select')
646
+ const node = createSelectNode({
647
+ options: [
648
+ { label: '🇺🇸 United States', value: 'us' },
649
+ { label: '🇬🇧 United Kingdom', value: 'uk' },
650
+ ],
651
+ })
652
+ const ctx = createContext('text')
653
+ const result = renderSelect(node, ctx)
654
+
655
+ expect(result).toContain('United States')
656
+ })
657
+ })
658
+ })