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 ImmediatePriority;
    
  21. let NormalPriority;
    
  22. let UserBlockingPriority;
    
  23. let LowPriority;
    
  24. let IdlePriority;
    
  25. let shouldYield;
    
  26. 
    
  27. // The Scheduler postTask implementation uses a new postTask browser API to
    
  28. // schedule work on the main thread. This test suite mocks all browser methods
    
  29. // used in our implementation. It assumes as little as possible about the order
    
  30. // and timing of events.
    
  31. describe('SchedulerPostTask', () => {
    
  32.   beforeEach(() => {
    
  33.     jest.resetModules();
    
  34.     jest.mock('scheduler', () =>
    
  35.       jest.requireActual('scheduler/unstable_post_task'),
    
  36.     );
    
  37. 
    
  38.     runtime = installMockBrowserRuntime();
    
  39.     performance = window.performance;
    
  40.     Scheduler = require('scheduler');
    
  41.     cancelCallback = Scheduler.unstable_cancelCallback;
    
  42.     scheduleCallback = Scheduler.unstable_scheduleCallback;
    
  43.     ImmediatePriority = Scheduler.unstable_ImmediatePriority;
    
  44.     UserBlockingPriority = Scheduler.unstable_UserBlockingPriority;
    
  45.     NormalPriority = Scheduler.unstable_NormalPriority;
    
  46.     LowPriority = Scheduler.unstable_LowPriority;
    
  47.     IdlePriority = Scheduler.unstable_IdlePriority;
    
  48.     shouldYield = Scheduler.unstable_shouldYield;
    
  49.   });
    
  50. 
    
  51.   afterEach(() => {
    
  52.     if (!runtime.isLogEmpty()) {
    
  53.       throw Error('Test exited without clearing log.');
    
  54.     }
    
  55.   });
    
  56. 
    
  57.   function installMockBrowserRuntime() {
    
  58.     let taskQueue = new Map();
    
  59.     let eventLog = [];
    
  60. 
    
  61.     // Mock window functions
    
  62.     const window = {};
    
  63.     global.window = window;
    
  64. 
    
  65.     let idCounter = 0;
    
  66.     let currentTime = 0;
    
  67.     window.performance = {
    
  68.       now() {
    
  69.         return currentTime;
    
  70.       },
    
  71.     };
    
  72. 
    
  73.     // Note: setTimeout is used to report errors and nothing else.
    
  74.     window.setTimeout = cb => {
    
  75.       try {
    
  76.         cb();
    
  77.       } catch (error) {
    
  78.         runtime.log(`Error: ${error.message}`);
    
  79.       }
    
  80.     };
    
  81. 
    
  82.     // Mock browser scheduler.
    
  83.     const scheduler = {};
    
  84.     global.scheduler = scheduler;
    
  85. 
    
  86.     scheduler.postTask = function (callback, {signal}) {
    
  87.       const {priority} = signal;
    
  88.       const id = idCounter++;
    
  89.       log(
    
  90.         `Post Task ${id} [${priority === undefined ? '<default>' : priority}]`,
    
  91.       );
    
  92.       const controller = signal._controller;
    
  93.       return new Promise((resolve, reject) => {
    
  94.         taskQueue.set(controller, {id, callback, resolve, reject});
    
  95.       });
    
  96.     };
    
  97. 
    
  98.     scheduler.yield = function ({signal}) {
    
  99.       const {priority} = signal;
    
  100.       const id = idCounter++;
    
  101.       log(`Yield ${id} [${priority === undefined ? '<default>' : priority}]`);
    
  102.       const controller = signal._controller;
    
  103.       let callback;
    
  104. 
    
  105.       return {
    
  106.         then(cb) {
    
  107.           callback = cb;
    
  108.           return new Promise((resolve, reject) => {
    
  109.             taskQueue.set(controller, {id, callback, resolve, reject});
    
  110.           });
    
  111.         },
    
  112.       };
    
  113.     };
    
  114. 
    
  115.     global.TaskController = class TaskController {
    
  116.       constructor({priority}) {
    
  117.         this.signal = {_controller: this, priority};
    
  118.       }
    
  119.       abort() {
    
  120.         const task = taskQueue.get(this);
    
  121.         if (task !== undefined) {
    
  122.           taskQueue.delete(this);
    
  123.           const reject = task.reject;
    
  124.           reject(new Error('Aborted'));
    
  125.         }
    
  126.       }
    
  127.     };
    
  128. 
    
  129.     function ensureLogIsEmpty() {
    
  130.       if (eventLog.length !== 0) {
    
  131.         throw Error('Log is not empty. Call assertLog before continuing.');
    
  132.       }
    
  133.     }
    
  134.     function advanceTime(ms) {
    
  135.       currentTime += ms;
    
  136.     }
    
  137.     function flushTasks() {
    
  138.       ensureLogIsEmpty();
    
  139. 
    
  140.       // If there's a continuation, it will call postTask again
    
  141.       // which will set nextTask. That means we need to clear
    
  142.       // nextTask before the invocation, otherwise we would
    
  143.       // delete the continuation task.
    
  144.       const prevTaskQueue = taskQueue;
    
  145.       taskQueue = new Map();
    
  146.       for (const [, {id, callback, resolve}] of prevTaskQueue) {
    
  147.         log(`Task ${id} Fired`);
    
  148.         callback(false);
    
  149.         resolve();
    
  150.       }
    
  151.     }
    
  152.     function log(val) {
    
  153.       eventLog.push(val);
    
  154.     }
    
  155.     function isLogEmpty() {
    
  156.       return eventLog.length === 0;
    
  157.     }
    
  158.     function assertLog(expected) {
    
  159.       const actual = eventLog;
    
  160.       eventLog = [];
    
  161.       expect(actual).toEqual(expected);
    
  162.     }
    
  163.     return {
    
  164.       advanceTime,
    
  165.       flushTasks,
    
  166.       log,
    
  167.       isLogEmpty,
    
  168.       assertLog,
    
  169.     };
    
  170.   }
    
  171. 
    
  172.   it('task that finishes before deadline', () => {
    
  173.     scheduleCallback(NormalPriority, () => {
    
  174.       runtime.log('A');
    
  175.     });
    
  176.     runtime.assertLog(['Post Task 0 [user-visible]']);
    
  177.     runtime.flushTasks();
    
  178.     runtime.assertLog(['Task 0 Fired', 'A']);
    
  179.   });
    
  180. 
    
  181.   it('task with continuation', () => {
    
  182.     scheduleCallback(NormalPriority, () => {
    
  183.       runtime.log('A');
    
  184.       while (!Scheduler.unstable_shouldYield()) {
    
  185.         runtime.advanceTime(1);
    
  186.       }
    
  187.       runtime.log(`Yield at ${performance.now()}ms`);
    
  188.       return () => {
    
  189.         runtime.log('Continuation');
    
  190.       };
    
  191.     });
    
  192.     runtime.assertLog(['Post Task 0 [user-visible]']);
    
  193. 
    
  194.     runtime.flushTasks();
    
  195.     runtime.assertLog([
    
  196.       'Task 0 Fired',
    
  197.       'A',
    
  198.       'Yield at 5ms',
    
  199.       'Yield 1 [user-visible]',
    
  200.     ]);
    
  201. 
    
  202.     runtime.flushTasks();
    
  203.     runtime.assertLog(['Task 1 Fired', 'Continuation']);
    
  204.   });
    
  205. 
    
  206.   it('multiple tasks', () => {
    
  207.     scheduleCallback(NormalPriority, () => {
    
  208.       runtime.log('A');
    
  209.     });
    
  210.     scheduleCallback(NormalPriority, () => {
    
  211.       runtime.log('B');
    
  212.     });
    
  213.     runtime.assertLog([
    
  214.       'Post Task 0 [user-visible]',
    
  215.       'Post Task 1 [user-visible]',
    
  216.     ]);
    
  217.     runtime.flushTasks();
    
  218.     runtime.assertLog(['Task 0 Fired', 'A', 'Task 1 Fired', 'B']);
    
  219.   });
    
  220. 
    
  221.   it('cancels tasks', () => {
    
  222.     const task = scheduleCallback(NormalPriority, () => {
    
  223.       runtime.log('A');
    
  224.     });
    
  225.     runtime.assertLog(['Post Task 0 [user-visible]']);
    
  226.     cancelCallback(task);
    
  227.     runtime.flushTasks();
    
  228.     runtime.assertLog([]);
    
  229.   });
    
  230. 
    
  231.   it('an error in one task does not affect execution of other tasks', () => {
    
  232.     scheduleCallback(NormalPriority, () => {
    
  233.       throw Error('Oops!');
    
  234.     });
    
  235.     scheduleCallback(NormalPriority, () => {
    
  236.       runtime.log('Yay');
    
  237.     });
    
  238.     runtime.assertLog([
    
  239.       'Post Task 0 [user-visible]',
    
  240.       'Post Task 1 [user-visible]',
    
  241.     ]);
    
  242.     runtime.flushTasks();
    
  243.     runtime.assertLog(['Task 0 Fired', 'Error: Oops!', 'Task 1 Fired', 'Yay']);
    
  244.   });
    
  245. 
    
  246.   it('schedule new task after queue has emptied', () => {
    
  247.     scheduleCallback(NormalPriority, () => {
    
  248.       runtime.log('A');
    
  249.     });
    
  250. 
    
  251.     runtime.assertLog(['Post Task 0 [user-visible]']);
    
  252.     runtime.flushTasks();
    
  253.     runtime.assertLog(['Task 0 Fired', 'A']);
    
  254. 
    
  255.     scheduleCallback(NormalPriority, () => {
    
  256.       runtime.log('B');
    
  257.     });
    
  258.     runtime.assertLog(['Post Task 1 [user-visible]']);
    
  259.     runtime.flushTasks();
    
  260.     runtime.assertLog(['Task 1 Fired', 'B']);
    
  261.   });
    
  262. 
    
  263.   it('schedule new task after a cancellation', () => {
    
  264.     const handle = scheduleCallback(NormalPriority, () => {
    
  265.       runtime.log('A');
    
  266.     });
    
  267. 
    
  268.     runtime.assertLog(['Post Task 0 [user-visible]']);
    
  269.     cancelCallback(handle);
    
  270. 
    
  271.     runtime.flushTasks();
    
  272.     runtime.assertLog([]);
    
  273. 
    
  274.     scheduleCallback(NormalPriority, () => {
    
  275.       runtime.log('B');
    
  276.     });
    
  277.     runtime.assertLog(['Post Task 1 [user-visible]']);
    
  278.     runtime.flushTasks();
    
  279.     runtime.assertLog(['Task 1 Fired', 'B']);
    
  280.   });
    
  281. 
    
  282.   it('schedules tasks at different priorities', () => {
    
  283.     scheduleCallback(ImmediatePriority, () => {
    
  284.       runtime.log('A');
    
  285.     });
    
  286.     scheduleCallback(UserBlockingPriority, () => {
    
  287.       runtime.log('B');
    
  288.     });
    
  289.     scheduleCallback(NormalPriority, () => {
    
  290.       runtime.log('C');
    
  291.     });
    
  292.     scheduleCallback(LowPriority, () => {
    
  293.       runtime.log('D');
    
  294.     });
    
  295.     scheduleCallback(IdlePriority, () => {
    
  296.       runtime.log('E');
    
  297.     });
    
  298.     runtime.assertLog([
    
  299.       'Post Task 0 [user-blocking]',
    
  300.       'Post Task 1 [user-blocking]',
    
  301.       'Post Task 2 [user-visible]',
    
  302.       'Post Task 3 [user-visible]',
    
  303.       'Post Task 4 [background]',
    
  304.     ]);
    
  305.     runtime.flushTasks();
    
  306.     runtime.assertLog([
    
  307.       'Task 0 Fired',
    
  308.       'A',
    
  309.       'Task 1 Fired',
    
  310.       'B',
    
  311.       'Task 2 Fired',
    
  312.       'C',
    
  313.       'Task 3 Fired',
    
  314.       'D',
    
  315.       'Task 4 Fired',
    
  316.       'E',
    
  317.     ]);
    
  318.   });
    
  319. 
    
  320.   it('yielding continues in a new task regardless of how much time is remaining', () => {
    
  321.     scheduleCallback(NormalPriority, () => {
    
  322.       runtime.log('Original Task');
    
  323.       runtime.log('shouldYield: ' + shouldYield());
    
  324.       runtime.log('Return a continuation');
    
  325.       return () => {
    
  326.         runtime.log('Continuation Task');
    
  327.       };
    
  328.     });
    
  329.     runtime.assertLog(['Post Task 0 [user-visible]']);
    
  330. 
    
  331.     runtime.flushTasks();
    
  332.     runtime.assertLog([
    
  333.       'Task 0 Fired',
    
  334.       'Original Task',
    
  335.       // Immediately before returning a continuation, `shouldYield` returns
    
  336.       // false, which means there must be time remaining in the frame.
    
  337.       'shouldYield: false',
    
  338.       'Return a continuation',
    
  339. 
    
  340.       // The continuation should be scheduled in a separate macrotask even
    
  341.       // though there's time remaining.
    
  342.       'Yield 1 [user-visible]',
    
  343.     ]);
    
  344. 
    
  345.     // No time has elapsed
    
  346.     expect(performance.now()).toBe(0);
    
  347. 
    
  348.     runtime.flushTasks();
    
  349.     runtime.assertLog(['Task 1 Fired', 'Continuation Task']);
    
  350.   });
    
  351. 
    
  352.   describe('falls back to postTask for scheduling continuations when scheduler.yield is not available', () => {
    
  353.     beforeEach(() => {
    
  354.       delete global.scheduler.yield;
    
  355.     });
    
  356. 
    
  357.     it('task with continuation', () => {
    
  358.       scheduleCallback(NormalPriority, () => {
    
  359.         runtime.log('A');
    
  360.         while (!Scheduler.unstable_shouldYield()) {
    
  361.           runtime.advanceTime(1);
    
  362.         }
    
  363.         runtime.log(`Yield at ${performance.now()}ms`);
    
  364.         return () => {
    
  365.           runtime.log('Continuation');
    
  366.         };
    
  367.       });
    
  368.       runtime.assertLog(['Post Task 0 [user-visible]']);
    
  369. 
    
  370.       runtime.flushTasks();
    
  371.       runtime.assertLog([
    
  372.         'Task 0 Fired',
    
  373.         'A',
    
  374.         'Yield at 5ms',
    
  375.         'Post Task 1 [user-visible]',
    
  376.       ]);
    
  377. 
    
  378.       runtime.flushTasks();
    
  379.       runtime.assertLog(['Task 1 Fired', 'Continuation']);
    
  380.     });
    
  381. 
    
  382.     it('yielding continues in a new task regardless of how much time is remaining', () => {
    
  383.       scheduleCallback(NormalPriority, () => {
    
  384.         runtime.log('Original Task');
    
  385.         runtime.log('shouldYield: ' + shouldYield());
    
  386.         runtime.log('Return a continuation');
    
  387.         return () => {
    
  388.           runtime.log('Continuation Task');
    
  389.         };
    
  390.       });
    
  391.       runtime.assertLog(['Post Task 0 [user-visible]']);
    
  392. 
    
  393.       runtime.flushTasks();
    
  394.       runtime.assertLog([
    
  395.         'Task 0 Fired',
    
  396.         'Original Task',
    
  397.         // Immediately before returning a continuation, `shouldYield` returns
    
  398.         // false, which means there must be time remaining in the frame.
    
  399.         'shouldYield: false',
    
  400.         'Return a continuation',
    
  401. 
    
  402.         // The continuation should be scheduled in a separate macrotask even
    
  403.         // though there's time remaining.
    
  404.         'Post Task 1 [user-visible]',
    
  405.       ]);
    
  406. 
    
  407.       // No time has elapsed
    
  408.       expect(performance.now()).toBe(0);
    
  409. 
    
  410.       runtime.flushTasks();
    
  411.       runtime.assertLog(['Task 1 Fired', 'Continuation Task']);
    
  412.     });
    
  413.   });
    
  414. });