@webiny/website-builder-sdk 6.0.0 → 6.1.0-beta.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.
@@ -0,0 +1,1456 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { evaluateConstraints, evaluateDeleteConstraint } from "./ConstraintEvaluator.js";
3
+ function makeElement(id, componentName, parent) {
4
+ return {
5
+ type: "Webiny/Element",
6
+ id,
7
+ component: {
8
+ name: componentName
9
+ },
10
+ parent
11
+ };
12
+ }
13
+ function makeManifest(name, overrides) {
14
+ return {
15
+ name,
16
+ label: name,
17
+ inputs: [],
18
+ tags: [],
19
+ ...overrides
20
+ };
21
+ }
22
+ function makeDocument(elements, bindings = {}) {
23
+ const elementMap = {};
24
+ for (const el of elements) {
25
+ elementMap[el.id] = el;
26
+ }
27
+ return {
28
+ id: "doc-1",
29
+ state: {},
30
+ version: 1,
31
+ properties: {},
32
+ metadata: {},
33
+ extensions: {},
34
+ bindings,
35
+ elements: elementMap
36
+ };
37
+ }
38
+ describe("evaluateConstraints", () => {
39
+ it("should allow placement when there are no constraints", () => {
40
+ const parent = makeElement("parent-1", "Container");
41
+ const doc = makeDocument([parent]);
42
+ const components = {
43
+ Container: makeManifest("Container"),
44
+ Button: makeManifest("Button")
45
+ };
46
+ const result = evaluateConstraints({
47
+ componentName: "Button",
48
+ parentId: "parent-1",
49
+ slot: "children",
50
+ document: doc,
51
+ components
52
+ });
53
+ expect(result.allowed).toBe(true);
54
+ expect(result.violation).toBeUndefined();
55
+ });
56
+ it("should block when component's parent constraint fails", () => {
57
+ const parent = makeElement("parent-1", "Container");
58
+ const doc = makeDocument([parent]);
59
+ const components = {
60
+ Container: makeManifest("Container"),
61
+ TabPanel: makeManifest("TabPanel", {
62
+ constraints: [ctx => {
63
+ if (ctx.parent.name !== "Tabs") {
64
+ return ctx.block("TabPanel must be inside Tabs");
65
+ }
66
+ }]
67
+ })
68
+ };
69
+ const result = evaluateConstraints({
70
+ componentName: "TabPanel",
71
+ parentId: "parent-1",
72
+ slot: "children",
73
+ document: doc,
74
+ components
75
+ });
76
+ expect(result.allowed).toBe(false);
77
+ expect(result.violation).toBeDefined();
78
+ expect(result.violation.message).toBe("TabPanel must be inside Tabs");
79
+ });
80
+ it("should allow when parent constraint passes", () => {
81
+ const parent = makeElement("parent-1", "Tabs");
82
+ const doc = makeDocument([parent]);
83
+ const components = {
84
+ Tabs: makeManifest("Tabs"),
85
+ TabPanel: makeManifest("TabPanel", {
86
+ constraints: [ctx => {
87
+ if (ctx.parent.name !== "Tabs") {
88
+ return ctx.block("TabPanel must be inside Tabs");
89
+ }
90
+ }]
91
+ })
92
+ };
93
+ const result = evaluateConstraints({
94
+ componentName: "TabPanel",
95
+ parentId: "parent-1",
96
+ slot: "children",
97
+ document: doc,
98
+ components
99
+ });
100
+ expect(result.allowed).toBe(true);
101
+ expect(result.violation).toBeUndefined();
102
+ });
103
+ it("should block when ancestor constraint fails", () => {
104
+ const root = makeElement("root", "Page");
105
+ const section = makeElement("section-1", "Section", {
106
+ id: "root",
107
+ slot: "children"
108
+ });
109
+ const doc = makeDocument([root, section]);
110
+ const components = {
111
+ Page: makeManifest("Page"),
112
+ Section: makeManifest("Section"),
113
+ ProductPrice: makeManifest("ProductPrice", {
114
+ constraints: [ctx => {
115
+ if (!ctx.isDescendantOf("ProductBox")) {
116
+ return ctx.block("ProductPrice must be inside a ProductBox");
117
+ }
118
+ }]
119
+ })
120
+ };
121
+ const result = evaluateConstraints({
122
+ componentName: "ProductPrice",
123
+ parentId: "section-1",
124
+ slot: "children",
125
+ document: doc,
126
+ components
127
+ });
128
+ expect(result.allowed).toBe(false);
129
+ expect(result.violation.message).toBe("ProductPrice must be inside a ProductBox");
130
+ });
131
+ it("should allow when grandparent matches ancestor constraint", () => {
132
+ const root = makeElement("root", "ProductBox");
133
+ const inner = makeElement("inner-1", "Container", {
134
+ id: "root",
135
+ slot: "children"
136
+ });
137
+ const doc = makeDocument([root, inner]);
138
+ const components = {
139
+ ProductBox: makeManifest("ProductBox"),
140
+ Container: makeManifest("Container"),
141
+ ProductPrice: makeManifest("ProductPrice", {
142
+ constraints: [ctx => {
143
+ if (!ctx.isDescendantOf("ProductBox")) {
144
+ return ctx.block("ProductPrice must be inside a ProductBox");
145
+ }
146
+ }]
147
+ })
148
+ };
149
+ const result = evaluateConstraints({
150
+ componentName: "ProductPrice",
151
+ parentId: "inner-1",
152
+ slot: "children",
153
+ document: doc,
154
+ components
155
+ });
156
+ expect(result.allowed).toBe(true);
157
+ });
158
+ it("should block via slot children limit constraint using slotChildCount", () => {
159
+ const parent = makeElement("parent-1", "Grid");
160
+ const child1 = makeElement("child-1", "Cell", {
161
+ id: "parent-1",
162
+ slot: "children"
163
+ });
164
+ const child2 = makeElement("child-2", "Cell", {
165
+ id: "parent-1",
166
+ slot: "children"
167
+ });
168
+ const doc = makeDocument([parent, child1, child2], {
169
+ "parent-1": {
170
+ inputs: {
171
+ children: {
172
+ id: "inp-1",
173
+ type: "slot",
174
+ static: ["child-1", "child-2"]
175
+ }
176
+ }
177
+ }
178
+ });
179
+ const components = {
180
+ Grid: makeManifest("Grid"),
181
+ Cell: makeManifest("Cell", {
182
+ constraints: [ctx => {
183
+ if (!(ctx.slotChildCount() < 2)) {
184
+ return ctx.block("Maximum 2 children allowed");
185
+ }
186
+ }]
187
+ })
188
+ };
189
+ const result = evaluateConstraints({
190
+ componentName: "Cell",
191
+ parentId: "parent-1",
192
+ slot: "children",
193
+ document: doc,
194
+ components
195
+ });
196
+ expect(result.allowed).toBe(false);
197
+ expect(result.violation.message).toBe("Maximum 2 children allowed");
198
+ });
199
+ it("should block via countInstances when max instances reached", () => {
200
+ const root = makeElement("root", "Page");
201
+ const hero = makeElement("hero-1", "Hero", {
202
+ id: "root",
203
+ slot: "children"
204
+ });
205
+ const doc = makeDocument([root, hero]);
206
+ const components = {
207
+ Page: makeManifest("Page"),
208
+ Hero: makeManifest("Hero", {
209
+ constraints: [ctx => {
210
+ if (!(ctx.countInstances("Hero") < 1)) {
211
+ return ctx.block("Only one Hero per page");
212
+ }
213
+ }]
214
+ })
215
+ };
216
+ const result = evaluateConstraints({
217
+ componentName: "Hero",
218
+ parentId: "root",
219
+ slot: "children",
220
+ document: doc,
221
+ components
222
+ });
223
+ expect(result.allowed).toBe(false);
224
+ expect(result.violation.message).toBe("Only one Hero per page");
225
+ });
226
+ it("should not evaluate parent's constraints against the placed component", () => {
227
+ const parent = makeElement("parent-1", "Tabs");
228
+ const doc = makeDocument([parent]);
229
+ const components = {
230
+ Tabs: makeManifest("Tabs", {
231
+ constraints: [ctx => ctx.block("This should not run for children")]
232
+ }),
233
+ Button: makeManifest("Button")
234
+ };
235
+
236
+ // Parent's constraints should not affect children being dropped into it
237
+ const result = evaluateConstraints({
238
+ componentName: "Button",
239
+ parentId: "parent-1",
240
+ slot: "children",
241
+ document: doc,
242
+ components
243
+ });
244
+ expect(result.allowed).toBe(true);
245
+ });
246
+ it("should short-circuit on the first failed constraint", () => {
247
+ const parent = makeElement("parent-1", "Container");
248
+ const doc = makeDocument([parent]);
249
+ const components = {
250
+ Container: makeManifest("Container"),
251
+ Widget: makeManifest("Widget", {
252
+ constraints: [() => {}, ctx => ctx.block("Always fails"), ctx => ctx.block("Also fails")]
253
+ })
254
+ };
255
+ const result = evaluateConstraints({
256
+ componentName: "Widget",
257
+ parentId: "parent-1",
258
+ slot: "children",
259
+ document: doc,
260
+ components
261
+ });
262
+ expect(result.allowed).toBe(false);
263
+ expect(result.violation.message).toBe("Always fails");
264
+ });
265
+ it("should allow placement when component manifest is not found", () => {
266
+ const parent = makeElement("parent-1", "Container");
267
+ const doc = makeDocument([parent]);
268
+ const components = {
269
+ Container: makeManifest("Container")
270
+ };
271
+ const result = evaluateConstraints({
272
+ componentName: "Unknown",
273
+ parentId: "parent-1",
274
+ slot: "children",
275
+ document: doc,
276
+ components
277
+ });
278
+ expect(result.allowed).toBe(true);
279
+ });
280
+ it("should allow placement when parent element is not found", () => {
281
+ const doc = makeDocument([]);
282
+ const components = {
283
+ Button: makeManifest("Button")
284
+ };
285
+ const result = evaluateConstraints({
286
+ componentName: "Button",
287
+ parentId: "nonexistent",
288
+ slot: "children",
289
+ document: doc,
290
+ components
291
+ });
292
+ expect(result.allowed).toBe(true);
293
+ });
294
+ it("should handle multi-slot parent constraint checking specific slot", () => {
295
+ const parent = makeElement("parent-1", "TwoColumn");
296
+ const child = makeElement("child-1", "Widget", {
297
+ id: "parent-1",
298
+ slot: "leftColumn"
299
+ });
300
+ const doc = makeDocument([parent, child], {
301
+ "parent-1": {
302
+ inputs: {
303
+ leftColumn: {
304
+ id: "inp-left",
305
+ type: "slot",
306
+ static: ["child-1"]
307
+ },
308
+ rightColumn: {
309
+ id: "inp-right",
310
+ type: "slot",
311
+ static: []
312
+ }
313
+ }
314
+ }
315
+ });
316
+ const components = {
317
+ TwoColumn: makeManifest("TwoColumn"),
318
+ Widget: makeManifest("Widget", {
319
+ constraints: [ctx => {
320
+ if (!(ctx.slotChildCount() < 1)) {
321
+ return ctx.block("Slot is full");
322
+ }
323
+ }]
324
+ })
325
+ };
326
+
327
+ // leftColumn is full
328
+ const leftResult = evaluateConstraints({
329
+ componentName: "Widget",
330
+ parentId: "parent-1",
331
+ slot: "leftColumn",
332
+ document: doc,
333
+ components
334
+ });
335
+ expect(leftResult.allowed).toBe(false);
336
+
337
+ // rightColumn is empty
338
+ const rightResult = evaluateConstraints({
339
+ componentName: "Widget",
340
+ parentId: "parent-1",
341
+ slot: "rightColumn",
342
+ document: doc,
343
+ components
344
+ });
345
+ expect(rightResult.allowed).toBe(true);
346
+ });
347
+ it("should provide a default message when constraint has no message", () => {
348
+ const parent = makeElement("parent-1", "Container");
349
+ const doc = makeDocument([parent]);
350
+ const components = {
351
+ Container: makeManifest("Container"),
352
+ Widget: makeManifest("Widget", {
353
+ constraints: [ctx => ctx.block("Blocked")]
354
+ })
355
+ };
356
+ const result = evaluateConstraints({
357
+ componentName: "Widget",
358
+ parentId: "parent-1",
359
+ slot: "children",
360
+ document: doc,
361
+ components
362
+ });
363
+ expect(result.violation.message).toBe("Blocked");
364
+ });
365
+ it("should use thrown error message as the violation message", () => {
366
+ const parent = makeElement("parent-1", "Container");
367
+ const doc = makeDocument([parent]);
368
+ const components = {
369
+ Container: makeManifest("Container"),
370
+ Widget: makeManifest("Widget", {
371
+ constraints: [ctx => {
372
+ throw new Error(`Widget cannot be placed inside ${ctx.parent.name}`);
373
+ }]
374
+ })
375
+ };
376
+ const result = evaluateConstraints({
377
+ componentName: "Widget",
378
+ parentId: "parent-1",
379
+ slot: "children",
380
+ document: doc,
381
+ components
382
+ });
383
+ expect(result.allowed).toBe(false);
384
+ expect(result.violation.message).toBe("Widget cannot be placed inside Container");
385
+ });
386
+ it("should fall back to static message when a non-Error is thrown", () => {
387
+ const parent = makeElement("parent-1", "Container");
388
+ const doc = makeDocument([parent]);
389
+ const components = {
390
+ Container: makeManifest("Container"),
391
+ Widget: makeManifest("Widget", {
392
+ constraints: [() => {
393
+ throw "not an error object";
394
+ }]
395
+ })
396
+ };
397
+ const result = evaluateConstraints({
398
+ componentName: "Widget",
399
+ parentId: "parent-1",
400
+ slot: "children",
401
+ document: doc,
402
+ components
403
+ });
404
+ expect(result.allowed).toBe(false);
405
+ expect(result.violation.message).toBe("Cannot place Widget here.");
406
+ });
407
+ it("should fall back to default message when a non-Error is thrown and no static message", () => {
408
+ const parent = makeElement("parent-1", "Container");
409
+ const doc = makeDocument([parent]);
410
+ const components = {
411
+ Container: makeManifest("Container"),
412
+ Widget: makeManifest("Widget", {
413
+ constraints: [() => {
414
+ throw null;
415
+ }]
416
+ })
417
+ };
418
+ const result = evaluateConstraints({
419
+ componentName: "Widget",
420
+ parentId: "parent-1",
421
+ slot: "children",
422
+ document: doc,
423
+ components
424
+ });
425
+ expect(result.allowed).toBe(false);
426
+ expect(result.violation.message).toBe("Cannot place Widget here.");
427
+ });
428
+ describe("ctx.isChildOf", () => {
429
+ it("should return true when direct parent matches", () => {
430
+ const parent = makeElement("parent-1", "Tabs");
431
+ const doc = makeDocument([parent]);
432
+ const components = {
433
+ Tabs: makeManifest("Tabs"),
434
+ TabPanel: makeManifest("TabPanel", {
435
+ constraints: [ctx => {
436
+ if (!ctx.isChildOf("Tabs")) {
437
+ return ctx.block("TabPanel must be a direct child of Tabs");
438
+ }
439
+ }]
440
+ })
441
+ };
442
+ const result = evaluateConstraints({
443
+ componentName: "TabPanel",
444
+ parentId: "parent-1",
445
+ slot: "children",
446
+ document: doc,
447
+ components
448
+ });
449
+ expect(result.allowed).toBe(true);
450
+ });
451
+ it("should return false when direct parent does not match", () => {
452
+ const parent = makeElement("parent-1", "Container");
453
+ const doc = makeDocument([parent]);
454
+ const components = {
455
+ Container: makeManifest("Container"),
456
+ TabPanel: makeManifest("TabPanel", {
457
+ constraints: [ctx => {
458
+ if (!ctx.isChildOf("Tabs")) {
459
+ return ctx.block("TabPanel must be a direct child of Tabs");
460
+ }
461
+ }]
462
+ })
463
+ };
464
+ const result = evaluateConstraints({
465
+ componentName: "TabPanel",
466
+ parentId: "parent-1",
467
+ slot: "children",
468
+ document: doc,
469
+ components
470
+ });
471
+ expect(result.allowed).toBe(false);
472
+ });
473
+ });
474
+ describe("ctx.isDescendantOf", () => {
475
+ it("should return true when direct parent matches", () => {
476
+ const parent = makeElement("parent-1", "ProductBox");
477
+ const doc = makeDocument([parent]);
478
+ const components = {
479
+ ProductBox: makeManifest("ProductBox"),
480
+ ProductPrice: makeManifest("ProductPrice", {
481
+ constraints: [ctx => {
482
+ if (!ctx.isDescendantOf("ProductBox")) {
483
+ return ctx.block("Blocked");
484
+ }
485
+ }]
486
+ })
487
+ };
488
+ const result = evaluateConstraints({
489
+ componentName: "ProductPrice",
490
+ parentId: "parent-1",
491
+ slot: "children",
492
+ document: doc,
493
+ components
494
+ });
495
+ expect(result.allowed).toBe(true);
496
+ });
497
+ it("should return true when grandparent matches", () => {
498
+ const root = makeElement("root", "ProductBox");
499
+ const inner = makeElement("inner", "Container", {
500
+ id: "root",
501
+ slot: "children"
502
+ });
503
+ const doc = makeDocument([root, inner]);
504
+ const components = {
505
+ ProductBox: makeManifest("ProductBox"),
506
+ Container: makeManifest("Container"),
507
+ ProductPrice: makeManifest("ProductPrice", {
508
+ constraints: [ctx => {
509
+ if (!ctx.isDescendantOf("ProductBox")) {
510
+ return ctx.block("Blocked");
511
+ }
512
+ }]
513
+ })
514
+ };
515
+ const result = evaluateConstraints({
516
+ componentName: "ProductPrice",
517
+ parentId: "inner",
518
+ slot: "children",
519
+ document: doc,
520
+ components
521
+ });
522
+ expect(result.allowed).toBe(true);
523
+ });
524
+ it("should return false when no ancestor matches", () => {
525
+ const root = makeElement("root", "Page");
526
+ const inner = makeElement("inner", "Section", {
527
+ id: "root",
528
+ slot: "children"
529
+ });
530
+ const doc = makeDocument([root, inner]);
531
+ const components = {
532
+ Page: makeManifest("Page"),
533
+ Section: makeManifest("Section"),
534
+ ProductPrice: makeManifest("ProductPrice", {
535
+ constraints: [ctx => {
536
+ if (!ctx.isDescendantOf("ProductBox")) {
537
+ return ctx.block("Blocked");
538
+ }
539
+ }]
540
+ })
541
+ };
542
+ const result = evaluateConstraints({
543
+ componentName: "ProductPrice",
544
+ parentId: "inner",
545
+ slot: "children",
546
+ document: doc,
547
+ components
548
+ });
549
+ expect(result.allowed).toBe(false);
550
+ });
551
+ });
552
+ describe("ctx.slotChildCount", () => {
553
+ it("should return the number of children in the target slot", () => {
554
+ const parent = makeElement("parent-1", "Grid");
555
+ const doc = makeDocument([parent], {
556
+ "parent-1": {
557
+ inputs: {
558
+ children: {
559
+ id: "inp-1",
560
+ type: "slot",
561
+ static: ["child-1", "child-2", "child-3"]
562
+ }
563
+ }
564
+ }
565
+ });
566
+ const components = {
567
+ Grid: makeManifest("Grid"),
568
+ Cell: makeManifest("Cell", {
569
+ constraints: [ctx => {
570
+ if (!(ctx.slotChildCount() < 3)) {
571
+ return ctx.block("Max 3 children");
572
+ }
573
+ }]
574
+ })
575
+ };
576
+ const result = evaluateConstraints({
577
+ componentName: "Cell",
578
+ parentId: "parent-1",
579
+ slot: "children",
580
+ document: doc,
581
+ components
582
+ });
583
+ expect(result.allowed).toBe(false);
584
+ });
585
+ it("should return 0 when slot has no bindings", () => {
586
+ const parent = makeElement("parent-1", "Container");
587
+ const doc = makeDocument([parent]);
588
+ const components = {
589
+ Container: makeManifest("Container"),
590
+ Widget: makeManifest("Widget", {
591
+ constraints: [ctx => {
592
+ if (!(ctx.slotChildCount() < 5)) {
593
+ return ctx.block("Blocked");
594
+ }
595
+ }]
596
+ })
597
+ };
598
+ const result = evaluateConstraints({
599
+ componentName: "Widget",
600
+ parentId: "parent-1",
601
+ slot: "children",
602
+ document: doc,
603
+ components
604
+ });
605
+ expect(result.allowed).toBe(true);
606
+ });
607
+ });
608
+ describe("ctx.hasTag", () => {
609
+ it("should return true when the placed component has the tag", () => {
610
+ const parent = makeElement("parent-1", "Container");
611
+ const doc = makeDocument([parent]);
612
+ const components = {
613
+ Container: makeManifest("Container"),
614
+ FunnelField: makeManifest("FunnelField", {
615
+ tags: ["funnel-field", "input"],
616
+ constraints: [ctx => {
617
+ if (ctx.hasTag("funnel-field")) {
618
+ return ctx.block("Funnel fields not allowed here");
619
+ }
620
+ }]
621
+ })
622
+ };
623
+ const result = evaluateConstraints({
624
+ componentName: "FunnelField",
625
+ parentId: "parent-1",
626
+ slot: "children",
627
+ document: doc,
628
+ components
629
+ });
630
+ expect(result.allowed).toBe(false);
631
+ });
632
+ it("should return false when the placed component does not have the tag", () => {
633
+ const parent = makeElement("parent-1", "Container");
634
+ const doc = makeDocument([parent]);
635
+ const components = {
636
+ Container: makeManifest("Container"),
637
+ Button: makeManifest("Button", {
638
+ constraints: [ctx => {
639
+ if (ctx.hasTag("funnel-field")) {
640
+ return ctx.block("Blocked");
641
+ }
642
+ }]
643
+ })
644
+ };
645
+ const result = evaluateConstraints({
646
+ componentName: "Button",
647
+ parentId: "parent-1",
648
+ slot: "children",
649
+ document: doc,
650
+ components
651
+ });
652
+ expect(result.allowed).toBe(true);
653
+ });
654
+ });
655
+ describe("ctx.getAncestor", () => {
656
+ it("should return the matching ancestor element context", () => {
657
+ const root = makeElement("root", "ProductBox");
658
+ const inner = makeElement("inner", "Container", {
659
+ id: "root",
660
+ slot: "children"
661
+ });
662
+ const doc = makeDocument([root, inner]);
663
+ const components = {
664
+ ProductBox: makeManifest("ProductBox", {
665
+ tags: ["product"]
666
+ }),
667
+ Container: makeManifest("Container"),
668
+ ProductPrice: makeManifest("ProductPrice", {
669
+ constraints: [ctx => {
670
+ const ancestor = ctx.getAncestor("ProductBox");
671
+ if (!(ancestor !== undefined && ancestor.name === "ProductBox")) {
672
+ return ctx.block("Blocked");
673
+ }
674
+ }]
675
+ })
676
+ };
677
+ const result = evaluateConstraints({
678
+ componentName: "ProductPrice",
679
+ parentId: "inner",
680
+ slot: "children",
681
+ document: doc,
682
+ components
683
+ });
684
+ expect(result.allowed).toBe(true);
685
+ });
686
+ it("should return undefined when no ancestor matches", () => {
687
+ const root = makeElement("root", "Page");
688
+ const doc = makeDocument([root]);
689
+ const components = {
690
+ Page: makeManifest("Page"),
691
+ Widget: makeManifest("Widget", {
692
+ constraints: [ctx => {
693
+ if (ctx.getAncestor("ProductBox") === undefined) {
694
+ return ctx.block("Blocked");
695
+ }
696
+ }]
697
+ })
698
+ };
699
+ const result = evaluateConstraints({
700
+ componentName: "Widget",
701
+ parentId: "root",
702
+ slot: "children",
703
+ document: doc,
704
+ components
705
+ });
706
+ expect(result.allowed).toBe(false);
707
+ });
708
+ });
709
+ describe("ctx.countInstances", () => {
710
+ it("should count instances of a component in the document", () => {
711
+ const root = makeElement("root", "Page");
712
+ const hero1 = makeElement("hero-1", "Hero", {
713
+ id: "root",
714
+ slot: "children"
715
+ });
716
+ const hero2 = makeElement("hero-2", "Hero", {
717
+ id: "root",
718
+ slot: "children"
719
+ });
720
+ const doc = makeDocument([root, hero1, hero2]);
721
+ const components = {
722
+ Page: makeManifest("Page"),
723
+ Hero: makeManifest("Hero", {
724
+ constraints: [ctx => {
725
+ if (!(ctx.countInstances("Hero") < 2)) {
726
+ return ctx.block("Max 2 Heroes");
727
+ }
728
+ }]
729
+ })
730
+ };
731
+ const result = evaluateConstraints({
732
+ componentName: "Hero",
733
+ parentId: "root",
734
+ slot: "children",
735
+ document: doc,
736
+ components
737
+ });
738
+ expect(result.allowed).toBe(false);
739
+ });
740
+ });
741
+ describe("ctx.component", () => {
742
+ it("should expose the placed component's name and tags", () => {
743
+ const parent = makeElement("parent-1", "Container");
744
+ const doc = makeDocument([parent]);
745
+ let capturedComponent;
746
+ const components = {
747
+ Container: makeManifest("Container"),
748
+ Widget: makeManifest("Widget", {
749
+ tags: ["interactive", "form"],
750
+ constraints: [ctx => {
751
+ capturedComponent = ctx.component;
752
+ }]
753
+ })
754
+ };
755
+ evaluateConstraints({
756
+ componentName: "Widget",
757
+ parentId: "parent-1",
758
+ slot: "children",
759
+ document: doc,
760
+ components
761
+ });
762
+ expect(capturedComponent).toBeDefined();
763
+ expect(capturedComponent.name).toBe("Widget");
764
+ expect(capturedComponent.tags).toEqual(["interactive", "form"]);
765
+ });
766
+ });
767
+ describe("parent.getParent", () => {
768
+ it("should return the grandparent element context", () => {
769
+ const root = makeElement("root", "Page");
770
+ const section = makeElement("section", "Section", {
771
+ id: "root",
772
+ slot: "children"
773
+ });
774
+ const doc = makeDocument([root, section]);
775
+ let grandparentName;
776
+ const components = {
777
+ Page: makeManifest("Page"),
778
+ Section: makeManifest("Section"),
779
+ Widget: makeManifest("Widget", {
780
+ constraints: [ctx => {
781
+ const grandparent = ctx.parent.getParent();
782
+ grandparentName = grandparent?.name;
783
+ }]
784
+ })
785
+ };
786
+ evaluateConstraints({
787
+ componentName: "Widget",
788
+ parentId: "section",
789
+ slot: "children",
790
+ document: doc,
791
+ components
792
+ });
793
+ expect(grandparentName).toBe("Page");
794
+ });
795
+ it("should return undefined for root element", () => {
796
+ const root = makeElement("root", "Page");
797
+ const doc = makeDocument([root]);
798
+ let parentResult = "not-called";
799
+ const components = {
800
+ Page: makeManifest("Page"),
801
+ Widget: makeManifest("Widget", {
802
+ constraints: [ctx => {
803
+ parentResult = ctx.parent.getParent();
804
+ }]
805
+ })
806
+ };
807
+ evaluateConstraints({
808
+ componentName: "Widget",
809
+ parentId: "root",
810
+ slot: "children",
811
+ document: doc,
812
+ components
813
+ });
814
+ expect(parentResult).toBeUndefined();
815
+ });
816
+ });
817
+ describe("parent.childIndex / childCount / isFirstChild / isLastChild", () => {
818
+ it("should return -1 for elements not in a list slot", () => {
819
+ const root = makeElement("root", "Page");
820
+ const doc = makeDocument([root]);
821
+ let capturedIndex;
822
+ let capturedCount;
823
+ const components = {
824
+ Page: makeManifest("Page"),
825
+ Widget: makeManifest("Widget", {
826
+ constraints: [ctx => {
827
+ capturedIndex = ctx.parent.childIndex();
828
+ capturedCount = ctx.parent.childCount();
829
+ }]
830
+ })
831
+ };
832
+ evaluateConstraints({
833
+ componentName: "Widget",
834
+ parentId: "root",
835
+ slot: "children",
836
+ document: doc,
837
+ components
838
+ });
839
+ expect(capturedIndex).toBe(-1);
840
+ expect(capturedCount).toBe(-1);
841
+ });
842
+ it("should parse child index from list slot path", () => {
843
+ const root = makeElement("root", "FunnelBuilder");
844
+ const step = makeElement("step-1", "Step", {
845
+ id: "root",
846
+ slot: "steps/2/step"
847
+ });
848
+ const doc = makeDocument([root, step], {
849
+ root: {
850
+ inputs: {
851
+ "steps/0/step": {
852
+ id: "s0",
853
+ type: "slot",
854
+ static: ["step-0"]
855
+ },
856
+ "steps/1/step": {
857
+ id: "s1",
858
+ type: "slot",
859
+ static: ["step-other"]
860
+ },
861
+ "steps/2/step": {
862
+ id: "s2",
863
+ type: "slot",
864
+ static: ["step-1"]
865
+ }
866
+ }
867
+ }
868
+ });
869
+ let capturedIndex;
870
+ let capturedCount;
871
+ let capturedIsLast;
872
+ let capturedIsFirst;
873
+ const components = {
874
+ FunnelBuilder: makeManifest("FunnelBuilder"),
875
+ Step: makeManifest("Step"),
876
+ Widget: makeManifest("Widget", {
877
+ constraints: [ctx => {
878
+ capturedIndex = ctx.parent.childIndex();
879
+ capturedCount = ctx.parent.childCount();
880
+ capturedIsLast = ctx.parent.isLastChild();
881
+ capturedIsFirst = ctx.parent.isFirstChild();
882
+ }]
883
+ })
884
+ };
885
+ evaluateConstraints({
886
+ componentName: "Widget",
887
+ parentId: "step-1",
888
+ slot: "children",
889
+ document: doc,
890
+ components
891
+ });
892
+ expect(capturedIndex).toBe(2);
893
+ expect(capturedCount).toBe(3);
894
+ expect(capturedIsLast).toBe(true);
895
+ expect(capturedIsFirst).toBe(false);
896
+ });
897
+ it("should identify the first child correctly", () => {
898
+ const root = makeElement("root", "FunnelBuilder");
899
+ const step = makeElement("step-0", "Step", {
900
+ id: "root",
901
+ slot: "steps/0/step"
902
+ });
903
+ const doc = makeDocument([root, step], {
904
+ root: {
905
+ inputs: {
906
+ "steps/0/step": {
907
+ id: "s0",
908
+ type: "slot",
909
+ static: ["step-0"]
910
+ },
911
+ "steps/1/step": {
912
+ id: "s1",
913
+ type: "slot",
914
+ static: ["step-1"]
915
+ }
916
+ }
917
+ }
918
+ });
919
+ let capturedIsFirst;
920
+ let capturedIsLast;
921
+ const components = {
922
+ FunnelBuilder: makeManifest("FunnelBuilder"),
923
+ Step: makeManifest("Step"),
924
+ Widget: makeManifest("Widget", {
925
+ constraints: [ctx => {
926
+ capturedIsFirst = ctx.parent.isFirstChild();
927
+ capturedIsLast = ctx.parent.isLastChild();
928
+ }]
929
+ })
930
+ };
931
+ evaluateConstraints({
932
+ componentName: "Widget",
933
+ parentId: "step-0",
934
+ slot: "children",
935
+ document: doc,
936
+ components
937
+ });
938
+ expect(capturedIsFirst).toBe(true);
939
+ expect(capturedIsLast).toBe(false);
940
+ });
941
+ it("should resolve position from bindings even when element.parent.slot is stale", () => {
942
+ const root = makeElement("root", "FunnelBuilder");
943
+ // Element says slot "steps/1/step" but bindings put it at steps/3/step
944
+ const step = makeElement("step-final", "Step", {
945
+ id: "root",
946
+ slot: "steps/1/step"
947
+ });
948
+ const doc = makeDocument([root, step], {
949
+ root: {
950
+ inputs: {
951
+ "steps/0/step": {
952
+ id: "s0",
953
+ type: "slot",
954
+ static: "step-0"
955
+ },
956
+ "steps/1/step": {
957
+ id: "s1",
958
+ type: "slot",
959
+ static: "step-1"
960
+ },
961
+ "steps/2/step": {
962
+ id: "s2",
963
+ type: "slot",
964
+ static: "step-2"
965
+ },
966
+ "steps/3/step": {
967
+ id: "s3",
968
+ type: "slot",
969
+ static: "step-final"
970
+ }
971
+ }
972
+ }
973
+ });
974
+ let capturedIndex;
975
+ let capturedIsLast;
976
+ const components = {
977
+ FunnelBuilder: makeManifest("FunnelBuilder"),
978
+ Step: makeManifest("Step"),
979
+ Widget: makeManifest("Widget", {
980
+ constraints: [ctx => {
981
+ capturedIndex = ctx.parent.childIndex();
982
+ capturedIsLast = ctx.parent.isLastChild();
983
+ }]
984
+ })
985
+ };
986
+ evaluateConstraints({
987
+ componentName: "Widget",
988
+ parentId: "step-final",
989
+ slot: "children",
990
+ document: doc,
991
+ components
992
+ });
993
+
994
+ // Should find actual position (index 3, last) not stale slot (index 1)
995
+ expect(capturedIndex).toBe(3);
996
+ expect(capturedIsLast).toBe(true);
997
+ });
998
+ it("should resolve position from direct list slot binding (static is an array of IDs)", () => {
999
+ const root = makeElement("root", "FunnelBuilder");
1000
+ const step0 = makeElement("step-0", "Step", {
1001
+ id: "root",
1002
+ slot: "steps"
1003
+ });
1004
+ const step1 = makeElement("step-1", "Step", {
1005
+ id: "root",
1006
+ slot: "steps"
1007
+ });
1008
+ const step2 = makeElement("step-2", "Step", {
1009
+ id: "root",
1010
+ slot: "steps"
1011
+ });
1012
+ const doc = makeDocument([root, step0, step1, step2], {
1013
+ root: {
1014
+ inputs: {
1015
+ steps: {
1016
+ id: "s",
1017
+ type: "slot",
1018
+ list: true,
1019
+ static: ["step-0", "step-1", "step-2"]
1020
+ }
1021
+ }
1022
+ }
1023
+ });
1024
+ let captured = {};
1025
+ const comps = {
1026
+ FunnelBuilder: makeManifest("FunnelBuilder"),
1027
+ Step: makeManifest("Step"),
1028
+ Widget: makeManifest("Widget", {
1029
+ constraints: [ctx => {
1030
+ captured = {
1031
+ index: ctx.parent.childIndex(),
1032
+ count: ctx.parent.childCount(),
1033
+ isFirst: ctx.parent.isFirstChild(),
1034
+ isLast: ctx.parent.isLastChild()
1035
+ };
1036
+ }]
1037
+ })
1038
+ };
1039
+
1040
+ // Place into first step
1041
+ evaluateConstraints({
1042
+ componentName: "Widget",
1043
+ parentId: "step-0",
1044
+ slot: "children",
1045
+ document: doc,
1046
+ components: comps
1047
+ });
1048
+ expect(captured.index).toBe(0);
1049
+ expect(captured.count).toBe(3);
1050
+ expect(captured.isFirst).toBe(true);
1051
+ expect(captured.isLast).toBe(false);
1052
+
1053
+ // Place into middle step
1054
+ evaluateConstraints({
1055
+ componentName: "Widget",
1056
+ parentId: "step-1",
1057
+ slot: "children",
1058
+ document: doc,
1059
+ components: comps
1060
+ });
1061
+ expect(captured.index).toBe(1);
1062
+ expect(captured.count).toBe(3);
1063
+ expect(captured.isFirst).toBe(false);
1064
+ expect(captured.isLast).toBe(false);
1065
+
1066
+ // Place into last step
1067
+ evaluateConstraints({
1068
+ componentName: "Widget",
1069
+ parentId: "step-2",
1070
+ slot: "children",
1071
+ document: doc,
1072
+ components: comps
1073
+ });
1074
+ expect(captured.index).toBe(2);
1075
+ expect(captured.count).toBe(3);
1076
+ expect(captured.isFirst).toBe(false);
1077
+ expect(captured.isLast).toBe(true);
1078
+ });
1079
+ });
1080
+ describe("descendantConstraints", () => {
1081
+ it("should block a direct child via parent's descendantConstraints", () => {
1082
+ const parent = makeElement("parent-1", "Step");
1083
+ const doc = makeDocument([parent]);
1084
+ const components = {
1085
+ Step: makeManifest("Step", {
1086
+ descendantConstraints: [ctx => {
1087
+ if (ctx.hasTag("funnel-field")) {
1088
+ return ctx.block("No funnel fields allowed");
1089
+ }
1090
+ }]
1091
+ }),
1092
+ TextField: makeManifest("TextField", {
1093
+ tags: ["funnel-field"]
1094
+ })
1095
+ };
1096
+ const result = evaluateConstraints({
1097
+ componentName: "TextField",
1098
+ parentId: "parent-1",
1099
+ slot: "children",
1100
+ document: doc,
1101
+ components
1102
+ });
1103
+ expect(result.allowed).toBe(false);
1104
+ expect(result.violation.message).toBe("No funnel fields allowed");
1105
+ });
1106
+ it("should block a nested descendant via ancestor's descendantConstraints", () => {
1107
+ const step = makeElement("step-1", "Step");
1108
+ const container = makeElement("container-1", "Container", {
1109
+ id: "step-1",
1110
+ slot: "children"
1111
+ });
1112
+ const doc = makeDocument([step, container]);
1113
+ const components = {
1114
+ Step: makeManifest("Step", {
1115
+ descendantConstraints: [ctx => {
1116
+ if (ctx.hasTag("funnel-field")) {
1117
+ return ctx.block("No funnel fields in this step");
1118
+ }
1119
+ }]
1120
+ }),
1121
+ Container: makeManifest("Container"),
1122
+ TextField: makeManifest("TextField", {
1123
+ tags: ["funnel-field"]
1124
+ })
1125
+ };
1126
+
1127
+ // TextField dropped into Container, which is inside Step
1128
+ const result = evaluateConstraints({
1129
+ componentName: "TextField",
1130
+ parentId: "container-1",
1131
+ slot: "children",
1132
+ document: doc,
1133
+ components
1134
+ });
1135
+ expect(result.allowed).toBe(false);
1136
+ expect(result.violation.message).toBe("No funnel fields in this step");
1137
+ });
1138
+ it("should allow when descendantConstraints pass", () => {
1139
+ const step = makeElement("step-1", "Step");
1140
+ const doc = makeDocument([step]);
1141
+ const components = {
1142
+ Step: makeManifest("Step", {
1143
+ descendantConstraints: [ctx => {
1144
+ if (ctx.hasTag("funnel-field")) {
1145
+ return ctx.block("Blocked");
1146
+ }
1147
+ }]
1148
+ }),
1149
+ Button: makeManifest("Button")
1150
+ };
1151
+ const result = evaluateConstraints({
1152
+ componentName: "Button",
1153
+ parentId: "step-1",
1154
+ slot: "children",
1155
+ document: doc,
1156
+ components
1157
+ });
1158
+ expect(result.allowed).toBe(true);
1159
+ });
1160
+ it("should not run parent's descendantConstraints for unrelated ancestors", () => {
1161
+ const page = makeElement("page", "Page");
1162
+ const step = makeElement("step-1", "Step", {
1163
+ id: "page",
1164
+ slot: "children"
1165
+ });
1166
+ const doc = makeDocument([page, step]);
1167
+ const components = {
1168
+ Page: makeManifest("Page", {
1169
+ descendantConstraints: [ctx => ctx.block("Page blocks everything")]
1170
+ }),
1171
+ Step: makeManifest("Step"),
1172
+ Button: makeManifest("Button")
1173
+ };
1174
+
1175
+ // Button into Step — Page's descendantConstraints should still fire
1176
+ const result = evaluateConstraints({
1177
+ componentName: "Button",
1178
+ parentId: "step-1",
1179
+ slot: "children",
1180
+ document: doc,
1181
+ components
1182
+ });
1183
+ expect(result.allowed).toBe(false);
1184
+ expect(result.violation.message).toBe("Page blocks everything");
1185
+ });
1186
+ it("should evaluate component constraints before descendantConstraints", () => {
1187
+ const parent = makeElement("parent-1", "Step");
1188
+ const doc = makeDocument([parent]);
1189
+ const components = {
1190
+ Step: makeManifest("Step", {
1191
+ descendantConstraints: [ctx => ctx.block("descendant constraint")]
1192
+ }),
1193
+ Widget: makeManifest("Widget", {
1194
+ constraints: [ctx => ctx.block("component constraint")]
1195
+ })
1196
+ };
1197
+ const result = evaluateConstraints({
1198
+ componentName: "Widget",
1199
+ parentId: "parent-1",
1200
+ slot: "children",
1201
+ document: doc,
1202
+ components
1203
+ });
1204
+ // Component's own constraints should short-circuit first
1205
+ expect(result.violation.message).toBe("component constraint");
1206
+ });
1207
+ });
1208
+ });
1209
+ describe("evaluateDeleteConstraint", () => {
1210
+ it("should allow when canDelete is undefined", () => {
1211
+ const el = makeElement("el-1", "Widget", {
1212
+ id: "root",
1213
+ slot: "children"
1214
+ });
1215
+ const root = makeElement("root", "Page");
1216
+ const doc = makeDocument([root, el]);
1217
+ const components = {
1218
+ Page: makeManifest("Page"),
1219
+ Widget: makeManifest("Widget")
1220
+ };
1221
+ const result = evaluateDeleteConstraint({
1222
+ elementId: "el-1",
1223
+ document: doc,
1224
+ components
1225
+ });
1226
+ expect(result.allowed).toBe(true);
1227
+ });
1228
+ it("should allow when canDelete is true", () => {
1229
+ const el = makeElement("el-1", "Widget", {
1230
+ id: "root",
1231
+ slot: "children"
1232
+ });
1233
+ const root = makeElement("root", "Page");
1234
+ const doc = makeDocument([root, el]);
1235
+ const components = {
1236
+ Page: makeManifest("Page"),
1237
+ Widget: makeManifest("Widget", {
1238
+ canDelete: true
1239
+ })
1240
+ };
1241
+ const result = evaluateDeleteConstraint({
1242
+ elementId: "el-1",
1243
+ document: doc,
1244
+ components
1245
+ });
1246
+ expect(result.allowed).toBe(true);
1247
+ });
1248
+ it("should block when canDelete is false", () => {
1249
+ const el = makeElement("el-1", "GridColumn", {
1250
+ id: "root",
1251
+ slot: "children"
1252
+ });
1253
+ const root = makeElement("root", "Grid");
1254
+ const doc = makeDocument([root, el]);
1255
+ const components = {
1256
+ Grid: makeManifest("Grid"),
1257
+ GridColumn: makeManifest("GridColumn", {
1258
+ canDelete: false
1259
+ })
1260
+ };
1261
+ const result = evaluateDeleteConstraint({
1262
+ elementId: "el-1",
1263
+ document: doc,
1264
+ components
1265
+ });
1266
+ expect(result.allowed).toBe(false);
1267
+ expect(result.violation.message).toBe("GridColumn cannot be deleted.");
1268
+ });
1269
+ it("should allow when canDelete check returns true", () => {
1270
+ const funnel = makeElement("funnel", "Funnel");
1271
+ const step1 = makeElement("step-1", "Step", {
1272
+ id: "funnel",
1273
+ slot: "steps/0/step"
1274
+ });
1275
+ const step2 = makeElement("step-2", "Step", {
1276
+ id: "funnel",
1277
+ slot: "steps/1/step"
1278
+ });
1279
+ const step3 = makeElement("step-3", "Step", {
1280
+ id: "funnel",
1281
+ slot: "steps/2/step"
1282
+ });
1283
+ const doc = makeDocument([funnel, step1, step2, step3], {
1284
+ funnel: {
1285
+ inputs: {
1286
+ "steps/0/step": {
1287
+ id: "s0",
1288
+ type: "slot",
1289
+ static: "step-1"
1290
+ },
1291
+ "steps/1/step": {
1292
+ id: "s1",
1293
+ type: "slot",
1294
+ static: "step-2"
1295
+ },
1296
+ "steps/2/step": {
1297
+ id: "s2",
1298
+ type: "slot",
1299
+ static: "step-3"
1300
+ }
1301
+ }
1302
+ }
1303
+ });
1304
+ const components = {
1305
+ Funnel: makeManifest("Funnel"),
1306
+ Step: makeManifest("Step", {
1307
+ canDelete: ctx => {
1308
+ if (!(ctx.countInstances("Step") > 2)) {
1309
+ return ctx.block("Blocked");
1310
+ }
1311
+ }
1312
+ })
1313
+ };
1314
+ const result = evaluateDeleteConstraint({
1315
+ elementId: "step-2",
1316
+ document: doc,
1317
+ components
1318
+ });
1319
+ expect(result.allowed).toBe(true);
1320
+ });
1321
+ it("should block when canDelete check returns false", () => {
1322
+ const funnel = makeElement("funnel", "Funnel");
1323
+ const step1 = makeElement("step-1", "Step", {
1324
+ id: "funnel",
1325
+ slot: "steps/0/step"
1326
+ });
1327
+ const step2 = makeElement("step-2", "Step", {
1328
+ id: "funnel",
1329
+ slot: "steps/1/step"
1330
+ });
1331
+ const doc = makeDocument([funnel, step1, step2], {
1332
+ funnel: {
1333
+ inputs: {
1334
+ "steps/0/step": {
1335
+ id: "s0",
1336
+ type: "slot",
1337
+ static: "step-1"
1338
+ },
1339
+ "steps/1/step": {
1340
+ id: "s1",
1341
+ type: "slot",
1342
+ static: "step-2"
1343
+ }
1344
+ }
1345
+ }
1346
+ });
1347
+ const components = {
1348
+ Funnel: makeManifest("Funnel"),
1349
+ Step: makeManifest("Step", {
1350
+ canDelete: ctx => {
1351
+ if (!(ctx.countInstances("Step") > 2)) {
1352
+ return ctx.block("Need at least 2 steps");
1353
+ }
1354
+ }
1355
+ })
1356
+ };
1357
+ const result = evaluateDeleteConstraint({
1358
+ elementId: "step-1",
1359
+ document: doc,
1360
+ components
1361
+ });
1362
+ expect(result.allowed).toBe(false);
1363
+ expect(result.violation.message).toBe("Need at least 2 steps");
1364
+ });
1365
+ it("should use thrown error message as violation message", () => {
1366
+ const root = makeElement("root", "Page");
1367
+ const el = makeElement("el-1", "Widget", {
1368
+ id: "root",
1369
+ slot: "children"
1370
+ });
1371
+ const doc = makeDocument([root, el]);
1372
+ const components = {
1373
+ Page: makeManifest("Page"),
1374
+ Widget: makeManifest("Widget", {
1375
+ canDelete: () => {
1376
+ throw new Error("Cannot delete this widget right now");
1377
+ }
1378
+ })
1379
+ };
1380
+ const result = evaluateDeleteConstraint({
1381
+ elementId: "el-1",
1382
+ document: doc,
1383
+ components
1384
+ });
1385
+ expect(result.allowed).toBe(false);
1386
+ expect(result.violation.message).toBe("Cannot delete this widget right now");
1387
+ });
1388
+ it("should provide correct context to the check function", () => {
1389
+ const root = makeElement("root", "Funnel");
1390
+ const step = makeElement("step-1", "Step", {
1391
+ id: "root",
1392
+ slot: "steps/0/step"
1393
+ });
1394
+ const doc = makeDocument([root, step], {
1395
+ root: {
1396
+ inputs: {
1397
+ "steps/0/step": {
1398
+ id: "s0",
1399
+ type: "slot",
1400
+ static: "step-1"
1401
+ },
1402
+ "steps/1/step": {
1403
+ id: "s1",
1404
+ type: "slot",
1405
+ static: "step-2"
1406
+ }
1407
+ }
1408
+ }
1409
+ });
1410
+ let capturedCtx;
1411
+ const components = {
1412
+ Funnel: makeManifest("Funnel"),
1413
+ Step: makeManifest("Step", {
1414
+ tags: ["funnel-step"],
1415
+ canDelete: ctx => {
1416
+ capturedCtx = ctx;
1417
+ }
1418
+ })
1419
+ };
1420
+ evaluateDeleteConstraint({
1421
+ elementId: "step-1",
1422
+ document: doc,
1423
+ components
1424
+ });
1425
+ expect(capturedCtx).toBeDefined();
1426
+ expect(capturedCtx.component.name).toBe("Step");
1427
+ expect(capturedCtx.component.tags).toEqual(["funnel-step"]);
1428
+ expect(capturedCtx.parent.name).toBe("Funnel");
1429
+ expect(capturedCtx.slot).toBe("steps/0/step");
1430
+ expect(capturedCtx.isChildOf("Funnel")).toBe(true);
1431
+ expect(capturedCtx.hasTag("funnel-step")).toBe(true);
1432
+ });
1433
+ it("should allow when element is not found", () => {
1434
+ const doc = makeDocument([]);
1435
+ const components = {};
1436
+ const result = evaluateDeleteConstraint({
1437
+ elementId: "nonexistent",
1438
+ document: doc,
1439
+ components
1440
+ });
1441
+ expect(result.allowed).toBe(true);
1442
+ });
1443
+ it("should allow when manifest is not found", () => {
1444
+ const el = makeElement("el-1", "Unknown");
1445
+ const doc = makeDocument([el]);
1446
+ const components = {};
1447
+ const result = evaluateDeleteConstraint({
1448
+ elementId: "el-1",
1449
+ document: doc,
1450
+ components
1451
+ });
1452
+ expect(result.allowed).toBe(true);
1453
+ });
1454
+ });
1455
+
1456
+ //# sourceMappingURL=ConstraintEvaluator.test.js.map