@ryupold/vode 1.8.8 → 1.8.11

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.
@@ -1,16 +1,16 @@
1
- import { app, createState, memo } from "../src/vode"
1
+ import { app, Component, ContainerNode, createState, memo } from "../src/vode"
2
2
  import { ARTICLE, ASIDE, DIV, INPUT, MAIN, NAV, P, SECTION, SPAN } from "../src/vode-tags";
3
- import { expect } from "./helper";
3
+ import { expect, ExpectationError } from "./helper";
4
4
 
5
5
  function setup() {
6
6
  const root = document.createElement("div");
7
7
  const container = document.createElement("div");
8
8
  root.appendChild(container);
9
- return container;
9
+ return container as unknown as ContainerNode;
10
10
  }
11
11
 
12
12
  export default {
13
- "onMount(): called when node is attached to the DOM": () => {
13
+ "onMount(): called when node is attached to the DOM": async () => {
14
14
  const container = setup();
15
15
  let mountCalled = false;
16
16
  app(container, {}, () =>
@@ -18,7 +18,7 @@ export default {
18
18
  [ARTICLE,
19
19
  {
20
20
  onMount: (s: unknown, ele: HTMLElement) => {
21
- expect(ele.tagName).toEqual("ARTICLE");
21
+ if (ele.tagName !== "ARTICLE") throw new ExpectationError(expect(ele), `Expected ARTICLE, got ${ele.tagName}`);
22
22
  mountCalled = true;
23
23
  }
24
24
  },
@@ -27,11 +27,11 @@ export default {
27
27
  ]
28
28
  );
29
29
 
30
- expect(mountCalled)
30
+ await expect(mountCalled)
31
31
  .toEqual(true);
32
32
  },
33
33
 
34
- "onMount(): called in order of child nodes first, then parent onMounts": () => {
34
+ "onMount(): called in order of child nodes first, then parent onMounts": async () => {
35
35
  const container = setup();
36
36
  const mounts: string[] = [];
37
37
  app(container, {}, () =>
@@ -55,11 +55,11 @@ export default {
55
55
  ]
56
56
  );
57
57
 
58
- expect(mounts)
58
+ await expect(mounts)
59
59
  .toEqual(["mount inner", "mount outer"]);
60
60
  },
61
61
 
62
- "onMount(): deep nesting 4+ levels with onMount at each level": () => {
62
+ "onMount(): deep nesting 4+ levels with onMount at each level": async () => {
63
63
  const container = setup();
64
64
  const mounts: string[] = [];
65
65
  app(container, {}, () =>
@@ -96,7 +96,7 @@ export default {
96
96
  ]
97
97
  );
98
98
 
99
- expect(mounts)
99
+ await expect(mounts)
100
100
  .toEqual([
101
101
  "mount article",
102
102
  "mount section",
@@ -105,7 +105,7 @@ export default {
105
105
  ]);
106
106
  },
107
107
 
108
- "onMount(): multiple siblings with onMount on initial render": () => {
108
+ "onMount(): multiple siblings with onMount on initial render": async () => {
109
109
  const container = setup();
110
110
  const mounts: string[] = [];
111
111
  app(container, {}, () =>
@@ -129,11 +129,11 @@ export default {
129
129
  ]
130
130
  );
131
131
 
132
- expect(mounts)
132
+ await expect(mounts)
133
133
  .toEqual(["mount p", "mount span"]);
134
134
  },
135
135
 
136
- "onMount(): A->A path - onMount added during update does NOT fire": () => {
136
+ "onMount(): A->A path - onMount added during update does NOT fire": async () => {
137
137
  const container = setup();
138
138
  const mounts: string[] = [];
139
139
  const state = createState({ addMount: false });
@@ -150,12 +150,12 @@ export default {
150
150
  ]
151
151
  );
152
152
 
153
- expect(mounts).toEqual([]);
153
+ await expect(mounts).toEqual([]);
154
154
  patch({ addMount: true });
155
- expect(mounts).toEqual([]);
155
+ await expect(mounts).toEqual([]);
156
156
  },
157
157
 
158
- "onMount(): A->A path - onMount removed during update does not cause issues": () => {
158
+ "onMount(): A->A path - onMount removed during update does not cause issues": async () => {
159
159
  const container = setup();
160
160
  const mounts: string[] = [];
161
161
  const state = createState({ removeMount: false });
@@ -172,12 +172,12 @@ export default {
172
172
  ]
173
173
  );
174
174
 
175
- expect(mounts).toEqual(["mount p"]);
175
+ await expect(mounts).toEqual(["mount p"]);
176
176
  patch({ removeMount: true });
177
- expect(mounts).toEqual(["mount p"]);
177
+ await expect(mounts).toEqual(["mount p"]);
178
178
  },
179
179
 
180
- "onMount(): A->A path - onMount changed during update does NOT fire the new one": () => {
180
+ "onMount(): A->A path - onMount changed during update does NOT fire the new one": async () => {
181
181
  const container = setup();
182
182
  const mounts: string[] = [];
183
183
  const state = createState({ version: "a" });
@@ -194,12 +194,12 @@ export default {
194
194
  ]
195
195
  );
196
196
 
197
- expect(mounts).toEqual(["mount a"]);
197
+ await expect(mounts).toEqual(["mount a"]);
198
198
  patch({ version: "b" });
199
- expect(mounts).toEqual(["mount a"]);
199
+ await expect(mounts).toEqual(["mount a"]);
200
200
  },
201
201
 
202
- "onMount(): A->B path - element replaced with different tag fires new onMount": () => {
202
+ "onMount(): A->B path - element replaced with different tag fires new onMount": async () => {
203
203
  const container = setup();
204
204
  const mounts: string[] = [];
205
205
  const state = createState({ showArticle: true });
@@ -225,12 +225,12 @@ export default {
225
225
  ]
226
226
  );
227
227
 
228
- expect(mounts).toEqual(["mount article"]);
228
+ await expect(mounts).toEqual(["mount article"]);
229
229
  patch({ showArticle: false });
230
- expect(mounts).toEqual(["mount article", "mount aside"]);
230
+ await expect(mounts).toEqual(["mount article", "mount aside"]);
231
231
  },
232
232
 
233
- "onMount(): A->B path - swap back fires the other element's onMount": () => {
233
+ "onMount(): A->B path - swap back fires the other element's onMount": async () => {
234
234
  const container = setup();
235
235
  const mounts: string[] = [];
236
236
  const state = createState({ showArticle: true });
@@ -256,13 +256,13 @@ export default {
256
256
  ]
257
257
  );
258
258
 
259
- expect(mounts).toEqual(["mount article"]);
259
+ await expect(mounts).toEqual(["mount article"]);
260
260
  patch({ showArticle: false });
261
- expect(mounts).toEqual(["mount article", "mount aside"]);
261
+ await expect(mounts).toEqual(["mount article", "mount aside"]);
262
262
  patch({ showArticle: true });
263
- expect(mounts).toEqual(["mount article", "mount aside", "mount article"]);
263
+ await expect(mounts).toEqual(["mount article", "mount aside", "mount article"]);
264
264
  patch({ showArticle: false });
265
- expect(mounts)
265
+ await expect(mounts)
266
266
  .toEqual([
267
267
  "mount article",
268
268
  "mount aside",
@@ -271,7 +271,7 @@ export default {
271
271
  ]);
272
272
  },
273
273
 
274
- "onMount(): A->B path - children's onMounts also fire in new tree": () => {
274
+ "onMount(): A->B path - children's onMounts also fire in new tree": async () => {
275
275
  const container = setup();
276
276
  const mounts: string[] = [];
277
277
  const state = createState({ showArticle: true });
@@ -301,12 +301,12 @@ export default {
301
301
  ]
302
302
  );
303
303
 
304
- expect(mounts).toEqual(["mount p"]);
304
+ await expect(mounts).toEqual(["mount p"]);
305
305
  patch({ showArticle: false });
306
- expect(mounts).toEqual(["mount p", "mount div"]);
306
+ await expect(mounts).toEqual(["mount p", "mount div"]);
307
307
  },
308
308
 
309
- "onMount(): text -> element fires new element's onMount": () => {
309
+ "onMount(): text -> element fires new element's onMount": async () => {
310
310
  const container = setup();
311
311
  const mounts: string[] = [];
312
312
  const state = createState({ showElement: false });
@@ -325,12 +325,12 @@ export default {
325
325
  ]
326
326
  );
327
327
 
328
- expect(mounts).toEqual([]);
328
+ await expect(mounts).toEqual([]);
329
329
  patch({ showElement: true });
330
- expect(mounts).toEqual(["mount article"]);
330
+ await expect(mounts).toEqual(["mount article"]);
331
331
  },
332
332
 
333
- "onMount(): mixed onMount presence in tree": () => {
333
+ "onMount(): mixed onMount presence in tree": async () => {
334
334
  const container = setup();
335
335
  const mounts: string[] = [];
336
336
  app(container, {}, () =>
@@ -358,10 +358,10 @@ export default {
358
358
  ]
359
359
  );
360
360
 
361
- expect(mounts).toEqual(["mount p", "mount section"]);
361
+ await expect(mounts).toEqual(["mount p", "mount section"]);
362
362
  },
363
363
 
364
- "onMount(): sibling subtree depths fire in correct order": () => {
364
+ "onMount(): sibling subtree depths fire in correct order": async () => {
365
365
  const container = setup();
366
366
  const mounts: string[] = [];
367
367
  app(container, {}, () =>
@@ -394,7 +394,7 @@ export default {
394
394
  ]
395
395
  );
396
396
 
397
- expect(mounts)
397
+ await expect(mounts)
398
398
  .toEqual([
399
399
  "mount p-deep",
400
400
  "mount div",
@@ -402,7 +402,7 @@ export default {
402
402
  ]);
403
403
  },
404
404
 
405
- "onMount(): added children from count increase fire onMount": () => {
405
+ "onMount(): added children from count increase fire onMount": async () => {
406
406
  const container = setup();
407
407
  const mounts: string[] = [];
408
408
  const state = createState({ count: 1 });
@@ -416,14 +416,14 @@ export default {
416
416
  ]
417
417
  );
418
418
 
419
- expect(mounts).toEqual(["mount p0"]);
419
+ await expect(mounts).toEqual(["mount p0"]);
420
420
  patch({ count: 2 });
421
- expect(mounts).toEqual(["mount p0", "mount p1"]);
421
+ await expect(mounts).toEqual(["mount p0", "mount p1"]);
422
422
  patch({ count: 3 });
423
- expect(mounts).toEqual(["mount p0", "mount p1", "mount p2"]);
423
+ await expect(mounts).toEqual(["mount p0", "mount p1", "mount p2"]);
424
424
  },
425
425
 
426
- "onMount(): conditional child fires onMount when added": () => {
426
+ "onMount(): conditional child fires onMount when added": async () => {
427
427
  const container = setup();
428
428
  const mounts: string[] = [];
429
429
  const state = createState({ show: false });
@@ -443,12 +443,98 @@ export default {
443
443
  ]
444
444
  );
445
445
 
446
- expect(mounts).toEqual([]);
446
+ await expect(mounts).toEqual([]);
447
447
  patch({ show: true });
448
- expect(mounts).toEqual(["mount span"]);
448
+ await expect(mounts).toEqual(["mount span"]);
449
449
  },
450
450
 
451
- "onUnmount(): called when node is removed from the DOM": () => {
451
+ "onMount(): with catched component, replacement vode's onMount fires when error occurs": async () => {
452
+ const container = setup();
453
+ const mounts: string[] = [];
454
+ const broken: any = () => { throw new Error("boom"); };
455
+ app(container, {}, () =>
456
+ [DIV,
457
+ {
458
+ catch: [SECTION,
459
+ {
460
+ onMount: (s: unknown, ele: HTMLElement) => {
461
+ mounts.push("mount fallback");
462
+ }
463
+ },
464
+ "fallback"
465
+ ]
466
+ },
467
+ broken
468
+ ]
469
+ );
470
+
471
+ await expect(mounts).toEqual(["mount fallback"]);
472
+ },
473
+
474
+ "onMount(): with catched component, returned vode's onMount fires and receives error": async () => {
475
+ const container = setup();
476
+ const mounts: string[] = [];
477
+ const caughtErrors: string[] = [];
478
+ const broken: any = () => { throw new Error("boom"); };
479
+ app(container, {}, () =>
480
+ [DIV,
481
+ {
482
+ catch: (s: unknown, err: Error) => {
483
+ caughtErrors.push(err.message);
484
+ return [SECTION,
485
+ {
486
+ onMount: (s: unknown, ele: HTMLElement) => {
487
+ mounts.push("mount fallback");
488
+ }
489
+ },
490
+ "fallback"
491
+ ];
492
+ }
493
+ },
494
+ broken
495
+ ]
496
+ );
497
+
498
+ await expect(mounts).toEqual(["mount fallback"]);
499
+ await expect(caughtErrors).toEqual(["boom"]);
500
+ },
501
+
502
+ "onMount(): with catched component, original element's onMount does NOT fire when error caused replacement": async () => {
503
+ const container = setup();
504
+ const logs: string[] = [];
505
+ const broken: any = () => { throw new Error("boom"); };
506
+ app(container, {}, () =>
507
+ [DIV,
508
+ {
509
+ catch: [ARTICLE,
510
+ {
511
+ onMount: (s: unknown, ele: HTMLElement) => {
512
+ logs.push("mount fallback");
513
+ }
514
+ },
515
+ "fallback"
516
+ ]
517
+ },
518
+ [SECTION,
519
+ {
520
+ onMount: (s: unknown, ele: HTMLElement) => {
521
+ logs.push("mount original section");
522
+ },
523
+ onUnmount: (s: unknown, ele: HTMLElement) => {
524
+ logs.push("unmount original section");
525
+ }
526
+ },
527
+ broken
528
+ ]
529
+ ]
530
+ );
531
+
532
+ // SECTION never finishes mounting (its child broke), so its onMount must not fire.
533
+ // The catch on DIV replaces the broken subtree with ARTICLE whose onMount must fire.
534
+ await expect(logs).toEqual(["mount fallback"]);
535
+ },
536
+
537
+ "onUnmount(): called when node is removed from the DOM": async () => {
452
538
  const container = setup();
453
539
  const unmounts: string[] = [];
454
540
  const state = createState({ showArticle: true });
@@ -465,12 +551,12 @@ export default {
465
551
  ]
466
552
  );
467
553
 
468
- expect(unmounts).toEqual([]);
554
+ await expect(unmounts).toEqual([]);
469
555
  patch({ showArticle: false });
470
- expect(unmounts).toEqual(["unmount article"]);
556
+ await expect(unmounts).toEqual(["unmount article"]);
471
557
  },
472
558
 
473
- "onUnmount(): called for all child nodes that have registerd when parent node is removed from the DOM": () => {
559
+ "onUnmount(): called for all child nodes that have registerd when parent node is removed from the DOM": async () => {
474
560
  const container = setup();
475
561
  const unmounts: string[] = [];
476
562
  const state = createState({ showArticle: true });
@@ -495,13 +581,13 @@ export default {
495
581
  ]
496
582
  );
497
583
 
498
- expect(unmounts).toEqual([]);
584
+ await expect(unmounts).toEqual([]);
499
585
  patch({ showArticle: false });
500
- expect(unmounts).toEqual(["unmount inner", "unmount outer"]);
586
+ await expect(unmounts).toEqual(["unmount inner", "unmount outer"]);
501
587
  },
502
588
 
503
- "onUnmount(): A->A path - onUnmount added during update fires on later removal": () => {
504
- const container = setup();
589
+ "onUnmount(): A->A path - onUnmount added during update fires on later removal": async () => {
590
+ const container = setup() as unknown as ContainerNode;
505
591
  const unmounts: string[] = [];
506
592
  const state = createState({ toggle: false, remove: false });
507
593
  const patch = app<typeof state>(container, state, (s) =>
@@ -512,26 +598,35 @@ export default {
512
598
  unmounts.push("unmount section");
513
599
  }
514
600
  } : {},
515
- [P, "text"]
601
+ [P, {
602
+ onUnmount: s.toggle && ((s: unknown, ele: HTMLElement) => {
603
+ unmounts.push("unmount p");
604
+ })
605
+ }, "text"]
516
606
  ]
517
607
  ]
518
608
  );
519
609
 
520
- expect(unmounts).toEqual([]);
610
+ await expect(unmounts).toEqual([]);
611
+ let before = container._vode.stats.syncRenderCount;
521
612
  patch({ toggle: true });
522
- expect(unmounts).toEqual([]);
613
+ await expect(() => expect(container._vode.stats.syncRenderCount).toBeGreaterThan(before))
614
+ .toSucceedAsync();
615
+ await expect(unmounts).toEqual([]);
616
+ before = container._vode.stats.syncRenderCount;
523
617
  patch({ remove: true });
524
- expect(unmounts).toEqual(["unmount section"]);
618
+ await expect(() => expect(container._vode.stats.syncRenderCount).toBeGreaterThan(before)).toSucceedAsync();
619
+ await expect(unmounts).toEqual(["unmount p", "unmount section"]);
525
620
  },
526
621
 
527
- "onUnmount(): A->A path - onUnmount removed during update does not fire": () => {
622
+ "onUnmount(): A->A path - onUnmount removed during update does not fire": async () => {
528
623
  const container = setup();
529
624
  const unmounts: string[] = [];
530
625
  const state = createState({ toggle: false, remove: false });
531
626
  const patch = app<typeof state>(container, state, (s) =>
532
627
  [DIV,
533
628
  !s.remove && [SECTION,
534
- s.toggle ? {} : {
629
+ !s.toggle && {
535
630
  onUnmount: (s: unknown, ele: HTMLElement) => {
536
631
  unmounts.push("unmount section");
537
632
  }
@@ -541,14 +636,12 @@ export default {
541
636
  ]
542
637
  );
543
638
 
544
- expect(unmounts).toEqual([]);
545
- patch({ toggle: true });
546
- expect(unmounts).toEqual([]);
547
- patch({ remove: true });
548
- expect(unmounts).toEqual([]);
639
+ await expect(unmounts).toEqual([]);
640
+ patch({ remove: true, toggle: false });
641
+ await expect(unmounts).toEqual([]);
549
642
  },
550
643
 
551
- "onUnmount(): A->A path - onUnmount changed during update fires the new one": () => {
644
+ "onUnmount(): A->A path - onUnmount changed during update fires the new one": async () => {
552
645
  const container = setup();
553
646
  const unmounts: string[] = [];
554
647
  const state = createState({ version: "a", remove: false });
@@ -565,14 +658,17 @@ export default {
565
658
  ]
566
659
  );
567
660
 
568
- expect(unmounts).toEqual([]);
661
+ await expect(unmounts).toEqual([]);
662
+ const before = container._vode.stats.syncRenderCount;
569
663
  patch({ version: "b" });
570
- expect(unmounts).toEqual([]);
664
+ await expect(async () => await expect(container._vode.stats.syncRenderCount).toBeGreaterThan(before))
665
+ .toSucceedAsync();
666
+ await expect(unmounts).toEqual([]);
571
667
  patch({ remove: true });
572
- expect(unmounts).toEqual(["unmount b"]);
668
+ await expect(unmounts).toEqual(["unmount b"]);
573
669
  },
574
670
 
575
- "onUnmount(): A->B path - element replaced with different tag fires old onUnmount": () => {
671
+ "onUnmount(): A->B path - element replaced with different tag fires old onUnmount": async () => {
576
672
  const container = setup();
577
673
  const unmounts: string[] = [];
578
674
  const state = createState({ showArticle: true });
@@ -598,12 +694,12 @@ export default {
598
694
  ]
599
695
  );
600
696
 
601
- expect(unmounts).toEqual([]);
697
+ await expect(unmounts).toEqual([]);
602
698
  patch({ showArticle: false });
603
- expect(unmounts).toEqual(["unmount article"]);
699
+ await expect(unmounts).toEqual(["unmount article"]);
604
700
  },
605
701
 
606
- "onUnmount(): A->B path - swap back fires the other element's onUnmount": () => {
702
+ "onUnmount(): A->B path - swap back fires the other element's onUnmount": async () => {
607
703
  const container = setup();
608
704
  const unmounts: string[] = [];
609
705
  const state = createState({ showArticle: true });
@@ -629,18 +725,18 @@ export default {
629
725
  ]
630
726
  );
631
727
 
632
- expect(unmounts).toEqual([]);
728
+ await expect(unmounts).toEqual([]);
633
729
  patch({ showArticle: false });
634
- expect(unmounts).toEqual(["unmount article"]);
730
+ await expect(unmounts).toEqual(["unmount article"]);
635
731
  unmounts.length = 0;
636
732
  patch({ showArticle: true });
637
- expect(unmounts).toEqual(["unmount aside"]);
733
+ await expect(unmounts).toEqual(["unmount aside"]);
638
734
  unmounts.length = 0;
639
735
  patch({ showArticle: false });
640
- expect(unmounts).toEqual(["unmount article"]);
736
+ await expect(unmounts).toEqual(["unmount article"]);
641
737
  },
642
738
 
643
- "onUnmount(): A->B path - replaced element's children onUnmounts also fire": () => {
739
+ "onUnmount(): A->B path - replaced element's children onUnmounts also fire": async () => {
644
740
  const container = setup();
645
741
  const unmounts: string[] = [];
646
742
  const state = createState({ showArticle: true });
@@ -673,12 +769,12 @@ export default {
673
769
  ]
674
770
  );
675
771
 
676
- expect(unmounts).toEqual([]);
772
+ await expect(unmounts).toEqual([]);
677
773
  patch({ showArticle: false });
678
- expect(unmounts).toEqual(["unmount p", "unmount article"]);
774
+ await expect(unmounts).toEqual(["unmount p", "unmount article"]);
679
775
  },
680
776
 
681
- "onUnmount(): element -> text fires onUnmount": () => {
777
+ "onUnmount(): element -> text fires onUnmount": async () => {
682
778
  const container = setup();
683
779
  const unmounts: string[] = [];
684
780
  const state = createState({ showElement: true });
@@ -697,12 +793,12 @@ export default {
697
793
  ]
698
794
  );
699
795
 
700
- expect(unmounts).toEqual([]);
796
+ await expect(unmounts).toEqual([]);
701
797
  patch({ showElement: false });
702
- expect(unmounts).toEqual(["unmount article"]);
798
+ await expect(unmounts).toEqual(["unmount article"]);
703
799
  },
704
800
 
705
- "onUnmount(): text -> element registers onUnmount that fires later": () => {
801
+ "onUnmount(): text -> element registers onUnmount that fires later": async () => {
706
802
  const container = setup();
707
803
  const unmounts: string[] = [];
708
804
  const state = createState({ showElement: false, remove: false });
@@ -722,14 +818,17 @@ export default {
722
818
  ]
723
819
  );
724
820
 
725
- expect(unmounts).toEqual([]);
821
+ await expect(unmounts).toEqual([]);
822
+ const before = container._vode.stats.syncRenderCount;
726
823
  patch({ showElement: true });
727
- expect(unmounts).toEqual([]);
824
+ await expect(() => expect(container._vode.stats.syncRenderCount).toBeGreaterThan(before))
825
+ .toSucceedAsync();
826
+ await expect(unmounts).toEqual([]);
728
827
  patch({ remove: true });
729
- expect(unmounts).toEqual(["unmount article"]);
828
+ await expect(unmounts).toEqual(["unmount article"]);
730
829
  },
731
830
 
732
- "onUnmount(): deep nesting 4+ levels with onUnmount at each level": () => {
831
+ "onUnmount(): deep nesting 4+ levels with onUnmount at each level": async () => {
733
832
  const container = setup();
734
833
  const unmounts: string[] = [];
735
834
  const state = createState({ show: true });
@@ -767,9 +866,9 @@ export default {
767
866
  ]
768
867
  );
769
868
 
770
- expect(unmounts).toEqual([]);
869
+ await expect(unmounts).toEqual([]);
771
870
  patch({ show: false });
772
- expect(unmounts)
871
+ await expect(unmounts)
773
872
  .toEqual([
774
873
  "unmount article",
775
874
  "unmount section",
@@ -778,7 +877,7 @@ export default {
778
877
  ]);
779
878
  },
780
879
 
781
- "onUnmount(): multiple siblings - remove one fires only that sibling's subtree": () => {
880
+ "onUnmount(): multiple siblings - remove one fires only that sibling's subtree": async () => {
782
881
  const container = setup();
783
882
  const unmounts: string[] = [];
784
883
  const state = createState({ showFirst: true, showSecond: true });
@@ -817,12 +916,12 @@ export default {
817
916
  ]
818
917
  );
819
918
 
820
- expect(unmounts).toEqual([]);
919
+ await expect(unmounts).toEqual([]);
821
920
  patch({ showFirst: false });
822
- expect(unmounts).toEqual(["unmount first-child", "unmount first"]);
921
+ await expect(unmounts).toEqual(["unmount first-child", "unmount first"]);
823
922
  },
824
923
 
825
- "onUnmount(): multiple siblings - remove parent fires all": () => {
924
+ "onUnmount(): multiple siblings - remove parent fires all": async () => {
826
925
  const container = setup();
827
926
  const unmounts: string[] = [];
828
927
  const state = createState({ show: true });
@@ -849,12 +948,12 @@ export default {
849
948
  ]
850
949
  );
851
950
 
852
- expect(unmounts).toEqual([]);
951
+ await expect(unmounts).toEqual([]);
853
952
  patch({ show: false });
854
- expect(unmounts).toEqual(["unmount second", "unmount first"]);
953
+ await expect(unmounts).toEqual(["unmount second", "unmount first"]);
855
954
  },
856
955
 
857
- "onUnmount(): stale children cleanup - fewer new children than old fires removed children's onUnmounts": () => {
956
+ "onUnmount(): stale children cleanup - fewer new children than old fires removed children's onUnmounts": async () => {
858
957
  const container = setup();
859
958
  const unmounts: string[] = [];
860
959
  const state = createState({ count: 3 });
@@ -868,12 +967,12 @@ export default {
868
967
  ]
869
968
  );
870
969
 
871
- expect(unmounts).toEqual([]);
970
+ await expect(unmounts).toEqual([]);
872
971
  patch({ count: 1 });
873
- expect(unmounts).toEqual(["unmount p1", "unmount p2"]);
972
+ await expect(unmounts).toEqual(["unmount p1", "unmount p2"]);
874
973
  },
875
974
 
876
- "onUnmount(): mixed onUnmount presence in tree - only elements with onUnmount fire": () => {
975
+ "onUnmount(): mixed onUnmount presence in tree - only elements with onUnmount fire": async () => {
877
976
  const container = setup();
878
977
  const unmounts: string[] = [];
879
978
  const state = createState({ show: true });
@@ -902,12 +1001,12 @@ export default {
902
1001
  ]
903
1002
  );
904
1003
 
905
- expect(unmounts).toEqual([]);
1004
+ await expect(unmounts).toEqual([]);
906
1005
  patch({ show: false });
907
- expect(unmounts).toEqual(["unmount p", "unmount section"]);
1006
+ await expect(unmounts).toEqual(["unmount p", "unmount section"]);
908
1007
  },
909
1008
 
910
- "onUnmount(): sibling ordering - sibling subtree depths": () => {
1009
+ "onUnmount(): sibling ordering - sibling subtree depths": async () => {
911
1010
  const container = setup();
912
1011
  const unmounts: string[] = [];
913
1012
  const state = createState({ show: true });
@@ -941,12 +1040,12 @@ export default {
941
1040
  ]
942
1041
  );
943
1042
 
944
- expect(unmounts).toEqual([]);
1043
+ await expect(unmounts).toEqual([]);
945
1044
  patch({ show: false });
946
- expect(unmounts).toEqual(["unmount nav", "unmount p-deep", "unmount div"]);
1045
+ await expect(unmounts).toEqual(["unmount nav", "unmount p-deep", "unmount div"]);
947
1046
  },
948
1047
 
949
- "onUnmount(): A->A path - children's unmounts shift when previous sibling's subtree changes": () => {
1048
+ "onUnmount(): A->A path - children's unmounts shift when previous sibling's subtree changes": async () => {
950
1049
  const container = setup();
951
1050
  const unmounts: string[] = [];
952
1051
  const state = createState({ showExtraChild: true, remove: false });
@@ -981,11 +1080,11 @@ export default {
981
1080
  ]
982
1081
  );
983
1082
 
984
- expect(unmounts).toEqual([]);
1083
+ await expect(unmounts).toEqual([]);
985
1084
  patch({ showExtraChild: false });
986
- expect(unmounts).toEqual(["unmount span"]);
1085
+ await expect(unmounts).toEqual(["unmount span"]);
987
1086
  patch({ remove: true });
988
- expect(unmounts)
1087
+ await expect(unmounts)
989
1088
  .toEqual([
990
1089
  "unmount span",
991
1090
  "unmount aside",
@@ -993,7 +1092,7 @@ export default {
993
1092
  ]);
994
1093
  },
995
1094
 
996
- "onUnmount(): root element replacement fires root's onUnmount": () => {
1095
+ "onUnmount(): root element replacement fires root's onUnmount": async () => {
997
1096
  const container = setup();
998
1097
  const unmounts: string[] = [];
999
1098
  const state = createState({ showDiv: true });
@@ -1017,12 +1116,12 @@ export default {
1017
1116
  ]
1018
1117
  );
1019
1118
 
1020
- expect(unmounts).toEqual([]);
1119
+ await expect(unmounts).toEqual([]);
1021
1120
  patch({ showDiv: false });
1022
- expect(unmounts).toEqual(["unmount div"]);
1121
+ await expect(unmounts).toEqual(["unmount div"]);
1023
1122
  },
1024
1123
 
1025
- "onUnmount(): child onUnmount fires when element is falsified after onUnmount was added via A->A update": () => {
1124
+ "onUnmount(): child onUnmount fires when element is falsified after onUnmount was added via A->A update": async () => {
1026
1125
  const container = setup();
1027
1126
  const unmounts: string[] = [];
1028
1127
  const state = createState({ addUnmount: false, show: true });
@@ -1039,14 +1138,17 @@ export default {
1039
1138
  ]
1040
1139
  );
1041
1140
 
1042
- expect(unmounts).toEqual([]);
1141
+ await expect(unmounts).toEqual([]);
1142
+ const before = container._vode.stats.syncRenderCount;
1043
1143
  patch({ addUnmount: true });
1044
- expect(unmounts).toEqual([]);
1144
+ await expect(async () => await expect(container._vode.stats.syncRenderCount).toEqual(before + 1))
1145
+ .toSucceedAsync();
1146
+ await expect(unmounts).toEqual([]);
1045
1147
  patch({ show: false });
1046
- expect(unmounts).toEqual(["unmount article"]);
1148
+ await expect(unmounts).toEqual(["unmount article"]);
1047
1149
  },
1048
1150
 
1049
- "onUnmount(): A->B path - onUnmount from old children fire when switching tags": () => {
1151
+ "onUnmount(): A->B path - onUnmount from old children fire when switching tags": async () => {
1050
1152
  const container = setup();
1051
1153
  const unmounts: string[] = [];
1052
1154
  const state = createState({ showArticle: true });
@@ -1076,12 +1178,12 @@ export default {
1076
1178
  ]
1077
1179
  );
1078
1180
 
1079
- expect(unmounts).toEqual([]);
1181
+ await expect(unmounts).toEqual([]);
1080
1182
  patch({ showArticle: false });
1081
- expect(unmounts).toEqual(["unmount p-inner"]);
1183
+ await expect(unmounts).toEqual(["unmount p-inner"]);
1082
1184
  },
1083
1185
 
1084
- "onUnmount(): memo hit + earlier sibling growth corrupts unmount indices": () => {
1186
+ "onUnmount(): memo hit + earlier sibling growth corrupts unmount indices": async () => {
1085
1187
  const container = setup();
1086
1188
  const fired: string[] = [];
1087
1189
  const state = createState({ expanded: false, showB: true });
@@ -1112,16 +1214,16 @@ export default {
1112
1214
  ]
1113
1215
  );
1114
1216
 
1115
- expect(fired).toEqual([]);
1217
+ await expect(fired).toEqual([]);
1116
1218
 
1117
1219
  patch({ expanded: true });
1118
- expect(fired).toEqual([]);
1220
+ await expect(fired).toEqual([]);
1119
1221
 
1120
1222
  patch({ showB: false });
1121
- expect(fired).toEqual(["unmount B"]);
1223
+ await expect(fired).toEqual(["unmount B"]);
1122
1224
  },
1123
1225
 
1124
- "onUnmount(): excess child removal + same-render sibling growth": () => {
1226
+ "onUnmount(): excess child removal + same-render sibling growth": async () => {
1125
1227
  const container = setup();
1126
1228
  const fired: string[] = [];
1127
1229
  const state = createState({ expanded: false, showB: true });
@@ -1152,120 +1254,12 @@ export default {
1152
1254
  ]
1153
1255
  );
1154
1256
 
1155
- expect(fired).toEqual([]);
1257
+ await expect(fired).toEqual([]);
1156
1258
  patch({ expanded: true, showB: false });
1157
- expect(fired).toEqual(["unmount B"]);
1259
+ await expect(fired).toEqual(["unmount B"]);
1158
1260
  },
1159
1261
 
1160
- "onMount() + onUnmount: symmetry of calls": () => {
1161
- const container = setup();
1162
- const state = createState({
1163
- startTime: 0,
1164
- inputReady: false,
1165
- showInput: true,
1166
- showTimer: true
1167
- });
1168
- type State = typeof state;
1169
- const logs: string[] = [];
1170
-
1171
- const patch = app<State>(container, state, (s) =>
1172
- [DIV,
1173
- s.showInput && [INPUT, {
1174
- type: 'text',
1175
- placeholder: 'Auto-focused on mount',
1176
- onMount: (s: State, ele: HTMLElement) => {
1177
- //(ele as HTMLInputElement).focus();
1178
- logs.push('Input mounted');
1179
- return { inputReady: true };
1180
- },
1181
- onUnmount: (s: State, ele: HTMLElement) => {
1182
- // console.log('Input removed');
1183
- logs.push('Input removed');
1184
- return { inputReady: false };
1185
- }
1186
- }],
1187
-
1188
- s.showTimer && [P, {
1189
- onMount: (s: State, ele: HTMLElement) => {
1190
- logs.push('Timer started');
1191
- s.patch({ startTime: Date.now() });
1192
- },
1193
- onUnmount: (s: State, ele: HTMLElement) => {
1194
- console.log('Timer stopped after', Date.now() - s.startTime, 'ms');
1195
- logs.push('Timer removed');
1196
- }
1197
- }, 'Mount/unmount lifecycle demo']
1198
- ]
1199
- );
1200
-
1201
- expect(state.inputReady)
1202
- .toEqual(true);
1203
- expect(state.startTime != 0)
1204
- .toEqual(true);
1205
- patch({ showInput: false });
1206
- expect(state.inputReady)
1207
- .toEqual(false);
1208
- patch({ showTimer: false });
1209
- expect(logs).toEqual([
1210
- 'Input mounted',
1211
- 'Timer started',
1212
- 'Input removed',
1213
- 'Timer removed'
1214
- ]);
1215
- },
1216
-
1217
- "onMount(): with catched component, replacement vode's onMount fires when error occurs": () => {
1218
- const container = setup();
1219
- const mounts: string[] = [];
1220
- const broken: any = () => { throw new Error("boom"); };
1221
- app(container, {}, () =>
1222
- [DIV,
1223
- {
1224
- catch: [SECTION,
1225
- {
1226
- onMount: (s: unknown, ele: HTMLElement) => {
1227
- mounts.push("mount fallback");
1228
- }
1229
- },
1230
- "fallback"
1231
- ]
1232
- },
1233
- broken
1234
- ]
1235
- );
1236
-
1237
- expect(mounts).toEqual(["mount fallback"]);
1238
- },
1239
-
1240
- "onMount(): with catched component, returned vode's onMount fires and receives error": () => {
1241
- const container = setup();
1242
- const mounts: string[] = [];
1243
- const caughtErrors: string[] = [];
1244
- const broken: any = () => { throw new Error("boom"); };
1245
- app(container, {}, () =>
1246
- [DIV,
1247
- {
1248
- catch: (s: unknown, err: Error) => {
1249
- caughtErrors.push(err.message);
1250
- return [SECTION,
1251
- {
1252
- onMount: (s: unknown, ele: HTMLElement) => {
1253
- mounts.push("mount fallback");
1254
- }
1255
- },
1256
- "fallback"
1257
- ];
1258
- }
1259
- },
1260
- broken
1261
- ]
1262
- );
1263
-
1264
- expect(mounts).toEqual(["mount fallback"]);
1265
- expect(caughtErrors).toEqual(["boom"]);
1266
- },
1267
-
1268
- "onUnmount(): with catched component, replacement vode's onUnmount fires when removed": () => {
1262
+ "onUnmount(): with catched component, replacement vode's onUnmount fires when removed": async () => {
1269
1263
  const container = setup();
1270
1264
  const unmounts: string[] = [];
1271
1265
  const state = createState({ show: true });
@@ -1288,12 +1282,12 @@ export default {
1288
1282
  ]
1289
1283
  );
1290
1284
 
1291
- expect(unmounts).toEqual([]);
1285
+ await expect(unmounts).toEqual([]);
1292
1286
  patch({ show: false });
1293
- expect(unmounts).toEqual(["unmount fallback"]);
1287
+ await expect(unmounts).toEqual(["unmount fallback"]);
1294
1288
  },
1295
1289
 
1296
- "onUnmount(): with catched component, deep replacement tree fires in post-order": () => {
1290
+ "onUnmount(): with catched component, deep replacement tree fires in post-order": async () => {
1297
1291
  const container = setup();
1298
1292
  const unmounts: string[] = [];
1299
1293
  const state = createState({ show: true });
@@ -1331,12 +1325,12 @@ export default {
1331
1325
  ]
1332
1326
  );
1333
1327
 
1334
- expect(unmounts).toEqual([]);
1328
+ await expect(unmounts).toEqual([]);
1335
1329
  patch({ show: false });
1336
- expect(unmounts).toEqual(["unmount span", "unmount p", "unmount article"]);
1330
+ await expect(unmounts).toEqual(["unmount span", "unmount p", "unmount article"]);
1337
1331
  },
1338
1332
 
1339
- "onMount()/onUnmount(): with catched component, full lifecycle symmetry of catch replacement": () => {
1333
+ "onMount() + onUnmount(): with catched component, full lifecycle symmetry of catch replacement": async () => {
1340
1334
  const container = setup();
1341
1335
  const logs: string[] = [];
1342
1336
  const state = createState({ show: true });
@@ -1362,43 +1356,149 @@ export default {
1362
1356
  ]
1363
1357
  );
1364
1358
 
1365
- expect(logs).toEqual(["mount article"]);
1359
+ await expect(logs).toEqual(["mount article"]);
1366
1360
  patch({ show: false });
1367
- expect(logs).toEqual(["mount article", "unmount article"]);
1361
+ await expect(logs).toEqual(["mount article", "unmount article"]);
1368
1362
  },
1369
1363
 
1370
- "onMount(): with catched component, original element's onMount does NOT fire when error caused replacement": () => {
1364
+ "onMount() + onUnmount: symmetry of calls": async () => {
1371
1365
  const container = setup();
1366
+ const state = createState({
1367
+ startTime: 0,
1368
+ inputReady: false,
1369
+ showInput: true,
1370
+ showTimer: true
1371
+ });
1372
+ type State = typeof state;
1372
1373
  const logs: string[] = [];
1373
- const broken: any = () => { throw new Error("boom"); };
1374
- app(container, {}, () =>
1374
+
1375
+ const patch = app<State>(container, state, (s) => {
1376
+ return [DIV,
1377
+ s.showInput && [INPUT, {
1378
+ type: 'text',
1379
+ placeholder: 'Auto-focused on mount',
1380
+ onMount: (s: State, ele: HTMLElement) => {
1381
+ logs.push('Input mounted');
1382
+ return { inputReady: true };
1383
+ },
1384
+ onUnmount: (s: State, ele: HTMLElement) => {
1385
+ logs.push('Input removed');
1386
+ return { inputReady: false };
1387
+ }
1388
+ }],
1389
+
1390
+ s.showTimer && [P, {
1391
+ onMount: (s: State, ele: HTMLElement) => {
1392
+ logs.push('Timer started');
1393
+ return { startTime: Date.now() };
1394
+ },
1395
+ onUnmount: (s: State, ele: HTMLElement) => {
1396
+ logs.push('Timer removed');
1397
+ }
1398
+ }, 'Mount/unmount lifecycle demo']
1399
+ ]
1400
+ }
1401
+ );
1402
+
1403
+ await expect(state.inputReady)
1404
+ .toEqual(true);
1405
+ await expect(state.startTime != 0)
1406
+ .toEqual(true);
1407
+ patch({ showInput: false });
1408
+
1409
+ await expect(
1410
+ async () => await expect(state.inputReady).toEqual(false, "expected: inputReady == false")
1411
+ ).toSucceedAsync();
1412
+
1413
+ patch({ showTimer: false });
1414
+
1415
+ await expect(
1416
+ async () => await expect(container._vode.stats.syncRenderCount >= 4)
1417
+ .toEqual(true)
1418
+ ).toSucceedAsync();
1419
+
1420
+ await expect(logs).toEqual([
1421
+ 'Input mounted',
1422
+ 'Timer started',
1423
+ 'Input removed',
1424
+ 'Timer removed'
1425
+ ]);
1426
+ },
1427
+
1428
+ "onMount() + onUnmount(): Not called when DOM does not require element creation or removal (same TAGs)": async () => {
1429
+ const container = setup();
1430
+ const logs = <string[]>[];
1431
+
1432
+ const Comp: (name: string) => Component = (name: string) => () => [ARTICLE,
1375
1433
  [DIV,
1376
1434
  {
1377
- catch: [ARTICLE,
1378
- {
1379
- onMount: (s: unknown, ele: HTMLElement) => {
1380
- logs.push("mount fallback");
1381
- }
1382
- },
1383
- "fallback"
1384
- ]
1435
+ onMount: () => logs.push("mount " + name),
1436
+ onUnmount: () => logs.push("unmount " + name)
1385
1437
  },
1386
- [SECTION,
1387
- {
1388
- onMount: (s: unknown, ele: HTMLElement) => {
1389
- logs.push("mount original section");
1390
- },
1391
- onUnmount: (s: unknown, ele: HTMLElement) => {
1392
- logs.push("unmount original section");
1393
- }
1394
- },
1395
- broken
1396
- ]
1438
+ "Component " + name]
1439
+ ];
1440
+
1441
+ const state = createState({ showB: false, showD: false });
1442
+ app<typeof state>(container, state, s => [DIV,
1443
+ // this way they both "share a slot"
1444
+ s.showB ? Comp("B") : Comp("A"),
1445
+
1446
+ // this way each component occupies its own "slot"
1447
+ !s.showD && Comp("C"),
1448
+ s.showD && Comp("D"),
1449
+ ]);
1450
+
1451
+ await expect(container).toMatch(
1452
+ [DIV,
1453
+ [ARTICLE,
1454
+ [DIV, "Component A"],
1455
+ ],
1456
+ [ARTICLE,
1457
+ [DIV, "Component C"],
1458
+ ],
1397
1459
  ]
1398
1460
  );
1461
+ await expect(logs).toEqual(["mount A", "mount C"]);
1399
1462
 
1400
- // SECTION never finishes mounting (its child broke), so its onMount must not fire.
1401
- // The catch on DIV replaces the broken subtree with ARTICLE whose onMount must fire.
1402
- expect(logs).toEqual(["mount fallback"]);
1463
+ state.patch({ showB: true });
1464
+
1465
+ await expect(container).toMatch(
1466
+ [DIV,
1467
+ [ARTICLE,
1468
+ [DIV, "Component B"],
1469
+ ],
1470
+ [ARTICLE,
1471
+ [DIV, "Component C"],
1472
+ ],
1473
+ ]
1474
+ );
1475
+
1476
+ // as both components result in the same structure
1477
+ // of element types the unmount of A
1478
+ // and mount of B does not occur
1479
+ await expect(logs).toEqual(["mount A", "mount C"]);
1480
+
1481
+
1482
+ state.patch({ showD: true });
1483
+
1484
+ await expect(container).toMatch(
1485
+ [DIV,
1486
+ [ARTICLE,
1487
+ [DIV, "Component B"],
1488
+ ],
1489
+ [ARTICLE,
1490
+ [DIV, "Component D"],
1491
+ ],
1492
+ ]
1493
+ );
1494
+
1495
+ // when the components occupy different slots in the vdom
1496
+ // their mount/unmount functions are called
1497
+ await expect(logs).toEqual([
1498
+ "mount A",
1499
+ "mount C",
1500
+ "unmount C",
1501
+ "mount D",
1502
+ ]);
1403
1503
  },
1404
1504
  }