@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.
Files changed (191) hide show
  1. package/README.md +571 -0
  2. package/dist/ansi-css-Sk5mWtdK.d.ts +119 -0
  3. package/dist/ansi-css-V6JIHGsM.d.ts +119 -0
  4. package/dist/ansi-css-_3eSEU9d.d.ts +119 -0
  5. package/dist/chunk-3EFDH7PK.js +5235 -0
  6. package/dist/chunk-3RG5ZIWI.js +10 -0
  7. package/dist/chunk-3X5IR6WE.js +884 -0
  8. package/dist/chunk-4FV5ZDCE.js +5236 -0
  9. package/dist/chunk-4OVMSF2J.js +243 -0
  10. package/dist/chunk-63FEETIS.js +4048 -0
  11. package/dist/chunk-B43KP7XJ.js +884 -0
  12. package/dist/chunk-BMTJXWUV.js +655 -0
  13. package/dist/chunk-C3SVH4N7.js +882 -0
  14. package/dist/chunk-EVWR7Y47.js +874 -0
  15. package/dist/chunk-F6A5VWUC.js +1285 -0
  16. package/dist/chunk-FD7KW7GE.js +882 -0
  17. package/dist/chunk-GBQ6UD6I.js +655 -0
  18. package/dist/chunk-GMDD3M6U.js +5227 -0
  19. package/dist/chunk-JBHRXOXM.js +1058 -0
  20. package/dist/chunk-JFOO3EYO.js +1182 -0
  21. package/dist/chunk-JQ5H3WXL.js +1291 -0
  22. package/dist/chunk-JQD5NASE.js +234 -0
  23. package/dist/chunk-KRHJP5R7.js +592 -0
  24. package/dist/chunk-KWF6WVJE.js +962 -0
  25. package/dist/chunk-LHYQVN3H.js +1038 -0
  26. package/dist/chunk-M3TLQLGC.js +1032 -0
  27. package/dist/chunk-MVW4Q5OP.js +240 -0
  28. package/dist/chunk-NXCZSWLU.js +1294 -0
  29. package/dist/chunk-O25TNRO6.js +607 -0
  30. package/dist/chunk-PNECDA2I.js +884 -0
  31. package/dist/chunk-QIHWRLJR.js +962 -0
  32. package/dist/chunk-QW5YMQ7K.js +882 -0
  33. package/dist/chunk-R5U7XKVJ.js +16 -0
  34. package/dist/chunk-RP2MVQLR.js +962 -0
  35. package/dist/chunk-TP6RXGXA.js +1087 -0
  36. package/dist/chunk-TQQSTITZ.js +655 -0
  37. package/dist/chunk-X24GWXQV.js +1281 -0
  38. package/dist/components/index.d.ts +802 -0
  39. package/dist/components/index.js +149 -0
  40. package/dist/data/index.d.ts +2554 -0
  41. package/dist/data/index.js +51 -0
  42. package/dist/forms/index.d.ts +1596 -0
  43. package/dist/forms/index.js +464 -0
  44. package/dist/index-CQRFZntR.d.ts +867 -0
  45. package/dist/index.d.ts +579 -0
  46. package/dist/index.js +786 -0
  47. package/dist/interactive-D0JkWosD.d.ts +217 -0
  48. package/dist/keyboard/index.d.ts +2 -0
  49. package/dist/keyboard/index.js +43 -0
  50. package/dist/renderers/index.d.ts +546 -0
  51. package/dist/renderers/index.js +2157 -0
  52. package/dist/storybook/index.d.ts +396 -0
  53. package/dist/storybook/index.js +641 -0
  54. package/dist/theme/index.d.ts +1339 -0
  55. package/dist/theme/index.js +123 -0
  56. package/dist/types-Bxu5PAgA.d.ts +710 -0
  57. package/dist/types-CIlop5Ji.d.ts +701 -0
  58. package/dist/types-Ca8p_p5X.d.ts +710 -0
  59. package/package.json +90 -0
  60. package/src/__tests__/components/data/card.test.ts +458 -0
  61. package/src/__tests__/components/data/list.test.ts +473 -0
  62. package/src/__tests__/components/data/metrics.test.ts +541 -0
  63. package/src/__tests__/components/data/table.test.ts +448 -0
  64. package/src/__tests__/components/input/field.test.ts +555 -0
  65. package/src/__tests__/components/input/form.test.ts +870 -0
  66. package/src/__tests__/components/input/search.test.ts +1238 -0
  67. package/src/__tests__/components/input/select.test.ts +658 -0
  68. package/src/__tests__/components/navigation/breadcrumb.test.ts +923 -0
  69. package/src/__tests__/components/navigation/command-palette.test.ts +1095 -0
  70. package/src/__tests__/components/navigation/sidebar.test.ts +1018 -0
  71. package/src/__tests__/components/navigation/tabs.test.ts +995 -0
  72. package/src/__tests__/components.test.tsx +1197 -0
  73. package/src/__tests__/core/compiler.test.ts +986 -0
  74. package/src/__tests__/core/parser.test.ts +785 -0
  75. package/src/__tests__/core/tier-switcher.test.ts +1103 -0
  76. package/src/__tests__/core/types.test.ts +1398 -0
  77. package/src/__tests__/data/collections.test.ts +1337 -0
  78. package/src/__tests__/data/db.test.ts +1265 -0
  79. package/src/__tests__/data/reactive.test.ts +1010 -0
  80. package/src/__tests__/data/sync.test.ts +1614 -0
  81. package/src/__tests__/errors.test.ts +660 -0
  82. package/src/__tests__/forms/integration.test.ts +444 -0
  83. package/src/__tests__/integration.test.ts +905 -0
  84. package/src/__tests__/keyboard.test.ts +1791 -0
  85. package/src/__tests__/renderer.test.ts +489 -0
  86. package/src/__tests__/renderers/ansi-css.test.ts +948 -0
  87. package/src/__tests__/renderers/ansi.test.ts +1366 -0
  88. package/src/__tests__/renderers/ascii.test.ts +1360 -0
  89. package/src/__tests__/renderers/interactive.test.ts +2353 -0
  90. package/src/__tests__/renderers/markdown.test.ts +1483 -0
  91. package/src/__tests__/renderers/text.test.ts +1369 -0
  92. package/src/__tests__/renderers/unicode.test.ts +1307 -0
  93. package/src/__tests__/theme.test.ts +639 -0
  94. package/src/__tests__/utils/assertions.ts +685 -0
  95. package/src/__tests__/utils/index.ts +115 -0
  96. package/src/__tests__/utils/test-renderer.ts +381 -0
  97. package/src/__tests__/utils/utils.test.ts +560 -0
  98. package/src/components/containers/card.ts +56 -0
  99. package/src/components/containers/dialog.ts +53 -0
  100. package/src/components/containers/index.ts +9 -0
  101. package/src/components/containers/panel.ts +59 -0
  102. package/src/components/feedback/badge.ts +40 -0
  103. package/src/components/feedback/index.ts +8 -0
  104. package/src/components/feedback/spinner.ts +23 -0
  105. package/src/components/helpers.ts +81 -0
  106. package/src/components/index.ts +153 -0
  107. package/src/components/layout/breadcrumb.ts +31 -0
  108. package/src/components/layout/index.ts +10 -0
  109. package/src/components/layout/list.ts +29 -0
  110. package/src/components/layout/sidebar.ts +79 -0
  111. package/src/components/layout/table.ts +62 -0
  112. package/src/components/primitives/box.ts +95 -0
  113. package/src/components/primitives/button.ts +54 -0
  114. package/src/components/primitives/index.ts +11 -0
  115. package/src/components/primitives/input.ts +88 -0
  116. package/src/components/primitives/select.ts +97 -0
  117. package/src/components/primitives/text.ts +60 -0
  118. package/src/components/render.ts +155 -0
  119. package/src/components/templates/app.ts +43 -0
  120. package/src/components/templates/index.ts +8 -0
  121. package/src/components/templates/site.ts +54 -0
  122. package/src/components/types.ts +777 -0
  123. package/src/core/compiler.ts +718 -0
  124. package/src/core/parser.ts +127 -0
  125. package/src/core/tier-switcher.ts +607 -0
  126. package/src/core/types.ts +672 -0
  127. package/src/data/collection.ts +316 -0
  128. package/src/data/collections.ts +50 -0
  129. package/src/data/context.tsx +174 -0
  130. package/src/data/db.ts +127 -0
  131. package/src/data/hooks.ts +532 -0
  132. package/src/data/index.ts +138 -0
  133. package/src/data/reactive.ts +1225 -0
  134. package/src/data/saas-collections.ts +375 -0
  135. package/src/data/sync.ts +1213 -0
  136. package/src/data/types.ts +660 -0
  137. package/src/forms/converters.ts +512 -0
  138. package/src/forms/index.ts +133 -0
  139. package/src/forms/schemas.ts +403 -0
  140. package/src/forms/types.ts +476 -0
  141. package/src/index.ts +542 -0
  142. package/src/keyboard/focus.ts +748 -0
  143. package/src/keyboard/index.ts +96 -0
  144. package/src/keyboard/integration.ts +371 -0
  145. package/src/keyboard/manager.ts +377 -0
  146. package/src/keyboard/presets.ts +90 -0
  147. package/src/renderers/ansi-css.ts +576 -0
  148. package/src/renderers/ansi.ts +802 -0
  149. package/src/renderers/ascii.ts +680 -0
  150. package/src/renderers/breadcrumb.ts +480 -0
  151. package/src/renderers/command-palette.ts +802 -0
  152. package/src/renderers/components/field.ts +210 -0
  153. package/src/renderers/components/form.ts +327 -0
  154. package/src/renderers/components/index.ts +21 -0
  155. package/src/renderers/components/search.ts +449 -0
  156. package/src/renderers/components/select.ts +222 -0
  157. package/src/renderers/index.ts +101 -0
  158. package/src/renderers/interactive/component-handlers.ts +622 -0
  159. package/src/renderers/interactive/cursor-manager.ts +147 -0
  160. package/src/renderers/interactive/focus-manager.ts +279 -0
  161. package/src/renderers/interactive/index.ts +661 -0
  162. package/src/renderers/interactive/input-handler.ts +164 -0
  163. package/src/renderers/interactive/keyboard-handler.ts +212 -0
  164. package/src/renderers/interactive/mouse-handler.ts +167 -0
  165. package/src/renderers/interactive/state-manager.ts +109 -0
  166. package/src/renderers/interactive/types.ts +338 -0
  167. package/src/renderers/interactive-string.ts +299 -0
  168. package/src/renderers/interactive.ts +59 -0
  169. package/src/renderers/markdown.ts +950 -0
  170. package/src/renderers/sidebar.ts +549 -0
  171. package/src/renderers/tabs.ts +682 -0
  172. package/src/renderers/text.ts +791 -0
  173. package/src/renderers/unicode.ts +917 -0
  174. package/src/renderers/utils.ts +942 -0
  175. package/src/router/adapters.ts +383 -0
  176. package/src/router/types.ts +140 -0
  177. package/src/router/utils.ts +452 -0
  178. package/src/schemas.ts +205 -0
  179. package/src/storybook/index.ts +91 -0
  180. package/src/storybook/interactive-decorator.tsx +659 -0
  181. package/src/storybook/keyboard-simulator.ts +501 -0
  182. package/src/theme/ansi-codes.ts +80 -0
  183. package/src/theme/box-drawing.ts +132 -0
  184. package/src/theme/color-convert.ts +254 -0
  185. package/src/theme/color-support.ts +321 -0
  186. package/src/theme/index.ts +134 -0
  187. package/src/theme/strip-ansi.ts +50 -0
  188. package/src/theme/tailwind-map.ts +469 -0
  189. package/src/theme/text-styles.ts +206 -0
  190. package/src/theme/theme-system.ts +568 -0
  191. package/src/types.ts +103 -0
