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