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