@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,1360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mdxui/terminal ASCII Renderer Tests (RED phase)
|
|
3
|
+
*
|
|
4
|
+
* TDD RED Phase: These tests define the contract for the ASCII renderer,
|
|
5
|
+
* which outputs basic ASCII art for low-capability terminals (no unicode, no colors).
|
|
6
|
+
*
|
|
7
|
+
* The ASCII renderer is part of the Universal Terminal UI 6-tier rendering system:
|
|
8
|
+
* - TEXT: Plain text without formatting
|
|
9
|
+
* - MARKDOWN: Markdown syntax for simple formatting
|
|
10
|
+
* - ASCII: ASCII art and basic drawing characters (this renderer)
|
|
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
|
+
* CRITICAL CONSTRAINTS:
|
|
16
|
+
* - NO unicode characters allowed in output
|
|
17
|
+
* - Only ASCII characters (0x00-0x7F)
|
|
18
|
+
* - Box drawing uses +---+ and | characters
|
|
19
|
+
* - Bullet points use * or -
|
|
20
|
+
* - Progress bars use [==== ]
|
|
21
|
+
*
|
|
22
|
+
* NOTE: These tests are expected to FAIL until implementation is complete.
|
|
23
|
+
* Run: pnpm --filter @mdxui/terminal test
|
|
24
|
+
*/
|
|
25
|
+
import { describe, it, expect, beforeEach } from 'vitest'
|
|
26
|
+
import type { UINode, RenderContext, ThemeTokens } from '../../core/types'
|
|
27
|
+
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// This import WILL FAIL until src/renderers/ascii.ts is implemented
|
|
30
|
+
// ============================================================================
|
|
31
|
+
import { renderASCII } from '../../renderers/ascii'
|
|
32
|
+
|
|
33
|
+
// ============================================================================
|
|
34
|
+
// Test Helpers
|
|
35
|
+
// ============================================================================
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Check if a string contains only ASCII characters (0x00-0x7F)
|
|
39
|
+
*/
|
|
40
|
+
function isASCIIOnly(str: string): boolean {
|
|
41
|
+
// eslint-disable-next-line no-control-regex
|
|
42
|
+
return /^[\x00-\x7F]*$/.test(str)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Check if output contains any unicode box drawing characters
|
|
47
|
+
*/
|
|
48
|
+
function containsUnicodeBoxDrawing(str: string): boolean {
|
|
49
|
+
// Unicode box drawing range: U+2500 to U+257F
|
|
50
|
+
return /[\u2500-\u257F]/.test(str)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Check if output contains any ANSI escape sequences
|
|
55
|
+
*/
|
|
56
|
+
function containsANSI(str: string): boolean {
|
|
57
|
+
// ANSI escape codes start with ESC (0x1B) or \x1b
|
|
58
|
+
return /\x1b\[[\d;]*m/.test(str)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Create a minimal test context
|
|
63
|
+
*/
|
|
64
|
+
function createTestContext(overrides: Partial<RenderContext> = {}): RenderContext {
|
|
65
|
+
const defaultTheme: ThemeTokens = {
|
|
66
|
+
primary: '',
|
|
67
|
+
secondary: '',
|
|
68
|
+
muted: '',
|
|
69
|
+
foreground: '',
|
|
70
|
+
background: '',
|
|
71
|
+
border: '',
|
|
72
|
+
success: '',
|
|
73
|
+
warning: '',
|
|
74
|
+
error: '',
|
|
75
|
+
info: '',
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
tier: 'ascii',
|
|
80
|
+
width: 80,
|
|
81
|
+
height: 24,
|
|
82
|
+
depth: 0,
|
|
83
|
+
theme: defaultTheme,
|
|
84
|
+
interactive: false,
|
|
85
|
+
...overrides,
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Create a text node
|
|
91
|
+
*/
|
|
92
|
+
function textNode(content: string, props: Record<string, unknown> = {}): UINode {
|
|
93
|
+
return {
|
|
94
|
+
type: 'text',
|
|
95
|
+
props: { content, ...props },
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Create a box node
|
|
101
|
+
*/
|
|
102
|
+
function boxNode(
|
|
103
|
+
children: UINode[] = [],
|
|
104
|
+
props: Record<string, unknown> = {}
|
|
105
|
+
): UINode {
|
|
106
|
+
return {
|
|
107
|
+
type: 'box',
|
|
108
|
+
props: { border: 'single', ...props },
|
|
109
|
+
children,
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ============================================================================
|
|
114
|
+
// Core Renderer Contract Tests
|
|
115
|
+
// ============================================================================
|
|
116
|
+
|
|
117
|
+
describe('renderASCII', () => {
|
|
118
|
+
describe('function signature', () => {
|
|
119
|
+
it('exists and is a function', () => {
|
|
120
|
+
expect(renderASCII).toBeDefined()
|
|
121
|
+
expect(typeof renderASCII).toBe('function')
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('accepts UINode and returns string', () => {
|
|
125
|
+
const node: UINode = textNode('Hello')
|
|
126
|
+
const result = renderASCII(node)
|
|
127
|
+
|
|
128
|
+
expect(typeof result).toBe('string')
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('accepts optional RenderContext parameter', () => {
|
|
132
|
+
const node: UINode = textNode('Hello')
|
|
133
|
+
const context = createTestContext({ width: 40 })
|
|
134
|
+
const result = renderASCII(node, context)
|
|
135
|
+
|
|
136
|
+
expect(typeof result).toBe('string')
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
describe('ASCII-only output constraint', () => {
|
|
141
|
+
it('outputs only ASCII characters for text nodes', () => {
|
|
142
|
+
const node: UINode = textNode('Hello World')
|
|
143
|
+
const result = renderASCII(node)
|
|
144
|
+
|
|
145
|
+
expect(isASCIIOnly(result)).toBe(true)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('outputs only ASCII characters for box nodes', () => {
|
|
149
|
+
const node: UINode = boxNode([textNode('Content')])
|
|
150
|
+
const result = renderASCII(node)
|
|
151
|
+
|
|
152
|
+
expect(isASCIIOnly(result)).toBe(true)
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('never outputs unicode box drawing characters', () => {
|
|
156
|
+
const node: UINode = boxNode([textNode('Content')], { border: 'single' })
|
|
157
|
+
const result = renderASCII(node)
|
|
158
|
+
|
|
159
|
+
expect(containsUnicodeBoxDrawing(result)).toBe(false)
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('never outputs unicode box drawing for double borders', () => {
|
|
163
|
+
const node: UINode = boxNode([textNode('Content')], { border: 'double' })
|
|
164
|
+
const result = renderASCII(node)
|
|
165
|
+
|
|
166
|
+
expect(containsUnicodeBoxDrawing(result)).toBe(false)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('never outputs unicode box drawing for rounded borders', () => {
|
|
170
|
+
const node: UINode = boxNode([textNode('Content')], { border: 'rounded' })
|
|
171
|
+
const result = renderASCII(node)
|
|
172
|
+
|
|
173
|
+
expect(containsUnicodeBoxDrawing(result)).toBe(false)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('never outputs ANSI escape sequences', () => {
|
|
177
|
+
const node: UINode = textNode('Colored text', { color: 'red', bold: true })
|
|
178
|
+
const result = renderASCII(node)
|
|
179
|
+
|
|
180
|
+
expect(containsANSI(result)).toBe(false)
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it('handles unicode content by replacing with ASCII equivalents', () => {
|
|
184
|
+
const node: UINode = textNode('Hello \u2022 World') // Unicode bullet
|
|
185
|
+
const result = renderASCII(node)
|
|
186
|
+
|
|
187
|
+
expect(isASCIIOnly(result)).toBe(true)
|
|
188
|
+
expect(result).not.toContain('\u2022')
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('handles emoji by replacing or removing', () => {
|
|
192
|
+
const node: UINode = textNode('Hello \u{1F600} World') // Emoji
|
|
193
|
+
const result = renderASCII(node)
|
|
194
|
+
|
|
195
|
+
expect(isASCIIOnly(result)).toBe(true)
|
|
196
|
+
})
|
|
197
|
+
})
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
// ============================================================================
|
|
201
|
+
// Box Drawing Tests
|
|
202
|
+
// ============================================================================
|
|
203
|
+
|
|
204
|
+
describe('ASCII Box Drawing', () => {
|
|
205
|
+
describe('basic boxes', () => {
|
|
206
|
+
it('draws top-left corner with +', () => {
|
|
207
|
+
const node: UINode = boxNode([textNode('X')])
|
|
208
|
+
const result = renderASCII(node)
|
|
209
|
+
const lines = result.split('\n')
|
|
210
|
+
|
|
211
|
+
expect(lines[0][0]).toBe('+')
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it('draws top-right corner with +', () => {
|
|
215
|
+
const node: UINode = boxNode([textNode('X')])
|
|
216
|
+
const result = renderASCII(node)
|
|
217
|
+
const lines = result.split('\n')
|
|
218
|
+
|
|
219
|
+
// Top-right corner is last non-whitespace char on first line
|
|
220
|
+
const topLine = lines[0].trimEnd()
|
|
221
|
+
expect(topLine[topLine.length - 1]).toBe('+')
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
it('draws bottom-left corner with +', () => {
|
|
225
|
+
const node: UINode = boxNode([textNode('X')])
|
|
226
|
+
const result = renderASCII(node)
|
|
227
|
+
const lines = result.split('\n').filter((l) => l.trim())
|
|
228
|
+
|
|
229
|
+
const bottomLine = lines[lines.length - 1]
|
|
230
|
+
expect(bottomLine[0]).toBe('+')
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
it('draws bottom-right corner with +', () => {
|
|
234
|
+
const node: UINode = boxNode([textNode('X')])
|
|
235
|
+
const result = renderASCII(node)
|
|
236
|
+
const lines = result.split('\n').filter((l) => l.trim())
|
|
237
|
+
|
|
238
|
+
const bottomLine = lines[lines.length - 1].trimEnd()
|
|
239
|
+
expect(bottomLine[bottomLine.length - 1]).toBe('+')
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
it('draws horizontal borders with -', () => {
|
|
243
|
+
const node: UINode = boxNode([textNode('Content')])
|
|
244
|
+
const result = renderASCII(node)
|
|
245
|
+
const lines = result.split('\n')
|
|
246
|
+
|
|
247
|
+
// Top line should have dashes between corners
|
|
248
|
+
expect(lines[0]).toMatch(/^\+[-]+\+$/)
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it('draws vertical borders with |', () => {
|
|
252
|
+
const node: UINode = boxNode([textNode('Content')])
|
|
253
|
+
const result = renderASCII(node)
|
|
254
|
+
const lines = result.split('\n').filter((l) => l.trim())
|
|
255
|
+
|
|
256
|
+
// Middle lines should start and end with |
|
|
257
|
+
for (let i = 1; i < lines.length - 1; i++) {
|
|
258
|
+
const line = lines[i].trimEnd()
|
|
259
|
+
expect(line[0]).toBe('|')
|
|
260
|
+
expect(line[line.length - 1]).toBe('|')
|
|
261
|
+
}
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
it('creates properly closed box structure', () => {
|
|
265
|
+
const node: UINode = boxNode([textNode('Test')])
|
|
266
|
+
const result = renderASCII(node)
|
|
267
|
+
const lines = result.split('\n').filter((l) => l.trim())
|
|
268
|
+
|
|
269
|
+
// Should have at least 3 lines (top, content, bottom)
|
|
270
|
+
expect(lines.length).toBeGreaterThanOrEqual(3)
|
|
271
|
+
|
|
272
|
+
// Top and bottom should be borders
|
|
273
|
+
expect(lines[0]).toMatch(/^\+[-]+\+$/)
|
|
274
|
+
expect(lines[lines.length - 1]).toMatch(/^\+[-]+\+$/)
|
|
275
|
+
})
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
describe('box sizing', () => {
|
|
279
|
+
it('adjusts box width to content', () => {
|
|
280
|
+
const shortNode = boxNode([textNode('Hi')])
|
|
281
|
+
const longNode = boxNode([textNode('Hello World')])
|
|
282
|
+
|
|
283
|
+
const shortResult = renderASCII(shortNode)
|
|
284
|
+
const longResult = renderASCII(longNode)
|
|
285
|
+
|
|
286
|
+
const shortWidth = shortResult.split('\n')[0].length
|
|
287
|
+
const longWidth = longResult.split('\n')[0].length
|
|
288
|
+
|
|
289
|
+
expect(longWidth).toBeGreaterThan(shortWidth)
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
it('respects explicit width prop', () => {
|
|
293
|
+
const node: UINode = boxNode([textNode('Hi')], { width: 20 })
|
|
294
|
+
const result = renderASCII(node)
|
|
295
|
+
const lines = result.split('\n')
|
|
296
|
+
|
|
297
|
+
expect(lines[0].trimEnd().length).toBe(20)
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
it('respects context width for wrapping', () => {
|
|
301
|
+
const node: UINode = boxNode([textNode('A '.repeat(50))])
|
|
302
|
+
const context = createTestContext({ width: 40 })
|
|
303
|
+
const result = renderASCII(node, context)
|
|
304
|
+
const lines = result.split('\n')
|
|
305
|
+
|
|
306
|
+
// All lines should respect max width
|
|
307
|
+
lines.forEach((line) => {
|
|
308
|
+
expect(line.length).toBeLessThanOrEqual(40)
|
|
309
|
+
})
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
it('handles minimum box size', () => {
|
|
313
|
+
const node: UINode = boxNode([textNode('')])
|
|
314
|
+
const result = renderASCII(node)
|
|
315
|
+
const lines = result.split('\n').filter((l) => l.trim())
|
|
316
|
+
|
|
317
|
+
// Even empty box should have valid structure
|
|
318
|
+
expect(lines.length).toBeGreaterThanOrEqual(2) // At least top and bottom
|
|
319
|
+
expect(lines[0].length).toBeGreaterThanOrEqual(2) // At least two corners
|
|
320
|
+
})
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
describe('box styles', () => {
|
|
324
|
+
it('renders single border style with ASCII', () => {
|
|
325
|
+
const node: UINode = boxNode([textNode('Single')], { border: 'single' })
|
|
326
|
+
const result = renderASCII(node)
|
|
327
|
+
|
|
328
|
+
expect(isASCIIOnly(result)).toBe(true)
|
|
329
|
+
expect(result).toMatch(/^\+[-]+\+/)
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
it('renders double border style with ASCII fallback', () => {
|
|
333
|
+
const node: UINode = boxNode([textNode('Double')], { border: 'double' })
|
|
334
|
+
const result = renderASCII(node)
|
|
335
|
+
|
|
336
|
+
// Double borders should fall back to ASCII
|
|
337
|
+
expect(isASCIIOnly(result)).toBe(true)
|
|
338
|
+
expect(result).toMatch(/^\+[=]+\+/) // Double uses = or still -
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
it('renders rounded border style with ASCII fallback', () => {
|
|
342
|
+
const node: UINode = boxNode([textNode('Rounded')], { border: 'rounded' })
|
|
343
|
+
const result = renderASCII(node)
|
|
344
|
+
|
|
345
|
+
// Rounded should use same ASCII as single
|
|
346
|
+
expect(isASCIIOnly(result)).toBe(true)
|
|
347
|
+
expect(result).toMatch(/^\+[-]+\+/)
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
it('renders no border when border is none', () => {
|
|
351
|
+
const node: UINode = boxNode([textNode('No Border')], { border: 'none' })
|
|
352
|
+
const result = renderASCII(node)
|
|
353
|
+
|
|
354
|
+
expect(result).not.toContain('+')
|
|
355
|
+
expect(result).not.toContain('|')
|
|
356
|
+
expect(result).toContain('No Border')
|
|
357
|
+
})
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
describe('nested boxes', () => {
|
|
361
|
+
it('renders nested boxes correctly', () => {
|
|
362
|
+
const innerBox = boxNode([textNode('Inner')])
|
|
363
|
+
const outerBox = boxNode([innerBox])
|
|
364
|
+
const result = renderASCII(outerBox)
|
|
365
|
+
|
|
366
|
+
// Should have nested box structure
|
|
367
|
+
const lines = result.split('\n').filter((l) => l.trim())
|
|
368
|
+
|
|
369
|
+
// Count + characters - should have more than 4 (outer corners) + 4 (inner corners)
|
|
370
|
+
const plusCount = (result.match(/\+/g) || []).length
|
|
371
|
+
expect(plusCount).toBeGreaterThanOrEqual(8)
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
it('properly indents nested content', () => {
|
|
375
|
+
const innerBox = boxNode([textNode('Nested')])
|
|
376
|
+
const outerBox = boxNode([innerBox], { padding: 1 })
|
|
377
|
+
const result = renderASCII(outerBox)
|
|
378
|
+
|
|
379
|
+
const lines = result.split('\n')
|
|
380
|
+
// Inner box should be indented from outer
|
|
381
|
+
const innerBoxLine = lines.find(
|
|
382
|
+
(l) => l.includes('+') && !l.startsWith('+')
|
|
383
|
+
)
|
|
384
|
+
expect(innerBoxLine).toBeDefined()
|
|
385
|
+
})
|
|
386
|
+
})
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
// ============================================================================
|
|
390
|
+
// Table Rendering Tests
|
|
391
|
+
// ============================================================================
|
|
392
|
+
|
|
393
|
+
describe('ASCII Table Rendering', () => {
|
|
394
|
+
/**
|
|
395
|
+
* Create a table node
|
|
396
|
+
*/
|
|
397
|
+
function tableNode(
|
|
398
|
+
headers: string[],
|
|
399
|
+
rows: string[][],
|
|
400
|
+
props: Record<string, unknown> = {}
|
|
401
|
+
): UINode {
|
|
402
|
+
return {
|
|
403
|
+
type: 'table',
|
|
404
|
+
props: { headers, rows, ...props },
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
describe('basic tables', () => {
|
|
409
|
+
it('renders table with ASCII borders', () => {
|
|
410
|
+
const node = tableNode(['Name', 'Age'], [['Alice', '30']])
|
|
411
|
+
const result = renderASCII(node)
|
|
412
|
+
|
|
413
|
+
expect(isASCIIOnly(result)).toBe(true)
|
|
414
|
+
expect(result).toContain('+')
|
|
415
|
+
expect(result).toContain('-')
|
|
416
|
+
expect(result).toContain('|')
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
it('separates header from body with horizontal line', () => {
|
|
420
|
+
const node = tableNode(['Name', 'Age'], [['Alice', '30']])
|
|
421
|
+
const result = renderASCII(node)
|
|
422
|
+
const lines = result.split('\n').filter((l) => l.trim())
|
|
423
|
+
|
|
424
|
+
// Should have header separator line with + at intersections
|
|
425
|
+
const separatorLines = lines.filter((l) => l.match(/^\+[-+]+\+$/))
|
|
426
|
+
expect(separatorLines.length).toBeGreaterThanOrEqual(2) // Top border + header separator
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
it('aligns columns properly', () => {
|
|
430
|
+
const node = tableNode(['Name', 'Age'], [
|
|
431
|
+
['Alice', '30'],
|
|
432
|
+
['Bob', '25'],
|
|
433
|
+
])
|
|
434
|
+
const result = renderASCII(node)
|
|
435
|
+
const lines = result.split('\n').filter((l) => l.includes('|'))
|
|
436
|
+
|
|
437
|
+
// All content lines should have same number of | characters
|
|
438
|
+
const pipeCount = lines[0].split('|').length
|
|
439
|
+
lines.forEach((line) => {
|
|
440
|
+
expect(line.split('|').length).toBe(pipeCount)
|
|
441
|
+
})
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
it('handles empty table', () => {
|
|
445
|
+
const node = tableNode([], [])
|
|
446
|
+
const result = renderASCII(node)
|
|
447
|
+
|
|
448
|
+
// Should still output something valid (maybe just borders or empty string)
|
|
449
|
+
expect(typeof result).toBe('string')
|
|
450
|
+
if (result.length > 0) {
|
|
451
|
+
expect(isASCIIOnly(result)).toBe(true)
|
|
452
|
+
}
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
it('handles table with only headers', () => {
|
|
456
|
+
const node = tableNode(['Name', 'Age', 'City'], [])
|
|
457
|
+
const result = renderASCII(node)
|
|
458
|
+
|
|
459
|
+
expect(result).toContain('Name')
|
|
460
|
+
expect(result).toContain('Age')
|
|
461
|
+
expect(result).toContain('City')
|
|
462
|
+
})
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
describe('table cell content', () => {
|
|
466
|
+
it('truncates long cell content when necessary', () => {
|
|
467
|
+
const longContent = 'A'.repeat(100)
|
|
468
|
+
const node = tableNode(['Header'], [[longContent]])
|
|
469
|
+
const context = createTestContext({ width: 40 })
|
|
470
|
+
const result = renderASCII(node, context)
|
|
471
|
+
|
|
472
|
+
// Lines should not exceed context width
|
|
473
|
+
result.split('\n').forEach((line) => {
|
|
474
|
+
expect(line.length).toBeLessThanOrEqual(40)
|
|
475
|
+
})
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
it('pads shorter cells to match column width', () => {
|
|
479
|
+
const node = tableNode(['Name'], [['A'], ['Alice']])
|
|
480
|
+
const result = renderASCII(node)
|
|
481
|
+
const lines = result.split('\n').filter((l) => l.includes('|'))
|
|
482
|
+
|
|
483
|
+
// Content lines should all be same width
|
|
484
|
+
const widths = lines.map((l) => l.trimEnd().length)
|
|
485
|
+
const allSame = widths.every((w) => w === widths[0])
|
|
486
|
+
expect(allSame).toBe(true)
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
it('handles multi-line cell content', () => {
|
|
490
|
+
const node = tableNode(['Description'], [['Line 1\nLine 2']])
|
|
491
|
+
const result = renderASCII(node)
|
|
492
|
+
|
|
493
|
+
// Should either show both lines or truncate gracefully
|
|
494
|
+
expect(result).toContain('Line')
|
|
495
|
+
expect(isASCIIOnly(result)).toBe(true)
|
|
496
|
+
})
|
|
497
|
+
})
|
|
498
|
+
|
|
499
|
+
describe('table intersections', () => {
|
|
500
|
+
it('uses + for all corner intersections', () => {
|
|
501
|
+
const node = tableNode(['A', 'B'], [['1', '2']])
|
|
502
|
+
const result = renderASCII(node)
|
|
503
|
+
const lines = result.split('\n').filter((l) => l.includes('+'))
|
|
504
|
+
|
|
505
|
+
// All + should be at intersection points
|
|
506
|
+
lines.forEach((line) => {
|
|
507
|
+
// Corners and intersections use +
|
|
508
|
+
expect(line).toMatch(/\+/)
|
|
509
|
+
})
|
|
510
|
+
})
|
|
511
|
+
|
|
512
|
+
it('uses + for column separators in border rows', () => {
|
|
513
|
+
const node = tableNode(['A', 'B', 'C'], [['1', '2', '3']])
|
|
514
|
+
const result = renderASCII(node)
|
|
515
|
+
const borderLines = result.split('\n').filter((l) => l.match(/^[\+\-]+$/))
|
|
516
|
+
|
|
517
|
+
// Border lines should have + at column boundaries
|
|
518
|
+
borderLines.forEach((line) => {
|
|
519
|
+
const plusCount = (line.match(/\+/g) || []).length
|
|
520
|
+
expect(plusCount).toBeGreaterThanOrEqual(2) // At least start and end
|
|
521
|
+
})
|
|
522
|
+
})
|
|
523
|
+
})
|
|
524
|
+
})
|
|
525
|
+
|
|
526
|
+
// ============================================================================
|
|
527
|
+
// List and Bullet Point Tests
|
|
528
|
+
// ============================================================================
|
|
529
|
+
|
|
530
|
+
describe('ASCII List Rendering', () => {
|
|
531
|
+
/**
|
|
532
|
+
* Create a list node
|
|
533
|
+
*/
|
|
534
|
+
function listNode(
|
|
535
|
+
items: string[],
|
|
536
|
+
props: Record<string, unknown> = {}
|
|
537
|
+
): UINode {
|
|
538
|
+
return {
|
|
539
|
+
type: 'list',
|
|
540
|
+
props: { items, ...props },
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
describe('unordered lists', () => {
|
|
545
|
+
it('uses * for bullet points', () => {
|
|
546
|
+
const node = listNode(['Item 1', 'Item 2'], { ordered: false })
|
|
547
|
+
const result = renderASCII(node)
|
|
548
|
+
|
|
549
|
+
expect(result).toContain('* Item 1')
|
|
550
|
+
expect(result).toContain('* Item 2')
|
|
551
|
+
})
|
|
552
|
+
|
|
553
|
+
it('alternatively uses - for bullet points', () => {
|
|
554
|
+
const node = listNode(['Item 1'], { ordered: false, bullet: '-' })
|
|
555
|
+
const result = renderASCII(node)
|
|
556
|
+
|
|
557
|
+
expect(result).toContain('- Item 1')
|
|
558
|
+
})
|
|
559
|
+
|
|
560
|
+
it('never uses unicode bullets', () => {
|
|
561
|
+
const node = listNode(['Item 1', 'Item 2'])
|
|
562
|
+
const result = renderASCII(node)
|
|
563
|
+
|
|
564
|
+
expect(result).not.toContain('\u2022') // Unicode bullet
|
|
565
|
+
expect(result).not.toContain('\u25CF') // Black circle
|
|
566
|
+
expect(result).not.toContain('\u25CB') // White circle
|
|
567
|
+
expect(result).not.toContain('\u25A0') // Black square
|
|
568
|
+
expect(isASCIIOnly(result)).toBe(true)
|
|
569
|
+
})
|
|
570
|
+
|
|
571
|
+
it('indents list items consistently', () => {
|
|
572
|
+
const node = listNode(['First', 'Second', 'Third'])
|
|
573
|
+
const result = renderASCII(node)
|
|
574
|
+
const lines = result.split('\n').filter((l) => l.trim())
|
|
575
|
+
|
|
576
|
+
// All bullets should start at same column
|
|
577
|
+
const bulletIndents = lines.map((l) => l.search(/[*\-]/))
|
|
578
|
+
const allSame = bulletIndents.every((i) => i === bulletIndents[0])
|
|
579
|
+
expect(allSame).toBe(true)
|
|
580
|
+
})
|
|
581
|
+
})
|
|
582
|
+
|
|
583
|
+
describe('ordered lists', () => {
|
|
584
|
+
it('uses numbers with . or ) for ordered lists', () => {
|
|
585
|
+
const node = listNode(['First', 'Second'], { ordered: true })
|
|
586
|
+
const result = renderASCII(node)
|
|
587
|
+
|
|
588
|
+
expect(result).toMatch(/1[.\)]\s*First/)
|
|
589
|
+
expect(result).toMatch(/2[.\)]\s*Second/)
|
|
590
|
+
})
|
|
591
|
+
|
|
592
|
+
it('aligns numbers properly for multi-digit lists', () => {
|
|
593
|
+
const items = Array.from({ length: 12 }, (_, i) => `Item ${i + 1}`)
|
|
594
|
+
const node = listNode(items, { ordered: true })
|
|
595
|
+
const result = renderASCII(node)
|
|
596
|
+
const lines = result.split('\n').filter((l) => l.trim())
|
|
597
|
+
|
|
598
|
+
// Items 1-9 and 10-12 should have content starting at same column
|
|
599
|
+
const contentStarts = lines.map((l) => l.search(/Item/))
|
|
600
|
+
const allSame = contentStarts.every((s) => s === contentStarts[0])
|
|
601
|
+
expect(allSame).toBe(true)
|
|
602
|
+
})
|
|
603
|
+
})
|
|
604
|
+
|
|
605
|
+
describe('nested lists', () => {
|
|
606
|
+
it('indents nested list items', () => {
|
|
607
|
+
const innerList = listNode(['Nested 1', 'Nested 2'])
|
|
608
|
+
const node: UINode = {
|
|
609
|
+
type: 'list',
|
|
610
|
+
props: { items: ['Parent'] },
|
|
611
|
+
children: [innerList],
|
|
612
|
+
}
|
|
613
|
+
const result = renderASCII(node)
|
|
614
|
+
|
|
615
|
+
// Nested items should be indented more
|
|
616
|
+
const lines = result.split('\n').filter((l) => l.includes('Nested'))
|
|
617
|
+
const parentLine = result.split('\n').find((l) => l.includes('Parent'))
|
|
618
|
+
|
|
619
|
+
if (lines.length > 0 && parentLine) {
|
|
620
|
+
const nestedIndent = lines[0].search(/\S/)
|
|
621
|
+
const parentIndent = parentLine.search(/\S/)
|
|
622
|
+
expect(nestedIndent).toBeGreaterThan(parentIndent)
|
|
623
|
+
}
|
|
624
|
+
})
|
|
625
|
+
|
|
626
|
+
it('uses consistent bullet style for each level', () => {
|
|
627
|
+
const innerList = listNode(['Sub-item'])
|
|
628
|
+
const node: UINode = {
|
|
629
|
+
type: 'list',
|
|
630
|
+
props: { items: ['Top item'] },
|
|
631
|
+
children: [innerList],
|
|
632
|
+
}
|
|
633
|
+
const result = renderASCII(node)
|
|
634
|
+
|
|
635
|
+
expect(isASCIIOnly(result)).toBe(true)
|
|
636
|
+
})
|
|
637
|
+
})
|
|
638
|
+
|
|
639
|
+
describe('empty and edge cases', () => {
|
|
640
|
+
it('handles empty list', () => {
|
|
641
|
+
const node = listNode([])
|
|
642
|
+
const result = renderASCII(node)
|
|
643
|
+
|
|
644
|
+
expect(typeof result).toBe('string')
|
|
645
|
+
})
|
|
646
|
+
|
|
647
|
+
it('handles single item list', () => {
|
|
648
|
+
const node = listNode(['Only item'])
|
|
649
|
+
const result = renderASCII(node)
|
|
650
|
+
|
|
651
|
+
expect(result).toContain('Only item')
|
|
652
|
+
})
|
|
653
|
+
|
|
654
|
+
it('handles items with special characters', () => {
|
|
655
|
+
const node = listNode(['Item with * asterisk', 'Item with - dash'])
|
|
656
|
+
const result = renderASCII(node)
|
|
657
|
+
|
|
658
|
+
expect(result).toContain('asterisk')
|
|
659
|
+
expect(result).toContain('dash')
|
|
660
|
+
expect(isASCIIOnly(result)).toBe(true)
|
|
661
|
+
})
|
|
662
|
+
})
|
|
663
|
+
})
|
|
664
|
+
|
|
665
|
+
// ============================================================================
|
|
666
|
+
// Progress Bar Tests
|
|
667
|
+
// ============================================================================
|
|
668
|
+
|
|
669
|
+
describe('ASCII Progress Bar Rendering', () => {
|
|
670
|
+
/**
|
|
671
|
+
* Create a progress bar node
|
|
672
|
+
*/
|
|
673
|
+
function progressNode(
|
|
674
|
+
value: number,
|
|
675
|
+
props: Record<string, unknown> = {}
|
|
676
|
+
): UINode {
|
|
677
|
+
return {
|
|
678
|
+
type: 'progress',
|
|
679
|
+
props: { value, ...props },
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
describe('progress bar format', () => {
|
|
684
|
+
it('renders with [ and ] brackets', () => {
|
|
685
|
+
const node = progressNode(50)
|
|
686
|
+
const result = renderASCII(node)
|
|
687
|
+
|
|
688
|
+
expect(result).toContain('[')
|
|
689
|
+
expect(result).toContain(']')
|
|
690
|
+
})
|
|
691
|
+
|
|
692
|
+
it('uses = for filled portion', () => {
|
|
693
|
+
const node = progressNode(50, { width: 12 })
|
|
694
|
+
const result = renderASCII(node)
|
|
695
|
+
|
|
696
|
+
expect(result).toContain('=')
|
|
697
|
+
})
|
|
698
|
+
|
|
699
|
+
it('uses space for unfilled portion', () => {
|
|
700
|
+
const node = progressNode(50, { width: 12 })
|
|
701
|
+
const result = renderASCII(node)
|
|
702
|
+
|
|
703
|
+
// Should have format like [===== ]
|
|
704
|
+
expect(result).toMatch(/\[=+\s+\]/)
|
|
705
|
+
})
|
|
706
|
+
|
|
707
|
+
it('renders 0% progress as empty bar', () => {
|
|
708
|
+
const node = progressNode(0, { width: 12 })
|
|
709
|
+
const result = renderASCII(node)
|
|
710
|
+
|
|
711
|
+
// Should be [ ] - all spaces
|
|
712
|
+
expect(result).toMatch(/\[\s+\]/)
|
|
713
|
+
expect(result).not.toContain('=')
|
|
714
|
+
})
|
|
715
|
+
|
|
716
|
+
it('renders 100% progress as full bar', () => {
|
|
717
|
+
const node = progressNode(100, { width: 12 })
|
|
718
|
+
const result = renderASCII(node)
|
|
719
|
+
|
|
720
|
+
// Should be [==========] - all equals
|
|
721
|
+
expect(result).toMatch(/\[=+\]/)
|
|
722
|
+
expect(result).not.toMatch(/\[=+\s/)
|
|
723
|
+
})
|
|
724
|
+
|
|
725
|
+
it('only uses ASCII characters', () => {
|
|
726
|
+
const node = progressNode(75)
|
|
727
|
+
const result = renderASCII(node)
|
|
728
|
+
|
|
729
|
+
expect(isASCIIOnly(result)).toBe(true)
|
|
730
|
+
})
|
|
731
|
+
})
|
|
732
|
+
|
|
733
|
+
describe('progress bar sizing', () => {
|
|
734
|
+
it('respects width prop', () => {
|
|
735
|
+
const node = progressNode(50, { width: 20 })
|
|
736
|
+
const result = renderASCII(node)
|
|
737
|
+
|
|
738
|
+
// Bar should be approximately 20 chars including brackets
|
|
739
|
+
expect(result.length).toBeGreaterThanOrEqual(20)
|
|
740
|
+
})
|
|
741
|
+
|
|
742
|
+
it('scales filled portion to percentage', () => {
|
|
743
|
+
const node25 = progressNode(25, { width: 12 })
|
|
744
|
+
const node75 = progressNode(75, { width: 12 })
|
|
745
|
+
|
|
746
|
+
const result25 = renderASCII(node25)
|
|
747
|
+
const result75 = renderASCII(node75)
|
|
748
|
+
|
|
749
|
+
const equals25 = (result25.match(/=/g) || []).length
|
|
750
|
+
const equals75 = (result75.match(/=/g) || []).length
|
|
751
|
+
|
|
752
|
+
expect(equals75).toBeGreaterThan(equals25)
|
|
753
|
+
})
|
|
754
|
+
})
|
|
755
|
+
|
|
756
|
+
describe('progress bar labels', () => {
|
|
757
|
+
it('can show percentage label', () => {
|
|
758
|
+
const node = progressNode(50, { showLabel: true })
|
|
759
|
+
const result = renderASCII(node)
|
|
760
|
+
|
|
761
|
+
expect(result).toContain('50%')
|
|
762
|
+
})
|
|
763
|
+
|
|
764
|
+
it('can show custom label', () => {
|
|
765
|
+
const node = progressNode(50, { label: 'Loading...' })
|
|
766
|
+
const result = renderASCII(node)
|
|
767
|
+
|
|
768
|
+
expect(result).toContain('Loading...')
|
|
769
|
+
})
|
|
770
|
+
})
|
|
771
|
+
|
|
772
|
+
describe('edge cases', () => {
|
|
773
|
+
it('clamps value below 0 to 0', () => {
|
|
774
|
+
const node = progressNode(-10)
|
|
775
|
+
const result = renderASCII(node)
|
|
776
|
+
|
|
777
|
+
expect(result).not.toContain('=')
|
|
778
|
+
expect(isASCIIOnly(result)).toBe(true)
|
|
779
|
+
})
|
|
780
|
+
|
|
781
|
+
it('clamps value above 100 to 100', () => {
|
|
782
|
+
const node = progressNode(150, { width: 10 })
|
|
783
|
+
const result = renderASCII(node)
|
|
784
|
+
|
|
785
|
+
// Should be completely filled
|
|
786
|
+
expect(result).toMatch(/\[=+\]/)
|
|
787
|
+
})
|
|
788
|
+
|
|
789
|
+
it('handles decimal percentages', () => {
|
|
790
|
+
const node = progressNode(33.33)
|
|
791
|
+
const result = renderASCII(node)
|
|
792
|
+
|
|
793
|
+
expect(isASCIIOnly(result)).toBe(true)
|
|
794
|
+
})
|
|
795
|
+
})
|
|
796
|
+
})
|
|
797
|
+
|
|
798
|
+
// ============================================================================
|
|
799
|
+
// Spinner Tests
|
|
800
|
+
// ============================================================================
|
|
801
|
+
|
|
802
|
+
describe('ASCII Spinner Rendering', () => {
|
|
803
|
+
/**
|
|
804
|
+
* Create a spinner node
|
|
805
|
+
*/
|
|
806
|
+
function spinnerNode(props: Record<string, unknown> = {}): UINode {
|
|
807
|
+
return {
|
|
808
|
+
type: 'spinner',
|
|
809
|
+
props,
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
describe('spinner frames', () => {
|
|
814
|
+
it('uses ASCII characters for spinner frames', () => {
|
|
815
|
+
const node = spinnerNode()
|
|
816
|
+
const result = renderASCII(node)
|
|
817
|
+
|
|
818
|
+
expect(isASCIIOnly(result)).toBe(true)
|
|
819
|
+
})
|
|
820
|
+
|
|
821
|
+
it('never uses unicode spinner characters', () => {
|
|
822
|
+
const node = spinnerNode()
|
|
823
|
+
const result = renderASCII(node)
|
|
824
|
+
|
|
825
|
+
// Common unicode spinners
|
|
826
|
+
expect(result).not.toContain('\u280B') // Braille
|
|
827
|
+
expect(result).not.toContain('\u25DC') // Arc
|
|
828
|
+
expect(result).not.toContain('\u2588') // Block
|
|
829
|
+
})
|
|
830
|
+
|
|
831
|
+
it('uses common ASCII spinner patterns', () => {
|
|
832
|
+
const node = spinnerNode()
|
|
833
|
+
const result = renderASCII(node)
|
|
834
|
+
|
|
835
|
+
// Common ASCII spinner chars: | / - \ or . o O
|
|
836
|
+
expect(result).toMatch(/[|/\-\\\.oO]/)
|
|
837
|
+
})
|
|
838
|
+
})
|
|
839
|
+
|
|
840
|
+
describe('spinner with label', () => {
|
|
841
|
+
it('displays label text', () => {
|
|
842
|
+
const node = spinnerNode({ label: 'Loading...' })
|
|
843
|
+
const result = renderASCII(node)
|
|
844
|
+
|
|
845
|
+
expect(result).toContain('Loading...')
|
|
846
|
+
})
|
|
847
|
+
|
|
848
|
+
it('positions spinner before label', () => {
|
|
849
|
+
const node = spinnerNode({ label: 'Working' })
|
|
850
|
+
const result = renderASCII(node)
|
|
851
|
+
|
|
852
|
+
const spinnerPos = result.search(/[|/\-\\\.oO]/)
|
|
853
|
+
const labelPos = result.indexOf('Working')
|
|
854
|
+
|
|
855
|
+
expect(spinnerPos).toBeLessThan(labelPos)
|
|
856
|
+
})
|
|
857
|
+
})
|
|
858
|
+
})
|
|
859
|
+
|
|
860
|
+
// ============================================================================
|
|
861
|
+
// Text Wrapping Tests
|
|
862
|
+
// ============================================================================
|
|
863
|
+
|
|
864
|
+
describe('ASCII Text Wrapping', () => {
|
|
865
|
+
describe('word wrapping', () => {
|
|
866
|
+
it('wraps text at context width', () => {
|
|
867
|
+
const longText = 'This is a very long line of text that should wrap'
|
|
868
|
+
const node: UINode = textNode(longText)
|
|
869
|
+
const context = createTestContext({ width: 20 })
|
|
870
|
+
const result = renderASCII(node, context)
|
|
871
|
+
const lines = result.split('\n')
|
|
872
|
+
|
|
873
|
+
lines.forEach((line) => {
|
|
874
|
+
expect(line.length).toBeLessThanOrEqual(20)
|
|
875
|
+
})
|
|
876
|
+
})
|
|
877
|
+
|
|
878
|
+
it('preserves words when wrapping', () => {
|
|
879
|
+
const text = 'Hello World Testing'
|
|
880
|
+
const node: UINode = textNode(text)
|
|
881
|
+
const context = createTestContext({ width: 12 })
|
|
882
|
+
const result = renderASCII(node, context)
|
|
883
|
+
|
|
884
|
+
// Words should not be split mid-word if avoidable
|
|
885
|
+
expect(result).toContain('Hello')
|
|
886
|
+
expect(result).toContain('World')
|
|
887
|
+
expect(result).toContain('Testing')
|
|
888
|
+
})
|
|
889
|
+
|
|
890
|
+
it('handles very long words by breaking them', () => {
|
|
891
|
+
const longWord = 'Supercalifragilisticexpialidocious'
|
|
892
|
+
const node: UINode = textNode(longWord)
|
|
893
|
+
const context = createTestContext({ width: 10 })
|
|
894
|
+
const result = renderASCII(node, context)
|
|
895
|
+
|
|
896
|
+
// Each line should respect max width
|
|
897
|
+
result.split('\n').forEach((line) => {
|
|
898
|
+
expect(line.length).toBeLessThanOrEqual(10)
|
|
899
|
+
})
|
|
900
|
+
})
|
|
901
|
+
})
|
|
902
|
+
|
|
903
|
+
describe('whitespace handling', () => {
|
|
904
|
+
it('preserves single spaces between words', () => {
|
|
905
|
+
const node: UINode = textNode('Hello World')
|
|
906
|
+
const result = renderASCII(node)
|
|
907
|
+
|
|
908
|
+
expect(result).toContain('Hello World')
|
|
909
|
+
})
|
|
910
|
+
|
|
911
|
+
it('collapses multiple spaces', () => {
|
|
912
|
+
const node: UINode = textNode('Hello World')
|
|
913
|
+
const result = renderASCII(node)
|
|
914
|
+
|
|
915
|
+
expect(result).not.toContain(' ')
|
|
916
|
+
})
|
|
917
|
+
|
|
918
|
+
it('handles leading whitespace', () => {
|
|
919
|
+
const node: UINode = textNode(' Indented')
|
|
920
|
+
const result = renderASCII(node)
|
|
921
|
+
|
|
922
|
+
expect(typeof result).toBe('string')
|
|
923
|
+
expect(isASCIIOnly(result)).toBe(true)
|
|
924
|
+
})
|
|
925
|
+
|
|
926
|
+
it('handles trailing whitespace', () => {
|
|
927
|
+
const node: UINode = textNode('Text ')
|
|
928
|
+
const result = renderASCII(node)
|
|
929
|
+
|
|
930
|
+
expect(result.includes('Text')).toBe(true)
|
|
931
|
+
})
|
|
932
|
+
})
|
|
933
|
+
|
|
934
|
+
describe('newline handling', () => {
|
|
935
|
+
it('respects explicit newlines', () => {
|
|
936
|
+
const node: UINode = textNode('Line 1\nLine 2')
|
|
937
|
+
const result = renderASCII(node)
|
|
938
|
+
const lines = result.split('\n')
|
|
939
|
+
|
|
940
|
+
expect(lines.length).toBeGreaterThanOrEqual(2)
|
|
941
|
+
expect(result).toContain('Line 1')
|
|
942
|
+
expect(result).toContain('Line 2')
|
|
943
|
+
})
|
|
944
|
+
|
|
945
|
+
it('handles multiple consecutive newlines', () => {
|
|
946
|
+
const node: UINode = textNode('Line 1\n\n\nLine 2')
|
|
947
|
+
const result = renderASCII(node)
|
|
948
|
+
|
|
949
|
+
expect(result).toContain('Line 1')
|
|
950
|
+
expect(result).toContain('Line 2')
|
|
951
|
+
})
|
|
952
|
+
})
|
|
953
|
+
})
|
|
954
|
+
|
|
955
|
+
// ============================================================================
|
|
956
|
+
// Component Type Coverage Tests
|
|
957
|
+
// ============================================================================
|
|
958
|
+
|
|
959
|
+
describe('ASCII Renderer Component Coverage', () => {
|
|
960
|
+
describe('text component', () => {
|
|
961
|
+
it('renders plain text', () => {
|
|
962
|
+
const node: UINode = textNode('Hello')
|
|
963
|
+
const result = renderASCII(node)
|
|
964
|
+
|
|
965
|
+
expect(result).toContain('Hello')
|
|
966
|
+
expect(isASCIIOnly(result)).toBe(true)
|
|
967
|
+
})
|
|
968
|
+
|
|
969
|
+
it('ignores color props', () => {
|
|
970
|
+
const node: UINode = textNode('Colored', { color: 'red' })
|
|
971
|
+
const result = renderASCII(node)
|
|
972
|
+
|
|
973
|
+
expect(result).toContain('Colored')
|
|
974
|
+
expect(containsANSI(result)).toBe(false)
|
|
975
|
+
})
|
|
976
|
+
|
|
977
|
+
it('ignores bold/italic/underline', () => {
|
|
978
|
+
const node: UINode = textNode('Styled', {
|
|
979
|
+
bold: true,
|
|
980
|
+
italic: true,
|
|
981
|
+
underline: true,
|
|
982
|
+
})
|
|
983
|
+
const result = renderASCII(node)
|
|
984
|
+
|
|
985
|
+
expect(result).toContain('Styled')
|
|
986
|
+
expect(containsANSI(result)).toBe(false)
|
|
987
|
+
})
|
|
988
|
+
})
|
|
989
|
+
|
|
990
|
+
describe('panel component', () => {
|
|
991
|
+
it('renders panel with title', () => {
|
|
992
|
+
const node: UINode = {
|
|
993
|
+
type: 'panel',
|
|
994
|
+
props: { title: 'My Panel' },
|
|
995
|
+
children: [textNode('Content')],
|
|
996
|
+
}
|
|
997
|
+
const result = renderASCII(node)
|
|
998
|
+
|
|
999
|
+
expect(result).toContain('My Panel')
|
|
1000
|
+
expect(result).toContain('Content')
|
|
1001
|
+
expect(isASCIIOnly(result)).toBe(true)
|
|
1002
|
+
})
|
|
1003
|
+
|
|
1004
|
+
it('uses ASCII borders for panel', () => {
|
|
1005
|
+
const node: UINode = {
|
|
1006
|
+
type: 'panel',
|
|
1007
|
+
props: { title: 'Panel' },
|
|
1008
|
+
children: [textNode('Text')],
|
|
1009
|
+
}
|
|
1010
|
+
const result = renderASCII(node)
|
|
1011
|
+
|
|
1012
|
+
expect(result).toContain('+')
|
|
1013
|
+
expect(result).toContain('-')
|
|
1014
|
+
expect(result).toContain('|')
|
|
1015
|
+
})
|
|
1016
|
+
})
|
|
1017
|
+
|
|
1018
|
+
describe('card component', () => {
|
|
1019
|
+
it('renders card with content', () => {
|
|
1020
|
+
const node: UINode = {
|
|
1021
|
+
type: 'card',
|
|
1022
|
+
props: {},
|
|
1023
|
+
children: [textNode('Card content')],
|
|
1024
|
+
}
|
|
1025
|
+
const result = renderASCII(node)
|
|
1026
|
+
|
|
1027
|
+
expect(result).toContain('Card content')
|
|
1028
|
+
expect(isASCIIOnly(result)).toBe(true)
|
|
1029
|
+
})
|
|
1030
|
+
})
|
|
1031
|
+
|
|
1032
|
+
describe('badge component', () => {
|
|
1033
|
+
it('renders badge text', () => {
|
|
1034
|
+
const node: UINode = {
|
|
1035
|
+
type: 'badge',
|
|
1036
|
+
props: { label: 'NEW' },
|
|
1037
|
+
}
|
|
1038
|
+
const result = renderASCII(node)
|
|
1039
|
+
|
|
1040
|
+
expect(result).toContain('NEW')
|
|
1041
|
+
expect(isASCIIOnly(result)).toBe(true)
|
|
1042
|
+
})
|
|
1043
|
+
|
|
1044
|
+
it('ignores badge variant colors', () => {
|
|
1045
|
+
const node: UINode = {
|
|
1046
|
+
type: 'badge',
|
|
1047
|
+
props: { label: 'Error', variant: 'error' },
|
|
1048
|
+
}
|
|
1049
|
+
const result = renderASCII(node)
|
|
1050
|
+
|
|
1051
|
+
expect(result).toContain('Error')
|
|
1052
|
+
expect(containsANSI(result)).toBe(false)
|
|
1053
|
+
})
|
|
1054
|
+
})
|
|
1055
|
+
|
|
1056
|
+
describe('button component', () => {
|
|
1057
|
+
it('renders button with ASCII brackets', () => {
|
|
1058
|
+
const node: UINode = {
|
|
1059
|
+
type: 'button',
|
|
1060
|
+
props: { label: 'Click Me' },
|
|
1061
|
+
}
|
|
1062
|
+
const result = renderASCII(node)
|
|
1063
|
+
|
|
1064
|
+
expect(result).toContain('Click Me')
|
|
1065
|
+
// Button might be rendered with [ ] brackets
|
|
1066
|
+
expect(result).toMatch(/[\[\]<>]?Click Me[\[\]<>]?/)
|
|
1067
|
+
expect(isASCIIOnly(result)).toBe(true)
|
|
1068
|
+
})
|
|
1069
|
+
})
|
|
1070
|
+
|
|
1071
|
+
describe('input component', () => {
|
|
1072
|
+
it('renders input field with ASCII borders', () => {
|
|
1073
|
+
const node: UINode = {
|
|
1074
|
+
type: 'input',
|
|
1075
|
+
props: { placeholder: 'Enter text...', value: '' },
|
|
1076
|
+
}
|
|
1077
|
+
const result = renderASCII(node)
|
|
1078
|
+
|
|
1079
|
+
expect(isASCIIOnly(result)).toBe(true)
|
|
1080
|
+
})
|
|
1081
|
+
|
|
1082
|
+
it('shows input value', () => {
|
|
1083
|
+
const node: UINode = {
|
|
1084
|
+
type: 'input',
|
|
1085
|
+
props: { value: 'User input' },
|
|
1086
|
+
}
|
|
1087
|
+
const result = renderASCII(node)
|
|
1088
|
+
|
|
1089
|
+
expect(result).toContain('User input')
|
|
1090
|
+
})
|
|
1091
|
+
})
|
|
1092
|
+
|
|
1093
|
+
describe('select component', () => {
|
|
1094
|
+
it('renders select with options', () => {
|
|
1095
|
+
const node: UINode = {
|
|
1096
|
+
type: 'select',
|
|
1097
|
+
props: {
|
|
1098
|
+
options: [
|
|
1099
|
+
{ label: 'Option 1', value: '1' },
|
|
1100
|
+
{ label: 'Option 2', value: '2' },
|
|
1101
|
+
],
|
|
1102
|
+
value: '1',
|
|
1103
|
+
},
|
|
1104
|
+
}
|
|
1105
|
+
const result = renderASCII(node)
|
|
1106
|
+
|
|
1107
|
+
expect(result).toContain('Option')
|
|
1108
|
+
expect(isASCIIOnly(result)).toBe(true)
|
|
1109
|
+
})
|
|
1110
|
+
})
|
|
1111
|
+
|
|
1112
|
+
describe('dialog component', () => {
|
|
1113
|
+
it('renders dialog with ASCII border', () => {
|
|
1114
|
+
const node: UINode = {
|
|
1115
|
+
type: 'dialog',
|
|
1116
|
+
props: { title: 'Alert', open: true },
|
|
1117
|
+
children: [textNode('Dialog content')],
|
|
1118
|
+
}
|
|
1119
|
+
const result = renderASCII(node)
|
|
1120
|
+
|
|
1121
|
+
expect(result).toContain('Alert')
|
|
1122
|
+
expect(result).toContain('Dialog content')
|
|
1123
|
+
expect(result).toContain('+')
|
|
1124
|
+
expect(isASCIIOnly(result)).toBe(true)
|
|
1125
|
+
})
|
|
1126
|
+
})
|
|
1127
|
+
|
|
1128
|
+
describe('breadcrumb component', () => {
|
|
1129
|
+
it('renders breadcrumb with ASCII separators', () => {
|
|
1130
|
+
const node: UINode = {
|
|
1131
|
+
type: 'breadcrumb',
|
|
1132
|
+
props: {
|
|
1133
|
+
items: [
|
|
1134
|
+
{ label: 'Home', href: '/' },
|
|
1135
|
+
{ label: 'Products', href: '/products' },
|
|
1136
|
+
{ label: 'Item', href: '/products/item' },
|
|
1137
|
+
],
|
|
1138
|
+
},
|
|
1139
|
+
}
|
|
1140
|
+
const result = renderASCII(node)
|
|
1141
|
+
|
|
1142
|
+
expect(result).toContain('Home')
|
|
1143
|
+
expect(result).toContain('Products')
|
|
1144
|
+
expect(result).toContain('Item')
|
|
1145
|
+
// Should use ASCII separator like > or /
|
|
1146
|
+
expect(result).toMatch(/[>\/]/)
|
|
1147
|
+
expect(isASCIIOnly(result)).toBe(true)
|
|
1148
|
+
})
|
|
1149
|
+
})
|
|
1150
|
+
|
|
1151
|
+
describe('sidebar component', () => {
|
|
1152
|
+
it('renders sidebar with ASCII structure', () => {
|
|
1153
|
+
const node: UINode = {
|
|
1154
|
+
type: 'sidebar',
|
|
1155
|
+
props: {
|
|
1156
|
+
items: [
|
|
1157
|
+
{ label: 'Dashboard', icon: 'home' },
|
|
1158
|
+
{ label: 'Settings', icon: 'gear' },
|
|
1159
|
+
],
|
|
1160
|
+
},
|
|
1161
|
+
}
|
|
1162
|
+
const result = renderASCII(node)
|
|
1163
|
+
|
|
1164
|
+
expect(result).toContain('Dashboard')
|
|
1165
|
+
expect(result).toContain('Settings')
|
|
1166
|
+
expect(isASCIIOnly(result)).toBe(true)
|
|
1167
|
+
})
|
|
1168
|
+
})
|
|
1169
|
+
})
|
|
1170
|
+
|
|
1171
|
+
// ============================================================================
|
|
1172
|
+
// Edge Cases and Error Handling Tests
|
|
1173
|
+
// ============================================================================
|
|
1174
|
+
|
|
1175
|
+
describe('ASCII Renderer Edge Cases', () => {
|
|
1176
|
+
describe('null and undefined handling', () => {
|
|
1177
|
+
it('handles empty text content', () => {
|
|
1178
|
+
const node: UINode = textNode('')
|
|
1179
|
+
const result = renderASCII(node)
|
|
1180
|
+
|
|
1181
|
+
expect(typeof result).toBe('string')
|
|
1182
|
+
})
|
|
1183
|
+
|
|
1184
|
+
it('handles node with no children', () => {
|
|
1185
|
+
const node: UINode = boxNode([])
|
|
1186
|
+
const result = renderASCII(node)
|
|
1187
|
+
|
|
1188
|
+
expect(typeof result).toBe('string')
|
|
1189
|
+
expect(isASCIIOnly(result)).toBe(true)
|
|
1190
|
+
})
|
|
1191
|
+
|
|
1192
|
+
it('handles deeply nested empty structures', () => {
|
|
1193
|
+
const node: UINode = boxNode([boxNode([boxNode([])])])
|
|
1194
|
+
const result = renderASCII(node)
|
|
1195
|
+
|
|
1196
|
+
expect(typeof result).toBe('string')
|
|
1197
|
+
})
|
|
1198
|
+
})
|
|
1199
|
+
|
|
1200
|
+
describe('special characters', () => {
|
|
1201
|
+
it('escapes or handles < and > characters', () => {
|
|
1202
|
+
const node: UINode = textNode('1 < 2 > 0')
|
|
1203
|
+
const result = renderASCII(node)
|
|
1204
|
+
|
|
1205
|
+
expect(result).toContain('1')
|
|
1206
|
+
expect(result).toContain('2')
|
|
1207
|
+
expect(result).toContain('0')
|
|
1208
|
+
expect(isASCIIOnly(result)).toBe(true)
|
|
1209
|
+
})
|
|
1210
|
+
|
|
1211
|
+
it('handles ampersand', () => {
|
|
1212
|
+
const node: UINode = textNode('Fish & Chips')
|
|
1213
|
+
const result = renderASCII(node)
|
|
1214
|
+
|
|
1215
|
+
expect(result).toContain('&')
|
|
1216
|
+
expect(isASCIIOnly(result)).toBe(true)
|
|
1217
|
+
})
|
|
1218
|
+
|
|
1219
|
+
it('handles quotes', () => {
|
|
1220
|
+
const node: UINode = textNode('He said "Hello"')
|
|
1221
|
+
const result = renderASCII(node)
|
|
1222
|
+
|
|
1223
|
+
expect(result).toContain('"')
|
|
1224
|
+
expect(isASCIIOnly(result)).toBe(true)
|
|
1225
|
+
})
|
|
1226
|
+
|
|
1227
|
+
it('handles backslashes', () => {
|
|
1228
|
+
const node: UINode = textNode('path\\to\\file')
|
|
1229
|
+
const result = renderASCII(node)
|
|
1230
|
+
|
|
1231
|
+
expect(result).toContain('\\')
|
|
1232
|
+
expect(isASCIIOnly(result)).toBe(true)
|
|
1233
|
+
})
|
|
1234
|
+
})
|
|
1235
|
+
|
|
1236
|
+
describe('extreme dimensions', () => {
|
|
1237
|
+
it('handles very narrow width', () => {
|
|
1238
|
+
const node: UINode = boxNode([textNode('Test')])
|
|
1239
|
+
const context = createTestContext({ width: 5 })
|
|
1240
|
+
const result = renderASCII(node, context)
|
|
1241
|
+
|
|
1242
|
+
expect(typeof result).toBe('string')
|
|
1243
|
+
})
|
|
1244
|
+
|
|
1245
|
+
it('handles very wide width', () => {
|
|
1246
|
+
const node: UINode = boxNode([textNode('Test')])
|
|
1247
|
+
const context = createTestContext({ width: 500 })
|
|
1248
|
+
const result = renderASCII(node, context)
|
|
1249
|
+
|
|
1250
|
+
expect(typeof result).toBe('string')
|
|
1251
|
+
expect(isASCIIOnly(result)).toBe(true)
|
|
1252
|
+
})
|
|
1253
|
+
|
|
1254
|
+
it('handles zero width gracefully', () => {
|
|
1255
|
+
const node: UINode = textNode('Test')
|
|
1256
|
+
const context = createTestContext({ width: 0 })
|
|
1257
|
+
const result = renderASCII(node, context)
|
|
1258
|
+
|
|
1259
|
+
expect(typeof result).toBe('string')
|
|
1260
|
+
})
|
|
1261
|
+
})
|
|
1262
|
+
|
|
1263
|
+
describe('unknown node types', () => {
|
|
1264
|
+
it('handles unknown node type gracefully', () => {
|
|
1265
|
+
const node: UINode = {
|
|
1266
|
+
type: 'unknown-component',
|
|
1267
|
+
props: { content: 'Test' },
|
|
1268
|
+
}
|
|
1269
|
+
const result = renderASCII(node)
|
|
1270
|
+
|
|
1271
|
+
// Should either render something or return empty string
|
|
1272
|
+
expect(typeof result).toBe('string')
|
|
1273
|
+
})
|
|
1274
|
+
|
|
1275
|
+
it('renders children of unknown nodes', () => {
|
|
1276
|
+
const node: UINode = {
|
|
1277
|
+
type: 'custom-wrapper',
|
|
1278
|
+
props: {},
|
|
1279
|
+
children: [textNode('Child content')],
|
|
1280
|
+
}
|
|
1281
|
+
const result = renderASCII(node)
|
|
1282
|
+
|
|
1283
|
+
// Should at least render the text child
|
|
1284
|
+
expect(result).toContain('Child content')
|
|
1285
|
+
})
|
|
1286
|
+
})
|
|
1287
|
+
|
|
1288
|
+
describe('performance edge cases', () => {
|
|
1289
|
+
it('handles many children efficiently', () => {
|
|
1290
|
+
const children = Array.from({ length: 100 }, (_, i) =>
|
|
1291
|
+
textNode(`Item ${i}`)
|
|
1292
|
+
)
|
|
1293
|
+
const node: UINode = boxNode(children)
|
|
1294
|
+
const result = renderASCII(node)
|
|
1295
|
+
|
|
1296
|
+
expect(result).toContain('Item 0')
|
|
1297
|
+
expect(result).toContain('Item 99')
|
|
1298
|
+
})
|
|
1299
|
+
|
|
1300
|
+
it('handles deep nesting', () => {
|
|
1301
|
+
let node: UINode = textNode('Deep')
|
|
1302
|
+
for (let i = 0; i < 20; i++) {
|
|
1303
|
+
node = boxNode([node])
|
|
1304
|
+
}
|
|
1305
|
+
const result = renderASCII(node)
|
|
1306
|
+
|
|
1307
|
+
expect(result).toContain('Deep')
|
|
1308
|
+
expect(isASCIIOnly(result)).toBe(true)
|
|
1309
|
+
})
|
|
1310
|
+
})
|
|
1311
|
+
})
|
|
1312
|
+
|
|
1313
|
+
// ============================================================================
|
|
1314
|
+
// Context Handling Tests
|
|
1315
|
+
// ============================================================================
|
|
1316
|
+
|
|
1317
|
+
describe('ASCII Renderer Context Handling', () => {
|
|
1318
|
+
describe('default context', () => {
|
|
1319
|
+
it('uses default width when context not provided', () => {
|
|
1320
|
+
const node: UINode = textNode('Test')
|
|
1321
|
+
const result = renderASCII(node)
|
|
1322
|
+
|
|
1323
|
+
expect(typeof result).toBe('string')
|
|
1324
|
+
})
|
|
1325
|
+
|
|
1326
|
+
it('uses default tier as ascii', () => {
|
|
1327
|
+
const node: UINode = boxNode([textNode('Test')])
|
|
1328
|
+
const result = renderASCII(node)
|
|
1329
|
+
|
|
1330
|
+
// Output should be ASCII regardless
|
|
1331
|
+
expect(isASCIIOnly(result)).toBe(true)
|
|
1332
|
+
})
|
|
1333
|
+
})
|
|
1334
|
+
|
|
1335
|
+
describe('context width', () => {
|
|
1336
|
+
it('respects context width for layout', () => {
|
|
1337
|
+
const node: UINode = boxNode([textNode('Test')])
|
|
1338
|
+
const narrowContext = createTestContext({ width: 20 })
|
|
1339
|
+
const wideContext = createTestContext({ width: 80 })
|
|
1340
|
+
|
|
1341
|
+
const narrowResult = renderASCII(node, narrowContext)
|
|
1342
|
+
const wideResult = renderASCII(node, wideContext)
|
|
1343
|
+
|
|
1344
|
+
// Both should be valid ASCII
|
|
1345
|
+
expect(isASCIIOnly(narrowResult)).toBe(true)
|
|
1346
|
+
expect(isASCIIOnly(wideResult)).toBe(true)
|
|
1347
|
+
})
|
|
1348
|
+
})
|
|
1349
|
+
|
|
1350
|
+
describe('depth tracking', () => {
|
|
1351
|
+
it('increases depth for nested components', () => {
|
|
1352
|
+
const node: UINode = boxNode([boxNode([textNode('Nested')])])
|
|
1353
|
+
const context = createTestContext({ depth: 0 })
|
|
1354
|
+
const result = renderASCII(node, context)
|
|
1355
|
+
|
|
1356
|
+
// Nested box should be indented or otherwise differentiated
|
|
1357
|
+
expect(result).toContain('Nested')
|
|
1358
|
+
})
|
|
1359
|
+
})
|
|
1360
|
+
})
|