wyreframe 0.1.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 (117) hide show
  1. package/README.md +123 -0
  2. package/dist/index.d.ts +267 -0
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +195 -0
  5. package/dist/index.js.map +1 -0
  6. package/package.json +63 -0
  7. package/src/parser/Core/Bounds.mjs +61 -0
  8. package/src/parser/Core/Bounds.res +65 -0
  9. package/src/parser/Core/Grid.mjs +268 -0
  10. package/src/parser/Core/Grid.res +265 -0
  11. package/src/parser/Core/Position.mjs +83 -0
  12. package/src/parser/Core/Position.res +54 -0
  13. package/src/parser/Core/Types.mjs +435 -0
  14. package/src/parser/Core/Types.res +331 -0
  15. package/src/parser/Core/__tests__/Bounds_test.mjs +326 -0
  16. package/src/parser/Core/__tests__/Bounds_test.res +412 -0
  17. package/src/parser/Core/__tests__/Grid_test.mjs +322 -0
  18. package/src/parser/Core/__tests__/Grid_test.res +319 -0
  19. package/src/parser/Core/__tests__/Types_test.mjs +614 -0
  20. package/src/parser/Core/__tests__/Types_test.res +650 -0
  21. package/src/parser/Detector/BoxTracer.mjs +302 -0
  22. package/src/parser/Detector/BoxTracer.res +374 -0
  23. package/src/parser/Detector/HierarchyBuilder.mjs +158 -0
  24. package/src/parser/Detector/HierarchyBuilder.res +315 -0
  25. package/src/parser/Detector/ShapeDetector.mjs +134 -0
  26. package/src/parser/Detector/ShapeDetector.res +236 -0
  27. package/src/parser/Detector/__tests__/BoxTracer_test.mjs +70 -0
  28. package/src/parser/Detector/__tests__/BoxTracer_test.res +92 -0
  29. package/src/parser/Detector/__tests__/HierarchyBuilder_test.mjs +489 -0
  30. package/src/parser/Detector/__tests__/HierarchyBuilder_test.res +849 -0
  31. package/src/parser/Detector/__tests__/ShapeDetector_test.mjs +377 -0
  32. package/src/parser/Detector/__tests__/ShapeDetector_test.res +563 -0
  33. package/src/parser/Errors/ErrorContext.mjs +106 -0
  34. package/src/parser/Errors/ErrorContext.res +191 -0
  35. package/src/parser/Errors/ErrorMessages.mjs +289 -0
  36. package/src/parser/Errors/ErrorMessages.res +303 -0
  37. package/src/parser/Errors/ErrorTypes.mjs +105 -0
  38. package/src/parser/Errors/ErrorTypes.res +169 -0
  39. package/src/parser/Interactions/InteractionMerger.mjs +266 -0
  40. package/src/parser/Interactions/InteractionMerger.res +450 -0
  41. package/src/parser/Interactions/InteractionParser.mjs +88 -0
  42. package/src/parser/Interactions/InteractionParser.res +127 -0
  43. package/src/parser/Interactions/SimpleInteractionParser.mjs +278 -0
  44. package/src/parser/Interactions/SimpleInteractionParser.res +262 -0
  45. package/src/parser/Interactions/__tests__/InteractionMerger_test.mjs +576 -0
  46. package/src/parser/Interactions/__tests__/InteractionMerger_test.res +646 -0
  47. package/src/parser/Parser.gen.tsx +96 -0
  48. package/src/parser/Parser.mjs +212 -0
  49. package/src/parser/Parser.res +481 -0
  50. package/src/parser/Scanner/__tests__/Grid_manual.mjs +214 -0
  51. package/src/parser/Scanner/__tests__/Grid_manual.res +141 -0
  52. package/src/parser/Semantic/ASTBuilder.mjs +197 -0
  53. package/src/parser/Semantic/ASTBuilder.res +288 -0
  54. package/src/parser/Semantic/AlignmentCalc.mjs +41 -0
  55. package/src/parser/Semantic/AlignmentCalc.res +104 -0
  56. package/src/parser/Semantic/Elements/ButtonParser.mjs +58 -0
  57. package/src/parser/Semantic/Elements/ButtonParser.res +131 -0
  58. package/src/parser/Semantic/Elements/CheckboxParser.mjs +58 -0
  59. package/src/parser/Semantic/Elements/CheckboxParser.res +79 -0
  60. package/src/parser/Semantic/Elements/CodeTextParser.mjs +50 -0
  61. package/src/parser/Semantic/Elements/CodeTextParser.res +111 -0
  62. package/src/parser/Semantic/Elements/ElementParser.mjs +15 -0
  63. package/src/parser/Semantic/Elements/ElementParser.res +83 -0
  64. package/src/parser/Semantic/Elements/EmphasisParser.mjs +46 -0
  65. package/src/parser/Semantic/Elements/EmphasisParser.res +67 -0
  66. package/src/parser/Semantic/Elements/InputParser.mjs +41 -0
  67. package/src/parser/Semantic/Elements/InputParser.res +97 -0
  68. package/src/parser/Semantic/Elements/LinkParser.mjs +60 -0
  69. package/src/parser/Semantic/Elements/LinkParser.res +156 -0
  70. package/src/parser/Semantic/Elements/TextParser.mjs +19 -0
  71. package/src/parser/Semantic/Elements/TextParser.res +42 -0
  72. package/src/parser/Semantic/Elements/__tests__/ButtonParser_test.mjs +189 -0
  73. package/src/parser/Semantic/Elements/__tests__/ButtonParser_test.res +257 -0
  74. package/src/parser/Semantic/Elements/__tests__/CheckboxParser_test.mjs +202 -0
  75. package/src/parser/Semantic/Elements/__tests__/CheckboxParser_test.res +250 -0
  76. package/src/parser/Semantic/Elements/__tests__/CodeTextParser_manual.mjs +293 -0
  77. package/src/parser/Semantic/Elements/__tests__/CodeTextParser_manual.res +134 -0
  78. package/src/parser/Semantic/Elements/__tests__/InputParser_test.mjs +253 -0
  79. package/src/parser/Semantic/Elements/__tests__/InputParser_test.res +304 -0
  80. package/src/parser/Semantic/Elements/__tests__/LinkParser_test.mjs +289 -0
  81. package/src/parser/Semantic/Elements/__tests__/LinkParser_test.res +402 -0
  82. package/src/parser/Semantic/Elements/__tests__/TextParser_test.mjs +149 -0
  83. package/src/parser/Semantic/Elements/__tests__/TextParser_test.res +167 -0
  84. package/src/parser/Semantic/ParserRegistry.mjs +82 -0
  85. package/src/parser/Semantic/ParserRegistry.res +145 -0
  86. package/src/parser/Semantic/SemanticParser.mjs +850 -0
  87. package/src/parser/Semantic/SemanticParser.res +1368 -0
  88. package/src/parser/Semantic/__tests__/ASTBuilder_test.mjs +187 -0
  89. package/src/parser/Semantic/__tests__/ASTBuilder_test.res +192 -0
  90. package/src/parser/Semantic/__tests__/ParserRegistry_test.mjs +154 -0
  91. package/src/parser/Semantic/__tests__/ParserRegistry_test.res +191 -0
  92. package/src/parser/Semantic/__tests__/SemanticParser_integration_test.mjs +768 -0
  93. package/src/parser/Semantic/__tests__/SemanticParser_integration_test.res +1069 -0
  94. package/src/parser/Semantic/__tests__/SemanticParser_manual.mjs +1329 -0
  95. package/src/parser/Semantic/__tests__/SemanticParser_manual.res +544 -0
  96. package/src/parser/TestMain.mjs +21 -0
  97. package/src/parser/TestMain.res +14 -0
  98. package/src/parser/TextExtractor.mjs +179 -0
  99. package/src/parser/TextExtractor.res +264 -0
  100. package/src/parser/__tests__/GridScanner_integration.test.mjs +632 -0
  101. package/src/parser/__tests__/GridScanner_integration.test.res +816 -0
  102. package/src/parser/__tests__/Performance.test.mjs +244 -0
  103. package/src/parser/__tests__/Performance.test.res +371 -0
  104. package/src/parser/__tests__/PerformanceFixtures.mjs +200 -0
  105. package/src/parser/__tests__/PerformanceFixtures.res +284 -0
  106. package/src/parser/__tests__/WyreframeParser_integration.test.mjs +770 -0
  107. package/src/parser/__tests__/WyreframeParser_integration.test.res +1008 -0
  108. package/src/parser/__tests__/fixtures/alignment-test.txt +9 -0
  109. package/src/parser/__tests__/fixtures/all-elements.txt +16 -0
  110. package/src/parser/__tests__/fixtures/login-scene.txt +17 -0
  111. package/src/parser/__tests__/fixtures/multi-scene.txt +25 -0
  112. package/src/parser/__tests__/fixtures/nested-boxes.txt +15 -0
  113. package/src/parser/__tests__/fixtures/simple-box.txt +5 -0
  114. package/src/parser/__tests__/fixtures/with-dividers.txt +14 -0
  115. package/src/renderer/Renderer.gen.tsx +32 -0
  116. package/src/renderer/Renderer.mjs +391 -0
  117. package/src/renderer/Renderer.res +558 -0
