wyreframe 0.1.0 → 0.1.1

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 (59) 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/test/Expect.mjs +9 -0
  9. package/src/parser/Core/__tests__/Bounds_test.mjs +0 -326
  10. package/src/parser/Core/__tests__/Bounds_test.res +0 -412
  11. package/src/parser/Core/__tests__/Grid_test.mjs +0 -322
  12. package/src/parser/Core/__tests__/Grid_test.res +0 -319
  13. package/src/parser/Core/__tests__/Types_test.mjs +0 -614
  14. package/src/parser/Core/__tests__/Types_test.res +0 -650
  15. package/src/parser/Detector/__tests__/BoxTracer_test.mjs +0 -70
  16. package/src/parser/Detector/__tests__/BoxTracer_test.res +0 -92
  17. package/src/parser/Detector/__tests__/HierarchyBuilder_test.mjs +0 -489
  18. package/src/parser/Detector/__tests__/HierarchyBuilder_test.res +0 -849
  19. package/src/parser/Detector/__tests__/ShapeDetector_test.mjs +0 -377
  20. package/src/parser/Detector/__tests__/ShapeDetector_test.res +0 -563
  21. package/src/parser/Interactions/__tests__/InteractionMerger_test.mjs +0 -576
  22. package/src/parser/Interactions/__tests__/InteractionMerger_test.res +0 -646
  23. package/src/parser/Scanner/__tests__/Grid_manual.mjs +0 -214
  24. package/src/parser/Scanner/__tests__/Grid_manual.res +0 -141
  25. package/src/parser/Semantic/Elements/__tests__/ButtonParser_test.mjs +0 -189
  26. package/src/parser/Semantic/Elements/__tests__/ButtonParser_test.res +0 -257
  27. package/src/parser/Semantic/Elements/__tests__/CheckboxParser_test.mjs +0 -202
  28. package/src/parser/Semantic/Elements/__tests__/CheckboxParser_test.res +0 -250
  29. package/src/parser/Semantic/Elements/__tests__/CodeTextParser_manual.mjs +0 -293
  30. package/src/parser/Semantic/Elements/__tests__/CodeTextParser_manual.res +0 -134
  31. package/src/parser/Semantic/Elements/__tests__/InputParser_test.mjs +0 -253
  32. package/src/parser/Semantic/Elements/__tests__/InputParser_test.res +0 -304
  33. package/src/parser/Semantic/Elements/__tests__/LinkParser_test.mjs +0 -289
  34. package/src/parser/Semantic/Elements/__tests__/LinkParser_test.res +0 -402
  35. package/src/parser/Semantic/Elements/__tests__/TextParser_test.mjs +0 -149
  36. package/src/parser/Semantic/Elements/__tests__/TextParser_test.res +0 -167
  37. package/src/parser/Semantic/__tests__/ASTBuilder_test.mjs +0 -187
  38. package/src/parser/Semantic/__tests__/ASTBuilder_test.res +0 -192
  39. package/src/parser/Semantic/__tests__/ParserRegistry_test.mjs +0 -154
  40. package/src/parser/Semantic/__tests__/ParserRegistry_test.res +0 -191
  41. package/src/parser/Semantic/__tests__/SemanticParser_integration_test.mjs +0 -768
  42. package/src/parser/Semantic/__tests__/SemanticParser_integration_test.res +0 -1069
  43. package/src/parser/Semantic/__tests__/SemanticParser_manual.mjs +0 -1329
  44. package/src/parser/Semantic/__tests__/SemanticParser_manual.res +0 -544
  45. package/src/parser/__tests__/GridScanner_integration.test.mjs +0 -632
  46. package/src/parser/__tests__/GridScanner_integration.test.res +0 -816
  47. package/src/parser/__tests__/Performance.test.mjs +0 -244
  48. package/src/parser/__tests__/Performance.test.res +0 -371
  49. package/src/parser/__tests__/PerformanceFixtures.mjs +0 -200
  50. package/src/parser/__tests__/PerformanceFixtures.res +0 -284
  51. package/src/parser/__tests__/WyreframeParser_integration.test.mjs +0 -770
  52. package/src/parser/__tests__/WyreframeParser_integration.test.res +0 -1008
  53. package/src/parser/__tests__/fixtures/alignment-test.txt +0 -9
  54. package/src/parser/__tests__/fixtures/all-elements.txt +0 -16
  55. package/src/parser/__tests__/fixtures/login-scene.txt +0 -17
  56. package/src/parser/__tests__/fixtures/multi-scene.txt +0 -25
  57. package/src/parser/__tests__/fixtures/nested-boxes.txt +0 -15
  58. package/src/parser/__tests__/fixtures/simple-box.txt +0 -5
  59. package/src/parser/__tests__/fixtures/with-dividers.txt +0 -14
