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.
- package/LICENSE +692 -0
- package/README.md +65 -5
- package/package.json +8 -7
- package/src/index.ts +425 -0
- package/src/renderer/Renderer.gen.tsx +49 -0
- package/src/renderer/Renderer.mjs +41 -1
- package/src/renderer/Renderer.res +78 -0
- package/src/test/Expect.mjs +9 -0
- package/src/parser/Core/__tests__/Bounds_test.mjs +0 -326
- package/src/parser/Core/__tests__/Bounds_test.res +0 -412
- package/src/parser/Core/__tests__/Grid_test.mjs +0 -322
- package/src/parser/Core/__tests__/Grid_test.res +0 -319
- package/src/parser/Core/__tests__/Types_test.mjs +0 -614
- package/src/parser/Core/__tests__/Types_test.res +0 -650
- package/src/parser/Detector/__tests__/BoxTracer_test.mjs +0 -70
- package/src/parser/Detector/__tests__/BoxTracer_test.res +0 -92
- package/src/parser/Detector/__tests__/HierarchyBuilder_test.mjs +0 -489
- package/src/parser/Detector/__tests__/HierarchyBuilder_test.res +0 -849
- package/src/parser/Detector/__tests__/ShapeDetector_test.mjs +0 -377
- package/src/parser/Detector/__tests__/ShapeDetector_test.res +0 -563
- package/src/parser/Interactions/__tests__/InteractionMerger_test.mjs +0 -576
- package/src/parser/Interactions/__tests__/InteractionMerger_test.res +0 -646
- package/src/parser/Scanner/__tests__/Grid_manual.mjs +0 -214
- package/src/parser/Scanner/__tests__/Grid_manual.res +0 -141
- package/src/parser/Semantic/Elements/__tests__/ButtonParser_test.mjs +0 -189
- package/src/parser/Semantic/Elements/__tests__/ButtonParser_test.res +0 -257
- package/src/parser/Semantic/Elements/__tests__/CheckboxParser_test.mjs +0 -202
- package/src/parser/Semantic/Elements/__tests__/CheckboxParser_test.res +0 -250
- package/src/parser/Semantic/Elements/__tests__/CodeTextParser_manual.mjs +0 -293
- package/src/parser/Semantic/Elements/__tests__/CodeTextParser_manual.res +0 -134
- package/src/parser/Semantic/Elements/__tests__/InputParser_test.mjs +0 -253
- package/src/parser/Semantic/Elements/__tests__/InputParser_test.res +0 -304
- package/src/parser/Semantic/Elements/__tests__/LinkParser_test.mjs +0 -289
- package/src/parser/Semantic/Elements/__tests__/LinkParser_test.res +0 -402
- package/src/parser/Semantic/Elements/__tests__/TextParser_test.mjs +0 -149
- package/src/parser/Semantic/Elements/__tests__/TextParser_test.res +0 -167
- package/src/parser/Semantic/__tests__/ASTBuilder_test.mjs +0 -187
- package/src/parser/Semantic/__tests__/ASTBuilder_test.res +0 -192
- package/src/parser/Semantic/__tests__/ParserRegistry_test.mjs +0 -154
- package/src/parser/Semantic/__tests__/ParserRegistry_test.res +0 -191
- package/src/parser/Semantic/__tests__/SemanticParser_integration_test.mjs +0 -768
- package/src/parser/Semantic/__tests__/SemanticParser_integration_test.res +0 -1069
- package/src/parser/Semantic/__tests__/SemanticParser_manual.mjs +0 -1329
- package/src/parser/Semantic/__tests__/SemanticParser_manual.res +0 -544
- package/src/parser/__tests__/GridScanner_integration.test.mjs +0 -632
- package/src/parser/__tests__/GridScanner_integration.test.res +0 -816
- package/src/parser/__tests__/Performance.test.mjs +0 -244
- package/src/parser/__tests__/Performance.test.res +0 -371
- package/src/parser/__tests__/PerformanceFixtures.mjs +0 -200
- package/src/parser/__tests__/PerformanceFixtures.res +0 -284
- package/src/parser/__tests__/WyreframeParser_integration.test.mjs +0 -770
- package/src/parser/__tests__/WyreframeParser_integration.test.res +0 -1008
- package/src/parser/__tests__/fixtures/alignment-test.txt +0 -9
- package/src/parser/__tests__/fixtures/all-elements.txt +0 -16
- package/src/parser/__tests__/fixtures/login-scene.txt +0 -17
- package/src/parser/__tests__/fixtures/multi-scene.txt +0 -25
- package/src/parser/__tests__/fixtures/nested-boxes.txt +0 -15
- package/src/parser/__tests__/fixtures/simple-box.txt +0 -5
- package/src/parser/__tests__/fixtures/with-dividers.txt +0 -14
|
@@ -1,816 +0,0 @@
|
|
|
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
|