@@ -0,0 +1,816 @@
1
+ /**
2
+ * Grid Scanner Integration Tests
3
+ *
4
+ * Requirements: REQ-25 (Testability - Unit Test Coverage)
5
+ *
6
+ * These integration tests verify the Grid Scanner module's ability to:
7
+ * - Parse simple boxes
8
+ * - Handle nested boxes
9
+ * - Detect dividers
10
+ * - Normalize uneven lines
11
+ * - Recognize special characters
12
+ *
13
+ * Test Framework: Vitest with rescript-vitest
14
+ * Language: ReScript
15
+ * Date: 2025-12-22
16
+ */
17
+
18
+ open Vitest
19
+
20
+ // Helper for passing tests
21
+ let pass = ()
22
+
23
+ // Note: These tests are written against the Grid module interface
24
+ // defined in the design specification.
25
+
26
+ /**
27
+ * GS-01: Simple Box Creation and Scanning
28
+ *
29
+ * Tests basic grid creation from a simple rectangular box,
30
+ * verifying dimensions, character indexing, and directional scanning.
31
+ */
32
+ describe("GS-01: Simple Box Creation and Scanning", t => {
33
+ let simpleBox = [
34
+ "+----------+",
35
+ "| |",
36
+ "| Content |",
37
+ "| |",
38
+ "+----------+",
39
+ ]
40
+
41
+ test("creates grid with correct dimensions", t => {
42
+ let grid = Grid.fromLines(simpleBox)
43
+
44
+ // "+----------+" has 12 characters (1 + 10 dashes + 1)
45
+ t->expect(grid.width)->Expect.toBe(12)
46
+ t->expect(grid.height)->Expect.toBe(5)
47
+ })
48
+
49
+ test("indexes all corner characters correctly", t => {
50
+ let grid = Grid.fromLines(simpleBox)
51
+
52
+ t->expect(Array.length(grid.cornerIndex))->Expect.toBe(4)
53
+
54
+ // Verify corner positions - corners at column 11 (0-indexed) for 12-char width
55
+ let corners = grid.cornerIndex
56
+ t->expect(Array.get(corners, 0))->Expect.toEqual(Some({Types.Position.row: 0, col: 0})) // Top-left
57
+ t->expect(Array.get(corners, 1))->Expect.toEqual(Some({Types.Position.row: 0, col: 11})) // Top-right
58
+ t->expect(Array.get(corners, 2))->Expect.toEqual(Some({Types.Position.row: 4, col: 0})) // Bottom-left
59
+ t->expect(Array.get(corners, 3))->Expect.toEqual(Some({Types.Position.row: 4, col: 11})) // Bottom-right
60
+ })
61
+
62
+ test("indexes horizontal line characters", t => {
63
+ let grid = Grid.fromLines(simpleBox)
64
+
65
+ // Top border: 10 dashes + bottom border: 10 dashes = 20 total
66
+ t->expect(Array.length(grid.hLineIndex))->Expect.toBe(20)
67
+ })
68
+
69
+ test("indexes vertical line characters", t => {
70
+ let grid = Grid.fromLines(simpleBox)
71
+
72
+ // 2 vertical lines per row × 3 middle rows = 6 total
73
+ t->expect(Array.length(grid.vLineIndex))->Expect.toBe(6)
74
+ })
75
+
76
+ test("correctly accesses characters at specific positions", t => {
77
+ let grid = Grid.fromLines(simpleBox)
78
+
79
+ // Top-left corner
80
+ switch Grid.get(grid, Types.Position.make(0, 0)) {
81
+ | Some(Corner) => pass
82
+ | _ => t->expect(true)->Expect.toBe(false) // fail: Expected Corner at (0, 0)
83
+ }
84
+
85
+ // Top border horizontal line
86
+ switch Grid.get(grid, Types.Position.make(0, 1)) {
87
+ | Some(HLine) => pass
88
+ | _ => t->expect(true)->Expect.toBe(false) // fail: Expected HLine at (0, 1)
89
+ }
90
+
91
+ // Left border vertical line
92
+ switch Grid.get(grid, Types.Position.make(1, 0)) {
93
+ | Some(VLine) => pass
94
+ | _ => t->expect(true)->Expect.toBe(false) // fail: Expected VLine at (1, 0)
95
+ }
96
+
97
+ // Space character inside box
98
+ switch Grid.get(grid, Types.Position.make(1, 1)) {
99
+ | Some(Space) => pass
100
+ | _ => t->expect(true)->Expect.toBe(false) // fail: Expected Space at (1, 1)
101
+ }
102
+
103
+ // Regular text character
104
+ switch Grid.get(grid, Types.Position.make(2, 3)) {
105
+ | Some(Char("C")) => pass
106
+ | _ => t->expect(true)->Expect.toBe(false) // fail: Expected Char('C') at (2, 3)
107
+ }
108
+ })
109
+
110
+ test("scans right from top-left corner correctly", t => {
111
+ let grid = Grid.fromLines(simpleBox)
112
+ let start = Types.Position.make(0, 0)
113
+
114
+ let results = Grid.scanRight(grid, start, cell => {
115
+ switch cell {
116
+ | Corner | HLine => true
117
+ | _ => false
118
+ }
119
+ })
120
+
121
+ // Should scan entire top border: + and 10 dashes and +
122
+ t->expect(Array.length(results))->Expect.toBe(12)
123
+ })
124
+
125
+ test("scans down from top-left corner correctly", t => {
126
+ let grid = Grid.fromLines(simpleBox)
127
+ let start = Types.Position.make(0, 0)
128
+
129
+ let results = Grid.scanDown(grid, start, cell => {
130
+ switch cell {
131
+ | Corner | VLine => true
132
+ | _ => false
133
+ }
134
+ })
135
+
136
+ // Should scan entire left border: + and 3 pipes and +
137
+ t->expect(Array.length(results))->Expect.toBe(5)
138
+ })
139
+
140
+ test("finds all corners using findAll", t => {
141
+ let grid = Grid.fromLines(simpleBox)
142
+ let corners = Grid.findAll(grid, Corner)
143
+
144
+ t->expect(Array.length(corners))->Expect.toBe(4)
145
+ })
146
+ })
147
+
148
+ /**
149
+ * GS-02: Nested Boxes with Hierarchy
150
+ *
151
+ * Tests grid scanner's ability to handle nested box structures
152
+ * while preserving spatial relationships and alignment.
153
+ */
154
+ describe("GS-02: Nested Boxes with Hierarchy", t => {
155
+ let nestedBoxes = [
156
+ "+--Outer--------------+",
157
+ "| |",
158
+ "| +--Inner-------+ |",
159
+ "| | | |",
160
+ "| | [ Button ] | |",
161
+ "| | | |",
162
+ "| +--------------+ |",
163
+ "| |",
164
+ "+---------------------+",
165
+ ]
166
+
167
+ test("creates grid with normalized dimensions", t => {
168
+ let grid = Grid.fromLines(nestedBoxes)
169
+
170
+ // "+--Outer--------------+" has 23 characters
171
+ t->expect(grid.width)->Expect.toBe(23)
172
+ t->expect(grid.height)->Expect.toBe(9)
173
+ })
174
+
175
+ test("indexes corners from both outer and inner boxes", t => {
176
+ let grid = Grid.fromLines(nestedBoxes)
177
+
178
+ // 4 outer corners + 4 inner corners = 8 total
179
+ t->expect(Array.length(grid.cornerIndex))->Expect.toBe(8)
180
+ })
181
+
182
+ test("preserves spatial relationships between boxes", t => {
183
+ let grid = Grid.fromLines(nestedBoxes)
184
+
185
+ // Verify outer box top-left corner
186
+ switch Grid.get(grid, Types.Position.make(0, 0)) {
187
+ | Some(Corner) => pass
188
+ | _ => t->expect(true)->Expect.toBe(false) // fail: Expected outer box corner at (0, 0)
189
+ }
190
+
191
+ // Verify inner box top-left corner
192
+ switch Grid.get(grid, Types.Position.make(2, 3)) {
193
+ | Some(Corner) => pass
194
+ | _ => t->expect(true)->Expect.toBe(false) // fail: Expected inner box corner at (2, 3)
195
+ }
196
+
197
+ // Verify spacing between boxes (should be spaces)
198
+ switch Grid.get(grid, Types.Position.make(1, 3)) {
199
+ | Some(Space) => pass
200
+ | _ => t->expect(true)->Expect.toBe(false) // fail: Expected space between boxes at (1, 3)
201
+ }
202
+ })
203
+
204
+ test("handles button text inside nested box", t => {
205
+ let grid = Grid.fromLines(nestedBoxes)
206
+
207
+ // Button text starts at row 4, column 7 (0-indexed)
208
+ // Row 4: "| | [ Button ] | |"
209
+ switch Grid.get(grid, Types.Position.make(4, 7)) {
210
+ | Some(Char("[")) => pass
211
+ | _ => t->expect(true)->Expect.toBe(false) // fail: Expected '[' character at button position
212
+ }
213
+ })
214
+
215
+ test("finds corners within specific bounds", t => {
216
+ let grid = Grid.fromLines(nestedBoxes)
217
+
218
+ // Define bounds for inner box area
219
+ let innerBounds = Types.Bounds.make(~top=2, ~left=3, ~bottom=6, ~right=18)
220
+ let cornersInRange = Grid.findInRange(grid, Corner, innerBounds)
221
+
222
+ // Should find 4 corners of inner box
223
+ t->expect(Array.length(cornersInRange))->Expect.toBe(4)
224
+ })
225
+
226
+ test("scans across nested structure correctly", t => {
227
+ let grid = Grid.fromLines(nestedBoxes)
228
+
229
+ // Scan right from row 4 (inner box content row)
230
+ let start = Types.Position.make(4, 0)
231
+ let results = Grid.scanRight(grid, start, _ => true)
232
+
233
+ // Should scan entire width (23 characters)
234
+ t->expect(Array.length(results))->Expect.toBe(23)
235
+ })
236
+ })
237
+
238
+ /**
239
+ * GS-03: Divider Detection and Indexing
240
+ *
241
+ * Tests grid scanner's ability to identify and index divider
242
+ * characters ('=') used as section separators within boxes.
243
+ */
244
+ describe("GS-03: Divider Detection and Indexing", t => {
245
+ let boxWithDividers = [
246
+ "+--Section Box--+",
247
+ "| |",
248
+ "| Header |",
249
+ "| |",
250
+ "+===============+",
251
+ "| Body Content |",
252
+ "| |",
253
+ "+===============+",
254
+ "| Footer |",
255
+ "| |",
256
+ "+---------------+",
257
+ ]
258
+
259
+ test("indexes all divider characters", t => {
260
+ let grid = Grid.fromLines(boxWithDividers)
261
+
262
+ // Two divider rows with 15 '=' each = 30 total
263
+ t->expect(Array.length(grid.dividerIndex))->Expect.toBe(30)
264
+ })
265
+
266
+ test("divider positions are correct", t => {
267
+ let grid = Grid.fromLines(boxWithDividers)
268
+
269
+ // First divider should be at row 4
270
+ let firstDivider = Array.getUnsafe(grid.dividerIndex, 0)
271
+ t->expect(firstDivider.row)->Expect.toBe(4)
272
+
273
+ // Second divider should be at row 7
274
+ let dividers = Grid.findAll(grid, Divider)
275
+ let row7Dividers = dividers->Array.filter(pos => pos.row == 7)
276
+ t->expect(Array.length(row7Dividers))->Expect.toBe(15)
277
+ })
278
+
279
+ test("distinguishes dividers from horizontal lines", t => {
280
+ let grid = Grid.fromLines(boxWithDividers)
281
+
282
+ // Row 4 should have dividers
283
+ switch Grid.get(grid, Types.Position.make(4, 1)) {
284
+ | Some(Divider) => pass
285
+ | _ => t->expect(true)->Expect.toBe(false) // fail: Expected Divider at row 4
286
+ }
287
+
288
+ // Row 10 (bottom) should have horizontal lines
289
+ switch Grid.get(grid, Types.Position.make(10, 1)) {
290
+ | Some(HLine) => pass
291
+ | _ => t->expect(true)->Expect.toBe(false) // fail: Expected HLine at row 10
292
+ }
293
+ })
294
+
295
+ test("findAll returns all divider positions", t => {
296
+ let grid = Grid.fromLines(boxWithDividers)
297
+ let dividers = Grid.findAll(grid, Divider)
298
+
299
+ t->expect(Array.length(dividers))->Expect.toBe(30)
300
+ })
301
+
302
+ test("scans across divider line correctly", t => {
303
+ let grid = Grid.fromLines(boxWithDividers)
304
+ let start = Types.Position.make(4, 0)
305
+
306
+ let results = Grid.scanRight(grid, start, cell => {
307
+ switch cell {
308
+ | Corner | Divider => true
309
+ | _ => false
310
+ }
311
+ })
312
+
313
+ // Should scan: + and 15 '=' and +
314
+ t->expect(Array.length(results))->Expect.toBe(17)
315
+ })
316
+
317
+ test("dividers maintain consistent width with box", t => {
318
+ let grid = Grid.fromLines(boxWithDividers)
319
+
320
+ // Get dividers from row 4
321
+ let row4Dividers = grid.dividerIndex->Array.filter(pos => pos.row == 4)
322
+
323
+ // Should span from column 1 to 15 (width - 2 for corners)
324
+ let minCol = row4Dividers->Array.reduce(999, (min, pos) =>
325
+ Math.Int.min(min, pos.col)
326
+ )
327
+ let maxCol = row4Dividers->Array.reduce(0, (max, pos) =>
328
+ Math.Int.max(max, pos.col)
329
+ )
330
+
331
+ t->expect(minCol)->Expect.toBe(1)
332
+ t->expect(maxCol)->Expect.toBe(15)
333
+ })
334
+ })
335
+
336
+ /**
337
+ * GS-04: Uneven Line Normalization
338
+ *
339
+ * Tests grid scanner's ability to normalize lines of varying
340
+ * lengths by padding with spaces.
341
+ */
342
+ describe("GS-04: Uneven Line Normalization", t => {
343
+ let unevenLines = [
344
+ "+-+",
345
+ "| |",
346
+ "+------+",
347
+ "| X",
348
+ "+--------+",
349
+ ]
350
+
351
+ test("sets grid width to maximum line length", t => {
352
+ let grid = Grid.fromLines(unevenLines)
353
+
354
+ t->expect(grid.width)->Expect.toBe(10)
355
+ })
356
+
357
+ test("pads shorter lines with spaces", t => {
358
+ let grid = Grid.fromLines(unevenLines)
359
+
360
+ // First line "+-+" should be padded to width 10
361
+ // Check position beyond original line
362
+ switch Grid.get(grid, Types.Position.make(0, 5)) {
363
+ | Some(Space) => pass
364
+ | _ => t->expect(true)->Expect.toBe(false) // fail: Expected Space padding at (0, 5)
365
+ }
366
+
367
+ switch Grid.get(grid, Types.Position.make(0, 9)) {
368
+ | Some(Space) => pass
369
+ | _ => t->expect(true)->Expect.toBe(false) // fail: Expected Space padding at (0, 9)
370
+ }
371
+ })
372
+
373
+ test("preserves original content of each line", t => {
374
+ let grid = Grid.fromLines(unevenLines)
375
+
376
+ // First line starts with "+-+"
377
+ switch Grid.get(grid, Types.Position.make(0, 0)) {
378
+ | Some(Corner) => pass
379
+ | _ => t->expect(true)->Expect.toBe(false) // fail: Expected Corner at (0, 0)
380
+ }
381
+
382
+ switch Grid.get(grid, Types.Position.make(0, 1)) {
383
+ | Some(HLine) => pass
384
+ | _ => t->expect(true)->Expect.toBe(false) // fail: Expected HLine at (0, 1)
385
+ }
386
+
387
+ switch Grid.get(grid, Types.Position.make(0, 2)) {
388
+ | Some(Corner) => pass
389
+ | _ => t->expect(true)->Expect.toBe(false) // fail: Expected Corner at (0, 2)
390
+ }
391
+ })
392
+
393
+ test("handles line with trailing content", t => {
394
+ let grid = Grid.fromLines(unevenLines)
395
+
396
+ // Line 3: "| X" (4 chars, padded to 10)
397
+ switch Grid.get(grid, Types.Position.make(3, 3)) {
398
+ | Some(Char("X")) => pass
399
+ | _ => t->expect(true)->Expect.toBe(false) // fail: Expected Char('X') at (3, 3)
400
+ }
401
+
402
+ // Check padding after 'X'
403
+ switch Grid.get(grid, Types.Position.make(3, 4)) {
404
+ | Some(Space) => pass
405
+ | _ => t->expect(true)->Expect.toBe(false) // fail: Expected Space padding at (3, 4)
406
+ }
407
+ })
408
+
409
+ test("all lines accessible as full-width arrays", t => {
410
+ let grid = Grid.fromLines(unevenLines)
411
+
412
+ // Get each line and verify width
413
+ for row in 0 to 4 {
414
+ switch Grid.getLine(grid, row) {
415
+ | Some(line) => t->expect(Array.length(line))->Expect.toBe(10)
416
+ | None => t->expect(true)->Expect.toBe(false) // fail: Expected line at row
417
+ }
418
+ }
419
+ })
420
+
421
+ test("getRange works correctly with padding", t => {
422
+ let grid = Grid.fromLines(unevenLines)
423
+
424
+ // Get range from first line beyond original content
425
+ switch Grid.getRange(grid, 0, ~startCol=3, ~endCol=9) {
426
+ | Some(range) => {
427
+ t->expect(Array.length(range))->Expect.toBe(7)
428
+ // All should be spaces
429
+ let allSpaces = range->Array.every(cell => {
430
+ switch cell {
431
+ | Space => true
432
+ | _ => false
433
+ }
434
+ })
435
+ t->expect(allSpaces)->Expect.toBe(true)
436
+ }
437
+ | None => t->expect(true)->Expect.toBe(false) // fail: Expected range
438
+ }
439
+ })
440
+ })
441
+
442
+ /**
443
+ * GS-05: Special Character Recognition
444
+ *
445
+ * Tests grid scanner's ability to correctly identify and
446
+ * categorize all special characters used in wireframes.
447
+ */
448
+ describe("GS-05: Special Character Recognition", t => {
449
+ let specialChars = [
450
+ "+------|",
451
+ "| |",
452
+ "+======+",
453
+ ]
454
+
455
+ test("recognizes Corner characters", t => {
456
+ let grid = Grid.fromLines(specialChars)
457
+
458
+ // Row 0: "+------|" has corner at 0 only (ends with |, not +)
459
+ // Row 2: "+======+" has corners at 0 and 7
460
+ // Total: 3 corners
461
+ t->expect(Array.length(grid.cornerIndex))->Expect.toBe(3)
462
+
463
+ // Verify corner character type
464
+ switch Grid.get(grid, Types.Position.make(0, 0)) {
465
+ | Some(Corner) => pass
466
+ | _ => t->expect(true)->Expect.toBe(false) // fail: Expected Corner type
467
+ }
468
+ })
469
+
470
+ test("recognizes HLine characters", t => {
471
+ let grid = Grid.fromLines(specialChars)
472
+
473
+ // Top line has 4 dashes
474
+ let hLines = Grid.findAll(grid, HLine)
475
+ t->expect(Array.length(hLines))->Expect.Int.toBeGreaterThan(0)
476
+
477
+ switch Grid.get(grid, Types.Position.make(0, 1)) {
478
+ | Some(HLine) => pass
479
+ | _ => t->expect(true)->Expect.toBe(false) // fail: Expected HLine type
480
+ }
481
+ })
482
+
483
+ test("recognizes VLine characters", t => {
484
+ let grid = Grid.fromLines(specialChars)
485
+
486
+ let vLines = Grid.findAll(grid, VLine)
487
+ t->expect(Array.length(vLines))->Expect.Int.toBeGreaterThan(0)
488
+
489
+ switch Grid.get(grid, Types.Position.make(1, 0)) {
490
+ | Some(VLine) => pass
491
+ | _ => t->expect(true)->Expect.toBe(false) // fail: Expected VLine type
492
+ }
493
+ })
494
+
495
+ test("recognizes Divider characters", t => {
496
+ let grid = Grid.fromLines(specialChars)
497
+
498
+ t->expect(Array.length(grid.dividerIndex))->Expect.toBe(6)
499
+
500
+ switch Grid.get(grid, Types.Position.make(2, 1)) {
501
+ | Some(Divider) => pass
502
+ | _ => t->expect(true)->Expect.toBe(false) // fail: Expected Divider type
503
+ }
504
+ })
505
+
506
+ test("recognizes Space characters", t => {
507
+ let grid = Grid.fromLines(specialChars)
508
+
509
+ // Spaces inside the box
510
+ switch Grid.get(grid, Types.Position.make(1, 1)) {
511
+ | Some(Space) => pass
512
+ | _ => t->expect(true)->Expect.toBe(false) // fail: Expected Space type
513
+ }
514
+ })
515
+
516
+ test("recognizes regular text as Char", t => {
517
+ let textGrid = Grid.fromLines(["Hello"])
518
+
519
+ switch Grid.get(textGrid, Types.Position.make(0, 0)) {
520
+ | Some(Char("H")) => pass
521
+ | _ => t->expect(true)->Expect.toBe(false) // fail: Expected Char('H') type
522
+ }
523
+
524
+ switch Grid.get(textGrid, Types.Position.make(0, 1)) {
525
+ | Some(Char("e")) => pass
526
+ | _ => t->expect(true)->Expect.toBe(false) // fail: Expected Char('e') type
527
+ }
528
+ })
529
+
530
+ test("all special character indices are mutually exclusive", t => {
531
+ let grid = Grid.fromLines(specialChars)
532
+
533
+ // No position should appear in multiple indices
534
+ let allPositions = Array.concat(
535
+ Array.concat(
536
+ Array.concat(grid.cornerIndex, grid.hLineIndex),
537
+ grid.vLineIndex
538
+ ),
539
+ grid.dividerIndex
540
+ )
541
+
542
+ // Convert to set and check for uniqueness
543
+ let uniquePositions = Set.fromArray(
544
+ allPositions->Array.map(pos => Types.Position.toString(pos))
545
+ )
546
+
547
+ t->expect(Set.size(uniquePositions))->Expect.toBe(Array.length(allPositions))
548
+ })
549
+ })
550
+
551
+ /**
552
+ * GS-06: Empty Input Handling
553
+ *
554
+ * Tests grid scanner's graceful handling of edge cases
555
+ * including empty and minimal inputs.
556
+ */
557
+ describe("GS-06: Empty Input Handling", t => {
558
+ test("handles empty array", t => {
559
+ let grid = Grid.fromLines([])
560
+
561
+ t->expect(grid.width)->Expect.toBe(0)
562
+ t->expect(grid.height)->Expect.toBe(0)
563
+ t->expect(Array.length(grid.cornerIndex))->Expect.toBe(0)
564
+ })
565
+
566
+ test("handles array of empty strings", t => {
567
+ let grid = Grid.fromLines(["", "", ""])
568
+
569
+ t->expect(grid.width)->Expect.toBe(0)
570
+ t->expect(grid.height)->Expect.toBe(3)
571
+ })
572
+
573
+ test("handles single character", t => {
574
+ let grid = Grid.fromLines(["a"])
575
+
576
+ t->expect(grid.width)->Expect.toBe(1)
577
+ t->expect(grid.height)->Expect.toBe(1)
578
+
579
+ switch Grid.get(grid, Types.Position.make(0, 0)) {
580
+ | Some(Char("a")) => pass
581
+ | _ => t->expect(true)->Expect.toBe(false) // fail: Expected Char('a')
582
+ }
583
+ })
584
+
585
+ test("handles single line with single special char", t => {
586
+ let grid = Grid.fromLines(["+"])
587
+
588
+ t->expect(grid.width)->Expect.toBe(1)
589
+ t->expect(grid.height)->Expect.toBe(1)
590
+ t->expect(Array.length(grid.cornerIndex))->Expect.toBe(1)
591
+ })
592
+
593
+ test("get returns None for invalid positions on empty grid", t => {
594
+ let grid = Grid.fromLines([])
595
+
596
+ t->expect(Grid.get(grid, Types.Position.make(0, 0)))->Expect.toBe(None)
597
+ t->expect(Grid.get(grid, Types.Position.make(5, 5)))->Expect.toBe(None)
598
+ })
599
+
600
+ test("isValidPosition returns false for empty grid", t => {
601
+ let grid = Grid.fromLines([])
602
+
603
+ t->expect(Grid.isValidPosition(grid, Types.Position.make(0, 0)))->Expect.toBe(false)
604
+ })
605
+ })
606
+
607
+ /**
608
+ * GS-07: Large Wireframe Performance
609
+ *
610
+ * Tests grid scanner performance with large inputs to verify
611
+ * it meets the <10ms requirement for 1000-line grids.
612
+ */
613
+ describe("GS-07: Large Wireframe Performance", t => {
614
+ // Helper to generate large grid
615
+ let generateLargeGrid = (~lines: int): array<string> => {
616
+ let result = []
617
+ for i in 0 to lines - 1 {
618
+ if mod(i, 10) == 0 {
619
+ result->Array.push("+--------------------+")->ignore
620
+ } else if mod(i, 5) == 0 {
621
+ result->Array.push("+====================+")->ignore
622
+ } else {
623
+ result->Array.push("| Content Line |")->ignore
624
+ }
625
+ }
626
+ result
627
+ }
628
+
629
+ test("parses 1000-line grid in under 10ms", t => {
630
+ let largeInput = generateLargeGrid(~lines=1000)
631
+
632
+ let start = Date.now()
633
+ let grid = Grid.fromLines(largeInput)
634
+ let duration = Date.now() -. start
635
+
636
+ t->expect(duration)->Expect.Float.toBeLessThan(10.0)
637
+ t->expect(grid.height)->Expect.toBe(1000)
638
+ })
639
+
640
+ test("builds character indices for large grid", t => {
641
+ let largeInput = generateLargeGrid(~lines=1000)
642
+ let grid = Grid.fromLines(largeInput)
643
+
644
+ // Should have indexed all corners
645
+ t->expect(Array.length(grid.cornerIndex))->Expect.Int.toBeGreaterThan(0)
646
+
647
+ // Should have indexed dividers
648
+ t->expect(Array.length(grid.dividerIndex))->Expect.Int.toBeGreaterThan(0)
649
+ })
650
+
651
+ test("random access is performant on large grid", t => {
652
+ let largeInput = generateLargeGrid(~lines=1000)
653
+ let grid = Grid.fromLines(largeInput)
654
+
655
+ let start = Date.now()
656
+
657
+ // Perform 1000 random accesses
658
+ for _ in 0 to 999 {
659
+ let randomRow = Math.Int.random(0, 999)
660
+ let randomCol = Math.Int.random(0, 22)
661
+ let _ = Grid.get(grid, Types.Position.make(randomRow, randomCol))
662
+ }
663
+
664
+ let duration = Date.now() -. start
665
+
666
+ // 1000 accesses should be very fast (under 5ms with timing variance)
667
+ t->expect(duration)->Expect.Float.toBeLessThan(5.0)
668
+ })
669
+
670
+ test("findAll is efficient on large grid", t => {
671
+ let largeInput = generateLargeGrid(~lines=1000)
672
+ let grid = Grid.fromLines(largeInput)
673
+
674
+ let start = Date.now()
675
+ let corners = Grid.findAll(grid, Corner)
676
+ let duration = Date.now() -. start
677
+
678
+ t->expect(duration)->Expect.Float.toBeLessThan(5.0)
679
+ t->expect(Array.length(corners))->Expect.Int.toBeGreaterThan(0)
680
+ })
681
+ })
682
+
683
+ /**
684
+ * GS-08: Complex Multi-Box Wireframe
685
+ *
686
+ * Integration test with realistic multi-box wireframe structure
687
+ * simulating a dashboard layout.
688
+ */
689
+ describe("GS-08: Complex Multi-Box Wireframe", t => {
690
+ let complexWireframe = [
691
+ "@scene: dashboard",
692
+ "",
693
+ "+--Header-----------------------+",
694
+ "| Logo [ Logout ] |",
695
+ "+===============================+",
696
+ "",
697
+ "+--Sidebar--+ +--Main Content-----------+",
698
+ "| | | |",
699
+ "| [ Home ] | | +--Card 1----------+ |",
700
+ "| | | | Title | |",
701
+ "| [ Data ] | | | Content here... | |",
702
+ "| | | +------------------+ |",
703
+ "| [Reports] | | |",
704
+ "| | | +--Card 2----------+ |",
705
+ "+-----------+ | | Title | |",
706
+ " | | More content... | |",
707
+ " | +------------------+ |",
708
+ " | |",
709
+ " +--------------------------+",
710
+ ]
711
+
712
+ test("parses entire complex structure", t => {
713
+ let grid = Grid.fromLines(complexWireframe)
714
+
715
+ t->expect(grid.height)->Expect.toBe(19)
716
+ t->expect(grid.width)->Expect.Int.toBeGreaterThan(0)
717
+ })
718
+
719
+ test("indexes all box corners correctly", t => {
720
+ let grid = Grid.fromLines(complexWireframe)
721
+
722
+ // Header: 4, Sidebar: 4, Main: 4, Card1: 4, Card2: 4 = 20 corners
723
+ t->expect(Array.length(grid.cornerIndex))->Expect.toBe(20)
724
+ })
725
+
726
+ test("indexes divider line in header", t => {
727
+ let grid = Grid.fromLines(complexWireframe)
728
+
729
+ // Row 4 should have divider
730
+ let row4Dividers = grid.dividerIndex->Array.filter(pos => pos.row == 4)
731
+ t->expect(Array.length(row4Dividers))->Expect.Int.toBeGreaterThan(0)
732
+ })
733
+
734
+ test("preserves spacing between sidebar and main content", t => {
735
+ let grid = Grid.fromLines(complexWireframe)
736
+
737
+ // Row 6: "+--Sidebar--+ +--Main Content-----------+"
738
+ // Position 12 is '+' (Sidebar end), positions 13-14 are spaces
739
+ switch Grid.get(grid, Types.Position.make(6, 13)) {
740
+ | Some(Space) => pass
741
+ | _ => t->expect(true)->Expect.toBe(false) // fail: Expected space between boxes
742
+ }
743
+ })
744
+
745
+ test("handles multiple adjacent boxes on same row", t => {
746
+ let grid = Grid.fromLines(complexWireframe)
747
+
748
+ // Row 6: "+--Sidebar--+ +--Main Content-----------+"
749
+ // Has 4 corners: positions 0, 11, 14, and end of line
750
+ let row6Corners = grid.cornerIndex->Array.filter(pos => pos.row == 6)
751
+ t->expect(Array.length(row6Corners))->Expect.toBe(4)
752
+ })
753
+
754
+ test("character access works across all regions", t => {
755
+ let grid = Grid.fromLines(complexWireframe)
756
+
757
+ // Header region
758
+ switch Grid.get(grid, Types.Position.make(2, 0)) {
759
+ | Some(Corner) => pass
760
+ | _ => t->expect(true)->Expect.toBe(false) // fail: Expected corner in header
761
+ }
762
+
763
+ // Sidebar region
764
+ switch Grid.get(grid, Types.Position.make(6, 0)) {
765
+ | Some(Corner) => pass
766
+ | _ => t->expect(true)->Expect.toBe(false) // fail: Expected corner in sidebar
767
+ }
768
+
769
+ // Main content region
770
+ switch Grid.get(grid, Types.Position.make(6, 15)) {
771
+ | Some(Corner) => pass
772
+ | _ => t->expect(true)->Expect.toBe(false) // fail: Expected corner in main content
773
+ }
774
+ })
775
+
776
+ test("scans across complex row with multiple boxes", t => {
777
+ let grid = Grid.fromLines(complexWireframe)
778
+
779
+ // Row 8 has content from both Sidebar and Main Content
780
+ let start = Types.Position.make(8, 0)
781
+ let results = Grid.scanRight(grid, start, _ => true)
782
+
783
+ // Should scan entire width
784
+ t->expect(Array.length(results))->Expect.toBe(grid.width)
785
+ })
786
+
787
+ test("finds corners in nested card region", t => {
788
+ let grid = Grid.fromLines(complexWireframe)
789
+
790
+ // Define bounds for Card 1 area
791
+ let card1Bounds = Types.Bounds.make(~top=8, ~left=17, ~bottom=11, ~right=37)
792
+ let cornersInCard = Grid.findInRange(grid, Corner, card1Bounds)
793
+
794
+ // Should find 4 corners of Card 1
795
+ t->expect(Array.length(cornersInCard))->Expect.toBe(4)
796
+ })
797
+ })
798
+
799
+ // Summary
800
+ //
801
+ // These integration tests cover:
802
+ // - Simple box creation and scanning (GS-01)
803
+ // - Nested boxes with hierarchy (GS-02)
804
+ // - Divider detection and indexing (GS-03)
805
+ // - Uneven line normalization (GS-04)
806
+ // - Special character recognition (GS-05)
807
+ // - Empty input handling (GS-06)
808
+ // - Large wireframe performance (GS-07)
809
+ // - Complex multi-box wireframe (GS-08)
810
+ //
811
+ // Total test cases: 60+
812
+ // Coverage target: >=90% for Grid module
813
+ //
814
+ // Test execution:
815
+ // - npm run test -- GridScanner_integration.test.mjs
816
+ // - npm run test:coverage