@@ -1,849 +0,0 @@
1
- // HierarchyBuilder_test.res
2
- // Tests for hierarchy building functionality
3
-
4
- open Vitest
5
- open Types
6
-
7
- describe("HierarchyBuilder - Containment Detection", () => {
8
- test("contains returns true when outer completely contains inner", t => {
9
- let outer = Bounds.make(~top=0, ~left=0, ~bottom=10, ~right=10)
10
- let inner = Bounds.make(~top=2, ~left=2, ~bottom=8, ~right=8)
11
- t->expect(HierarchyBuilder.contains(outer, inner))->Expect.toBe(true)
12
- })
13
-
14
- test("contains returns false when boxes are equal", t => {
15
- let bounds = Bounds.make(~top=0, ~left=0, ~bottom=10, ~right=10)
16
- t->expect(HierarchyBuilder.contains(bounds, bounds))->Expect.toBe(false)
17
- })
18
-
19
- test("contains returns false when boxes are disjoint", t => {
20
- let box1 = Bounds.make(~top=0, ~left=0, ~bottom=5, ~right=5)
21
- let box2 = Bounds.make(~top=10, ~left=10, ~bottom=15, ~right=15)
22
- t->expect(HierarchyBuilder.contains(box1, box2))->Expect.toBe(false)
23
- })
24
-
25
- test("contains returns false when boxes partially overlap", t => {
26
- let box1 = Bounds.make(~top=0, ~left=0, ~bottom=10, ~right=10)
27
- let box2 = Bounds.make(~top=5, ~left=5, ~bottom=15, ~right=15)
28
- t->expect(HierarchyBuilder.contains(box1, box2))->Expect.toBe(false)
29
- })
30
-
31
- test("contains returns false when inner touches outer's edge", t => {
32
- let outer = Bounds.make(~top=0, ~left=0, ~bottom=10, ~right=10)
33
- let inner = Bounds.make(~top=0, ~left=0, ~bottom=5, ~right=5)
34
- // Inner touches outer's top-left edge, so not strict containment
35
- t->expect(HierarchyBuilder.contains(outer, inner))->Expect.toBe(false)
36
- })
37
- })
38
-
39
- describe("HierarchyBuilder - findParent", () => {
40
- test("findParent returns None for root box (no container)", t => {
41
- let box = HierarchyBuilder.makeBox(
42
- ~name="Root",
43
- Bounds.make(~top=0, ~left=0, ~bottom=10, ~right=10),
44
- )
45
-
46
- let candidates = [box]
47
-
48
- t->expect(HierarchyBuilder.findParent(box, candidates))->Expect.toEqual(None)
49
- })
50
-
51
- test("findParent returns immediate parent (smallest containing box)", t => {
52
- let grandparent = HierarchyBuilder.makeBox(
53
- ~name="Grandparent",
54
- Bounds.make(~top=0, ~left=0, ~bottom=20, ~right=20),
55
- )
56
-
57
- let parent = HierarchyBuilder.makeBox(
58
- ~name="Parent",
59
- Bounds.make(~top=2, ~left=2, ~bottom=18, ~right=18),
60
- )
61
-
62
- let child = HierarchyBuilder.makeBox(
63
- ~name="Child",
64
- Bounds.make(~top=4, ~left=4, ~bottom=16, ~right=16),
65
- )
66
-
67
- let candidates = [grandparent, parent, child]
68
-
69
- // Child's immediate parent should be parent, not grandparent
70
- t->expect(HierarchyBuilder.findParent(child, candidates))->Expect.toEqual(Some(parent))
71
- })
72
-
73
- test("findParent ignores boxes that don't contain the target", t => {
74
- let box1 = HierarchyBuilder.makeBox(
75
- Bounds.make(~top=0, ~left=0, ~bottom=5, ~right=5),
76
- )
77
-
78
- let box2 = HierarchyBuilder.makeBox(
79
- Bounds.make(~top=10, ~left=10, ~bottom=15, ~right=15),
80
- )
81
-
82
- let candidates = [box1, box2]
83
-
84
- t->expect(HierarchyBuilder.findParent(box2, candidates))->Expect.toEqual(None)
85
- })
86
- })
87
-
88
- describe("HierarchyBuilder - buildHierarchy - 2-level nesting", () => {
89
- test("builds hierarchy with one root and one child", t => {
90
- let parent = HierarchyBuilder.makeBox(
91
- ~name="Parent",
92
- Bounds.make(~top=0, ~left=0, ~bottom=10, ~right=10),
93
- )
94
-
95
- let child = HierarchyBuilder.makeBox(
96
- ~name="Child",
97
- Bounds.make(~top=2, ~left=2, ~bottom=8, ~right=8),
98
- )
99
-
100
- let boxes = [parent, child]
101
- let result = HierarchyBuilder.buildHierarchy(boxes)
102
-
103
- switch result {
104
- | Ok(roots) => {
105
- // Should return only the root (parent)
106
- t->expect(Array.length(roots))->Expect.toBe(1)
107
-
108
- switch roots[0] {
109
- | Some(root) => {
110
- t->expect(root.name)->Expect.toEqual(Some("Parent"))
111
- // Parent should have 1 child
112
- t->expect(Array.length(root.children))->Expect.toBe(1)
113
-
114
- switch root.children[0] {
115
- | Some(childBox) => t->expect(childBox.name)->Expect.toEqual(Some("Child"))
116
- | None => t->expect(true)->Expect.toBe(false) // fail: Expected child to exist
117
- }
118
- }
119
- | None => t->expect(true)->Expect.toBe(false) // fail: Expected root to exist
120
- }
121
- }
122
- | Error(_) => t->expect(true)->Expect.toBe(false) // fail: Expected Ok result
123
- }
124
- })
125
-
126
- test("builds hierarchy with one root and multiple children", t => {
127
- let parent = HierarchyBuilder.makeBox(
128
- ~name="Parent",
129
- Bounds.make(~top=0, ~left=0, ~bottom=20, ~right=20),
130
- )
131
-
132
- let child1 = HierarchyBuilder.makeBox(
133
- ~name="Child1",
134
- Bounds.make(~top=2, ~left=2, ~bottom=8, ~right=8),
135
- )
136
-
137
- let child2 = HierarchyBuilder.makeBox(
138
- ~name="Child2",
139
- Bounds.make(~top=12, ~left=12, ~bottom=18, ~right=18),
140
- )
141
-
142
- let boxes = [parent, child1, child2]
143
- let result = HierarchyBuilder.buildHierarchy(boxes)
144
-
145
- switch result {
146
- | Ok(roots) => {
147
- t->expect(Array.length(roots))->Expect.toBe(1)
148
-
149
- switch roots[0] {
150
- | Some(root) => {
151
- t->expect(root.name)->Expect.toEqual(Some("Parent"))
152
- // Parent should have 2 children
153
- t->expect(Array.length(root.children))->Expect.toBe(2)
154
- }
155
- | None => t->expect(true)->Expect.toBe(false) // fail: Expected root to exist
156
- }
157
- }
158
- | Error(_) => t->expect(true)->Expect.toBe(false) // fail: Expected Ok result
159
- }
160
- })
161
- })
162
-
163
- describe("HierarchyBuilder - buildHierarchy - 3-level nesting", () => {
164
- test("builds hierarchy with 3 levels: root -> child -> grandchild", t => {
165
- let root = HierarchyBuilder.makeBox(
166
- ~name="Root",
167
- Bounds.make(~top=0, ~left=0, ~bottom=30, ~right=30),
168
- )
169
-
170
- let child = HierarchyBuilder.makeBox(
171
- ~name="Child",
172
- Bounds.make(~top=5, ~left=5, ~bottom=25, ~right=25),
173
- )
174
-
175
- let grandchild = HierarchyBuilder.makeBox(
176
- ~name="Grandchild",
177
- Bounds.make(~top=10, ~left=10, ~bottom=20, ~right=20),
178
- )
179
-
180
- let boxes = [root, child, grandchild]
181
- let result = HierarchyBuilder.buildHierarchy(boxes)
182
-
183
- switch result {
184
- | Ok(roots) => {
185
- // Should return only the root
186
- t->expect(Array.length(roots))->Expect.toBe(1)
187
-
188
- switch roots[0] {
189
- | Some(rootBox) => {
190
- t->expect(rootBox.name)->Expect.toEqual(Some("Root"))
191
- // Root should have 1 child
192
- t->expect(Array.length(rootBox.children))->Expect.toBe(1)
193
-
194
- switch rootBox.children[0] {
195
- | Some(childBox) => {
196
- t->expect(childBox.name)->Expect.toEqual(Some("Child"))
197
- // Child should have 1 grandchild
198
- t->expect(Array.length(childBox.children))->Expect.toBe(1)
199
-
200
- switch childBox.children[0] {
201
- | Some(grandchildBox) => t->expect(grandchildBox.name)->Expect.toEqual(Some("Grandchild"))
202
- | None => t->expect(true)->Expect.toBe(false) // fail: Expected grandchild to exist
203
- }
204
- }
205
- | None => t->expect(true)->Expect.toBe(false) // fail: Expected child to exist
206
- }
207
- }
208
- | None => t->expect(true)->Expect.toBe(false) // fail: Expected root to exist
209
- }
210
- }
211
- | Error(_) => t->expect(true)->Expect.toBe(false) // fail: Expected Ok result
212
- }
213
- })
214
-
215
- test("getDepth correctly calculates depth for 3-level hierarchy", t => {
216
- let root = HierarchyBuilder.makeBox(
217
- Bounds.make(~top=0, ~left=0, ~bottom=30, ~right=30),
218
- )
219
-
220
- let child = HierarchyBuilder.makeBox(
221
- Bounds.make(~top=5, ~left=5, ~bottom=25, ~right=25),
222
- )
223
-
224
- let grandchild = HierarchyBuilder.makeBox(
225
- Bounds.make(~top=10, ~left=10, ~bottom=20, ~right=20),
226
- )
227
-
228
- let allBoxes = [root, child, grandchild]
229
-
230
- t->expect(HierarchyBuilder.getDepth(root, allBoxes))->Expect.toBe(0)
231
- t->expect(HierarchyBuilder.getDepth(child, allBoxes))->Expect.toBe(1)
232
- t->expect(HierarchyBuilder.getDepth(grandchild, allBoxes))->Expect.toBe(2)
233
- })
234
- })
235
-
236
- describe("HierarchyBuilder - buildHierarchy - 4-level nesting", () => {
237
- test("builds hierarchy with 4 levels: root -> child -> grandchild -> great-grandchild", t => {
238
- let root = HierarchyBuilder.makeBox(
239
- ~name="Root",
240
- Bounds.make(~top=0, ~left=0, ~bottom=40, ~right=40),
241
- )
242
-
243
- let child = HierarchyBuilder.makeBox(
244
- ~name="Child",
245
- Bounds.make(~top=5, ~left=5, ~bottom=35, ~right=35),
246
- )
247
-
248
- let grandchild = HierarchyBuilder.makeBox(
249
- ~name="Grandchild",
250
- Bounds.make(~top=10, ~left=10, ~bottom=30, ~right=30),
251
- )
252
-
253
- let greatGrandchild = HierarchyBuilder.makeBox(
254
- ~name="GreatGrandchild",
255
- Bounds.make(~top=15, ~left=15, ~bottom=25, ~right=25),
256
- )
257
-
258
- let boxes = [root, child, grandchild, greatGrandchild]
259
- let result = HierarchyBuilder.buildHierarchy(boxes)
260
-
261
- switch result {
262
- | Ok(roots) => {
263
- // Should return only the root
264
- t->expect(Array.length(roots))->Expect.toBe(1)
265
-
266
- switch roots[0] {
267
- | Some(rootBox) => {
268
- t->expect(rootBox.name)->Expect.toEqual(Some("Root"))
269
- t->expect(Array.length(rootBox.children))->Expect.toBe(1)
270
-
271
- switch rootBox.children[0] {
272
- | Some(childBox) => {
273
- t->expect(childBox.name)->Expect.toEqual(Some("Child"))
274
- t->expect(Array.length(childBox.children))->Expect.toBe(1)
275
-
276
- switch childBox.children[0] {
277
- | Some(grandchildBox) => {
278
- t->expect(grandchildBox.name)->Expect.toEqual(Some("Grandchild"))
279
- t->expect(Array.length(grandchildBox.children))->Expect.toBe(1)
280
-
281
- switch grandchildBox.children[0] {
282
- | Some(greatGrandchildBox) =>
283
- t->expect(greatGrandchildBox.name)->Expect.toEqual(Some("GreatGrandchild"))
284
- | None => t->expect(true)->Expect.toBe(false) // fail: Expected great-grandchild to exist
285
- }
286
- }
287
- | None => t->expect(true)->Expect.toBe(false) // fail: Expected grandchild to exist
288
- }
289
- }
290
- | None => t->expect(true)->Expect.toBe(false) // fail: Expected child to exist
291
- }
292
- }
293
- | None => t->expect(true)->Expect.toBe(false) // fail: Expected root to exist
294
- }
295
- }
296
- | Error(_) => t->expect(true)->Expect.toBe(false) // fail: Expected Ok result
297
- }
298
- })
299
-
300
- test("getDepth correctly calculates depth for 4-level hierarchy", t => {
301
- let root = HierarchyBuilder.makeBox(
302
- Bounds.make(~top=0, ~left=0, ~bottom=40, ~right=40),
303
- )
304
-
305
- let child = HierarchyBuilder.makeBox(
306
- Bounds.make(~top=5, ~left=5, ~bottom=35, ~right=35),
307
- )
308
-
309
- let grandchild = HierarchyBuilder.makeBox(
310
- Bounds.make(~top=10, ~left=10, ~bottom=30, ~right=30),
311
- )
312
-
313
- let greatGrandchild = HierarchyBuilder.makeBox(
314
- Bounds.make(~top=15, ~left=15, ~bottom=25, ~right=25),
315
- )
316
-
317
- let allBoxes = [root, child, grandchild, greatGrandchild]
318
-
319
- t->expect(HierarchyBuilder.getDepth(root, allBoxes))->Expect.toBe(0)
320
- t->expect(HierarchyBuilder.getDepth(child, allBoxes))->Expect.toBe(1)
321
- t->expect(HierarchyBuilder.getDepth(grandchild, allBoxes))->Expect.toBe(2)
322
- t->expect(HierarchyBuilder.getDepth(greatGrandchild, allBoxes))->Expect.toBe(3)
323
- })
324
- })
325
-
326
- describe("HierarchyBuilder - buildHierarchy - Multiple roots", () => {
327
- test("handles multiple root boxes with children", t => {
328
- let root1 = HierarchyBuilder.makeBox(
329
- ~name="Root1",
330
- Bounds.make(~top=0, ~left=0, ~bottom=10, ~right=10),
331
- )
332
-
333
- let root1Child = HierarchyBuilder.makeBox(
334
- ~name="Root1Child",
335
- Bounds.make(~top=2, ~left=2, ~bottom=8, ~right=8),
336
- )
337
-
338
- let root2 = HierarchyBuilder.makeBox(
339
- ~name="Root2",
340
- Bounds.make(~top=20, ~left=20, ~bottom=30, ~right=30),
341
- )
342
-
343
- let root2Child = HierarchyBuilder.makeBox(
344
- ~name="Root2Child",
345
- Bounds.make(~top=22, ~left=22, ~bottom=28, ~right=28),
346
- )
347
-
348
- let boxes = [root1, root1Child, root2, root2Child]
349
- let result = HierarchyBuilder.buildHierarchy(boxes)
350
-
351
- switch result {
352
- | Ok(roots) => {
353
- // Should return 2 roots
354
- t->expect(Array.length(roots))->Expect.toBe(2)
355
-
356
- // Each root should have 1 child
357
- roots->Array.forEach(root => {
358
- t->expect(Array.length(root.children))->Expect.toBe(1)
359
- })
360
- }
361
- | Error(_) => t->expect(true)->Expect.toBe(false) // fail: Expected Ok result
362
- }
363
- })
364
- })
365
-
366
- describe("HierarchyBuilder - buildHierarchy - Error cases", () => {
367
- test("detects overlapping boxes (invalid partial overlap)", t => {
368
- let box1 = HierarchyBuilder.makeBox(
369
- ~name="Box1",
370
- Bounds.make(~top=0, ~left=0, ~bottom=10, ~right=10),
371
- )
372
-
373
- let box2 = HierarchyBuilder.makeBox(
374
- ~name="Box2",
375
- // Overlaps with box1 but doesn't contain or is contained by it
376
- Bounds.make(~top=5, ~left=5, ~bottom=15, ~right=15),
377
- )
378
-
379
- let boxes = [box1, box2]
380
- let result = HierarchyBuilder.buildHierarchy(boxes)
381
-
382
- switch result {
383
- | Ok(_) => t->expect(true)->Expect.toBe(false) // fail: Expected Error for overlapping boxes
384
- | Error(HierarchyBuilder.OverlappingBoxes({box1: b1, box2: b2})) => {
385
- // Verify the error contains the overlapping boxes
386
- t->expect(b1 === box1 || b1 === box2)->Expect.toBe(true)
387
- t->expect(b2 === box1 || b2 === box2)->Expect.toBe(true)
388
- t->expect(b1 !== b2)->Expect.toBe(true)
389
- }
390
- | Error(_) => t->expect(true)->Expect.toBe(false) // fail: Expected OverlappingBoxes error
391
- }
392
- })
393
-
394
- test("allows disjoint boxes (no overlap)", t => {
395
- let box1 = HierarchyBuilder.makeBox(
396
- Bounds.make(~top=0, ~left=0, ~bottom=5, ~right=5),
397
- )
398
-
399
- let box2 = HierarchyBuilder.makeBox(
400
- Bounds.make(~top=10, ~left=10, ~bottom=15, ~right=15),
401
- )
402
-
403
- let boxes = [box1, box2]
404
- let result = HierarchyBuilder.buildHierarchy(boxes)
405
-
406
- switch result {
407
- | Ok(roots) => {
408
- // Should return 2 roots (both boxes are disjoint)
409
- t->expect(Array.length(roots))->Expect.toBe(2)
410
- }
411
- | Error(_) => t->expect(true)->Expect.toBe(false) // fail: Expected Ok result for disjoint boxes
412
- }
413
- })
414
-
415
- test("allows nested boxes (one contains the other)", t => {
416
- let outer = HierarchyBuilder.makeBox(
417
- Bounds.make(~top=0, ~left=0, ~bottom=10, ~right=10),
418
- )
419
-
420
- let inner = HierarchyBuilder.makeBox(
421
- Bounds.make(~top=2, ~left=2, ~bottom=8, ~right=8),
422
- )
423
-
424
- let boxes = [outer, inner]
425
- let result = HierarchyBuilder.buildHierarchy(boxes)
426
-
427
- switch result {
428
- | Ok(roots) => {
429
- // Should return 1 root with 1 child
430
- t->expect(Array.length(roots))->Expect.toBe(1)
431
-
432
- switch roots[0] {
433
- | Some(root) => t->expect(Array.length(root.children))->Expect.toBe(1)
434
- | None => t->expect(true)->Expect.toBe(false) // fail: Expected root to exist
435
- }
436
- }
437
- | Error(_) => t->expect(true)->Expect.toBe(false) // fail: Expected Ok result for nested boxes
438
- }
439
- })
440
- })
441
-
442
- // ============================================================================
443
- // Deep Nesting Warning Detection Tests
444
- // ============================================================================
445
-
446
- describe("HierarchyBuilder - Deep Nesting Detection", () => {
447
- describe("collectDeepNestingWarnings", () => {
448
- test("should not warn for boxes at or below threshold (depth 0-4)", t => {
449
- // Create a simple root box (depth 0)
450
- let root = HierarchyBuilder.makeBox(
451
- ~name="Root",
452
- Bounds.make(~top=0, ~left=0, ~bottom=10, ~right=10),
453
- )
454
-
455
- // Should not generate warnings at depth 0-4
456
- let warnings0 = HierarchyBuilder.collectDeepNestingWarnings(root, 0, ~threshold=4)
457
- t->expect(Array.length(warnings0))->Expect.toBe(0)
458
-
459
- let warnings4 = HierarchyBuilder.collectDeepNestingWarnings(root, 4, ~threshold=4)
460
- t->expect(Array.length(warnings4))->Expect.toBe(0)
461
- })
462
-
463
- test("should warn when depth exceeds threshold of 4", t => {
464
- // Create a deeply nested box at depth 5
465
- let deepBox = HierarchyBuilder.makeBox(
466
- ~name="DeepBox",
467
- Bounds.make(~top=5, ~left=5, ~bottom=8, ~right=8),
468
- )
469
-
470
- // Should generate warning at depth 5 (threshold is 4)
471
- let warnings = HierarchyBuilder.collectDeepNestingWarnings(deepBox, 5, ~threshold=4)
472
-
473
- t->expect(Array.length(warnings))->Expect.toBe(1)
474
-
475
- // Verify warning details
476
- let warning = Array.getUnsafe(warnings, 0)
477
- t->expect(warning.severity)->Expect.toEqual(ErrorTypes.Warning)
478
-
479
- switch warning.code {
480
- | DeepNesting({depth, position}) => {
481
- t->expect(depth)->Expect.toBe(5)
482
- t->expect(position.row)->Expect.toBe(5)
483
- t->expect(position.col)->Expect.toBe(5)
484
- }
485
- | _ => t->expect(true)->Expect.toBe(false) // fail: Expected DeepNesting warning
486
- }
487
- })
488
-
489
- test("should collect warnings from all children recursively", t => {
490
- // Create parent box (depth 4)
491
- let parent = HierarchyBuilder.makeBox(
492
- ~name="Parent",
493
- Bounds.make(~top=0, ~left=0, ~bottom=20, ~right=20),
494
- )
495
-
496
- // Create two children (depth 5 - both should warn)
497
- let child1 = HierarchyBuilder.makeBox(
498
- ~name="Child1",
499
- Bounds.make(~top=2, ~left=2, ~bottom=8, ~right=8),
500
- )
501
- let child2 = HierarchyBuilder.makeBox(
502
- ~name="Child2",
503
- Bounds.make(~top=12, ~left=12, ~bottom=18, ~right=18),
504
- )
505
-
506
- // Add children to parent
507
- parent.children->Array.push(child1)->ignore
508
- parent.children->Array.push(child2)->ignore
509
-
510
- // Collect warnings starting at depth 4 (so children will be at depth 5)
511
- let warnings = HierarchyBuilder.collectDeepNestingWarnings(parent, 4, ~threshold=4)
512
-
513
- // Should have 2 warnings (one for each child)
514
- t->expect(Array.length(warnings))->Expect.toBe(2)
515
- })
516
-
517
- test("should handle custom threshold", t => {
518
- let box = HierarchyBuilder.makeBox(
519
- ~name="Box",
520
- Bounds.make(~top=0, ~left=0, ~bottom=5, ~right=5),
521
- )
522
-
523
- // With threshold 2, depth 3 should warn
524
- let warnings = HierarchyBuilder.collectDeepNestingWarnings(box, 3, ~threshold=2)
525
- t->expect(Array.length(warnings))->Expect.toBe(1)
526
-
527
- // With threshold 5, depth 3 should not warn
528
- let warnings2 = HierarchyBuilder.collectDeepNestingWarnings(box, 3, ~threshold=5)
529
- t->expect(Array.length(warnings2))->Expect.toBe(0)
530
- })
531
- })
532
-
533
- describe("detectDeepNesting", () => {
534
- test("should detect deep nesting in complete hierarchy", t => {
535
- // Create a 5-level deep hierarchy
536
- let root = HierarchyBuilder.makeBox(
537
- ~name="Root",
538
- Bounds.make(~top=0, ~left=0, ~bottom=50, ~right=50),
539
- )
540
- let level1 = HierarchyBuilder.makeBox(
541
- ~name="Level1",
542
- Bounds.make(~top=2, ~left=2, ~bottom=48, ~right=48),
543
- )
544
- let level2 = HierarchyBuilder.makeBox(
545
- ~name="Level2",
546
- Bounds.make(~top=4, ~left=4, ~bottom=46, ~right=46),
547
- )
548
- let level3 = HierarchyBuilder.makeBox(
549
- ~name="Level3",
550
- Bounds.make(~top=6, ~left=6, ~bottom=44, ~right=44),
551
- )
552
- let level4 = HierarchyBuilder.makeBox(
553
- ~name="Level4",
554
- Bounds.make(~top=8, ~left=8, ~bottom=42, ~right=42),
555
- )
556
- let level5 = HierarchyBuilder.makeBox(
557
- ~name="Level5",
558
- Bounds.make(~top=10, ~left=10, ~bottom=40, ~right=40),
559
- )
560
-
561
- // Build hierarchy
562
- root.children->Array.push(level1)->ignore
563
- level1.children->Array.push(level2)->ignore
564
- level2.children->Array.push(level3)->ignore
565
- level3.children->Array.push(level4)->ignore
566
- level4.children->Array.push(level5)->ignore
567
-
568
- // Detect warnings
569
- let warnings = HierarchyBuilder.detectDeepNesting([root], ~threshold=4)
570
-
571
- // Should warn for level5 (depth 5)
572
- t->expect(Array.length(warnings))->Expect.toBe(1)
573
-
574
- switch warnings[0] {
575
- | Some(warning) =>
576
- switch warning.code {
577
- | DeepNesting({depth, position}) => {
578
- t->expect(depth)->Expect.toBe(5)
579
- t->expect(position.row)->Expect.toBe(10)
580
- t->expect(position.col)->Expect.toBe(10)
581
- }
582
- | _ => t->expect(true)->Expect.toBe(false) // fail: Expected DeepNesting warning
583
- }
584
- | None => t->expect(true)->Expect.toBe(false) // fail: Expected warning to exist
585
- }
586
- })
587
-
588
- test("should detect multiple deep boxes in different branches", t => {
589
- // Create root with two branches
590
- let root = HierarchyBuilder.makeBox(
591
- ~name="Root",
592
- Bounds.make(~top=0, ~left=0, ~bottom=100, ~right=100),
593
- )
594
-
595
- // Branch 1: 5 levels deep
596
- let b1_l1 = HierarchyBuilder.makeBox(
597
- Bounds.make(~top=2, ~left=2, ~bottom=48, ~right=48),
598
- )
599
- let b1_l2 = HierarchyBuilder.makeBox(
600
- Bounds.make(~top=4, ~left=4, ~bottom=46, ~right=46),
601
- )
602
- let b1_l3 = HierarchyBuilder.makeBox(
603
- Bounds.make(~top=6, ~left=6, ~bottom=44, ~right=44),
604
- )
605
- let b1_l4 = HierarchyBuilder.makeBox(
606
- Bounds.make(~top=8, ~left=8, ~bottom=42, ~right=42),
607
- )
608
- let b1_l5 = HierarchyBuilder.makeBox(
609
- Bounds.make(~top=10, ~left=10, ~bottom=40, ~right=40),
610
- )
611
-
612
- // Branch 2: 6 levels deep
613
- let b2_l1 = HierarchyBuilder.makeBox(
614
- Bounds.make(~top=52, ~left=52, ~bottom=98, ~right=98),
615
- )
616
- let b2_l2 = HierarchyBuilder.makeBox(
617
- Bounds.make(~top=54, ~left=54, ~bottom=96, ~right=96),
618
- )
619
- let b2_l3 = HierarchyBuilder.makeBox(
620
- Bounds.make(~top=56, ~left=56, ~bottom=94, ~right=94),
621
- )
622
- let b2_l4 = HierarchyBuilder.makeBox(
623
- Bounds.make(~top=58, ~left=58, ~bottom=92, ~right=92),
624
- )
625
- let b2_l5 = HierarchyBuilder.makeBox(
626
- Bounds.make(~top=60, ~left=60, ~bottom=90, ~right=90),
627
- )
628
- let b2_l6 = HierarchyBuilder.makeBox(
629
- Bounds.make(~top=62, ~left=62, ~bottom=88, ~right=88),
630
- )
631
-
632
- // Build hierarchies
633
- root.children->Array.push(b1_l1)->ignore
634
- root.children->Array.push(b2_l1)->ignore
635
-
636
- b1_l1.children->Array.push(b1_l2)->ignore
637
- b1_l2.children->Array.push(b1_l3)->ignore
638
- b1_l3.children->Array.push(b1_l4)->ignore
639
- b1_l4.children->Array.push(b1_l5)->ignore
640
-
641
- b2_l1.children->Array.push(b2_l2)->ignore
642
- b2_l2.children->Array.push(b2_l3)->ignore
643
- b2_l3.children->Array.push(b2_l4)->ignore
644
- b2_l4.children->Array.push(b2_l5)->ignore
645
- b2_l5.children->Array.push(b2_l6)->ignore
646
-
647
- // Detect warnings
648
- let warnings = HierarchyBuilder.detectDeepNesting([root], ~threshold=4)
649
-
650
- // Should warn for b1_l5 (depth 5), b2_l5 (depth 5), and b2_l6 (depth 6)
651
- t->expect(Array.length(warnings))->Expect.toBe(3)
652
- })
653
-
654
- test("should handle multiple root boxes", t => {
655
- // Create two separate root hierarchies
656
- let root1 = HierarchyBuilder.makeBox(
657
- ~name="Root1",
658
- Bounds.make(~top=0, ~left=0, ~bottom=30, ~right=30),
659
- )
660
- let root1_child = HierarchyBuilder.makeBox(
661
- Bounds.make(~top=2, ~left=2, ~bottom=28, ~right=28),
662
- )
663
-
664
- let root2 = HierarchyBuilder.makeBox(
665
- ~name="Root2",
666
- Bounds.make(~top=40, ~left=40, ~bottom=90, ~right=90),
667
- )
668
- let root2_l1 = HierarchyBuilder.makeBox(
669
- Bounds.make(~top=42, ~left=42, ~bottom=88, ~right=88),
670
- )
671
- let root2_l2 = HierarchyBuilder.makeBox(
672
- Bounds.make(~top=44, ~left=44, ~bottom=86, ~right=86),
673
- )
674
- let root2_l3 = HierarchyBuilder.makeBox(
675
- Bounds.make(~top=46, ~left=46, ~bottom=84, ~right=84),
676
- )
677
- let root2_l4 = HierarchyBuilder.makeBox(
678
- Bounds.make(~top=48, ~left=48, ~bottom=82, ~right=82),
679
- )
680
- let root2_l5 = HierarchyBuilder.makeBox(
681
- Bounds.make(~top=50, ~left=50, ~bottom=80, ~right=80),
682
- )
683
-
684
- // Build hierarchies
685
- root1.children->Array.push(root1_child)->ignore
686
-
687
- root2.children->Array.push(root2_l1)->ignore
688
- root2_l1.children->Array.push(root2_l2)->ignore
689
- root2_l2.children->Array.push(root2_l3)->ignore
690
- root2_l3.children->Array.push(root2_l4)->ignore
691
- root2_l4.children->Array.push(root2_l5)->ignore
692
-
693
- // Detect warnings
694
- let warnings = HierarchyBuilder.detectDeepNesting([root1, root2], ~threshold=4)
695
-
696
- // Should warn only for root2_l5 (depth 5)
697
- t->expect(Array.length(warnings))->Expect.toBe(1)
698
- })
699
-
700
- test("should return empty array when no deep nesting", t => {
701
- // Create shallow hierarchy (max depth 2)
702
- let root = HierarchyBuilder.makeBox(
703
- Bounds.make(~top=0, ~left=0, ~bottom=30, ~right=30),
704
- )
705
- let child1 = HierarchyBuilder.makeBox(
706
- Bounds.make(~top=2, ~left=2, ~bottom=14, ~right=14),
707
- )
708
- let child2 = HierarchyBuilder.makeBox(
709
- Bounds.make(~top=16, ~left=16, ~bottom=28, ~right=28),
710
- )
711
-
712
- root.children->Array.push(child1)->ignore
713
- root.children->Array.push(child2)->ignore
714
-
715
- // Detect warnings
716
- let warnings = HierarchyBuilder.detectDeepNesting([root], ~threshold=4)
717
-
718
- t->expect(Array.length(warnings))->Expect.toBe(0)
719
- })
720
- })
721
-
722
- describe("getMaxDepth", () => {
723
- test("should return 0 for empty array", t => {
724
- let maxDepth = HierarchyBuilder.getMaxDepth([])
725
- t->expect(maxDepth)->Expect.toBe(0)
726
- })
727
-
728
- test("should return 0 for single root box with no children", t => {
729
- let root = HierarchyBuilder.makeBox(
730
- Bounds.make(~top=0, ~left=0, ~bottom=10, ~right=10),
731
- )
732
- let maxDepth = HierarchyBuilder.getMaxDepth([root])
733
- t->expect(maxDepth)->Expect.toBe(0)
734
- })
735
-
736
- test("should calculate max depth for single branch", t => {
737
- // Create 3-level hierarchy
738
- let root = HierarchyBuilder.makeBox(
739
- Bounds.make(~top=0, ~left=0, ~bottom=30, ~right=30),
740
- )
741
- let level1 = HierarchyBuilder.makeBox(
742
- Bounds.make(~top=2, ~left=2, ~bottom=28, ~right=28),
743
- )
744
- let level2 = HierarchyBuilder.makeBox(
745
- Bounds.make(~top=4, ~left=4, ~bottom=26, ~right=26),
746
- )
747
-
748
- root.children->Array.push(level1)->ignore
749
- level1.children->Array.push(level2)->ignore
750
-
751
- let maxDepth = HierarchyBuilder.getMaxDepth([root])
752
- t->expect(maxDepth)->Expect.toBe(2)
753
- })
754
-
755
- test("should find max depth across multiple branches", t => {
756
- let root = HierarchyBuilder.makeBox(
757
- Bounds.make(~top=0, ~left=0, ~bottom=100, ~right=100),
758
- )
759
-
760
- // Branch 1: depth 2
761
- let b1_l1 = HierarchyBuilder.makeBox(
762
- Bounds.make(~top=2, ~left=2, ~bottom=20, ~right=20),
763
- )
764
- let b1_l2 = HierarchyBuilder.makeBox(
765
- Bounds.make(~top=4, ~left=4, ~bottom=18, ~right=18),
766
- )
767
-
768
- // Branch 2: depth 4 (this is the max)
769
- let b2_l1 = HierarchyBuilder.makeBox(
770
- Bounds.make(~top=30, ~left=30, ~bottom=98, ~right=98),
771
- )
772
- let b2_l2 = HierarchyBuilder.makeBox(
773
- Bounds.make(~top=32, ~left=32, ~bottom=96, ~right=96),
774
- )
775
- let b2_l3 = HierarchyBuilder.makeBox(
776
- Bounds.make(~top=34, ~left=34, ~bottom=94, ~right=94),
777
- )
778
- let b2_l4 = HierarchyBuilder.makeBox(
779
- Bounds.make(~top=36, ~left=36, ~bottom=92, ~right=92),
780
- )
781
-
782
- // Build hierarchies
783
- root.children->Array.push(b1_l1)->ignore
784
- root.children->Array.push(b2_l1)->ignore
785
-
786
- b1_l1.children->Array.push(b1_l2)->ignore
787
-
788
- b2_l1.children->Array.push(b2_l2)->ignore
789
- b2_l2.children->Array.push(b2_l3)->ignore
790
- b2_l3.children->Array.push(b2_l4)->ignore
791
-
792
- let maxDepth = HierarchyBuilder.getMaxDepth([root])
793
- t->expect(maxDepth)->Expect.toBe(4)
794
- })
795
- })
796
-
797
- describe("Integration with buildHierarchy", () => {
798
- test("should detect deep nesting after building hierarchy from flat array", t => {
799
- // Create flat array of boxes representing 5-level nesting
800
- let boxes = [
801
- HierarchyBuilder.makeBox(
802
- ~name="Root",
803
- Bounds.make(~top=0, ~left=0, ~bottom=50, ~right=50),
804
- ),
805
- HierarchyBuilder.makeBox(
806
- ~name="L1",
807
- Bounds.make(~top=2, ~left=2, ~bottom=48, ~right=48),
808
- ),
809
- HierarchyBuilder.makeBox(
810
- ~name="L2",
811
- Bounds.make(~top=4, ~left=4, ~bottom=46, ~right=46),
812
- ),
813
- HierarchyBuilder.makeBox(
814
- ~name="L3",
815
- Bounds.make(~top=6, ~left=6, ~bottom=44, ~right=44),
816
- ),
817
- HierarchyBuilder.makeBox(
818
- ~name="L4",
819
- Bounds.make(~top=8, ~left=8, ~bottom=42, ~right=42),
820
- ),
821
- HierarchyBuilder.makeBox(
822
- ~name="L5",
823
- Bounds.make(~top=10, ~left=10, ~bottom=40, ~right=40),
824
- ),
825
- ]
826
-
827
- // Build hierarchy
828
- let result = HierarchyBuilder.buildHierarchy(boxes)
829
-
830
- switch result {
831
- | Ok(roots) => {
832
- // Should have 1 root
833
- t->expect(Array.length(roots))->Expect.toBe(1)
834
-
835
- // Detect deep nesting
836
- let warnings = HierarchyBuilder.detectDeepNesting(roots, ~threshold=4)
837
-
838
- // Should warn for L5 (depth 5)
839
- t->expect(Array.length(warnings))->Expect.toBe(1)
840
-
841
- // Verify max depth
842
- let maxDepth = HierarchyBuilder.getMaxDepth(roots)
843
- t->expect(maxDepth)->Expect.toBe(5)
844
- }
845
- | Error(_) => t->expect(true)->Expect.toBe(false) // fail: buildHierarchy should succeed
846
- }
847
- })
848
- })
849
- })