@@ -0,0 +1,785 @@
1
+ /**
2
+ * @mdxui/terminal JSON → UINode Parser Tests (RED phase)
3
+ *
4
+ * TDD RED Phase: These tests define the contract for JSON/YAML parsing
5
+ * functionality that converts JSON strings and objects into UINode structures.
6
+ *
7
+ * The parser must handle:
8
+ * - Valid JSON object → UINode conversion
9
+ * - Nested structure parsing with recursive children
10
+ * - Array children parsing
11
+ * - Schema validation using UINodeSchema
12
+ * - Comprehensive error handling for invalid JSON
13
+ * - Comprehensive error handling for invalid UINode structures
14
+ * - Optional: YAML string parsing support
15
+ *
16
+ * NOTE: These tests are expected to FAIL until the parser implementation
17
+ * is complete. Run: pnpm --filter @mdxui/terminal test
18
+ */
19
+ import { describe, it, expect, vi } from 'vitest'
20
+ import { z } from 'zod'
21
+
22
+ // ============================================================================
23
+ // These imports WILL FAIL until src/core/parser.ts is implemented
24
+ // ============================================================================
25
+ import { parseUINode, parseUINodeFromJSON, ParseError } from '../../core/parser'
26
+
27
+ import type { UINode } from '../../core/types'
28
+ import { UINodeSchema } from '../../core/types'
29
+
30
+ // ============================================================================
31
+ // Test Utilities
32
+ // ============================================================================
33
+
34
+ /**
35
+ * Helper to create a valid minimal UINode for testing
36
+ */
37
+ function createNode(
38
+ type: string,
39
+ props: Record<string, unknown> = {},
40
+ children?: UINode[],
41
+ data?: unknown,
42
+ key?: string
43
+ ): UINode {
44
+ return { type, props, ...(children && { children }), ...(data && { data }), ...(key && { key }) }
45
+ }
46
+
47
+ // ============================================================================
48
+ // parseUINode Function Tests - Valid Parsing
49
+ // ============================================================================
50
+
51
+ describe('parseUINode function', () => {
52
+ describe('valid JSON string parsing', () => {
53
+ it('parses minimal valid JSON string to UINode', () => {
54
+ const json = '{"type":"text","props":{}}'
55
+ const result = parseUINode(json)
56
+
57
+ expect(result).toBeDefined()
58
+ expect(result.type).toBe('text')
59
+ expect(result.props).toEqual({})
60
+ })
61
+
62
+ it('parses JSON string with props', () => {
63
+ const json = '{"type":"box","props":{"padding":2,"border":"single"}}'
64
+ const result = parseUINode(json)
65
+
66
+ expect(result.type).toBe('box')
67
+ expect(result.props.padding).toBe(2)
68
+ expect(result.props.border).toBe('single')
69
+ })
70
+
71
+ it('parses JSON string with empty children array', () => {
72
+ const json = '{"type":"box","props":{},"children":[]}'
73
+ const result = parseUINode(json)
74
+
75
+ expect(result.children).toEqual([])
76
+ })
77
+
78
+ it('parses JSON string with single child', () => {
79
+ const json =
80
+ '{"type":"box","props":{},"children":[{"type":"text","props":{"content":"Child"}}]}'
81
+ const result = parseUINode(json)
82
+
83
+ expect(result.children).toHaveLength(1)
84
+ expect(result.children![0].type).toBe('text')
85
+ expect(result.children![0].props.content).toBe('Child')
86
+ })
87
+
88
+ it('parses JSON string with multiple children', () => {
89
+ const json =
90
+ '{"type":"box","props":{},"children":[{"type":"text","props":{"content":"First"}},{"type":"text","props":{"content":"Second"}}]}'
91
+ const result = parseUINode(json)
92
+
93
+ expect(result.children).toHaveLength(2)
94
+ expect(result.children![0].props.content).toBe('First')
95
+ expect(result.children![1].props.content).toBe('Second')
96
+ })
97
+
98
+ it('parses JSON string with data field', () => {
99
+ const json = '{"type":"table","props":{},"data":{"rows":[{"id":1,"name":"Alice"}]}}'
100
+ const result = parseUINode(json)
101
+
102
+ expect(result.type).toBe('table')
103
+ expect(result.data).toBeDefined()
104
+ expect((result.data as any).rows).toHaveLength(1)
105
+ })
106
+
107
+ it('parses JSON string with key field', () => {
108
+ const json = '{"type":"item","props":{},"key":"item-123"}'
109
+ const result = parseUINode(json)
110
+
111
+ expect(result.key).toBe('item-123')
112
+ })
113
+
114
+ it('parses JSON string with all optional fields', () => {
115
+ const json =
116
+ '{"type":"complex","props":{"flag":true},"children":[{"type":"child","props":{}}],"data":{"value":42},"key":"unique"}'
117
+ const result = parseUINode(json)
118
+
119
+ expect(result.type).toBe('complex')
120
+ expect(result.props.flag).toBe(true)
121
+ expect(result.children).toHaveLength(1)
122
+ expect(result.data).toEqual({ value: 42 })
123
+ expect(result.key).toBe('unique')
124
+ })
125
+
126
+ it('parses JSON with whitespace and newlines', () => {
127
+ const json = `{
128
+ "type": "text",
129
+ "props": {
130
+ "content": "Hello"
131
+ }
132
+ }`
133
+ const result = parseUINode(json)
134
+
135
+ expect(result.type).toBe('text')
136
+ expect(result.props.content).toBe('Hello')
137
+ })
138
+ })
139
+
140
+ describe('valid parsed object parsing', () => {
141
+ it('accepts pre-parsed JavaScript object', () => {
142
+ const obj: UINode = {
143
+ type: 'text',
144
+ props: { content: 'Direct object' },
145
+ }
146
+ const result = parseUINode(obj)
147
+
148
+ expect(result.type).toBe('text')
149
+ expect(result.props.content).toBe('Direct object')
150
+ })
151
+
152
+ it('accepts object with nested children', () => {
153
+ const obj: UINode = {
154
+ type: 'box',
155
+ props: {},
156
+ children: [{ type: 'text', props: { content: 'Nested' } }],
157
+ }
158
+ const result = parseUINode(obj)
159
+
160
+ expect(result.children![0].type).toBe('text')
161
+ })
162
+ })
163
+
164
+ describe('deeply nested structure parsing', () => {
165
+ it('parses 2-level nested structure', () => {
166
+ const json = `{
167
+ "type": "box",
168
+ "props": {},
169
+ "children": [
170
+ {
171
+ "type": "panel",
172
+ "props": { "title": "Panel" },
173
+ "children": [
174
+ { "type": "text", "props": { "content": "Nested" } }
175
+ ]
176
+ }
177
+ ]
178
+ }`
179
+ const result = parseUINode(json)
180
+
181
+ expect(result.children![0].type).toBe('panel')
182
+ expect(result.children![0].children![0].type).toBe('text')
183
+ })
184
+
185
+ it('parses 3-level nested structure', () => {
186
+ const json = `{
187
+ "type": "container",
188
+ "props": {},
189
+ "children": [
190
+ {
191
+ "type": "row",
192
+ "props": {},
193
+ "children": [
194
+ {
195
+ "type": "column",
196
+ "props": {},
197
+ "children": [
198
+ { "type": "text", "props": { "content": "Deep" } }
199
+ ]
200
+ }
201
+ ]
202
+ }
203
+ ]
204
+ }`
205
+ const result = parseUINode(json)
206
+
207
+ expect(result.children![0].children![0].children![0].type).toBe('text')
208
+ })
209
+
210
+ it('parses complex nested tree with multiple branches', () => {
211
+ const json = `{
212
+ "type": "root",
213
+ "props": {},
214
+ "children": [
215
+ {
216
+ "type": "branch1",
217
+ "props": {},
218
+ "children": [
219
+ { "type": "leaf1a", "props": {} },
220
+ { "type": "leaf1b", "props": {} }
221
+ ]
222
+ },
223
+ {
224
+ "type": "branch2",
225
+ "props": {},
226
+ "children": [
227
+ { "type": "leaf2a", "props": {} }
228
+ ]
229
+ }
230
+ ]
231
+ }`
232
+ const result = parseUINode(json)
233
+
234
+ expect(result.children).toHaveLength(2)
235
+ expect(result.children![0].children).toHaveLength(2)
236
+ expect(result.children![1].children).toHaveLength(1)
237
+ })
238
+ })
239
+
240
+ describe('array children parsing', () => {
241
+ it('handles empty children array', () => {
242
+ const json = '{"type":"box","props":{},"children":[]}'
243
+ const result = parseUINode(json)
244
+
245
+ expect(Array.isArray(result.children)).toBe(true)
246
+ expect(result.children).toHaveLength(0)
247
+ })
248
+
249
+ it('handles children with mixed prop types', () => {
250
+ const json = `{
251
+ "type": "container",
252
+ "props": {},
253
+ "children": [
254
+ { "type": "node1", "props": { "string": "text", "number": 42, "bool": true } },
255
+ { "type": "node2", "props": { "array": [1,2,3], "obj": { "nested": true } } }
256
+ ]
257
+ }`
258
+ const result = parseUINode(json)
259
+
260
+ expect(result.children![0].props.string).toBe('text')
261
+ expect(result.children![0].props.number).toBe(42)
262
+ expect(result.children![0].props.bool).toBe(true)
263
+ expect(Array.isArray(result.children![1].props.array)).toBe(true)
264
+ expect(typeof result.children![1].props.obj).toBe('object')
265
+ })
266
+
267
+ it('preserves child order', () => {
268
+ const json = `{
269
+ "type": "list",
270
+ "props": {},
271
+ "children": [
272
+ { "type": "item", "props": { "index": 0 } },
273
+ { "type": "item", "props": { "index": 1 } },
274
+ { "type": "item", "props": { "index": 2 } }
275
+ ]
276
+ }`
277
+ const result = parseUINode(json)
278
+
279
+ expect(result.children![0].props.index).toBe(0)
280
+ expect(result.children![1].props.index).toBe(1)
281
+ expect(result.children![2].props.index).toBe(2)
282
+ })
283
+ })
284
+
285
+ describe('schema validation', () => {
286
+ it('validates parsed node against UINodeSchema', () => {
287
+ const json = '{"type":"text","props":{"content":"Valid"}}'
288
+ const result = parseUINode(json)
289
+
290
+ const validation = UINodeSchema.safeParse(result)
291
+ expect(validation.success).toBe(true)
292
+ })
293
+
294
+ it('validates nested children against UINodeSchema', () => {
295
+ const json = `{
296
+ "type": "box",
297
+ "props": {},
298
+ "children": [
299
+ { "type": "text", "props": {} }
300
+ ]
301
+ }`
302
+ const result = parseUINode(json)
303
+
304
+ const validation = UINodeSchema.safeParse(result)
305
+ expect(validation.success).toBe(true)
306
+ })
307
+
308
+ it('ensures all required fields are present after parsing', () => {
309
+ const json = '{"type":"component","props":{"prop1":"value"}}'
310
+ const result = parseUINode(json)
311
+
312
+ expect(result.type).toBeDefined()
313
+ expect(typeof result.type).toBe('string')
314
+ expect(result.props).toBeDefined()
315
+ expect(typeof result.props).toBe('object')
316
+ })
317
+ })
318
+ })
319
+
320
+ // ============================================================================
321
+ // parseUINode Function Tests - Invalid Input Handling
322
+ // ============================================================================
323
+
324
+ describe('parseUINode error handling', () => {
325
+ describe('invalid JSON strings', () => {
326
+ it('throws ParseError for malformed JSON', () => {
327
+ const invalidJson = '{type: "text"}'
328
+ expect(() => parseUINode(invalidJson)).toThrow(ParseError)
329
+ })
330
+
331
+ it('throws ParseError for incomplete JSON', () => {
332
+ const invalidJson = '{"type":"text"'
333
+ expect(() => parseUINode(invalidJson)).toThrow(ParseError)
334
+ })
335
+
336
+ it('throws ParseError for trailing comma in JSON', () => {
337
+ const invalidJson = '{"type":"text","props":{},}'
338
+ expect(() => parseUINode(invalidJson)).toThrow(ParseError)
339
+ })
340
+
341
+ it('throws ParseError for single quotes in JSON', () => {
342
+ const invalidJson = "{'type':'text'}"
343
+ expect(() => parseUINode(invalidJson)).toThrow(ParseError)
344
+ })
345
+
346
+ it('throws ParseError for undefined value in JSON', () => {
347
+ const invalidJson = '{"type":"text","props":undefined}'
348
+ expect(() => parseUINode(invalidJson)).toThrow(ParseError)
349
+ })
350
+
351
+ it('throws ParseError for JSON array as root', () => {
352
+ const invalidJson = '[{"type":"text","props":{}}]'
353
+ expect(() => parseUINode(invalidJson)).toThrow(ParseError)
354
+ })
355
+
356
+ it('throws ParseError for null value', () => {
357
+ const invalidJson = 'null'
358
+ expect(() => parseUINode(invalidJson)).toThrow(ParseError)
359
+ })
360
+
361
+ it('throws ParseError for JSON string', () => {
362
+ const invalidJson = '"just a string"'
363
+ expect(() => parseUINode(invalidJson)).toThrow(ParseError)
364
+ })
365
+
366
+ it('throws ParseError for JSON number', () => {
367
+ const invalidJson = '42'
368
+ expect(() => parseUINode(invalidJson)).toThrow(ParseError)
369
+ })
370
+
371
+ it('throws ParseError for empty string', () => {
372
+ const invalidJson = ''
373
+ expect(() => parseUINode(invalidJson)).toThrow(ParseError)
374
+ })
375
+ })
376
+
377
+ describe('invalid UINode structure', () => {
378
+ it('throws ParseError for missing type field', () => {
379
+ const json = '{"props":{}}'
380
+ expect(() => parseUINode(json)).toThrow(ParseError)
381
+ })
382
+
383
+ it('accepts missing props field (props is optional)', () => {
384
+ const json = '{"type":"text"}'
385
+ const result = parseUINode(json)
386
+ expect(result.type).toBe('text')
387
+ })
388
+
389
+ it('throws ParseError for non-string type', () => {
390
+ const json = '{"type":123,"props":{}}'
391
+ expect(() => parseUINode(json)).toThrow(ParseError)
392
+ })
393
+
394
+ it('throws ParseError for non-object props', () => {
395
+ const json = '{"type":"text","props":"invalid"}'
396
+ expect(() => parseUINode(json)).toThrow(ParseError)
397
+ })
398
+
399
+ it('throws ParseError for array props', () => {
400
+ const json = '{"type":"text","props":[]}'
401
+ expect(() => parseUINode(json)).toThrow(ParseError)
402
+ })
403
+
404
+ it('accepts string children (children can be string or array)', () => {
405
+ const json = '{"type":"box","props":{},"children":"string content"}'
406
+ const result = parseUINode(json)
407
+ expect(result.children).toBe('string content')
408
+ })
409
+
410
+ it('throws ParseError for invalid child in children array', () => {
411
+ const json = `{
412
+ "type": "box",
413
+ "props": {},
414
+ "children": [
415
+ { "type": "valid", "props": {} },
416
+ "invalid child",
417
+ { "props": {} }
418
+ ]
419
+ }`
420
+ expect(() => parseUINode(json)).toThrow(ParseError)
421
+ })
422
+
423
+ it('throws ParseError for non-string key', () => {
424
+ const json = '{"type":"item","props":{},"key":123}'
425
+ expect(() => parseUINode(json)).toThrow(ParseError)
426
+ })
427
+
428
+ it('throws ParseError for deeply nested invalid child', () => {
429
+ const json = `{
430
+ "type": "root",
431
+ "props": {},
432
+ "children": [
433
+ {
434
+ "type": "parent",
435
+ "props": {},
436
+ "children": [
437
+ { "invalid": true }
438
+ ]
439
+ }
440
+ ]
441
+ }`
442
+ expect(() => parseUINode(json)).toThrow(ParseError)
443
+ })
444
+ })
445
+
446
+ describe('error information', () => {
447
+ it('ParseError contains helpful message for missing type', () => {
448
+ const json = '{"props":{}}'
449
+ try {
450
+ parseUINode(json)
451
+ expect.fail('Should have thrown ParseError')
452
+ } catch (error) {
453
+ expect(error).toBeInstanceOf(ParseError)
454
+ expect((error as ParseError).message).toContain('type')
455
+ }
456
+ })
457
+
458
+ it('ParseError contains helpful message for invalid JSON', () => {
459
+ const json = '{invalid}'
460
+ try {
461
+ parseUINode(json)
462
+ expect.fail('Should have thrown ParseError')
463
+ } catch (error) {
464
+ expect(error).toBeInstanceOf(ParseError)
465
+ expect((error as ParseError).message).toBeDefined()
466
+ }
467
+ })
468
+
469
+ it('ParseError includes parsing context', () => {
470
+ const json = '{"type":"test","props":{"nested":{"value":incomplete}}}'
471
+ try {
472
+ parseUINode(json)
473
+ expect.fail('Should have thrown ParseError')
474
+ } catch (error) {
475
+ expect(error).toBeInstanceOf(ParseError)
476
+ }
477
+ })
478
+ })
479
+ })
480
+
481
+ // ============================================================================
482
+ // parseUINodeFromJSON Helper Function Tests
483
+ // ============================================================================
484
+
485
+ describe('parseUINodeFromJSON function', () => {
486
+ describe('parsing from JSON string', () => {
487
+ it('parses valid JSON string', () => {
488
+ const json = '{"type":"text","props":{}}'
489
+ const result = parseUINodeFromJSON(json)
490
+
491
+ expect(result).toBeDefined()
492
+ expect(result.type).toBe('text')
493
+ })
494
+
495
+ it('parses JSON with complex props', () => {
496
+ const json = `{
497
+ "type": "component",
498
+ "props": {
499
+ "title": "Example",
500
+ "count": 5,
501
+ "active": true,
502
+ "tags": ["a", "b", "c"],
503
+ "metadata": { "key": "value" }
504
+ }
505
+ }`
506
+ const result = parseUINodeFromJSON(json)
507
+
508
+ expect(result.props.title).toBe('Example')
509
+ expect(result.props.count).toBe(5)
510
+ expect(result.props.active).toBe(true)
511
+ expect(Array.isArray(result.props.tags)).toBe(true)
512
+ expect(typeof result.props.metadata).toBe('object')
513
+ })
514
+ })
515
+
516
+ describe('error handling for JSON string', () => {
517
+ it('throws ParseError for invalid JSON', () => {
518
+ const json = '{type: "invalid"}'
519
+ expect(() => parseUINodeFromJSON(json)).toThrow(ParseError)
520
+ })
521
+
522
+ it('throws ParseError for non-object JSON', () => {
523
+ const json = '[1, 2, 3]'
524
+ expect(() => parseUINodeFromJSON(json)).toThrow(ParseError)
525
+ })
526
+ })
527
+ })
528
+
529
+ // ============================================================================
530
+ // Edge Cases and Special Scenarios
531
+ // ============================================================================
532
+
533
+ describe('edge cases', () => {
534
+ describe('special prop values', () => {
535
+ it('handles null props values', () => {
536
+ const json = '{"type":"component","props":{"nullable":null}}'
537
+ const result = parseUINode(json)
538
+
539
+ expect(result.props.nullable).toBeNull()
540
+ })
541
+
542
+ it('handles boolean props', () => {
543
+ const json = '{"type":"component","props":{"disabled":false,"enabled":true}}'
544
+ const result = parseUINode(json)
545
+
546
+ expect(result.props.disabled).toBe(false)
547
+ expect(result.props.enabled).toBe(true)
548
+ })
549
+
550
+ it('handles numeric props (integers and floats)', () => {
551
+ const json = '{"type":"component","props":{"integer":42,"float":3.14}}'
552
+ const result = parseUINode(json)
553
+
554
+ expect(result.props.integer).toBe(42)
555
+ expect(result.props.float).toBe(3.14)
556
+ })
557
+
558
+ it('handles string props with special characters', () => {
559
+ const json = '{"type":"component","props":{"text":"Hello\\nWorld\\t!"}}'
560
+ const result = parseUINode(json)
561
+
562
+ expect(result.props.text).toContain('Hello')
563
+ })
564
+
565
+ it('handles empty string props', () => {
566
+ const json = '{"type":"component","props":{"empty":""}}'
567
+ const result = parseUINode(json)
568
+
569
+ expect(result.props.empty).toBe('')
570
+ })
571
+
572
+ it('handles array props', () => {
573
+ const json = '{"type":"component","props":{"items":[1,2,3,{"nested":true}]}}'
574
+ const result = parseUINode(json)
575
+
576
+ expect(Array.isArray(result.props.items)).toBe(true)
577
+ expect(result.props.items).toHaveLength(4)
578
+ })
579
+
580
+ it('handles object props with nested structure', () => {
581
+ const json =
582
+ '{"type":"component","props":{"config":{"level1":{"level2":{"level3":"deep"}}}}}'
583
+ const result = parseUINode(json)
584
+
585
+ expect((result.props.config as any).level1.level2.level3).toBe('deep')
586
+ })
587
+ })
588
+
589
+ describe('data field variations', () => {
590
+ it('handles data as array', () => {
591
+ const json = '{"type":"list","props":{},"data":[1,2,3]}'
592
+ const result = parseUINode(json)
593
+
594
+ expect(Array.isArray(result.data)).toBe(true)
595
+ })
596
+
597
+ it('handles data as object', () => {
598
+ const json = '{"type":"table","props":{},"data":{"rows":[],"cols":[]}}'
599
+ const result = parseUINode(json)
600
+
601
+ expect(typeof result.data).toBe('object')
602
+ })
603
+
604
+ it('handles data as primitive', () => {
605
+ const json = '{"type":"counter","props":{},"data":42}'
606
+ const result = parseUINode(json)
607
+
608
+ expect(result.data).toBe(42)
609
+ })
610
+
611
+ it('handles data as string', () => {
612
+ const json = '{"type":"text","props":{},"data":"bound data"}'
613
+ const result = parseUINode(json)
614
+
615
+ expect(result.data).toBe('bound data')
616
+ })
617
+ })
618
+
619
+ describe('Unicode and internationalization', () => {
620
+ it('handles Unicode characters in strings', () => {
621
+ const json = '{"type":"text","props":{"content":"Hello 世界 مرحبا"}}'
622
+ const result = parseUINode(json)
623
+
624
+ expect(result.props.content).toContain('世界')
625
+ })
626
+
627
+ it('handles emoji in props', () => {
628
+ const json = '{"type":"component","props":{"icon":"🎉"}}'
629
+ const result = parseUINode(json)
630
+
631
+ expect(result.props.icon).toBe('🎉')
632
+ })
633
+ })
634
+
635
+ describe('large and complex structures', () => {
636
+ it('handles large number of children', () => {
637
+ const childrenJson = Array.from({ length: 100 }, (_, i) =>
638
+ `{"type":"item","props":{"index":${i}}}`
639
+ ).join(',')
640
+ const json = `{"type":"list","props":{},"children":[${childrenJson}]}`
641
+
642
+ const result = parseUINode(json)
643
+ expect(result.children).toHaveLength(100)
644
+ })
645
+
646
+ it('handles deeply nested structure (10+ levels)', () => {
647
+ let json = '{"type":"root","props":{},"children":[{'
648
+ for (let i = 0; i < 10; i++) {
649
+ json += `"type":"level${i}","props":{},"children":[{`
650
+ }
651
+ json += '"type":"leaf","props":{}'
652
+ for (let i = 0; i < 10; i++) {
653
+ json += '}]'
654
+ }
655
+ json += '}]}'
656
+
657
+ const result = parseUINode(json)
658
+ let current = result
659
+ // Traverse 11 levels: root -> level0 -> level1 -> ... -> level9 -> leaf
660
+ for (let i = 0; i < 11; i++) {
661
+ expect(current.children).toBeDefined()
662
+ current = current.children![0]
663
+ }
664
+ expect(current.type).toBe('leaf')
665
+ })
666
+ })
667
+ })
668
+
669
+ // ============================================================================
670
+ // Optional: YAML Parsing Support
671
+ // ============================================================================
672
+
673
+ describe('YAML parsing (optional)', () => {
674
+ describe('parseUINode with YAML input', () => {
675
+ it('parses YAML string to UINode', () => {
676
+ const yaml = `
677
+ type: text
678
+ props:
679
+ content: Hello
680
+ `
681
+ // This test may be skipped if YAML support is not implemented
682
+ try {
683
+ const result = parseUINode(yaml)
684
+ expect(result.type).toBe('text')
685
+ expect(result.props.content).toBe('Hello')
686
+ } catch (error) {
687
+ // YAML support is optional, so we don't fail the test suite
688
+ console.log('YAML support not implemented (optional)')
689
+ }
690
+ })
691
+
692
+ it('parses YAML with nested children', () => {
693
+ const yaml = `
694
+ type: box
695
+ props:
696
+ padding: 2
697
+ children:
698
+ - type: text
699
+ props:
700
+ content: Child
701
+ `
702
+ try {
703
+ const result = parseUINode(yaml)
704
+ expect(result.type).toBe('box')
705
+ expect(result.children![0].type).toBe('text')
706
+ } catch (error) {
707
+ console.log('YAML support not implemented (optional)')
708
+ }
709
+ })
710
+
711
+ it('handles YAML multiline strings', () => {
712
+ const yaml = `
713
+ type: text
714
+ props:
715
+ content: |
716
+ Line 1
717
+ Line 2
718
+ Line 3
719
+ `
720
+ try {
721
+ const result = parseUINode(yaml)
722
+ expect((result.props.content as string).includes('Line 1')).toBe(true)
723
+ } catch (error) {
724
+ console.log('YAML support not implemented (optional)')
725
+ }
726
+ })
727
+ })
728
+
729
+ describe('YAML error handling', () => {
730
+ it('throws ParseError for invalid YAML', () => {
731
+ const invalidYaml = `
732
+ type: box
733
+ props:
734
+ - invalid list syntax
735
+ `
736
+ try {
737
+ expect(() => parseUINode(invalidYaml)).toThrow()
738
+ } catch (error) {
739
+ console.log('YAML support not implemented (optional)')
740
+ }
741
+ })
742
+ })
743
+ })
744
+
745
+ // ============================================================================
746
+ // Roundtrip / Idempotency Tests
747
+ // ============================================================================
748
+
749
+ describe('parsing idempotency', () => {
750
+ it('parsing JSON-stringified UINode produces equivalent UINode', () => {
751
+ const original: UINode = {
752
+ type: 'box',
753
+ props: { padding: 2 },
754
+ children: [{ type: 'text', props: { content: 'Test' } }],
755
+ data: { value: 42 },
756
+ key: 'test-key',
757
+ }
758
+
759
+ const json = JSON.stringify(original)
760
+ const parsed = parseUINode(json)
761
+
762
+ expect(parsed.type).toBe(original.type)
763
+ expect(parsed.props).toEqual(original.props)
764
+ expect(parsed.children).toEqual(original.children)
765
+ expect(parsed.data).toEqual(original.data)
766
+ expect(parsed.key).toEqual(original.key)
767
+ })
768
+
769
+ it('validates parsed result against schema', () => {
770
+ const json = `{
771
+ "type": "component",
772
+ "props": { "prop1": "value" },
773
+ "children": [
774
+ { "type": "child", "props": {} }
775
+ ],
776
+ "data": { "records": [] },
777
+ "key": "unique-id"
778
+ }`
779
+
780
+ const parsed = parseUINode(json)
781
+ const validation = UINodeSchema.safeParse(parsed)
782
+
783
+ expect(validation.success).toBe(true)
784
+ })
785
+ })