@simonklee/yoga 0.2.24

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,939 @@
1
+ import { expect, test, describe } from "bun:test";
2
+ import Yoga, {
3
+ Node,
4
+ Config,
5
+ MeasureMode,
6
+ Direction,
7
+ FlexDirection,
8
+ Edge,
9
+ Align,
10
+ BoxSizing,
11
+ Errata,
12
+ ExperimentalFeature,
13
+ Gutter,
14
+ Unit,
15
+ } from "./index";
16
+
17
+ describe("Node", () => {
18
+ test("create and free", () => {
19
+ const node = Node.create();
20
+ expect(node).toBeDefined();
21
+ node.free();
22
+ });
23
+
24
+ test("create with config", () => {
25
+ const config = Config.create();
26
+ const node = Node.create(config);
27
+ expect(node).toBeDefined();
28
+ node.free();
29
+ config.free();
30
+ });
31
+
32
+ test("basic layout", () => {
33
+ const root = Node.create();
34
+ root.setWidth(100);
35
+ root.setHeight(100);
36
+ root.setFlexDirection(FlexDirection.Row);
37
+
38
+ const child1 = Node.create();
39
+ child1.setFlexGrow(1);
40
+ root.insertChild(child1, 0);
41
+
42
+ const child2 = Node.create();
43
+ child2.setFlexGrow(1);
44
+ root.insertChild(child2, 1);
45
+
46
+ root.calculateLayout(100, 100, Direction.LTR);
47
+
48
+ const layout = root.getComputedLayout();
49
+ expect(layout.width).toBe(100);
50
+ expect(layout.height).toBe(100);
51
+
52
+ expect(child1.getComputedWidth()).toBe(50);
53
+ expect(child2.getComputedWidth()).toBe(50);
54
+
55
+ root.freeRecursive();
56
+ });
57
+ });
58
+
59
+ describe("MeasureFunc", () => {
60
+ test("setMeasureFunc and hasMeasureFunc", () => {
61
+ const node = Node.create();
62
+
63
+ expect(node.hasMeasureFunc()).toBe(false);
64
+
65
+ node.setMeasureFunc((width, widthMode, height, heightMode) => {
66
+ return { width: 100, height: 50 };
67
+ });
68
+
69
+ expect(node.hasMeasureFunc()).toBe(true);
70
+
71
+ node.unsetMeasureFunc();
72
+ expect(node.hasMeasureFunc()).toBe(false);
73
+
74
+ node.free();
75
+ });
76
+
77
+ test("measure function is called during layout", () => {
78
+ const root = Node.create();
79
+ root.setWidth(200);
80
+ root.setHeight(200);
81
+
82
+ const child = Node.create();
83
+ // Prevent default stretch behavior so child uses measured size
84
+ child.setAlignSelf(Align.FlexStart);
85
+ let measureCalled = false;
86
+
87
+ child.setMeasureFunc((width, widthMode, height, heightMode) => {
88
+ measureCalled = true;
89
+ return { width: 50, height: 25 };
90
+ });
91
+
92
+ root.insertChild(child, 0);
93
+ root.calculateLayout(200, 200, Direction.LTR);
94
+
95
+ expect(measureCalled).toBe(true);
96
+ expect(child.getComputedWidth()).toBe(50);
97
+ expect(child.getComputedHeight()).toBe(25);
98
+
99
+ root.freeRecursive();
100
+ });
101
+
102
+ test("measure function receives correct modes", () => {
103
+ const root = Node.create();
104
+ root.setWidth(100);
105
+ root.setFlexDirection(FlexDirection.Row);
106
+
107
+ const child = Node.create();
108
+ // Don't use flexGrow - let child determine its own size via measure
109
+ child.setAlignSelf(Align.FlexStart);
110
+
111
+ let receivedWidthMode = -1;
112
+ let receivedHeightMode = -1;
113
+
114
+ child.setMeasureFunc((width, widthMode, height, heightMode) => {
115
+ receivedWidthMode = widthMode;
116
+ receivedHeightMode = heightMode;
117
+ return { width: 50, height: 30 };
118
+ });
119
+
120
+ root.insertChild(child, 0);
121
+ root.calculateLayout(100, undefined, Direction.LTR);
122
+
123
+ // Width should be AtMost because parent has fixed width
124
+ expect(receivedWidthMode).toBe(MeasureMode.AtMost);
125
+ // Height should be Undefined because no height constraint
126
+ expect(receivedHeightMode).toBe(MeasureMode.Undefined);
127
+
128
+ root.freeRecursive();
129
+ });
130
+ });
131
+
132
+ describe("DirtiedFunc", () => {
133
+ test("setDirtiedFunc callback is called", () => {
134
+ const root = Node.create();
135
+ root.setWidth(100);
136
+ root.setHeight(100);
137
+
138
+ let dirtiedCalled = false;
139
+
140
+ // Only nodes with measure function can be marked dirty
141
+ root.setMeasureFunc(() => ({ width: 100, height: 100 }));
142
+
143
+ // Must calculate layout first so node becomes "clean"
144
+ // Dirtied callback only fires when transitioning from clean to dirty
145
+ root.calculateLayout(100, 100, Direction.LTR);
146
+
147
+ root.setDirtiedFunc(() => {
148
+ dirtiedCalled = true;
149
+ });
150
+
151
+ root.markDirty();
152
+
153
+ expect(dirtiedCalled).toBe(true);
154
+
155
+ root.free();
156
+ });
157
+
158
+ test("hasDirtiedFunc reflects callback state", () => {
159
+ const node = Node.create();
160
+ expect(node.hasDirtiedFunc()).toBe(false);
161
+
162
+ node.setMeasureFunc(() => ({ width: 10, height: 10 }));
163
+ node.setDirtiedFunc(() => {});
164
+ expect(node.hasDirtiedFunc()).toBe(true);
165
+
166
+ node.unsetDirtiedFunc();
167
+ expect(node.hasDirtiedFunc()).toBe(false);
168
+
169
+ node.free();
170
+ });
171
+ });
172
+
173
+ describe("Margins, Padding, Border", () => {
174
+ test("margins affect layout", () => {
175
+ const root = Node.create();
176
+ root.setWidth(100);
177
+ root.setHeight(100);
178
+ root.setFlexDirection(FlexDirection.Column);
179
+
180
+ const child = Node.create();
181
+ child.setWidth(50);
182
+ child.setHeight(50);
183
+ child.setMargin(Edge.Left, 10);
184
+ child.setMargin(Edge.Top, 5);
185
+
186
+ root.insertChild(child, 0);
187
+ root.calculateLayout(100, 100, Direction.LTR);
188
+
189
+ expect(child.getComputedLeft()).toBe(10);
190
+ expect(child.getComputedTop()).toBe(5);
191
+
192
+ root.freeRecursive();
193
+ });
194
+
195
+ test("padding affects children", () => {
196
+ const root = Node.create();
197
+ root.setWidth(100);
198
+ root.setHeight(100);
199
+ root.setPadding(Edge.All, 10);
200
+
201
+ const child = Node.create();
202
+ child.setWidth(50);
203
+ child.setHeight(50);
204
+
205
+ root.insertChild(child, 0);
206
+ root.calculateLayout(100, 100, Direction.LTR);
207
+
208
+ expect(child.getComputedLeft()).toBe(10);
209
+ expect(child.getComputedTop()).toBe(10);
210
+
211
+ root.freeRecursive();
212
+ });
213
+
214
+ test("border affects layout", () => {
215
+ const root = Node.create();
216
+ root.setWidth(100);
217
+ root.setHeight(100);
218
+ root.setBorder(Edge.All, 5);
219
+
220
+ const child = Node.create();
221
+ child.setFlexGrow(1);
222
+
223
+ root.insertChild(child, 0);
224
+ root.calculateLayout(100, 100, Direction.LTR);
225
+
226
+ // Child should be offset by border
227
+ expect(child.getComputedLeft()).toBe(5);
228
+ expect(child.getComputedTop()).toBe(5);
229
+ // Child should be smaller due to borders
230
+ expect(child.getComputedWidth()).toBe(90);
231
+ expect(child.getComputedHeight()).toBe(90);
232
+
233
+ root.freeRecursive();
234
+ });
235
+ });
236
+
237
+ describe("Max/Fit/Stretch units", () => {
238
+ test("Unit includes max/fit/stretch", () => {
239
+ expect(Unit.MaxContent).toBe(4);
240
+ expect(Unit.FitContent).toBe(5);
241
+ expect(Unit.Stretch).toBe(6);
242
+ });
243
+
244
+ test("setters update value units", () => {
245
+ const node = Node.create();
246
+
247
+ const cases = [
248
+ { set: () => node.setFlexBasisMaxContent(), get: () => node.getFlexBasis(), unit: Unit.MaxContent },
249
+ { set: () => node.setFlexBasisFitContent(), get: () => node.getFlexBasis(), unit: Unit.FitContent },
250
+ { set: () => node.setFlexBasisStretch(), get: () => node.getFlexBasis(), unit: Unit.Stretch },
251
+ { set: () => node.setWidthMaxContent(), get: () => node.getWidth(), unit: Unit.MaxContent },
252
+ { set: () => node.setWidthFitContent(), get: () => node.getWidth(), unit: Unit.FitContent },
253
+ { set: () => node.setWidthStretch(), get: () => node.getWidth(), unit: Unit.Stretch },
254
+ { set: () => node.setHeightMaxContent(), get: () => node.getHeight(), unit: Unit.MaxContent },
255
+ { set: () => node.setHeightFitContent(), get: () => node.getHeight(), unit: Unit.FitContent },
256
+ { set: () => node.setHeightStretch(), get: () => node.getHeight(), unit: Unit.Stretch },
257
+ { set: () => node.setMinWidthMaxContent(), get: () => node.getMinWidth(), unit: Unit.MaxContent },
258
+ { set: () => node.setMinWidthFitContent(), get: () => node.getMinWidth(), unit: Unit.FitContent },
259
+ { set: () => node.setMinWidthStretch(), get: () => node.getMinWidth(), unit: Unit.Stretch },
260
+ { set: () => node.setMinHeightMaxContent(), get: () => node.getMinHeight(), unit: Unit.MaxContent },
261
+ { set: () => node.setMinHeightFitContent(), get: () => node.getMinHeight(), unit: Unit.FitContent },
262
+ { set: () => node.setMinHeightStretch(), get: () => node.getMinHeight(), unit: Unit.Stretch },
263
+ { set: () => node.setMaxWidthMaxContent(), get: () => node.getMaxWidth(), unit: Unit.MaxContent },
264
+ { set: () => node.setMaxWidthFitContent(), get: () => node.getMaxWidth(), unit: Unit.FitContent },
265
+ { set: () => node.setMaxWidthStretch(), get: () => node.getMaxWidth(), unit: Unit.Stretch },
266
+ { set: () => node.setMaxHeightMaxContent(), get: () => node.getMaxHeight(), unit: Unit.MaxContent },
267
+ { set: () => node.setMaxHeightFitContent(), get: () => node.getMaxHeight(), unit: Unit.FitContent },
268
+ { set: () => node.setMaxHeightStretch(), get: () => node.getMaxHeight(), unit: Unit.Stretch },
269
+ ];
270
+
271
+ for (const { set, get, unit } of cases) {
272
+ set();
273
+ expect(get().unit).toBe(unit);
274
+ }
275
+
276
+ node.free();
277
+ });
278
+ });
279
+
280
+ describe("BaselineFunc", () => {
281
+ test("setBaselineFunc callback affects layout", () => {
282
+ const root = Node.create();
283
+ root.setWidth(200);
284
+ root.setHeight(100);
285
+ root.setFlexDirection(FlexDirection.Row);
286
+ root.setAlignItems(Align.Baseline);
287
+
288
+ const child1 = Node.create();
289
+ child1.setWidth(50);
290
+ child1.setHeight(40);
291
+ let baselineCalled = false;
292
+ child1.setBaselineFunc((width, height) => {
293
+ baselineCalled = true;
294
+ return 30; // baseline at 30px from top
295
+ });
296
+
297
+ const child2 = Node.create();
298
+ child2.setWidth(50);
299
+ child2.setHeight(60);
300
+
301
+ root.insertChild(child1, 0);
302
+ root.insertChild(child2, 1);
303
+ root.calculateLayout(200, 100, Direction.LTR);
304
+
305
+ expect(baselineCalled).toBe(true);
306
+ // child1 should be offset down to align its baseline (30px) with child2's baseline
307
+ expect(child1.getComputedTop()).toBe(30);
308
+ expect(child2.getComputedTop()).toBe(0);
309
+
310
+ root.freeRecursive();
311
+ });
312
+ });
313
+
314
+ describe("Gap", () => {
315
+ test("row gap", () => {
316
+ const root = Node.create();
317
+ root.setWidth(100);
318
+ root.setHeight(100);
319
+ root.setFlexDirection(FlexDirection.Column);
320
+ root.setGap(Gutter.Row, 10);
321
+
322
+ const child1 = Node.create();
323
+ child1.setHeight(20);
324
+ root.insertChild(child1, 0);
325
+
326
+ const child2 = Node.create();
327
+ child2.setHeight(20);
328
+ root.insertChild(child2, 1);
329
+
330
+ root.calculateLayout(100, 100, Direction.LTR);
331
+
332
+ expect(child1.getComputedTop()).toBe(0);
333
+ expect(child2.getComputedTop()).toBe(30); // 20 + 10 gap
334
+
335
+ root.freeRecursive();
336
+ });
337
+ });
338
+
339
+ describe("Percent values", () => {
340
+ test("setWidth with percent string", () => {
341
+ const root = Node.create();
342
+ root.setWidth(200);
343
+ root.setHeight(100);
344
+
345
+ const child = Node.create();
346
+ child.setWidth("50%");
347
+ child.setHeight(50);
348
+
349
+ root.insertChild(child, 0);
350
+ root.calculateLayout(200, 100, Direction.LTR);
351
+
352
+ expect(child.getComputedWidth()).toBe(100); // 50% of 200
353
+
354
+ root.freeRecursive();
355
+ });
356
+
357
+ test("setHeight with percent string", () => {
358
+ const root = Node.create();
359
+ root.setWidth(100);
360
+ root.setHeight(200);
361
+
362
+ const child = Node.create();
363
+ child.setWidth(50);
364
+ child.setHeight("25%");
365
+
366
+ root.insertChild(child, 0);
367
+ root.calculateLayout(100, 200, Direction.LTR);
368
+
369
+ expect(child.getComputedHeight()).toBe(50); // 25% of 200
370
+
371
+ root.freeRecursive();
372
+ });
373
+
374
+ test("setMargin with auto", () => {
375
+ const root = Node.create();
376
+ root.setWidth(200);
377
+ root.setHeight(100);
378
+ root.setFlexDirection(FlexDirection.Row);
379
+ root.setJustifyContent(Yoga.Justify.FlexStart);
380
+
381
+ const child = Node.create();
382
+ child.setWidth(50);
383
+ child.setHeight(50);
384
+ child.setMargin(Edge.Left, "auto");
385
+
386
+ root.insertChild(child, 0);
387
+ root.calculateLayout(200, 100, Direction.LTR);
388
+
389
+ // With auto margin, child should be pushed to the right
390
+ expect(child.getComputedLeft()).toBe(150); // 200 - 50
391
+
392
+ root.freeRecursive();
393
+ });
394
+
395
+ test("setMargin with percent string", () => {
396
+ const root = Node.create();
397
+ root.setWidth(200);
398
+ root.setHeight(100);
399
+
400
+ const child = Node.create();
401
+ child.setWidth(50);
402
+ child.setHeight(50);
403
+ child.setMargin(Edge.Left, "10%");
404
+
405
+ root.insertChild(child, 0);
406
+ root.calculateLayout(200, 100, Direction.LTR);
407
+
408
+ expect(child.getComputedLeft()).toBe(20); // 10% of 200
409
+
410
+ root.freeRecursive();
411
+ });
412
+
413
+ test("setPadding with percent string", () => {
414
+ const root = Node.create();
415
+ root.setWidth(200);
416
+ root.setHeight(100);
417
+ root.setPadding(Edge.All, "10%");
418
+
419
+ const child = Node.create();
420
+ child.setFlexGrow(1);
421
+
422
+ root.insertChild(child, 0);
423
+ root.calculateLayout(200, 100, Direction.LTR);
424
+
425
+ // Padding percent is based on width dimension in CSS/Yoga
426
+ // 10% of 200 = 20 on all sides
427
+ expect(child.getComputedLeft()).toBe(20);
428
+ expect(child.getComputedTop()).toBe(20);
429
+ expect(child.getComputedWidth()).toBe(160); // 200 - 20 - 20
430
+ expect(child.getComputedHeight()).toBe(60); // 100 - 20 - 20
431
+
432
+ root.freeRecursive();
433
+ });
434
+
435
+ test("setFlexBasis with percent string", () => {
436
+ const root = Node.create();
437
+ root.setWidth(200);
438
+ root.setHeight(100);
439
+ root.setFlexDirection(FlexDirection.Row);
440
+
441
+ const child = Node.create();
442
+ child.setFlexBasis("50%");
443
+
444
+ root.insertChild(child, 0);
445
+ root.calculateLayout(200, 100, Direction.LTR);
446
+
447
+ expect(child.getComputedWidth()).toBe(100); // 50% of 200
448
+
449
+ root.freeRecursive();
450
+ });
451
+
452
+ test("setGap with percent string", () => {
453
+ const root = Node.create();
454
+ root.setWidth(100);
455
+ root.setHeight(200);
456
+ root.setFlexDirection(FlexDirection.Column);
457
+ root.setGap(Gutter.Row, "10%");
458
+
459
+ const child1 = Node.create();
460
+ child1.setHeight(50);
461
+ root.insertChild(child1, 0);
462
+
463
+ const child2 = Node.create();
464
+ child2.setHeight(50);
465
+ root.insertChild(child2, 1);
466
+
467
+ root.calculateLayout(100, 200, Direction.LTR);
468
+
469
+ expect(child1.getComputedTop()).toBe(0);
470
+ expect(child2.getComputedTop()).toBe(70); // 50 + 10% of 200 = 50 + 20
471
+
472
+ root.freeRecursive();
473
+ });
474
+
475
+ test("setMinWidth and setMaxWidth with percent", () => {
476
+ const root = Node.create();
477
+ root.setWidth(200);
478
+ root.setHeight(100);
479
+
480
+ const child = Node.create();
481
+ child.setMinWidth("25%");
482
+ child.setMaxWidth("75%");
483
+ child.setWidth("100%");
484
+ child.setHeight(50);
485
+
486
+ root.insertChild(child, 0);
487
+ root.calculateLayout(200, 100, Direction.LTR);
488
+
489
+ // Width should be capped at maxWidth (75% of 200 = 150)
490
+ expect(child.getComputedWidth()).toBe(150);
491
+
492
+ root.freeRecursive();
493
+ });
494
+ });
495
+
496
+ describe("New API methods", () => {
497
+ test("copyStyle", () => {
498
+ const node1 = Node.create();
499
+ node1.setWidth(100);
500
+ node1.setHeight(200);
501
+ node1.setFlexDirection(FlexDirection.Row);
502
+
503
+ const node2 = Node.create();
504
+ node2.copyStyle(node1);
505
+
506
+ expect(node2.getFlexDirection()).toBe(FlexDirection.Row);
507
+
508
+ node1.free();
509
+ node2.free();
510
+ });
511
+
512
+ test("setBoxSizing and getBoxSizing", () => {
513
+ const node = Node.create();
514
+
515
+ node.setBoxSizing(BoxSizing.ContentBox);
516
+ expect(node.getBoxSizing()).toBe(BoxSizing.ContentBox);
517
+
518
+ node.setBoxSizing(BoxSizing.BorderBox);
519
+ expect(node.getBoxSizing()).toBe(BoxSizing.BorderBox);
520
+
521
+ node.free();
522
+ });
523
+
524
+ test("setIsReferenceBaseline and isReferenceBaseline", () => {
525
+ const node = Node.create();
526
+
527
+ expect(node.isReferenceBaseline()).toBe(false);
528
+
529
+ node.setIsReferenceBaseline(true);
530
+ expect(node.isReferenceBaseline()).toBe(true);
531
+
532
+ node.setIsReferenceBaseline(false);
533
+ expect(node.isReferenceBaseline()).toBe(false);
534
+
535
+ node.free();
536
+ });
537
+
538
+ test("setAlwaysFormsContainingBlock", () => {
539
+ const node = Node.create();
540
+ // Just verify it doesn't throw
541
+ node.setAlwaysFormsContainingBlock(true);
542
+ node.setAlwaysFormsContainingBlock(false);
543
+ node.free();
544
+ });
545
+ });
546
+
547
+ describe("Value getters", () => {
548
+ test("getWidth returns Value with unit and value", () => {
549
+ const node = Node.create();
550
+
551
+ node.setWidth(100);
552
+ const width = node.getWidth();
553
+ expect(width.unit).toBe(Yoga.Unit.Point);
554
+ expect(width.value).toBe(100);
555
+
556
+ node.setWidth("50%");
557
+ const widthPercent = node.getWidth();
558
+ expect(widthPercent.unit).toBe(Yoga.Unit.Percent);
559
+ expect(widthPercent.value).toBe(50);
560
+
561
+ node.setWidth("auto");
562
+ const widthAuto = node.getWidth();
563
+ expect(widthAuto.unit).toBe(Yoga.Unit.Auto);
564
+
565
+ node.free();
566
+ });
567
+
568
+ test("getMargin returns Value with unit and value", () => {
569
+ const node = Node.create();
570
+
571
+ node.setMargin(Edge.Left, 20);
572
+ const margin = node.getMargin(Edge.Left);
573
+ expect(margin.unit).toBe(Yoga.Unit.Point);
574
+ expect(margin.value).toBe(20);
575
+
576
+ node.setMargin(Edge.Top, "10%");
577
+ const marginPercent = node.getMargin(Edge.Top);
578
+ expect(marginPercent.unit).toBe(Yoga.Unit.Percent);
579
+ expect(marginPercent.value).toBe(10);
580
+
581
+ node.free();
582
+ });
583
+
584
+ test("getFlexBasis returns Value", () => {
585
+ const node = Node.create();
586
+
587
+ node.setFlexBasis(50);
588
+ const basis = node.getFlexBasis();
589
+ expect(basis.unit).toBe(Yoga.Unit.Point);
590
+ expect(basis.value).toBe(50);
591
+
592
+ node.setFlexBasis("auto");
593
+ const basisAuto = node.getFlexBasis();
594
+ expect(basisAuto.unit).toBe(Yoga.Unit.Auto);
595
+
596
+ node.free();
597
+ });
598
+ });
599
+
600
+ describe("DirtiedFunction signature", () => {
601
+ test("dirtiedFunc receives node as parameter", () => {
602
+ const root = Node.create();
603
+ root.setWidth(100);
604
+ root.setHeight(100);
605
+
606
+ let receivedNode: Node | undefined = undefined;
607
+
608
+ root.setMeasureFunc(() => ({ width: 100, height: 100 }));
609
+ root.calculateLayout(100, 100, Direction.LTR);
610
+
611
+ root.setDirtiedFunc((node) => {
612
+ receivedNode = node;
613
+ });
614
+
615
+ root.markDirty();
616
+
617
+ expect(receivedNode).toBeDefined();
618
+ expect(receivedNode === root).toBe(true);
619
+
620
+ root.free();
621
+ });
622
+ });
623
+
624
+ describe("Config", () => {
625
+ test("errata settings", () => {
626
+ const config = Config.create();
627
+
628
+ config.setErrata(Errata.Classic);
629
+ expect(config.getErrata()).toBe(Errata.Classic);
630
+
631
+ config.setErrata(Errata.None);
632
+ expect(config.getErrata()).toBe(Errata.None);
633
+
634
+ config.free();
635
+ });
636
+
637
+ test("experimental features", () => {
638
+ const config = Config.create();
639
+
640
+ expect(config.isExperimentalFeatureEnabled(ExperimentalFeature.WebFlexBasis)).toBe(false);
641
+
642
+ config.setExperimentalFeatureEnabled(ExperimentalFeature.WebFlexBasis, true);
643
+ expect(config.isExperimentalFeatureEnabled(ExperimentalFeature.WebFlexBasis)).toBe(true);
644
+
645
+ config.setExperimentalFeatureEnabled(ExperimentalFeature.WebFlexBasis, false);
646
+ expect(config.isExperimentalFeatureEnabled(ExperimentalFeature.WebFlexBasis)).toBe(false);
647
+
648
+ config.free();
649
+ });
650
+ });
651
+
652
+ describe("Type exports", () => {
653
+ test("enum types can be used as types", () => {
654
+ // This test verifies that the type exports work correctly
655
+ // If the types weren't exported, this wouldn't compile
656
+ const align: Align = Align.Center;
657
+ const direction: Direction = Direction.LTR;
658
+ const edge: Edge = Edge.All;
659
+ const flexDir: FlexDirection = FlexDirection.Row;
660
+ const boxSizing: BoxSizing = BoxSizing.BorderBox;
661
+ const errata: Errata = Errata.None;
662
+
663
+ expect(align).toBe(2);
664
+ expect(direction).toBe(1);
665
+ expect(edge).toBe(8);
666
+ expect(flexDir).toBe(2);
667
+ expect(boxSizing).toBe(0);
668
+ expect(errata).toBe(0);
669
+ });
670
+ });
671
+
672
+ describe("Use-after-free protection", () => {
673
+ test("isFreed returns false before free", () => {
674
+ const node = Node.create();
675
+ expect(node.isFreed()).toBe(false);
676
+ node.free();
677
+ });
678
+
679
+ test("isFreed returns true after free", () => {
680
+ const node = Node.create();
681
+ node.free();
682
+ expect(node.isFreed()).toBe(true);
683
+ });
684
+
685
+ test("isFreed returns true after freeRecursive", () => {
686
+ const root = Node.create();
687
+ root.setWidth(100);
688
+ root.setHeight(100);
689
+
690
+ const child = Node.create();
691
+ root.insertChild(child, 0);
692
+
693
+ root.freeRecursive();
694
+ expect(root.isFreed()).toBe(true);
695
+ });
696
+
697
+ test("methods return default values after free (yoga-layout compatible)", () => {
698
+ const node = Node.create();
699
+ node.setWidth(100);
700
+ node.free();
701
+
702
+ // After free, getters return default values instead of throwing (matches yoga-layout)
703
+ expect(node.getComputedWidth()).toBe(0);
704
+ expect(node.getWidth()).toEqual({ unit: Unit.Undefined, value: NaN });
705
+ expect(node.getFlexDirection()).toBe(FlexDirection.Column);
706
+ expect(node.isDirty()).toBe(true);
707
+
708
+ // Setters are no-ops (don't throw)
709
+ node.setWidth(50); // Should not throw
710
+ node.calculateLayout(); // Should not throw
711
+ });
712
+
713
+ test("double free is safe (no-op)", () => {
714
+ const node = Node.create();
715
+ node.free();
716
+ // Should not throw
717
+ node.free();
718
+ node.free();
719
+ expect(node.isFreed()).toBe(true);
720
+ });
721
+
722
+ test("double freeRecursive is safe (no-op)", () => {
723
+ const root = Node.create();
724
+ const child = Node.create();
725
+ root.insertChild(child, 0);
726
+
727
+ root.freeRecursive();
728
+ // Should not throw
729
+ root.freeRecursive();
730
+ expect(root.isFreed()).toBe(true);
731
+ });
732
+
733
+ test("accessing freed node in rapid cycles does not crash", () => {
734
+ // This test verifies the fix for malloc corruption on Linux
735
+ // Note: Only the root node that called freeRecursive() knows it's freed.
736
+ // Child nodes' JS wrappers don't automatically know they were freed.
737
+ const config = Config.create();
738
+
739
+ for (let i = 0; i < 50; i++) {
740
+ const root = Node.create(config);
741
+ root.setWidth(100);
742
+ root.setFlexDirection(FlexDirection.Column);
743
+
744
+ const child = Node.create(config);
745
+ child.setAlignSelf(Align.FlexStart);
746
+ child.setMeasureFunc(() => ({ width: 50, height: 50 }));
747
+ root.insertChild(child, 0);
748
+
749
+ root.calculateLayout();
750
+ root.freeRecursive();
751
+
752
+ // The root node should return default values, not crash (matches yoga-layout)
753
+ expect(root.getComputedWidth()).toBe(0);
754
+ root.setWidth(50); // Should be a no-op, not crash
755
+ }
756
+
757
+ config.free();
758
+ });
759
+ });
760
+
761
+ describe("Memory management", () => {
762
+ test("freeRecursive cleans up callbacks without errors", () => {
763
+ const root = Node.create();
764
+ root.setWidth(200);
765
+ root.setHeight(200);
766
+ root.setFlexDirection(FlexDirection.Column);
767
+
768
+ // Create a child with measure function (nodes with measure funcs can't have children)
769
+ const child = Node.create();
770
+ child.setAlignSelf(Align.FlexStart);
771
+
772
+ let measureCalled = false;
773
+ child.setMeasureFunc(() => {
774
+ measureCalled = true;
775
+ return { width: 100, height: 100 };
776
+ });
777
+
778
+ root.insertChild(child, 0);
779
+ root.calculateLayout();
780
+ expect(measureCalled).toBe(true);
781
+
782
+ // freeRecursive should clean up callbacks properly
783
+ // This should not throw or cause memory corruption
784
+ root.freeRecursive();
785
+ });
786
+
787
+ test("reset cleans up callbacks and allows new ones", () => {
788
+ const node = Node.create();
789
+
790
+ // Set measure function (don't set width/height so measure func is called)
791
+ let firstCallbackCalled = false;
792
+ node.setMeasureFunc(() => {
793
+ firstCallbackCalled = true;
794
+ return { width: 50, height: 50 };
795
+ });
796
+
797
+ node.calculateLayout();
798
+ expect(firstCallbackCalled).toBe(true);
799
+ expect(node.hasMeasureFunc()).toBe(true);
800
+
801
+ // Reset should clean up the callback
802
+ node.reset();
803
+
804
+ // After reset, node should not have measure function
805
+ expect(node.hasMeasureFunc()).toBe(false);
806
+
807
+ // Should be able to set a new measure function
808
+ let secondCallbackCalled = false;
809
+ node.setMeasureFunc(() => {
810
+ secondCallbackCalled = true;
811
+ return { width: 75, height: 75 };
812
+ });
813
+
814
+ expect(node.hasMeasureFunc()).toBe(true);
815
+ node.calculateLayout();
816
+ expect(secondCallbackCalled).toBe(true);
817
+
818
+ node.free();
819
+ });
820
+
821
+ test("rapid free/create cycles with measure functions", () => {
822
+ // This test verifies that rapid free/create cycles don't cause
823
+ // memory corruption (the original bug on Linux)
824
+ const config = Config.create();
825
+
826
+ for (let i = 0; i < 100; i++) {
827
+ const node = Node.create(config);
828
+ // Don't set width/height so measure function is called
829
+
830
+ const expectedSize = 10 + i;
831
+ node.setMeasureFunc(() => ({
832
+ width: expectedSize,
833
+ height: expectedSize,
834
+ }));
835
+
836
+ node.calculateLayout();
837
+
838
+ const width = node.getComputedWidth();
839
+ expect(width).toBe(expectedSize);
840
+ expect(Number.isNaN(width)).toBe(false);
841
+
842
+ node.free();
843
+ }
844
+
845
+ config.free();
846
+ });
847
+
848
+ test("reset followed by free works correctly", () => {
849
+ const node = Node.create();
850
+
851
+ node.setMeasureFunc(() => ({ width: 50, height: 50 }));
852
+ node.setBaselineFunc(() => 25);
853
+ node.setDirtiedFunc(() => {});
854
+
855
+ node.calculateLayout();
856
+
857
+ // Reset clears callbacks
858
+ node.reset();
859
+
860
+ // Free should work without double-free
861
+ node.free();
862
+ });
863
+
864
+ test("multiple reset calls are safe", () => {
865
+ const node = Node.create();
866
+
867
+ node.setMeasureFunc(() => ({ width: 50, height: 50 }));
868
+ node.calculateLayout();
869
+
870
+ // Multiple resets should be safe
871
+ node.reset();
872
+ node.reset();
873
+ node.reset();
874
+
875
+ node.free();
876
+ });
877
+
878
+ test("freeRecursive with nested children with callbacks", () => {
879
+ const root = Node.create();
880
+ root.setWidth(200);
881
+ root.setHeight(200);
882
+ root.setFlexDirection(FlexDirection.Column);
883
+
884
+ // Create children with measure functions
885
+ // Note: We can't add children to nodes with measure functions,
886
+ // so we set up the hierarchy first, then add measure func to leaf nodes
887
+ const child1 = Node.create();
888
+ child1.setAlignSelf(Align.FlexStart);
889
+ child1.setMeasureFunc(() => ({ width: 50, height: 50 }));
890
+
891
+ const child2 = Node.create();
892
+ child2.setAlignSelf(Align.FlexStart);
893
+ child2.setMeasureFunc(() => ({ width: 60, height: 60 }));
894
+
895
+ root.insertChild(child1, 0);
896
+ root.insertChild(child2, 1);
897
+
898
+ root.calculateLayout();
899
+
900
+ expect(child1.getComputedWidth()).toBe(50);
901
+ expect(child2.getComputedWidth()).toBe(60);
902
+
903
+ // freeRecursive should clean up all nodes and their callbacks
904
+ // The native context is cleaned up by Zig's freeContextRecursive
905
+ root.freeRecursive();
906
+ });
907
+
908
+ test("interleaved node lifecycle with callbacks", () => {
909
+ const config = Config.create();
910
+ const nodes: Node[] = [];
911
+
912
+ // Create several nodes with callbacks
913
+ for (let i = 0; i < 20; i++) {
914
+ const node = Node.create(config);
915
+ node.setMeasureFunc(() => ({ width: 10 + i, height: 10 + i }));
916
+ nodes.push(node);
917
+ }
918
+
919
+ // Free some, keep others
920
+ for (let i = 0; i < nodes.length; i += 2) {
921
+ nodes[i]!.free();
922
+ }
923
+
924
+ // Calculate remaining nodes
925
+ for (let i = 1; i < nodes.length; i += 2) {
926
+ nodes[i]!.calculateLayout();
927
+ const width = nodes[i]!.getComputedWidth();
928
+ expect(width).toBe(10 + i);
929
+ expect(Number.isNaN(width)).toBe(false);
930
+ }
931
+
932
+ // Free remaining
933
+ for (let i = 1; i < nodes.length; i += 2) {
934
+ nodes[i]!.free();
935
+ }
936
+
937
+ config.free();
938
+ });
939
+ });