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