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 NormalPriority;
    
  21. let UserBlockingPriority;
    
  22. 
    
  23. // The Scheduler implementation uses browser APIs like `MessageChannel` and
    
  24. // `setTimeout` to schedule work on the main thread. Most of our tests treat
    
  25. // these as implementation details; however, the sequence and timing of these
    
  26. // APIs are not precisely specified, and can vary across browsers.
    
  27. //
    
  28. // To prevent regressions, we need the ability to simulate specific edge cases
    
  29. // that we may encounter in various browsers.
    
  30. //
    
  31. // This test suite mocks all browser methods used in our implementation. It
    
  32. // assumes as little as possible about the order and timing of events.
    
  33. describe('SchedulerDOMSetImmediate', () => {
    
  34.   beforeEach(() => {
    
  35.     jest.resetModules();
    
  36.     runtime = installMockBrowserRuntime();
    
  37.     jest.unmock('scheduler');
    
  38. 
    
  39.     performance = global.performance;
    
  40.     Scheduler = require('scheduler');
    
  41.     cancelCallback = Scheduler.unstable_cancelCallback;
    
  42.     scheduleCallback = Scheduler.unstable_scheduleCallback;
    
  43.     NormalPriority = Scheduler.unstable_NormalPriority;
    
  44.     UserBlockingPriority = Scheduler.unstable_UserBlockingPriority;
    
  45.   });
    
  46. 
    
  47.   afterEach(() => {
    
  48.     delete global.performance;
    
  49. 
    
  50.     if (!runtime.isLogEmpty()) {
    
  51.       throw Error('Test exited without clearing log.');
    
  52.     }
    
  53.   });
    
  54. 
    
  55.   function installMockBrowserRuntime() {
    
  56.     let timerIDCounter = 0;
    
  57.     // let timerIDs = new Map();
    
  58. 
    
  59.     let eventLog = [];
    
  60. 
    
  61.     let currentTime = 0;
    
  62. 
    
  63.     global.performance = {
    
  64.       now() {
    
  65.         return currentTime;
    
  66.       },
    
  67.     };
    
  68. 
    
  69.     global.setTimeout = (cb, delay) => {
    
  70.       const id = timerIDCounter++;
    
  71.       log(`Set Timer`);
    
  72.       return id;
    
  73.     };
    
  74.     global.clearTimeout = id => {
    
  75.       // TODO
    
  76.     };
    
  77. 
    
  78.     // Unused: we expect setImmediate to be preferred.
    
  79.     global.MessageChannel = function () {
    
  80.       return {
    
  81.         port1: {},
    
  82.         port2: {
    
  83.           postMessage() {
    
  84.             throw Error('Should be unused');
    
  85.           },
    
  86.         },
    
  87.       };
    
  88.     };
    
  89. 
    
  90.     let pendingSetImmediateCallback = null;
    
  91.     global.setImmediate = function (cb) {
    
  92.       if (pendingSetImmediateCallback) {
    
  93.         throw Error('Message event already scheduled');
    
  94.       }
    
  95.       log('Set Immediate');
    
  96.       pendingSetImmediateCallback = cb;
    
  97.     };
    
  98. 
    
  99.     function ensureLogIsEmpty() {
    
  100.       if (eventLog.length !== 0) {
    
  101.         throw Error('Log is not empty. Call assertLog before continuing.');
    
  102.       }
    
  103.     }
    
  104.     function advanceTime(ms) {
    
  105.       currentTime += ms;
    
  106.     }
    
  107.     function fireSetImmediate() {
    
  108.       ensureLogIsEmpty();
    
  109.       if (!pendingSetImmediateCallback) {
    
  110.         throw Error('No setImmediate was scheduled');
    
  111.       }
    
  112.       const cb = pendingSetImmediateCallback;
    
  113.       pendingSetImmediateCallback = null;
    
  114.       log('setImmediate Callback');
    
  115.       cb();
    
  116.     }
    
  117.     function log(val) {
    
  118.       eventLog.push(val);
    
  119.     }
    
  120.     function isLogEmpty() {
    
  121.       return eventLog.length === 0;
    
  122.     }
    
  123.     function assertLog(expected) {
    
  124.       const actual = eventLog;
    
  125.       eventLog = [];
    
  126.       expect(actual).toEqual(expected);
    
  127.     }
    
  128.     return {
    
  129.       advanceTime,
    
  130.       fireSetImmediate,
    
  131.       log,
    
  132.       isLogEmpty,
    
  133.       assertLog,
    
  134.     };
    
  135.   }
    
  136. 
    
  137.   it('does not use setImmediate override', () => {
    
  138.     global.setImmediate = () => {
    
  139.       throw new Error('Should not throw');
    
  140.     };
    
  141. 
    
  142.     scheduleCallback(NormalPriority, () => {
    
  143.       runtime.log('Task');
    
  144.     });
    
  145.     runtime.assertLog(['Set Immediate']);
    
  146.     runtime.fireSetImmediate();
    
  147.     runtime.assertLog(['setImmediate Callback', 'Task']);
    
  148.   });
    
  149. 
    
  150.   it('task that finishes before deadline', () => {
    
  151.     scheduleCallback(NormalPriority, () => {
    
  152.       runtime.log('Task');
    
  153.     });
    
  154.     runtime.assertLog(['Set Immediate']);
    
  155.     runtime.fireSetImmediate();
    
  156.     runtime.assertLog(['setImmediate Callback', 'Task']);
    
  157.   });
    
  158. 
    
  159.   it('task with continuation', () => {
    
  160.     scheduleCallback(NormalPriority, () => {
    
  161.       runtime.log('Task');
    
  162.       while (!Scheduler.unstable_shouldYield()) {
    
  163.         runtime.advanceTime(1);
    
  164.       }
    
  165.       runtime.log(`Yield at ${performance.now()}ms`);
    
  166.       return () => {
    
  167.         runtime.log('Continuation');
    
  168.       };
    
  169.     });
    
  170.     runtime.assertLog(['Set Immediate']);
    
  171. 
    
  172.     runtime.fireSetImmediate();
    
  173.     runtime.assertLog([
    
  174.       'setImmediate Callback',
    
  175.       'Task',
    
  176.       'Yield at 5ms',
    
  177.       'Set Immediate',
    
  178.     ]);
    
  179. 
    
  180.     runtime.fireSetImmediate();
    
  181.     runtime.assertLog(['setImmediate Callback', 'Continuation']);
    
  182.   });
    
  183. 
    
  184.   it('multiple tasks', () => {
    
  185.     scheduleCallback(NormalPriority, () => {
    
  186.       runtime.log('A');
    
  187.     });
    
  188.     scheduleCallback(NormalPriority, () => {
    
  189.       runtime.log('B');
    
  190.     });
    
  191.     runtime.assertLog(['Set Immediate']);
    
  192.     runtime.fireSetImmediate();
    
  193.     runtime.assertLog(['setImmediate Callback', 'A', 'B']);
    
  194.   });
    
  195. 
    
  196.   it('multiple tasks at different priority', () => {
    
  197.     scheduleCallback(NormalPriority, () => {
    
  198.       runtime.log('A');
    
  199.     });
    
  200.     scheduleCallback(UserBlockingPriority, () => {
    
  201.       runtime.log('B');
    
  202.     });
    
  203.     runtime.assertLog(['Set Immediate']);
    
  204.     runtime.fireSetImmediate();
    
  205.     runtime.assertLog(['setImmediate Callback', 'B', 'A']);
    
  206.   });
    
  207. 
    
  208.   it('multiple tasks with a yield in between', () => {
    
  209.     scheduleCallback(NormalPriority, () => {
    
  210.       runtime.log('A');
    
  211.       runtime.advanceTime(4999);
    
  212.     });
    
  213.     scheduleCallback(NormalPriority, () => {
    
  214.       runtime.log('B');
    
  215.     });
    
  216.     runtime.assertLog(['Set Immediate']);
    
  217.     runtime.fireSetImmediate();
    
  218.     runtime.assertLog([
    
  219.       'setImmediate Callback',
    
  220.       'A',
    
  221.       // Ran out of time. Post a continuation event.
    
  222.       'Set Immediate',
    
  223.     ]);
    
  224.     runtime.fireSetImmediate();
    
  225.     runtime.assertLog(['setImmediate Callback', 'B']);
    
  226.   });
    
  227. 
    
  228.   it('cancels tasks', () => {
    
  229.     const task = scheduleCallback(NormalPriority, () => {
    
  230.       runtime.log('Task');
    
  231.     });
    
  232.     runtime.assertLog(['Set Immediate']);
    
  233.     cancelCallback(task);
    
  234.     runtime.assertLog([]);
    
  235.   });
    
  236. 
    
  237.   it('throws when a task errors then continues in a new event', () => {
    
  238.     scheduleCallback(NormalPriority, () => {
    
  239.       runtime.log('Oops!');
    
  240.       throw Error('Oops!');
    
  241.     });
    
  242.     scheduleCallback(NormalPriority, () => {
    
  243.       runtime.log('Yay');
    
  244.     });
    
  245.     runtime.assertLog(['Set Immediate']);
    
  246. 
    
  247.     expect(() => runtime.fireSetImmediate()).toThrow('Oops!');
    
  248.     runtime.assertLog(['setImmediate Callback', 'Oops!', 'Set Immediate']);
    
  249. 
    
  250.     runtime.fireSetImmediate();
    
  251.     runtime.assertLog(['setImmediate Callback', 'Yay']);
    
  252.   });
    
  253. 
    
  254.   it('schedule new task after queue has emptied', () => {
    
  255.     scheduleCallback(NormalPriority, () => {
    
  256.       runtime.log('A');
    
  257.     });
    
  258. 
    
  259.     runtime.assertLog(['Set Immediate']);
    
  260.     runtime.fireSetImmediate();
    
  261.     runtime.assertLog(['setImmediate Callback', 'A']);
    
  262. 
    
  263.     scheduleCallback(NormalPriority, () => {
    
  264.       runtime.log('B');
    
  265.     });
    
  266.     runtime.assertLog(['Set Immediate']);
    
  267.     runtime.fireSetImmediate();
    
  268.     runtime.assertLog(['setImmediate Callback', 'B']);
    
  269.   });
    
  270. 
    
  271.   it('schedule new task after a cancellation', () => {
    
  272.     const handle = scheduleCallback(NormalPriority, () => {
    
  273.       runtime.log('A');
    
  274.     });
    
  275. 
    
  276.     runtime.assertLog(['Set Immediate']);
    
  277.     cancelCallback(handle);
    
  278. 
    
  279.     runtime.fireSetImmediate();
    
  280.     runtime.assertLog(['setImmediate Callback']);
    
  281. 
    
  282.     scheduleCallback(NormalPriority, () => {
    
  283.       runtime.log('B');
    
  284.     });
    
  285.     runtime.assertLog(['Set Immediate']);
    
  286.     runtime.fireSetImmediate();
    
  287.     runtime.assertLog(['setImmediate Callback', 'B']);
    
  288.   });
    
  289. });
    
  290. 
    
  291. it('does not crash if setImmediate is undefined', () => {
    
  292.   jest.resetModules();
    
  293.   const originalSetImmediate = global.setImmediate;
    
  294.   try {
    
  295.     delete global.setImmediate;
    
  296.     jest.unmock('scheduler');
    
  297.     expect(() => {
    
  298.       require('scheduler');
    
  299.     }).not.toThrow();
    
  300.   } finally {
    
  301.     global.setImmediate = originalSetImmediate;
    
  302.   }
    
  303. });