wyreframe 0.1.0 → 0.1.5

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 (58) hide show
  1. package/LICENSE +692 -0
  2. package/README.md +65 -5
  3. package/package.json +8 -7
  4. package/src/index.ts +425 -0
  5. package/src/renderer/Renderer.gen.tsx +49 -0
  6. package/src/renderer/Renderer.mjs +41 -1
  7. package/src/renderer/Renderer.res +78 -0
  8. package/src/parser/Core/__tests__/Bounds_test.mjs +0 -326
  9. package/src/parser/Core/__tests__/Bounds_test.res +0 -412
  10. package/src/parser/Core/__tests__/Grid_test.mjs +0 -322
  11. package/src/parser/Core/__tests__/Grid_test.res +0 -319
  12. package/src/parser/Core/__tests__/Types_test.mjs +0 -614
  13. package/src/parser/Core/__tests__/Types_test.res +0 -650
  14. package/src/parser/Detector/__tests__/BoxTracer_test.mjs +0 -70
  15. package/src/parser/Detector/__tests__/BoxTracer_test.res +0 -92
  16. package/src/parser/Detector/__tests__/HierarchyBuilder_test.mjs +0 -489
  17. package/src/parser/Detector/__tests__/HierarchyBuilder_test.res +0 -849
  18. package/src/parser/Detector/__tests__/ShapeDetector_test.mjs +0 -377
  19. package/src/parser/Detector/__tests__/ShapeDetector_test.res +0 -563
  20. package/src/parser/Interactions/__tests__/InteractionMerger_test.mjs +0 -576
  21. package/src/parser/Interactions/__tests__/InteractionMerger_test.res +0 -646
  22. package/src/parser/Scanner/__tests__/Grid_manual.mjs +0 -214
  23. package/src/parser/Scanner/__tests__/Grid_manual.res +0 -141
  24. package/src/parser/Semantic/Elements/__tests__/ButtonParser_test.mjs +0 -189
  25. package/src/parser/Semantic/Elements/__tests__/ButtonParser_test.res +0 -257
  26. package/src/parser/Semantic/Elements/__tests__/CheckboxParser_test.mjs +0 -202
  27. package/src/parser/Semantic/Elements/__tests__/CheckboxParser_test.res +0 -250
  28. package/src/parser/Semantic/Elements/__tests__/CodeTextParser_manual.mjs +0 -293
  29. package/src/parser/Semantic/Elements/__tests__/CodeTextParser_manual.res +0 -134
  30. package/src/parser/Semantic/Elements/__tests__/InputParser_test.mjs +0 -253
  31. package/src/parser/Semantic/Elements/__tests__/InputParser_test.res +0 -304
  32. package/src/parser/Semantic/Elements/__tests__/LinkParser_test.mjs +0 -289
  33. package/src/parser/Semantic/Elements/__tests__/LinkParser_test.res +0 -402
  34. package/src/parser/Semantic/Elements/__tests__/TextParser_test.mjs +0 -149
  35. package/src/parser/Semantic/Elements/__tests__/TextParser_test.res +0 -167
  36. package/src/parser/Semantic/__tests__/ASTBuilder_test.mjs +0 -187
  37. package/src/parser/Semantic/__tests__/ASTBuilder_test.res +0 -192
  38. package/src/parser/Semantic/__tests__/ParserRegistry_test.mjs +0 -154
  39. package/src/parser/Semantic/__tests__/ParserRegistry_test.res +0 -191
  40. package/src/parser/Semantic/__tests__/SemanticParser_integration_test.mjs +0 -768
  41. package/src/parser/Semantic/__tests__/SemanticParser_integration_test.res +0 -1069
  42. package/src/parser/Semantic/__tests__/SemanticParser_manual.mjs +0 -1329
  43. package/src/parser/Semantic/__tests__/SemanticParser_manual.res +0 -544
  44. package/src/parser/__tests__/GridScanner_integration.test.mjs +0 -632
  45. package/src/parser/__tests__/GridScanner_integration.test.res +0 -816
  46. package/src/parser/__tests__/Performance.test.mjs +0 -244
  47. package/src/parser/__tests__/Performance.test.res +0 -371
  48. package/src/parser/__tests__/PerformanceFixtures.mjs +0 -200
  49. package/src/parser/__tests__/PerformanceFixtures.res +0 -284
  50. package/src/parser/__tests__/WyreframeParser_integration.test.mjs +0 -770
  51. package/src/parser/__tests__/WyreframeParser_integration.test.res +0 -1008
  52. package/src/parser/__tests__/fixtures/alignment-test.txt +0 -9
  53. package/src/parser/__tests__/fixtures/all-elements.txt +0 -16
  54. package/src/parser/__tests__/fixtures/login-scene.txt +0 -17
  55. package/src/parser/__tests__/fixtures/multi-scene.txt +0 -25
  56. package/src/parser/__tests__/fixtures/nested-boxes.txt +0 -15
  57. package/src/parser/__tests__/fixtures/simple-box.txt +0 -5
  58. package/src/parser/__tests__/fixtures/with-dividers.txt +0 -14
