@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,1307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mdxui/terminal UNICODE Renderer Tests (RED phase)
|
|
3
|
+
*
|
|
4
|
+
* TDD RED Phase: These tests define the contract for the UNICODE tier renderer.
|
|
5
|
+
* The UNICODE renderer uses unicode box-drawing characters (─, │, ┌, ┐, └, ┘, etc.)
|
|
6
|
+
* for prettier terminal output without colors.
|
|
7
|
+
*
|
|
8
|
+
* Part of the Universal Terminal UI 6-tier system:
|
|
9
|
+
* - TEXT: Plain text without formatting
|
|
10
|
+
* - MARKDOWN: Markdown syntax for simple formatting
|
|
11
|
+
* - ASCII: ASCII art (+, -, |, etc.)
|
|
12
|
+
* - UNICODE: Unicode box drawing (─, │, ┌, ┐, └, ┘) <-- This tier
|
|
13
|
+
* - ANSI: Full ANSI escape sequences for colors/styles
|
|
14
|
+
* - INTERACTIVE: Full interactive terminal UI with input handling
|
|
15
|
+
*
|
|
16
|
+
* NOTE: These tests are expected to FAIL until implementation is complete.
|
|
17
|
+
* Run: pnpm --filter @mdxui/terminal test
|
|
18
|
+
*/
|
|
19
|
+
import { describe, it, expect, beforeEach } from 'vitest'
|
|
20
|
+
|
|
21
|
+
// ============================================================================
|
|
22
|
+
// This import WILL FAIL until src/renderers/unicode.ts is implemented
|
|
23
|
+
// ============================================================================
|
|
24
|
+
import { renderUnicode } from '../../renderers/unicode'
|
|
25
|
+
import type { UINode, RenderContext, ThemeTokens } from '../../core/types'
|
|
26
|
+
|
|
27
|
+
// ============================================================================
|
|
28
|
+
// Test Helpers
|
|
29
|
+
// ============================================================================
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Unicode box-drawing characters used by this renderer
|
|
33
|
+
*/
|
|
34
|
+
const UNICODE_CHARS = {
|
|
35
|
+
// Single box drawing
|
|
36
|
+
topLeft: '┌',
|
|
37
|
+
topRight: '┐',
|
|
38
|
+
bottomLeft: '└',
|
|
39
|
+
bottomRight: '┘',
|
|
40
|
+
horizontal: '─',
|
|
41
|
+
vertical: '│',
|
|
42
|
+
|
|
43
|
+
// T-junctions
|
|
44
|
+
teeLeft: '├',
|
|
45
|
+
teeRight: '┤',
|
|
46
|
+
teeTop: '┬',
|
|
47
|
+
teeBottom: '┴',
|
|
48
|
+
crossJunction: '┼',
|
|
49
|
+
|
|
50
|
+
// Double box drawing
|
|
51
|
+
doubleTopLeft: '╔',
|
|
52
|
+
doubleTopRight: '╗',
|
|
53
|
+
doubleBottomLeft: '╚',
|
|
54
|
+
doubleBottomRight: '╝',
|
|
55
|
+
doubleHorizontal: '═',
|
|
56
|
+
doubleVertical: '║',
|
|
57
|
+
|
|
58
|
+
// Rounded corners
|
|
59
|
+
roundedTopLeft: '╭',
|
|
60
|
+
roundedTopRight: '╮',
|
|
61
|
+
roundedBottomLeft: '╰',
|
|
62
|
+
roundedBottomRight: '╯',
|
|
63
|
+
|
|
64
|
+
// Bullets and list markers
|
|
65
|
+
bullet: '•',
|
|
66
|
+
hollowBullet: '◦',
|
|
67
|
+
squareBullet: '▪',
|
|
68
|
+
triangleRight: '▸',
|
|
69
|
+
triangleDown: '▾',
|
|
70
|
+
checkmark: '✓',
|
|
71
|
+
crossMark: '✗',
|
|
72
|
+
arrowRight: '→',
|
|
73
|
+
arrowDown: '↓',
|
|
74
|
+
|
|
75
|
+
// Progress bar characters
|
|
76
|
+
progressFull: '▓',
|
|
77
|
+
progressEmpty: '░',
|
|
78
|
+
progressHalf: '▒',
|
|
79
|
+
|
|
80
|
+
// Dividers
|
|
81
|
+
ellipsis: '…',
|
|
82
|
+
middleDot: '·',
|
|
83
|
+
} as const
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* ASCII fallback characters - used to verify UNICODE is NOT using these
|
|
87
|
+
*/
|
|
88
|
+
const ASCII_CHARS = {
|
|
89
|
+
topLeft: '+',
|
|
90
|
+
topRight: '+',
|
|
91
|
+
bottomLeft: '+',
|
|
92
|
+
bottomRight: '+',
|
|
93
|
+
horizontal: '-',
|
|
94
|
+
vertical: '|',
|
|
95
|
+
bullet: '*',
|
|
96
|
+
progressFull: '#',
|
|
97
|
+
progressEmpty: '-',
|
|
98
|
+
} as const
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Create a default theme for testing
|
|
102
|
+
*/
|
|
103
|
+
function createTestTheme(): ThemeTokens {
|
|
104
|
+
return {
|
|
105
|
+
primary: '',
|
|
106
|
+
secondary: '',
|
|
107
|
+
muted: '',
|
|
108
|
+
foreground: '',
|
|
109
|
+
background: '',
|
|
110
|
+
border: '',
|
|
111
|
+
success: '',
|
|
112
|
+
warning: '',
|
|
113
|
+
error: '',
|
|
114
|
+
info: '',
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Create a default render context for UNICODE tier
|
|
120
|
+
*/
|
|
121
|
+
function createTestContext(overrides?: Partial<RenderContext>): RenderContext {
|
|
122
|
+
return {
|
|
123
|
+
tier: 'unicode',
|
|
124
|
+
width: 80,
|
|
125
|
+
height: 24,
|
|
126
|
+
depth: 0,
|
|
127
|
+
theme: createTestTheme(),
|
|
128
|
+
interactive: false,
|
|
129
|
+
...overrides,
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ============================================================================
|
|
134
|
+
// renderUnicode Function Signature Tests
|
|
135
|
+
// ============================================================================
|
|
136
|
+
|
|
137
|
+
describe('renderUnicode', () => {
|
|
138
|
+
describe('function signature', () => {
|
|
139
|
+
it('exports renderUnicode function', () => {
|
|
140
|
+
expect(typeof renderUnicode).toBe('function')
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('accepts UINode as first argument', () => {
|
|
144
|
+
const node: UINode = { type: 'text', props: { content: 'Hello' } }
|
|
145
|
+
const result = renderUnicode(node)
|
|
146
|
+
expect(result).toBeDefined()
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('accepts optional RenderContext as second argument', () => {
|
|
150
|
+
const node: UINode = { type: 'text', props: { content: 'Hello' } }
|
|
151
|
+
const context = createTestContext()
|
|
152
|
+
const result = renderUnicode(node, context)
|
|
153
|
+
expect(result).toBeDefined()
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('returns a string', () => {
|
|
157
|
+
const node: UINode = { type: 'text', props: { content: 'Hello' } }
|
|
158
|
+
const result = renderUnicode(node)
|
|
159
|
+
expect(typeof result).toBe('string')
|
|
160
|
+
})
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
// ============================================================================
|
|
164
|
+
// Box Drawing Tests - Single Border Style
|
|
165
|
+
// ============================================================================
|
|
166
|
+
|
|
167
|
+
describe('box drawing - single style', () => {
|
|
168
|
+
it('renders simple box with single border corners', () => {
|
|
169
|
+
const node: UINode = {
|
|
170
|
+
type: 'box',
|
|
171
|
+
props: { border: 'single', width: 10, height: 3 },
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const result = renderUnicode(node)
|
|
175
|
+
|
|
176
|
+
// Should contain unicode box corners
|
|
177
|
+
expect(result).toContain(UNICODE_CHARS.topLeft)
|
|
178
|
+
expect(result).toContain(UNICODE_CHARS.topRight)
|
|
179
|
+
expect(result).toContain(UNICODE_CHARS.bottomLeft)
|
|
180
|
+
expect(result).toContain(UNICODE_CHARS.bottomRight)
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it('renders horizontal lines with unicode dash', () => {
|
|
184
|
+
const node: UINode = {
|
|
185
|
+
type: 'box',
|
|
186
|
+
props: { border: 'single', width: 10, height: 3 },
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const result = renderUnicode(node)
|
|
190
|
+
|
|
191
|
+
// Should contain unicode horizontal line character
|
|
192
|
+
expect(result).toContain(UNICODE_CHARS.horizontal)
|
|
193
|
+
// Should NOT contain ASCII dash for borders
|
|
194
|
+
expect(result).not.toMatch(/^\+-+\+$/m) // No ASCII box top/bottom
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('renders vertical lines with unicode pipe', () => {
|
|
198
|
+
const node: UINode = {
|
|
199
|
+
type: 'box',
|
|
200
|
+
props: { border: 'single', width: 10, height: 5 },
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const result = renderUnicode(node)
|
|
204
|
+
|
|
205
|
+
// Should contain unicode vertical line character
|
|
206
|
+
expect(result).toContain(UNICODE_CHARS.vertical)
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it('renders complete single-border box structure', () => {
|
|
210
|
+
const node: UINode = {
|
|
211
|
+
type: 'box',
|
|
212
|
+
props: { border: 'single', width: 6, height: 3 },
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const result = renderUnicode(node)
|
|
216
|
+
const lines = result.split('\n')
|
|
217
|
+
|
|
218
|
+
// Top line should be: ┌────┐
|
|
219
|
+
expect(lines[0]).toMatch(/^┌─+┐$/)
|
|
220
|
+
// Middle lines should be: │ │
|
|
221
|
+
expect(lines[1]).toMatch(/^│.+│$/)
|
|
222
|
+
// Bottom line should be: └────┘
|
|
223
|
+
expect(lines[2]).toMatch(/^└─+┘$/)
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
it('box with content displays content inside', () => {
|
|
227
|
+
const node: UINode = {
|
|
228
|
+
type: 'box',
|
|
229
|
+
props: { border: 'single' },
|
|
230
|
+
children: [{ type: 'text', props: { content: 'Hello' } }],
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const result = renderUnicode(node)
|
|
234
|
+
|
|
235
|
+
expect(result).toContain('Hello')
|
|
236
|
+
expect(result).toContain(UNICODE_CHARS.vertical)
|
|
237
|
+
})
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
// ============================================================================
|
|
241
|
+
// Box Drawing Tests - Double Border Style
|
|
242
|
+
// ============================================================================
|
|
243
|
+
|
|
244
|
+
describe('box drawing - double style', () => {
|
|
245
|
+
it('renders double border corners', () => {
|
|
246
|
+
const node: UINode = {
|
|
247
|
+
type: 'box',
|
|
248
|
+
props: { border: 'double', width: 10, height: 3 },
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const result = renderUnicode(node)
|
|
252
|
+
|
|
253
|
+
expect(result).toContain(UNICODE_CHARS.doubleTopLeft)
|
|
254
|
+
expect(result).toContain(UNICODE_CHARS.doubleTopRight)
|
|
255
|
+
expect(result).toContain(UNICODE_CHARS.doubleBottomLeft)
|
|
256
|
+
expect(result).toContain(UNICODE_CHARS.doubleBottomRight)
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
it('renders double horizontal lines', () => {
|
|
260
|
+
const node: UINode = {
|
|
261
|
+
type: 'box',
|
|
262
|
+
props: { border: 'double', width: 10, height: 3 },
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const result = renderUnicode(node)
|
|
266
|
+
|
|
267
|
+
expect(result).toContain(UNICODE_CHARS.doubleHorizontal)
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
it('renders double vertical lines', () => {
|
|
271
|
+
const node: UINode = {
|
|
272
|
+
type: 'box',
|
|
273
|
+
props: { border: 'double', width: 10, height: 5 },
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const result = renderUnicode(node)
|
|
277
|
+
|
|
278
|
+
expect(result).toContain(UNICODE_CHARS.doubleVertical)
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
it('renders complete double-border box structure', () => {
|
|
282
|
+
const node: UINode = {
|
|
283
|
+
type: 'box',
|
|
284
|
+
props: { border: 'double', width: 6, height: 3 },
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const result = renderUnicode(node)
|
|
288
|
+
const lines = result.split('\n')
|
|
289
|
+
|
|
290
|
+
// Top line should be: ╔════╗
|
|
291
|
+
expect(lines[0]).toMatch(/^╔═+╗$/)
|
|
292
|
+
// Middle lines should be: ║ ║
|
|
293
|
+
expect(lines[1]).toMatch(/^║.+║$/)
|
|
294
|
+
// Bottom line should be: ╚════╝
|
|
295
|
+
expect(lines[2]).toMatch(/^╚═+╝$/)
|
|
296
|
+
})
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
// ============================================================================
|
|
300
|
+
// Box Drawing Tests - Rounded Border Style
|
|
301
|
+
// ============================================================================
|
|
302
|
+
|
|
303
|
+
describe('box drawing - rounded style', () => {
|
|
304
|
+
it('renders rounded border corners', () => {
|
|
305
|
+
const node: UINode = {
|
|
306
|
+
type: 'box',
|
|
307
|
+
props: { border: 'rounded', width: 10, height: 3 },
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const result = renderUnicode(node)
|
|
311
|
+
|
|
312
|
+
expect(result).toContain(UNICODE_CHARS.roundedTopLeft)
|
|
313
|
+
expect(result).toContain(UNICODE_CHARS.roundedTopRight)
|
|
314
|
+
expect(result).toContain(UNICODE_CHARS.roundedBottomLeft)
|
|
315
|
+
expect(result).toContain(UNICODE_CHARS.roundedBottomRight)
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
it('uses single line characters with rounded corners', () => {
|
|
319
|
+
const node: UINode = {
|
|
320
|
+
type: 'box',
|
|
321
|
+
props: { border: 'rounded', width: 10, height: 3 },
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const result = renderUnicode(node)
|
|
325
|
+
|
|
326
|
+
// Rounded uses single-line horizontal/vertical but rounded corners
|
|
327
|
+
expect(result).toContain(UNICODE_CHARS.horizontal)
|
|
328
|
+
expect(result).toContain(UNICODE_CHARS.roundedTopLeft)
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
it('renders complete rounded-border box structure', () => {
|
|
332
|
+
const node: UINode = {
|
|
333
|
+
type: 'box',
|
|
334
|
+
props: { border: 'rounded', width: 6, height: 3 },
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const result = renderUnicode(node)
|
|
338
|
+
const lines = result.split('\n')
|
|
339
|
+
|
|
340
|
+
// Top line should be: ╭────╮
|
|
341
|
+
expect(lines[0]).toMatch(/^╭─+╮$/)
|
|
342
|
+
// Middle lines should be: │ │
|
|
343
|
+
expect(lines[1]).toMatch(/^│.+│$/)
|
|
344
|
+
// Bottom line should be: ╰────╯
|
|
345
|
+
expect(lines[2]).toMatch(/^╰─+╯$/)
|
|
346
|
+
})
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
// ============================================================================
|
|
350
|
+
// Text Rendering Tests
|
|
351
|
+
// ============================================================================
|
|
352
|
+
|
|
353
|
+
describe('text rendering', () => {
|
|
354
|
+
it('renders plain text content', () => {
|
|
355
|
+
const node: UINode = {
|
|
356
|
+
type: 'text',
|
|
357
|
+
props: { content: 'Hello, World!' },
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const result = renderUnicode(node)
|
|
361
|
+
|
|
362
|
+
expect(result).toBe('Hello, World!')
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
it('renders empty text as empty string', () => {
|
|
366
|
+
const node: UINode = {
|
|
367
|
+
type: 'text',
|
|
368
|
+
props: { content: '' },
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const result = renderUnicode(node)
|
|
372
|
+
|
|
373
|
+
expect(result).toBe('')
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
it('renders multiline text preserving newlines', () => {
|
|
377
|
+
const node: UINode = {
|
|
378
|
+
type: 'text',
|
|
379
|
+
props: { content: 'Line 1\nLine 2\nLine 3' },
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const result = renderUnicode(node)
|
|
383
|
+
|
|
384
|
+
expect(result).toContain('Line 1')
|
|
385
|
+
expect(result).toContain('Line 2')
|
|
386
|
+
expect(result).toContain('Line 3')
|
|
387
|
+
expect(result.split('\n')).toHaveLength(3)
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
it('ignores style props for unicode tier (no ANSI)', () => {
|
|
391
|
+
const node: UINode = {
|
|
392
|
+
type: 'text',
|
|
393
|
+
props: {
|
|
394
|
+
content: 'Styled text',
|
|
395
|
+
bold: true,
|
|
396
|
+
color: 'red',
|
|
397
|
+
},
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const result = renderUnicode(node)
|
|
401
|
+
|
|
402
|
+
// Should render content but NO ANSI escape codes
|
|
403
|
+
expect(result).toContain('Styled text')
|
|
404
|
+
expect(result).not.toMatch(/\x1b\[/)
|
|
405
|
+
})
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
// ============================================================================
|
|
409
|
+
// List Rendering Tests
|
|
410
|
+
// ============================================================================
|
|
411
|
+
|
|
412
|
+
describe('list rendering', () => {
|
|
413
|
+
it('renders unordered list with unicode bullets', () => {
|
|
414
|
+
const node: UINode = {
|
|
415
|
+
type: 'list',
|
|
416
|
+
props: { style: 'unordered' },
|
|
417
|
+
children: [
|
|
418
|
+
{ type: 'list-item', props: { content: 'First item' } },
|
|
419
|
+
{ type: 'list-item', props: { content: 'Second item' } },
|
|
420
|
+
{ type: 'list-item', props: { content: 'Third item' } },
|
|
421
|
+
],
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const result = renderUnicode(node)
|
|
425
|
+
|
|
426
|
+
expect(result).toContain(UNICODE_CHARS.bullet)
|
|
427
|
+
expect(result).toContain('First item')
|
|
428
|
+
expect(result).toContain('Second item')
|
|
429
|
+
expect(result).toContain('Third item')
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
it('does NOT use ASCII asterisk for bullets', () => {
|
|
433
|
+
const node: UINode = {
|
|
434
|
+
type: 'list',
|
|
435
|
+
props: { style: 'unordered' },
|
|
436
|
+
children: [
|
|
437
|
+
{ type: 'list-item', props: { content: 'Item' } },
|
|
438
|
+
],
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const result = renderUnicode(node)
|
|
442
|
+
|
|
443
|
+
// Should use unicode bullet, not ASCII asterisk
|
|
444
|
+
expect(result).toContain(UNICODE_CHARS.bullet)
|
|
445
|
+
expect(result).not.toMatch(/^\s*\*\s/m)
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
it('renders ordered list with numbers', () => {
|
|
449
|
+
const node: UINode = {
|
|
450
|
+
type: 'list',
|
|
451
|
+
props: { style: 'ordered' },
|
|
452
|
+
children: [
|
|
453
|
+
{ type: 'list-item', props: { content: 'First' } },
|
|
454
|
+
{ type: 'list-item', props: { content: 'Second' } },
|
|
455
|
+
{ type: 'list-item', props: { content: 'Third' } },
|
|
456
|
+
],
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const result = renderUnicode(node)
|
|
460
|
+
|
|
461
|
+
expect(result).toMatch(/1\.\s*First/)
|
|
462
|
+
expect(result).toMatch(/2\.\s*Second/)
|
|
463
|
+
expect(result).toMatch(/3\.\s*Third/)
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
it('renders nested list with hollow bullets', () => {
|
|
467
|
+
const node: UINode = {
|
|
468
|
+
type: 'list',
|
|
469
|
+
props: { style: 'unordered' },
|
|
470
|
+
children: [
|
|
471
|
+
{
|
|
472
|
+
type: 'list-item',
|
|
473
|
+
props: { content: 'Parent' },
|
|
474
|
+
children: [
|
|
475
|
+
{
|
|
476
|
+
type: 'list',
|
|
477
|
+
props: { style: 'unordered' },
|
|
478
|
+
children: [
|
|
479
|
+
{ type: 'list-item', props: { content: 'Child' } },
|
|
480
|
+
],
|
|
481
|
+
},
|
|
482
|
+
],
|
|
483
|
+
},
|
|
484
|
+
],
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const result = renderUnicode(node)
|
|
488
|
+
|
|
489
|
+
// Parent uses solid bullet, nested uses hollow
|
|
490
|
+
expect(result).toContain(UNICODE_CHARS.bullet)
|
|
491
|
+
expect(result).toContain(UNICODE_CHARS.hollowBullet)
|
|
492
|
+
})
|
|
493
|
+
|
|
494
|
+
it('renders checklist with checkmark and cross', () => {
|
|
495
|
+
const node: UINode = {
|
|
496
|
+
type: 'list',
|
|
497
|
+
props: { style: 'checklist' },
|
|
498
|
+
children: [
|
|
499
|
+
{ type: 'list-item', props: { content: 'Done task', checked: true } },
|
|
500
|
+
{ type: 'list-item', props: { content: 'Todo task', checked: false } },
|
|
501
|
+
],
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const result = renderUnicode(node)
|
|
505
|
+
|
|
506
|
+
expect(result).toContain(UNICODE_CHARS.checkmark)
|
|
507
|
+
expect(result).toContain('Done task')
|
|
508
|
+
// Should show unchecked indicator
|
|
509
|
+
expect(result).toMatch(/[☐◻□]/)
|
|
510
|
+
})
|
|
511
|
+
})
|
|
512
|
+
|
|
513
|
+
// ============================================================================
|
|
514
|
+
// Progress Bar Tests
|
|
515
|
+
// ============================================================================
|
|
516
|
+
|
|
517
|
+
describe('progress bar', () => {
|
|
518
|
+
it('renders progress bar with unicode block characters', () => {
|
|
519
|
+
const node: UINode = {
|
|
520
|
+
type: 'progress',
|
|
521
|
+
props: { value: 50, max: 100, width: 10 },
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const result = renderUnicode(node)
|
|
525
|
+
|
|
526
|
+
expect(result).toContain(UNICODE_CHARS.progressFull)
|
|
527
|
+
expect(result).toContain(UNICODE_CHARS.progressEmpty)
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
it('does NOT use ASCII hash for progress', () => {
|
|
531
|
+
const node: UINode = {
|
|
532
|
+
type: 'progress',
|
|
533
|
+
props: { value: 50, max: 100, width: 10 },
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const result = renderUnicode(node)
|
|
537
|
+
|
|
538
|
+
// Should NOT use ASCII progress characters
|
|
539
|
+
expect(result).not.toMatch(/#+/)
|
|
540
|
+
expect(result).not.toMatch(/-{2,}/)
|
|
541
|
+
})
|
|
542
|
+
|
|
543
|
+
it('renders 0% progress as all empty', () => {
|
|
544
|
+
const node: UINode = {
|
|
545
|
+
type: 'progress',
|
|
546
|
+
props: { value: 0, max: 100, width: 10 },
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const result = renderUnicode(node)
|
|
550
|
+
|
|
551
|
+
expect(result).toContain(UNICODE_CHARS.progressEmpty)
|
|
552
|
+
expect(result).not.toContain(UNICODE_CHARS.progressFull)
|
|
553
|
+
})
|
|
554
|
+
|
|
555
|
+
it('renders 100% progress as all full', () => {
|
|
556
|
+
const node: UINode = {
|
|
557
|
+
type: 'progress',
|
|
558
|
+
props: { value: 100, max: 100, width: 10 },
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const result = renderUnicode(node)
|
|
562
|
+
|
|
563
|
+
expect(result).toContain(UNICODE_CHARS.progressFull)
|
|
564
|
+
expect(result).not.toContain(UNICODE_CHARS.progressEmpty)
|
|
565
|
+
})
|
|
566
|
+
|
|
567
|
+
it('renders partial progress with half block', () => {
|
|
568
|
+
const node: UINode = {
|
|
569
|
+
type: 'progress',
|
|
570
|
+
props: { value: 55, max: 100, width: 10 },
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const result = renderUnicode(node)
|
|
574
|
+
|
|
575
|
+
// At 55% with width 10, we have 5.5 filled blocks
|
|
576
|
+
// Implementation should use half block for the partial
|
|
577
|
+
expect(result).toContain(UNICODE_CHARS.progressFull)
|
|
578
|
+
expect(result).toContain(UNICODE_CHARS.progressEmpty)
|
|
579
|
+
// May contain half block for partial fill
|
|
580
|
+
expect(result.match(new RegExp(`[${UNICODE_CHARS.progressFull}${UNICODE_CHARS.progressHalf}${UNICODE_CHARS.progressEmpty}]`, 'g'))).toBeTruthy()
|
|
581
|
+
})
|
|
582
|
+
})
|
|
583
|
+
|
|
584
|
+
// ============================================================================
|
|
585
|
+
// Table Rendering Tests
|
|
586
|
+
// ============================================================================
|
|
587
|
+
|
|
588
|
+
describe('table rendering', () => {
|
|
589
|
+
it('renders table with unicode borders', () => {
|
|
590
|
+
const node: UINode = {
|
|
591
|
+
type: 'table',
|
|
592
|
+
props: {
|
|
593
|
+
columns: [
|
|
594
|
+
{ key: 'name', header: 'Name', width: 10 },
|
|
595
|
+
{ key: 'age', header: 'Age', width: 5 },
|
|
596
|
+
],
|
|
597
|
+
},
|
|
598
|
+
data: [
|
|
599
|
+
{ name: 'Alice', age: 30 },
|
|
600
|
+
{ name: 'Bob', age: 25 },
|
|
601
|
+
],
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const result = renderUnicode(node)
|
|
605
|
+
|
|
606
|
+
// Should contain unicode box characters
|
|
607
|
+
expect(result).toContain(UNICODE_CHARS.horizontal)
|
|
608
|
+
expect(result).toContain(UNICODE_CHARS.vertical)
|
|
609
|
+
})
|
|
610
|
+
|
|
611
|
+
it('renders table header separator with T-junctions', () => {
|
|
612
|
+
const node: UINode = {
|
|
613
|
+
type: 'table',
|
|
614
|
+
props: {
|
|
615
|
+
columns: [
|
|
616
|
+
{ key: 'col1', header: 'Column 1', width: 10 },
|
|
617
|
+
{ key: 'col2', header: 'Column 2', width: 10 },
|
|
618
|
+
],
|
|
619
|
+
},
|
|
620
|
+
data: [{ col1: 'a', col2: 'b' }],
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const result = renderUnicode(node)
|
|
624
|
+
|
|
625
|
+
// Header separator should use T-junctions
|
|
626
|
+
expect(result).toContain(UNICODE_CHARS.teeTop)
|
|
627
|
+
expect(result).toContain(UNICODE_CHARS.teeBottom)
|
|
628
|
+
})
|
|
629
|
+
|
|
630
|
+
it('renders table corner intersections', () => {
|
|
631
|
+
const node: UINode = {
|
|
632
|
+
type: 'table',
|
|
633
|
+
props: {
|
|
634
|
+
columns: [
|
|
635
|
+
{ key: 'a', header: 'A', width: 5 },
|
|
636
|
+
{ key: 'b', header: 'B', width: 5 },
|
|
637
|
+
],
|
|
638
|
+
},
|
|
639
|
+
data: [{ a: '1', b: '2' }],
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const result = renderUnicode(node)
|
|
643
|
+
|
|
644
|
+
expect(result).toContain(UNICODE_CHARS.topLeft)
|
|
645
|
+
expect(result).toContain(UNICODE_CHARS.topRight)
|
|
646
|
+
expect(result).toContain(UNICODE_CHARS.bottomLeft)
|
|
647
|
+
expect(result).toContain(UNICODE_CHARS.bottomRight)
|
|
648
|
+
})
|
|
649
|
+
|
|
650
|
+
it('renders table row separators with cross junctions', () => {
|
|
651
|
+
const node: UINode = {
|
|
652
|
+
type: 'table',
|
|
653
|
+
props: {
|
|
654
|
+
columns: [
|
|
655
|
+
{ key: 'a', header: 'A', width: 5 },
|
|
656
|
+
{ key: 'b', header: 'B', width: 5 },
|
|
657
|
+
],
|
|
658
|
+
rowSeparators: true,
|
|
659
|
+
},
|
|
660
|
+
data: [
|
|
661
|
+
{ a: '1', b: '2' },
|
|
662
|
+
{ a: '3', b: '4' },
|
|
663
|
+
],
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const result = renderUnicode(node)
|
|
667
|
+
|
|
668
|
+
// Row separators should include cross junctions
|
|
669
|
+
expect(result).toContain(UNICODE_CHARS.crossJunction)
|
|
670
|
+
})
|
|
671
|
+
|
|
672
|
+
it('does NOT use ASCII characters for table borders', () => {
|
|
673
|
+
const node: UINode = {
|
|
674
|
+
type: 'table',
|
|
675
|
+
props: {
|
|
676
|
+
columns: [{ key: 'a', header: 'A', width: 5 }],
|
|
677
|
+
},
|
|
678
|
+
data: [{ a: '1' }],
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const result = renderUnicode(node)
|
|
682
|
+
|
|
683
|
+
// Should NOT have ASCII box drawing
|
|
684
|
+
expect(result).not.toMatch(/^\+-+\+$/m)
|
|
685
|
+
expect(result).not.toMatch(/^\|.+\|$/m)
|
|
686
|
+
})
|
|
687
|
+
})
|
|
688
|
+
|
|
689
|
+
// ============================================================================
|
|
690
|
+
// Panel/Card Rendering Tests
|
|
691
|
+
// ============================================================================
|
|
692
|
+
|
|
693
|
+
describe('panel rendering', () => {
|
|
694
|
+
it('renders panel with title bar', () => {
|
|
695
|
+
const node: UINode = {
|
|
696
|
+
type: 'panel',
|
|
697
|
+
props: { title: 'My Panel', width: 20 },
|
|
698
|
+
children: [{ type: 'text', props: { content: 'Content' } }],
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
const result = renderUnicode(node)
|
|
702
|
+
|
|
703
|
+
expect(result).toContain('My Panel')
|
|
704
|
+
expect(result).toContain('Content')
|
|
705
|
+
expect(result).toContain(UNICODE_CHARS.topLeft)
|
|
706
|
+
})
|
|
707
|
+
|
|
708
|
+
it('renders panel title with unicode divider', () => {
|
|
709
|
+
const node: UINode = {
|
|
710
|
+
type: 'panel',
|
|
711
|
+
props: { title: 'Title', width: 20 },
|
|
712
|
+
children: [{ type: 'text', props: { content: 'Body' } }],
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const result = renderUnicode(node)
|
|
716
|
+
|
|
717
|
+
// Title should be separated from body
|
|
718
|
+
expect(result).toContain(UNICODE_CHARS.teeLeft)
|
|
719
|
+
expect(result).toContain(UNICODE_CHARS.teeRight)
|
|
720
|
+
})
|
|
721
|
+
})
|
|
722
|
+
|
|
723
|
+
// ============================================================================
|
|
724
|
+
// Divider/Separator Tests
|
|
725
|
+
// ============================================================================
|
|
726
|
+
|
|
727
|
+
describe('divider rendering', () => {
|
|
728
|
+
it('renders horizontal divider with unicode line', () => {
|
|
729
|
+
const node: UINode = {
|
|
730
|
+
type: 'divider',
|
|
731
|
+
props: { width: 20 },
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
const result = renderUnicode(node)
|
|
735
|
+
|
|
736
|
+
expect(result).toContain(UNICODE_CHARS.horizontal)
|
|
737
|
+
expect(result.length).toBeGreaterThanOrEqual(20)
|
|
738
|
+
})
|
|
739
|
+
|
|
740
|
+
it('renders divider with label', () => {
|
|
741
|
+
const node: UINode = {
|
|
742
|
+
type: 'divider',
|
|
743
|
+
props: { label: 'Section', width: 30 },
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
const result = renderUnicode(node)
|
|
747
|
+
|
|
748
|
+
expect(result).toContain('Section')
|
|
749
|
+
expect(result).toContain(UNICODE_CHARS.horizontal)
|
|
750
|
+
})
|
|
751
|
+
|
|
752
|
+
it('does NOT use ASCII dash for divider', () => {
|
|
753
|
+
const node: UINode = {
|
|
754
|
+
type: 'divider',
|
|
755
|
+
props: { width: 20 },
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const result = renderUnicode(node)
|
|
759
|
+
|
|
760
|
+
// Should not be a line of dashes
|
|
761
|
+
expect(result).not.toMatch(/^-+$/)
|
|
762
|
+
})
|
|
763
|
+
})
|
|
764
|
+
|
|
765
|
+
// ============================================================================
|
|
766
|
+
// Spinner/Loading Tests
|
|
767
|
+
// ============================================================================
|
|
768
|
+
|
|
769
|
+
describe('spinner rendering', () => {
|
|
770
|
+
it('renders spinner with unicode braille pattern', () => {
|
|
771
|
+
const node: UINode = {
|
|
772
|
+
type: 'spinner',
|
|
773
|
+
props: { frame: 0 },
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
const result = renderUnicode(node)
|
|
777
|
+
|
|
778
|
+
// Should contain a unicode spinner character (braille dots, etc.)
|
|
779
|
+
// Common unicode spinners: ⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏
|
|
780
|
+
expect(result).toMatch(/[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏⣾⣽⣻⢿⡿⣟⣯⣷]/)
|
|
781
|
+
})
|
|
782
|
+
|
|
783
|
+
it('renders spinner with label', () => {
|
|
784
|
+
const node: UINode = {
|
|
785
|
+
type: 'spinner',
|
|
786
|
+
props: { frame: 0, label: 'Loading...' },
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
const result = renderUnicode(node)
|
|
790
|
+
|
|
791
|
+
expect(result).toContain('Loading...')
|
|
792
|
+
})
|
|
793
|
+
|
|
794
|
+
it('does NOT use ASCII spinner characters', () => {
|
|
795
|
+
const node: UINode = {
|
|
796
|
+
type: 'spinner',
|
|
797
|
+
props: { frame: 0 },
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
const result = renderUnicode(node)
|
|
801
|
+
|
|
802
|
+
// Should NOT use ASCII spinner like | / - \
|
|
803
|
+
expect(result).not.toMatch(/^[|\\/-]$/)
|
|
804
|
+
})
|
|
805
|
+
})
|
|
806
|
+
|
|
807
|
+
// ============================================================================
|
|
808
|
+
// Badge/Status Indicator Tests
|
|
809
|
+
// ============================================================================
|
|
810
|
+
|
|
811
|
+
describe('badge rendering', () => {
|
|
812
|
+
it('renders badge with unicode brackets', () => {
|
|
813
|
+
const node: UINode = {
|
|
814
|
+
type: 'badge',
|
|
815
|
+
props: { content: 'NEW', variant: 'info' },
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
const result = renderUnicode(node)
|
|
819
|
+
|
|
820
|
+
expect(result).toContain('NEW')
|
|
821
|
+
// May use unicode brackets or rounded corners
|
|
822
|
+
expect(result).toMatch(/[【】「」『』〖〗\[〔〕]/)
|
|
823
|
+
})
|
|
824
|
+
|
|
825
|
+
it('renders status indicator with unicode symbols', () => {
|
|
826
|
+
const node: UINode = {
|
|
827
|
+
type: 'badge',
|
|
828
|
+
props: { content: 'Success', variant: 'success' },
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
const result = renderUnicode(node)
|
|
832
|
+
|
|
833
|
+
// Success might include checkmark
|
|
834
|
+
expect(result).toContain('Success')
|
|
835
|
+
})
|
|
836
|
+
})
|
|
837
|
+
|
|
838
|
+
// ============================================================================
|
|
839
|
+
// Tree/Hierarchy Rendering Tests
|
|
840
|
+
// ============================================================================
|
|
841
|
+
|
|
842
|
+
describe('tree rendering', () => {
|
|
843
|
+
it('renders tree structure with unicode connectors', () => {
|
|
844
|
+
const node: UINode = {
|
|
845
|
+
type: 'tree',
|
|
846
|
+
props: {},
|
|
847
|
+
children: [
|
|
848
|
+
{
|
|
849
|
+
type: 'tree-item',
|
|
850
|
+
props: { label: 'Root' },
|
|
851
|
+
children: [
|
|
852
|
+
{ type: 'tree-item', props: { label: 'Child 1' } },
|
|
853
|
+
{ type: 'tree-item', props: { label: 'Child 2' } },
|
|
854
|
+
],
|
|
855
|
+
},
|
|
856
|
+
],
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
const result = renderUnicode(node)
|
|
860
|
+
|
|
861
|
+
// Should use unicode tree connectors
|
|
862
|
+
expect(result).toContain('Root')
|
|
863
|
+
expect(result).toContain('Child 1')
|
|
864
|
+
expect(result).toContain('Child 2')
|
|
865
|
+
// Tree connectors: ├── └── │
|
|
866
|
+
expect(result).toContain(UNICODE_CHARS.teeLeft)
|
|
867
|
+
expect(result).toContain(UNICODE_CHARS.bottomLeft)
|
|
868
|
+
})
|
|
869
|
+
|
|
870
|
+
it('renders last child with corner connector', () => {
|
|
871
|
+
const node: UINode = {
|
|
872
|
+
type: 'tree',
|
|
873
|
+
props: {},
|
|
874
|
+
children: [
|
|
875
|
+
{
|
|
876
|
+
type: 'tree-item',
|
|
877
|
+
props: { label: 'Parent' },
|
|
878
|
+
children: [
|
|
879
|
+
{ type: 'tree-item', props: { label: 'Last child' } },
|
|
880
|
+
],
|
|
881
|
+
},
|
|
882
|
+
],
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
const result = renderUnicode(node)
|
|
886
|
+
|
|
887
|
+
// Last child should use └ not ├
|
|
888
|
+
expect(result).toContain(UNICODE_CHARS.bottomLeft)
|
|
889
|
+
})
|
|
890
|
+
|
|
891
|
+
it('does NOT use ASCII tree characters', () => {
|
|
892
|
+
const node: UINode = {
|
|
893
|
+
type: 'tree',
|
|
894
|
+
props: {},
|
|
895
|
+
children: [
|
|
896
|
+
{
|
|
897
|
+
type: 'tree-item',
|
|
898
|
+
props: { label: 'Root' },
|
|
899
|
+
children: [
|
|
900
|
+
{ type: 'tree-item', props: { label: 'Child' } },
|
|
901
|
+
],
|
|
902
|
+
},
|
|
903
|
+
],
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
const result = renderUnicode(node)
|
|
907
|
+
|
|
908
|
+
// Should NOT use ASCII +-- or |-- patterns
|
|
909
|
+
expect(result).not.toMatch(/\+--/)
|
|
910
|
+
expect(result).not.toMatch(/\|--/)
|
|
911
|
+
})
|
|
912
|
+
})
|
|
913
|
+
|
|
914
|
+
// ============================================================================
|
|
915
|
+
// Breadcrumb Rendering Tests
|
|
916
|
+
// ============================================================================
|
|
917
|
+
|
|
918
|
+
describe('breadcrumb rendering', () => {
|
|
919
|
+
it('renders breadcrumb with unicode arrow separator', () => {
|
|
920
|
+
const node: UINode = {
|
|
921
|
+
type: 'breadcrumb',
|
|
922
|
+
props: {},
|
|
923
|
+
children: [
|
|
924
|
+
{ type: 'breadcrumb-item', props: { label: 'Home' } },
|
|
925
|
+
{ type: 'breadcrumb-item', props: { label: 'Products' } },
|
|
926
|
+
{ type: 'breadcrumb-item', props: { label: 'Item' } },
|
|
927
|
+
],
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
const result = renderUnicode(node)
|
|
931
|
+
|
|
932
|
+
expect(result).toContain('Home')
|
|
933
|
+
expect(result).toContain('Products')
|
|
934
|
+
expect(result).toContain('Item')
|
|
935
|
+
// Should use unicode separator like → or ›
|
|
936
|
+
expect(result).toMatch(/[→›»▸]/)
|
|
937
|
+
})
|
|
938
|
+
|
|
939
|
+
it('does NOT use ASCII greater-than for separator', () => {
|
|
940
|
+
const node: UINode = {
|
|
941
|
+
type: 'breadcrumb',
|
|
942
|
+
props: {},
|
|
943
|
+
children: [
|
|
944
|
+
{ type: 'breadcrumb-item', props: { label: 'A' } },
|
|
945
|
+
{ type: 'breadcrumb-item', props: { label: 'B' } },
|
|
946
|
+
],
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
const result = renderUnicode(node)
|
|
950
|
+
|
|
951
|
+
// Should NOT use plain > as separator
|
|
952
|
+
expect(result).not.toMatch(/A\s*>\s*B/)
|
|
953
|
+
})
|
|
954
|
+
})
|
|
955
|
+
|
|
956
|
+
// ============================================================================
|
|
957
|
+
// Tooltip/Info Rendering Tests
|
|
958
|
+
// ============================================================================
|
|
959
|
+
|
|
960
|
+
describe('tooltip rendering', () => {
|
|
961
|
+
it('renders tooltip with unicode pointer', () => {
|
|
962
|
+
const node: UINode = {
|
|
963
|
+
type: 'tooltip',
|
|
964
|
+
props: { content: 'Helpful tip', position: 'top' },
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
const result = renderUnicode(node)
|
|
968
|
+
|
|
969
|
+
expect(result).toContain('Helpful tip')
|
|
970
|
+
// Should have unicode pointer like ▲ ▼ ◀ ▶
|
|
971
|
+
expect(result).toMatch(/[▲▼◀▶△▽◁▷]/)
|
|
972
|
+
})
|
|
973
|
+
})
|
|
974
|
+
|
|
975
|
+
// ============================================================================
|
|
976
|
+
// Edge Cases and Error Handling
|
|
977
|
+
// ============================================================================
|
|
978
|
+
|
|
979
|
+
describe('edge cases', () => {
|
|
980
|
+
it('handles empty children array', () => {
|
|
981
|
+
const node: UINode = {
|
|
982
|
+
type: 'box',
|
|
983
|
+
props: { border: 'single' },
|
|
984
|
+
children: [],
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
const result = renderUnicode(node)
|
|
988
|
+
|
|
989
|
+
expect(result).toBeDefined()
|
|
990
|
+
expect(result).toContain(UNICODE_CHARS.topLeft)
|
|
991
|
+
})
|
|
992
|
+
|
|
993
|
+
it('handles undefined children', () => {
|
|
994
|
+
const node: UINode = {
|
|
995
|
+
type: 'box',
|
|
996
|
+
props: { border: 'single' },
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
const result = renderUnicode(node)
|
|
1000
|
+
|
|
1001
|
+
expect(result).toBeDefined()
|
|
1002
|
+
})
|
|
1003
|
+
|
|
1004
|
+
it('handles zero-width box gracefully', () => {
|
|
1005
|
+
const node: UINode = {
|
|
1006
|
+
type: 'box',
|
|
1007
|
+
props: { border: 'single', width: 0, height: 3 },
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
const result = renderUnicode(node)
|
|
1011
|
+
|
|
1012
|
+
// Should either render minimum size or empty string
|
|
1013
|
+
expect(typeof result).toBe('string')
|
|
1014
|
+
})
|
|
1015
|
+
|
|
1016
|
+
it('handles zero-height box gracefully', () => {
|
|
1017
|
+
const node: UINode = {
|
|
1018
|
+
type: 'box',
|
|
1019
|
+
props: { border: 'single', width: 10, height: 0 },
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
const result = renderUnicode(node)
|
|
1023
|
+
|
|
1024
|
+
expect(typeof result).toBe('string')
|
|
1025
|
+
})
|
|
1026
|
+
|
|
1027
|
+
it('handles very long text', () => {
|
|
1028
|
+
const longText = 'A'.repeat(1000)
|
|
1029
|
+
const node: UINode = {
|
|
1030
|
+
type: 'text',
|
|
1031
|
+
props: { content: longText },
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
const result = renderUnicode(node)
|
|
1035
|
+
|
|
1036
|
+
expect(result).toContain('A')
|
|
1037
|
+
})
|
|
1038
|
+
|
|
1039
|
+
it('handles special unicode characters in content', () => {
|
|
1040
|
+
const node: UINode = {
|
|
1041
|
+
type: 'text',
|
|
1042
|
+
props: { content: 'Emoji: 🎉 and symbols: ∞ ≠ ≤' },
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
const result = renderUnicode(node)
|
|
1046
|
+
|
|
1047
|
+
expect(result).toContain('🎉')
|
|
1048
|
+
expect(result).toContain('∞')
|
|
1049
|
+
})
|
|
1050
|
+
|
|
1051
|
+
it('handles RTL text', () => {
|
|
1052
|
+
const node: UINode = {
|
|
1053
|
+
type: 'text',
|
|
1054
|
+
props: { content: 'مرحبا' }, // Arabic "Hello"
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
const result = renderUnicode(node)
|
|
1058
|
+
|
|
1059
|
+
expect(result).toContain('مرحبا')
|
|
1060
|
+
})
|
|
1061
|
+
|
|
1062
|
+
it('handles unknown node type gracefully', () => {
|
|
1063
|
+
const node: UINode = {
|
|
1064
|
+
type: 'unknown-type',
|
|
1065
|
+
props: { content: 'Test' },
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// Should not throw, may return empty or placeholder
|
|
1069
|
+
expect(() => renderUnicode(node)).not.toThrow()
|
|
1070
|
+
})
|
|
1071
|
+
})
|
|
1072
|
+
|
|
1073
|
+
// ============================================================================
|
|
1074
|
+
// Context Handling Tests
|
|
1075
|
+
// ============================================================================
|
|
1076
|
+
|
|
1077
|
+
describe('context handling', () => {
|
|
1078
|
+
it('respects width from context', () => {
|
|
1079
|
+
const node: UINode = {
|
|
1080
|
+
type: 'divider',
|
|
1081
|
+
props: {},
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
const context = createTestContext({ width: 40 })
|
|
1085
|
+
const result = renderUnicode(node, context)
|
|
1086
|
+
|
|
1087
|
+
// Divider should respect context width
|
|
1088
|
+
expect(result.length).toBeLessThanOrEqual(40)
|
|
1089
|
+
})
|
|
1090
|
+
|
|
1091
|
+
it('increases depth for nested children', () => {
|
|
1092
|
+
const node: UINode = {
|
|
1093
|
+
type: 'box',
|
|
1094
|
+
props: {},
|
|
1095
|
+
children: [
|
|
1096
|
+
{
|
|
1097
|
+
type: 'box',
|
|
1098
|
+
props: {},
|
|
1099
|
+
children: [{ type: 'text', props: { content: 'Deep' } }],
|
|
1100
|
+
},
|
|
1101
|
+
],
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// This is more of an internal test - ensuring nested rendering works
|
|
1105
|
+
const result = renderUnicode(node)
|
|
1106
|
+
|
|
1107
|
+
expect(result).toContain('Deep')
|
|
1108
|
+
})
|
|
1109
|
+
|
|
1110
|
+
it('uses default context when none provided', () => {
|
|
1111
|
+
const node: UINode = {
|
|
1112
|
+
type: 'text',
|
|
1113
|
+
props: { content: 'Hello' },
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// Should not throw when context is omitted
|
|
1117
|
+
const result = renderUnicode(node)
|
|
1118
|
+
|
|
1119
|
+
expect(result).toBe('Hello')
|
|
1120
|
+
})
|
|
1121
|
+
})
|
|
1122
|
+
|
|
1123
|
+
// ============================================================================
|
|
1124
|
+
// Comparison with ASCII Fallback
|
|
1125
|
+
// ============================================================================
|
|
1126
|
+
|
|
1127
|
+
describe('ASCII fallback verification', () => {
|
|
1128
|
+
it('uses unicode corners, not ASCII plus signs', () => {
|
|
1129
|
+
const node: UINode = {
|
|
1130
|
+
type: 'box',
|
|
1131
|
+
props: { border: 'single', width: 10, height: 3 },
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
const result = renderUnicode(node)
|
|
1135
|
+
|
|
1136
|
+
// Corners should be unicode, not +
|
|
1137
|
+
expect(result).not.toMatch(/^\+/)
|
|
1138
|
+
expect(result).not.toMatch(/\+$/)
|
|
1139
|
+
expect(result).toContain(UNICODE_CHARS.topLeft)
|
|
1140
|
+
})
|
|
1141
|
+
|
|
1142
|
+
it('uses unicode horizontal lines, not dashes', () => {
|
|
1143
|
+
const node: UINode = {
|
|
1144
|
+
type: 'box',
|
|
1145
|
+
props: { border: 'single', width: 10, height: 3 },
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
const result = renderUnicode(node)
|
|
1149
|
+
|
|
1150
|
+
// Top/bottom lines should use ─ not -
|
|
1151
|
+
const lines = result.split('\n')
|
|
1152
|
+
expect(lines[0]).toContain(UNICODE_CHARS.horizontal)
|
|
1153
|
+
expect(lines[0]).not.toMatch(/^.+-+.+$/) // Not a row of dashes
|
|
1154
|
+
})
|
|
1155
|
+
|
|
1156
|
+
it('uses unicode vertical lines, not pipes', () => {
|
|
1157
|
+
const node: UINode = {
|
|
1158
|
+
type: 'box',
|
|
1159
|
+
props: { border: 'single', width: 10, height: 5 },
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
const result = renderUnicode(node)
|
|
1163
|
+
|
|
1164
|
+
// Side lines should use │ not |
|
|
1165
|
+
expect(result).toContain(UNICODE_CHARS.vertical)
|
|
1166
|
+
// The ASCII pipe | should not appear in borders
|
|
1167
|
+
// (It might appear in content, so we check specific pattern)
|
|
1168
|
+
const lines = result.split('\n')
|
|
1169
|
+
for (let i = 1; i < lines.length - 1; i++) {
|
|
1170
|
+
expect(lines[i]).toMatch(/^│.*│$/)
|
|
1171
|
+
expect(lines[i]).not.toMatch(/^\|.*\|$/)
|
|
1172
|
+
}
|
|
1173
|
+
})
|
|
1174
|
+
|
|
1175
|
+
it('uses unicode bullets, not asterisks', () => {
|
|
1176
|
+
const node: UINode = {
|
|
1177
|
+
type: 'list',
|
|
1178
|
+
props: { style: 'unordered' },
|
|
1179
|
+
children: [
|
|
1180
|
+
{ type: 'list-item', props: { content: 'Item' } },
|
|
1181
|
+
],
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
const result = renderUnicode(node)
|
|
1185
|
+
|
|
1186
|
+
expect(result).toContain(UNICODE_CHARS.bullet)
|
|
1187
|
+
expect(result).not.toMatch(/^\s*\*\s/m)
|
|
1188
|
+
})
|
|
1189
|
+
})
|
|
1190
|
+
|
|
1191
|
+
// ============================================================================
|
|
1192
|
+
// Integration Tests
|
|
1193
|
+
// ============================================================================
|
|
1194
|
+
|
|
1195
|
+
describe('integration', () => {
|
|
1196
|
+
it('renders complex nested structure', () => {
|
|
1197
|
+
const node: UINode = {
|
|
1198
|
+
type: 'panel',
|
|
1199
|
+
props: { title: 'Dashboard', width: 40 },
|
|
1200
|
+
children: [
|
|
1201
|
+
{
|
|
1202
|
+
type: 'box',
|
|
1203
|
+
props: { border: 'rounded' },
|
|
1204
|
+
children: [
|
|
1205
|
+
{ type: 'text', props: { content: 'Welcome!' } },
|
|
1206
|
+
{
|
|
1207
|
+
type: 'list',
|
|
1208
|
+
props: { style: 'unordered' },
|
|
1209
|
+
children: [
|
|
1210
|
+
{ type: 'list-item', props: { content: 'Feature A' } },
|
|
1211
|
+
{ type: 'list-item', props: { content: 'Feature B' } },
|
|
1212
|
+
],
|
|
1213
|
+
},
|
|
1214
|
+
],
|
|
1215
|
+
},
|
|
1216
|
+
{
|
|
1217
|
+
type: 'progress',
|
|
1218
|
+
props: { value: 75, max: 100, width: 20 },
|
|
1219
|
+
},
|
|
1220
|
+
],
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
const result = renderUnicode(node)
|
|
1224
|
+
|
|
1225
|
+
// All content should be present
|
|
1226
|
+
expect(result).toContain('Dashboard')
|
|
1227
|
+
expect(result).toContain('Welcome!')
|
|
1228
|
+
expect(result).toContain('Feature A')
|
|
1229
|
+
expect(result).toContain('Feature B')
|
|
1230
|
+
|
|
1231
|
+
// Should use unicode characters throughout
|
|
1232
|
+
expect(result).toContain(UNICODE_CHARS.roundedTopLeft)
|
|
1233
|
+
expect(result).toContain(UNICODE_CHARS.bullet)
|
|
1234
|
+
expect(result).toContain(UNICODE_CHARS.progressFull)
|
|
1235
|
+
})
|
|
1236
|
+
|
|
1237
|
+
it('renders sidebar layout with unicode borders', () => {
|
|
1238
|
+
const node: UINode = {
|
|
1239
|
+
type: 'box',
|
|
1240
|
+
props: { border: 'single', flexDirection: 'row' },
|
|
1241
|
+
children: [
|
|
1242
|
+
{
|
|
1243
|
+
type: 'box',
|
|
1244
|
+
props: { border: 'single', width: 20 },
|
|
1245
|
+
children: [
|
|
1246
|
+
{
|
|
1247
|
+
type: 'list',
|
|
1248
|
+
props: { style: 'unordered' },
|
|
1249
|
+
children: [
|
|
1250
|
+
{ type: 'list-item', props: { content: 'Menu 1' } },
|
|
1251
|
+
{ type: 'list-item', props: { content: 'Menu 2' } },
|
|
1252
|
+
],
|
|
1253
|
+
},
|
|
1254
|
+
],
|
|
1255
|
+
},
|
|
1256
|
+
{
|
|
1257
|
+
type: 'box',
|
|
1258
|
+
props: { flexGrow: 1 },
|
|
1259
|
+
children: [
|
|
1260
|
+
{ type: 'text', props: { content: 'Main content area' } },
|
|
1261
|
+
],
|
|
1262
|
+
},
|
|
1263
|
+
],
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
const result = renderUnicode(node)
|
|
1267
|
+
|
|
1268
|
+
expect(result).toContain('Menu 1')
|
|
1269
|
+
expect(result).toContain('Menu 2')
|
|
1270
|
+
expect(result).toContain('Main content area')
|
|
1271
|
+
expect(result).toContain(UNICODE_CHARS.vertical)
|
|
1272
|
+
})
|
|
1273
|
+
|
|
1274
|
+
it('renders form with input fields', () => {
|
|
1275
|
+
const node: UINode = {
|
|
1276
|
+
type: 'box',
|
|
1277
|
+
props: { border: 'single', padding: 1 },
|
|
1278
|
+
children: [
|
|
1279
|
+
{ type: 'text', props: { content: 'Login Form' } },
|
|
1280
|
+
{
|
|
1281
|
+
type: 'divider',
|
|
1282
|
+
props: { width: 30 },
|
|
1283
|
+
},
|
|
1284
|
+
{
|
|
1285
|
+
type: 'input',
|
|
1286
|
+
props: { label: 'Username', value: 'john_doe' },
|
|
1287
|
+
},
|
|
1288
|
+
{
|
|
1289
|
+
type: 'input',
|
|
1290
|
+
props: { label: 'Password', value: '********', type: 'password' },
|
|
1291
|
+
},
|
|
1292
|
+
{
|
|
1293
|
+
type: 'button',
|
|
1294
|
+
props: { label: 'Submit' },
|
|
1295
|
+
},
|
|
1296
|
+
],
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
const result = renderUnicode(node)
|
|
1300
|
+
|
|
1301
|
+
expect(result).toContain('Login Form')
|
|
1302
|
+
expect(result).toContain('Username')
|
|
1303
|
+
expect(result).toContain('Submit')
|
|
1304
|
+
expect(result).toContain(UNICODE_CHARS.horizontal)
|
|
1305
|
+
})
|
|
1306
|
+
})
|
|
1307
|
+
})
|