@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.
- package/README.md +571 -0
- package/dist/ansi-css-Sk5mWtdK.d.ts +119 -0
- package/dist/ansi-css-V6JIHGsM.d.ts +119 -0
- package/dist/ansi-css-_3eSEU9d.d.ts +119 -0
- package/dist/chunk-3EFDH7PK.js +5235 -0
- package/dist/chunk-3RG5ZIWI.js +10 -0
- package/dist/chunk-3X5IR6WE.js +884 -0
- package/dist/chunk-4FV5ZDCE.js +5236 -0
- package/dist/chunk-4OVMSF2J.js +243 -0
- package/dist/chunk-63FEETIS.js +4048 -0
- package/dist/chunk-B43KP7XJ.js +884 -0
- package/dist/chunk-BMTJXWUV.js +655 -0
- package/dist/chunk-C3SVH4N7.js +882 -0
- package/dist/chunk-EVWR7Y47.js +874 -0
- package/dist/chunk-F6A5VWUC.js +1285 -0
- package/dist/chunk-FD7KW7GE.js +882 -0
- package/dist/chunk-GBQ6UD6I.js +655 -0
- package/dist/chunk-GMDD3M6U.js +5227 -0
- package/dist/chunk-JBHRXOXM.js +1058 -0
- package/dist/chunk-JFOO3EYO.js +1182 -0
- package/dist/chunk-JQ5H3WXL.js +1291 -0
- package/dist/chunk-JQD5NASE.js +234 -0
- package/dist/chunk-KRHJP5R7.js +592 -0
- package/dist/chunk-KWF6WVJE.js +962 -0
- package/dist/chunk-LHYQVN3H.js +1038 -0
- package/dist/chunk-M3TLQLGC.js +1032 -0
- package/dist/chunk-MVW4Q5OP.js +240 -0
- package/dist/chunk-NXCZSWLU.js +1294 -0
- package/dist/chunk-O25TNRO6.js +607 -0
- package/dist/chunk-PNECDA2I.js +884 -0
- package/dist/chunk-QIHWRLJR.js +962 -0
- package/dist/chunk-QW5YMQ7K.js +882 -0
- package/dist/chunk-R5U7XKVJ.js +16 -0
- package/dist/chunk-RP2MVQLR.js +962 -0
- package/dist/chunk-TP6RXGXA.js +1087 -0
- package/dist/chunk-TQQSTITZ.js +655 -0
- package/dist/chunk-X24GWXQV.js +1281 -0
- package/dist/components/index.d.ts +802 -0
- package/dist/components/index.js +149 -0
- package/dist/data/index.d.ts +2554 -0
- package/dist/data/index.js +51 -0
- package/dist/forms/index.d.ts +1596 -0
- package/dist/forms/index.js +464 -0
- package/dist/index-CQRFZntR.d.ts +867 -0
- package/dist/index.d.ts +579 -0
- package/dist/index.js +786 -0
- package/dist/interactive-D0JkWosD.d.ts +217 -0
- package/dist/keyboard/index.d.ts +2 -0
- package/dist/keyboard/index.js +43 -0
- package/dist/renderers/index.d.ts +546 -0
- package/dist/renderers/index.js +2157 -0
- package/dist/storybook/index.d.ts +396 -0
- package/dist/storybook/index.js +641 -0
- package/dist/theme/index.d.ts +1339 -0
- package/dist/theme/index.js +123 -0
- package/dist/types-Bxu5PAgA.d.ts +710 -0
- package/dist/types-CIlop5Ji.d.ts +701 -0
- package/dist/types-Ca8p_p5X.d.ts +710 -0
- package/package.json +90 -0
- package/src/__tests__/components/data/card.test.ts +458 -0
- package/src/__tests__/components/data/list.test.ts +473 -0
- package/src/__tests__/components/data/metrics.test.ts +541 -0
- package/src/__tests__/components/data/table.test.ts +448 -0
- package/src/__tests__/components/input/field.test.ts +555 -0
- package/src/__tests__/components/input/form.test.ts +870 -0
- package/src/__tests__/components/input/search.test.ts +1238 -0
- package/src/__tests__/components/input/select.test.ts +658 -0
- package/src/__tests__/components/navigation/breadcrumb.test.ts +923 -0
- package/src/__tests__/components/navigation/command-palette.test.ts +1095 -0
- package/src/__tests__/components/navigation/sidebar.test.ts +1018 -0
- package/src/__tests__/components/navigation/tabs.test.ts +995 -0
- package/src/__tests__/components.test.tsx +1197 -0
- package/src/__tests__/core/compiler.test.ts +986 -0
- package/src/__tests__/core/parser.test.ts +785 -0
- package/src/__tests__/core/tier-switcher.test.ts +1103 -0
- package/src/__tests__/core/types.test.ts +1398 -0
- package/src/__tests__/data/collections.test.ts +1337 -0
- package/src/__tests__/data/db.test.ts +1265 -0
- package/src/__tests__/data/reactive.test.ts +1010 -0
- package/src/__tests__/data/sync.test.ts +1614 -0
- package/src/__tests__/errors.test.ts +660 -0
- package/src/__tests__/forms/integration.test.ts +444 -0
- package/src/__tests__/integration.test.ts +905 -0
- package/src/__tests__/keyboard.test.ts +1791 -0
- package/src/__tests__/renderer.test.ts +489 -0
- package/src/__tests__/renderers/ansi-css.test.ts +948 -0
- package/src/__tests__/renderers/ansi.test.ts +1366 -0
- package/src/__tests__/renderers/ascii.test.ts +1360 -0
- package/src/__tests__/renderers/interactive.test.ts +2353 -0
- package/src/__tests__/renderers/markdown.test.ts +1483 -0
- package/src/__tests__/renderers/text.test.ts +1369 -0
- package/src/__tests__/renderers/unicode.test.ts +1307 -0
- package/src/__tests__/theme.test.ts +639 -0
- package/src/__tests__/utils/assertions.ts +685 -0
- package/src/__tests__/utils/index.ts +115 -0
- package/src/__tests__/utils/test-renderer.ts +381 -0
- package/src/__tests__/utils/utils.test.ts +560 -0
- package/src/components/containers/card.ts +56 -0
- package/src/components/containers/dialog.ts +53 -0
- package/src/components/containers/index.ts +9 -0
- package/src/components/containers/panel.ts +59 -0
- package/src/components/feedback/badge.ts +40 -0
- package/src/components/feedback/index.ts +8 -0
- package/src/components/feedback/spinner.ts +23 -0
- package/src/components/helpers.ts +81 -0
- package/src/components/index.ts +153 -0
- package/src/components/layout/breadcrumb.ts +31 -0
- package/src/components/layout/index.ts +10 -0
- package/src/components/layout/list.ts +29 -0
- package/src/components/layout/sidebar.ts +79 -0
- package/src/components/layout/table.ts +62 -0
- package/src/components/primitives/box.ts +95 -0
- package/src/components/primitives/button.ts +54 -0
- package/src/components/primitives/index.ts +11 -0
- package/src/components/primitives/input.ts +88 -0
- package/src/components/primitives/select.ts +97 -0
- package/src/components/primitives/text.ts +60 -0
- package/src/components/render.ts +155 -0
- package/src/components/templates/app.ts +43 -0
- package/src/components/templates/index.ts +8 -0
- package/src/components/templates/site.ts +54 -0
- package/src/components/types.ts +777 -0
- package/src/core/compiler.ts +718 -0
- package/src/core/parser.ts +127 -0
- package/src/core/tier-switcher.ts +607 -0
- package/src/core/types.ts +672 -0
- package/src/data/collection.ts +316 -0
- package/src/data/collections.ts +50 -0
- package/src/data/context.tsx +174 -0
- package/src/data/db.ts +127 -0
- package/src/data/hooks.ts +532 -0
- package/src/data/index.ts +138 -0
- package/src/data/reactive.ts +1225 -0
- package/src/data/saas-collections.ts +375 -0
- package/src/data/sync.ts +1213 -0
- package/src/data/types.ts +660 -0
- package/src/forms/converters.ts +512 -0
- package/src/forms/index.ts +133 -0
- package/src/forms/schemas.ts +403 -0
- package/src/forms/types.ts +476 -0
- package/src/index.ts +542 -0
- package/src/keyboard/focus.ts +748 -0
- package/src/keyboard/index.ts +96 -0
- package/src/keyboard/integration.ts +371 -0
- package/src/keyboard/manager.ts +377 -0
- package/src/keyboard/presets.ts +90 -0
- package/src/renderers/ansi-css.ts +576 -0
- package/src/renderers/ansi.ts +802 -0
- package/src/renderers/ascii.ts +680 -0
- package/src/renderers/breadcrumb.ts +480 -0
- package/src/renderers/command-palette.ts +802 -0
- package/src/renderers/components/field.ts +210 -0
- package/src/renderers/components/form.ts +327 -0
- package/src/renderers/components/index.ts +21 -0
- package/src/renderers/components/search.ts +449 -0
- package/src/renderers/components/select.ts +222 -0
- package/src/renderers/index.ts +101 -0
- package/src/renderers/interactive/component-handlers.ts +622 -0
- package/src/renderers/interactive/cursor-manager.ts +147 -0
- package/src/renderers/interactive/focus-manager.ts +279 -0
- package/src/renderers/interactive/index.ts +661 -0
- package/src/renderers/interactive/input-handler.ts +164 -0
- package/src/renderers/interactive/keyboard-handler.ts +212 -0
- package/src/renderers/interactive/mouse-handler.ts +167 -0
- package/src/renderers/interactive/state-manager.ts +109 -0
- package/src/renderers/interactive/types.ts +338 -0
- package/src/renderers/interactive-string.ts +299 -0
- package/src/renderers/interactive.ts +59 -0
- package/src/renderers/markdown.ts +950 -0
- package/src/renderers/sidebar.ts +549 -0
- package/src/renderers/tabs.ts +682 -0
- package/src/renderers/text.ts +791 -0
- package/src/renderers/unicode.ts +917 -0
- package/src/renderers/utils.ts +942 -0
- package/src/router/adapters.ts +383 -0
- package/src/router/types.ts +140 -0
- package/src/router/utils.ts +452 -0
- package/src/schemas.ts +205 -0
- package/src/storybook/index.ts +91 -0
- package/src/storybook/interactive-decorator.tsx +659 -0
- package/src/storybook/keyboard-simulator.ts +501 -0
- package/src/theme/ansi-codes.ts +80 -0
- package/src/theme/box-drawing.ts +132 -0
- package/src/theme/color-convert.ts +254 -0
- package/src/theme/color-support.ts +321 -0
- package/src/theme/index.ts +134 -0
- package/src/theme/strip-ansi.ts +50 -0
- package/src/theme/tailwind-map.ts +469 -0
- package/src/theme/text-styles.ts +206 -0
- package/src/theme/theme-system.ts +568 -0
- 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
|
+
})
|