@sigil-dev/runtime 0.7.5 → 0.7.7
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/easing.ts +66 -0
- package/index.test.ts +587 -40
- package/index.ts +486 -317
- package/package.json +1 -1
- package/reactivity/map.ts +98 -0
- package/reactivity/set.ts +74 -0
package/index.test.ts
CHANGED
|
@@ -5,10 +5,35 @@ import {
|
|
|
5
5
|
claimComment,
|
|
6
6
|
claimText,
|
|
7
7
|
createEffect,
|
|
8
|
+
createEffectPre,
|
|
9
|
+
createInspectEffect,
|
|
8
10
|
createMemo,
|
|
11
|
+
createProfilerEffect,
|
|
12
|
+
createRawSignal,
|
|
9
13
|
createSignal,
|
|
14
|
+
easeInCubic,
|
|
15
|
+
easeInOutCubic,
|
|
16
|
+
easeInOutQuad,
|
|
17
|
+
easeInOutQuart,
|
|
18
|
+
easeInOutQuint,
|
|
19
|
+
easeInOutSine,
|
|
20
|
+
easeInQuad,
|
|
21
|
+
easeInQuart,
|
|
22
|
+
easeInQuint,
|
|
23
|
+
easeInSine,
|
|
24
|
+
easeOutCubic,
|
|
25
|
+
easeOutQuad,
|
|
26
|
+
easeOutQuart,
|
|
27
|
+
easeOutQuint,
|
|
28
|
+
easeOutSine,
|
|
10
29
|
hydrateKeyedList,
|
|
30
|
+
linear,
|
|
31
|
+
ReactiveMap,
|
|
32
|
+
ReactiveSet,
|
|
11
33
|
reconcile,
|
|
34
|
+
snapshot,
|
|
35
|
+
tick,
|
|
36
|
+
tracking,
|
|
12
37
|
withEffectScope,
|
|
13
38
|
} from "./index.ts";
|
|
14
39
|
|
|
@@ -84,7 +109,9 @@ describe("Runtime", () => {
|
|
|
84
109
|
createEffect(() => {
|
|
85
110
|
seen = count();
|
|
86
111
|
});
|
|
87
|
-
|
|
112
|
+
batch(() => {
|
|
113
|
+
count.set(5);
|
|
114
|
+
});
|
|
88
115
|
expect(seen).toBe(5);
|
|
89
116
|
});
|
|
90
117
|
});
|
|
@@ -99,7 +126,9 @@ describe("Runtime", () => {
|
|
|
99
126
|
test("updates when dependency changes", () => {
|
|
100
127
|
const count = createSignal(2);
|
|
101
128
|
const doubled = createMemo(() => count() * 2);
|
|
102
|
-
|
|
129
|
+
batch(() => {
|
|
130
|
+
count.set(5);
|
|
131
|
+
});
|
|
103
132
|
expect(doubled()).toBe(10);
|
|
104
133
|
});
|
|
105
134
|
});
|
|
@@ -320,7 +349,8 @@ describe("Runtime", () => {
|
|
|
320
349
|
});
|
|
321
350
|
count.set(1);
|
|
322
351
|
count.set(2);
|
|
323
|
-
|
|
352
|
+
batch(() => {});
|
|
353
|
+
expect(log).toEqual(["cleanup"]);
|
|
324
354
|
});
|
|
325
355
|
|
|
326
356
|
test("disposer stops effect", () => {
|
|
@@ -330,9 +360,13 @@ describe("Runtime", () => {
|
|
|
330
360
|
count();
|
|
331
361
|
runs++;
|
|
332
362
|
});
|
|
333
|
-
|
|
363
|
+
batch(() => {
|
|
364
|
+
count.set(1);
|
|
365
|
+
});
|
|
334
366
|
stop();
|
|
335
|
-
|
|
367
|
+
batch(() => {
|
|
368
|
+
count.set(2);
|
|
369
|
+
});
|
|
336
370
|
expect(runs).toBe(2); // initial + one update, not three
|
|
337
371
|
});
|
|
338
372
|
|
|
@@ -382,7 +416,7 @@ describe("batch", () => {
|
|
|
382
416
|
expect(runs).toBe(2); // one run after outer exits
|
|
383
417
|
});
|
|
384
418
|
|
|
385
|
-
test("outside batch
|
|
419
|
+
test("outside batch auto-batches to microtask", () => {
|
|
386
420
|
const count = createSignal(0);
|
|
387
421
|
let runs = 0;
|
|
388
422
|
createEffect(() => {
|
|
@@ -392,7 +426,11 @@ describe("batch", () => {
|
|
|
392
426
|
|
|
393
427
|
count.set(1);
|
|
394
428
|
count.set(2);
|
|
395
|
-
|
|
429
|
+
// Without batch, effects are auto-batched — no synchronous run
|
|
430
|
+
expect(runs).toBe(1); // initial only
|
|
431
|
+
|
|
432
|
+
batch(() => {});
|
|
433
|
+
expect(runs).toBe(2); // initial + one batched update (last write wins)
|
|
396
434
|
});
|
|
397
435
|
});
|
|
398
436
|
|
|
@@ -409,7 +447,9 @@ describe("createSignal with objects", () => {
|
|
|
409
447
|
seen = state().name;
|
|
410
448
|
});
|
|
411
449
|
expect(seen).toBe("Alice");
|
|
412
|
-
|
|
450
|
+
batch(() => {
|
|
451
|
+
state().name = "Bob";
|
|
452
|
+
});
|
|
413
453
|
expect(seen).toBe("Bob");
|
|
414
454
|
});
|
|
415
455
|
|
|
@@ -421,9 +461,13 @@ describe("createSignal with objects", () => {
|
|
|
421
461
|
runs++;
|
|
422
462
|
});
|
|
423
463
|
expect(runs).toBe(1);
|
|
424
|
-
|
|
464
|
+
batch(() => {
|
|
465
|
+
state().age = 31;
|
|
466
|
+
});
|
|
425
467
|
expect(runs).toBe(1);
|
|
426
|
-
|
|
468
|
+
batch(() => {
|
|
469
|
+
state().name = "Bob";
|
|
470
|
+
});
|
|
427
471
|
expect(runs).toBe(2);
|
|
428
472
|
});
|
|
429
473
|
|
|
@@ -434,7 +478,9 @@ describe("createSignal with objects", () => {
|
|
|
434
478
|
seen = state().user.address.city;
|
|
435
479
|
});
|
|
436
480
|
expect(seen).toBe("NYC");
|
|
437
|
-
|
|
481
|
+
batch(() => {
|
|
482
|
+
state().user.address.city = "LA";
|
|
483
|
+
});
|
|
438
484
|
expect(seen).toBe("LA");
|
|
439
485
|
});
|
|
440
486
|
|
|
@@ -449,7 +495,9 @@ describe("createSignal with objects", () => {
|
|
|
449
495
|
seen = state().address.city;
|
|
450
496
|
});
|
|
451
497
|
expect(seen).toBe("Agartha");
|
|
452
|
-
|
|
498
|
+
batch(() => {
|
|
499
|
+
state().address.city = "Hyperborea";
|
|
500
|
+
});
|
|
453
501
|
expect(seen).toBe("Hyperborea");
|
|
454
502
|
});
|
|
455
503
|
|
|
@@ -460,7 +508,9 @@ describe("createSignal with objects", () => {
|
|
|
460
508
|
seen = state().name;
|
|
461
509
|
});
|
|
462
510
|
expect(seen).toBe("Alice");
|
|
463
|
-
|
|
511
|
+
batch(() => {
|
|
512
|
+
state.set({ name: "Charlie", age: 25 });
|
|
513
|
+
});
|
|
464
514
|
expect(seen).toBe("Charlie");
|
|
465
515
|
});
|
|
466
516
|
|
|
@@ -472,7 +522,9 @@ describe("createSignal with objects", () => {
|
|
|
472
522
|
runs++;
|
|
473
523
|
});
|
|
474
524
|
expect(runs).toBe(1);
|
|
475
|
-
|
|
525
|
+
batch(() => {
|
|
526
|
+
state().name = "Bob";
|
|
527
|
+
});
|
|
476
528
|
expect(runs).toBe(1);
|
|
477
529
|
});
|
|
478
530
|
|
|
@@ -501,7 +553,9 @@ describe("createSignal with arrays", () => {
|
|
|
501
553
|
len = items().length;
|
|
502
554
|
});
|
|
503
555
|
expect(len).toBe(3);
|
|
504
|
-
|
|
556
|
+
batch(() => {
|
|
557
|
+
items().push(4);
|
|
558
|
+
});
|
|
505
559
|
expect(len).toBe(4);
|
|
506
560
|
});
|
|
507
561
|
|
|
@@ -513,7 +567,9 @@ describe("createSignal with arrays", () => {
|
|
|
513
567
|
first = items()[0];
|
|
514
568
|
});
|
|
515
569
|
expect(first).toBe(1);
|
|
516
|
-
|
|
570
|
+
batch(() => {
|
|
571
|
+
items()[0] = 99;
|
|
572
|
+
});
|
|
517
573
|
expect(first).toBe(99);
|
|
518
574
|
});
|
|
519
575
|
|
|
@@ -524,9 +580,13 @@ describe("createSignal with arrays", () => {
|
|
|
524
580
|
seen = [...items()];
|
|
525
581
|
});
|
|
526
582
|
expect(seen).toEqual([]);
|
|
527
|
-
|
|
583
|
+
batch(() => {
|
|
584
|
+
items().push(1);
|
|
585
|
+
});
|
|
528
586
|
expect(seen).toEqual([1]);
|
|
529
|
-
|
|
587
|
+
batch(() => {
|
|
588
|
+
items().push(2);
|
|
589
|
+
});
|
|
530
590
|
expect(seen).toEqual([1, 2]);
|
|
531
591
|
});
|
|
532
592
|
|
|
@@ -537,7 +597,9 @@ describe("createSignal with arrays", () => {
|
|
|
537
597
|
len = items().length;
|
|
538
598
|
});
|
|
539
599
|
expect(len).toBe(3);
|
|
540
|
-
|
|
600
|
+
batch(() => {
|
|
601
|
+
items().pop();
|
|
602
|
+
});
|
|
541
603
|
expect(len).toBe(2);
|
|
542
604
|
});
|
|
543
605
|
|
|
@@ -547,7 +609,9 @@ describe("createSignal with arrays", () => {
|
|
|
547
609
|
createEffect(() => {
|
|
548
610
|
seen = [...items()];
|
|
549
611
|
});
|
|
550
|
-
|
|
612
|
+
batch(() => {
|
|
613
|
+
items().splice(1, 2);
|
|
614
|
+
});
|
|
551
615
|
expect(seen).toEqual([1, 4]);
|
|
552
616
|
});
|
|
553
617
|
|
|
@@ -555,7 +619,9 @@ describe("createSignal with arrays", () => {
|
|
|
555
619
|
const items = createSignal([1, 2, 3]);
|
|
556
620
|
const doubled = createMemo(() => items().map((x) => x * 2));
|
|
557
621
|
expect(doubled()).toEqual([2, 4, 6]);
|
|
558
|
-
|
|
622
|
+
batch(() => {
|
|
623
|
+
items().push(4);
|
|
624
|
+
});
|
|
559
625
|
expect(doubled()).toEqual([2, 4, 6, 8]);
|
|
560
626
|
});
|
|
561
627
|
});
|
|
@@ -574,7 +640,9 @@ describe("proxy edge cases", () => {
|
|
|
574
640
|
seen = [...state().items];
|
|
575
641
|
});
|
|
576
642
|
expect(seen).toEqual([1, 2, 3]);
|
|
577
|
-
|
|
643
|
+
batch(() => {
|
|
644
|
+
state().items.push(4);
|
|
645
|
+
});
|
|
578
646
|
expect(seen).toEqual([1, 2, 3, 4]);
|
|
579
647
|
});
|
|
580
648
|
|
|
@@ -595,13 +663,17 @@ describe("proxy edge cases", () => {
|
|
|
595
663
|
expect(xRuns).toBe(1);
|
|
596
664
|
expect(yRuns).toBe(1);
|
|
597
665
|
|
|
598
|
-
|
|
666
|
+
batch(() => {
|
|
667
|
+
state().x = 5;
|
|
668
|
+
});
|
|
599
669
|
expect(xSeen).toBe(5);
|
|
600
670
|
expect(ySeen).toBe(0);
|
|
601
671
|
expect(xRuns).toBe(2);
|
|
602
672
|
expect(yRuns).toBe(1);
|
|
603
673
|
|
|
604
|
-
|
|
674
|
+
batch(() => {
|
|
675
|
+
state().y = 10;
|
|
676
|
+
});
|
|
605
677
|
expect(xSeen).toBe(5);
|
|
606
678
|
expect(ySeen).toBe(10);
|
|
607
679
|
expect(xRuns).toBe(2);
|
|
@@ -771,11 +843,15 @@ describe("Scary reactivity", () => {
|
|
|
771
843
|
expect(runs).toBe(1);
|
|
772
844
|
|
|
773
845
|
// flip condition, effect should now track b, not a
|
|
774
|
-
|
|
846
|
+
batch(() => {
|
|
847
|
+
condition.set(false);
|
|
848
|
+
});
|
|
775
849
|
expect(runs).toBe(2);
|
|
776
850
|
|
|
777
851
|
// updating a should NOT trigger the effect anymore
|
|
778
|
-
|
|
852
|
+
batch(() => {
|
|
853
|
+
a.set(99);
|
|
854
|
+
});
|
|
779
855
|
expect(runs).toBe(2); // ← this will fail with current implementation
|
|
780
856
|
});
|
|
781
857
|
});
|
|
@@ -791,7 +867,9 @@ describe("withEffectScope", () => {
|
|
|
791
867
|
});
|
|
792
868
|
});
|
|
793
869
|
expect(runs).toBe(1);
|
|
794
|
-
|
|
870
|
+
batch(() => {
|
|
871
|
+
count.set(1);
|
|
872
|
+
});
|
|
795
873
|
expect(runs).toBe(2);
|
|
796
874
|
});
|
|
797
875
|
|
|
@@ -805,12 +883,16 @@ describe("withEffectScope", () => {
|
|
|
805
883
|
});
|
|
806
884
|
});
|
|
807
885
|
expect(runs).toBe(1);
|
|
808
|
-
|
|
886
|
+
batch(() => {
|
|
887
|
+
count.set(1);
|
|
888
|
+
});
|
|
809
889
|
expect(runs).toBe(2);
|
|
810
890
|
|
|
811
891
|
dispose();
|
|
812
892
|
|
|
813
|
-
|
|
893
|
+
batch(() => {
|
|
894
|
+
count.set(2);
|
|
895
|
+
});
|
|
814
896
|
expect(runs).toBe(2); // stopped — no further runs
|
|
815
897
|
});
|
|
816
898
|
|
|
@@ -829,15 +911,19 @@ describe("withEffectScope", () => {
|
|
|
829
911
|
runsB++;
|
|
830
912
|
});
|
|
831
913
|
});
|
|
832
|
-
|
|
833
|
-
|
|
914
|
+
batch(() => {
|
|
915
|
+
a.set(1);
|
|
916
|
+
b.set(1);
|
|
917
|
+
});
|
|
834
918
|
expect(runsA).toBe(2);
|
|
835
919
|
expect(runsB).toBe(2);
|
|
836
920
|
|
|
837
921
|
dispose();
|
|
838
922
|
|
|
839
|
-
|
|
840
|
-
|
|
923
|
+
batch(() => {
|
|
924
|
+
a.set(2);
|
|
925
|
+
b.set(2);
|
|
926
|
+
});
|
|
841
927
|
expect(runsA).toBe(2);
|
|
842
928
|
expect(runsB).toBe(2);
|
|
843
929
|
});
|
|
@@ -859,13 +945,17 @@ describe("withEffectScope", () => {
|
|
|
859
945
|
});
|
|
860
946
|
});
|
|
861
947
|
|
|
862
|
-
|
|
948
|
+
batch(() => {
|
|
949
|
+
count.set(1);
|
|
950
|
+
});
|
|
863
951
|
expect(outsideRuns).toBe(2);
|
|
864
952
|
expect(insideRuns).toBe(2);
|
|
865
953
|
|
|
866
954
|
dispose();
|
|
867
955
|
|
|
868
|
-
|
|
956
|
+
batch(() => {
|
|
957
|
+
count.set(2);
|
|
958
|
+
});
|
|
869
959
|
expect(outsideRuns).toBe(3); // still running
|
|
870
960
|
expect(insideRuns).toBe(2); // stopped
|
|
871
961
|
});
|
|
@@ -890,12 +980,16 @@ describe("withEffectScope", () => {
|
|
|
890
980
|
disposeInner();
|
|
891
981
|
});
|
|
892
982
|
|
|
893
|
-
|
|
983
|
+
batch(() => {
|
|
984
|
+
count.set(1);
|
|
985
|
+
});
|
|
894
986
|
expect(outerRuns).toBe(2);
|
|
895
987
|
expect(innerRuns).toBe(1); // stopped on inner dispose
|
|
896
988
|
|
|
897
989
|
disposeOuter();
|
|
898
|
-
|
|
990
|
+
batch(() => {
|
|
991
|
+
count.set(2);
|
|
992
|
+
});
|
|
899
993
|
expect(outerRuns).toBe(2); // stopped on outer dispose
|
|
900
994
|
});
|
|
901
995
|
|
|
@@ -911,11 +1005,15 @@ describe("withEffectScope", () => {
|
|
|
911
1005
|
expect(doubled()).toBe(0);
|
|
912
1006
|
});
|
|
913
1007
|
expect(memoRuns).toBe(1);
|
|
914
|
-
|
|
1008
|
+
batch(() => {
|
|
1009
|
+
count.set(5);
|
|
1010
|
+
});
|
|
915
1011
|
expect(memoRuns).toBe(2);
|
|
916
1012
|
|
|
917
1013
|
dispose();
|
|
918
|
-
|
|
1014
|
+
batch(() => {
|
|
1015
|
+
count.set(10);
|
|
1016
|
+
});
|
|
919
1017
|
expect(memoRuns).toBe(2); // memo's internal effect stopped
|
|
920
1018
|
});
|
|
921
1019
|
|
|
@@ -930,7 +1028,9 @@ describe("withEffectScope", () => {
|
|
|
930
1028
|
});
|
|
931
1029
|
dispose();
|
|
932
1030
|
dispose(); // second call must not throw or double-fire
|
|
933
|
-
|
|
1031
|
+
batch(() => {
|
|
1032
|
+
count.set(1);
|
|
1033
|
+
});
|
|
934
1034
|
expect(runs).toBe(1);
|
|
935
1035
|
});
|
|
936
1036
|
});
|
|
@@ -1063,3 +1163,450 @@ describe("hydrateKeyedList", () => {
|
|
|
1063
1163
|
expect(result.keyMap.size).toBe(0);
|
|
1064
1164
|
});
|
|
1065
1165
|
});
|
|
1166
|
+
|
|
1167
|
+
describe("A1 createRawSignal", () => {
|
|
1168
|
+
test("skips proxy for objects", () => {
|
|
1169
|
+
const state = createRawSignal({ name: "Alice" });
|
|
1170
|
+
expect(state()).toEqual({ name: "Alice" });
|
|
1171
|
+
// Raw signal — direct object, not proxied
|
|
1172
|
+
const obj = state();
|
|
1173
|
+
state.set({ name: "Bob" });
|
|
1174
|
+
expect(state()).toEqual({ name: "Bob" });
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
test("works with primitives", () => {
|
|
1178
|
+
const count = createRawSignal(0);
|
|
1179
|
+
expect(count()).toBe(0);
|
|
1180
|
+
count.set(5);
|
|
1181
|
+
expect(count()).toBe(5);
|
|
1182
|
+
});
|
|
1183
|
+
|
|
1184
|
+
test("is reactive", () => {
|
|
1185
|
+
const count = createRawSignal(0);
|
|
1186
|
+
let runs = 0;
|
|
1187
|
+
createEffect(() => {
|
|
1188
|
+
count();
|
|
1189
|
+
runs++;
|
|
1190
|
+
});
|
|
1191
|
+
expect(runs).toBe(1);
|
|
1192
|
+
batch(() => {
|
|
1193
|
+
count.set(1);
|
|
1194
|
+
});
|
|
1195
|
+
expect(runs).toBe(2);
|
|
1196
|
+
});
|
|
1197
|
+
|
|
1198
|
+
test(".peek() reads without tracking", () => {
|
|
1199
|
+
const count = createRawSignal(0);
|
|
1200
|
+
let runs = 0;
|
|
1201
|
+
createEffect(() => {
|
|
1202
|
+
count.peek();
|
|
1203
|
+
runs++;
|
|
1204
|
+
});
|
|
1205
|
+
expect(runs).toBe(1);
|
|
1206
|
+
batch(() => {
|
|
1207
|
+
count.set(1);
|
|
1208
|
+
});
|
|
1209
|
+
expect(runs).toBe(1);
|
|
1210
|
+
});
|
|
1211
|
+
});
|
|
1212
|
+
|
|
1213
|
+
describe("A2 snapshot", () => {
|
|
1214
|
+
test("returns primitive values as-is", () => {
|
|
1215
|
+
expect(snapshot(42)).toBe(42);
|
|
1216
|
+
expect(snapshot("hello")).toBe("hello");
|
|
1217
|
+
expect(snapshot(null)).toBe(null);
|
|
1218
|
+
expect(snapshot(undefined)).toBe(undefined);
|
|
1219
|
+
});
|
|
1220
|
+
|
|
1221
|
+
test("unwraps proxy to plain object", () => {
|
|
1222
|
+
const state = createSignal({ name: "Alice", age: 30 });
|
|
1223
|
+
const snap = snapshot(state());
|
|
1224
|
+
expect(snap).toEqual({ name: "Alice", age: 30 });
|
|
1225
|
+
expect(snap).not.toBe(state()); // distinct reference
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1228
|
+
test("unwraps proxy to plain array", () => {
|
|
1229
|
+
const items = createSignal([1, 2, 3]);
|
|
1230
|
+
const snap = snapshot(items());
|
|
1231
|
+
expect(snap).toEqual([1, 2, 3]);
|
|
1232
|
+
expect(Array.isArray(snap)).toBe(true);
|
|
1233
|
+
expect(snap).not.toBe(items()); // distinct reference
|
|
1234
|
+
});
|
|
1235
|
+
|
|
1236
|
+
test("non-proxy objects are returned as-is", () => {
|
|
1237
|
+
const obj = { plain: true };
|
|
1238
|
+
expect(snapshot(obj)).toBe(obj);
|
|
1239
|
+
});
|
|
1240
|
+
|
|
1241
|
+
test("deeply nested snapshot is fully un-proxied", () => {
|
|
1242
|
+
const obj = createSignal({ a: { b: { c: 1 } } });
|
|
1243
|
+
const s = snapshot(obj());
|
|
1244
|
+
expect(s.a.b.c).toBe(1);
|
|
1245
|
+
// s.a.b should NOT be a proxy — it's a plain object from the deep copy
|
|
1246
|
+
const proxyTargetCheck = (val: object) =>
|
|
1247
|
+
typeof val === "object" &&
|
|
1248
|
+
val !== null &&
|
|
1249
|
+
!Object.getPrototypeOf(val)?.constructor?.name?.includes("Map");
|
|
1250
|
+
// Verify no nested proxy: accessing s.a.b.c should not trigger tracking
|
|
1251
|
+
let tracked = false;
|
|
1252
|
+
const prev = (globalThis as any).__sigil_tracking_test;
|
|
1253
|
+
(globalThis as any).__sigil_tracking_test = () => (tracked = true);
|
|
1254
|
+
const raw = s.a.b;
|
|
1255
|
+
// No reactive tracking should happen on plain objects
|
|
1256
|
+
expect(raw.c).toBe(1);
|
|
1257
|
+
// Deep compare confirms it's a full independent copy
|
|
1258
|
+
expect(s).toEqual({ a: { b: { c: 1 } } });
|
|
1259
|
+
expect(s.a).not.toBe(obj().a);
|
|
1260
|
+
expect(s.a.b).not.toBe(obj().a.b);
|
|
1261
|
+
});
|
|
1262
|
+
});
|
|
1263
|
+
|
|
1264
|
+
describe("A3 tracking", () => {
|
|
1265
|
+
test("returns false outside effect", () => {
|
|
1266
|
+
expect(tracking()).toBe(false);
|
|
1267
|
+
});
|
|
1268
|
+
|
|
1269
|
+
test("returns true inside effect", () => {
|
|
1270
|
+
let inside = false;
|
|
1271
|
+
createEffect(() => {
|
|
1272
|
+
inside = tracking();
|
|
1273
|
+
});
|
|
1274
|
+
expect(inside).toBe(true);
|
|
1275
|
+
});
|
|
1276
|
+
|
|
1277
|
+
test("returns false after effect completes", () => {
|
|
1278
|
+
createEffect(() => {
|
|
1279
|
+
tracking();
|
|
1280
|
+
});
|
|
1281
|
+
expect(tracking()).toBe(false);
|
|
1282
|
+
});
|
|
1283
|
+
});
|
|
1284
|
+
|
|
1285
|
+
describe("A4 createInspectEffect", () => {
|
|
1286
|
+
test("does not throw", () => {
|
|
1287
|
+
const count = createSignal(0);
|
|
1288
|
+
expect(() => {
|
|
1289
|
+
createInspectEffect(() => [count()]);
|
|
1290
|
+
}).not.toThrow();
|
|
1291
|
+
});
|
|
1292
|
+
|
|
1293
|
+
test("accepts custom callback", () => {
|
|
1294
|
+
const count = createSignal(0);
|
|
1295
|
+
const logged: unknown[][] = [];
|
|
1296
|
+
createInspectEffect(
|
|
1297
|
+
() => [count()],
|
|
1298
|
+
(vals) => logged.push(vals),
|
|
1299
|
+
);
|
|
1300
|
+
expect(logged.length).toBe(1);
|
|
1301
|
+
expect(logged[0]).toEqual([0]);
|
|
1302
|
+
});
|
|
1303
|
+
});
|
|
1304
|
+
|
|
1305
|
+
describe("A5 createEffectPre", () => {
|
|
1306
|
+
test("runs immediately like createEffect", () => {
|
|
1307
|
+
const count = createSignal(0);
|
|
1308
|
+
let seen = -1;
|
|
1309
|
+
createEffectPre(() => {
|
|
1310
|
+
seen = count();
|
|
1311
|
+
});
|
|
1312
|
+
expect(seen).toBe(0);
|
|
1313
|
+
});
|
|
1314
|
+
|
|
1315
|
+
test("reruns on signal change", () => {
|
|
1316
|
+
const count = createSignal(0);
|
|
1317
|
+
let seen = -1;
|
|
1318
|
+
createEffectPre(() => {
|
|
1319
|
+
seen = count();
|
|
1320
|
+
});
|
|
1321
|
+
batch(() => {
|
|
1322
|
+
count.set(5);
|
|
1323
|
+
});
|
|
1324
|
+
expect(seen).toBe(5);
|
|
1325
|
+
});
|
|
1326
|
+
});
|
|
1327
|
+
|
|
1328
|
+
describe("A7 ReactiveMap", () => {
|
|
1329
|
+
test("basic set/get", () => {
|
|
1330
|
+
const map = new ReactiveMap<string, number>();
|
|
1331
|
+
map.set("a", 1);
|
|
1332
|
+
expect(map.get("a")).toBe(1);
|
|
1333
|
+
});
|
|
1334
|
+
|
|
1335
|
+
test("reactivity: get tracks reads", () => {
|
|
1336
|
+
const map = new ReactiveMap<string, number>();
|
|
1337
|
+
let size = 0;
|
|
1338
|
+
createEffect(() => {
|
|
1339
|
+
size = map.size;
|
|
1340
|
+
});
|
|
1341
|
+
expect(size).toBe(0);
|
|
1342
|
+
batch(() => {
|
|
1343
|
+
map.set("x", 1);
|
|
1344
|
+
});
|
|
1345
|
+
expect(size).toBe(1);
|
|
1346
|
+
});
|
|
1347
|
+
|
|
1348
|
+
test("deleted key triggers reactivity", () => {
|
|
1349
|
+
const map = new ReactiveMap<string, number>();
|
|
1350
|
+
map.set("a", 1);
|
|
1351
|
+
let accessed: number | undefined;
|
|
1352
|
+
createEffect(() => {
|
|
1353
|
+
accessed = map.get("a");
|
|
1354
|
+
});
|
|
1355
|
+
expect(accessed).toBe(1);
|
|
1356
|
+
batch(() => {
|
|
1357
|
+
map.delete("a");
|
|
1358
|
+
});
|
|
1359
|
+
expect(accessed).toBe(undefined);
|
|
1360
|
+
});
|
|
1361
|
+
|
|
1362
|
+
test("clear triggers reactivity", () => {
|
|
1363
|
+
const map = new ReactiveMap<string, number>();
|
|
1364
|
+
map.set("a", 1);
|
|
1365
|
+
let size = 0;
|
|
1366
|
+
createEffect(() => {
|
|
1367
|
+
size = map.size;
|
|
1368
|
+
});
|
|
1369
|
+
batch(() => {
|
|
1370
|
+
map.clear();
|
|
1371
|
+
});
|
|
1372
|
+
expect(size).toBe(0);
|
|
1373
|
+
});
|
|
1374
|
+
|
|
1375
|
+
test("forEach tracks reads", () => {
|
|
1376
|
+
const map = new ReactiveMap<string, number>();
|
|
1377
|
+
map.set("a", 1);
|
|
1378
|
+
let count = 0;
|
|
1379
|
+
createEffect(() => {
|
|
1380
|
+
count = 0;
|
|
1381
|
+
map.forEach(() => count++);
|
|
1382
|
+
});
|
|
1383
|
+
expect(count).toBe(1);
|
|
1384
|
+
batch(() => {
|
|
1385
|
+
map.set("b", 2);
|
|
1386
|
+
});
|
|
1387
|
+
expect(count).toBe(2);
|
|
1388
|
+
});
|
|
1389
|
+
|
|
1390
|
+
test("per-key granularity: setting key X does not re-trigger effect reading key Y", () => {
|
|
1391
|
+
const map = new ReactiveMap<string, number>();
|
|
1392
|
+
map.set("a", 1);
|
|
1393
|
+
map.set("b", 2);
|
|
1394
|
+
let aRuns = 0;
|
|
1395
|
+
let bRuns = 0;
|
|
1396
|
+
createEffect(() => {
|
|
1397
|
+
map.get("a");
|
|
1398
|
+
aRuns++;
|
|
1399
|
+
});
|
|
1400
|
+
createEffect(() => {
|
|
1401
|
+
map.get("b");
|
|
1402
|
+
bRuns++;
|
|
1403
|
+
});
|
|
1404
|
+
expect(aRuns).toBe(1);
|
|
1405
|
+
expect(bRuns).toBe(1);
|
|
1406
|
+
|
|
1407
|
+
batch(() => {
|
|
1408
|
+
map.set("a", 10); // only a-effect should rerun
|
|
1409
|
+
});
|
|
1410
|
+
expect(aRuns).toBe(2);
|
|
1411
|
+
expect(bRuns).toBe(1);
|
|
1412
|
+
|
|
1413
|
+
batch(() => {
|
|
1414
|
+
map.set("b", 20); // only b-effect should rerun
|
|
1415
|
+
});
|
|
1416
|
+
expect(aRuns).toBe(2);
|
|
1417
|
+
expect(bRuns).toBe(2);
|
|
1418
|
+
});
|
|
1419
|
+
|
|
1420
|
+
test("per-key: adding new key triggers struct but not existing key effects", () => {
|
|
1421
|
+
const map = new ReactiveMap<string, number>();
|
|
1422
|
+
map.set("a", 1);
|
|
1423
|
+
let aRuns = 0;
|
|
1424
|
+
let sizeRuns = 0;
|
|
1425
|
+
createEffect(() => {
|
|
1426
|
+
map.get("a");
|
|
1427
|
+
aRuns++;
|
|
1428
|
+
});
|
|
1429
|
+
createEffect(() => {
|
|
1430
|
+
map.size;
|
|
1431
|
+
sizeRuns++;
|
|
1432
|
+
});
|
|
1433
|
+
expect(aRuns).toBe(1);
|
|
1434
|
+
expect(sizeRuns).toBe(1);
|
|
1435
|
+
|
|
1436
|
+
batch(() => {
|
|
1437
|
+
map.set("b", 2); // new key — struct changes, a's value doesn't
|
|
1438
|
+
});
|
|
1439
|
+
expect(aRuns).toBe(1);
|
|
1440
|
+
expect(sizeRuns).toBe(2);
|
|
1441
|
+
});
|
|
1442
|
+
});
|
|
1443
|
+
|
|
1444
|
+
describe("A7 ReactiveSet", () => {
|
|
1445
|
+
test("basic add/has", () => {
|
|
1446
|
+
const set = new ReactiveSet<number>();
|
|
1447
|
+
set.add(1);
|
|
1448
|
+
expect(set.has(1)).toBe(true);
|
|
1449
|
+
});
|
|
1450
|
+
|
|
1451
|
+
test("reactivity: has tracks reads", () => {
|
|
1452
|
+
const set = new ReactiveSet<number>();
|
|
1453
|
+
let hasIt = false;
|
|
1454
|
+
createEffect(() => {
|
|
1455
|
+
hasIt = set.has(1);
|
|
1456
|
+
});
|
|
1457
|
+
expect(hasIt).toBe(false);
|
|
1458
|
+
batch(() => {
|
|
1459
|
+
set.add(1);
|
|
1460
|
+
});
|
|
1461
|
+
expect(hasIt).toBe(true);
|
|
1462
|
+
});
|
|
1463
|
+
|
|
1464
|
+
test("delete triggers reactivity", () => {
|
|
1465
|
+
const set = new ReactiveSet<number>();
|
|
1466
|
+
set.add(1);
|
|
1467
|
+
let size = 0;
|
|
1468
|
+
createEffect(() => {
|
|
1469
|
+
size = set.size;
|
|
1470
|
+
});
|
|
1471
|
+
expect(size).toBe(1);
|
|
1472
|
+
batch(() => {
|
|
1473
|
+
set.delete(1);
|
|
1474
|
+
});
|
|
1475
|
+
expect(size).toBe(0);
|
|
1476
|
+
});
|
|
1477
|
+
|
|
1478
|
+
test("clear triggers reactivity", () => {
|
|
1479
|
+
const set = new ReactiveSet<number>();
|
|
1480
|
+
set.add(1);
|
|
1481
|
+
let size = 0;
|
|
1482
|
+
createEffect(() => {
|
|
1483
|
+
size = set.size;
|
|
1484
|
+
});
|
|
1485
|
+
batch(() => {
|
|
1486
|
+
set.clear();
|
|
1487
|
+
});
|
|
1488
|
+
expect(size).toBe(0);
|
|
1489
|
+
});
|
|
1490
|
+
|
|
1491
|
+
test("forEach tracks reads", () => {
|
|
1492
|
+
const set = new ReactiveSet<string>();
|
|
1493
|
+
set.add("a");
|
|
1494
|
+
let count = 0;
|
|
1495
|
+
createEffect(() => {
|
|
1496
|
+
count = 0;
|
|
1497
|
+
set.forEach(() => count++);
|
|
1498
|
+
});
|
|
1499
|
+
expect(count).toBe(1);
|
|
1500
|
+
batch(() => {
|
|
1501
|
+
set.add("b");
|
|
1502
|
+
});
|
|
1503
|
+
expect(count).toBe(2);
|
|
1504
|
+
});
|
|
1505
|
+
});
|
|
1506
|
+
|
|
1507
|
+
describe("A8 easing functions", () => {
|
|
1508
|
+
test("linear is identity", () => {
|
|
1509
|
+
expect(linear(0)).toBe(0);
|
|
1510
|
+
expect(linear(0.5)).toBe(0.5);
|
|
1511
|
+
expect(linear(1)).toBe(1);
|
|
1512
|
+
});
|
|
1513
|
+
|
|
1514
|
+
test("easeInQuad is quadratic", () => {
|
|
1515
|
+
expect(easeInQuad(0)).toBe(0);
|
|
1516
|
+
expect(easeInQuad(0.5)).toBe(0.25);
|
|
1517
|
+
expect(easeInQuad(1)).toBe(1);
|
|
1518
|
+
});
|
|
1519
|
+
|
|
1520
|
+
test("easeOutQuad completes at 1", () => {
|
|
1521
|
+
expect(easeOutQuad(0)).toBe(0);
|
|
1522
|
+
expect(easeOutQuad(1)).toBe(1);
|
|
1523
|
+
});
|
|
1524
|
+
|
|
1525
|
+
test("all easing functions return 0 at t=0 and 1 at t=1", () => {
|
|
1526
|
+
const fns = [
|
|
1527
|
+
easeInQuad,
|
|
1528
|
+
easeOutQuad,
|
|
1529
|
+
easeInOutQuad,
|
|
1530
|
+
easeInCubic,
|
|
1531
|
+
easeOutCubic,
|
|
1532
|
+
easeInOutCubic,
|
|
1533
|
+
easeInQuart,
|
|
1534
|
+
easeOutQuart,
|
|
1535
|
+
easeInOutQuart,
|
|
1536
|
+
easeInQuint,
|
|
1537
|
+
easeOutQuint,
|
|
1538
|
+
easeInOutQuint,
|
|
1539
|
+
easeInSine,
|
|
1540
|
+
easeOutSine,
|
|
1541
|
+
easeInOutSine,
|
|
1542
|
+
];
|
|
1543
|
+
for (const fn of fns) {
|
|
1544
|
+
expect(fn(0)).toBeCloseTo(0, 10);
|
|
1545
|
+
expect(fn(1)).toBeCloseTo(1, 10);
|
|
1546
|
+
}
|
|
1547
|
+
});
|
|
1548
|
+
});
|
|
1549
|
+
|
|
1550
|
+
describe("A9 createProfilerEffect", () => {
|
|
1551
|
+
test("runs the effect", () => {
|
|
1552
|
+
let ran = false;
|
|
1553
|
+
createProfilerEffect("test", () => {
|
|
1554
|
+
ran = true;
|
|
1555
|
+
});
|
|
1556
|
+
expect(ran).toBe(true);
|
|
1557
|
+
});
|
|
1558
|
+
|
|
1559
|
+
test("reacts to signal changes", () => {
|
|
1560
|
+
const count = createSignal(0);
|
|
1561
|
+
let seen = -1;
|
|
1562
|
+
createProfilerEffect("test", () => {
|
|
1563
|
+
seen = count();
|
|
1564
|
+
});
|
|
1565
|
+
expect(seen).toBe(0);
|
|
1566
|
+
batch(() => {
|
|
1567
|
+
count.set(5);
|
|
1568
|
+
});
|
|
1569
|
+
expect(seen).toBe(5);
|
|
1570
|
+
});
|
|
1571
|
+
});
|
|
1572
|
+
|
|
1573
|
+
describe("Array reactivity", () => {
|
|
1574
|
+
test("push on $state array triggers reactivity", async () => {
|
|
1575
|
+
const items = createSignal([] as string[]);
|
|
1576
|
+
let count = 0;
|
|
1577
|
+
createEffect(() => {
|
|
1578
|
+
items(); // now auto-tracks length
|
|
1579
|
+
count++;
|
|
1580
|
+
});
|
|
1581
|
+
await tick();
|
|
1582
|
+
expect(count).toBe(1); // initial run
|
|
1583
|
+
items().push("a");
|
|
1584
|
+
await tick();
|
|
1585
|
+
expect(count).toBe(2); // re-ran after push
|
|
1586
|
+
expect(items().length).toBe(1);
|
|
1587
|
+
});
|
|
1588
|
+
|
|
1589
|
+
test("pop on $state array triggers reactivity", async () => {
|
|
1590
|
+
const items = createSignal(["a", "b"] as string[]);
|
|
1591
|
+
let len = 0;
|
|
1592
|
+
createEffect(() => {
|
|
1593
|
+
len = items().length; // reads length, subscribes
|
|
1594
|
+
});
|
|
1595
|
+
await tick();
|
|
1596
|
+
expect(len).toBe(2);
|
|
1597
|
+
items().pop();
|
|
1598
|
+
await tick();
|
|
1599
|
+
expect(len).toBe(1);
|
|
1600
|
+
});
|
|
1601
|
+
|
|
1602
|
+
test("splice on $state array triggers reactivity", async () => {
|
|
1603
|
+
const items = createSignal(["a", "b", "c"] as string[]);
|
|
1604
|
+
let len = 0;
|
|
1605
|
+
createEffect(() => { len = items().length; });
|
|
1606
|
+
await tick();
|
|
1607
|
+
expect(len).toBe(3);
|
|
1608
|
+
items().splice(1, 1);
|
|
1609
|
+
await tick();
|
|
1610
|
+
expect(len).toBe(2);
|
|
1611
|
+
});
|
|
1612
|
+
})
|