@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,1398 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mdxui/terminal UINode Type System Tests (RED phase)
|
|
3
|
+
*
|
|
4
|
+
* TDD RED Phase: These tests define the contract for the UINode type system
|
|
5
|
+
* that powers the Universal Terminal UI's multi-tier rendering architecture.
|
|
6
|
+
*
|
|
7
|
+
* The UINode system supports rendering to multiple tiers:
|
|
8
|
+
* - TEXT: Plain text without formatting
|
|
9
|
+
* - MARKDOWN: Markdown syntax for simple formatting
|
|
10
|
+
* - ASCII: ASCII art and basic drawing characters
|
|
11
|
+
* - UNICODE: Unicode box drawing and symbols
|
|
12
|
+
* - ANSI: Full ANSI escape sequences for colors/styles
|
|
13
|
+
* - INTERACTIVE: Full interactive terminal UI with input handling
|
|
14
|
+
*
|
|
15
|
+
* NOTE: These tests are expected to FAIL until implementation is complete.
|
|
16
|
+
* Run: pnpm --filter @mdxui/terminal test
|
|
17
|
+
*/
|
|
18
|
+
import { describe, it, expect, expectTypeOf } from 'vitest'
|
|
19
|
+
import { z } from 'zod'
|
|
20
|
+
|
|
21
|
+
// ============================================================================
|
|
22
|
+
// These imports WILL FAIL until src/core/types.ts is implemented
|
|
23
|
+
// ============================================================================
|
|
24
|
+
import type {
|
|
25
|
+
UINode,
|
|
26
|
+
RenderTier,
|
|
27
|
+
RenderContext,
|
|
28
|
+
ThemeTokens,
|
|
29
|
+
} from '../../core/types'
|
|
30
|
+
|
|
31
|
+
import {
|
|
32
|
+
UINodeSchema,
|
|
33
|
+
RenderTierSchema,
|
|
34
|
+
RenderContextSchema,
|
|
35
|
+
ThemeTokensSchema,
|
|
36
|
+
} from '../../core/types'
|
|
37
|
+
|
|
38
|
+
// ============================================================================
|
|
39
|
+
// UINode Interface Tests
|
|
40
|
+
// ============================================================================
|
|
41
|
+
|
|
42
|
+
describe('UINode Interface', () => {
|
|
43
|
+
describe('structure validation', () => {
|
|
44
|
+
it('requires type field as string', () => {
|
|
45
|
+
// UINode must have a type string identifying the component
|
|
46
|
+
const node: UINode = {
|
|
47
|
+
type: 'box',
|
|
48
|
+
props: {},
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
expect(node.type).toBe('box')
|
|
52
|
+
expectTypeOf(node.type).toBeString()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('requires props field as Record<string, unknown>', () => {
|
|
56
|
+
const node: UINode = {
|
|
57
|
+
type: 'text',
|
|
58
|
+
props: {
|
|
59
|
+
bold: true,
|
|
60
|
+
content: 'Hello',
|
|
61
|
+
size: 14,
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
expect(node.props).toBeDefined()
|
|
66
|
+
expect(typeof node.props).toBe('object')
|
|
67
|
+
expectTypeOf(node.props).toEqualTypeOf<Record<string, unknown>>()
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('allows optional children array of UINodes', () => {
|
|
71
|
+
const node: UINode = {
|
|
72
|
+
type: 'box',
|
|
73
|
+
props: { padding: 1 },
|
|
74
|
+
children: [
|
|
75
|
+
{ type: 'text', props: { content: 'Child 1' } },
|
|
76
|
+
{ type: 'text', props: { content: 'Child 2' } },
|
|
77
|
+
],
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
expect(node.children).toHaveLength(2)
|
|
81
|
+
expect(node.children![0].type).toBe('text')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('allows undefined children', () => {
|
|
85
|
+
const node: UINode = {
|
|
86
|
+
type: 'text',
|
|
87
|
+
props: {},
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
expect(node.children).toBeUndefined()
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('allows optional data field for bound query data', () => {
|
|
94
|
+
const queryResult = { users: [{ id: 1, name: 'Alice' }] }
|
|
95
|
+
const node: UINode = {
|
|
96
|
+
type: 'table',
|
|
97
|
+
props: {},
|
|
98
|
+
data: queryResult,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
expect(node.data).toBe(queryResult)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('allows optional key field for React-style reconciliation', () => {
|
|
105
|
+
const node: UINode = {
|
|
106
|
+
type: 'list-item',
|
|
107
|
+
props: {},
|
|
108
|
+
key: 'item-123',
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
expect(node.key).toBe('item-123')
|
|
112
|
+
expectTypeOf(node.key).toEqualTypeOf<string | undefined>()
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('supports deeply nested children', () => {
|
|
116
|
+
const node: UINode = {
|
|
117
|
+
type: 'box',
|
|
118
|
+
props: {},
|
|
119
|
+
children: [
|
|
120
|
+
{
|
|
121
|
+
type: 'panel',
|
|
122
|
+
props: { title: 'Section' },
|
|
123
|
+
children: [
|
|
124
|
+
{
|
|
125
|
+
type: 'text',
|
|
126
|
+
props: { content: 'Nested content' },
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
expect(node.children![0].children![0].type).toBe('text')
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
describe('type inference', () => {
|
|
138
|
+
it('infers correct structure from UINode type', () => {
|
|
139
|
+
// Type-level test: ensure UINode has correct shape
|
|
140
|
+
expectTypeOf<UINode>().toHaveProperty('type')
|
|
141
|
+
expectTypeOf<UINode>().toHaveProperty('props')
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('children array contains only UINode types', () => {
|
|
145
|
+
type ChildrenType = UINode['children']
|
|
146
|
+
expectTypeOf<ChildrenType>().toEqualTypeOf<UINode[] | undefined>()
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
// ============================================================================
|
|
152
|
+
// RenderTier Type Tests
|
|
153
|
+
// ============================================================================
|
|
154
|
+
|
|
155
|
+
describe('RenderTier Type', () => {
|
|
156
|
+
describe('tier completeness', () => {
|
|
157
|
+
it('includes text tier for plain text output', () => {
|
|
158
|
+
const tier: RenderTier = 'text'
|
|
159
|
+
expect(tier).toBe('text')
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('includes markdown tier for formatted text', () => {
|
|
163
|
+
const tier: RenderTier = 'markdown'
|
|
164
|
+
expect(tier).toBe('markdown')
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('includes ascii tier for ASCII art rendering', () => {
|
|
168
|
+
const tier: RenderTier = 'ascii'
|
|
169
|
+
expect(tier).toBe('ascii')
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('includes unicode tier for box drawing characters', () => {
|
|
173
|
+
const tier: RenderTier = 'unicode'
|
|
174
|
+
expect(tier).toBe('unicode')
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('includes ansi tier for colored terminal output', () => {
|
|
178
|
+
const tier: RenderTier = 'ansi'
|
|
179
|
+
expect(tier).toBe('ansi')
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('includes interactive tier for full terminal UI', () => {
|
|
183
|
+
const tier: RenderTier = 'interactive'
|
|
184
|
+
expect(tier).toBe('interactive')
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
describe('type exhaustiveness', () => {
|
|
189
|
+
it('only allows the 6 defined tiers', () => {
|
|
190
|
+
// This test verifies that RenderTier is a union of exactly 6 strings
|
|
191
|
+
const allTiers: RenderTier[] = [
|
|
192
|
+
'text',
|
|
193
|
+
'markdown',
|
|
194
|
+
'ascii',
|
|
195
|
+
'unicode',
|
|
196
|
+
'ansi',
|
|
197
|
+
'interactive',
|
|
198
|
+
]
|
|
199
|
+
|
|
200
|
+
expect(allTiers).toHaveLength(6)
|
|
201
|
+
|
|
202
|
+
// Type-level check: each tier is assignable to RenderTier
|
|
203
|
+
allTiers.forEach((tier) => {
|
|
204
|
+
expectTypeOf(tier).toMatchTypeOf<RenderTier>()
|
|
205
|
+
})
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it('rejects invalid tier strings at compile time', () => {
|
|
209
|
+
// This is a type-level test - invalid tiers should cause compile errors
|
|
210
|
+
// The test passes if the type system correctly rejects invalid values
|
|
211
|
+
// @ts-expect-error - 'invalid' is not a valid RenderTier
|
|
212
|
+
const _invalidTier: RenderTier = 'invalid'
|
|
213
|
+
|
|
214
|
+
// @ts-expect-error - 'html' is not a valid RenderTier
|
|
215
|
+
const _htmlTier: RenderTier = 'html'
|
|
216
|
+
|
|
217
|
+
// @ts-expect-error - numbers are not valid
|
|
218
|
+
const _numericTier: RenderTier = 42
|
|
219
|
+
})
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
describe('tier ordering', () => {
|
|
223
|
+
it('defines tiers in capability order (text < markdown < ascii < unicode < ansi < interactive)', () => {
|
|
224
|
+
// This documents the expected capability hierarchy
|
|
225
|
+
// Lower tiers have less capability, higher tiers have more
|
|
226
|
+
const tierOrder: Record<RenderTier, number> = {
|
|
227
|
+
text: 0,
|
|
228
|
+
markdown: 1,
|
|
229
|
+
ascii: 2,
|
|
230
|
+
unicode: 3,
|
|
231
|
+
ansi: 4,
|
|
232
|
+
interactive: 5,
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
expect(tierOrder.text).toBeLessThan(tierOrder.markdown)
|
|
236
|
+
expect(tierOrder.markdown).toBeLessThan(tierOrder.ascii)
|
|
237
|
+
expect(tierOrder.ascii).toBeLessThan(tierOrder.unicode)
|
|
238
|
+
expect(tierOrder.unicode).toBeLessThan(tierOrder.ansi)
|
|
239
|
+
expect(tierOrder.ansi).toBeLessThan(tierOrder.interactive)
|
|
240
|
+
})
|
|
241
|
+
})
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
// ============================================================================
|
|
245
|
+
// RenderContext Interface Tests
|
|
246
|
+
// ============================================================================
|
|
247
|
+
|
|
248
|
+
describe('RenderContext Interface', () => {
|
|
249
|
+
describe('required fields', () => {
|
|
250
|
+
it('requires tier field of type RenderTier', () => {
|
|
251
|
+
const context: RenderContext = {
|
|
252
|
+
tier: 'ansi',
|
|
253
|
+
width: 80,
|
|
254
|
+
height: 24,
|
|
255
|
+
depth: 0,
|
|
256
|
+
theme: {} as ThemeTokens,
|
|
257
|
+
interactive: false,
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
expect(context.tier).toBe('ansi')
|
|
261
|
+
expectTypeOf(context.tier).toEqualTypeOf<RenderTier>()
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
it('requires width field as number', () => {
|
|
265
|
+
const context: RenderContext = {
|
|
266
|
+
tier: 'text',
|
|
267
|
+
width: 120,
|
|
268
|
+
height: 40,
|
|
269
|
+
depth: 0,
|
|
270
|
+
theme: {} as ThemeTokens,
|
|
271
|
+
interactive: false,
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
expect(context.width).toBe(120)
|
|
275
|
+
expectTypeOf(context.width).toBeNumber()
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
it('requires height field as number', () => {
|
|
279
|
+
const context: RenderContext = {
|
|
280
|
+
tier: 'text',
|
|
281
|
+
width: 80,
|
|
282
|
+
height: 24,
|
|
283
|
+
depth: 0,
|
|
284
|
+
theme: {} as ThemeTokens,
|
|
285
|
+
interactive: false,
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
expect(context.height).toBe(24)
|
|
289
|
+
expectTypeOf(context.height).toBeNumber()
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
it('requires depth field as number for nesting level', () => {
|
|
293
|
+
const context: RenderContext = {
|
|
294
|
+
tier: 'unicode',
|
|
295
|
+
width: 80,
|
|
296
|
+
height: 24,
|
|
297
|
+
depth: 3,
|
|
298
|
+
theme: {} as ThemeTokens,
|
|
299
|
+
interactive: false,
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
expect(context.depth).toBe(3)
|
|
303
|
+
expectTypeOf(context.depth).toBeNumber()
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
it('requires theme field of type ThemeTokens', () => {
|
|
307
|
+
const theme: ThemeTokens = {
|
|
308
|
+
primary: '\x1b[34m',
|
|
309
|
+
secondary: '\x1b[36m',
|
|
310
|
+
muted: '\x1b[90m',
|
|
311
|
+
foreground: '\x1b[37m',
|
|
312
|
+
background: '\x1b[40m',
|
|
313
|
+
border: '\x1b[90m',
|
|
314
|
+
success: '\x1b[32m',
|
|
315
|
+
warning: '\x1b[33m',
|
|
316
|
+
error: '\x1b[31m',
|
|
317
|
+
info: '\x1b[34m',
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const context: RenderContext = {
|
|
321
|
+
tier: 'ansi',
|
|
322
|
+
width: 80,
|
|
323
|
+
height: 24,
|
|
324
|
+
depth: 0,
|
|
325
|
+
theme,
|
|
326
|
+
interactive: false,
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
expect(context.theme).toBe(theme)
|
|
330
|
+
expectTypeOf(context.theme).toEqualTypeOf<ThemeTokens>()
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
it('requires interactive field as boolean', () => {
|
|
334
|
+
const context: RenderContext = {
|
|
335
|
+
tier: 'interactive',
|
|
336
|
+
width: 80,
|
|
337
|
+
height: 24,
|
|
338
|
+
depth: 0,
|
|
339
|
+
theme: {} as ThemeTokens,
|
|
340
|
+
interactive: true,
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
expect(context.interactive).toBe(true)
|
|
344
|
+
expectTypeOf(context.interactive).toBeBoolean()
|
|
345
|
+
})
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
describe('field semantics', () => {
|
|
349
|
+
it('width represents terminal column count', () => {
|
|
350
|
+
const context: RenderContext = {
|
|
351
|
+
tier: 'text',
|
|
352
|
+
width: 80,
|
|
353
|
+
height: 24,
|
|
354
|
+
depth: 0,
|
|
355
|
+
theme: {} as ThemeTokens,
|
|
356
|
+
interactive: false,
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Standard terminal width
|
|
360
|
+
expect(context.width).toBeGreaterThan(0)
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
it('height represents terminal row count', () => {
|
|
364
|
+
const context: RenderContext = {
|
|
365
|
+
tier: 'text',
|
|
366
|
+
width: 80,
|
|
367
|
+
height: 24,
|
|
368
|
+
depth: 0,
|
|
369
|
+
theme: {} as ThemeTokens,
|
|
370
|
+
interactive: false,
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Standard terminal height
|
|
374
|
+
expect(context.height).toBeGreaterThan(0)
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
it('depth starts at 0 for root components', () => {
|
|
378
|
+
const rootContext: RenderContext = {
|
|
379
|
+
tier: 'text',
|
|
380
|
+
width: 80,
|
|
381
|
+
height: 24,
|
|
382
|
+
depth: 0,
|
|
383
|
+
theme: {} as ThemeTokens,
|
|
384
|
+
interactive: false,
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
expect(rootContext.depth).toBe(0)
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
it('depth increments for nested components', () => {
|
|
391
|
+
const nestedContext: RenderContext = {
|
|
392
|
+
tier: 'text',
|
|
393
|
+
width: 80,
|
|
394
|
+
height: 24,
|
|
395
|
+
depth: 2,
|
|
396
|
+
theme: {} as ThemeTokens,
|
|
397
|
+
interactive: false,
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
expect(nestedContext.depth).toBe(2)
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
it('interactive is true only for interactive tier', () => {
|
|
404
|
+
const textContext: RenderContext = {
|
|
405
|
+
tier: 'text',
|
|
406
|
+
width: 80,
|
|
407
|
+
height: 24,
|
|
408
|
+
depth: 0,
|
|
409
|
+
theme: {} as ThemeTokens,
|
|
410
|
+
interactive: false,
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const interactiveContext: RenderContext = {
|
|
414
|
+
tier: 'interactive',
|
|
415
|
+
width: 80,
|
|
416
|
+
height: 24,
|
|
417
|
+
depth: 0,
|
|
418
|
+
theme: {} as ThemeTokens,
|
|
419
|
+
interactive: true,
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
expect(textContext.interactive).toBe(false)
|
|
423
|
+
expect(interactiveContext.interactive).toBe(true)
|
|
424
|
+
})
|
|
425
|
+
})
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
// ============================================================================
|
|
429
|
+
// ThemeTokens Interface Tests
|
|
430
|
+
// ============================================================================
|
|
431
|
+
|
|
432
|
+
describe('ThemeTokens Interface', () => {
|
|
433
|
+
describe('required color tokens', () => {
|
|
434
|
+
it('includes primary color token', () => {
|
|
435
|
+
const theme: ThemeTokens = {
|
|
436
|
+
primary: '\x1b[34m',
|
|
437
|
+
secondary: '',
|
|
438
|
+
muted: '',
|
|
439
|
+
foreground: '',
|
|
440
|
+
background: '',
|
|
441
|
+
border: '',
|
|
442
|
+
success: '',
|
|
443
|
+
warning: '',
|
|
444
|
+
error: '',
|
|
445
|
+
info: '',
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
expect(theme.primary).toBe('\x1b[34m')
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
it('includes secondary color token', () => {
|
|
452
|
+
const theme: ThemeTokens = {
|
|
453
|
+
primary: '',
|
|
454
|
+
secondary: '\x1b[36m',
|
|
455
|
+
muted: '',
|
|
456
|
+
foreground: '',
|
|
457
|
+
background: '',
|
|
458
|
+
border: '',
|
|
459
|
+
success: '',
|
|
460
|
+
warning: '',
|
|
461
|
+
error: '',
|
|
462
|
+
info: '',
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
expect(theme.secondary).toBe('\x1b[36m')
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
it('includes muted color token', () => {
|
|
469
|
+
const theme: ThemeTokens = {
|
|
470
|
+
primary: '',
|
|
471
|
+
secondary: '',
|
|
472
|
+
muted: '\x1b[90m',
|
|
473
|
+
foreground: '',
|
|
474
|
+
background: '',
|
|
475
|
+
border: '',
|
|
476
|
+
success: '',
|
|
477
|
+
warning: '',
|
|
478
|
+
error: '',
|
|
479
|
+
info: '',
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
expect(theme.muted).toBe('\x1b[90m')
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
it('includes foreground color token', () => {
|
|
486
|
+
const theme: ThemeTokens = {
|
|
487
|
+
primary: '',
|
|
488
|
+
secondary: '',
|
|
489
|
+
muted: '',
|
|
490
|
+
foreground: '\x1b[37m',
|
|
491
|
+
background: '',
|
|
492
|
+
border: '',
|
|
493
|
+
success: '',
|
|
494
|
+
warning: '',
|
|
495
|
+
error: '',
|
|
496
|
+
info: '',
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
expect(theme.foreground).toBe('\x1b[37m')
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
it('includes background color token', () => {
|
|
503
|
+
const theme: ThemeTokens = {
|
|
504
|
+
primary: '',
|
|
505
|
+
secondary: '',
|
|
506
|
+
muted: '',
|
|
507
|
+
foreground: '',
|
|
508
|
+
background: '\x1b[40m',
|
|
509
|
+
border: '',
|
|
510
|
+
success: '',
|
|
511
|
+
warning: '',
|
|
512
|
+
error: '',
|
|
513
|
+
info: '',
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
expect(theme.background).toBe('\x1b[40m')
|
|
517
|
+
})
|
|
518
|
+
|
|
519
|
+
it('includes border color token', () => {
|
|
520
|
+
const theme: ThemeTokens = {
|
|
521
|
+
primary: '',
|
|
522
|
+
secondary: '',
|
|
523
|
+
muted: '',
|
|
524
|
+
foreground: '',
|
|
525
|
+
background: '',
|
|
526
|
+
border: '\x1b[90m',
|
|
527
|
+
success: '',
|
|
528
|
+
warning: '',
|
|
529
|
+
error: '',
|
|
530
|
+
info: '',
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
expect(theme.border).toBe('\x1b[90m')
|
|
534
|
+
})
|
|
535
|
+
|
|
536
|
+
it('includes semantic status tokens (success, warning, error, info)', () => {
|
|
537
|
+
const theme: ThemeTokens = {
|
|
538
|
+
primary: '',
|
|
539
|
+
secondary: '',
|
|
540
|
+
muted: '',
|
|
541
|
+
foreground: '',
|
|
542
|
+
background: '',
|
|
543
|
+
border: '',
|
|
544
|
+
success: '\x1b[32m',
|
|
545
|
+
warning: '\x1b[33m',
|
|
546
|
+
error: '\x1b[31m',
|
|
547
|
+
info: '\x1b[34m',
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
expect(theme.success).toBe('\x1b[32m')
|
|
551
|
+
expect(theme.warning).toBe('\x1b[33m')
|
|
552
|
+
expect(theme.error).toBe('\x1b[31m')
|
|
553
|
+
expect(theme.info).toBe('\x1b[34m')
|
|
554
|
+
})
|
|
555
|
+
})
|
|
556
|
+
|
|
557
|
+
describe('token type requirements', () => {
|
|
558
|
+
it('all tokens are strings (ANSI escape sequences)', () => {
|
|
559
|
+
const theme: ThemeTokens = {
|
|
560
|
+
primary: '\x1b[34m',
|
|
561
|
+
secondary: '\x1b[36m',
|
|
562
|
+
muted: '\x1b[90m',
|
|
563
|
+
foreground: '\x1b[37m',
|
|
564
|
+
background: '\x1b[40m',
|
|
565
|
+
border: '\x1b[90m',
|
|
566
|
+
success: '\x1b[32m',
|
|
567
|
+
warning: '\x1b[33m',
|
|
568
|
+
error: '\x1b[31m',
|
|
569
|
+
info: '\x1b[34m',
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
Object.values(theme).forEach((value) => {
|
|
573
|
+
expect(typeof value).toBe('string')
|
|
574
|
+
})
|
|
575
|
+
})
|
|
576
|
+
})
|
|
577
|
+
})
|
|
578
|
+
|
|
579
|
+
// ============================================================================
|
|
580
|
+
// Zod Schema Tests - UINodeSchema
|
|
581
|
+
// ============================================================================
|
|
582
|
+
|
|
583
|
+
describe('UINodeSchema (Zod)', () => {
|
|
584
|
+
describe('valid UINode objects', () => {
|
|
585
|
+
it('parses minimal valid UINode', () => {
|
|
586
|
+
const input = {
|
|
587
|
+
type: 'text',
|
|
588
|
+
props: {},
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const result = UINodeSchema.safeParse(input)
|
|
592
|
+
expect(result.success).toBe(true)
|
|
593
|
+
if (result.success) {
|
|
594
|
+
expect(result.data.type).toBe('text')
|
|
595
|
+
expect(result.data.props).toEqual({})
|
|
596
|
+
}
|
|
597
|
+
})
|
|
598
|
+
|
|
599
|
+
it('parses UINode with props', () => {
|
|
600
|
+
const input = {
|
|
601
|
+
type: 'box',
|
|
602
|
+
props: {
|
|
603
|
+
padding: 2,
|
|
604
|
+
border: 'single',
|
|
605
|
+
bg: '#1a1a1a',
|
|
606
|
+
},
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const result = UINodeSchema.safeParse(input)
|
|
610
|
+
expect(result.success).toBe(true)
|
|
611
|
+
if (result.success) {
|
|
612
|
+
expect(result.data.props.padding).toBe(2)
|
|
613
|
+
}
|
|
614
|
+
})
|
|
615
|
+
|
|
616
|
+
it('parses UINode with children', () => {
|
|
617
|
+
const input = {
|
|
618
|
+
type: 'box',
|
|
619
|
+
props: {},
|
|
620
|
+
children: [
|
|
621
|
+
{ type: 'text', props: { content: 'Hello' } },
|
|
622
|
+
{ type: 'text', props: { content: 'World' } },
|
|
623
|
+
],
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const result = UINodeSchema.safeParse(input)
|
|
627
|
+
expect(result.success).toBe(true)
|
|
628
|
+
if (result.success) {
|
|
629
|
+
expect(result.data.children).toHaveLength(2)
|
|
630
|
+
}
|
|
631
|
+
})
|
|
632
|
+
|
|
633
|
+
it('parses UINode with deeply nested children', () => {
|
|
634
|
+
const input = {
|
|
635
|
+
type: 'panel',
|
|
636
|
+
props: { title: 'Container' },
|
|
637
|
+
children: [
|
|
638
|
+
{
|
|
639
|
+
type: 'box',
|
|
640
|
+
props: {},
|
|
641
|
+
children: [
|
|
642
|
+
{
|
|
643
|
+
type: 'text',
|
|
644
|
+
props: { content: 'Deep' },
|
|
645
|
+
children: [],
|
|
646
|
+
},
|
|
647
|
+
],
|
|
648
|
+
},
|
|
649
|
+
],
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const result = UINodeSchema.safeParse(input)
|
|
653
|
+
expect(result.success).toBe(true)
|
|
654
|
+
})
|
|
655
|
+
|
|
656
|
+
it('parses UINode with data binding', () => {
|
|
657
|
+
const input = {
|
|
658
|
+
type: 'table',
|
|
659
|
+
props: {},
|
|
660
|
+
data: {
|
|
661
|
+
rows: [
|
|
662
|
+
{ id: 1, name: 'Alice' },
|
|
663
|
+
{ id: 2, name: 'Bob' },
|
|
664
|
+
],
|
|
665
|
+
},
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const result = UINodeSchema.safeParse(input)
|
|
669
|
+
expect(result.success).toBe(true)
|
|
670
|
+
if (result.success) {
|
|
671
|
+
expect(result.data.data).toEqual({
|
|
672
|
+
rows: [
|
|
673
|
+
{ id: 1, name: 'Alice' },
|
|
674
|
+
{ id: 2, name: 'Bob' },
|
|
675
|
+
],
|
|
676
|
+
})
|
|
677
|
+
}
|
|
678
|
+
})
|
|
679
|
+
|
|
680
|
+
it('parses UINode with key', () => {
|
|
681
|
+
const input = {
|
|
682
|
+
type: 'list-item',
|
|
683
|
+
props: {},
|
|
684
|
+
key: 'item-42',
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const result = UINodeSchema.safeParse(input)
|
|
688
|
+
expect(result.success).toBe(true)
|
|
689
|
+
if (result.success) {
|
|
690
|
+
expect(result.data.key).toBe('item-42')
|
|
691
|
+
}
|
|
692
|
+
})
|
|
693
|
+
|
|
694
|
+
it('parses UINode with all optional fields', () => {
|
|
695
|
+
const input = {
|
|
696
|
+
type: 'complex',
|
|
697
|
+
props: { flag: true, count: 5 },
|
|
698
|
+
children: [{ type: 'child', props: {} }],
|
|
699
|
+
data: { fetched: true },
|
|
700
|
+
key: 'unique-key',
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const result = UINodeSchema.safeParse(input)
|
|
704
|
+
expect(result.success).toBe(true)
|
|
705
|
+
})
|
|
706
|
+
})
|
|
707
|
+
|
|
708
|
+
describe('invalid UINode objects', () => {
|
|
709
|
+
it('rejects missing type field', () => {
|
|
710
|
+
const input = {
|
|
711
|
+
props: {},
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const result = UINodeSchema.safeParse(input)
|
|
715
|
+
expect(result.success).toBe(false)
|
|
716
|
+
if (!result.success) {
|
|
717
|
+
expect(result.error.issues[0].path).toContain('type')
|
|
718
|
+
}
|
|
719
|
+
})
|
|
720
|
+
|
|
721
|
+
it('accepts missing props field (props is optional)', () => {
|
|
722
|
+
const input = {
|
|
723
|
+
type: 'text',
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
const result = UINodeSchema.safeParse(input)
|
|
727
|
+
expect(result.success).toBe(true)
|
|
728
|
+
})
|
|
729
|
+
|
|
730
|
+
it('rejects non-string type field', () => {
|
|
731
|
+
const input = {
|
|
732
|
+
type: 123,
|
|
733
|
+
props: {},
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const result = UINodeSchema.safeParse(input)
|
|
737
|
+
expect(result.success).toBe(false)
|
|
738
|
+
})
|
|
739
|
+
|
|
740
|
+
it('rejects non-object props field', () => {
|
|
741
|
+
const input = {
|
|
742
|
+
type: 'text',
|
|
743
|
+
props: 'not an object',
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
const result = UINodeSchema.safeParse(input)
|
|
747
|
+
expect(result.success).toBe(false)
|
|
748
|
+
})
|
|
749
|
+
|
|
750
|
+
it('accepts string children field (children can be string or array)', () => {
|
|
751
|
+
const input = {
|
|
752
|
+
type: 'box',
|
|
753
|
+
props: {},
|
|
754
|
+
children: 'string content',
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
const result = UINodeSchema.safeParse(input)
|
|
758
|
+
expect(result.success).toBe(true)
|
|
759
|
+
})
|
|
760
|
+
|
|
761
|
+
it('rejects invalid children elements', () => {
|
|
762
|
+
const input = {
|
|
763
|
+
type: 'box',
|
|
764
|
+
props: {},
|
|
765
|
+
children: [
|
|
766
|
+
{ type: 'valid', props: {} },
|
|
767
|
+
'invalid child',
|
|
768
|
+
{ missingType: true, props: {} },
|
|
769
|
+
],
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
const result = UINodeSchema.safeParse(input)
|
|
773
|
+
expect(result.success).toBe(false)
|
|
774
|
+
})
|
|
775
|
+
|
|
776
|
+
it('rejects non-string key field', () => {
|
|
777
|
+
const input = {
|
|
778
|
+
type: 'item',
|
|
779
|
+
props: {},
|
|
780
|
+
key: 42,
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const result = UINodeSchema.safeParse(input)
|
|
784
|
+
expect(result.success).toBe(false)
|
|
785
|
+
})
|
|
786
|
+
|
|
787
|
+
it('rejects null input', () => {
|
|
788
|
+
const result = UINodeSchema.safeParse(null)
|
|
789
|
+
expect(result.success).toBe(false)
|
|
790
|
+
})
|
|
791
|
+
|
|
792
|
+
it('rejects undefined input', () => {
|
|
793
|
+
const result = UINodeSchema.safeParse(undefined)
|
|
794
|
+
expect(result.success).toBe(false)
|
|
795
|
+
})
|
|
796
|
+
|
|
797
|
+
it('rejects array input', () => {
|
|
798
|
+
const result = UINodeSchema.safeParse([{ type: 'text', props: {} }])
|
|
799
|
+
expect(result.success).toBe(false)
|
|
800
|
+
})
|
|
801
|
+
})
|
|
802
|
+
|
|
803
|
+
describe('error messages', () => {
|
|
804
|
+
it('provides clear error for missing type', () => {
|
|
805
|
+
const result = UINodeSchema.safeParse({ props: {} })
|
|
806
|
+
expect(result.success).toBe(false)
|
|
807
|
+
if (!result.success) {
|
|
808
|
+
const typeError = result.error.issues.find((i) => i.path.includes('type'))
|
|
809
|
+
expect(typeError).toBeDefined()
|
|
810
|
+
}
|
|
811
|
+
})
|
|
812
|
+
|
|
813
|
+
it('provides clear error for invalid nested children', () => {
|
|
814
|
+
const input = {
|
|
815
|
+
type: 'box',
|
|
816
|
+
props: {},
|
|
817
|
+
children: [
|
|
818
|
+
{
|
|
819
|
+
type: 'panel',
|
|
820
|
+
props: {},
|
|
821
|
+
children: [
|
|
822
|
+
{ props: {} }, // Missing type
|
|
823
|
+
],
|
|
824
|
+
},
|
|
825
|
+
],
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
const result = UINodeSchema.safeParse(input)
|
|
829
|
+
expect(result.success).toBe(false)
|
|
830
|
+
if (!result.success) {
|
|
831
|
+
// Error path should indicate the nested location
|
|
832
|
+
const hasNestedPath = result.error.issues.some(
|
|
833
|
+
(i) => i.path.includes('children')
|
|
834
|
+
)
|
|
835
|
+
expect(hasNestedPath).toBe(true)
|
|
836
|
+
}
|
|
837
|
+
})
|
|
838
|
+
})
|
|
839
|
+
})
|
|
840
|
+
|
|
841
|
+
// ============================================================================
|
|
842
|
+
// Zod Schema Tests - RenderTierSchema
|
|
843
|
+
// ============================================================================
|
|
844
|
+
|
|
845
|
+
describe('RenderTierSchema (Zod)', () => {
|
|
846
|
+
describe('valid tiers', () => {
|
|
847
|
+
it('parses "text" tier', () => {
|
|
848
|
+
const result = RenderTierSchema.safeParse('text')
|
|
849
|
+
expect(result.success).toBe(true)
|
|
850
|
+
expect(result.data).toBe('text')
|
|
851
|
+
})
|
|
852
|
+
|
|
853
|
+
it('parses "markdown" tier', () => {
|
|
854
|
+
const result = RenderTierSchema.safeParse('markdown')
|
|
855
|
+
expect(result.success).toBe(true)
|
|
856
|
+
expect(result.data).toBe('markdown')
|
|
857
|
+
})
|
|
858
|
+
|
|
859
|
+
it('parses "ascii" tier', () => {
|
|
860
|
+
const result = RenderTierSchema.safeParse('ascii')
|
|
861
|
+
expect(result.success).toBe(true)
|
|
862
|
+
expect(result.data).toBe('ascii')
|
|
863
|
+
})
|
|
864
|
+
|
|
865
|
+
it('parses "unicode" tier', () => {
|
|
866
|
+
const result = RenderTierSchema.safeParse('unicode')
|
|
867
|
+
expect(result.success).toBe(true)
|
|
868
|
+
expect(result.data).toBe('unicode')
|
|
869
|
+
})
|
|
870
|
+
|
|
871
|
+
it('parses "ansi" tier', () => {
|
|
872
|
+
const result = RenderTierSchema.safeParse('ansi')
|
|
873
|
+
expect(result.success).toBe(true)
|
|
874
|
+
expect(result.data).toBe('ansi')
|
|
875
|
+
})
|
|
876
|
+
|
|
877
|
+
it('parses "interactive" tier', () => {
|
|
878
|
+
const result = RenderTierSchema.safeParse('interactive')
|
|
879
|
+
expect(result.success).toBe(true)
|
|
880
|
+
expect(result.data).toBe('interactive')
|
|
881
|
+
})
|
|
882
|
+
})
|
|
883
|
+
|
|
884
|
+
describe('invalid tiers', () => {
|
|
885
|
+
it('rejects empty string', () => {
|
|
886
|
+
const result = RenderTierSchema.safeParse('')
|
|
887
|
+
expect(result.success).toBe(false)
|
|
888
|
+
})
|
|
889
|
+
|
|
890
|
+
it('rejects unknown tier names', () => {
|
|
891
|
+
const invalidTiers = ['html', 'pdf', 'json', 'xml', 'rich', 'plain']
|
|
892
|
+
|
|
893
|
+
invalidTiers.forEach((tier) => {
|
|
894
|
+
const result = RenderTierSchema.safeParse(tier)
|
|
895
|
+
expect(result.success).toBe(false)
|
|
896
|
+
})
|
|
897
|
+
})
|
|
898
|
+
|
|
899
|
+
it('rejects uppercase variants', () => {
|
|
900
|
+
const result = RenderTierSchema.safeParse('TEXT')
|
|
901
|
+
expect(result.success).toBe(false)
|
|
902
|
+
})
|
|
903
|
+
|
|
904
|
+
it('rejects numeric values', () => {
|
|
905
|
+
const result = RenderTierSchema.safeParse(1)
|
|
906
|
+
expect(result.success).toBe(false)
|
|
907
|
+
})
|
|
908
|
+
|
|
909
|
+
it('rejects null', () => {
|
|
910
|
+
const result = RenderTierSchema.safeParse(null)
|
|
911
|
+
expect(result.success).toBe(false)
|
|
912
|
+
})
|
|
913
|
+
|
|
914
|
+
it('rejects undefined', () => {
|
|
915
|
+
const result = RenderTierSchema.safeParse(undefined)
|
|
916
|
+
expect(result.success).toBe(false)
|
|
917
|
+
})
|
|
918
|
+
|
|
919
|
+
it('rejects objects', () => {
|
|
920
|
+
const result = RenderTierSchema.safeParse({ tier: 'text' })
|
|
921
|
+
expect(result.success).toBe(false)
|
|
922
|
+
})
|
|
923
|
+
})
|
|
924
|
+
})
|
|
925
|
+
|
|
926
|
+
// ============================================================================
|
|
927
|
+
// Zod Schema Tests - RenderContextSchema
|
|
928
|
+
// ============================================================================
|
|
929
|
+
|
|
930
|
+
describe('RenderContextSchema (Zod)', () => {
|
|
931
|
+
describe('valid RenderContext objects', () => {
|
|
932
|
+
it('parses valid RenderContext with all required fields', () => {
|
|
933
|
+
const input = {
|
|
934
|
+
tier: 'ansi',
|
|
935
|
+
width: 80,
|
|
936
|
+
height: 24,
|
|
937
|
+
depth: 0,
|
|
938
|
+
theme: {
|
|
939
|
+
primary: '\x1b[34m',
|
|
940
|
+
secondary: '\x1b[36m',
|
|
941
|
+
muted: '\x1b[90m',
|
|
942
|
+
foreground: '\x1b[37m',
|
|
943
|
+
background: '\x1b[40m',
|
|
944
|
+
border: '\x1b[90m',
|
|
945
|
+
success: '\x1b[32m',
|
|
946
|
+
warning: '\x1b[33m',
|
|
947
|
+
error: '\x1b[31m',
|
|
948
|
+
info: '\x1b[34m',
|
|
949
|
+
},
|
|
950
|
+
interactive: false,
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
const result = RenderContextSchema.safeParse(input)
|
|
954
|
+
expect(result.success).toBe(true)
|
|
955
|
+
})
|
|
956
|
+
|
|
957
|
+
it('parses RenderContext with interactive tier', () => {
|
|
958
|
+
const input = {
|
|
959
|
+
tier: 'interactive',
|
|
960
|
+
width: 120,
|
|
961
|
+
height: 40,
|
|
962
|
+
depth: 0,
|
|
963
|
+
theme: {
|
|
964
|
+
primary: '',
|
|
965
|
+
secondary: '',
|
|
966
|
+
muted: '',
|
|
967
|
+
foreground: '',
|
|
968
|
+
background: '',
|
|
969
|
+
border: '',
|
|
970
|
+
success: '',
|
|
971
|
+
warning: '',
|
|
972
|
+
error: '',
|
|
973
|
+
info: '',
|
|
974
|
+
},
|
|
975
|
+
interactive: true,
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
const result = RenderContextSchema.safeParse(input)
|
|
979
|
+
expect(result.success).toBe(true)
|
|
980
|
+
})
|
|
981
|
+
|
|
982
|
+
it('parses RenderContext with non-zero depth', () => {
|
|
983
|
+
const input = {
|
|
984
|
+
tier: 'text',
|
|
985
|
+
width: 80,
|
|
986
|
+
height: 24,
|
|
987
|
+
depth: 5,
|
|
988
|
+
theme: {
|
|
989
|
+
primary: '',
|
|
990
|
+
secondary: '',
|
|
991
|
+
muted: '',
|
|
992
|
+
foreground: '',
|
|
993
|
+
background: '',
|
|
994
|
+
border: '',
|
|
995
|
+
success: '',
|
|
996
|
+
warning: '',
|
|
997
|
+
error: '',
|
|
998
|
+
info: '',
|
|
999
|
+
},
|
|
1000
|
+
interactive: false,
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
const result = RenderContextSchema.safeParse(input)
|
|
1004
|
+
expect(result.success).toBe(true)
|
|
1005
|
+
if (result.success) {
|
|
1006
|
+
expect(result.data.depth).toBe(5)
|
|
1007
|
+
}
|
|
1008
|
+
})
|
|
1009
|
+
})
|
|
1010
|
+
|
|
1011
|
+
describe('invalid RenderContext objects', () => {
|
|
1012
|
+
const validTheme = {
|
|
1013
|
+
primary: '',
|
|
1014
|
+
secondary: '',
|
|
1015
|
+
muted: '',
|
|
1016
|
+
foreground: '',
|
|
1017
|
+
background: '',
|
|
1018
|
+
border: '',
|
|
1019
|
+
success: '',
|
|
1020
|
+
warning: '',
|
|
1021
|
+
error: '',
|
|
1022
|
+
info: '',
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
it('rejects missing tier field', () => {
|
|
1026
|
+
const input = {
|
|
1027
|
+
width: 80,
|
|
1028
|
+
height: 24,
|
|
1029
|
+
depth: 0,
|
|
1030
|
+
theme: validTheme,
|
|
1031
|
+
interactive: false,
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
const result = RenderContextSchema.safeParse(input)
|
|
1035
|
+
expect(result.success).toBe(false)
|
|
1036
|
+
})
|
|
1037
|
+
|
|
1038
|
+
it('rejects invalid tier value', () => {
|
|
1039
|
+
const input = {
|
|
1040
|
+
tier: 'invalid-tier',
|
|
1041
|
+
width: 80,
|
|
1042
|
+
height: 24,
|
|
1043
|
+
depth: 0,
|
|
1044
|
+
theme: validTheme,
|
|
1045
|
+
interactive: false,
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
const result = RenderContextSchema.safeParse(input)
|
|
1049
|
+
expect(result.success).toBe(false)
|
|
1050
|
+
})
|
|
1051
|
+
|
|
1052
|
+
it('rejects missing width field', () => {
|
|
1053
|
+
const input = {
|
|
1054
|
+
tier: 'text',
|
|
1055
|
+
height: 24,
|
|
1056
|
+
depth: 0,
|
|
1057
|
+
theme: validTheme,
|
|
1058
|
+
interactive: false,
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
const result = RenderContextSchema.safeParse(input)
|
|
1062
|
+
expect(result.success).toBe(false)
|
|
1063
|
+
})
|
|
1064
|
+
|
|
1065
|
+
it('rejects non-numeric width', () => {
|
|
1066
|
+
const input = {
|
|
1067
|
+
tier: 'text',
|
|
1068
|
+
width: '80',
|
|
1069
|
+
height: 24,
|
|
1070
|
+
depth: 0,
|
|
1071
|
+
theme: validTheme,
|
|
1072
|
+
interactive: false,
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
const result = RenderContextSchema.safeParse(input)
|
|
1076
|
+
expect(result.success).toBe(false)
|
|
1077
|
+
})
|
|
1078
|
+
|
|
1079
|
+
it('rejects negative width', () => {
|
|
1080
|
+
const input = {
|
|
1081
|
+
tier: 'text',
|
|
1082
|
+
width: -1,
|
|
1083
|
+
height: 24,
|
|
1084
|
+
depth: 0,
|
|
1085
|
+
theme: validTheme,
|
|
1086
|
+
interactive: false,
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
const result = RenderContextSchema.safeParse(input)
|
|
1090
|
+
expect(result.success).toBe(false)
|
|
1091
|
+
})
|
|
1092
|
+
|
|
1093
|
+
it('rejects missing height field', () => {
|
|
1094
|
+
const input = {
|
|
1095
|
+
tier: 'text',
|
|
1096
|
+
width: 80,
|
|
1097
|
+
depth: 0,
|
|
1098
|
+
theme: validTheme,
|
|
1099
|
+
interactive: false,
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
const result = RenderContextSchema.safeParse(input)
|
|
1103
|
+
expect(result.success).toBe(false)
|
|
1104
|
+
})
|
|
1105
|
+
|
|
1106
|
+
it('rejects negative height', () => {
|
|
1107
|
+
const input = {
|
|
1108
|
+
tier: 'text',
|
|
1109
|
+
width: 80,
|
|
1110
|
+
height: -10,
|
|
1111
|
+
depth: 0,
|
|
1112
|
+
theme: validTheme,
|
|
1113
|
+
interactive: false,
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
const result = RenderContextSchema.safeParse(input)
|
|
1117
|
+
expect(result.success).toBe(false)
|
|
1118
|
+
})
|
|
1119
|
+
|
|
1120
|
+
it('rejects missing depth field', () => {
|
|
1121
|
+
const input = {
|
|
1122
|
+
tier: 'text',
|
|
1123
|
+
width: 80,
|
|
1124
|
+
height: 24,
|
|
1125
|
+
theme: validTheme,
|
|
1126
|
+
interactive: false,
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
const result = RenderContextSchema.safeParse(input)
|
|
1130
|
+
expect(result.success).toBe(false)
|
|
1131
|
+
})
|
|
1132
|
+
|
|
1133
|
+
it('rejects negative depth', () => {
|
|
1134
|
+
const input = {
|
|
1135
|
+
tier: 'text',
|
|
1136
|
+
width: 80,
|
|
1137
|
+
height: 24,
|
|
1138
|
+
depth: -1,
|
|
1139
|
+
theme: validTheme,
|
|
1140
|
+
interactive: false,
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
const result = RenderContextSchema.safeParse(input)
|
|
1144
|
+
expect(result.success).toBe(false)
|
|
1145
|
+
})
|
|
1146
|
+
|
|
1147
|
+
it('rejects missing theme field', () => {
|
|
1148
|
+
const input = {
|
|
1149
|
+
tier: 'text',
|
|
1150
|
+
width: 80,
|
|
1151
|
+
height: 24,
|
|
1152
|
+
depth: 0,
|
|
1153
|
+
interactive: false,
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
const result = RenderContextSchema.safeParse(input)
|
|
1157
|
+
expect(result.success).toBe(false)
|
|
1158
|
+
})
|
|
1159
|
+
|
|
1160
|
+
it('rejects theme with missing tokens', () => {
|
|
1161
|
+
const input = {
|
|
1162
|
+
tier: 'text',
|
|
1163
|
+
width: 80,
|
|
1164
|
+
height: 24,
|
|
1165
|
+
depth: 0,
|
|
1166
|
+
theme: {
|
|
1167
|
+
primary: '',
|
|
1168
|
+
// Missing other required tokens
|
|
1169
|
+
},
|
|
1170
|
+
interactive: false,
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
const result = RenderContextSchema.safeParse(input)
|
|
1174
|
+
expect(result.success).toBe(false)
|
|
1175
|
+
})
|
|
1176
|
+
|
|
1177
|
+
it('rejects missing interactive field', () => {
|
|
1178
|
+
const input = {
|
|
1179
|
+
tier: 'text',
|
|
1180
|
+
width: 80,
|
|
1181
|
+
height: 24,
|
|
1182
|
+
depth: 0,
|
|
1183
|
+
theme: validTheme,
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
const result = RenderContextSchema.safeParse(input)
|
|
1187
|
+
expect(result.success).toBe(false)
|
|
1188
|
+
})
|
|
1189
|
+
|
|
1190
|
+
it('rejects non-boolean interactive field', () => {
|
|
1191
|
+
const input = {
|
|
1192
|
+
tier: 'text',
|
|
1193
|
+
width: 80,
|
|
1194
|
+
height: 24,
|
|
1195
|
+
depth: 0,
|
|
1196
|
+
theme: validTheme,
|
|
1197
|
+
interactive: 'yes',
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
const result = RenderContextSchema.safeParse(input)
|
|
1201
|
+
expect(result.success).toBe(false)
|
|
1202
|
+
})
|
|
1203
|
+
})
|
|
1204
|
+
})
|
|
1205
|
+
|
|
1206
|
+
// ============================================================================
|
|
1207
|
+
// Zod Schema Tests - ThemeTokensSchema
|
|
1208
|
+
// ============================================================================
|
|
1209
|
+
|
|
1210
|
+
describe('ThemeTokensSchema (Zod)', () => {
|
|
1211
|
+
describe('valid ThemeTokens objects', () => {
|
|
1212
|
+
it('parses valid ThemeTokens with all tokens', () => {
|
|
1213
|
+
const input = {
|
|
1214
|
+
primary: '\x1b[34m',
|
|
1215
|
+
secondary: '\x1b[36m',
|
|
1216
|
+
muted: '\x1b[90m',
|
|
1217
|
+
foreground: '\x1b[37m',
|
|
1218
|
+
background: '\x1b[40m',
|
|
1219
|
+
border: '\x1b[90m',
|
|
1220
|
+
success: '\x1b[32m',
|
|
1221
|
+
warning: '\x1b[33m',
|
|
1222
|
+
error: '\x1b[31m',
|
|
1223
|
+
info: '\x1b[34m',
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
const result = ThemeTokensSchema.safeParse(input)
|
|
1227
|
+
expect(result.success).toBe(true)
|
|
1228
|
+
})
|
|
1229
|
+
|
|
1230
|
+
it('parses ThemeTokens with empty string tokens (for text tier)', () => {
|
|
1231
|
+
const input = {
|
|
1232
|
+
primary: '',
|
|
1233
|
+
secondary: '',
|
|
1234
|
+
muted: '',
|
|
1235
|
+
foreground: '',
|
|
1236
|
+
background: '',
|
|
1237
|
+
border: '',
|
|
1238
|
+
success: '',
|
|
1239
|
+
warning: '',
|
|
1240
|
+
error: '',
|
|
1241
|
+
info: '',
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
const result = ThemeTokensSchema.safeParse(input)
|
|
1245
|
+
expect(result.success).toBe(true)
|
|
1246
|
+
})
|
|
1247
|
+
})
|
|
1248
|
+
|
|
1249
|
+
describe('invalid ThemeTokens objects', () => {
|
|
1250
|
+
it('rejects missing primary token', () => {
|
|
1251
|
+
const input = {
|
|
1252
|
+
secondary: '',
|
|
1253
|
+
muted: '',
|
|
1254
|
+
foreground: '',
|
|
1255
|
+
background: '',
|
|
1256
|
+
border: '',
|
|
1257
|
+
success: '',
|
|
1258
|
+
warning: '',
|
|
1259
|
+
error: '',
|
|
1260
|
+
info: '',
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
const result = ThemeTokensSchema.safeParse(input)
|
|
1264
|
+
expect(result.success).toBe(false)
|
|
1265
|
+
})
|
|
1266
|
+
|
|
1267
|
+
it('rejects missing semantic tokens', () => {
|
|
1268
|
+
const input = {
|
|
1269
|
+
primary: '',
|
|
1270
|
+
secondary: '',
|
|
1271
|
+
muted: '',
|
|
1272
|
+
foreground: '',
|
|
1273
|
+
background: '',
|
|
1274
|
+
border: '',
|
|
1275
|
+
// Missing success, warning, error, info
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
const result = ThemeTokensSchema.safeParse(input)
|
|
1279
|
+
expect(result.success).toBe(false)
|
|
1280
|
+
})
|
|
1281
|
+
|
|
1282
|
+
it('rejects non-string token values', () => {
|
|
1283
|
+
const input = {
|
|
1284
|
+
primary: 123,
|
|
1285
|
+
secondary: '',
|
|
1286
|
+
muted: '',
|
|
1287
|
+
foreground: '',
|
|
1288
|
+
background: '',
|
|
1289
|
+
border: '',
|
|
1290
|
+
success: '',
|
|
1291
|
+
warning: '',
|
|
1292
|
+
error: '',
|
|
1293
|
+
info: '',
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
const result = ThemeTokensSchema.safeParse(input)
|
|
1297
|
+
expect(result.success).toBe(false)
|
|
1298
|
+
})
|
|
1299
|
+
|
|
1300
|
+
it('rejects null token values', () => {
|
|
1301
|
+
const input = {
|
|
1302
|
+
primary: null,
|
|
1303
|
+
secondary: '',
|
|
1304
|
+
muted: '',
|
|
1305
|
+
foreground: '',
|
|
1306
|
+
background: '',
|
|
1307
|
+
border: '',
|
|
1308
|
+
success: '',
|
|
1309
|
+
warning: '',
|
|
1310
|
+
error: '',
|
|
1311
|
+
info: '',
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
const result = ThemeTokensSchema.safeParse(input)
|
|
1315
|
+
expect(result.success).toBe(false)
|
|
1316
|
+
})
|
|
1317
|
+
})
|
|
1318
|
+
})
|
|
1319
|
+
|
|
1320
|
+
// ============================================================================
|
|
1321
|
+
// Type Coercion Behavior Tests
|
|
1322
|
+
// ============================================================================
|
|
1323
|
+
|
|
1324
|
+
describe('Schema type coercion', () => {
|
|
1325
|
+
describe('UINodeSchema coercion', () => {
|
|
1326
|
+
it('does not coerce props array to object', () => {
|
|
1327
|
+
const input = {
|
|
1328
|
+
type: 'text',
|
|
1329
|
+
props: ['invalid', 'array'],
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
const result = UINodeSchema.safeParse(input)
|
|
1333
|
+
expect(result.success).toBe(false)
|
|
1334
|
+
})
|
|
1335
|
+
|
|
1336
|
+
it('does not coerce numeric type to string', () => {
|
|
1337
|
+
const input = {
|
|
1338
|
+
type: 42,
|
|
1339
|
+
props: {},
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
const result = UINodeSchema.safeParse(input)
|
|
1343
|
+
expect(result.success).toBe(false)
|
|
1344
|
+
})
|
|
1345
|
+
})
|
|
1346
|
+
|
|
1347
|
+
describe('RenderContextSchema coercion', () => {
|
|
1348
|
+
it('does not coerce string numbers to numbers', () => {
|
|
1349
|
+
const input = {
|
|
1350
|
+
tier: 'text',
|
|
1351
|
+
width: '80',
|
|
1352
|
+
height: '24',
|
|
1353
|
+
depth: '0',
|
|
1354
|
+
theme: {
|
|
1355
|
+
primary: '',
|
|
1356
|
+
secondary: '',
|
|
1357
|
+
muted: '',
|
|
1358
|
+
foreground: '',
|
|
1359
|
+
background: '',
|
|
1360
|
+
border: '',
|
|
1361
|
+
success: '',
|
|
1362
|
+
warning: '',
|
|
1363
|
+
error: '',
|
|
1364
|
+
info: '',
|
|
1365
|
+
},
|
|
1366
|
+
interactive: false,
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
const result = RenderContextSchema.safeParse(input)
|
|
1370
|
+
expect(result.success).toBe(false)
|
|
1371
|
+
})
|
|
1372
|
+
|
|
1373
|
+
it('does not coerce string boolean to boolean', () => {
|
|
1374
|
+
const input = {
|
|
1375
|
+
tier: 'text',
|
|
1376
|
+
width: 80,
|
|
1377
|
+
height: 24,
|
|
1378
|
+
depth: 0,
|
|
1379
|
+
theme: {
|
|
1380
|
+
primary: '',
|
|
1381
|
+
secondary: '',
|
|
1382
|
+
muted: '',
|
|
1383
|
+
foreground: '',
|
|
1384
|
+
background: '',
|
|
1385
|
+
border: '',
|
|
1386
|
+
success: '',
|
|
1387
|
+
warning: '',
|
|
1388
|
+
error: '',
|
|
1389
|
+
info: '',
|
|
1390
|
+
},
|
|
1391
|
+
interactive: 'true',
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
const result = RenderContextSchema.safeParse(input)
|
|
1395
|
+
expect(result.success).toBe(false)
|
|
1396
|
+
})
|
|
1397
|
+
})
|
|
1398
|
+
})
|