@@ -1,1008 +0,0 @@
1
- /**
2
- * Integration Tests: WyreframeParser
3
- *
4
- * Tests login scene, multi-scene, nested boxes, interactions, errors, warnings
5
- * with realistic examples.
6
- * Requirements: REQ-25
7
- *
8
- * This test suite validates the complete WyreframeParser pipeline
9
- * (Grid Scanner → Shape Detector → Semantic Parser) with realistic wireframe examples.
10
- *
11
- * Test Framework: Vitest with rescript-vitest
12
- * Date: 2025-12-22
13
- */
14
-
15
- open Vitest
16
-
17
- // Helper for passing tests
18
- let pass = ()
19
-
20
- // =============================================================================
21
- // Recursive Element Search Helpers
22
- // =============================================================================
23
-
24
- /**
25
- * Recursively collect all elements including those nested inside Box children.
26
- * This is needed because named boxes contain their elements in Box.children,
27
- * not directly in scene.elements.
28
- */
29
- let rec collectAllElements = (elements: array<Types.element>): array<Types.element> => {
30
- elements->Array.flatMap(el => {
31
- switch el {
32
- | Box({children}) => Array.concat([el], collectAllElements(children))
33
- | other => [other]
34
- }
35
- })
36
- }
37
-
38
- /**
39
- * Find an element matching a predicate, searching recursively into Box children.
40
- */
41
- let findElement = (elements: array<Types.element>, predicate: Types.element => bool): option<Types.element> => {
42
- let allElements = collectAllElements(elements)
43
- Belt.Array.getBy(allElements, predicate)
44
- }
45
-
46
- /**
47
- * Check if any element matches a predicate, searching recursively.
48
- */
49
- let hasElement = (elements: array<Types.element>, predicate: Types.element => bool): bool => {
50
- let allElements = collectAllElements(elements)
51
- Belt.Array.some(allElements, predicate)
52
- }
53
-
54
- /**
55
- * Count elements matching a predicate, searching recursively.
56
- */
57
- let countElements = (elements: array<Types.element>, predicate: Types.element => bool): int => {
58
- let allElements = collectAllElements(elements)
59
- Belt.Array.reduce(allElements, 0, (count, el) => predicate(el) ? count + 1 : count)
60
- }
61
-
62
- // =============================================================================
63
- // E2E-01: Parse Simple Login Scene
64
- // =============================================================================
65
-
66
- describe("E2E-01: Parse Simple Login Scene", t => {
67
- test("parses complete login scene with all element types", t => {
68
- let loginWireframe = `
69
- @scene: login
70
- @title: Login Page
71
- @transition: fade
72
-
73
- +--Login----------------+
74
- | |
75
- | * Welcome Back |
76
- | |
77
- | Email: |
78
- | #email |
79
- | |
80
- | Password: |
81
- | #password |
82
- | |
83
- | [x] Remember me |
84
- | |
85
- | [ Login ] |
86
- | |
87
- | "Forgot password?" |
88
- | |
89
- +-----------------------+
90
- `
91
-
92
- // Parse wireframe
93
- let result = Parser.parse(loginWireframe)
94
-
95
- // Verify successful parse
96
- switch result {
97
- | Ok(ast) => {
98
- // Verify scene properties
99
- t->expect(Belt.Array.length(ast.scenes))->Expect.toBe(1)
100
-
101
- let scene = ast.scenes->Array.getUnsafe(0)
102
- t->expect(scene.id)->Expect.toBe("login")
103
- t->expect(scene.title)->Expect.toBe("Login Page")
104
- t->expect(scene.transition)->Expect.toBe("fade")
105
-
106
- // Verify emphasis text element (search recursively inside boxes)
107
- let emphasisFound = hasElement(scene.elements, el => {
108
- switch el {
109
- | Text({content, emphasis: true}) => Js.String2.includes(content, "Welcome Back")
110
- | _ => false
111
- }
112
- })
113
- t->expect(emphasisFound)->Expect.toBe(true)
114
-
115
- // Verify input elements (search recursively inside boxes)
116
- let emailInput = findElement(scene.elements, el => {
117
- switch el {
118
- | Input({id: "email"}) => true
119
- | _ => false
120
- }
121
- })
122
- t->expect(emailInput)->Expect.not->Expect.toBe(None)
123
-
124
- let passwordInput = findElement(scene.elements, el => {
125
- switch el {
126
- | Input({id: "password"}) => true
127
- | _ => false
128
- }
129
- })
130
- t->expect(passwordInput)->Expect.not->Expect.toBe(None)
131
-
132
- // Verify checkbox element (search recursively inside boxes)
133
- let checkbox = findElement(scene.elements, el => {
134
- switch el {
135
- | Checkbox({checked: true, label}) => Js.String2.includes(label, "Remember me")
136
- | _ => false
137
- }
138
- })
139
- t->expect(checkbox)->Expect.not->Expect.toBe(None)
140
-
141
- // Verify button element (search recursively inside boxes)
142
- let button = findElement(scene.elements, el => {
143
- switch el {
144
- | Button({text: "Login", align: Center}) => true
145
- | _ => false
146
- }
147
- })
148
- t->expect(button)->Expect.not->Expect.toBe(None)
149
-
150
- // Verify link element (search recursively inside boxes)
151
- let link = findElement(scene.elements, el => {
152
- switch el {
153
- | Link({text}) => Js.String2.includes(text, "Forgot password")
154
- | _ => false
155
- }
156
- })
157
- t->expect(link)->Expect.not->Expect.toBe(None)
158
- }
159
- | Error(errors) => {
160
- Console.error2("Parse errors:", errors)
161
- t->expect(true)->Expect.toBe(false) // fail: Expected successful parse of login scene
162
- }
163
- }
164
- })
165
- })
166
-
167
- // =============================================================================
168
- // E2E-02: Parse Multi-Scene Wireframe
169
- // =============================================================================
170
-
171
- describe("E2E-02: Parse Multi-Scene Wireframe", t => {
172
- test("parses multiple scenes with correct transitions", t => {
173
- let multiSceneWireframe = `
174
- @scene: home
175
- @title: Home Screen
176
- @transition: slide-right
177
-
178
- +--Home---------------+
179
- | |
180
- | "Go to Settings" |
181
- | |
182
- +---------------------+
183
-
184
- ---
185
-
186
- @scene: settings
187
- @title: Settings
188
- @transition: slide-left
189
-
190
- +--Settings----------+
191
- | |
192
- | [x] Notifications |
193
- | [ ] Dark Mode |
194
- | |
195
- | [ Save ] |
196
- | |
197
- +--------------------+
198
- `
199
-
200
- let result = Parser.parse(multiSceneWireframe)
201
-
202
- switch result {
203
- | Ok(ast) => {
204
- // Verify scene count
205
- t->expect(Belt.Array.length(ast.scenes))->Expect.toBe(2)
206
-
207
- // Verify first scene
208
- let homeScene = ast.scenes->Array.getUnsafe(0)
209
- t->expect(homeScene.id)->Expect.toBe("home")
210
- t->expect(homeScene.title)->Expect.toBe("Home Screen")
211
- t->expect(homeScene.transition)->Expect.toBe("slide-right")
212
-
213
- // Verify second scene
214
- let settingsScene = ast.scenes->Array.getUnsafe(1)
215
- t->expect(settingsScene.id)->Expect.toBe("settings")
216
- t->expect(settingsScene.title)->Expect.toBe("Settings")
217
- t->expect(settingsScene.transition)->Expect.toBe("slide-left")
218
-
219
- // Verify settings scene contains checkboxes (search recursively)
220
- let notificationsCheckbox = findElement(settingsScene.elements, el => {
221
- switch el {
222
- | Checkbox({checked: true, label}) => Js.String2.includes(label, "Notifications")
223
- | _ => false
224
- }
225
- })
226
- t->expect(notificationsCheckbox)->Expect.not->Expect.toBe(None)
227
-
228
- let darkModeCheckbox = findElement(settingsScene.elements, el => {
229
- switch el {
230
- | Checkbox({checked: false, label}) => Js.String2.includes(label, "Dark Mode")
231
- | _ => false
232
- }
233
- })
234
- t->expect(darkModeCheckbox)->Expect.not->Expect.toBe(None)
235
-
236
- // Verify button (search recursively)
237
- let saveButton = findElement(settingsScene.elements, el => {
238
- switch el {
239
- | Button({text: "Save"}) => true
240
- | _ => false
241
- }
242
- })
243
- t->expect(saveButton)->Expect.not->Expect.toBe(None)
244
- }
245
- | Error(errors) => {
246
- Console.error2("Parse errors:", errors)
247
- t->expect(true)->Expect.toBe(false) // fail: Expected successful parse of multi-scene wireframe
248
- }
249
- }
250
- })
251
- })
252
-
253
- // =============================================================================
254
- // E2E-03: Parse Deeply Nested Boxes
255
- // =============================================================================
256
-
257
- describe("E2E-03: Parse Deeply Nested Boxes", t => {
258
- test("parses 3-level nested boxes with correct hierarchy", t => {
259
- let nestedWireframe = `
260
- @scene: nested-test
261
-
262
- +--Level1-(Outer)------------------+
263
- | |
264
- | +--Level2-(Middle)-----------+ |
265
- | | | |
266
- | | +--Level3-(Inner)------+ | |
267
- | | | | | |
268
- | | | [ Action Button ] | | |
269
- | | | | | |
270
- | | +----------------------+ | |
271
- | | | |
272
- | +----------------------------+ |
273
- | |
274
- +----------------------------------+
275
- `
276
-
277
- let result = Parser.parse(nestedWireframe)
278
-
279
- switch result {
280
- | Ok(ast) => {
281
- let scene = ast.scenes->Array.getUnsafe(0)
282
-
283
- // Find root box
284
- let rootBox = Belt.Array.getBy(scene.elements, el => {
285
- switch el {
286
- | Box({name: Some(boxName)}) => Js.String2.includes(boxName, "Level1")
287
- | _ => false
288
- }
289
- })
290
-
291
- t->expect(rootBox)->Expect.not->Expect.toBe(None)
292
-
293
- // Verify nesting hierarchy
294
- switch rootBox {
295
- | Some(Box({children, name: Some(boxName)})) => {
296
- t->expect(Js.String2.includes(boxName, "Outer"))->Expect.toBe(true)
297
- t->expect(Belt.Array.length(children))->Expect.Int.toBeGreaterThan(0)
298
-
299
- // Check for Level 2 box
300
- let level2Box = Belt.Array.getBy(children, el => {
301
- switch el {
302
- | Box({name: Some(name)}) => Js.String2.includes(name, "Level2")
303
- | _ => false
304
- }
305
- })
306
- t->expect(level2Box)->Expect.not->Expect.toBe(None)
307
-
308
- // Check for Level 3 box within Level 2
309
- switch level2Box {
310
- | Some(Box({children: level2Children})) => {
311
- let level3Box = Belt.Array.getBy(level2Children, el => {
312
- switch el {
313
- | Box({name: Some(name)}) => Js.String2.includes(name, "Level3")
314
- | _ => false
315
- }
316
- })
317
- t->expect(level3Box)->Expect.not->Expect.toBe(None)
318
-
319
- // Verify button is in Level 3
320
- switch level3Box {
321
- | Some(Box({children: level3Children})) => {
322
- let button = Belt.Array.getBy(level3Children, el => {
323
- switch el {
324
- | Button({text}) => Js.String2.includes(text, "Action Button")
325
- | _ => false
326
- }
327
- })
328
- t->expect(button)->Expect.not->Expect.toBe(None)
329
- }
330
- | _ => t->expect(true)->Expect.toBe(false) // fail: Expected Level 3 box to be a Box element
331
- }
332
- }
333
- | _ => t->expect(true)->Expect.toBe(false) // fail: Expected Level 2 box to be a Box element
334
- }
335
- }
336
- | _ => t->expect(true)->Expect.toBe(false) // fail: Expected root box to be a Box element
337
- }
338
- }
339
- | Error(errors) => {
340
- Console.error2("Parse errors:", errors)
341
- t->expect(true)->Expect.toBe(false) // fail: Expected successful parse of nested boxes
342
- }
343
- }
344
- })
345
- })
346
-
347
- // =============================================================================
348
- // E2E-04: Parse Wireframe with Dividers
349
- // =============================================================================
350
-
351
- describe("E2E-04: Parse Wireframe with Dividers", t => {
352
- test("recognizes dividers as section separators", t => {
353
- let dividerWireframe = `
354
- @scene: profile
355
-
356
- +--UserProfile---------+
357
- | |
358
- | * John Doe |
359
- | john@example.com |
360
- | |
361
- +======================+
362
- | |
363
- | Settings |
364
- | [x] Email Updates |
365
- | [ ] SMS Alerts |
366
- | |
367
- +======================+
368
- | |
369
- | [ Save Profile ] |
370
- | |
371
- +----------------------+
372
- `
373
-
374
- let result = Parser.parse(dividerWireframe)
375
-
376
- switch result {
377
- | Ok(ast) => {
378
- let scene = ast.scenes->Array.getUnsafe(0)
379
-
380
- // Verify box with dividers exists
381
- let profileBox = Belt.Array.getBy(scene.elements, el => {
382
- switch el {
383
- | Box({name: Some(boxName)}) => Js.String2.includes(boxName, "UserProfile")
384
- | _ => false
385
- }
386
- })
387
- t->expect(profileBox)->Expect.not->Expect.toBe(None)
388
-
389
- // Count divider elements (search recursively inside boxes)
390
- let dividerCount = countElements(scene.elements, el => {
391
- switch el {
392
- | Divider(_) => true
393
- | _ => false
394
- }
395
- })
396
-
397
- // Should have detected dividers
398
- t->expect(dividerCount)->Expect.Int.toBeGreaterThan(0)
399
-
400
- // Verify all content elements are present (search recursively)
401
- let emphasisFound = hasElement(scene.elements, el => {
402
- switch el {
403
- | Text({emphasis: true}) => true
404
- | _ => false
405
- }
406
- })
407
- t->expect(emphasisFound)->Expect.toBe(true)
408
-
409
- let checkboxesFound = hasElement(scene.elements, el => {
410
- switch el {
411
- | Checkbox(_) => true
412
- | _ => false
413
- }
414
- })
415
- t->expect(checkboxesFound)->Expect.toBe(true)
416
-
417
- let buttonFound = hasElement(scene.elements, el => {
418
- switch el {
419
- | Button({text}) => Js.String2.includes(text, "Save Profile")
420
- | _ => false
421
- }
422
- })
423
- t->expect(buttonFound)->Expect.toBe(true)
424
- }
425
- | Error(errors) => {
426
- Console.error2("Parse errors:", errors)
427
- t->expect(true)->Expect.toBe(false) // fail: Expected successful parse of wireframe with dividers
428
- }
429
- }
430
- })
431
- })
432
-
433
- // =============================================================================
434
- // E2E-05: Parse Wireframe with Interactions
435
- // =============================================================================
436
-
437
- describe("E2E-05: Parse Wireframe with Interactions", t => {
438
- test("merges interaction DSL with wireframe AST", t => {
439
- let wireframe = `
440
- @scene: login
441
-
442
- +--Login---------+
443
- | |
444
- | #username |
445
- | #password |
446
- | |
447
- | [ Submit ] |
448
- | |
449
- +----------------+
450
- `
451
-
452
- let interactions = `
453
- @scene: login
454
-
455
- #username:
456
- placeholder: "Enter username"
457
- required: true
458
-
459
- #password:
460
- type: "password"
461
- placeholder: "Enter password"
462
-
463
- [ Submit ]:
464
- variant: "primary"
465
- @click -> validate(username, password)
466
- @click -> goto(dashboard, fade)
467
- `
468
-
469
- let result = Parser.parse(wireframe ++ "\n" ++ interactions)
470
-
471
- switch result {
472
- | Ok(ast) => {
473
- let scene = ast.scenes->Array.getUnsafe(0)
474
-
475
- // Verify username input exists (search recursively)
476
- let usernameInput = findElement(scene.elements, el => {
477
- switch el {
478
- | Input({id: "username"}) => true
479
- | _ => false
480
- }
481
- })
482
- t->expect(usernameInput)->Expect.not->Expect.toBe(None)
483
-
484
- // Verify password input exists (search recursively)
485
- let passwordInput = findElement(scene.elements, el => {
486
- switch el {
487
- | Input({id: "password"}) => true
488
- | _ => false
489
- }
490
- })
491
- t->expect(passwordInput)->Expect.not->Expect.toBe(None)
492
-
493
- // Verify submit button exists with actions (search recursively)
494
- let submitButton = findElement(scene.elements, el => {
495
- switch el {
496
- | Button({text: "Submit", actions}) => Belt.Array.length(actions) > 0
497
- | _ => false
498
- }
499
- })
500
- t->expect(submitButton)->Expect.not->Expect.toBe(None)
501
- }
502
- | Error(errors) => {
503
- Console.error2("Parse errors:", errors)
504
- t->expect(true)->Expect.toBe(false) // fail: Expected successful parse with interactions
505
- }
506
- }
507
- })
508
- })
509
-
510
- // =============================================================================
511
- // E2E-06: Detect Structural Errors (Unclosed Boxes)
512
- // =============================================================================
513
-
514
- describe("E2E-06: Detect Structural Errors", t => {
515
- test("detects unclosed box errors", t => {
516
- let unclosedBoxWireframe = `
517
- @scene: error-test
518
-
519
- +--Unclosed Box---+
520
- | |
521
- | Content here |
522
- +--
523
- `
524
-
525
- let result = Parser.parse(unclosedBoxWireframe)
526
-
527
- switch result {
528
- | Ok(_) => t->expect(true)->Expect.toBe(false) // fail: Expected error for unclosed box
529
- | Error(errors) => {
530
- t->expect(Belt.Array.length(errors))->Expect.Int.toBeGreaterThan(0)
531
-
532
- // Verify error is about unclosed box
533
- let hasUncloseError = Belt.Array.some(errors, error => {
534
- switch error.code {
535
- | UncloseBox(_) => true
536
- | _ => false
537
- }
538
- })
539
- t->expect(hasUncloseError)->Expect.toBe(true)
540
- }
541
- }
542
- })
543
- })
544
-
545
- // =============================================================================
546
- // E2E-07: Detect Width Mismatch Errors
547
- // =============================================================================
548
-
549
- describe("E2E-07: Detect Width Mismatch Errors", t => {
550
- test("detects mismatched top and bottom widths", t => {
551
- let mismatchedWidthWireframe = `
552
- @scene: error-test
553
-
554
- +--ShortTop--+
555
- | |
556
- +--------------+
557
- `
558
-
559
- let result = Parser.parse(mismatchedWidthWireframe)
560
-
561
- switch result {
562
- | Ok(_) => t->expect(true)->Expect.toBe(false) // fail: Expected error for width mismatch
563
- | Error(errors) => {
564
- t->expect(Belt.Array.length(errors))->Expect.Int.toBeGreaterThan(0)
565
-
566
- // Verify error is about width mismatch
567
- let hasMismatchError = Belt.Array.some(errors, error => {
568
- switch error.code {
569
- | MismatchedWidth({topWidth, bottomWidth}) => {
570
- t->expect(topWidth)->Expect.not->Expect.toBe(bottomWidth)
571
- true
572
- }
573
- | _ => false
574
- }
575
- })
576
- t->expect(hasMismatchError)->Expect.toBe(true)
577
- }
578
- }
579
- })
580
- })
581
-
582
- // =============================================================================
583
- // E2E-08: Detect Overlapping Boxes Error
584
- // NOTE: Overlapping box detection is not yet implemented in the parser.
585
- // This test is skipped until the feature is added (REQ-XX).
586
- // =============================================================================
587
-
588
- describe("E2E-08: Detect Overlapping Boxes", _t => {
589
- test("detects overlapping non-nested boxes - feature not implemented", ~skip=true, t => {
590
- let overlappingBoxesWireframe = `
591
- @scene: error-test
592
-
593
- +--Box1-------+
594
- | |
595
- | +--Box2----+---+
596
- | | | |
597
- +--|----------+ |
598
- | |
599
- +--------------+
600
- `
601
-
602
- let result = Parser.parse(overlappingBoxesWireframe)
603
-
604
- switch result {
605
- | Ok(_) => t->expect(true)->Expect.toBe(false) // fail: Expected error for overlapping boxes
606
- | Error(errors) => {
607
- t->expect(Belt.Array.length(errors))->Expect.Int.toBeGreaterThan(0)
608
-
609
- // Verify error is about overlapping boxes
610
- let hasOverlapError = Belt.Array.some(errors, error => {
611
- switch error.code {
612
- | OverlappingBoxes(_) => true
613
- | _ => false
614
- }
615
- })
616
- t->expect(hasOverlapError)->Expect.toBe(true)
617
- }
618
- }
619
- })
620
- })
621
-
622
- // =============================================================================
623
- // E2E-09: Generate Warnings for Deep Nesting
624
- // =============================================================================
625
-
626
- describe("E2E-09: Generate Warnings for Deep Nesting", t => {
627
- test("generates warning for nesting depth > 4", t => {
628
- let deepNestingWireframe = `
629
- @scene: warning-test
630
-
631
- +--L1------------------+
632
- | +--L2-------------+ |
633
- | | +--L3--------+ | |
634
- | | | +--L4----+ | | |
635
- | | | | +--L5-+ | | | |
636
- | | | | | | | | | |
637
- | | | | +-----+ | | | |
638
- | | | +---------+ | | |
639
- | | +-------------+ | |
640
- | +-----------------+ |
641
- +-----------------------+
642
- `
643
-
644
- let result = Parser.parse(deepNestingWireframe)
645
-
646
- // Parsing should succeed even with warnings
647
- switch result {
648
- | Ok(ast) => {
649
- // Check if warnings are included in result
650
- // (Implementation may vary - warnings might be in a separate field)
651
- t->expect(Belt.Array.length(ast.scenes))->Expect.toBe(1)
652
- }
653
- | Error(errors) => {
654
- // Check if any errors are actually warnings
655
- let hasDeepNestingWarning = Belt.Array.some(errors, error => {
656
- switch error.code {
657
- | DeepNesting({depth}) => {
658
- t->expect(depth)->Expect.Int.toBeGreaterThan(4)
659
- true
660
- }
661
- | _ => false
662
- }
663
- })
664
-
665
- // If errors contain warnings, that's acceptable
666
- if hasDeepNestingWarning {
667
- pass
668
- } else {
669
- Console.error2("Parse errors:", errors)
670
- t->expect(true)->Expect.toBe(false) // fail: Expected warning for deep nesting
671
- }
672
- }
673
- }
674
- })
675
- })
676
-
677
- // =============================================================================
678
- // E2E-10: Parse Complete Registration Flow
679
- // =============================================================================
680
-
681
- describe("E2E-10: Parse Complete Registration Flow", t => {
682
- test("parses realistic registration scene", t => {
683
- let registrationWireframe = `
684
- @scene: register
685
- @title: Create Account
686
- @transition: fade
687
-
688
- +--CreateAccount--------------------+
689
- | |
690
- | * Join our community |
691
- | |
692
- | First Name: |
693
- | #firstName |
694
- | |
695
- | Last Name: |
696
- | #lastName |
697
- | |
698
- | Email Address: |
699
- | #email |
700
- | |
701
- | Password: |
702
- | #password |
703
- | |
704
- | Confirm Password: |
705
- | #confirmPassword |
706
- | |
707
- | [x] I agree to Terms of Service |
708
- | [x] Subscribe to newsletter |
709
- | |
710
- | [ Create Account ] |
711
- | |
712
- | "Already have an account?" |
713
- | |
714
- +-----------------------------------+
715
- `
716
-
717
- let result = Parser.parse(registrationWireframe)
718
-
719
- switch result {
720
- | Ok(ast) => {
721
- let scene = ast.scenes->Array.getUnsafe(0)
722
-
723
- t->expect(scene.id)->Expect.toBe("register")
724
- t->expect(scene.title)->Expect.toBe("Create Account")
725
-
726
- // Count input fields (search recursively)
727
- let inputCount = countElements(scene.elements, el => {
728
- switch el {
729
- | Input(_) => true
730
- | _ => false
731
- }
732
- })
733
- t->expect(inputCount)->Expect.toBe(5)
734
-
735
- // Count checkboxes (search recursively)
736
- let checkboxCount = countElements(scene.elements, el => {
737
- switch el {
738
- | Checkbox({checked: true}) => true
739
- | _ => false
740
- }
741
- })
742
- t->expect(checkboxCount)->Expect.toBe(2)
743
-
744
- // Verify button is center-aligned (search recursively)
745
- let centerButton = findElement(scene.elements, el => {
746
- switch el {
747
- | Button({text, align: Center}) => Js.String2.includes(text, "Create Account")
748
- | _ => false
749
- }
750
- })
751
- t->expect(centerButton)->Expect.not->Expect.toBe(None)
752
-
753
- // Verify link exists (search recursively)
754
- let linkFound = hasElement(scene.elements, el => {
755
- switch el {
756
- | Link(_) => true
757
- | _ => false
758
- }
759
- })
760
- t->expect(linkFound)->Expect.toBe(true)
761
-
762
- // Verify emphasis text (search recursively)
763
- let emphasisFound = hasElement(scene.elements, el => {
764
- switch el {
765
- | Text({emphasis: true}) => true
766
- | _ => false
767
- }
768
- })
769
- t->expect(emphasisFound)->Expect.toBe(true)
770
- }
771
- | Error(errors) => {
772
- Console.error2("Parse errors:", errors)
773
- t->expect(true)->Expect.toBe(false) // fail: Expected successful parse of registration flow
774
- }
775
- }
776
- })
777
- })
778
-
779
- // =============================================================================
780
- // E2E-11: Parse Dashboard with Multiple Components
781
- // =============================================================================
782
-
783
- describe("E2E-11: Parse Dashboard with Multiple Components", t => {
784
- test("parses complex dashboard layout", t => {
785
- let dashboardWireframe = `
786
- @scene: dashboard
787
- @title: Dashboard
788
-
789
- +--Dashboard------------------------+
790
- | |
791
- | +--Header---------------------+ |
792
- | | * Dashboard | |
793
- | | "Logout" | |
794
- | +-----------------------------+ |
795
- | |
796
- | +--Stats----------------------+ |
797
- | | | |
798
- | | Users: 1,234 | |
799
- | | Revenue: $45,678 | |
800
- | | | |
801
- | +-----------------------------+ |
802
- | |
803
- | +--Actions--------------------+ |
804
- | | | |
805
- | | [ Add User ] | |
806
- | | [ Generate Report ] | |
807
- | | | |
808
- | +-----------------------------+ |
809
- | |
810
- +-----------------------------------+
811
- `
812
-
813
- let result = Parser.parse(dashboardWireframe)
814
-
815
- switch result {
816
- | Ok(ast) => {
817
- let scene = ast.scenes->Array.getUnsafe(0)
818
-
819
- // Find root dashboard box
820
- let dashboardBox = Belt.Array.getBy(scene.elements, el => {
821
- switch el {
822
- | Box({name: Some(boxName)}) => Js.String2.includes(boxName, "Dashboard")
823
- | _ => false
824
- }
825
- })
826
- t->expect(dashboardBox)->Expect.not->Expect.toBe(None)
827
-
828
- // Verify nested boxes
829
- switch dashboardBox {
830
- | Some(Box({children})) => {
831
- // Should have 3 nested boxes: Header, Stats, Actions
832
- let boxCount = Belt.Array.reduce(children, 0, (count, el) => {
833
- switch el {
834
- | Box(_) => count + 1
835
- | _ => count
836
- }
837
- })
838
- t->expect(boxCount)->Expect.toBe(3)
839
-
840
- // Verify Header box has emphasis and link
841
- let headerBox = Belt.Array.getBy(children, el => {
842
- switch el {
843
- | Box({name: Some(name)}) => Js.String2.includes(name, "Header")
844
- | _ => false
845
- }
846
- })
847
- t->expect(headerBox)->Expect.not->Expect.toBe(None)
848
-
849
- // Verify Actions box has buttons
850
- let actionsBox = Belt.Array.getBy(children, el => {
851
- switch el {
852
- | Box({name: Some(name), children: actionChildren}) => {
853
- if Js.String2.includes(name, "Actions") {
854
- let buttonCount = Belt.Array.reduce(actionChildren, 0, (count, child) => {
855
- switch child {
856
- | Button(_) => count + 1
857
- | _ => count
858
- }
859
- })
860
- buttonCount >= 2
861
- } else {
862
- false
863
- }
864
- }
865
- | _ => false
866
- }
867
- })
868
- t->expect(actionsBox)->Expect.not->Expect.toBe(None)
869
- }
870
- | _ => t->expect(true)->Expect.toBe(false) // fail: Expected dashboard box to be a Box element
871
- }
872
- }
873
- | Error(errors) => {
874
- Console.error2("Parse errors:", errors)
875
- t->expect(true)->Expect.toBe(false) // fail: Expected successful parse of dashboard
876
- }
877
- }
878
- })
879
- })
880
-
881
- // =============================================================================
882
- // E2E-12: Handle Mixed Valid and Invalid Boxes
883
- // =============================================================================
884
-
885
- describe("E2E-12: Handle Mixed Valid and Invalid Boxes", t => {
886
- test("continues parsing after errors and collects all issues", t => {
887
- let mixedWireframe = `
888
- @scene: mixed
889
-
890
- +--Good Box 1-----+
891
- | |
892
- | [ Button 1 ] |
893
- | |
894
- +-----------------+
895
-
896
- +--Bad Box 1------+
897
- | |
898
- +-------
899
-
900
- +--Good Box 2-----+
901
- | |
902
- | [ Button 2 ] |
903
- | |
904
- +-----------------+
905
-
906
- +--Bad Box 2+
907
- | |
908
- +-------------+
909
- `
910
-
911
- let result = Parser.parse(mixedWireframe)
912
-
913
- switch result {
914
- | Ok(_) => t->expect(true)->Expect.toBe(false) // fail: Expected errors for invalid boxes
915
- | Error(errors) => {
916
- // Should have multiple errors
917
- t->expect(Belt.Array.length(errors))->Expect.Int.toBeGreaterThan(1)
918
-
919
- // Check for different error types
920
- let hasUncloseError = Belt.Array.some(errors, error => {
921
- switch error.code {
922
- | UncloseBox(_) => true
923
- | _ => false
924
- }
925
- })
926
-
927
- let hasMismatchError = Belt.Array.some(errors, error => {
928
- switch error.code {
929
- | MismatchedWidth(_) => true
930
- | _ => false
931
- }
932
- })
933
-
934
- // Should have detected both error types
935
- t->expect(hasUncloseError || hasMismatchError)->Expect.toBe(true)
936
- }
937
- }
938
- })
939
-
940
- test("successfully parses valid boxes even when errors exist", t => {
941
- let mixedWireframe = `
942
- @scene: mixed
943
-
944
- +--Good Box 1-----+
945
- | |
946
- | [ Button 1 ] |
947
- | |
948
- +-----------------+
949
-
950
- +--Bad Box-------+
951
- | |
952
- +-----
953
-
954
- +--Good Box 2-----+
955
- | |
956
- | [ Button 2 ] |
957
- | |
958
- +-----------------+
959
- `
960
-
961
- let result = Parser.parse(mixedWireframe)
962
-
963
- // Even with errors, should attempt to parse valid boxes
964
- // (Implementation may return partial AST with errors)
965
- switch result {
966
- | Ok(_) => {
967
- // If implementation returns Ok with warnings
968
- pass
969
- }
970
- | Error(errors) => {
971
- // Verify we collected errors
972
- t->expect(Belt.Array.length(errors))->Expect.Int.toBeGreaterThan(0)
973
-
974
- // Check that errors are reported
975
- let hasStructuralError = Belt.Array.some(errors, error => {
976
- switch error.code {
977
- | UncloseBox(_) | MismatchedWidth(_) => true
978
- | _ => false
979
- }
980
- })
981
- t->expect(hasStructuralError)->Expect.toBe(true)
982
- }
983
- }
984
- })
985
- })
986
-
987
- // Test Suite Summary
988
- //
989
- // This test suite validates:
990
- // 1. E2E-01: Simple login scene with all element types
991
- // 2. E2E-02: Multi-scene wireframe with transitions
992
- // 3. E2E-03: Deeply nested boxes (3 levels)
993
- // 4. E2E-04: Wireframe with dividers
994
- // 5. E2E-05: Wireframe with interactions DSL
995
- // 6. E2E-06: Unclosed box errors
996
- // 7. E2E-07: Width mismatch errors
997
- // 8. E2E-08: Overlapping boxes error
998
- // 9. E2E-09: Deep nesting warnings
999
- // 10. E2E-10: Complete registration flow
1000
- // 11. E2E-11: Dashboard with multiple components
1001
- // 12. E2E-12: Mixed valid and invalid boxes
1002
- //
1003
- // Total Test Cases: 14 (12 describe blocks with multiple assertions)
1004
- // Coverage: Comprehensive integration validation of all parser stages
1005
- //
1006
- // To run:
1007
- // npm test -- WyreframeParser_integration
1008
- // npm run test:coverage