1. /**
    
  2.  * Copyright (c) Meta Platforms, Inc. and affiliates.
    
  3.  *
    
  4.  * This source code is licensed under the MIT license found in the
    
  5.  * LICENSE file in the root directory of this source tree.
    
  6.  *
    
  7.  * @emails react-core
    
  8.  * @jest-environment node
    
  9.  */
    
  10. 
    
  11. /* eslint-disable no-for-of-loops/no-for-of-loops */
    
  12. 
    
  13. 'use strict';
    
  14. 
    
  15. let Scheduler;
    
  16. let runtime;
    
  17. let performance;
    
  18. let cancelCallback;
    
  19. let scheduleCallback;
    
  20. let requestPaint;
    
  21. let shouldYield;
    
  22. let NormalPriority;
    
  23. 
    
  24. // The Scheduler implementation uses browser APIs like `MessageChannel` and
    
  25. // `setTimeout` to schedule work on the main thread. Most of our tests treat
    
  26. // these as implementation details; however, the sequence and timing of these
    
  27. // APIs are not precisely specified, and can vary across browsers.
    
  28. //
    
  29. // To prevent regressions, we need the ability to simulate specific edge cases
    
  30. // that we may encounter in various browsers.
    
  31. //
    
  32. // This test suite mocks all browser methods used in our implementation. It
    
  33. // assumes as little as possible about the order and timing of events.
    
  34. describe('SchedulerBrowser', () => {
    
  35.   beforeEach(() => {
    
  36.     jest.resetModules();
    
  37.     runtime = installMockBrowserRuntime();
    
  38.     jest.unmock('scheduler');
    
  39. 
    
  40.     performance = global.performance;
    
  41.     Scheduler = require('scheduler');
    
  42.     cancelCallback = Scheduler.unstable_cancelCallback;
    
  43.     scheduleCallback = Scheduler.unstable_scheduleCallback;
    
  44.     NormalPriority = Scheduler.unstable_NormalPriority;
    
  45.     requestPaint = Scheduler.unstable_requestPaint;
    
  46.     shouldYield = Scheduler.unstable_shouldYield;
    
  47.   });
    
  48. 
    
  49.   afterEach(() => {
    
  50.     delete global.performance;
    
  51. 
    
  52.     if (!runtime.isLogEmpty()) {
    
  53.       throw Error('Test exited without clearing log.');
    
  54.     }
    
  55.   });
    
  56. 
    
  57.   function installMockBrowserRuntime() {
    
  58.     let hasPendingMessageEvent = false;
    
  59.     let isFiringMessageEvent = false;
    
  60.     let hasPendingDiscreteEvent = false;
    
  61.     let hasPendingContinuousEvent = false;
    
  62. 
    
  63.     let timerIDCounter = 0;
    
  64.     // let timerIDs = new Map();
    
  65. 
    
  66.     let eventLog = [];
    
  67. 
    
  68.     let currentTime = 0;
    
  69. 
    
  70.     global.performance = {
    
  71.       now() {
    
  72.         return currentTime;
    
  73.       },
    
  74.     };
    
  75. 
    
  76.     // Delete node provide setImmediate so we fall through to MessageChannel.
    
  77.     delete global.setImmediate;
    
  78. 
    
  79.     global.setTimeout = (cb, delay) => {
    
  80.       const id = timerIDCounter++;
    
  81.       log(`Set Timer`);
    
  82.       // TODO
    
  83.       return id;
    
  84.     };
    
  85.     global.clearTimeout = id => {
    
  86.       // TODO
    
  87.     };
    
  88. 
    
  89.     const port1 = {};
    
  90.     const port2 = {
    
  91.       postMessage() {
    
  92.         if (hasPendingMessageEvent) {
    
  93.           throw Error('Message event already scheduled');
    
  94.         }
    
  95.         log('Post Message');
    
  96.         hasPendingMessageEvent = true;
    
  97.       },
    
  98.     };
    
  99.     global.MessageChannel = function MessageChannel() {
    
  100.       this.port1 = port1;
    
  101.       this.port2 = port2;
    
  102.     };
    
  103. 
    
  104.     const scheduling = {
    
  105.       isInputPending(options) {
    
  106.         if (this !== scheduling) {
    
  107.           throw new Error(
    
  108.             'isInputPending called with incorrect `this` context',
    
  109.           );
    
  110.         }
    
  111. 
    
  112.         return (
    
  113.           hasPendingDiscreteEvent ||
    
  114.           (options && options.includeContinuous && hasPendingContinuousEvent)
    
  115.         );
    
  116.       },
    
  117.     };
    
  118. 
    
  119.     global.navigator = {scheduling};
    
  120. 
    
  121.     function ensureLogIsEmpty() {
    
  122.       if (eventLog.length !== 0) {
    
  123.         throw Error('Log is not empty. Call assertLog before continuing.');
    
  124.       }
    
  125.     }
    
  126.     function advanceTime(ms) {
    
  127.       currentTime += ms;
    
  128.     }
    
  129.     function resetTime() {
    
  130.       currentTime = 0;
    
  131.     }
    
  132.     function fireMessageEvent() {
    
  133.       ensureLogIsEmpty();
    
  134.       if (!hasPendingMessageEvent) {
    
  135.         throw Error('No message event was scheduled');
    
  136.       }
    
  137.       hasPendingMessageEvent = false;
    
  138.       const onMessage = port1.onmessage;
    
  139.       log('Message Event');
    
  140. 
    
  141.       isFiringMessageEvent = true;
    
  142.       try {
    
  143.         onMessage();
    
  144.       } finally {
    
  145.         isFiringMessageEvent = false;
    
  146.         if (hasPendingDiscreteEvent) {
    
  147.           log('Discrete Event');
    
  148.           hasPendingDiscreteEvent = false;
    
  149.         }
    
  150.         if (hasPendingContinuousEvent) {
    
  151.           log('Continuous Event');
    
  152.           hasPendingContinuousEvent = false;
    
  153.         }
    
  154.       }
    
  155.     }
    
  156.     function scheduleDiscreteEvent() {
    
  157.       if (isFiringMessageEvent) {
    
  158.         hasPendingDiscreteEvent = true;
    
  159.       } else {
    
  160.         log('Discrete Event');
    
  161.       }
    
  162.     }
    
  163.     function scheduleContinuousEvent() {
    
  164.       if (isFiringMessageEvent) {
    
  165.         hasPendingContinuousEvent = true;
    
  166.       } else {
    
  167.         log('Continuous Event');
    
  168.       }
    
  169.     }
    
  170.     function log(val) {
    
  171.       eventLog.push(val);
    
  172.     }
    
  173.     function isLogEmpty() {
    
  174.       return eventLog.length === 0;
    
  175.     }
    
  176.     function assertLog(expected) {
    
  177.       const actual = eventLog;
    
  178.       eventLog = [];
    
  179.       expect(actual).toEqual(expected);
    
  180.     }
    
  181.     return {
    
  182.       advanceTime,
    
  183.       resetTime,
    
  184.       fireMessageEvent,
    
  185.       log,
    
  186.       isLogEmpty,
    
  187.       assertLog,
    
  188.       scheduleDiscreteEvent,
    
  189.       scheduleContinuousEvent,
    
  190.     };
    
  191.   }
    
  192. 
    
  193.   it('task that finishes before deadline', () => {
    
  194.     scheduleCallback(NormalPriority, () => {
    
  195.       runtime.log('Task');
    
  196.     });
    
  197.     runtime.assertLog(['Post Message']);
    
  198.     runtime.fireMessageEvent();
    
  199.     runtime.assertLog(['Message Event', 'Task']);
    
  200.   });
    
  201. 
    
  202.   it('task with continuation', () => {
    
  203.     scheduleCallback(NormalPriority, () => {
    
  204.       runtime.log('Task');
    
  205.       // Request paint so that we yield at the end of the frame interval
    
  206.       requestPaint();
    
  207.       while (!Scheduler.unstable_shouldYield()) {
    
  208.         runtime.advanceTime(1);
    
  209.       }
    
  210.       runtime.log(`Yield at ${performance.now()}ms`);
    
  211.       return () => {
    
  212.         runtime.log('Continuation');
    
  213.       };
    
  214.     });
    
  215.     runtime.assertLog(['Post Message']);
    
  216. 
    
  217.     runtime.fireMessageEvent();
    
  218.     runtime.assertLog([
    
  219.       'Message Event',
    
  220.       'Task',
    
  221.       'Yield at 5ms',
    
  222.       'Post Message',
    
  223.     ]);
    
  224. 
    
  225.     runtime.fireMessageEvent();
    
  226.     runtime.assertLog(['Message Event', 'Continuation']);
    
  227.   });
    
  228. 
    
  229.   it('multiple tasks', () => {
    
  230.     scheduleCallback(NormalPriority, () => {
    
  231.       runtime.log('A');
    
  232.     });
    
  233.     scheduleCallback(NormalPriority, () => {
    
  234.       runtime.log('B');
    
  235.     });
    
  236.     runtime.assertLog(['Post Message']);
    
  237.     runtime.fireMessageEvent();
    
  238.     runtime.assertLog(['Message Event', 'A', 'B']);
    
  239.   });
    
  240. 
    
  241.   it('multiple tasks with a yield in between', () => {
    
  242.     scheduleCallback(NormalPriority, () => {
    
  243.       runtime.log('A');
    
  244.       runtime.advanceTime(4999);
    
  245.     });
    
  246.     scheduleCallback(NormalPriority, () => {
    
  247.       runtime.log('B');
    
  248.     });
    
  249.     runtime.assertLog(['Post Message']);
    
  250.     runtime.fireMessageEvent();
    
  251.     runtime.assertLog([
    
  252.       'Message Event',
    
  253.       'A',
    
  254.       // Ran out of time. Post a continuation event.
    
  255.       'Post Message',
    
  256.     ]);
    
  257.     runtime.fireMessageEvent();
    
  258.     runtime.assertLog(['Message Event', 'B']);
    
  259.   });
    
  260. 
    
  261.   it('cancels tasks', () => {
    
  262.     const task = scheduleCallback(NormalPriority, () => {
    
  263.       runtime.log('Task');
    
  264.     });
    
  265.     runtime.assertLog(['Post Message']);
    
  266.     cancelCallback(task);
    
  267.     runtime.assertLog([]);
    
  268.   });
    
  269. 
    
  270.   it('throws when a task errors then continues in a new event', () => {
    
  271.     scheduleCallback(NormalPriority, () => {
    
  272.       runtime.log('Oops!');
    
  273.       throw Error('Oops!');
    
  274.     });
    
  275.     scheduleCallback(NormalPriority, () => {
    
  276.       runtime.log('Yay');
    
  277.     });
    
  278.     runtime.assertLog(['Post Message']);
    
  279. 
    
  280.     expect(() => runtime.fireMessageEvent()).toThrow('Oops!');
    
  281.     runtime.assertLog(['Message Event', 'Oops!', 'Post Message']);
    
  282. 
    
  283.     runtime.fireMessageEvent();
    
  284.     runtime.assertLog(['Message Event', 'Yay']);
    
  285.   });
    
  286. 
    
  287.   it('schedule new task after queue has emptied', () => {
    
  288.     scheduleCallback(NormalPriority, () => {
    
  289.       runtime.log('A');
    
  290.     });
    
  291. 
    
  292.     runtime.assertLog(['Post Message']);
    
  293.     runtime.fireMessageEvent();
    
  294.     runtime.assertLog(['Message Event', 'A']);
    
  295. 
    
  296.     scheduleCallback(NormalPriority, () => {
    
  297.       runtime.log('B');
    
  298.     });
    
  299.     runtime.assertLog(['Post Message']);
    
  300.     runtime.fireMessageEvent();
    
  301.     runtime.assertLog(['Message Event', 'B']);
    
  302.   });
    
  303. 
    
  304.   it('schedule new task after a cancellation', () => {
    
  305.     const handle = scheduleCallback(NormalPriority, () => {
    
  306.       runtime.log('A');
    
  307.     });
    
  308. 
    
  309.     runtime.assertLog(['Post Message']);
    
  310.     cancelCallback(handle);
    
  311. 
    
  312.     runtime.fireMessageEvent();
    
  313.     runtime.assertLog(['Message Event']);
    
  314. 
    
  315.     scheduleCallback(NormalPriority, () => {
    
  316.       runtime.log('B');
    
  317.     });
    
  318.     runtime.assertLog(['Post Message']);
    
  319.     runtime.fireMessageEvent();
    
  320.     runtime.assertLog(['Message Event', 'B']);
    
  321.   });
    
  322. 
    
  323.   it('when isInputPending is available, we can wait longer before yielding', () => {
    
  324.     function blockUntilSchedulerAsksToYield() {
    
  325.       while (!Scheduler.unstable_shouldYield()) {
    
  326.         runtime.advanceTime(1);
    
  327.       }
    
  328.       runtime.log(`Yield at ${performance.now()}ms`);
    
  329.     }
    
  330. 
    
  331.     // First show what happens when we don't request a paint
    
  332.     scheduleCallback(NormalPriority, () => {
    
  333.       runtime.log('Task with no pending input');
    
  334.       blockUntilSchedulerAsksToYield();
    
  335.     });
    
  336.     runtime.assertLog(['Post Message']);
    
  337. 
    
  338.     runtime.fireMessageEvent();
    
  339.     runtime.assertLog([
    
  340.       'Message Event',
    
  341.       'Task with no pending input',
    
  342.       // Even though there's no input, eventually Scheduler will yield
    
  343.       // regardless in case there's a pending main thread task we don't know
    
  344.       // about, like a network event.
    
  345.       gate(flags =>
    
  346.         flags.enableIsInputPending
    
  347.           ? 'Yield at 10ms'
    
  348.           : // When isInputPending is disabled, we always yield quickly
    
  349.             'Yield at 5ms',
    
  350.       ),
    
  351.     ]);
    
  352. 
    
  353.     runtime.resetTime();
    
  354. 
    
  355.     // Now do the same thing, but while the task is running, simulate an
    
  356.     // input event.
    
  357.     scheduleCallback(NormalPriority, () => {
    
  358.       runtime.log('Task with pending input');
    
  359.       runtime.scheduleDiscreteEvent();
    
  360.       blockUntilSchedulerAsksToYield();
    
  361.     });
    
  362.     runtime.assertLog(['Post Message']);
    
  363. 
    
  364.     runtime.fireMessageEvent();
    
  365.     runtime.assertLog([
    
  366.       'Message Event',
    
  367.       'Task with pending input',
    
  368.       // This time we yielded quickly to unblock the discrete event.
    
  369.       'Yield at 5ms',
    
  370.       'Discrete Event',
    
  371.     ]);
    
  372.   });
    
  373. 
    
  374.   it(
    
  375.     'isInputPending will also check for continuous inputs, but after a ' +
    
  376.       'slightly larger threshold',
    
  377.     () => {
    
  378.       function blockUntilSchedulerAsksToYield() {
    
  379.         while (!Scheduler.unstable_shouldYield()) {
    
  380.           runtime.advanceTime(1);
    
  381.         }
    
  382.         runtime.log(`Yield at ${performance.now()}ms`);
    
  383.       }
    
  384. 
    
  385.       // First show what happens when we don't request a paint
    
  386.       scheduleCallback(NormalPriority, () => {
    
  387.         runtime.log('Task with no pending input');
    
  388.         blockUntilSchedulerAsksToYield();
    
  389.       });
    
  390.       runtime.assertLog(['Post Message']);
    
  391. 
    
  392.       runtime.fireMessageEvent();
    
  393.       runtime.assertLog([
    
  394.         'Message Event',
    
  395.         'Task with no pending input',
    
  396.         // Even though there's no input, eventually Scheduler will yield
    
  397.         // regardless in case there's a pending main thread task we don't know
    
  398.         // about, like a network event.
    
  399.         gate(flags =>
    
  400.           flags.enableIsInputPending
    
  401.             ? 'Yield at 10ms'
    
  402.             : // When isInputPending is disabled, we always yield quickly
    
  403.               'Yield at 5ms',
    
  404.         ),
    
  405.       ]);
    
  406. 
    
  407.       runtime.resetTime();
    
  408. 
    
  409.       // Now do the same thing, but while the task is running, simulate a
    
  410.       // continuous input event.
    
  411.       scheduleCallback(NormalPriority, () => {
    
  412.         runtime.log('Task with continuous input');
    
  413.         runtime.scheduleContinuousEvent();
    
  414.         blockUntilSchedulerAsksToYield();
    
  415.       });
    
  416.       runtime.assertLog(['Post Message']);
    
  417. 
    
  418.       runtime.fireMessageEvent();
    
  419.       runtime.assertLog([
    
  420.         'Message Event',
    
  421.         'Task with continuous input',
    
  422.         // This time we yielded quickly to unblock the continuous event. But not
    
  423.         // as quickly as for a discrete event.
    
  424.         gate(flags =>
    
  425.           flags.enableIsInputPending
    
  426.             ? 'Yield at 10ms'
    
  427.             : // When isInputPending is disabled, we always yield quickly
    
  428.               'Yield at 5ms',
    
  429.         ),
    
  430.         'Continuous Event',
    
  431.       ]);
    
  432.     },
    
  433.   );
    
  434. 
    
  435.   it('requestPaint forces a yield at the end of the next frame interval', () => {
    
  436.     function blockUntilSchedulerAsksToYield() {
    
  437.       while (!Scheduler.unstable_shouldYield()) {
    
  438.         runtime.advanceTime(1);
    
  439.       }
    
  440.       runtime.log(`Yield at ${performance.now()}ms`);
    
  441.     }
    
  442. 
    
  443.     // First show what happens when we don't request a paint
    
  444.     scheduleCallback(NormalPriority, () => {
    
  445.       runtime.log('Task with no paint');
    
  446.       blockUntilSchedulerAsksToYield();
    
  447.     });
    
  448.     runtime.assertLog(['Post Message']);
    
  449. 
    
  450.     runtime.fireMessageEvent();
    
  451.     runtime.assertLog([
    
  452.       'Message Event',
    
  453.       'Task with no paint',
    
  454.       gate(flags =>
    
  455.         flags.enableIsInputPending
    
  456.           ? 'Yield at 10ms'
    
  457.           : // When isInputPending is disabled, we always yield quickly
    
  458.             'Yield at 5ms',
    
  459.       ),
    
  460.     ]);
    
  461. 
    
  462.     runtime.resetTime();
    
  463. 
    
  464.     // Now do the same thing, but call requestPaint inside the task
    
  465.     scheduleCallback(NormalPriority, () => {
    
  466.       runtime.log('Task with paint');
    
  467.       requestPaint();
    
  468.       blockUntilSchedulerAsksToYield();
    
  469.     });
    
  470.     runtime.assertLog(['Post Message']);
    
  471. 
    
  472.     runtime.fireMessageEvent();
    
  473.     runtime.assertLog([
    
  474.       'Message Event',
    
  475.       'Task with paint',
    
  476.       // This time we yielded quickly (5ms) because we requested a paint.
    
  477.       'Yield at 5ms',
    
  478.     ]);
    
  479.   });
    
  480. 
    
  481.   it('yielding continues in a new task regardless of how much time is remaining', () => {
    
  482.     scheduleCallback(NormalPriority, () => {
    
  483.       runtime.log('Original Task');
    
  484.       runtime.log('shouldYield: ' + shouldYield());
    
  485.       runtime.log('Return a continuation');
    
  486.       return () => {
    
  487.         runtime.log('Continuation Task');
    
  488.       };
    
  489.     });
    
  490.     runtime.assertLog(['Post Message']);
    
  491. 
    
  492.     runtime.fireMessageEvent();
    
  493.     runtime.assertLog([
    
  494.       'Message Event',
    
  495.       'Original Task',
    
  496.       // Immediately before returning a continuation, `shouldYield` returns
    
  497.       // false, which means there must be time remaining in the frame.
    
  498.       'shouldYield: false',
    
  499.       'Return a continuation',
    
  500. 
    
  501.       // The continuation should be scheduled in a separate macrotask even
    
  502.       // though there's time remaining.
    
  503.       'Post Message',
    
  504.     ]);
    
  505. 
    
  506.     // No time has elapsed
    
  507.     expect(performance.now()).toBe(0);
    
  508. 
    
  509.     runtime.fireMessageEvent();
    
  510.     runtime.assertLog(['Message Event', 'Continuation Task']);
    
  511.   });
    
  512. });