wyreframe 0.1.0 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/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,646 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* InteractionMerger_test.res
|
|
3
|
-
*
|
|
4
|
-
* Unit tests for the InteractionMerger module.
|
|
5
|
-
* Tests validation, error detection, and successful merging of interactions.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
open Vitest
|
|
9
|
-
open Types
|
|
10
|
-
open InteractionMerger
|
|
11
|
-
|
|
12
|
-
describe("InteractionMerger", t => {
|
|
13
|
-
// ============================================================================
|
|
14
|
-
// Test Fixtures
|
|
15
|
-
// ============================================================================
|
|
16
|
-
|
|
17
|
-
let makePosition = (row, col) => Position.make(row, col)
|
|
18
|
-
let makeBounds = (top, left, bottom, right) => Bounds.make(~top, ~left, ~bottom, ~right)
|
|
19
|
-
|
|
20
|
-
// Simple scene with button and input
|
|
21
|
-
let simpleScene: scene = {
|
|
22
|
-
id: "login",
|
|
23
|
-
title: "Login",
|
|
24
|
-
transition: "fade",
|
|
25
|
-
device: Desktop,
|
|
26
|
-
elements: [
|
|
27
|
-
Button({
|
|
28
|
-
id: "submit-btn",
|
|
29
|
-
text: "Submit",
|
|
30
|
-
position: makePosition(5, 10),
|
|
31
|
-
align: Center,
|
|
32
|
-
actions: [],
|
|
33
|
-
}),
|
|
34
|
-
Input({
|
|
35
|
-
id: "email",
|
|
36
|
-
placeholder: Some("Email"),
|
|
37
|
-
position: makePosition(3, 10),
|
|
38
|
-
}),
|
|
39
|
-
],
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Scene with nested boxes
|
|
43
|
-
let nestedScene: scene = {
|
|
44
|
-
id: "dashboard",
|
|
45
|
-
title: "Dashboard",
|
|
46
|
-
transition: "slide",
|
|
47
|
-
device: Desktop,
|
|
48
|
-
elements: [
|
|
49
|
-
Box({
|
|
50
|
-
name: Some("Container"),
|
|
51
|
-
bounds: makeBounds(0, 0, 10, 20),
|
|
52
|
-
children: [
|
|
53
|
-
Button({
|
|
54
|
-
id: "action-btn",
|
|
55
|
-
text: "Action",
|
|
56
|
-
position: makePosition(2, 5),
|
|
57
|
-
align: Left,
|
|
58
|
-
actions: [],
|
|
59
|
-
}),
|
|
60
|
-
Section({
|
|
61
|
-
name: "settings",
|
|
62
|
-
children: [
|
|
63
|
-
Link({
|
|
64
|
-
id: "settings-link",
|
|
65
|
-
text: "Settings",
|
|
66
|
-
position: makePosition(4, 5),
|
|
67
|
-
align: Left,
|
|
68
|
-
actions: [],
|
|
69
|
-
}),
|
|
70
|
-
],
|
|
71
|
-
}),
|
|
72
|
-
],
|
|
73
|
-
}),
|
|
74
|
-
],
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Valid interactions for simple scene
|
|
78
|
-
let validInteractions: array<sceneInteractions> = [
|
|
79
|
-
{
|
|
80
|
-
sceneId: "login",
|
|
81
|
-
interactions: [
|
|
82
|
-
{
|
|
83
|
-
elementId: "submit-btn",
|
|
84
|
-
properties: Js.Dict.fromArray([("variant", Js.Json.string("primary"))]),
|
|
85
|
-
actions: [
|
|
86
|
-
Goto({
|
|
87
|
-
target: "dashboard",
|
|
88
|
-
transition: "slide",
|
|
89
|
-
condition: None,
|
|
90
|
-
}),
|
|
91
|
-
],
|
|
92
|
-
},
|
|
93
|
-
{
|
|
94
|
-
elementId: "email",
|
|
95
|
-
properties: Dict.fromArray([("required", JSON.Encode.bool(true))]),
|
|
96
|
-
actions: [],
|
|
97
|
-
},
|
|
98
|
-
],
|
|
99
|
-
},
|
|
100
|
-
]
|
|
101
|
-
|
|
102
|
-
// ============================================================================
|
|
103
|
-
// Element ID Collection Tests
|
|
104
|
-
// ============================================================================
|
|
105
|
-
|
|
106
|
-
describe("collectElementIds", t => {
|
|
107
|
-
test("collects ID from Button element", t => {
|
|
108
|
-
let button = Button({
|
|
109
|
-
id: "test-btn",
|
|
110
|
-
text: "Test",
|
|
111
|
-
position: makePosition(0, 0),
|
|
112
|
-
align: Left,
|
|
113
|
-
actions: [],
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
let ids = collectElementIds(button)
|
|
117
|
-
|
|
118
|
-
t->expect(ids->Belt.Set.String.has("test-btn"))->Expect.toBe(true)
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
test("collects ID from Input element", t => {
|
|
122
|
-
let input = Input({
|
|
123
|
-
id: "test-input",
|
|
124
|
-
placeholder: None,
|
|
125
|
-
position: makePosition(0, 0),
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
let ids = collectElementIds(input)
|
|
129
|
-
|
|
130
|
-
t->expect(ids->Belt.Set.String.has("test-input"))->Expect.toBe(true)
|
|
131
|
-
})
|
|
132
|
-
|
|
133
|
-
test("collects ID from Link element", t => {
|
|
134
|
-
let link = Link({
|
|
135
|
-
id: "test-link",
|
|
136
|
-
text: "Test Link",
|
|
137
|
-
position: makePosition(0, 0),
|
|
138
|
-
align: Left,
|
|
139
|
-
actions: [],
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
let ids = collectElementIds(link)
|
|
143
|
-
|
|
144
|
-
t->expect(ids->Belt.Set.String.has("test-link"))->Expect.toBe(true)
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
test("collects name from Section element", t => {
|
|
148
|
-
let section = Section({
|
|
149
|
-
name: "test-section",
|
|
150
|
-
children: [],
|
|
151
|
-
})
|
|
152
|
-
|
|
153
|
-
let ids = collectElementIds(section)
|
|
154
|
-
|
|
155
|
-
t->expect(ids->Belt.Set.String.has("test-section"))->Expect.toBe(true)
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
test("returns empty set for elements without IDs", t => {
|
|
159
|
-
let checkbox = Checkbox({
|
|
160
|
-
checked: true,
|
|
161
|
-
label: "Test",
|
|
162
|
-
position: makePosition(0, 0),
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
let ids = collectElementIds(checkbox)
|
|
166
|
-
|
|
167
|
-
t->expect(ids->Belt.Set.String.size)->Expect.toBe(0)
|
|
168
|
-
})
|
|
169
|
-
|
|
170
|
-
test("recursively collects IDs from Box children", t => {
|
|
171
|
-
let box = Box({
|
|
172
|
-
name: Some("Container"),
|
|
173
|
-
bounds: makeBounds(0, 0, 10, 10),
|
|
174
|
-
children: [
|
|
175
|
-
Button({
|
|
176
|
-
id: "btn1",
|
|
177
|
-
text: "Button 1",
|
|
178
|
-
position: makePosition(2, 2),
|
|
179
|
-
align: Left,
|
|
180
|
-
actions: [],
|
|
181
|
-
}),
|
|
182
|
-
Button({
|
|
183
|
-
id: "btn2",
|
|
184
|
-
text: "Button 2",
|
|
185
|
-
position: makePosition(4, 2),
|
|
186
|
-
align: Left,
|
|
187
|
-
actions: [],
|
|
188
|
-
}),
|
|
189
|
-
],
|
|
190
|
-
})
|
|
191
|
-
|
|
192
|
-
let ids = collectElementIds(box)
|
|
193
|
-
|
|
194
|
-
t->expect(ids->Belt.Set.String.has("btn1"))->Expect.toBe(true)
|
|
195
|
-
t->expect(ids->Belt.Set.String.has("btn2"))->Expect.toBe(true)
|
|
196
|
-
t->expect(ids->Belt.Set.String.size)->Expect.toBe(2)
|
|
197
|
-
})
|
|
198
|
-
|
|
199
|
-
test("recursively collects IDs from nested Boxes", t => {
|
|
200
|
-
let box = Box({
|
|
201
|
-
name: Some("Outer"),
|
|
202
|
-
bounds: makeBounds(0, 0, 20, 20),
|
|
203
|
-
children: [
|
|
204
|
-
Box({
|
|
205
|
-
name: Some("Inner"),
|
|
206
|
-
bounds: makeBounds(2, 2, 18, 18),
|
|
207
|
-
children: [
|
|
208
|
-
Input({
|
|
209
|
-
id: "nested-input",
|
|
210
|
-
placeholder: None,
|
|
211
|
-
position: makePosition(5, 5),
|
|
212
|
-
}),
|
|
213
|
-
],
|
|
214
|
-
}),
|
|
215
|
-
],
|
|
216
|
-
})
|
|
217
|
-
|
|
218
|
-
let ids = collectElementIds(box)
|
|
219
|
-
|
|
220
|
-
t->expect(ids->Belt.Set.String.has("nested-input"))->Expect.toBe(true)
|
|
221
|
-
})
|
|
222
|
-
|
|
223
|
-
test("collects IDs from Row children", t => {
|
|
224
|
-
let row = Row({
|
|
225
|
-
children: [
|
|
226
|
-
Button({
|
|
227
|
-
id: "row-btn1",
|
|
228
|
-
text: "Button 1",
|
|
229
|
-
position: makePosition(0, 0),
|
|
230
|
-
align: Left,
|
|
231
|
-
actions: [],
|
|
232
|
-
}),
|
|
233
|
-
Button({
|
|
234
|
-
id: "row-btn2",
|
|
235
|
-
text: "Button 2",
|
|
236
|
-
position: makePosition(0, 10),
|
|
237
|
-
align: Left,
|
|
238
|
-
actions: [],
|
|
239
|
-
}),
|
|
240
|
-
],
|
|
241
|
-
align: Center,
|
|
242
|
-
})
|
|
243
|
-
|
|
244
|
-
let ids = collectElementIds(row)
|
|
245
|
-
|
|
246
|
-
t->expect(ids->Belt.Set.String.has("row-btn1"))->Expect.toBe(true)
|
|
247
|
-
t->expect(ids->Belt.Set.String.has("row-btn2"))->Expect.toBe(true)
|
|
248
|
-
})
|
|
249
|
-
|
|
250
|
-
test("collects IDs from Section and its children", t => {
|
|
251
|
-
let section = Section({
|
|
252
|
-
name: "my-section",
|
|
253
|
-
children: [
|
|
254
|
-
Link({
|
|
255
|
-
id: "section-link",
|
|
256
|
-
text: "Link",
|
|
257
|
-
position: makePosition(0, 0),
|
|
258
|
-
align: Left,
|
|
259
|
-
actions: [],
|
|
260
|
-
}),
|
|
261
|
-
],
|
|
262
|
-
})
|
|
263
|
-
|
|
264
|
-
let ids = collectElementIds(section)
|
|
265
|
-
|
|
266
|
-
t->expect(ids->Belt.Set.String.has("my-section"))->Expect.toBe(true)
|
|
267
|
-
t->expect(ids->Belt.Set.String.has("section-link"))->Expect.toBe(true)
|
|
268
|
-
})
|
|
269
|
-
})
|
|
270
|
-
|
|
271
|
-
describe("collectSceneElementIds", t => {
|
|
272
|
-
test("collects all element IDs from scene", t => {
|
|
273
|
-
let ids = collectSceneElementIds(simpleScene)
|
|
274
|
-
|
|
275
|
-
t->expect(ids->Belt.Set.String.has("submit-btn"))->Expect.toBe(true)
|
|
276
|
-
t->expect(ids->Belt.Set.String.has("email"))->Expect.toBe(true)
|
|
277
|
-
t->expect(ids->Belt.Set.String.size)->Expect.toBe(2)
|
|
278
|
-
})
|
|
279
|
-
|
|
280
|
-
test("collects IDs from nested elements in scene", t => {
|
|
281
|
-
let ids = collectSceneElementIds(nestedScene)
|
|
282
|
-
|
|
283
|
-
t->expect(ids->Belt.Set.String.has("action-btn"))->Expect.toBe(true)
|
|
284
|
-
t->expect(ids->Belt.Set.String.has("settings"))->Expect.toBe(true)
|
|
285
|
-
t->expect(ids->Belt.Set.String.has("settings-link"))->Expect.toBe(true)
|
|
286
|
-
t->expect(ids->Belt.Set.String.size)->Expect.toBe(3)
|
|
287
|
-
})
|
|
288
|
-
})
|
|
289
|
-
|
|
290
|
-
describe("buildSceneElementMap", t => {
|
|
291
|
-
test("builds map of scene IDs to element IDs", t => {
|
|
292
|
-
let ast: ast = {
|
|
293
|
-
scenes: [simpleScene, nestedScene],
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
let sceneMap = buildSceneElementMap(ast)
|
|
297
|
-
|
|
298
|
-
t->expect(sceneMap->Belt.Map.String.size)->Expect.toBe(2)
|
|
299
|
-
|
|
300
|
-
let loginIds = sceneMap->Belt.Map.String.get("login")
|
|
301
|
-
t->expect(loginIds->Option.isSome)->Expect.toBe(true)
|
|
302
|
-
loginIds->Option.forEach(ids => {
|
|
303
|
-
t->expect(ids->Belt.Set.String.has("submit-btn"))->Expect.toBe(true)
|
|
304
|
-
t->expect(ids->Belt.Set.String.has("email"))->Expect.toBe(true)
|
|
305
|
-
})
|
|
306
|
-
|
|
307
|
-
let dashboardIds = sceneMap->Belt.Map.String.get("dashboard")
|
|
308
|
-
t->expect(dashboardIds->Option.isSome)->Expect.toBe(true)
|
|
309
|
-
dashboardIds->Option.forEach(ids => {
|
|
310
|
-
t->expect(ids->Belt.Set.String.has("action-btn"))->Expect.toBe(true)
|
|
311
|
-
t->expect(ids->Belt.Set.String.has("settings-link"))->Expect.toBe(true)
|
|
312
|
-
})
|
|
313
|
-
})
|
|
314
|
-
})
|
|
315
|
-
|
|
316
|
-
// ============================================================================
|
|
317
|
-
// Validation Tests
|
|
318
|
-
// ============================================================================
|
|
319
|
-
|
|
320
|
-
describe("validateInteractions", t => {
|
|
321
|
-
test("returns no errors for valid interactions", t => {
|
|
322
|
-
let ast: ast = {scenes: [simpleScene]}
|
|
323
|
-
let sceneMap = buildSceneElementMap(ast)
|
|
324
|
-
|
|
325
|
-
let errors = validateInteractions(validInteractions, sceneMap)
|
|
326
|
-
|
|
327
|
-
t->expect(errors->Array.length)->Expect.toBe(0)
|
|
328
|
-
})
|
|
329
|
-
|
|
330
|
-
test("detects missing element", t => {
|
|
331
|
-
let ast: ast = {scenes: [simpleScene]}
|
|
332
|
-
let sceneMap = buildSceneElementMap(ast)
|
|
333
|
-
|
|
334
|
-
let invalidInteractions = [
|
|
335
|
-
{
|
|
336
|
-
sceneId: "login",
|
|
337
|
-
interactions: [
|
|
338
|
-
{
|
|
339
|
-
elementId: "nonexistent-btn",
|
|
340
|
-
properties: Js.Dict.empty(),
|
|
341
|
-
actions: [],
|
|
342
|
-
},
|
|
343
|
-
],
|
|
344
|
-
},
|
|
345
|
-
]
|
|
346
|
-
|
|
347
|
-
let errors = validateInteractions(invalidInteractions, sceneMap)
|
|
348
|
-
|
|
349
|
-
t->expect(errors->Array.length)->Expect.toBe(1)
|
|
350
|
-
switch errors[0] {
|
|
351
|
-
| Some(ElementNotFound({elementId})) => t->expect(elementId)->Expect.toBe("nonexistent-btn")
|
|
352
|
-
| _ => t->expect(true)->Expect.toBe(false) // fail: Expected ElementNotFound error
|
|
353
|
-
}
|
|
354
|
-
})
|
|
355
|
-
|
|
356
|
-
test("detects missing scene", t => {
|
|
357
|
-
let ast: ast = {scenes: [simpleScene]}
|
|
358
|
-
let sceneMap = buildSceneElementMap(ast)
|
|
359
|
-
|
|
360
|
-
let invalidInteractions = [
|
|
361
|
-
{
|
|
362
|
-
sceneId: "nonexistent-scene",
|
|
363
|
-
interactions: [
|
|
364
|
-
{
|
|
365
|
-
elementId: "some-btn",
|
|
366
|
-
properties: Js.Dict.empty(),
|
|
367
|
-
actions: [],
|
|
368
|
-
},
|
|
369
|
-
],
|
|
370
|
-
},
|
|
371
|
-
]
|
|
372
|
-
|
|
373
|
-
let errors = validateInteractions(invalidInteractions, sceneMap)
|
|
374
|
-
|
|
375
|
-
t->expect(errors->Array.length)->Expect.toBe(1)
|
|
376
|
-
switch errors[0] {
|
|
377
|
-
| Some(SceneNotFound({sceneId})) => t->expect(sceneId)->Expect.toBe("nonexistent-scene")
|
|
378
|
-
| _ => t->expect(true)->Expect.toBe(false) // fail: Expected SceneNotFound error
|
|
379
|
-
}
|
|
380
|
-
})
|
|
381
|
-
|
|
382
|
-
test("detects duplicate interaction for same element", t => {
|
|
383
|
-
let ast: ast = {scenes: [simpleScene]}
|
|
384
|
-
let sceneMap = buildSceneElementMap(ast)
|
|
385
|
-
|
|
386
|
-
let duplicateInteractions = [
|
|
387
|
-
{
|
|
388
|
-
sceneId: "login",
|
|
389
|
-
interactions: [
|
|
390
|
-
{
|
|
391
|
-
elementId: "submit-btn",
|
|
392
|
-
properties: Js.Dict.empty(),
|
|
393
|
-
actions: [],
|
|
394
|
-
},
|
|
395
|
-
{
|
|
396
|
-
elementId: "submit-btn",
|
|
397
|
-
properties: Js.Dict.empty(),
|
|
398
|
-
actions: [],
|
|
399
|
-
},
|
|
400
|
-
],
|
|
401
|
-
},
|
|
402
|
-
]
|
|
403
|
-
|
|
404
|
-
let errors = validateInteractions(duplicateInteractions, sceneMap)
|
|
405
|
-
|
|
406
|
-
t->expect(errors->Array.length)->Expect.toBe(1)
|
|
407
|
-
switch errors[0] {
|
|
408
|
-
| Some(DuplicateInteraction({elementId})) => t->expect(elementId)->Expect.toBe("submit-btn")
|
|
409
|
-
| _ => t->expect(true)->Expect.toBe(false) // fail: Expected DuplicateInteraction error
|
|
410
|
-
}
|
|
411
|
-
})
|
|
412
|
-
|
|
413
|
-
test("reports multiple errors", t => {
|
|
414
|
-
let ast: ast = {scenes: [simpleScene]}
|
|
415
|
-
let sceneMap = buildSceneElementMap(ast)
|
|
416
|
-
|
|
417
|
-
let multiErrorInteractions = [
|
|
418
|
-
{
|
|
419
|
-
sceneId: "login",
|
|
420
|
-
interactions: [
|
|
421
|
-
{
|
|
422
|
-
elementId: "nonexistent1",
|
|
423
|
-
properties: Js.Dict.empty(),
|
|
424
|
-
actions: [],
|
|
425
|
-
},
|
|
426
|
-
{
|
|
427
|
-
elementId: "nonexistent2",
|
|
428
|
-
properties: Js.Dict.empty(),
|
|
429
|
-
actions: [],
|
|
430
|
-
},
|
|
431
|
-
],
|
|
432
|
-
},
|
|
433
|
-
]
|
|
434
|
-
|
|
435
|
-
let errors = validateInteractions(multiErrorInteractions, sceneMap)
|
|
436
|
-
|
|
437
|
-
t->expect(errors->Array.length)->Expect.toBe(2)
|
|
438
|
-
})
|
|
439
|
-
})
|
|
440
|
-
|
|
441
|
-
// ============================================================================
|
|
442
|
-
// Merge Function Tests
|
|
443
|
-
// ============================================================================
|
|
444
|
-
|
|
445
|
-
describe("mergeInteractions", t => {
|
|
446
|
-
test("successfully merges valid interactions", t => {
|
|
447
|
-
let ast: ast = {scenes: [simpleScene]}
|
|
448
|
-
|
|
449
|
-
let result = mergeInteractions(ast, validInteractions)
|
|
450
|
-
|
|
451
|
-
t->expect(result->Result.isOk)->Expect.toBe(true)
|
|
452
|
-
result->Result.forEach(mergedAst => {
|
|
453
|
-
t->expect(mergedAst.scenes->Array.length)->Expect.toBe(1)
|
|
454
|
-
})
|
|
455
|
-
})
|
|
456
|
-
|
|
457
|
-
test("returns error for invalid interactions (hard errors)", t => {
|
|
458
|
-
let ast: ast = {scenes: [simpleScene]}
|
|
459
|
-
|
|
460
|
-
// SceneNotFound is a hard error (unlike ElementNotFound which is soft)
|
|
461
|
-
let invalidInteractions = [
|
|
462
|
-
{
|
|
463
|
-
sceneId: "nonexistent-scene", // This scene doesn't exist
|
|
464
|
-
interactions: [
|
|
465
|
-
{
|
|
466
|
-
elementId: "some-btn",
|
|
467
|
-
properties: Js.Dict.empty(),
|
|
468
|
-
actions: [],
|
|
469
|
-
},
|
|
470
|
-
],
|
|
471
|
-
},
|
|
472
|
-
]
|
|
473
|
-
|
|
474
|
-
let result = mergeInteractions(ast, invalidInteractions)
|
|
475
|
-
|
|
476
|
-
t->expect(result->Result.isError)->Expect.toBe(true)
|
|
477
|
-
switch result {
|
|
478
|
-
| Error(errors) => t->expect(errors->Array.length)->Expect.Int.toBeGreaterThan(0)
|
|
479
|
-
| Ok(_) => t->expect(true)->Expect.toBe(false) // fail: Expected error result
|
|
480
|
-
}
|
|
481
|
-
})
|
|
482
|
-
|
|
483
|
-
test("preserves scene structure after merge", t => {
|
|
484
|
-
let ast: ast = {scenes: [simpleScene]}
|
|
485
|
-
|
|
486
|
-
let result = mergeInteractions(ast, validInteractions)
|
|
487
|
-
|
|
488
|
-
result->Result.forEach(mergedAst => {
|
|
489
|
-
t->expect(mergedAst.scenes[0]->Option.map(s => s.id))->Expect.toEqual(Some("login"))
|
|
490
|
-
t->expect(mergedAst.scenes[0]->Option.map(s => s.title))->Expect.toEqual(Some("Login"))
|
|
491
|
-
t->expect(mergedAst.scenes[0]->Option.map(s => s.elements->Array.length))->Expect.toEqual(Some(2))
|
|
492
|
-
})
|
|
493
|
-
})
|
|
494
|
-
|
|
495
|
-
test("handles multiple scenes with interactions", t => {
|
|
496
|
-
let ast: ast = {scenes: [simpleScene, nestedScene]}
|
|
497
|
-
|
|
498
|
-
let multiSceneInteractions = [
|
|
499
|
-
{
|
|
500
|
-
sceneId: "login",
|
|
501
|
-
interactions: [
|
|
502
|
-
{
|
|
503
|
-
elementId: "submit-btn",
|
|
504
|
-
properties: Js.Dict.empty(),
|
|
505
|
-
actions: [],
|
|
506
|
-
},
|
|
507
|
-
],
|
|
508
|
-
},
|
|
509
|
-
{
|
|
510
|
-
sceneId: "dashboard",
|
|
511
|
-
interactions: [
|
|
512
|
-
{
|
|
513
|
-
elementId: "action-btn",
|
|
514
|
-
properties: Js.Dict.empty(),
|
|
515
|
-
actions: [],
|
|
516
|
-
},
|
|
517
|
-
],
|
|
518
|
-
},
|
|
519
|
-
]
|
|
520
|
-
|
|
521
|
-
let result = mergeInteractions(ast, multiSceneInteractions)
|
|
522
|
-
|
|
523
|
-
t->expect(result->Result.isOk)->Expect.toBe(true)
|
|
524
|
-
result->Result.forEach(mergedAst => {
|
|
525
|
-
t->expect(mergedAst.scenes->Array.length)->Expect.toBe(2)
|
|
526
|
-
})
|
|
527
|
-
})
|
|
528
|
-
|
|
529
|
-
test("merges empty interactions successfully", t => {
|
|
530
|
-
let ast: ast = {scenes: [simpleScene]}
|
|
531
|
-
|
|
532
|
-
let result = mergeInteractions(ast, [])
|
|
533
|
-
|
|
534
|
-
t->expect(result->Result.isOk)->Expect.toBe(true)
|
|
535
|
-
})
|
|
536
|
-
})
|
|
537
|
-
|
|
538
|
-
// ============================================================================
|
|
539
|
-
// Error Formatting Tests
|
|
540
|
-
// ============================================================================
|
|
541
|
-
|
|
542
|
-
describe("formatError", t => {
|
|
543
|
-
test("formats ElementNotFound error", t => {
|
|
544
|
-
let error = ElementNotFound({
|
|
545
|
-
sceneId: "login",
|
|
546
|
-
elementId: "missing-btn",
|
|
547
|
-
position: None,
|
|
548
|
-
})
|
|
549
|
-
|
|
550
|
-
let message = formatError(error)
|
|
551
|
-
|
|
552
|
-
t->expect(message)->Expect.String.toContain("missing-btn")
|
|
553
|
-
t->expect(message)->Expect.String.toContain("login")
|
|
554
|
-
})
|
|
555
|
-
|
|
556
|
-
test("formats DuplicateInteraction error", t => {
|
|
557
|
-
let error = DuplicateInteraction({
|
|
558
|
-
sceneId: "login",
|
|
559
|
-
elementId: "duplicate-btn",
|
|
560
|
-
})
|
|
561
|
-
|
|
562
|
-
let message = formatError(error)
|
|
563
|
-
|
|
564
|
-
t->expect(message)->Expect.String.toContain("duplicate-btn")
|
|
565
|
-
t->expect(message)->Expect.String.toContain("Duplicate")
|
|
566
|
-
})
|
|
567
|
-
|
|
568
|
-
test("formats SceneNotFound error", t => {
|
|
569
|
-
let error = SceneNotFound({sceneId: "missing-scene"})
|
|
570
|
-
|
|
571
|
-
let message = formatError(error)
|
|
572
|
-
|
|
573
|
-
t->expect(message)->Expect.String.toContain("missing-scene")
|
|
574
|
-
t->expect(message)->Expect.String.toContain("not found")
|
|
575
|
-
})
|
|
576
|
-
})
|
|
577
|
-
|
|
578
|
-
describe("formatErrors", t => {
|
|
579
|
-
test("formats multiple errors with newlines", t => {
|
|
580
|
-
let errors = [
|
|
581
|
-
ElementNotFound({
|
|
582
|
-
sceneId: "login",
|
|
583
|
-
elementId: "btn1",
|
|
584
|
-
position: None,
|
|
585
|
-
}),
|
|
586
|
-
ElementNotFound({
|
|
587
|
-
sceneId: "login",
|
|
588
|
-
elementId: "btn2",
|
|
589
|
-
position: None,
|
|
590
|
-
}),
|
|
591
|
-
]
|
|
592
|
-
|
|
593
|
-
let message = formatErrors(errors)
|
|
594
|
-
|
|
595
|
-
t->expect(message)->Expect.String.toContain("btn1")
|
|
596
|
-
t->expect(message)->Expect.String.toContain("btn2")
|
|
597
|
-
t->expect(message)->Expect.String.toContain("\n")
|
|
598
|
-
})
|
|
599
|
-
})
|
|
600
|
-
|
|
601
|
-
// ============================================================================
|
|
602
|
-
// findInteractionForElement Tests
|
|
603
|
-
// ============================================================================
|
|
604
|
-
|
|
605
|
-
describe("findInteractionForElement", t => {
|
|
606
|
-
test("finds interaction for element", t => {
|
|
607
|
-
let sceneInteractions = Some({
|
|
608
|
-
sceneId: "login",
|
|
609
|
-
interactions: [
|
|
610
|
-
{
|
|
611
|
-
elementId: "test-btn",
|
|
612
|
-
properties: Js.Dict.empty(),
|
|
613
|
-
actions: [],
|
|
614
|
-
},
|
|
615
|
-
],
|
|
616
|
-
})
|
|
617
|
-
|
|
618
|
-
let result = findInteractionForElement("test-btn", sceneInteractions)
|
|
619
|
-
|
|
620
|
-
t->expect(result->Option.isSome)->Expect.toBe(true)
|
|
621
|
-
})
|
|
622
|
-
|
|
623
|
-
test("returns None when element not found", t => {
|
|
624
|
-
let sceneInteractions = Some({
|
|
625
|
-
sceneId: "login",
|
|
626
|
-
interactions: [
|
|
627
|
-
{
|
|
628
|
-
elementId: "test-btn",
|
|
629
|
-
properties: Js.Dict.empty(),
|
|
630
|
-
actions: [],
|
|
631
|
-
},
|
|
632
|
-
],
|
|
633
|
-
})
|
|
634
|
-
|
|
635
|
-
let result = findInteractionForElement("other-btn", sceneInteractions)
|
|
636
|
-
|
|
637
|
-
t->expect(result->Option.isNone)->Expect.toBe(true)
|
|
638
|
-
})
|
|
639
|
-
|
|
640
|
-
test("returns None when no scene interactions", t => {
|
|
641
|
-
let result = findInteractionForElement("test-btn", None)
|
|
642
|
-
|
|
643
|
-
t->expect(result->Option.isNone)->Expect.toBe(true)
|
|
644
|
-
})
|
|
645
|
-
})
|
|
646
|
-
})
|