@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,1307 @@
1
+ /**
2
+ * @mdxui/terminal UNICODE Renderer Tests (RED phase)
3
+ *
4
+ * TDD RED Phase: These tests define the contract for the UNICODE tier renderer.
5
+ * The UNICODE renderer uses unicode box-drawing characters (─, │, ┌, ┐, └, ┘, etc.)
6
+ * for prettier terminal output without colors.
7
+ *
8
+ * Part of the Universal Terminal UI 6-tier system:
9
+ * - TEXT: Plain text without formatting
10
+ * - MARKDOWN: Markdown syntax for simple formatting
11
+ * - ASCII: ASCII art (+, -, |, etc.)
12
+ * - UNICODE: Unicode box drawing (─, │, ┌, ┐, └, ┘) <-- This tier
13
+ * - ANSI: Full ANSI escape sequences for colors/styles
14
+ * - INTERACTIVE: Full interactive terminal UI with input handling
15
+ *
16
+ * NOTE: These tests are expected to FAIL until implementation is complete.
17
+ * Run: pnpm --filter @mdxui/terminal test
18
+ */
19
+ import { describe, it, expect, beforeEach } from 'vitest'
20
+
21
+ // ============================================================================
22
+ // This import WILL FAIL until src/renderers/unicode.ts is implemented
23
+ // ============================================================================
24
+ import { renderUnicode } from '../../renderers/unicode'
25
+ import type { UINode, RenderContext, ThemeTokens } from '../../core/types'
26
+
27
+ // ============================================================================
28
+ // Test Helpers
29
+ // ============================================================================
30
+
31
+ /**
32
+ * Unicode box-drawing characters used by this renderer
33
+ */
34
+ const UNICODE_CHARS = {
35
+ // Single box drawing
36
+ topLeft: '┌',
37
+ topRight: '┐',
38
+ bottomLeft: '└',
39
+ bottomRight: '┘',
40
+ horizontal: '─',
41
+ vertical: '│',
42
+
43
+ // T-junctions
44
+ teeLeft: '├',
45
+ teeRight: '┤',
46
+ teeTop: '┬',
47
+ teeBottom: '┴',
48
+ crossJunction: '┼',
49
+
50
+ // Double box drawing
51
+ doubleTopLeft: '╔',
52
+ doubleTopRight: '╗',
53
+ doubleBottomLeft: '╚',
54
+ doubleBottomRight: '╝',
55
+ doubleHorizontal: '═',
56
+ doubleVertical: '║',
57
+
58
+ // Rounded corners
59
+ roundedTopLeft: '╭',
60
+ roundedTopRight: '╮',
61
+ roundedBottomLeft: '╰',
62
+ roundedBottomRight: '╯',
63
+
64
+ // Bullets and list markers
65
+ bullet: '•',
66
+ hollowBullet: '◦',
67
+ squareBullet: '▪',
68
+ triangleRight: '▸',
69
+ triangleDown: '▾',
70
+ checkmark: '✓',
71
+ crossMark: '✗',
72
+ arrowRight: '→',
73
+ arrowDown: '↓',
74
+
75
+ // Progress bar characters
76
+ progressFull: '▓',
77
+ progressEmpty: '░',
78
+ progressHalf: '▒',
79
+
80
+ // Dividers
81
+ ellipsis: '…',
82
+ middleDot: '·',
83
+ } as const
84
+
85
+ /**
86
+ * ASCII fallback characters - used to verify UNICODE is NOT using these
87
+ */
88
+ const ASCII_CHARS = {
89
+ topLeft: '+',
90
+ topRight: '+',
91
+ bottomLeft: '+',
92
+ bottomRight: '+',
93
+ horizontal: '-',
94
+ vertical: '|',
95
+ bullet: '*',
96
+ progressFull: '#',
97
+ progressEmpty: '-',
98
+ } as const
99
+
100
+ /**
101
+ * Create a default theme for testing
102
+ */
103
+ function createTestTheme(): ThemeTokens {
104
+ return {
105
+ primary: '',
106
+ secondary: '',
107
+ muted: '',
108
+ foreground: '',
109
+ background: '',
110
+ border: '',
111
+ success: '',
112
+ warning: '',
113
+ error: '',
114
+ info: '',
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Create a default render context for UNICODE tier
120
+ */
121
+ function createTestContext(overrides?: Partial<RenderContext>): RenderContext {
122
+ return {
123
+ tier: 'unicode',
124
+ width: 80,
125
+ height: 24,
126
+ depth: 0,
127
+ theme: createTestTheme(),
128
+ interactive: false,
129
+ ...overrides,
130
+ }
131
+ }
132
+
133
+ // ============================================================================
134
+ // renderUnicode Function Signature Tests
135
+ // ============================================================================
136
+
137
+ describe('renderUnicode', () => {
138
+ describe('function signature', () => {
139
+ it('exports renderUnicode function', () => {
140
+ expect(typeof renderUnicode).toBe('function')
141
+ })
142
+
143
+ it('accepts UINode as first argument', () => {
144
+ const node: UINode = { type: 'text', props: { content: 'Hello' } }
145
+ const result = renderUnicode(node)
146
+ expect(result).toBeDefined()
147
+ })
148
+
149
+ it('accepts optional RenderContext as second argument', () => {
150
+ const node: UINode = { type: 'text', props: { content: 'Hello' } }
151
+ const context = createTestContext()
152
+ const result = renderUnicode(node, context)
153
+ expect(result).toBeDefined()
154
+ })
155
+
156
+ it('returns a string', () => {
157
+ const node: UINode = { type: 'text', props: { content: 'Hello' } }
158
+ const result = renderUnicode(node)
159
+ expect(typeof result).toBe('string')
160
+ })
161
+ })
162
+
163
+ // ============================================================================
164
+ // Box Drawing Tests - Single Border Style
165
+ // ============================================================================
166
+
167
+ describe('box drawing - single style', () => {
168
+ it('renders simple box with single border corners', () => {
169
+ const node: UINode = {
170
+ type: 'box',
171
+ props: { border: 'single', width: 10, height: 3 },
172
+ }
173
+
174
+ const result = renderUnicode(node)
175
+
176
+ // Should contain unicode box corners
177
+ expect(result).toContain(UNICODE_CHARS.topLeft)
178
+ expect(result).toContain(UNICODE_CHARS.topRight)
179
+ expect(result).toContain(UNICODE_CHARS.bottomLeft)
180
+ expect(result).toContain(UNICODE_CHARS.bottomRight)
181
+ })
182
+
183
+ it('renders horizontal lines with unicode dash', () => {
184
+ const node: UINode = {
185
+ type: 'box',
186
+ props: { border: 'single', width: 10, height: 3 },
187
+ }
188
+
189
+ const result = renderUnicode(node)
190
+
191
+ // Should contain unicode horizontal line character
192
+ expect(result).toContain(UNICODE_CHARS.horizontal)
193
+ // Should NOT contain ASCII dash for borders
194
+ expect(result).not.toMatch(/^\+-+\+$/m) // No ASCII box top/bottom
195
+ })
196
+
197
+ it('renders vertical lines with unicode pipe', () => {
198
+ const node: UINode = {
199
+ type: 'box',
200
+ props: { border: 'single', width: 10, height: 5 },
201
+ }
202
+
203
+ const result = renderUnicode(node)
204
+
205
+ // Should contain unicode vertical line character
206
+ expect(result).toContain(UNICODE_CHARS.vertical)
207
+ })
208
+
209
+ it('renders complete single-border box structure', () => {
210
+ const node: UINode = {
211
+ type: 'box',
212
+ props: { border: 'single', width: 6, height: 3 },
213
+ }
214
+
215
+ const result = renderUnicode(node)
216
+ const lines = result.split('\n')
217
+
218
+ // Top line should be: ┌────┐
219
+ expect(lines[0]).toMatch(/^┌─+┐$/)
220
+ // Middle lines should be: │ │
221
+ expect(lines[1]).toMatch(/^│.+│$/)
222
+ // Bottom line should be: └────┘
223
+ expect(lines[2]).toMatch(/^└─+┘$/)
224
+ })
225
+
226
+ it('box with content displays content inside', () => {
227
+ const node: UINode = {
228
+ type: 'box',
229
+ props: { border: 'single' },
230
+ children: [{ type: 'text', props: { content: 'Hello' } }],
231
+ }
232
+
233
+ const result = renderUnicode(node)
234
+
235
+ expect(result).toContain('Hello')
236
+ expect(result).toContain(UNICODE_CHARS.vertical)
237
+ })
238
+ })
239
+
240
+ // ============================================================================
241
+ // Box Drawing Tests - Double Border Style
242
+ // ============================================================================
243
+
244
+ describe('box drawing - double style', () => {
245
+ it('renders double border corners', () => {
246
+ const node: UINode = {
247
+ type: 'box',
248
+ props: { border: 'double', width: 10, height: 3 },
249
+ }
250
+
251
+ const result = renderUnicode(node)
252
+
253
+ expect(result).toContain(UNICODE_CHARS.doubleTopLeft)
254
+ expect(result).toContain(UNICODE_CHARS.doubleTopRight)
255
+ expect(result).toContain(UNICODE_CHARS.doubleBottomLeft)
256
+ expect(result).toContain(UNICODE_CHARS.doubleBottomRight)
257
+ })
258
+
259
+ it('renders double horizontal lines', () => {
260
+ const node: UINode = {
261
+ type: 'box',
262
+ props: { border: 'double', width: 10, height: 3 },
263
+ }
264
+
265
+ const result = renderUnicode(node)
266
+
267
+ expect(result).toContain(UNICODE_CHARS.doubleHorizontal)
268
+ })
269
+
270
+ it('renders double vertical lines', () => {
271
+ const node: UINode = {
272
+ type: 'box',
273
+ props: { border: 'double', width: 10, height: 5 },
274
+ }
275
+
276
+ const result = renderUnicode(node)
277
+
278
+ expect(result).toContain(UNICODE_CHARS.doubleVertical)
279
+ })
280
+
281
+ it('renders complete double-border box structure', () => {
282
+ const node: UINode = {
283
+ type: 'box',
284
+ props: { border: 'double', width: 6, height: 3 },
285
+ }
286
+
287
+ const result = renderUnicode(node)
288
+ const lines = result.split('\n')
289
+
290
+ // Top line should be: ╔════╗
291
+ expect(lines[0]).toMatch(/^╔═+╗$/)
292
+ // Middle lines should be: ║ ║
293
+ expect(lines[1]).toMatch(/^║.+║$/)
294
+ // Bottom line should be: ╚════╝
295
+ expect(lines[2]).toMatch(/^╚═+╝$/)
296
+ })
297
+ })
298
+
299
+ // ============================================================================
300
+ // Box Drawing Tests - Rounded Border Style
301
+ // ============================================================================
302
+
303
+ describe('box drawing - rounded style', () => {
304
+ it('renders rounded border corners', () => {
305
+ const node: UINode = {
306
+ type: 'box',
307
+ props: { border: 'rounded', width: 10, height: 3 },
308
+ }
309
+
310
+ const result = renderUnicode(node)
311
+
312
+ expect(result).toContain(UNICODE_CHARS.roundedTopLeft)
313
+ expect(result).toContain(UNICODE_CHARS.roundedTopRight)
314
+ expect(result).toContain(UNICODE_CHARS.roundedBottomLeft)
315
+ expect(result).toContain(UNICODE_CHARS.roundedBottomRight)
316
+ })
317
+
318
+ it('uses single line characters with rounded corners', () => {
319
+ const node: UINode = {
320
+ type: 'box',
321
+ props: { border: 'rounded', width: 10, height: 3 },
322
+ }
323
+
324
+ const result = renderUnicode(node)
325
+
326
+ // Rounded uses single-line horizontal/vertical but rounded corners
327
+ expect(result).toContain(UNICODE_CHARS.horizontal)
328
+ expect(result).toContain(UNICODE_CHARS.roundedTopLeft)
329
+ })
330
+
331
+ it('renders complete rounded-border box structure', () => {
332
+ const node: UINode = {
333
+ type: 'box',
334
+ props: { border: 'rounded', width: 6, height: 3 },
335
+ }
336
+
337
+ const result = renderUnicode(node)
338
+ const lines = result.split('\n')
339
+
340
+ // Top line should be: ╭────╮
341
+ expect(lines[0]).toMatch(/^╭─+╮$/)
342
+ // Middle lines should be: │ │
343
+ expect(lines[1]).toMatch(/^│.+│$/)
344
+ // Bottom line should be: ╰────╯
345
+ expect(lines[2]).toMatch(/^╰─+╯$/)
346
+ })
347
+ })
348
+
349
+ // ============================================================================
350
+ // Text Rendering Tests
351
+ // ============================================================================
352
+
353
+ describe('text rendering', () => {
354
+ it('renders plain text content', () => {
355
+ const node: UINode = {
356
+ type: 'text',
357
+ props: { content: 'Hello, World!' },
358
+ }
359
+
360
+ const result = renderUnicode(node)
361
+
362
+ expect(result).toBe('Hello, World!')
363
+ })
364
+
365
+ it('renders empty text as empty string', () => {
366
+ const node: UINode = {
367
+ type: 'text',
368
+ props: { content: '' },
369
+ }
370
+
371
+ const result = renderUnicode(node)
372
+
373
+ expect(result).toBe('')
374
+ })
375
+
376
+ it('renders multiline text preserving newlines', () => {
377
+ const node: UINode = {
378
+ type: 'text',
379
+ props: { content: 'Line 1\nLine 2\nLine 3' },
380
+ }
381
+
382
+ const result = renderUnicode(node)
383
+
384
+ expect(result).toContain('Line 1')
385
+ expect(result).toContain('Line 2')
386
+ expect(result).toContain('Line 3')
387
+ expect(result.split('\n')).toHaveLength(3)
388
+ })
389
+
390
+ it('ignores style props for unicode tier (no ANSI)', () => {
391
+ const node: UINode = {
392
+ type: 'text',
393
+ props: {
394
+ content: 'Styled text',
395
+ bold: true,
396
+ color: 'red',
397
+ },
398
+ }
399
+
400
+ const result = renderUnicode(node)
401
+
402
+ // Should render content but NO ANSI escape codes
403
+ expect(result).toContain('Styled text')
404
+ expect(result).not.toMatch(/\x1b\[/)
405
+ })
406
+ })
407
+
408
+ // ============================================================================
409
+ // List Rendering Tests
410
+ // ============================================================================
411
+
412
+ describe('list rendering', () => {
413
+ it('renders unordered list with unicode bullets', () => {
414
+ const node: UINode = {
415
+ type: 'list',
416
+ props: { style: 'unordered' },
417
+ children: [
418
+ { type: 'list-item', props: { content: 'First item' } },
419
+ { type: 'list-item', props: { content: 'Second item' } },
420
+ { type: 'list-item', props: { content: 'Third item' } },
421
+ ],
422
+ }
423
+
424
+ const result = renderUnicode(node)
425
+
426
+ expect(result).toContain(UNICODE_CHARS.bullet)
427
+ expect(result).toContain('First item')
428
+ expect(result).toContain('Second item')
429
+ expect(result).toContain('Third item')
430
+ })
431
+
432
+ it('does NOT use ASCII asterisk for bullets', () => {
433
+ const node: UINode = {
434
+ type: 'list',
435
+ props: { style: 'unordered' },
436
+ children: [
437
+ { type: 'list-item', props: { content: 'Item' } },
438
+ ],
439
+ }
440
+
441
+ const result = renderUnicode(node)
442
+
443
+ // Should use unicode bullet, not ASCII asterisk
444
+ expect(result).toContain(UNICODE_CHARS.bullet)
445
+ expect(result).not.toMatch(/^\s*\*\s/m)
446
+ })
447
+
448
+ it('renders ordered list with numbers', () => {
449
+ const node: UINode = {
450
+ type: 'list',
451
+ props: { style: 'ordered' },
452
+ children: [
453
+ { type: 'list-item', props: { content: 'First' } },
454
+ { type: 'list-item', props: { content: 'Second' } },
455
+ { type: 'list-item', props: { content: 'Third' } },
456
+ ],
457
+ }
458
+
459
+ const result = renderUnicode(node)
460
+
461
+ expect(result).toMatch(/1\.\s*First/)
462
+ expect(result).toMatch(/2\.\s*Second/)
463
+ expect(result).toMatch(/3\.\s*Third/)
464
+ })
465
+
466
+ it('renders nested list with hollow bullets', () => {
467
+ const node: UINode = {
468
+ type: 'list',
469
+ props: { style: 'unordered' },
470
+ children: [
471
+ {
472
+ type: 'list-item',
473
+ props: { content: 'Parent' },
474
+ children: [
475
+ {
476
+ type: 'list',
477
+ props: { style: 'unordered' },
478
+ children: [
479
+ { type: 'list-item', props: { content: 'Child' } },
480
+ ],
481
+ },
482
+ ],
483
+ },
484
+ ],
485
+ }
486
+
487
+ const result = renderUnicode(node)
488
+
489
+ // Parent uses solid bullet, nested uses hollow
490
+ expect(result).toContain(UNICODE_CHARS.bullet)
491
+ expect(result).toContain(UNICODE_CHARS.hollowBullet)
492
+ })
493
+
494
+ it('renders checklist with checkmark and cross', () => {
495
+ const node: UINode = {
496
+ type: 'list',
497
+ props: { style: 'checklist' },
498
+ children: [
499
+ { type: 'list-item', props: { content: 'Done task', checked: true } },
500
+ { type: 'list-item', props: { content: 'Todo task', checked: false } },
501
+ ],
502
+ }
503
+
504
+ const result = renderUnicode(node)
505
+
506
+ expect(result).toContain(UNICODE_CHARS.checkmark)
507
+ expect(result).toContain('Done task')
508
+ // Should show unchecked indicator
509
+ expect(result).toMatch(/[☐◻□]/)
510
+ })
511
+ })
512
+
513
+ // ============================================================================
514
+ // Progress Bar Tests
515
+ // ============================================================================
516
+
517
+ describe('progress bar', () => {
518
+ it('renders progress bar with unicode block characters', () => {
519
+ const node: UINode = {
520
+ type: 'progress',
521
+ props: { value: 50, max: 100, width: 10 },
522
+ }
523
+
524
+ const result = renderUnicode(node)
525
+
526
+ expect(result).toContain(UNICODE_CHARS.progressFull)
527
+ expect(result).toContain(UNICODE_CHARS.progressEmpty)
528
+ })
529
+
530
+ it('does NOT use ASCII hash for progress', () => {
531
+ const node: UINode = {
532
+ type: 'progress',
533
+ props: { value: 50, max: 100, width: 10 },
534
+ }
535
+
536
+ const result = renderUnicode(node)
537
+
538
+ // Should NOT use ASCII progress characters
539
+ expect(result).not.toMatch(/#+/)
540
+ expect(result).not.toMatch(/-{2,}/)
541
+ })
542
+
543
+ it('renders 0% progress as all empty', () => {
544
+ const node: UINode = {
545
+ type: 'progress',
546
+ props: { value: 0, max: 100, width: 10 },
547
+ }
548
+
549
+ const result = renderUnicode(node)
550
+
551
+ expect(result).toContain(UNICODE_CHARS.progressEmpty)
552
+ expect(result).not.toContain(UNICODE_CHARS.progressFull)
553
+ })
554
+
555
+ it('renders 100% progress as all full', () => {
556
+ const node: UINode = {
557
+ type: 'progress',
558
+ props: { value: 100, max: 100, width: 10 },
559
+ }
560
+
561
+ const result = renderUnicode(node)
562
+
563
+ expect(result).toContain(UNICODE_CHARS.progressFull)
564
+ expect(result).not.toContain(UNICODE_CHARS.progressEmpty)
565
+ })
566
+
567
+ it('renders partial progress with half block', () => {
568
+ const node: UINode = {
569
+ type: 'progress',
570
+ props: { value: 55, max: 100, width: 10 },
571
+ }
572
+
573
+ const result = renderUnicode(node)
574
+
575
+ // At 55% with width 10, we have 5.5 filled blocks
576
+ // Implementation should use half block for the partial
577
+ expect(result).toContain(UNICODE_CHARS.progressFull)
578
+ expect(result).toContain(UNICODE_CHARS.progressEmpty)
579
+ // May contain half block for partial fill
580
+ expect(result.match(new RegExp(`[${UNICODE_CHARS.progressFull}${UNICODE_CHARS.progressHalf}${UNICODE_CHARS.progressEmpty}]`, 'g'))).toBeTruthy()
581
+ })
582
+ })
583
+
584
+ // ============================================================================
585
+ // Table Rendering Tests
586
+ // ============================================================================
587
+
588
+ describe('table rendering', () => {
589
+ it('renders table with unicode borders', () => {
590
+ const node: UINode = {
591
+ type: 'table',
592
+ props: {
593
+ columns: [
594
+ { key: 'name', header: 'Name', width: 10 },
595
+ { key: 'age', header: 'Age', width: 5 },
596
+ ],
597
+ },
598
+ data: [
599
+ { name: 'Alice', age: 30 },
600
+ { name: 'Bob', age: 25 },
601
+ ],
602
+ }
603
+
604
+ const result = renderUnicode(node)
605
+
606
+ // Should contain unicode box characters
607
+ expect(result).toContain(UNICODE_CHARS.horizontal)
608
+ expect(result).toContain(UNICODE_CHARS.vertical)
609
+ })
610
+
611
+ it('renders table header separator with T-junctions', () => {
612
+ const node: UINode = {
613
+ type: 'table',
614
+ props: {
615
+ columns: [
616
+ { key: 'col1', header: 'Column 1', width: 10 },
617
+ { key: 'col2', header: 'Column 2', width: 10 },
618
+ ],
619
+ },
620
+ data: [{ col1: 'a', col2: 'b' }],
621
+ }
622
+
623
+ const result = renderUnicode(node)
624
+
625
+ // Header separator should use T-junctions
626
+ expect(result).toContain(UNICODE_CHARS.teeTop)
627
+ expect(result).toContain(UNICODE_CHARS.teeBottom)
628
+ })
629
+
630
+ it('renders table corner intersections', () => {
631
+ const node: UINode = {
632
+ type: 'table',
633
+ props: {
634
+ columns: [
635
+ { key: 'a', header: 'A', width: 5 },
636
+ { key: 'b', header: 'B', width: 5 },
637
+ ],
638
+ },
639
+ data: [{ a: '1', b: '2' }],
640
+ }
641
+
642
+ const result = renderUnicode(node)
643
+
644
+ expect(result).toContain(UNICODE_CHARS.topLeft)
645
+ expect(result).toContain(UNICODE_CHARS.topRight)
646
+ expect(result).toContain(UNICODE_CHARS.bottomLeft)
647
+ expect(result).toContain(UNICODE_CHARS.bottomRight)
648
+ })
649
+
650
+ it('renders table row separators with cross junctions', () => {
651
+ const node: UINode = {
652
+ type: 'table',
653
+ props: {
654
+ columns: [
655
+ { key: 'a', header: 'A', width: 5 },
656
+ { key: 'b', header: 'B', width: 5 },
657
+ ],
658
+ rowSeparators: true,
659
+ },
660
+ data: [
661
+ { a: '1', b: '2' },
662
+ { a: '3', b: '4' },
663
+ ],
664
+ }
665
+
666
+ const result = renderUnicode(node)
667
+
668
+ // Row separators should include cross junctions
669
+ expect(result).toContain(UNICODE_CHARS.crossJunction)
670
+ })
671
+
672
+ it('does NOT use ASCII characters for table borders', () => {
673
+ const node: UINode = {
674
+ type: 'table',
675
+ props: {
676
+ columns: [{ key: 'a', header: 'A', width: 5 }],
677
+ },
678
+ data: [{ a: '1' }],
679
+ }
680
+
681
+ const result = renderUnicode(node)
682
+
683
+ // Should NOT have ASCII box drawing
684
+ expect(result).not.toMatch(/^\+-+\+$/m)
685
+ expect(result).not.toMatch(/^\|.+\|$/m)
686
+ })
687
+ })
688
+
689
+ // ============================================================================
690
+ // Panel/Card Rendering Tests
691
+ // ============================================================================
692
+
693
+ describe('panel rendering', () => {
694
+ it('renders panel with title bar', () => {
695
+ const node: UINode = {
696
+ type: 'panel',
697
+ props: { title: 'My Panel', width: 20 },
698
+ children: [{ type: 'text', props: { content: 'Content' } }],
699
+ }
700
+
701
+ const result = renderUnicode(node)
702
+
703
+ expect(result).toContain('My Panel')
704
+ expect(result).toContain('Content')
705
+ expect(result).toContain(UNICODE_CHARS.topLeft)
706
+ })
707
+
708
+ it('renders panel title with unicode divider', () => {
709
+ const node: UINode = {
710
+ type: 'panel',
711
+ props: { title: 'Title', width: 20 },
712
+ children: [{ type: 'text', props: { content: 'Body' } }],
713
+ }
714
+
715
+ const result = renderUnicode(node)
716
+
717
+ // Title should be separated from body
718
+ expect(result).toContain(UNICODE_CHARS.teeLeft)
719
+ expect(result).toContain(UNICODE_CHARS.teeRight)
720
+ })
721
+ })
722
+
723
+ // ============================================================================
724
+ // Divider/Separator Tests
725
+ // ============================================================================
726
+
727
+ describe('divider rendering', () => {
728
+ it('renders horizontal divider with unicode line', () => {
729
+ const node: UINode = {
730
+ type: 'divider',
731
+ props: { width: 20 },
732
+ }
733
+
734
+ const result = renderUnicode(node)
735
+
736
+ expect(result).toContain(UNICODE_CHARS.horizontal)
737
+ expect(result.length).toBeGreaterThanOrEqual(20)
738
+ })
739
+
740
+ it('renders divider with label', () => {
741
+ const node: UINode = {
742
+ type: 'divider',
743
+ props: { label: 'Section', width: 30 },
744
+ }
745
+
746
+ const result = renderUnicode(node)
747
+
748
+ expect(result).toContain('Section')
749
+ expect(result).toContain(UNICODE_CHARS.horizontal)
750
+ })
751
+
752
+ it('does NOT use ASCII dash for divider', () => {
753
+ const node: UINode = {
754
+ type: 'divider',
755
+ props: { width: 20 },
756
+ }
757
+
758
+ const result = renderUnicode(node)
759
+
760
+ // Should not be a line of dashes
761
+ expect(result).not.toMatch(/^-+$/)
762
+ })
763
+ })
764
+
765
+ // ============================================================================
766
+ // Spinner/Loading Tests
767
+ // ============================================================================
768
+
769
+ describe('spinner rendering', () => {
770
+ it('renders spinner with unicode braille pattern', () => {
771
+ const node: UINode = {
772
+ type: 'spinner',
773
+ props: { frame: 0 },
774
+ }
775
+
776
+ const result = renderUnicode(node)
777
+
778
+ // Should contain a unicode spinner character (braille dots, etc.)
779
+ // Common unicode spinners: ⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏
780
+ expect(result).toMatch(/[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏⣾⣽⣻⢿⡿⣟⣯⣷]/)
781
+ })
782
+
783
+ it('renders spinner with label', () => {
784
+ const node: UINode = {
785
+ type: 'spinner',
786
+ props: { frame: 0, label: 'Loading...' },
787
+ }
788
+
789
+ const result = renderUnicode(node)
790
+
791
+ expect(result).toContain('Loading...')
792
+ })
793
+
794
+ it('does NOT use ASCII spinner characters', () => {
795
+ const node: UINode = {
796
+ type: 'spinner',
797
+ props: { frame: 0 },
798
+ }
799
+
800
+ const result = renderUnicode(node)
801
+
802
+ // Should NOT use ASCII spinner like | / - \
803
+ expect(result).not.toMatch(/^[|\\/-]$/)
804
+ })
805
+ })
806
+
807
+ // ============================================================================
808
+ // Badge/Status Indicator Tests
809
+ // ============================================================================
810
+
811
+ describe('badge rendering', () => {
812
+ it('renders badge with unicode brackets', () => {
813
+ const node: UINode = {
814
+ type: 'badge',
815
+ props: { content: 'NEW', variant: 'info' },
816
+ }
817
+
818
+ const result = renderUnicode(node)
819
+
820
+ expect(result).toContain('NEW')
821
+ // May use unicode brackets or rounded corners
822
+ expect(result).toMatch(/[【】「」『』〖〗\[〔〕]/)
823
+ })
824
+
825
+ it('renders status indicator with unicode symbols', () => {
826
+ const node: UINode = {
827
+ type: 'badge',
828
+ props: { content: 'Success', variant: 'success' },
829
+ }
830
+
831
+ const result = renderUnicode(node)
832
+
833
+ // Success might include checkmark
834
+ expect(result).toContain('Success')
835
+ })
836
+ })
837
+
838
+ // ============================================================================
839
+ // Tree/Hierarchy Rendering Tests
840
+ // ============================================================================
841
+
842
+ describe('tree rendering', () => {
843
+ it('renders tree structure with unicode connectors', () => {
844
+ const node: UINode = {
845
+ type: 'tree',
846
+ props: {},
847
+ children: [
848
+ {
849
+ type: 'tree-item',
850
+ props: { label: 'Root' },
851
+ children: [
852
+ { type: 'tree-item', props: { label: 'Child 1' } },
853
+ { type: 'tree-item', props: { label: 'Child 2' } },
854
+ ],
855
+ },
856
+ ],
857
+ }
858
+
859
+ const result = renderUnicode(node)
860
+
861
+ // Should use unicode tree connectors
862
+ expect(result).toContain('Root')
863
+ expect(result).toContain('Child 1')
864
+ expect(result).toContain('Child 2')
865
+ // Tree connectors: ├── └── │
866
+ expect(result).toContain(UNICODE_CHARS.teeLeft)
867
+ expect(result).toContain(UNICODE_CHARS.bottomLeft)
868
+ })
869
+
870
+ it('renders last child with corner connector', () => {
871
+ const node: UINode = {
872
+ type: 'tree',
873
+ props: {},
874
+ children: [
875
+ {
876
+ type: 'tree-item',
877
+ props: { label: 'Parent' },
878
+ children: [
879
+ { type: 'tree-item', props: { label: 'Last child' } },
880
+ ],
881
+ },
882
+ ],
883
+ }
884
+
885
+ const result = renderUnicode(node)
886
+
887
+ // Last child should use └ not ├
888
+ expect(result).toContain(UNICODE_CHARS.bottomLeft)
889
+ })
890
+
891
+ it('does NOT use ASCII tree characters', () => {
892
+ const node: UINode = {
893
+ type: 'tree',
894
+ props: {},
895
+ children: [
896
+ {
897
+ type: 'tree-item',
898
+ props: { label: 'Root' },
899
+ children: [
900
+ { type: 'tree-item', props: { label: 'Child' } },
901
+ ],
902
+ },
903
+ ],
904
+ }
905
+
906
+ const result = renderUnicode(node)
907
+
908
+ // Should NOT use ASCII +-- or |-- patterns
909
+ expect(result).not.toMatch(/\+--/)
910
+ expect(result).not.toMatch(/\|--/)
911
+ })
912
+ })
913
+
914
+ // ============================================================================
915
+ // Breadcrumb Rendering Tests
916
+ // ============================================================================
917
+
918
+ describe('breadcrumb rendering', () => {
919
+ it('renders breadcrumb with unicode arrow separator', () => {
920
+ const node: UINode = {
921
+ type: 'breadcrumb',
922
+ props: {},
923
+ children: [
924
+ { type: 'breadcrumb-item', props: { label: 'Home' } },
925
+ { type: 'breadcrumb-item', props: { label: 'Products' } },
926
+ { type: 'breadcrumb-item', props: { label: 'Item' } },
927
+ ],
928
+ }
929
+
930
+ const result = renderUnicode(node)
931
+
932
+ expect(result).toContain('Home')
933
+ expect(result).toContain('Products')
934
+ expect(result).toContain('Item')
935
+ // Should use unicode separator like → or ›
936
+ expect(result).toMatch(/[→›»▸]/)
937
+ })
938
+
939
+ it('does NOT use ASCII greater-than for separator', () => {
940
+ const node: UINode = {
941
+ type: 'breadcrumb',
942
+ props: {},
943
+ children: [
944
+ { type: 'breadcrumb-item', props: { label: 'A' } },
945
+ { type: 'breadcrumb-item', props: { label: 'B' } },
946
+ ],
947
+ }
948
+
949
+ const result = renderUnicode(node)
950
+
951
+ // Should NOT use plain > as separator
952
+ expect(result).not.toMatch(/A\s*>\s*B/)
953
+ })
954
+ })
955
+
956
+ // ============================================================================
957
+ // Tooltip/Info Rendering Tests
958
+ // ============================================================================
959
+
960
+ describe('tooltip rendering', () => {
961
+ it('renders tooltip with unicode pointer', () => {
962
+ const node: UINode = {
963
+ type: 'tooltip',
964
+ props: { content: 'Helpful tip', position: 'top' },
965
+ }
966
+
967
+ const result = renderUnicode(node)
968
+
969
+ expect(result).toContain('Helpful tip')
970
+ // Should have unicode pointer like ▲ ▼ ◀ ▶
971
+ expect(result).toMatch(/[▲▼◀▶△▽◁▷]/)
972
+ })
973
+ })
974
+
975
+ // ============================================================================
976
+ // Edge Cases and Error Handling
977
+ // ============================================================================
978
+
979
+ describe('edge cases', () => {
980
+ it('handles empty children array', () => {
981
+ const node: UINode = {
982
+ type: 'box',
983
+ props: { border: 'single' },
984
+ children: [],
985
+ }
986
+
987
+ const result = renderUnicode(node)
988
+
989
+ expect(result).toBeDefined()
990
+ expect(result).toContain(UNICODE_CHARS.topLeft)
991
+ })
992
+
993
+ it('handles undefined children', () => {
994
+ const node: UINode = {
995
+ type: 'box',
996
+ props: { border: 'single' },
997
+ }
998
+
999
+ const result = renderUnicode(node)
1000
+
1001
+ expect(result).toBeDefined()
1002
+ })
1003
+
1004
+ it('handles zero-width box gracefully', () => {
1005
+ const node: UINode = {
1006
+ type: 'box',
1007
+ props: { border: 'single', width: 0, height: 3 },
1008
+ }
1009
+
1010
+ const result = renderUnicode(node)
1011
+
1012
+ // Should either render minimum size or empty string
1013
+ expect(typeof result).toBe('string')
1014
+ })
1015
+
1016
+ it('handles zero-height box gracefully', () => {
1017
+ const node: UINode = {
1018
+ type: 'box',
1019
+ props: { border: 'single', width: 10, height: 0 },
1020
+ }
1021
+
1022
+ const result = renderUnicode(node)
1023
+
1024
+ expect(typeof result).toBe('string')
1025
+ })
1026
+
1027
+ it('handles very long text', () => {
1028
+ const longText = 'A'.repeat(1000)
1029
+ const node: UINode = {
1030
+ type: 'text',
1031
+ props: { content: longText },
1032
+ }
1033
+
1034
+ const result = renderUnicode(node)
1035
+
1036
+ expect(result).toContain('A')
1037
+ })
1038
+
1039
+ it('handles special unicode characters in content', () => {
1040
+ const node: UINode = {
1041
+ type: 'text',
1042
+ props: { content: 'Emoji: 🎉 and symbols: ∞ ≠ ≤' },
1043
+ }
1044
+
1045
+ const result = renderUnicode(node)
1046
+
1047
+ expect(result).toContain('🎉')
1048
+ expect(result).toContain('∞')
1049
+ })
1050
+
1051
+ it('handles RTL text', () => {
1052
+ const node: UINode = {
1053
+ type: 'text',
1054
+ props: { content: 'مرحبا' }, // Arabic "Hello"
1055
+ }
1056
+
1057
+ const result = renderUnicode(node)
1058
+
1059
+ expect(result).toContain('مرحبا')
1060
+ })
1061
+
1062
+ it('handles unknown node type gracefully', () => {
1063
+ const node: UINode = {
1064
+ type: 'unknown-type',
1065
+ props: { content: 'Test' },
1066
+ }
1067
+
1068
+ // Should not throw, may return empty or placeholder
1069
+ expect(() => renderUnicode(node)).not.toThrow()
1070
+ })
1071
+ })
1072
+
1073
+ // ============================================================================
1074
+ // Context Handling Tests
1075
+ // ============================================================================
1076
+
1077
+ describe('context handling', () => {
1078
+ it('respects width from context', () => {
1079
+ const node: UINode = {
1080
+ type: 'divider',
1081
+ props: {},
1082
+ }
1083
+
1084
+ const context = createTestContext({ width: 40 })
1085
+ const result = renderUnicode(node, context)
1086
+
1087
+ // Divider should respect context width
1088
+ expect(result.length).toBeLessThanOrEqual(40)
1089
+ })
1090
+
1091
+ it('increases depth for nested children', () => {
1092
+ const node: UINode = {
1093
+ type: 'box',
1094
+ props: {},
1095
+ children: [
1096
+ {
1097
+ type: 'box',
1098
+ props: {},
1099
+ children: [{ type: 'text', props: { content: 'Deep' } }],
1100
+ },
1101
+ ],
1102
+ }
1103
+
1104
+ // This is more of an internal test - ensuring nested rendering works
1105
+ const result = renderUnicode(node)
1106
+
1107
+ expect(result).toContain('Deep')
1108
+ })
1109
+
1110
+ it('uses default context when none provided', () => {
1111
+ const node: UINode = {
1112
+ type: 'text',
1113
+ props: { content: 'Hello' },
1114
+ }
1115
+
1116
+ // Should not throw when context is omitted
1117
+ const result = renderUnicode(node)
1118
+
1119
+ expect(result).toBe('Hello')
1120
+ })
1121
+ })
1122
+
1123
+ // ============================================================================
1124
+ // Comparison with ASCII Fallback
1125
+ // ============================================================================
1126
+
1127
+ describe('ASCII fallback verification', () => {
1128
+ it('uses unicode corners, not ASCII plus signs', () => {
1129
+ const node: UINode = {
1130
+ type: 'box',
1131
+ props: { border: 'single', width: 10, height: 3 },
1132
+ }
1133
+
1134
+ const result = renderUnicode(node)
1135
+
1136
+ // Corners should be unicode, not +
1137
+ expect(result).not.toMatch(/^\+/)
1138
+ expect(result).not.toMatch(/\+$/)
1139
+ expect(result).toContain(UNICODE_CHARS.topLeft)
1140
+ })
1141
+
1142
+ it('uses unicode horizontal lines, not dashes', () => {
1143
+ const node: UINode = {
1144
+ type: 'box',
1145
+ props: { border: 'single', width: 10, height: 3 },
1146
+ }
1147
+
1148
+ const result = renderUnicode(node)
1149
+
1150
+ // Top/bottom lines should use ─ not -
1151
+ const lines = result.split('\n')
1152
+ expect(lines[0]).toContain(UNICODE_CHARS.horizontal)
1153
+ expect(lines[0]).not.toMatch(/^.+-+.+$/) // Not a row of dashes
1154
+ })
1155
+
1156
+ it('uses unicode vertical lines, not pipes', () => {
1157
+ const node: UINode = {
1158
+ type: 'box',
1159
+ props: { border: 'single', width: 10, height: 5 },
1160
+ }
1161
+
1162
+ const result = renderUnicode(node)
1163
+
1164
+ // Side lines should use │ not |
1165
+ expect(result).toContain(UNICODE_CHARS.vertical)
1166
+ // The ASCII pipe | should not appear in borders
1167
+ // (It might appear in content, so we check specific pattern)
1168
+ const lines = result.split('\n')
1169
+ for (let i = 1; i < lines.length - 1; i++) {
1170
+ expect(lines[i]).toMatch(/^│.*│$/)
1171
+ expect(lines[i]).not.toMatch(/^\|.*\|$/)
1172
+ }
1173
+ })
1174
+
1175
+ it('uses unicode bullets, not asterisks', () => {
1176
+ const node: UINode = {
1177
+ type: 'list',
1178
+ props: { style: 'unordered' },
1179
+ children: [
1180
+ { type: 'list-item', props: { content: 'Item' } },
1181
+ ],
1182
+ }
1183
+
1184
+ const result = renderUnicode(node)
1185
+
1186
+ expect(result).toContain(UNICODE_CHARS.bullet)
1187
+ expect(result).not.toMatch(/^\s*\*\s/m)
1188
+ })
1189
+ })
1190
+
1191
+ // ============================================================================
1192
+ // Integration Tests
1193
+ // ============================================================================
1194
+
1195
+ describe('integration', () => {
1196
+ it('renders complex nested structure', () => {
1197
+ const node: UINode = {
1198
+ type: 'panel',
1199
+ props: { title: 'Dashboard', width: 40 },
1200
+ children: [
1201
+ {
1202
+ type: 'box',
1203
+ props: { border: 'rounded' },
1204
+ children: [
1205
+ { type: 'text', props: { content: 'Welcome!' } },
1206
+ {
1207
+ type: 'list',
1208
+ props: { style: 'unordered' },
1209
+ children: [
1210
+ { type: 'list-item', props: { content: 'Feature A' } },
1211
+ { type: 'list-item', props: { content: 'Feature B' } },
1212
+ ],
1213
+ },
1214
+ ],
1215
+ },
1216
+ {
1217
+ type: 'progress',
1218
+ props: { value: 75, max: 100, width: 20 },
1219
+ },
1220
+ ],
1221
+ }
1222
+
1223
+ const result = renderUnicode(node)
1224
+
1225
+ // All content should be present
1226
+ expect(result).toContain('Dashboard')
1227
+ expect(result).toContain('Welcome!')
1228
+ expect(result).toContain('Feature A')
1229
+ expect(result).toContain('Feature B')
1230
+
1231
+ // Should use unicode characters throughout
1232
+ expect(result).toContain(UNICODE_CHARS.roundedTopLeft)
1233
+ expect(result).toContain(UNICODE_CHARS.bullet)
1234
+ expect(result).toContain(UNICODE_CHARS.progressFull)
1235
+ })
1236
+
1237
+ it('renders sidebar layout with unicode borders', () => {
1238
+ const node: UINode = {
1239
+ type: 'box',
1240
+ props: { border: 'single', flexDirection: 'row' },
1241
+ children: [
1242
+ {
1243
+ type: 'box',
1244
+ props: { border: 'single', width: 20 },
1245
+ children: [
1246
+ {
1247
+ type: 'list',
1248
+ props: { style: 'unordered' },
1249
+ children: [
1250
+ { type: 'list-item', props: { content: 'Menu 1' } },
1251
+ { type: 'list-item', props: { content: 'Menu 2' } },
1252
+ ],
1253
+ },
1254
+ ],
1255
+ },
1256
+ {
1257
+ type: 'box',
1258
+ props: { flexGrow: 1 },
1259
+ children: [
1260
+ { type: 'text', props: { content: 'Main content area' } },
1261
+ ],
1262
+ },
1263
+ ],
1264
+ }
1265
+
1266
+ const result = renderUnicode(node)
1267
+
1268
+ expect(result).toContain('Menu 1')
1269
+ expect(result).toContain('Menu 2')
1270
+ expect(result).toContain('Main content area')
1271
+ expect(result).toContain(UNICODE_CHARS.vertical)
1272
+ })
1273
+
1274
+ it('renders form with input fields', () => {
1275
+ const node: UINode = {
1276
+ type: 'box',
1277
+ props: { border: 'single', padding: 1 },
1278
+ children: [
1279
+ { type: 'text', props: { content: 'Login Form' } },
1280
+ {
1281
+ type: 'divider',
1282
+ props: { width: 30 },
1283
+ },
1284
+ {
1285
+ type: 'input',
1286
+ props: { label: 'Username', value: 'john_doe' },
1287
+ },
1288
+ {
1289
+ type: 'input',
1290
+ props: { label: 'Password', value: '********', type: 'password' },
1291
+ },
1292
+ {
1293
+ type: 'button',
1294
+ props: { label: 'Submit' },
1295
+ },
1296
+ ],
1297
+ }
1298
+
1299
+ const result = renderUnicode(node)
1300
+
1301
+ expect(result).toContain('Login Form')
1302
+ expect(result).toContain('Username')
1303
+ expect(result).toContain('Submit')
1304
+ expect(result).toContain(UNICODE_CHARS.horizontal)
1305
+ })
1306
+ })
1307
+ })