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