20
let rAFCallbacks = [];
21
let postMessageCallback;
22
let postMessageEvents = [];
23
+ let postMessageErrors = [];
24
+ let catchPostMessageErrors = false;
25
26
function runPostMessageCallbacks(config: FrameTimeoutConfigType) {
27
let timeLeftInFrame = 0;
33
currentTime = startOfLatestFrame + frameSize - timeLeftInFrame;
34
if (postMessageCallback) {
35
while (postMessageEvents.length) {
34
- postMessageCallback(postMessageEvents.shift());
36
+ if (catchPostMessageErrors) {
37
+ // catch errors for testing error handling
38
+ try {
39
+ postMessageCallback(postMessageEvents.shift());
40
+ } catch (e) {
41
+ postMessageErrors.push(e);
42
+ }
43
+ } else {
44
+ // we are not expecting errors
45
+ postMessageCallback(postMessageEvents.shift());
46
+ }
47
}
48
}
49
}
76
const originalAddEventListener = global.addEventListener;
77
postMessageCallback = null;
78
postMessageEvents = [];
79
+ postMessageErrors = [];
80
global.addEventListener = function(eventName, callback, useCapture) {
81
if (eventName === 'message') {
82
postMessageCallback = callback;
293
294
describe('when there is some time left in the frame', () => {
295
it('calls timed out callbacks and then any more pending callbacks, defers others if time runs out', () => {
283
- // TODO first call timed out callbacks
284
- // then any non-timed out callbacks if there is time
296
const {scheduleWork} = ReactScheduler;
297
startOfLatestFrame = 1000000000000;
298
currentTime = startOfLatestFrame - 10;
351
});
352
353
describe('with multiple callbacks', () => {
354
+ it('when called more than once', () => {
355
+ const {scheduleWork, cancelScheduledWork} = ReactScheduler;
356
+ const callbackLog = [];
357
+ const callbackA = jest.fn(() => callbackLog.push('A'));
358
+ const callbackB = jest.fn(() => callbackLog.push('B'));
359
+ const callbackC = jest.fn(() => callbackLog.push('C'));
360
+ scheduleWork(callbackA);
361
+ const callbackId = scheduleWork(callbackB);
362
+ scheduleWork(callbackC);
363
+ cancelScheduledWork(callbackId);
364
+ cancelScheduledWork(callbackId);
365
+ cancelScheduledWork(callbackId);
366
+ // Initially doesn't call anything
367
+ expect(callbackLog).toEqual([]);
368
+ advanceOneFrame({timeLeftInFrame: 15});
369
+
370
+ // Should still call A and C
371
+ expect(callbackLog).toEqual(['A', 'C']);
372
+ expect(callbackB).toHaveBeenCalledTimes(0);
373
+ });
374
+
375
it('when one callback cancels the next one', () => {
376
const {scheduleWork, cancelScheduledWork} = ReactScheduler;
377
const callbackLog = [];
393
});
394
});
395
396
+ describe('when callbacks throw errors', () => {
397
+ describe('when some callbacks throw', () => {
398
+ /**
399
+ * + +
400
+ * | rAF postMessage |
401
+ * | |
402
+ * | +---------------------+ |
403
+ * | | paint/layout | cbA() cbB() cbC() cbD() cbE() |
404
+ * | +---------------------+ ^ ^ |
405
+ * | | | |
406
+ * + | | +
407
+ * + +
408
+ * throw errors
409
+ *
410
+ *
411
+ */
412
+ it('still calls all callbacks within same frame', () => {
413
+ const {scheduleWork} = ReactScheduler;
414
+ const callbackLog = [];
415
+ const callbackA = jest.fn(() => callbackLog.push('A'));
416
+ const callbackB = jest.fn(() => {
417
+ callbackLog.push('B');
418
+ throw new Error('B error');
419
+ });
420
+ const callbackC = jest.fn(() => callbackLog.push('C'));
421
+ const callbackD = jest.fn(() => {
422
+ callbackLog.push('D');
423
+ throw new Error('D error');
424
+ });
425
+ const callbackE = jest.fn(() => callbackLog.push('E'));
426
+ scheduleWork(callbackA);
427
+ scheduleWork(callbackB);
428
+ scheduleWork(callbackC);
429
+ scheduleWork(callbackD);
430
+ scheduleWork(callbackE);
431
+ // Initially doesn't call anything
432
+ expect(callbackLog).toEqual([]);
433
+ catchPostMessageErrors = true;
434
+ advanceOneFrame({timeLeftInFrame: 15});
435
+ // calls all callbacks
436
+ expect(callbackLog).toEqual(['A', 'B', 'C', 'D', 'E']);
437
+ // errors should still get thrown
438
+ const postMessageErrorMessages = postMessageErrors.map(e => e.message);
439
+ expect(postMessageErrorMessages).toEqual(['B error', 'D error']);
440
+ catchPostMessageErrors = false;
441
+ });
442
+
443
+ /**
444
+ * timed out
445
+ * + + +--+
446
+ * + rAF postMessage | | | +
447
+ * | | | | |
448
+ * | +---------------------+ v v v |
449
+ * | | paint/layout | cbA() cbB() cbC() cbD() cbE() |
450
+ * | +---------------------+ ^ ^ |
451
+ * | | | |
452
+ * + | | +
453
+ * + +
454
+ * throw errors
455
+ *
456
+ *
457
+ */
458
+ it('and with some timed out callbacks, still calls all callbacks within same frame', () => {
459
+ const {scheduleWork} = ReactScheduler;
460
+ const callbackLog = [];
461
+ const callbackA = jest.fn(() => {
462
+ callbackLog.push('A');
463
+ throw new Error('A error');
464
+ });
465
+ const callbackB = jest.fn(() => callbackLog.push('B'));
466
+ const callbackC = jest.fn(() => callbackLog.push('C'));
467
+ const callbackD = jest.fn(() => {
468
+ callbackLog.push('D');
469
+ throw new Error('D error');
470
+ });
471
+ const callbackE = jest.fn(() => callbackLog.push('E'));
472
+ scheduleWork(callbackA);
473
+ scheduleWork(callbackB);
474
+ scheduleWork(callbackC, {timeout: 2}); // times out fast
475
+ scheduleWork(callbackD, {timeout: 2}); // times out fast
476
+ scheduleWork(callbackE, {timeout: 2}); // times out fast
477
+ // Initially doesn't call anything
478
+ expect(callbackLog).toEqual([]);
479
+ catchPostMessageErrors = true;
480
+ advanceOneFrame({timeLeftInFrame: 15});
481
+ // calls all callbacks; calls timed out ones first
482
+ expect(callbackLog).toEqual(['C', 'D', 'E', 'A', 'B']);
483
+ // errors should still get thrown
484
+ const postMessageErrorMessages = postMessageErrors.map(e => e.message);
485
+ expect(postMessageErrorMessages).toEqual(['D error', 'A error']);
486
+ catchPostMessageErrors = false;
487
+ });
488
+ });
489
+ describe('when all scheduled callbacks throw', () => {
490
+ /**
491
+ * + +
492
+ * | rAF postMessage |
493
+ * | |
494
+ * | +---------------------+ |
495
+ * | | paint/layout | cbA() cbB() cbC() cbD() cbE() |
496
+ * | +---------------------+ ^ ^ ^ ^ ^ |
497
+ * | | | | | | |
498
+ * + | | | | | +
499
+ * | + + + +
500
+ * + all callbacks throw errors
501
+ *
502
+ *
503
+ */
504
+ it('still calls all callbacks within same frame', () => {
505
+ const {scheduleWork} = ReactScheduler;
506
+ const callbackLog = [];
507
+ const callbackA = jest.fn(() => {
508
+ callbackLog.push('A');
509
+ throw new Error('A error');
510
+ });
511
+ const callbackB = jest.fn(() => {
512
+ callbackLog.push('B');
513
+ throw new Error('B error');
514
+ });
515
+ const callbackC = jest.fn(() => {
516
+ callbackLog.push('C');
517
+ throw new Error('C error');
518
+ });
519
+ const callbackD = jest.fn(() => {
520
+ callbackLog.push('D');
521
+ throw new Error('D error');
522
+ });
523
+ const callbackE = jest.fn(() => {
524
+ callbackLog.push('E');
525
+ throw new Error('E error');
526
+ });
527
+ scheduleWork(callbackA);
528
+ scheduleWork(callbackB);
529
+ scheduleWork(callbackC);
530
+ scheduleWork(callbackD);
531
+ scheduleWork(callbackE);
532
+ // Initially doesn't call anything
533
+ expect(callbackLog).toEqual([]);
534
+ catchPostMessageErrors = true;
535
+ advanceOneFrame({timeLeftInFrame: 15});
536
+ // calls all callbacks
537
+ expect(callbackLog).toEqual(['A', 'B', 'C', 'D', 'E']);
538
+ // errors should still get thrown
539
+ const postMessageErrorMessages = postMessageErrors.map(e => e.message);
540
+ expect(postMessageErrorMessages).toEqual([
541
+ 'A error',
542
+ 'B error',
543
+ 'C error',
544
+ 'D error',
545
+ 'E error',
546
+ ]);
547
+ catchPostMessageErrors = false;
548
+ });
549
+
550
+ /**
551
+ * postMessage
552
+ * + +
553
+ * | rAF all callbacks time out |
554
+ * | |
555
+ * | +---------------------+ |
556
+ * | | paint/layout | cbA() cbB() cbC() cbD() cbE() |
557
+ * | +---------------------+ ^ ^ ^ ^ ^ |
558
+ * | | | | | | |
559
+ * + | | | | | +
560
+ * | + + + +
561
+ * + all callbacks throw errors
562
+ *
563
+ *
564
+ */
565
+ it('and with all timed out callbacks, still calls all callbacks within same frame', () => {
566
+ const {scheduleWork} = ReactScheduler;
567
+ const callbackLog = [];
568
+ const callbackA = jest.fn(() => {
569
+ callbackLog.push('A');
570
+ throw new Error('A error');
571
+ });
572
+ const callbackB = jest.fn(() => {
573
+ callbackLog.push('B');
574
+ throw new Error('B error');
575
+ });
576
+ const callbackC = jest.fn(() => {
577
+ callbackLog.push('C');
578
+ throw new Error('C error');
579
+ });
580
+ const callbackD = jest.fn(() => {
581
+ callbackLog.push('D');
582
+ throw new Error('D error');
583
+ });
584
+ const callbackE = jest.fn(() => {
585
+ callbackLog.push('E');
586
+ throw new Error('E error');
587
+ });
588
+ scheduleWork(callbackA, {timeout: 2}); // times out fast
589
+ scheduleWork(callbackB, {timeout: 2}); // times out fast
590
+ scheduleWork(callbackC, {timeout: 2}); // times out fast
591
+ scheduleWork(callbackD, {timeout: 2}); // times out fast
592
+ scheduleWork(callbackE, {timeout: 2}); // times out fast
593
+ // Initially doesn't call anything
594
+ expect(callbackLog).toEqual([]);
595
+ catchPostMessageErrors = true;
596
+ advanceOneFrame({timeLeftInFrame: 15});
597
+ // calls all callbacks
598
+ expect(callbackLog).toEqual(['A', 'B', 'C', 'D', 'E']);
599
+ // errors should still get thrown
600
+ const postMessageErrorMessages = postMessageErrors.map(e => e.message);
601
+ expect(postMessageErrorMessages).toEqual([
602
+ 'A error',
603
+ 'B error',
604
+ 'C error',
605
+ 'D error',
606
+ 'E error',
607
+ ]);
608
+ catchPostMessageErrors = false;
609
+ });
610
+ });
611
+ describe('when callbacks throw over multiple frames', () => {
612
+ /**
613
+ *
614
+ * **Detail View of Frame 1**
615
+ *
616
+ * + +
617
+ * | rAF postMessage |
618
+ * | |
619
+ * | +---------------------+ |
620
+ * | | paint/layout | cbA() cbB() | ... Frame 2
621
+ * | +---------------------+ ^ ^ |
622
+ * | | | |
623
+ * + + | +
624
+ * errors |
625
+ * +
626
+ * takes long time
627
+ * and pushes rest of
628
+ * callbacks into
629
+ * next frame ->
630
+ *
631
+ *
632
+ *
633
+ * **Overview of frames 1-4**
634
+ *
635
+ *
636
+ * + + + + +
637
+ * | | | | |
638
+ * | +--+ | +--+ | +--+ | +--+ |
639
+ * | +--+ A,B+-> +--+ C,D+-> +--+ E,F+-> +--+ G |
640
+ * + ^ + ^ + ^ + +
641
+ * | | |
642
+ * error error error
643
+ *
644
+ *
645
+ */
646
+ it('still calls all callbacks within same frame', () => {
647
+ const {scheduleWork} = ReactScheduler;
648
+ startOfLatestFrame = 1000000000000;
649
+ currentTime = startOfLatestFrame - 10;
650
+ catchPostMessageErrors = true;
651
+ const callbackLog = [];
652
+ const callbackA = jest.fn(() => {
653
+ callbackLog.push('A');
654
+ throw new Error('A error');
655
+ });
656
+ const callbackB = jest.fn(() => {
657
+ callbackLog.push('B');
658
+ // time passes, causing us to run out of idle time
659
+ currentTime += 25;
660
+ });
661
+ const callbackC = jest.fn(() => {
662
+ callbackLog.push('C');
663
+ throw new Error('C error');
664
+ });
665
+ const callbackD = jest.fn(() => {
666
+ callbackLog.push('D');
667
+ // time passes, causing us to run out of idle time
668
+ currentTime += 25;
669
+ });
670
+ const callbackE = jest.fn(() => {
671
+ callbackLog.push('E');
672
+ throw new Error('E error');
673
+ });
674
+ const callbackF = jest.fn(() => {
675
+ callbackLog.push('F');
676
+ // time passes, causing us to run out of idle time
677
+ currentTime += 25;
678
+ });
679
+ const callbackG = jest.fn(() => callbackLog.push('G'));
680
+
681
+ scheduleWork(callbackA);
682
+ scheduleWork(callbackB);
683
+ scheduleWork(callbackC);
684
+ scheduleWork(callbackD);
685
+ scheduleWork(callbackE);
686
+ scheduleWork(callbackF);
687
+ scheduleWork(callbackG);
688
+
689
+ // does nothing initially
690
+ expect(callbackLog).toEqual([]);
691
+
692
+ // frame 1;
693
+ // callback A runs and throws, callback B takes up rest of frame
694
+ advanceOneFrame({timeLeftInFrame: 15}); // runs rAF and postMessage callbacks
695
+
696
+ // calls A and B
697
+ expect(callbackLog).toEqual(['A', 'B']);
698
+ // error was thrown from A
699
+ let postMessageErrorMessages = postMessageErrors.map(e => e.message);
700
+ expect(postMessageErrorMessages).toEqual(['A error']);
701
+
702
+ // frame 2;
703
+ // callback C runs and throws, callback D takes up rest of frame
704
+ advanceOneFrame({timeLeftInFrame: 15}); // runs rAF and postMessage callbacks
705
+
706
+ // calls C and D
707
+ expect(callbackLog).toEqual(['A', 'B', 'C', 'D']);
708
+ // error was thrown from A
709
+ postMessageErrorMessages = postMessageErrors.map(e => e.message);
710
+ expect(postMessageErrorMessages).toEqual(['A error', 'C error']);
711
+
712
+ // frame 3;
713
+ // callback E runs and throws, callback F takes up rest of frame
714
+ advanceOneFrame({timeLeftInFrame: 15}); // runs rAF and postMessage callbacks
715
+
716
+ // calls E and F
717
+ expect(callbackLog).toEqual(['A', 'B', 'C', 'D', 'E', 'F']);
718
+ // error was thrown from A
719
+ postMessageErrorMessages = postMessageErrors.map(e => e.message);
720
+ expect(postMessageErrorMessages).toEqual([
721
+ 'A error',
722
+ 'C error',
723
+ 'E error',
724
+ ]);
725
+
726
+ // frame 4;
727
+ // callback G runs and it's the last one
728
+ advanceOneFrame({timeLeftInFrame: 15}); // runs rAF and postMessage callbacks
729
+
730
+ // calls G
731
+ expect(callbackLog).toEqual(['A', 'B', 'C', 'D', 'E', 'F', 'G']);
732
+ // error was thrown from A
733
+ postMessageErrorMessages = postMessageErrors.map(e => e.message);
734
+ expect(postMessageErrorMessages).toEqual([
735
+ 'A error',
736
+ 'C error',
737
+ 'E error',
738
+ ]);
739
+
740
+ catchPostMessageErrors = true;
741
+ });
742
+ });
743
+ });
744
+
745
// TODO: test 'now'
746
});