@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,1369 @@
1
+ /**
2
+ * @mdxui/terminal Text Renderer Tests (RED phase)
3
+ *
4
+ * TDD RED Phase: These tests define the contract for the TEXT renderer
5
+ * that outputs plain text without any formatting or special characters.
6
+ *
7
+ * The TEXT renderer is the first tier in the Universal Terminal UI's
8
+ * multi-tier rendering architecture:
9
+ * - TEXT: Plain text without formatting (THIS FILE)
10
+ * - MARKDOWN: Markdown syntax for AI agents
11
+ * - ASCII: ASCII art and basic drawing characters
12
+ * - UNICODE: Unicode box drawing and symbols
13
+ * - ANSI: Full ANSI escape sequences for colors/styles
14
+ * - INTERACTIVE: Full interactive terminal UI with input handling
15
+ *
16
+ * Text output format rules:
17
+ * - Simple content only, no bold/italic/code markers
18
+ * - Tables rendered as key=value lines (one per line)
19
+ * - Lists rendered as bullet points (- item)
20
+ * - Metrics rendered as "Label: value" format
21
+ * - Nested structures indented (2 spaces per level)
22
+ * - Dashboard layout as labeled sections with separators
23
+ * - All special characters and formatting stripped
24
+ *
25
+ * NOTE: These tests are expected to FAIL until implementation is complete.
26
+ * Run: pnpm --filter @mdxui/terminal test -- --run src/__tests__/renderers/text.test.ts
27
+ */
28
+ import { describe, it, expect } from 'vitest'
29
+
30
+ // ============================================================================
31
+ // These imports WILL FAIL until src/renderers/text.ts is implemented
32
+ // ============================================================================
33
+ import { renderText } from '../../renderers/text'
34
+ import type { UINode } from '../../core/types'
35
+
36
+ // ============================================================================
37
+ // Helper Functions for Tests
38
+ // ============================================================================
39
+
40
+ /**
41
+ * Creates a minimal UINode for testing
42
+ */
43
+ function createNode(
44
+ type: string,
45
+ props: Record<string, unknown> = {},
46
+ children?: UINode[]
47
+ ): UINode {
48
+ return { type, props, children }
49
+ }
50
+
51
+ // ============================================================================
52
+ // Basic Rendering Tests
53
+ // ============================================================================
54
+
55
+ describe('renderText', () => {
56
+ describe('function signature', () => {
57
+ it('exports renderText function', () => {
58
+ expect(typeof renderText).toBe('function')
59
+ })
60
+
61
+ it('accepts UINode and returns string', () => {
62
+ const node = createNode('text', { content: 'Hello' })
63
+ const result = renderText(node)
64
+ expect(typeof result).toBe('string')
65
+ })
66
+
67
+ it('returns empty string for empty node', () => {
68
+ const node = createNode('box', {})
69
+ const result = renderText(node)
70
+ expect(typeof result).toBe('string')
71
+ })
72
+ })
73
+
74
+ // ============================================================================
75
+ // Text Component Tests
76
+ // ============================================================================
77
+
78
+ describe('Text component', () => {
79
+ it('renders plain text content', () => {
80
+ const node = createNode('text', { content: 'Hello World' })
81
+ const result = renderText(node)
82
+ expect(result).toContain('Hello World')
83
+ })
84
+
85
+ it('renders text without any formatting markers', () => {
86
+ const node = createNode('text', {
87
+ content: 'Important',
88
+ bold: true,
89
+ italic: true,
90
+ code: true,
91
+ })
92
+ const result = renderText(node)
93
+ // Text tier should strip all formatting
94
+ expect(result).toContain('Important')
95
+ expect(result).not.toContain('**')
96
+ expect(result).not.toContain('*')
97
+ expect(result).not.toContain('`')
98
+ })
99
+
100
+ it('renders strikethrough as plain text', () => {
101
+ const node = createNode('text', { content: 'deprecated', strikethrough: true })
102
+ const result = renderText(node)
103
+ expect(result).toContain('deprecated')
104
+ expect(result).not.toContain('~~')
105
+ })
106
+
107
+ it('preserves text with special characters', () => {
108
+ const node = createNode('text', { content: 'Use *asterisks* and _underscores_' })
109
+ const result = renderText(node)
110
+ expect(result).toContain('asterisks')
111
+ expect(result).toContain('underscores')
112
+ })
113
+
114
+ it('handles empty text content', () => {
115
+ const node = createNode('text', { content: '' })
116
+ const result = renderText(node)
117
+ expect(typeof result).toBe('string')
118
+ })
119
+
120
+ it('handles text with newlines', () => {
121
+ const node = createNode('text', { content: 'Line 1\nLine 2\nLine 3' })
122
+ const result = renderText(node)
123
+ expect(result).toContain('Line 1')
124
+ expect(result).toContain('Line 2')
125
+ expect(result).toContain('Line 3')
126
+ })
127
+ })
128
+
129
+ // ============================================================================
130
+ // Header Tests
131
+ // ============================================================================
132
+
133
+ describe('Header rendering', () => {
134
+ it('renders header without # prefix', () => {
135
+ const node = createNode('header', { level: 1, content: 'Main Title' })
136
+ const result = renderText(node)
137
+ expect(result).toContain('Main Title')
138
+ expect(result).not.toContain('#')
139
+ })
140
+
141
+ it('renders all header levels as plain text', () => {
142
+ for (let level = 1; level <= 6; level++) {
143
+ const node = createNode('header', { level, content: `Header Level ${level}` })
144
+ const result = renderText(node)
145
+ expect(result).toContain(`Header Level ${level}`)
146
+ expect(result).not.toContain('#')
147
+ }
148
+ })
149
+
150
+ it('defaults to plain text when level is not specified', () => {
151
+ const node = createNode('header', { content: 'Default Header' })
152
+ const result = renderText(node)
153
+ expect(result).toContain('Default Header')
154
+ })
155
+ })
156
+
157
+ // ============================================================================
158
+ // List Tests
159
+ // ============================================================================
160
+
161
+ describe('List component', () => {
162
+ it('renders unordered list with - bullets', () => {
163
+ const node = createNode('list', {
164
+ items: ['First item', 'Second item', 'Third item'],
165
+ })
166
+ const result = renderText(node)
167
+
168
+ expect(result).toContain('- First item')
169
+ expect(result).toContain('- Second item')
170
+ expect(result).toContain('- Third item')
171
+ })
172
+
173
+ it('renders ordered list with 1. 2. 3. numbers', () => {
174
+ const node = createNode('list', {
175
+ items: ['Step one', 'Step two', 'Step three'],
176
+ numbered: true,
177
+ })
178
+ const result = renderText(node)
179
+
180
+ expect(result).toContain('1. Step one')
181
+ expect(result).toContain('2. Step two')
182
+ expect(result).toContain('3. Step three')
183
+ })
184
+
185
+ it('renders nested lists with proper indentation', () => {
186
+ const nestedList = createNode('list', {
187
+ items: ['Nested A', 'Nested B'],
188
+ })
189
+ const node = createNode('list', {
190
+ items: ['Parent item'],
191
+ children: [nestedList],
192
+ })
193
+ const result = renderText(node)
194
+
195
+ expect(result).toContain('- Parent item')
196
+ expect(result).toMatch(/ - Nested A/)
197
+ expect(result).toMatch(/ - Nested B/)
198
+ })
199
+
200
+ it('handles empty list gracefully', () => {
201
+ const node = createNode('list', { items: [] })
202
+ const result = renderText(node)
203
+ expect(typeof result).toBe('string')
204
+ })
205
+
206
+ it('renders task list items without checkboxes', () => {
207
+ const node = createNode('list', {
208
+ items: [
209
+ { text: 'Done task', checked: true },
210
+ { text: 'Pending task', checked: false },
211
+ ],
212
+ taskList: true,
213
+ })
214
+ const result = renderText(node)
215
+
216
+ // Text tier renders without checkbox indicators
217
+ expect(result).toContain('Done task')
218
+ expect(result).toContain('Pending task')
219
+ })
220
+ })
221
+
222
+ // ============================================================================
223
+ // Table Tests
224
+ // ============================================================================
225
+
226
+ describe('Table component', () => {
227
+ it('renders table as key=value lines', () => {
228
+ const node = createNode('table', {
229
+ columns: [
230
+ { key: 'name', header: 'Name' },
231
+ { key: 'status', header: 'Status' },
232
+ ],
233
+ data: [
234
+ { name: 'Alice', status: 'Active' },
235
+ { name: 'Bob', status: 'Pending' },
236
+ ],
237
+ })
238
+ const result = renderText(node)
239
+
240
+ // Text tier renders as label: value format
241
+ expect(result).toContain('Alice')
242
+ expect(result).toContain('Active')
243
+ expect(result).toContain('Bob')
244
+ expect(result).toContain('Pending')
245
+ // No pipes or table formatting
246
+ expect(result).not.toContain('|')
247
+ })
248
+
249
+ it('renders table headers as labels', () => {
250
+ const node = createNode('table', {
251
+ columns: [
252
+ { key: 'col1', header: 'Column 1' },
253
+ { key: 'col2', header: 'Column 2' },
254
+ ],
255
+ data: [{ col1: 'a', col2: 'b' }],
256
+ })
257
+ const result = renderText(node)
258
+
259
+ // Headers should be present
260
+ expect(result).toContain('Column 1')
261
+ expect(result).toContain('Column 2')
262
+ })
263
+
264
+ it('handles empty table gracefully', () => {
265
+ const node = createNode('table', {
266
+ columns: [{ key: 'col', header: 'Column' }],
267
+ data: [],
268
+ })
269
+ const result = renderText(node)
270
+
271
+ expect(result).toContain('Column')
272
+ })
273
+
274
+ it('handles missing column values', () => {
275
+ const node = createNode('table', {
276
+ columns: [
277
+ { key: 'a', header: 'A' },
278
+ { key: 'b', header: 'B' },
279
+ ],
280
+ data: [{ a: 'only a' }],
281
+ })
282
+ const result = renderText(node)
283
+
284
+ expect(result).toContain('only a')
285
+ })
286
+
287
+ it('renders table with single column', () => {
288
+ const node = createNode('table', {
289
+ columns: [{ key: 'item', header: 'Items' }],
290
+ data: [{ item: 'One' }, { item: 'Two' }],
291
+ })
292
+ const result = renderText(node)
293
+
294
+ expect(result).toContain('Items')
295
+ expect(result).toContain('One')
296
+ expect(result).toContain('Two')
297
+ })
298
+ })
299
+
300
+ // ============================================================================
301
+ // Code Block Tests
302
+ // ============================================================================
303
+
304
+ describe('Code block component', () => {
305
+ it('renders code block as plain text', () => {
306
+ const node = createNode('code', {
307
+ code: 'const x = 1',
308
+ language: 'typescript',
309
+ })
310
+ const result = renderText(node)
311
+
312
+ // Text tier renders code without fences
313
+ expect(result).toContain('const x = 1')
314
+ expect(result).not.toContain('```')
315
+ })
316
+
317
+ it('does not include language identifier', () => {
318
+ const node = createNode('code', {
319
+ code: 'print("hello")',
320
+ language: 'python',
321
+ })
322
+ const result = renderText(node)
323
+
324
+ expect(result).toContain('print("hello")')
325
+ expect(result).not.toContain('python')
326
+ expect(result).not.toContain('```')
327
+ })
328
+
329
+ it('preserves code indentation', () => {
330
+ const code = `function test() {
331
+ if (true) {
332
+ console.log("indented");
333
+ }
334
+ }`
335
+ const node = createNode('code', { code, language: 'javascript' })
336
+ const result = renderText(node)
337
+
338
+ expect(result).toContain(' if (true)')
339
+ expect(result).toContain(' console.log')
340
+ })
341
+
342
+ it('handles multiline code', () => {
343
+ const code = 'line 1\nline 2\nline 3'
344
+ const node = createNode('code', { code, language: 'text' })
345
+ const result = renderText(node)
346
+
347
+ expect(result).toContain('line 1')
348
+ expect(result).toContain('line 2')
349
+ expect(result).toContain('line 3')
350
+ })
351
+ })
352
+
353
+ // ============================================================================
354
+ // Link Tests
355
+ // ============================================================================
356
+
357
+ describe('Link/Action rendering', () => {
358
+ it('renders links as plain text', () => {
359
+ const node = createNode('link', {
360
+ text: 'Click here',
361
+ href: 'https://example.com',
362
+ })
363
+ const result = renderText(node)
364
+
365
+ expect(result).toContain('Click here')
366
+ expect(result).not.toContain('[')
367
+ expect(result).not.toContain('](')
368
+ })
369
+
370
+ it('renders button as plain text', () => {
371
+ const node = createNode('button', {
372
+ label: 'Submit',
373
+ action: '/api/submit',
374
+ })
375
+ const result = renderText(node)
376
+
377
+ expect(result).toContain('Submit')
378
+ expect(result).not.toContain('[')
379
+ })
380
+
381
+ it('renders button with hotkey hint', () => {
382
+ const node = createNode('button', {
383
+ label: 'Save',
384
+ hotkey: 'Ctrl+S',
385
+ action: '/save',
386
+ })
387
+ const result = renderText(node)
388
+
389
+ expect(result).toContain('Save')
390
+ expect(result).toContain('Ctrl+S')
391
+ })
392
+
393
+ it('handles external vs internal links', () => {
394
+ const external = createNode('link', {
395
+ text: 'External',
396
+ href: 'https://external.com',
397
+ external: true,
398
+ })
399
+ const internal = createNode('link', {
400
+ text: 'Internal',
401
+ href: '/internal',
402
+ })
403
+
404
+ const extResult = renderText(external)
405
+ const intResult = renderText(internal)
406
+
407
+ expect(extResult).toContain('External')
408
+ expect(intResult).toContain('Internal')
409
+ })
410
+ })
411
+
412
+ // ============================================================================
413
+ // Box/Container Tests
414
+ // ============================================================================
415
+
416
+ describe('Box/Container component', () => {
417
+ it('renders box children sequentially', () => {
418
+ const node = createNode('box', {}, [
419
+ createNode('text', { content: 'First' }),
420
+ createNode('text', { content: 'Second' }),
421
+ ])
422
+ const result = renderText(node)
423
+
424
+ expect(result).toContain('First')
425
+ expect(result).toContain('Second')
426
+ expect(result.indexOf('First')).toBeLessThan(result.indexOf('Second'))
427
+ })
428
+
429
+ it('handles nested boxes', () => {
430
+ const node = createNode('box', {}, [
431
+ createNode('box', {}, [createNode('text', { content: 'Nested content' })]),
432
+ ])
433
+ const result = renderText(node)
434
+
435
+ expect(result).toContain('Nested content')
436
+ })
437
+
438
+ it('renders box with title as header', () => {
439
+ const node = createNode('box', { title: 'Section Title' }, [
440
+ createNode('text', { content: 'Content' }),
441
+ ])
442
+ const result = renderText(node)
443
+
444
+ expect(result).toContain('Section Title')
445
+ expect(result).toContain('Content')
446
+ })
447
+
448
+ it('renders bordered boxes as plain text', () => {
449
+ const node = createNode('box', { border: 'single' }, [
450
+ createNode('text', { content: 'Bordered' }),
451
+ ])
452
+ const result = renderText(node)
453
+
454
+ expect(result).toContain('Bordered')
455
+ expect(result).not.toContain('┌')
456
+ expect(result).not.toContain('├')
457
+ })
458
+ })
459
+
460
+ // ============================================================================
461
+ // Panel Component Tests
462
+ // ============================================================================
463
+
464
+ describe('Panel component', () => {
465
+ it('renders panel with title as label', () => {
466
+ const node = createNode('panel', { title: 'Panel Title' }, [
467
+ createNode('text', { content: 'Panel content' }),
468
+ ])
469
+ const result = renderText(node)
470
+
471
+ expect(result).toContain('Panel Title')
472
+ expect(result).toContain('Panel content')
473
+ })
474
+
475
+ it('renders collapsible panel as plain text', () => {
476
+ const node = createNode('panel', {
477
+ title: 'Collapsible',
478
+ collapsible: true,
479
+ }, [createNode('text', { content: 'Hidden content' })])
480
+ const result = renderText(node)
481
+
482
+ expect(result).toContain('Collapsible')
483
+ expect(result).toContain('Hidden content')
484
+ })
485
+
486
+ it('handles collapsed panel state', () => {
487
+ const node = createNode('panel', {
488
+ title: 'Collapsed',
489
+ collapsible: true,
490
+ collapsed: true,
491
+ }, [createNode('text', { content: 'Content' })])
492
+ const result = renderText(node)
493
+
494
+ expect(result).toContain('Collapsed')
495
+ })
496
+ })
497
+
498
+ // ============================================================================
499
+ // Card Component Tests
500
+ // ============================================================================
501
+
502
+ describe('Card component', () => {
503
+ it('renders card title as label', () => {
504
+ const node = createNode('card', { title: 'Card Title' }, [
505
+ createNode('text', { content: 'Card body' }),
506
+ ])
507
+ const result = renderText(node)
508
+
509
+ expect(result).toContain('Card Title')
510
+ expect(result).toContain('Card body')
511
+ })
512
+
513
+ it('renders card sections separated by newlines', () => {
514
+ const node = createNode('card', { title: 'Title' }, [
515
+ createNode('text', { content: 'Body' }),
516
+ ])
517
+ const result = renderText(node)
518
+
519
+ expect(result).toContain('Title')
520
+ expect(result).toContain('Body')
521
+ })
522
+ })
523
+
524
+ // ============================================================================
525
+ // Metrics Tests
526
+ // ============================================================================
527
+
528
+ describe('Metrics component', () => {
529
+ it('renders metrics as Label: value format', () => {
530
+ const node = createNode('metrics', {
531
+ metrics: [
532
+ { label: 'Users', value: 1234 },
533
+ { label: 'Revenue', value: '$5,000' },
534
+ ],
535
+ })
536
+ const result = renderText(node)
537
+
538
+ // Should contain label and value
539
+ expect(result).toContain('Users')
540
+ expect(result).toContain('1234')
541
+ expect(result).toContain('Revenue')
542
+ expect(result).toContain('$5,000')
543
+ })
544
+
545
+ it('renders metrics with trend indicators', () => {
546
+ const node = createNode('metrics', {
547
+ metrics: [
548
+ { label: 'Up', value: 100, trend: 'up' },
549
+ { label: 'Down', value: 50, trend: 'down' },
550
+ ],
551
+ })
552
+ const result = renderText(node)
553
+
554
+ expect(result).toContain('Up')
555
+ expect(result).toContain('100')
556
+ expect(result).toContain('Down')
557
+ expect(result).toContain('50')
558
+ })
559
+
560
+ it('renders single metric', () => {
561
+ const node = createNode('metrics', {
562
+ metrics: [
563
+ { label: 'Total', value: 999 },
564
+ ],
565
+ })
566
+ const result = renderText(node)
567
+
568
+ expect(result).toContain('Total')
569
+ expect(result).toContain('999')
570
+ })
571
+
572
+ it('handles empty metrics gracefully', () => {
573
+ const node = createNode('metrics', {
574
+ metrics: [],
575
+ })
576
+ const result = renderText(node)
577
+
578
+ expect(typeof result).toBe('string')
579
+ })
580
+ })
581
+
582
+ // ============================================================================
583
+ // Sidebar Component Tests
584
+ // ============================================================================
585
+
586
+ describe('Sidebar component', () => {
587
+ it('renders sidebar nav items as list', () => {
588
+ const node = createNode('sidebar', {
589
+ nav: [
590
+ { label: 'Home', href: '/' },
591
+ { label: 'Settings', href: '/settings' },
592
+ ],
593
+ })
594
+ const result = renderText(node)
595
+
596
+ expect(result).toContain('Home')
597
+ expect(result).toContain('Settings')
598
+ })
599
+
600
+ it('highlights active nav item', () => {
601
+ const node = createNode('sidebar', {
602
+ nav: [
603
+ { label: 'Home', href: '/', active: false },
604
+ { label: 'Current', href: '/current', active: true },
605
+ ],
606
+ })
607
+ const result = renderText(node)
608
+
609
+ expect(result).toContain('Current')
610
+ })
611
+
612
+ it('renders sidebar sections with headers', () => {
613
+ const node = createNode('sidebar', {
614
+ sections: [
615
+ {
616
+ title: 'Navigation',
617
+ items: [{ label: 'Dashboard', href: '/dashboard' }],
618
+ },
619
+ {
620
+ title: 'Settings',
621
+ items: [{ label: 'Profile', href: '/profile' }],
622
+ },
623
+ ],
624
+ })
625
+ const result = renderText(node)
626
+
627
+ expect(result).toContain('Navigation')
628
+ expect(result).toContain('Settings')
629
+ })
630
+
631
+ it('renders nested sidebar items with indentation', () => {
632
+ const node = createNode('sidebar', {
633
+ nav: [
634
+ {
635
+ label: 'Parent',
636
+ href: '/parent',
637
+ children: [
638
+ { label: 'Child 1', href: '/child1' },
639
+ { label: 'Child 2', href: '/child2' },
640
+ ],
641
+ },
642
+ ],
643
+ })
644
+ const result = renderText(node)
645
+
646
+ expect(result).toContain('Parent')
647
+ expect(result).toContain('Child 1')
648
+ expect(result).toContain('Child 2')
649
+ })
650
+ })
651
+
652
+ // ============================================================================
653
+ // Breadcrumb Component Tests
654
+ // ============================================================================
655
+
656
+ describe('Breadcrumb component', () => {
657
+ it('renders breadcrumbs with > separator', () => {
658
+ const node = createNode('breadcrumb', {
659
+ items: [
660
+ { label: 'Home', path: '/' },
661
+ { label: 'Products', path: '/products' },
662
+ { label: 'Widget' },
663
+ ],
664
+ })
665
+ const result = renderText(node)
666
+
667
+ expect(result).toContain('Home')
668
+ expect(result).toContain('>')
669
+ expect(result).toContain('Products')
670
+ expect(result).toContain('Widget')
671
+ })
672
+
673
+ it('renders all breadcrumb items as text', () => {
674
+ const node = createNode('breadcrumb', {
675
+ items: [
676
+ { label: 'Home', path: '/' },
677
+ { label: 'Current' },
678
+ ],
679
+ })
680
+ const result = renderText(node)
681
+
682
+ expect(result).toContain('Home')
683
+ expect(result).toContain('Current')
684
+ })
685
+
686
+ it('uses custom separator', () => {
687
+ const node = createNode('breadcrumb', {
688
+ items: [{ label: 'A' }, { label: 'B' }],
689
+ separator: '/',
690
+ })
691
+ const result = renderText(node)
692
+
693
+ expect(result).toContain('/')
694
+ })
695
+ })
696
+
697
+ // ============================================================================
698
+ // Badge Component Tests
699
+ // ============================================================================
700
+
701
+ describe('Badge component', () => {
702
+ it('renders badge with label', () => {
703
+ const node = createNode('badge', { children: 'New' })
704
+ const result = renderText(node)
705
+
706
+ expect(result).toContain('New')
707
+ })
708
+
709
+ it('renders badge with variant indicator as text', () => {
710
+ const successBadge = createNode('badge', { children: 'Active', variant: 'success' })
711
+ const errorBadge = createNode('badge', { children: 'Error', variant: 'error' })
712
+
713
+ const successResult = renderText(successBadge)
714
+ const errorResult = renderText(errorBadge)
715
+
716
+ expect(successResult).toContain('Active')
717
+ expect(errorResult).toContain('Error')
718
+ })
719
+ })
720
+
721
+ // ============================================================================
722
+ // Dialog Component Tests
723
+ // ============================================================================
724
+
725
+ describe('Dialog component', () => {
726
+ it('renders dialog with title as header', () => {
727
+ const node = createNode('dialog', {
728
+ title: 'Confirm Action',
729
+ open: true,
730
+ }, [createNode('text', { content: 'Are you sure?' })])
731
+ const result = renderText(node)
732
+
733
+ expect(result).toContain('Confirm Action')
734
+ expect(result).toContain('Are you sure?')
735
+ })
736
+
737
+ it('renders dialog actions as text', () => {
738
+ const node = createNode('dialog', {
739
+ title: 'Dialog',
740
+ open: true,
741
+ actions: [
742
+ { label: 'Cancel', action: 'cancel' },
743
+ { label: 'Confirm', action: 'confirm' },
744
+ ],
745
+ }, [createNode('text', { content: 'Content' })])
746
+ const result = renderText(node)
747
+
748
+ expect(result).toContain('Cancel')
749
+ expect(result).toContain('Confirm')
750
+ })
751
+
752
+ it('skips closed dialog', () => {
753
+ const node = createNode('dialog', {
754
+ title: 'Hidden',
755
+ open: false,
756
+ }, [createNode('text', { content: 'Should not appear' })])
757
+ const result = renderText(node)
758
+
759
+ expect(result).not.toContain('Should not appear')
760
+ })
761
+ })
762
+
763
+ // ============================================================================
764
+ // Spinner Component Tests
765
+ // ============================================================================
766
+
767
+ describe('Spinner component', () => {
768
+ it('renders spinner with loading text', () => {
769
+ const node = createNode('spinner', { label: 'Loading...' })
770
+ const result = renderText(node)
771
+
772
+ expect(result).toContain('Loading')
773
+ })
774
+
775
+ it('renders spinner with default text when no label', () => {
776
+ const node = createNode('spinner', {})
777
+ const result = renderText(node)
778
+
779
+ expect(result.length).toBeGreaterThan(0)
780
+ })
781
+ })
782
+
783
+ // ============================================================================
784
+ // Dashboard Component Tests
785
+ // ============================================================================
786
+
787
+ describe('Dashboard component', () => {
788
+ it('renders dashboard title', () => {
789
+ const node = createNode('dashboard', { title: 'My Dashboard' })
790
+ const result = renderText(node)
791
+
792
+ expect(result).toContain('My Dashboard')
793
+ })
794
+
795
+ it('renders metrics as labeled lines', () => {
796
+ const node = createNode('dashboard', {
797
+ title: 'Stats',
798
+ metrics: [
799
+ { label: 'Users', value: 1234, trend: 'up' },
800
+ { label: 'Revenue', value: '$5,000', trend: 'up' },
801
+ ],
802
+ })
803
+ const result = renderText(node)
804
+
805
+ expect(result).toContain('Users')
806
+ expect(result).toContain('1234')
807
+ expect(result).toContain('Revenue')
808
+ expect(result).toContain('$5,000')
809
+ })
810
+
811
+ it('renders dashboard children (widgets) with proper structure', () => {
812
+ const node = createNode('dashboard', { title: 'Dashboard' }, [
813
+ createNode('panel', { title: 'Widget 1' }, [
814
+ createNode('text', { content: 'Widget content' }),
815
+ ]),
816
+ ])
817
+ const result = renderText(node)
818
+
819
+ expect(result).toContain('Widget 1')
820
+ expect(result).toContain('Widget content')
821
+ })
822
+
823
+ it('separates sections with blank lines', () => {
824
+ const node = createNode('dashboard', {
825
+ title: 'Dashboard',
826
+ metrics: [{ label: 'Count', value: 100 }],
827
+ }, [
828
+ createNode('panel', { title: 'Section' }, [
829
+ createNode('text', { content: 'Content' }),
830
+ ]),
831
+ ])
832
+ const result = renderText(node)
833
+
834
+ expect(result).toContain('\n\n')
835
+ })
836
+ })
837
+
838
+ // ============================================================================
839
+ // Settings Component Tests
840
+ // ============================================================================
841
+
842
+ describe('Settings component', () => {
843
+ it('renders settings sections as labels', () => {
844
+ const node = createNode('settings', {
845
+ sections: ['profile', 'security', 'notifications'],
846
+ })
847
+ const result = renderText(node)
848
+
849
+ expect(result).toMatch(/profile/i)
850
+ expect(result).toMatch(/security/i)
851
+ expect(result).toMatch(/notifications/i)
852
+ })
853
+
854
+ it('renders settings children', () => {
855
+ const node = createNode('settings', {}, [
856
+ createNode('panel', { title: 'Profile Settings' }),
857
+ ])
858
+ const result = renderText(node)
859
+
860
+ expect(result).toContain('Profile Settings')
861
+ })
862
+ })
863
+
864
+ // ============================================================================
865
+ // Input Component Tests
866
+ // ============================================================================
867
+
868
+ describe('Input component', () => {
869
+ it('renders input with label', () => {
870
+ const node = createNode('input', {
871
+ label: 'Username',
872
+ value: 'john_doe',
873
+ placeholder: 'Enter username',
874
+ })
875
+ const result = renderText(node)
876
+
877
+ expect(result).toContain('Username')
878
+ })
879
+
880
+ it('shows current value', () => {
881
+ const node = createNode('input', {
882
+ label: 'Email',
883
+ value: 'test@example.com',
884
+ })
885
+ const result = renderText(node)
886
+
887
+ expect(result).toContain('test@example.com')
888
+ })
889
+
890
+ it('shows placeholder when no value', () => {
891
+ const node = createNode('input', {
892
+ label: 'Name',
893
+ placeholder: 'Enter your name',
894
+ })
895
+ const result = renderText(node)
896
+
897
+ expect(result).toContain('Enter your name')
898
+ })
899
+
900
+ it('indicates disabled state', () => {
901
+ const node = createNode('input', {
902
+ label: 'Locked',
903
+ value: 'Cannot edit',
904
+ disabled: true,
905
+ })
906
+ const result = renderText(node)
907
+
908
+ expect(result).toContain('Locked')
909
+ })
910
+ })
911
+
912
+ // ============================================================================
913
+ // Select Component Tests
914
+ // ============================================================================
915
+
916
+ describe('Select component', () => {
917
+ it('renders select with label', () => {
918
+ const node = createNode('select', {
919
+ label: 'Country',
920
+ options: [
921
+ { label: 'USA', value: 'us' },
922
+ { label: 'Canada', value: 'ca' },
923
+ ],
924
+ })
925
+ const result = renderText(node)
926
+
927
+ expect(result).toContain('Country')
928
+ })
929
+
930
+ it('shows current selection', () => {
931
+ const node = createNode('select', {
932
+ label: 'Color',
933
+ value: 'red',
934
+ options: [
935
+ { label: 'Red', value: 'red' },
936
+ { label: 'Blue', value: 'blue' },
937
+ ],
938
+ })
939
+ const result = renderText(node)
940
+
941
+ expect(result).toContain('Red')
942
+ })
943
+
944
+ it('lists available options', () => {
945
+ const node = createNode('select', {
946
+ options: [
947
+ { label: 'Option A', value: 'a' },
948
+ { label: 'Option B', value: 'b' },
949
+ { label: 'Option C', value: 'c' },
950
+ ],
951
+ })
952
+ const result = renderText(node)
953
+
954
+ expect(result).toContain('Option A')
955
+ expect(result).toContain('Option B')
956
+ expect(result).toContain('Option C')
957
+ })
958
+ })
959
+
960
+ // ============================================================================
961
+ // Hero Component Tests
962
+ // ============================================================================
963
+
964
+ describe('Hero component', () => {
965
+ it('renders hero title', () => {
966
+ const node = createNode('hero', {
967
+ title: 'Welcome to Our Product',
968
+ subtitle: 'The best solution for your needs',
969
+ })
970
+ const result = renderText(node)
971
+
972
+ expect(result).toContain('Welcome to Our Product')
973
+ })
974
+
975
+ it('renders subtitle', () => {
976
+ const node = createNode('hero', {
977
+ title: 'Title',
978
+ subtitle: 'Subtitle text here',
979
+ })
980
+ const result = renderText(node)
981
+
982
+ expect(result).toContain('Subtitle text here')
983
+ })
984
+
985
+ it('renders CTA buttons as text', () => {
986
+ const node = createNode('hero', {
987
+ title: 'Hero',
988
+ callToAction: 'Get Started',
989
+ secondaryCallToAction: 'Learn More',
990
+ actions: {
991
+ primary: '/signup',
992
+ secondary: '/docs',
993
+ },
994
+ })
995
+ const result = renderText(node)
996
+
997
+ expect(result).toContain('Get Started')
998
+ expect(result).toContain('Learn More')
999
+ })
1000
+
1001
+ it('renders badge if present', () => {
1002
+ const node = createNode('hero', {
1003
+ title: 'Hero',
1004
+ badge: 'New Feature',
1005
+ })
1006
+ const result = renderText(node)
1007
+
1008
+ expect(result).toContain('New Feature')
1009
+ })
1010
+ })
1011
+
1012
+ // ============================================================================
1013
+ // Features Component Tests
1014
+ // ============================================================================
1015
+
1016
+ describe('Features component', () => {
1017
+ it('renders features section title', () => {
1018
+ const node = createNode('features', {
1019
+ title: 'Our Features',
1020
+ features: [],
1021
+ })
1022
+ const result = renderText(node)
1023
+
1024
+ expect(result).toContain('Our Features')
1025
+ })
1026
+
1027
+ it('renders feature items', () => {
1028
+ const node = createNode('features', {
1029
+ features: [
1030
+ { title: 'Fast', description: 'Lightning quick' },
1031
+ { title: 'Secure', description: 'Bank-level security' },
1032
+ ],
1033
+ })
1034
+ const result = renderText(node)
1035
+
1036
+ expect(result).toContain('Fast')
1037
+ expect(result).toContain('Lightning quick')
1038
+ expect(result).toContain('Secure')
1039
+ })
1040
+
1041
+ it('renders feature icons as text', () => {
1042
+ const node = createNode('features', {
1043
+ features: [{ title: 'Speed', description: 'Fast', icon: 'rocket' }],
1044
+ })
1045
+ const result = renderText(node)
1046
+
1047
+ expect(result).toContain('Speed')
1048
+ })
1049
+ })
1050
+
1051
+ // ============================================================================
1052
+ // Pricing Component Tests
1053
+ // ============================================================================
1054
+
1055
+ describe('Pricing component', () => {
1056
+ it('renders pricing tiers', () => {
1057
+ const node = createNode('pricing', {
1058
+ tiers: [
1059
+ { name: 'Free', price: '$0/mo', features: ['Basic access'] },
1060
+ { name: 'Pro', price: '$10/mo', features: ['All features'] },
1061
+ ],
1062
+ })
1063
+ const result = renderText(node)
1064
+
1065
+ expect(result).toContain('Free')
1066
+ expect(result).toContain('$0/mo')
1067
+ expect(result).toContain('Pro')
1068
+ expect(result).toContain('$10/mo')
1069
+ })
1070
+
1071
+ it('renders tier features', () => {
1072
+ const node = createNode('pricing', {
1073
+ tiers: [
1074
+ {
1075
+ name: 'Basic',
1076
+ price: '$5',
1077
+ features: ['Feature 1', 'Feature 2', 'Feature 3'],
1078
+ },
1079
+ ],
1080
+ })
1081
+ const result = renderText(node)
1082
+
1083
+ expect(result).toContain('Feature 1')
1084
+ expect(result).toContain('Feature 2')
1085
+ expect(result).toContain('Feature 3')
1086
+ })
1087
+
1088
+ it('renders CTA for each tier', () => {
1089
+ const node = createNode('pricing', {
1090
+ tiers: [
1091
+ { name: 'Free', price: '$0', features: [], callToAction: 'Start Free' },
1092
+ ],
1093
+ })
1094
+ const result = renderText(node)
1095
+
1096
+ expect(result).toContain('Start Free')
1097
+ })
1098
+ })
1099
+
1100
+ // ============================================================================
1101
+ // FAQ Component Tests
1102
+ // ============================================================================
1103
+
1104
+ describe('FAQ component', () => {
1105
+ it('renders FAQ items', () => {
1106
+ const node = createNode('faq', {
1107
+ items: [
1108
+ { question: 'What is this?', answer: 'A great product.' },
1109
+ { question: 'How does it work?', answer: 'Like magic.' },
1110
+ ],
1111
+ })
1112
+ const result = renderText(node)
1113
+
1114
+ expect(result).toContain('What is this?')
1115
+ expect(result).toContain('A great product.')
1116
+ expect(result).toContain('How does it work?')
1117
+ })
1118
+
1119
+ it('renders questions distinctly', () => {
1120
+ const node = createNode('faq', {
1121
+ items: [{ question: 'Question?', answer: 'Answer.' }],
1122
+ })
1123
+ const result = renderText(node)
1124
+
1125
+ expect(result).toContain('Question?')
1126
+ })
1127
+
1128
+ it('renders section title if provided', () => {
1129
+ const node = createNode('faq', {
1130
+ title: 'Frequently Asked Questions',
1131
+ items: [],
1132
+ })
1133
+ const result = renderText(node)
1134
+
1135
+ expect(result).toContain('Frequently Asked Questions')
1136
+ })
1137
+ })
1138
+
1139
+ // ============================================================================
1140
+ // Footer Component Tests
1141
+ // ============================================================================
1142
+
1143
+ describe('Footer component', () => {
1144
+ it('renders footer links', () => {
1145
+ const node = createNode('footer', {
1146
+ links: [
1147
+ {
1148
+ title: 'Company',
1149
+ links: [
1150
+ { label: 'About', href: '/about' },
1151
+ { label: 'Careers', href: '/careers' },
1152
+ ],
1153
+ },
1154
+ ],
1155
+ })
1156
+ const result = renderText(node)
1157
+
1158
+ expect(result).toContain('Company')
1159
+ expect(result).toContain('About')
1160
+ expect(result).toContain('Careers')
1161
+ })
1162
+
1163
+ it('renders copyright notice', () => {
1164
+ const node = createNode('footer', {
1165
+ copyright: '2024 My Company',
1166
+ links: [],
1167
+ })
1168
+ const result = renderText(node)
1169
+
1170
+ expect(result).toContain('2024 My Company')
1171
+ })
1172
+
1173
+ it('renders social links', () => {
1174
+ const node = createNode('footer', {
1175
+ social: [
1176
+ { platform: 'twitter', href: 'https://twitter.com/company' },
1177
+ { platform: 'github', href: 'https://github.com/company' },
1178
+ ],
1179
+ links: [],
1180
+ })
1181
+ const result = renderText(node)
1182
+
1183
+ expect(result).toContain('twitter')
1184
+ expect(result).toContain('github')
1185
+ })
1186
+ })
1187
+
1188
+ // ============================================================================
1189
+ // Header Component Tests
1190
+ // ============================================================================
1191
+
1192
+ describe('Header component', () => {
1193
+ it('renders header with nav links', () => {
1194
+ const node = createNode('header', {
1195
+ nav: [
1196
+ { label: 'Home', href: '/' },
1197
+ { label: 'Docs', href: '/docs' },
1198
+ ],
1199
+ })
1200
+ const result = renderText(node)
1201
+
1202
+ expect(result).toContain('Home')
1203
+ expect(result).toContain('Docs')
1204
+ })
1205
+
1206
+ it('renders CTA button', () => {
1207
+ const node = createNode('header', {
1208
+ nav: [],
1209
+ callToAction: 'Sign Up',
1210
+ actions: { primary: '/signup' },
1211
+ })
1212
+ const result = renderText(node)
1213
+
1214
+ expect(result).toContain('Sign Up')
1215
+ })
1216
+
1217
+ it('renders breadcrumbs for app header', () => {
1218
+ const node = createNode('header', {
1219
+ breadcrumbs: [
1220
+ { label: 'Dashboard', href: '/dashboard' },
1221
+ { label: 'Settings' },
1222
+ ],
1223
+ })
1224
+ const result = renderText(node)
1225
+
1226
+ expect(result).toContain('Dashboard')
1227
+ expect(result).toContain('Settings')
1228
+ })
1229
+ })
1230
+
1231
+ // ============================================================================
1232
+ // Edge Cases and Error Handling
1233
+ // ============================================================================
1234
+
1235
+ describe('Edge cases', () => {
1236
+ it('handles null/undefined props gracefully', () => {
1237
+ const node = createNode('text', { content: null as unknown as string })
1238
+ const result = renderText(node)
1239
+ expect(typeof result).toBe('string')
1240
+ })
1241
+
1242
+ it('handles unknown component types', () => {
1243
+ const node = createNode('unknown-component', { foo: 'bar' })
1244
+ const result = renderText(node)
1245
+ expect(typeof result).toBe('string')
1246
+ })
1247
+
1248
+ it('handles deeply nested structures', () => {
1249
+ let node = createNode('text', { content: 'Deep' })
1250
+ for (let i = 0; i < 10; i++) {
1251
+ node = createNode('box', {}, [node])
1252
+ }
1253
+ const result = renderText(node)
1254
+ expect(result).toContain('Deep')
1255
+ })
1256
+
1257
+ it('handles empty children array', () => {
1258
+ const node = createNode('box', {}, [])
1259
+ const result = renderText(node)
1260
+ expect(typeof result).toBe('string')
1261
+ })
1262
+
1263
+ it('handles very long content', () => {
1264
+ const longContent = 'A'.repeat(10000)
1265
+ const node = createNode('text', { content: longContent })
1266
+ const result = renderText(node)
1267
+ expect(result).toContain('A')
1268
+ })
1269
+
1270
+ it('handles emoji in content', () => {
1271
+ const node = createNode('text', { content: 'Hello :rocket: World' })
1272
+ const result = renderText(node)
1273
+ expect(result).toContain('rocket')
1274
+ })
1275
+
1276
+ it('handles unicode characters', () => {
1277
+ const node = createNode('text', { content: 'Hello 世界 مرحبا' })
1278
+ const result = renderText(node)
1279
+ expect(result).toContain('世界')
1280
+ })
1281
+ })
1282
+
1283
+ // ============================================================================
1284
+ // Text Output Quality Tests
1285
+ // ============================================================================
1286
+
1287
+ describe('Text output quality', () => {
1288
+ it('produces output with no trailing whitespace on lines', () => {
1289
+ const node = createNode('box', {}, [
1290
+ createNode('header', { level: 1, content: 'Title' }),
1291
+ createNode('text', { content: 'Content' }),
1292
+ ])
1293
+ const result = renderText(node)
1294
+
1295
+ const lines = result.split('\n')
1296
+ lines.forEach((line) => {
1297
+ expect(line).toBe(line.trimEnd())
1298
+ })
1299
+ })
1300
+
1301
+ it('uses consistent line endings', () => {
1302
+ const node = createNode('list', { items: ['A', 'B', 'C'] })
1303
+ const result = renderText(node)
1304
+
1305
+ expect(result).not.toContain('\r')
1306
+ })
1307
+
1308
+ it('has proper spacing between elements', () => {
1309
+ const node = createNode('box', {}, [
1310
+ createNode('header', { level: 1, content: 'Title' }),
1311
+ createNode('text', { content: 'Paragraph' }),
1312
+ createNode('list', { items: ['Item'] }),
1313
+ ])
1314
+ const result = renderText(node)
1315
+
1316
+ expect(result).toContain('\n\n')
1317
+ })
1318
+
1319
+ it('renders complete dashboard with structure', () => {
1320
+ const node = createNode('dashboard', {
1321
+ title: 'Dashboard',
1322
+ metrics: [{ label: 'Users', value: 100 }],
1323
+ }, [
1324
+ createNode('panel', { title: 'Activity' }, [
1325
+ createNode('text', { content: 'Recent activity here' }),
1326
+ ]),
1327
+ ])
1328
+ const result = renderText(node)
1329
+
1330
+ expect(result).toContain('Dashboard')
1331
+ expect(result).toContain('Users')
1332
+ expect(result).toContain('Activity')
1333
+ expect(result).toContain('Recent activity here')
1334
+ })
1335
+ })
1336
+
1337
+ // ============================================================================
1338
+ // Indentation Tests
1339
+ // ============================================================================
1340
+
1341
+ describe('Indentation for nested structures', () => {
1342
+ it('indents nested content 2 spaces per level', () => {
1343
+ const node = createNode('box', { title: 'Outer' }, [
1344
+ createNode('box', { title: 'Inner' }, [
1345
+ createNode('text', { content: 'Deepest' }),
1346
+ ]),
1347
+ ])
1348
+ const result = renderText(node)
1349
+
1350
+ expect(result).toContain('Outer')
1351
+ expect(result).toContain('Inner')
1352
+ expect(result).toContain('Deepest')
1353
+ })
1354
+
1355
+ it('indents list items consistently', () => {
1356
+ const node = createNode('list', {
1357
+ items: [
1358
+ 'Top level',
1359
+ 'Another top',
1360
+ ],
1361
+ })
1362
+ const result = renderText(node)
1363
+
1364
+ const lines = result.split('\n').filter((line) => line.trim())
1365
+ expect(lines.length).toBeGreaterThan(0)
1366
+ expect(result).toContain('- Top level')
1367
+ })
1368
+ })
1369
+ })