@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,1366 @@
1
+ /**
2
+ * @mdxui/terminal ANSI Renderer Tests
3
+ *
4
+ * TDD RED Phase: Tests for the ANSI escape code renderer.
5
+ * All tests should FAIL initially because renderANSI doesn't exist yet.
6
+ *
7
+ * The ANSI renderer converts UINode trees to colored terminal output strings
8
+ * using ANSI escape codes for styling (colors, bold, underline, etc.).
9
+ *
10
+ * This is part of the Universal Terminal UI 6-tier rendering system.
11
+ */
12
+ import { describe, it, expect, beforeEach } from 'vitest'
13
+
14
+ // ============================================================================
15
+ // Types for ANSI Renderer (these define the contract)
16
+ // ============================================================================
17
+
18
+ /**
19
+ * UINode represents a node in the terminal UI tree.
20
+ * This is the input format for all renderers.
21
+ */
22
+ interface UINode {
23
+ type: string
24
+ props?: Record<string, unknown>
25
+ children?: UINode[] | string
26
+ text?: string
27
+ }
28
+
29
+ /**
30
+ * ANSI renderer options
31
+ */
32
+ interface ANSIRenderOptions {
33
+ /** Color support level */
34
+ colorSupport?: 'none' | '16' | '256' | 'truecolor'
35
+ /** Theme mode */
36
+ theme?: 'dark' | 'light'
37
+ /** Terminal width for wrapping */
38
+ width?: number
39
+ /** Whether to include reset codes between elements */
40
+ resetBetweenElements?: boolean
41
+ }
42
+
43
+ // ============================================================================
44
+ // Test Suite: Basic ANSI Output
45
+ // ============================================================================
46
+
47
+ describe('renderANSI', () => {
48
+ describe('basic text rendering', () => {
49
+ it('renders plain text without ANSI codes', async () => {
50
+ const { renderANSI } = await import('../../renderers/ansi')
51
+
52
+ const node: UINode = {
53
+ type: 'text',
54
+ text: 'Hello World',
55
+ }
56
+
57
+ const result = renderANSI(node)
58
+ expect(result).toBe('Hello World')
59
+ })
60
+
61
+ it('renders nested text nodes', async () => {
62
+ const { renderANSI } = await import('../../renderers/ansi')
63
+
64
+ const node: UINode = {
65
+ type: 'box',
66
+ children: [
67
+ { type: 'text', text: 'Line 1' },
68
+ { type: 'text', text: 'Line 2' },
69
+ ],
70
+ }
71
+
72
+ const result = renderANSI(node)
73
+ expect(result).toContain('Line 1')
74
+ expect(result).toContain('Line 2')
75
+ })
76
+
77
+ it('returns empty string for empty node', async () => {
78
+ const { renderANSI } = await import('../../renderers/ansi')
79
+
80
+ const node: UINode = {
81
+ type: 'text',
82
+ text: '',
83
+ }
84
+
85
+ const result = renderANSI(node)
86
+ expect(result).toBe('')
87
+ })
88
+ })
89
+
90
+ // ============================================================================
91
+ // Foreground Colors (Basic 16 ANSI)
92
+ // ============================================================================
93
+
94
+ describe('foreground colors (16 color)', () => {
95
+ it('applies red foreground color', async () => {
96
+ const { renderANSI } = await import('../../renderers/ansi')
97
+
98
+ const node: UINode = {
99
+ type: 'text',
100
+ text: 'Error',
101
+ props: { color: 'red' },
102
+ }
103
+
104
+ const result = renderANSI(node)
105
+ expect(result).toContain('\x1b[31m') // Red foreground
106
+ expect(result).toContain('Error')
107
+ expect(result).toContain('\x1b[0m') // Reset
108
+ })
109
+
110
+ it('applies green foreground color', async () => {
111
+ const { renderANSI } = await import('../../renderers/ansi')
112
+
113
+ const node: UINode = {
114
+ type: 'text',
115
+ text: 'Success',
116
+ props: { color: 'green' },
117
+ }
118
+
119
+ const result = renderANSI(node)
120
+ expect(result).toContain('\x1b[32m') // Green foreground
121
+ expect(result).toContain('Success')
122
+ })
123
+
124
+ it('applies yellow foreground color', async () => {
125
+ const { renderANSI } = await import('../../renderers/ansi')
126
+
127
+ const node: UINode = {
128
+ type: 'text',
129
+ text: 'Warning',
130
+ props: { color: 'yellow' },
131
+ }
132
+
133
+ const result = renderANSI(node)
134
+ expect(result).toContain('\x1b[33m') // Yellow foreground
135
+ })
136
+
137
+ it('applies blue foreground color', async () => {
138
+ const { renderANSI } = await import('../../renderers/ansi')
139
+
140
+ const node: UINode = {
141
+ type: 'text',
142
+ text: 'Info',
143
+ props: { color: 'blue' },
144
+ }
145
+
146
+ const result = renderANSI(node)
147
+ expect(result).toContain('\x1b[34m') // Blue foreground
148
+ })
149
+
150
+ it('applies magenta foreground color', async () => {
151
+ const { renderANSI } = await import('../../renderers/ansi')
152
+
153
+ const node: UINode = {
154
+ type: 'text',
155
+ text: 'Highlight',
156
+ props: { color: 'magenta' },
157
+ }
158
+
159
+ const result = renderANSI(node)
160
+ expect(result).toContain('\x1b[35m') // Magenta foreground
161
+ })
162
+
163
+ it('applies cyan foreground color', async () => {
164
+ const { renderANSI } = await import('../../renderers/ansi')
165
+
166
+ const node: UINode = {
167
+ type: 'text',
168
+ text: 'Accent',
169
+ props: { color: 'cyan' },
170
+ }
171
+
172
+ const result = renderANSI(node)
173
+ expect(result).toContain('\x1b[36m') // Cyan foreground
174
+ })
175
+
176
+ it('applies white foreground color', async () => {
177
+ const { renderANSI } = await import('../../renderers/ansi')
178
+
179
+ const node: UINode = {
180
+ type: 'text',
181
+ text: 'Light',
182
+ props: { color: 'white' },
183
+ }
184
+
185
+ const result = renderANSI(node)
186
+ expect(result).toContain('\x1b[37m') // White foreground
187
+ })
188
+
189
+ it('applies black foreground color', async () => {
190
+ const { renderANSI } = await import('../../renderers/ansi')
191
+
192
+ const node: UINode = {
193
+ type: 'text',
194
+ text: 'Dark',
195
+ props: { color: 'black' },
196
+ }
197
+
198
+ const result = renderANSI(node)
199
+ expect(result).toContain('\x1b[30m') // Black foreground
200
+ })
201
+ })
202
+
203
+ // ============================================================================
204
+ // Bright Foreground Colors
205
+ // ============================================================================
206
+
207
+ describe('bright foreground colors', () => {
208
+ it('applies bright red foreground', async () => {
209
+ const { renderANSI } = await import('../../renderers/ansi')
210
+
211
+ const node: UINode = {
212
+ type: 'text',
213
+ text: 'Alert',
214
+ props: { color: 'brightRed' },
215
+ }
216
+
217
+ const result = renderANSI(node)
218
+ expect(result).toContain('\x1b[91m') // Bright red
219
+ })
220
+
221
+ it('applies bright green foreground', async () => {
222
+ const { renderANSI } = await import('../../renderers/ansi')
223
+
224
+ const node: UINode = {
225
+ type: 'text',
226
+ text: 'Done',
227
+ props: { color: 'brightGreen' },
228
+ }
229
+
230
+ const result = renderANSI(node)
231
+ expect(result).toContain('\x1b[92m') // Bright green
232
+ })
233
+
234
+ it('applies bright yellow foreground', async () => {
235
+ const { renderANSI } = await import('../../renderers/ansi')
236
+
237
+ const node: UINode = {
238
+ type: 'text',
239
+ text: 'Caution',
240
+ props: { color: 'brightYellow' },
241
+ }
242
+
243
+ const result = renderANSI(node)
244
+ expect(result).toContain('\x1b[93m') // Bright yellow
245
+ })
246
+
247
+ it('applies bright blue foreground', async () => {
248
+ const { renderANSI } = await import('../../renderers/ansi')
249
+
250
+ const node: UINode = {
251
+ type: 'text',
252
+ text: 'Link',
253
+ props: { color: 'brightBlue' },
254
+ }
255
+
256
+ const result = renderANSI(node)
257
+ expect(result).toContain('\x1b[94m') // Bright blue
258
+ })
259
+
260
+ it('applies bright magenta foreground', async () => {
261
+ const { renderANSI } = await import('../../renderers/ansi')
262
+
263
+ const node: UINode = {
264
+ type: 'text',
265
+ text: 'Special',
266
+ props: { color: 'brightMagenta' },
267
+ }
268
+
269
+ const result = renderANSI(node)
270
+ expect(result).toContain('\x1b[95m') // Bright magenta
271
+ })
272
+
273
+ it('applies bright cyan foreground', async () => {
274
+ const { renderANSI } = await import('../../renderers/ansi')
275
+
276
+ const node: UINode = {
277
+ type: 'text',
278
+ text: 'Primary',
279
+ props: { color: 'brightCyan' },
280
+ }
281
+
282
+ const result = renderANSI(node)
283
+ expect(result).toContain('\x1b[96m') // Bright cyan
284
+ })
285
+
286
+ it('applies bright white foreground', async () => {
287
+ const { renderANSI } = await import('../../renderers/ansi')
288
+
289
+ const node: UINode = {
290
+ type: 'text',
291
+ text: 'Bright',
292
+ props: { color: 'brightWhite' },
293
+ }
294
+
295
+ const result = renderANSI(node)
296
+ expect(result).toContain('\x1b[97m') // Bright white
297
+ })
298
+
299
+ it('applies bright black (gray) foreground', async () => {
300
+ const { renderANSI } = await import('../../renderers/ansi')
301
+
302
+ const node: UINode = {
303
+ type: 'text',
304
+ text: 'Muted',
305
+ props: { color: 'brightBlack' },
306
+ }
307
+
308
+ const result = renderANSI(node)
309
+ expect(result).toContain('\x1b[90m') // Bright black (gray)
310
+ })
311
+ })
312
+
313
+ // ============================================================================
314
+ // Background Colors
315
+ // ============================================================================
316
+
317
+ describe('background colors', () => {
318
+ it('applies red background color', async () => {
319
+ const { renderANSI } = await import('../../renderers/ansi')
320
+
321
+ const node: UINode = {
322
+ type: 'text',
323
+ text: 'Error Badge',
324
+ props: { backgroundColor: 'red' },
325
+ }
326
+
327
+ const result = renderANSI(node)
328
+ expect(result).toContain('\x1b[41m') // Red background
329
+ expect(result).toContain('Error Badge')
330
+ })
331
+
332
+ it('applies green background color', async () => {
333
+ const { renderANSI } = await import('../../renderers/ansi')
334
+
335
+ const node: UINode = {
336
+ type: 'text',
337
+ text: 'Success Badge',
338
+ props: { backgroundColor: 'green' },
339
+ }
340
+
341
+ const result = renderANSI(node)
342
+ expect(result).toContain('\x1b[42m') // Green background
343
+ })
344
+
345
+ it('applies yellow background color', async () => {
346
+ const { renderANSI } = await import('../../renderers/ansi')
347
+
348
+ const node: UINode = {
349
+ type: 'text',
350
+ text: 'Warning Badge',
351
+ props: { backgroundColor: 'yellow' },
352
+ }
353
+
354
+ const result = renderANSI(node)
355
+ expect(result).toContain('\x1b[43m') // Yellow background
356
+ })
357
+
358
+ it('applies blue background color', async () => {
359
+ const { renderANSI } = await import('../../renderers/ansi')
360
+
361
+ const node: UINode = {
362
+ type: 'text',
363
+ text: 'Info Badge',
364
+ props: { backgroundColor: 'blue' },
365
+ }
366
+
367
+ const result = renderANSI(node)
368
+ expect(result).toContain('\x1b[44m') // Blue background
369
+ })
370
+
371
+ it('applies magenta background color', async () => {
372
+ const { renderANSI } = await import('../../renderers/ansi')
373
+
374
+ const node: UINode = {
375
+ type: 'text',
376
+ text: 'Highlight',
377
+ props: { backgroundColor: 'magenta' },
378
+ }
379
+
380
+ const result = renderANSI(node)
381
+ expect(result).toContain('\x1b[45m') // Magenta background
382
+ })
383
+
384
+ it('applies cyan background color', async () => {
385
+ const { renderANSI } = await import('../../renderers/ansi')
386
+
387
+ const node: UINode = {
388
+ type: 'text',
389
+ text: 'Selected',
390
+ props: { backgroundColor: 'cyan' },
391
+ }
392
+
393
+ const result = renderANSI(node)
394
+ expect(result).toContain('\x1b[46m') // Cyan background
395
+ })
396
+
397
+ it('applies white background color', async () => {
398
+ const { renderANSI } = await import('../../renderers/ansi')
399
+
400
+ const node: UINode = {
401
+ type: 'text',
402
+ text: 'Inverted',
403
+ props: { backgroundColor: 'white' },
404
+ }
405
+
406
+ const result = renderANSI(node)
407
+ expect(result).toContain('\x1b[47m') // White background
408
+ })
409
+
410
+ it('applies black background color', async () => {
411
+ const { renderANSI } = await import('../../renderers/ansi')
412
+
413
+ const node: UINode = {
414
+ type: 'text',
415
+ text: 'Dark Mode',
416
+ props: { backgroundColor: 'black' },
417
+ }
418
+
419
+ const result = renderANSI(node)
420
+ expect(result).toContain('\x1b[40m') // Black background
421
+ })
422
+ })
423
+
424
+ // ============================================================================
425
+ // Text Formatting (Bold, Italic, Underline, etc.)
426
+ // ============================================================================
427
+
428
+ describe('text formatting', () => {
429
+ it('applies bold formatting', async () => {
430
+ const { renderANSI } = await import('../../renderers/ansi')
431
+
432
+ const node: UINode = {
433
+ type: 'text',
434
+ text: 'Important',
435
+ props: { bold: true },
436
+ }
437
+
438
+ const result = renderANSI(node)
439
+ expect(result).toContain('\x1b[1m') // Bold
440
+ expect(result).toContain('Important')
441
+ expect(result).toContain('\x1b[0m') // Reset
442
+ })
443
+
444
+ it('applies italic formatting', async () => {
445
+ const { renderANSI } = await import('../../renderers/ansi')
446
+
447
+ const node: UINode = {
448
+ type: 'text',
449
+ text: 'Emphasis',
450
+ props: { italic: true },
451
+ }
452
+
453
+ const result = renderANSI(node)
454
+ expect(result).toContain('\x1b[3m') // Italic
455
+ })
456
+
457
+ it('applies underline formatting', async () => {
458
+ const { renderANSI } = await import('../../renderers/ansi')
459
+
460
+ const node: UINode = {
461
+ type: 'text',
462
+ text: 'Link',
463
+ props: { underline: true },
464
+ }
465
+
466
+ const result = renderANSI(node)
467
+ expect(result).toContain('\x1b[4m') // Underline
468
+ })
469
+
470
+ it('applies dim/faint formatting', async () => {
471
+ const { renderANSI } = await import('../../renderers/ansi')
472
+
473
+ const node: UINode = {
474
+ type: 'text',
475
+ text: 'Subtle',
476
+ props: { dim: true },
477
+ }
478
+
479
+ const result = renderANSI(node)
480
+ expect(result).toContain('\x1b[2m') // Dim
481
+ })
482
+
483
+ it('applies strikethrough formatting', async () => {
484
+ const { renderANSI } = await import('../../renderers/ansi')
485
+
486
+ const node: UINode = {
487
+ type: 'text',
488
+ text: 'Deleted',
489
+ props: { strikethrough: true },
490
+ }
491
+
492
+ const result = renderANSI(node)
493
+ expect(result).toContain('\x1b[9m') // Strikethrough
494
+ })
495
+
496
+ it('applies inverse/reverse formatting', async () => {
497
+ const { renderANSI } = await import('../../renderers/ansi')
498
+
499
+ const node: UINode = {
500
+ type: 'text',
501
+ text: 'Inverted',
502
+ props: { inverse: true },
503
+ }
504
+
505
+ const result = renderANSI(node)
506
+ expect(result).toContain('\x1b[7m') // Inverse
507
+ })
508
+ })
509
+
510
+ // ============================================================================
511
+ // Combined Styles
512
+ // ============================================================================
513
+
514
+ describe('combined styles', () => {
515
+ it('combines bold and color', async () => {
516
+ const { renderANSI } = await import('../../renderers/ansi')
517
+
518
+ const node: UINode = {
519
+ type: 'text',
520
+ text: 'Bold Red',
521
+ props: { bold: true, color: 'red' },
522
+ }
523
+
524
+ const result = renderANSI(node)
525
+ expect(result).toContain('\x1b[1m') // Bold
526
+ expect(result).toContain('\x1b[31m') // Red
527
+ expect(result).toContain('Bold Red')
528
+ expect(result).toContain('\x1b[0m') // Reset
529
+ })
530
+
531
+ it('combines foreground and background colors', async () => {
532
+ const { renderANSI } = await import('../../renderers/ansi')
533
+
534
+ const node: UINode = {
535
+ type: 'text',
536
+ text: 'Badge',
537
+ props: { color: 'white', backgroundColor: 'blue' },
538
+ }
539
+
540
+ const result = renderANSI(node)
541
+ expect(result).toContain('\x1b[37m') // White foreground
542
+ expect(result).toContain('\x1b[44m') // Blue background
543
+ })
544
+
545
+ it('combines bold, italic, and underline', async () => {
546
+ const { renderANSI } = await import('../../renderers/ansi')
547
+
548
+ const node: UINode = {
549
+ type: 'text',
550
+ text: 'Fancy',
551
+ props: { bold: true, italic: true, underline: true },
552
+ }
553
+
554
+ const result = renderANSI(node)
555
+ expect(result).toContain('\x1b[1m') // Bold
556
+ expect(result).toContain('\x1b[3m') // Italic
557
+ expect(result).toContain('\x1b[4m') // Underline
558
+ })
559
+
560
+ it('combines all style properties', async () => {
561
+ const { renderANSI } = await import('../../renderers/ansi')
562
+
563
+ const node: UINode = {
564
+ type: 'text',
565
+ text: 'Everything',
566
+ props: {
567
+ bold: true,
568
+ italic: true,
569
+ underline: true,
570
+ color: 'cyan',
571
+ backgroundColor: 'black',
572
+ },
573
+ }
574
+
575
+ const result = renderANSI(node)
576
+ expect(result).toContain('\x1b[1m') // Bold
577
+ expect(result).toContain('\x1b[3m') // Italic
578
+ expect(result).toContain('\x1b[4m') // Underline
579
+ expect(result).toContain('\x1b[36m') // Cyan
580
+ expect(result).toContain('\x1b[40m') // Black bg
581
+ expect(result).toContain('\x1b[0m') // Reset
582
+ })
583
+ })
584
+
585
+ // ============================================================================
586
+ // 256 Color Support
587
+ // ============================================================================
588
+
589
+ describe('256 color support', () => {
590
+ it('applies 256-color foreground using numeric code', async () => {
591
+ const { renderANSI } = await import('../../renderers/ansi')
592
+
593
+ const node: UINode = {
594
+ type: 'text',
595
+ text: 'Extended',
596
+ props: { color: 196 }, // Bright red in 256 palette
597
+ }
598
+
599
+ const result = renderANSI(node)
600
+ expect(result).toContain('\x1b[38;5;196m') // 256-color foreground
601
+ })
602
+
603
+ it('applies 256-color background using numeric code', async () => {
604
+ const { renderANSI } = await import('../../renderers/ansi')
605
+
606
+ const node: UINode = {
607
+ type: 'text',
608
+ text: 'Extended BG',
609
+ props: { backgroundColor: 21 }, // Blue in 256 palette
610
+ }
611
+
612
+ const result = renderANSI(node)
613
+ expect(result).toContain('\x1b[48;5;21m') // 256-color background
614
+ })
615
+
616
+ it('applies grayscale 256-color', async () => {
617
+ const { renderANSI } = await import('../../renderers/ansi')
618
+
619
+ const node: UINode = {
620
+ type: 'text',
621
+ text: 'Gray',
622
+ props: { color: 240 }, // Mid gray in grayscale ramp (232-255)
623
+ }
624
+
625
+ const result = renderANSI(node)
626
+ expect(result).toContain('\x1b[38;5;240m')
627
+ })
628
+
629
+ it('applies 256-color cube values', async () => {
630
+ const { renderANSI } = await import('../../renderers/ansi')
631
+
632
+ // Color cube starts at 16: 6x6x6 = 216 colors (16-231)
633
+ const node: UINode = {
634
+ type: 'text',
635
+ text: 'Cube Color',
636
+ props: { color: 75 }, // Light blue in color cube
637
+ }
638
+
639
+ const result = renderANSI(node)
640
+ expect(result).toContain('\x1b[38;5;75m')
641
+ })
642
+ })
643
+
644
+ // ============================================================================
645
+ // True Color (24-bit RGB) Support
646
+ // ============================================================================
647
+
648
+ describe('true color (24-bit) support', () => {
649
+ it('applies RGB foreground color', async () => {
650
+ const { renderANSI } = await import('../../renderers/ansi')
651
+
652
+ const node: UINode = {
653
+ type: 'text',
654
+ text: 'RGB Text',
655
+ props: { color: { r: 59, g: 130, b: 246 } }, // Tailwind blue-500
656
+ }
657
+
658
+ const result = renderANSI(node)
659
+ expect(result).toContain('\x1b[38;2;59;130;246m') // True color foreground
660
+ })
661
+
662
+ it('applies RGB background color', async () => {
663
+ const { renderANSI } = await import('../../renderers/ansi')
664
+
665
+ const node: UINode = {
666
+ type: 'text',
667
+ text: 'RGB Background',
668
+ props: { backgroundColor: { r: 239, g: 68, b: 68 } }, // Tailwind red-500
669
+ }
670
+
671
+ const result = renderANSI(node)
672
+ expect(result).toContain('\x1b[48;2;239;68;68m') // True color background
673
+ })
674
+
675
+ it('applies hex color foreground', async () => {
676
+ const { renderANSI } = await import('../../renderers/ansi')
677
+
678
+ const node: UINode = {
679
+ type: 'text',
680
+ text: 'Hex Color',
681
+ props: { color: '#3b82f6' }, // Tailwind blue-500
682
+ }
683
+
684
+ const result = renderANSI(node)
685
+ expect(result).toContain('\x1b[38;2;59;130;246m')
686
+ })
687
+
688
+ it('applies hex color background', async () => {
689
+ const { renderANSI } = await import('../../renderers/ansi')
690
+
691
+ const node: UINode = {
692
+ type: 'text',
693
+ text: 'Hex Background',
694
+ props: { backgroundColor: '#10b981' }, // Tailwind emerald-500
695
+ }
696
+
697
+ const result = renderANSI(node)
698
+ expect(result).toContain('\x1b[48;2;16;185;129m')
699
+ })
700
+
701
+ it('handles 3-char hex shorthand', async () => {
702
+ const { renderANSI } = await import('../../renderers/ansi')
703
+
704
+ const node: UINode = {
705
+ type: 'text',
706
+ text: 'Short Hex',
707
+ props: { color: '#f00' }, // Red
708
+ }
709
+
710
+ const result = renderANSI(node)
711
+ expect(result).toContain('\x1b[38;2;255;0;0m')
712
+ })
713
+ })
714
+
715
+ // ============================================================================
716
+ // Reset Codes
717
+ // ============================================================================
718
+
719
+ describe('reset codes', () => {
720
+ it('appends reset code after styled text', async () => {
721
+ const { renderANSI } = await import('../../renderers/ansi')
722
+
723
+ const node: UINode = {
724
+ type: 'text',
725
+ text: 'Styled',
726
+ props: { color: 'red' },
727
+ }
728
+
729
+ const result = renderANSI(node)
730
+ expect(result.endsWith('\x1b[0m')).toBe(true)
731
+ })
732
+
733
+ it('resets between sibling elements', async () => {
734
+ const { renderANSI } = await import('../../renderers/ansi')
735
+
736
+ const node: UINode = {
737
+ type: 'box',
738
+ children: [
739
+ { type: 'text', text: 'Red', props: { color: 'red' } },
740
+ { type: 'text', text: 'Blue', props: { color: 'blue' } },
741
+ ],
742
+ }
743
+
744
+ const result = renderANSI(node)
745
+ // Red text should be followed by reset before blue
746
+ const redIndex = result.indexOf('\x1b[31m')
747
+ const resetIndex = result.indexOf('\x1b[0m', redIndex)
748
+ const blueIndex = result.indexOf('\x1b[34m')
749
+
750
+ expect(redIndex).toBeLessThan(resetIndex)
751
+ expect(resetIndex).toBeLessThan(blueIndex)
752
+ })
753
+
754
+ it('does not add unnecessary resets for plain text', async () => {
755
+ const { renderANSI } = await import('../../renderers/ansi')
756
+
757
+ const node: UINode = {
758
+ type: 'text',
759
+ text: 'Plain',
760
+ }
761
+
762
+ const result = renderANSI(node)
763
+ expect(result).not.toContain('\x1b[')
764
+ })
765
+ })
766
+
767
+ // ============================================================================
768
+ // Color Support Degradation
769
+ // ============================================================================
770
+
771
+ describe('color support degradation', () => {
772
+ it('degrades true color to 256 colors when colorSupport is 256', async () => {
773
+ const { renderANSI } = await import('../../renderers/ansi')
774
+
775
+ const node: UINode = {
776
+ type: 'text',
777
+ text: 'Degraded',
778
+ props: { color: '#3b82f6' },
779
+ }
780
+
781
+ const result = renderANSI(node, { colorSupport: '256' })
782
+ // Should use 256-color escape sequence instead of true color
783
+ expect(result).toMatch(/\x1b\[38;5;\d+m/)
784
+ expect(result).not.toContain('\x1b[38;2;')
785
+ })
786
+
787
+ it('degrades 256 color to 16 colors when colorSupport is 16', async () => {
788
+ const { renderANSI } = await import('../../renderers/ansi')
789
+
790
+ const node: UINode = {
791
+ type: 'text',
792
+ text: 'Basic',
793
+ props: { color: 196 }, // 256-color red
794
+ }
795
+
796
+ const result = renderANSI(node, { colorSupport: '16' })
797
+ // Should use basic 16-color escape sequence
798
+ expect(result).toMatch(/\x1b\[3[0-7]m|\x1b\[9[0-7]m/)
799
+ expect(result).not.toContain('\x1b[38;5;')
800
+ })
801
+
802
+ it('strips all colors when colorSupport is none', async () => {
803
+ const { renderANSI } = await import('../../renderers/ansi')
804
+
805
+ const node: UINode = {
806
+ type: 'text',
807
+ text: 'No Color',
808
+ props: { color: 'red', backgroundColor: 'blue', bold: true },
809
+ }
810
+
811
+ const result = renderANSI(node, { colorSupport: 'none' })
812
+ // Should not contain any color codes, but may contain bold
813
+ expect(result).not.toContain('\x1b[31m')
814
+ expect(result).not.toContain('\x1b[44m')
815
+ expect(result).toContain('No Color')
816
+ })
817
+
818
+ it('preserves text formatting when colors are stripped', async () => {
819
+ const { renderANSI } = await import('../../renderers/ansi')
820
+
821
+ const node: UINode = {
822
+ type: 'text',
823
+ text: 'Bold Only',
824
+ props: { color: 'red', bold: true },
825
+ }
826
+
827
+ const result = renderANSI(node, { colorSupport: 'none' })
828
+ // Bold formatting should be preserved even without colors
829
+ expect(result).toContain('\x1b[1m')
830
+ expect(result).toContain('Bold Only')
831
+ })
832
+ })
833
+
834
+ // ============================================================================
835
+ // Theme Support
836
+ // ============================================================================
837
+
838
+ describe('theme support', () => {
839
+ it('uses dark theme colors by default', async () => {
840
+ const { renderANSI } = await import('../../renderers/ansi')
841
+
842
+ const node: UINode = {
843
+ type: 'text',
844
+ text: 'Default',
845
+ props: { color: 'primary' },
846
+ }
847
+
848
+ const result = renderANSI(node)
849
+ // Should use dark theme primary color (typically cyan/blue)
850
+ expect(result).toMatch(/\x1b\[/)
851
+ })
852
+
853
+ it('uses light theme colors when specified', async () => {
854
+ const { renderANSI } = await import('../../renderers/ansi')
855
+
856
+ const node: UINode = {
857
+ type: 'text',
858
+ text: 'Light Mode',
859
+ props: { color: 'primary' },
860
+ }
861
+
862
+ const darkResult = renderANSI(node, { theme: 'dark' })
863
+ const lightResult = renderANSI(node, { theme: 'light' })
864
+
865
+ // Light and dark theme should produce different colors for primary
866
+ expect(darkResult).not.toBe(lightResult)
867
+ })
868
+
869
+ it('resolves semantic color names', async () => {
870
+ const { renderANSI } = await import('../../renderers/ansi')
871
+
872
+ const semanticColors = ['primary', 'secondary', 'accent', 'muted', 'success', 'warning', 'error', 'info']
873
+
874
+ for (const color of semanticColors) {
875
+ const node: UINode = {
876
+ type: 'text',
877
+ text: color,
878
+ props: { color },
879
+ }
880
+
881
+ const result = renderANSI(node)
882
+ expect(result).toContain('\x1b[')
883
+ expect(result).toContain(color)
884
+ }
885
+ })
886
+
887
+ it('resolves foreground semantic color', async () => {
888
+ const { renderANSI } = await import('../../renderers/ansi')
889
+
890
+ const node: UINode = {
891
+ type: 'text',
892
+ text: 'Foreground',
893
+ props: { color: 'foreground' },
894
+ }
895
+
896
+ const result = renderANSI(node)
897
+ expect(result).toContain('\x1b[')
898
+ })
899
+
900
+ it('resolves background semantic color', async () => {
901
+ const { renderANSI } = await import('../../renderers/ansi')
902
+
903
+ const node: UINode = {
904
+ type: 'text',
905
+ text: 'Background',
906
+ props: { backgroundColor: 'background' },
907
+ }
908
+
909
+ const result = renderANSI(node)
910
+ expect(result).toContain('\x1b[')
911
+ })
912
+ })
913
+
914
+ // ============================================================================
915
+ // Component Type Rendering
916
+ // ============================================================================
917
+
918
+ describe('component types', () => {
919
+ it('renders box component with proper structure', async () => {
920
+ const { renderANSI } = await import('../../renderers/ansi')
921
+
922
+ const node: UINode = {
923
+ type: 'box',
924
+ props: { border: 'single' },
925
+ children: [{ type: 'text', text: 'Content' }],
926
+ }
927
+
928
+ const result = renderANSI(node)
929
+ expect(result).toContain('Content')
930
+ })
931
+
932
+ it('renders badge component with variant colors', async () => {
933
+ const { renderANSI } = await import('../../renderers/ansi')
934
+
935
+ const node: UINode = {
936
+ type: 'badge',
937
+ props: { variant: 'success' },
938
+ text: 'Active',
939
+ }
940
+
941
+ const result = renderANSI(node)
942
+ expect(result).toContain('\x1b[') // Should have color styling
943
+ expect(result).toContain('Active')
944
+ })
945
+
946
+ it('renders button component with focus styling', async () => {
947
+ const { renderANSI } = await import('../../renderers/ansi')
948
+
949
+ const node: UINode = {
950
+ type: 'button',
951
+ props: { focused: true, variant: 'primary' },
952
+ text: 'Submit',
953
+ }
954
+
955
+ const result = renderANSI(node)
956
+ expect(result).toContain('Submit')
957
+ // Focused buttons should have distinct styling
958
+ expect(result).toContain('\x1b[')
959
+ })
960
+
961
+ it('renders spinner component with animation frame', async () => {
962
+ const { renderANSI } = await import('../../renderers/ansi')
963
+
964
+ const node: UINode = {
965
+ type: 'spinner',
966
+ props: { label: 'Loading...' },
967
+ }
968
+
969
+ const result = renderANSI(node)
970
+ expect(result).toContain('Loading...')
971
+ })
972
+
973
+ it('renders panel component with title', async () => {
974
+ const { renderANSI } = await import('../../renderers/ansi')
975
+
976
+ const node: UINode = {
977
+ type: 'panel',
978
+ props: { title: 'Settings', border: 'single' },
979
+ children: [{ type: 'text', text: 'Panel content' }],
980
+ }
981
+
982
+ const result = renderANSI(node)
983
+ expect(result).toContain('Settings')
984
+ expect(result).toContain('Panel content')
985
+ })
986
+
987
+ it('renders card component with title styling', async () => {
988
+ const { renderANSI } = await import('../../renderers/ansi')
989
+
990
+ const node: UINode = {
991
+ type: 'card',
992
+ props: { title: 'Card Title' },
993
+ children: [{ type: 'text', text: 'Card body' }],
994
+ }
995
+
996
+ const result = renderANSI(node)
997
+ expect(result).toContain('Card Title')
998
+ expect(result).toContain('Card body')
999
+ })
1000
+
1001
+ it('renders input component with cursor', async () => {
1002
+ const { renderANSI } = await import('../../renderers/ansi')
1003
+
1004
+ const node: UINode = {
1005
+ type: 'input',
1006
+ props: { value: 'Hello', focused: true, cursorPosition: 5 },
1007
+ }
1008
+
1009
+ const result = renderANSI(node)
1010
+ expect(result).toContain('Hello')
1011
+ })
1012
+
1013
+ it('renders select component with options', async () => {
1014
+ const { renderANSI } = await import('../../renderers/ansi')
1015
+
1016
+ const node: UINode = {
1017
+ type: 'select',
1018
+ props: {
1019
+ options: [
1020
+ { label: 'Option 1', value: 1 },
1021
+ { label: 'Option 2', value: 2 },
1022
+ ],
1023
+ highlightedIndex: 0,
1024
+ },
1025
+ }
1026
+
1027
+ const result = renderANSI(node)
1028
+ expect(result).toContain('Option 1')
1029
+ expect(result).toContain('Option 2')
1030
+ })
1031
+
1032
+ it('renders table component with headers and rows', async () => {
1033
+ const { renderANSI } = await import('../../renderers/ansi')
1034
+
1035
+ const node: UINode = {
1036
+ type: 'table',
1037
+ props: {
1038
+ columns: [
1039
+ { key: 'name', header: 'Name' },
1040
+ { key: 'status', header: 'Status' },
1041
+ ],
1042
+ data: [
1043
+ { name: 'Item 1', status: 'Active' },
1044
+ { name: 'Item 2', status: 'Inactive' },
1045
+ ],
1046
+ },
1047
+ }
1048
+
1049
+ const result = renderANSI(node)
1050
+ expect(result).toContain('Name')
1051
+ expect(result).toContain('Status')
1052
+ expect(result).toContain('Item 1')
1053
+ expect(result).toContain('Item 2')
1054
+ })
1055
+
1056
+ it('renders list component with bullets', async () => {
1057
+ const { renderANSI } = await import('../../renderers/ansi')
1058
+
1059
+ const node: UINode = {
1060
+ type: 'list',
1061
+ props: {
1062
+ items: ['First', 'Second', 'Third'],
1063
+ bullet: '*',
1064
+ },
1065
+ }
1066
+
1067
+ const result = renderANSI(node)
1068
+ expect(result).toContain('First')
1069
+ expect(result).toContain('Second')
1070
+ expect(result).toContain('Third')
1071
+ })
1072
+
1073
+ it('renders breadcrumb component with separator', async () => {
1074
+ const { renderANSI } = await import('../../renderers/ansi')
1075
+
1076
+ const node: UINode = {
1077
+ type: 'breadcrumb',
1078
+ props: {
1079
+ items: [
1080
+ { label: 'Home', path: '/' },
1081
+ { label: 'Settings', path: '/settings' },
1082
+ ],
1083
+ separator: '>',
1084
+ },
1085
+ }
1086
+
1087
+ const result = renderANSI(node)
1088
+ expect(result).toContain('Home')
1089
+ expect(result).toContain('>')
1090
+ expect(result).toContain('Settings')
1091
+ })
1092
+ })
1093
+
1094
+ // ============================================================================
1095
+ // Edge Cases
1096
+ // ============================================================================
1097
+
1098
+ describe('edge cases', () => {
1099
+ it('handles null children', async () => {
1100
+ const { renderANSI } = await import('../../renderers/ansi')
1101
+
1102
+ const node: UINode = {
1103
+ type: 'box',
1104
+ children: undefined,
1105
+ }
1106
+
1107
+ const result = renderANSI(node)
1108
+ expect(result).toBe('')
1109
+ })
1110
+
1111
+ it('handles empty array children', async () => {
1112
+ const { renderANSI } = await import('../../renderers/ansi')
1113
+
1114
+ const node: UINode = {
1115
+ type: 'box',
1116
+ children: [],
1117
+ }
1118
+
1119
+ const result = renderANSI(node)
1120
+ expect(result).toBe('')
1121
+ })
1122
+
1123
+ it('handles string children', async () => {
1124
+ const { renderANSI } = await import('../../renderers/ansi')
1125
+
1126
+ const node: UINode = {
1127
+ type: 'text',
1128
+ children: 'String child',
1129
+ }
1130
+
1131
+ const result = renderANSI(node)
1132
+ expect(result).toContain('String child')
1133
+ })
1134
+
1135
+ it('handles deeply nested nodes', async () => {
1136
+ const { renderANSI } = await import('../../renderers/ansi')
1137
+
1138
+ const node: UINode = {
1139
+ type: 'box',
1140
+ children: [
1141
+ {
1142
+ type: 'box',
1143
+ children: [
1144
+ {
1145
+ type: 'box',
1146
+ children: [
1147
+ {
1148
+ type: 'text',
1149
+ text: 'Deep',
1150
+ props: { color: 'cyan' },
1151
+ },
1152
+ ],
1153
+ },
1154
+ ],
1155
+ },
1156
+ ],
1157
+ }
1158
+
1159
+ const result = renderANSI(node)
1160
+ expect(result).toContain('Deep')
1161
+ expect(result).toContain('\x1b[36m') // Cyan
1162
+ })
1163
+
1164
+ it('handles special characters in text', async () => {
1165
+ const { renderANSI } = await import('../../renderers/ansi')
1166
+
1167
+ const node: UINode = {
1168
+ type: 'text',
1169
+ text: 'Special: <>&"\'',
1170
+ }
1171
+
1172
+ const result = renderANSI(node)
1173
+ expect(result).toContain('<>&"\'')
1174
+ })
1175
+
1176
+ it('handles unicode characters', async () => {
1177
+ const { renderANSI } = await import('../../renderers/ansi')
1178
+
1179
+ const node: UINode = {
1180
+ type: 'text',
1181
+ text: 'Unicode: \u2713 \u2717 \u25CF \u25CB',
1182
+ }
1183
+
1184
+ const result = renderANSI(node)
1185
+ expect(result).toContain('\u2713')
1186
+ expect(result).toContain('\u2717')
1187
+ })
1188
+
1189
+ it('handles emoji', async () => {
1190
+ const { renderANSI } = await import('../../renderers/ansi')
1191
+
1192
+ const node: UINode = {
1193
+ type: 'text',
1194
+ text: 'Status: OK',
1195
+ }
1196
+
1197
+ const result = renderANSI(node)
1198
+ expect(result).toContain('OK')
1199
+ })
1200
+
1201
+ it('handles newlines in text', async () => {
1202
+ const { renderANSI } = await import('../../renderers/ansi')
1203
+
1204
+ const node: UINode = {
1205
+ type: 'text',
1206
+ text: 'Line 1\nLine 2\nLine 3',
1207
+ }
1208
+
1209
+ const result = renderANSI(node)
1210
+ expect(result).toContain('Line 1')
1211
+ expect(result).toContain('Line 2')
1212
+ expect(result).toContain('Line 3')
1213
+ })
1214
+
1215
+ it('handles undefined props', async () => {
1216
+ const { renderANSI } = await import('../../renderers/ansi')
1217
+
1218
+ const node: UINode = {
1219
+ type: 'text',
1220
+ text: 'No props',
1221
+ props: undefined,
1222
+ }
1223
+
1224
+ const result = renderANSI(node)
1225
+ expect(result).toBe('No props')
1226
+ })
1227
+
1228
+ it('handles unknown node types gracefully', async () => {
1229
+ const { renderANSI } = await import('../../renderers/ansi')
1230
+
1231
+ const node: UINode = {
1232
+ type: 'unknown-component',
1233
+ text: 'Unknown',
1234
+ }
1235
+
1236
+ // Should not throw, may render text content or empty
1237
+ expect(() => renderANSI(node)).not.toThrow()
1238
+ })
1239
+
1240
+ it('handles invalid color values gracefully', async () => {
1241
+ const { renderANSI } = await import('../../renderers/ansi')
1242
+
1243
+ const node: UINode = {
1244
+ type: 'text',
1245
+ text: 'Invalid',
1246
+ props: { color: 'not-a-color' },
1247
+ }
1248
+
1249
+ // Should not throw, should render text without that color
1250
+ expect(() => renderANSI(node)).not.toThrow()
1251
+ const result = renderANSI(node)
1252
+ expect(result).toContain('Invalid')
1253
+ })
1254
+ })
1255
+
1256
+ // ============================================================================
1257
+ // Terminal Width Handling
1258
+ // ============================================================================
1259
+
1260
+ describe('terminal width', () => {
1261
+ it('respects width option for text wrapping context', async () => {
1262
+ const { renderANSI } = await import('../../renderers/ansi')
1263
+
1264
+ const node: UINode = {
1265
+ type: 'text',
1266
+ text: 'This is a long text that might need wrapping',
1267
+ props: { wrap: 'wrap' },
1268
+ }
1269
+
1270
+ const result = renderANSI(node, { width: 20 })
1271
+ // Result should be aware of width for layout purposes
1272
+ expect(result).toContain('This is')
1273
+ })
1274
+ })
1275
+
1276
+ // ============================================================================
1277
+ // Performance / Output Size
1278
+ // ============================================================================
1279
+
1280
+ describe('output optimization', () => {
1281
+ it('does not duplicate reset codes unnecessarily', async () => {
1282
+ const { renderANSI } = await import('../../renderers/ansi')
1283
+
1284
+ const node: UINode = {
1285
+ type: 'box',
1286
+ children: [
1287
+ { type: 'text', text: 'A', props: { color: 'red' } },
1288
+ { type: 'text', text: 'B', props: { color: 'red' } },
1289
+ ],
1290
+ }
1291
+
1292
+ const result = renderANSI(node)
1293
+ // Should not have consecutive reset codes
1294
+ expect(result).not.toContain('\x1b[0m\x1b[0m')
1295
+ })
1296
+
1297
+ it('combines adjacent ANSI codes efficiently', async () => {
1298
+ const { renderANSI } = await import('../../renderers/ansi')
1299
+
1300
+ const node: UINode = {
1301
+ type: 'text',
1302
+ text: 'Optimized',
1303
+ props: { bold: true, color: 'red' },
1304
+ }
1305
+
1306
+ const result = renderANSI(node)
1307
+ // Both codes should be present
1308
+ expect(result).toContain('\x1b[1m')
1309
+ expect(result).toContain('\x1b[31m')
1310
+ // Only one reset at the end
1311
+ expect(result.match(/\x1b\[0m/g)?.length).toBe(1)
1312
+ })
1313
+ })
1314
+ })
1315
+
1316
+ // ============================================================================
1317
+ // Integration with Theme System
1318
+ // ============================================================================
1319
+
1320
+ describe('renderANSI theme integration', () => {
1321
+ it('accepts theme colors from createTerminalTheme', async () => {
1322
+ const { renderANSI } = await import('../../renderers/ansi')
1323
+ const { createTerminalTheme } = await import('../../theme')
1324
+
1325
+ const theme = createTerminalTheme({ mode: 'dark' })
1326
+
1327
+ const node: UINode = {
1328
+ type: 'text',
1329
+ text: 'Themed',
1330
+ props: { color: 'primary' },
1331
+ }
1332
+
1333
+ // Should be able to pass theme colors somehow
1334
+ const result = renderANSI(node, { theme: 'dark' })
1335
+ expect(result).toContain('Themed')
1336
+ expect(result).toContain('\x1b[')
1337
+ })
1338
+
1339
+ it('uses theme error color for error variant badge', async () => {
1340
+ const { renderANSI } = await import('../../renderers/ansi')
1341
+
1342
+ const node: UINode = {
1343
+ type: 'badge',
1344
+ props: { variant: 'error' },
1345
+ text: 'Error',
1346
+ }
1347
+
1348
+ const result = renderANSI(node)
1349
+ // Should use red-ish error color from theme
1350
+ expect(result).toMatch(/\x1b\[.*31m|\x1b\[.*91m|\x1b\[38;5;\d+m|\x1b\[38;2;\d+;\d+;\d+m/)
1351
+ })
1352
+
1353
+ it('uses theme success color for success variant badge', async () => {
1354
+ const { renderANSI } = await import('../../renderers/ansi')
1355
+
1356
+ const node: UINode = {
1357
+ type: 'badge',
1358
+ props: { variant: 'success' },
1359
+ text: 'Success',
1360
+ }
1361
+
1362
+ const result = renderANSI(node)
1363
+ // Should use green-ish success color from theme
1364
+ expect(result).toMatch(/\x1b\[.*32m|\x1b\[.*92m|\x1b\[38;5;\d+m|\x1b\[38;2;\d+;\d+;\d+m/)
1365
+ })
1366
+ })