main
js 351 lines 9.47 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 requestPaint;
19 let shouldYield;
20 let NormalPriority;
21 let SchedulerFeatureFlags;
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('SchedulerBrowser', () => {
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 requestPaint = Scheduler.unstable_requestPaint;
45 shouldYield = Scheduler.unstable_shouldYield;
46 SchedulerFeatureFlags = require('../SchedulerFeatureFlags');
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 function ensureLogIsEmpty() {
105 if (eventLog.length !== 0) {
106 throw Error('Log is not empty. Call assertLog before continuing.');
107 }
108 }
109 function advanceTime(ms) {
110 currentTime += ms;
111 }
112 function resetTime() {
113 currentTime = 0;
114 }
115 function fireMessageEvent() {
116 ensureLogIsEmpty();
117 if (!hasPendingMessageEvent) {
118 throw Error('No message event was scheduled');
119 }
120 hasPendingMessageEvent = false;
121 const onMessage = port1.onmessage;
122 log('Message Event');
123
124 isFiringMessageEvent = true;
125 try {
126 onMessage();
127 } finally {
128 isFiringMessageEvent = false;
129 if (hasPendingDiscreteEvent) {
130 log('Discrete Event');
131 hasPendingDiscreteEvent = false;
132 }
133 if (hasPendingContinuousEvent) {
134 log('Continuous Event');
135 hasPendingContinuousEvent = false;
136 }
137 }
138 }
139 function scheduleDiscreteEvent() {
140 if (isFiringMessageEvent) {
141 hasPendingDiscreteEvent = true;
142 } else {
143 log('Discrete Event');
144 }
145 }
146 function scheduleContinuousEvent() {
147 if (isFiringMessageEvent) {
148 hasPendingContinuousEvent = true;
149 } else {
150 log('Continuous Event');
151 }
152 }
153 function log(val) {
154 eventLog.push(val);
155 }
156 function isLogEmpty() {
157 return eventLog.length === 0;
158 }
159 function assertLog(expected) {
160 const actual = eventLog;
161 eventLog = [];
162 expect(actual).toEqual(expected);
163 }
164 return {
165 advanceTime,
166 resetTime,
167 fireMessageEvent,
168 log,
169 isLogEmpty,
170 assertLog,
171 scheduleDiscreteEvent,
172 scheduleContinuousEvent,
173 };
174 }
175
176 it('task that finishes before deadline', () => {
177 scheduleCallback(NormalPriority, () => {
178 runtime.log('Task');
179 });
180 runtime.assertLog(['Post Message']);
181 runtime.fireMessageEvent();
182 runtime.assertLog(['Message Event', 'Task']);
183 });
184
185 it('task with continuation', () => {
186 scheduleCallback(NormalPriority, () => {
187 runtime.log('Task');
188 // Request paint so that we yield immediately
189 requestPaint();
190 while (!Scheduler.unstable_shouldYield()) {
191 runtime.advanceTime(1);
192 }
193 runtime.log(`Yield at ${performance.now()}ms`);
194 return () => {
195 runtime.log('Continuation');
196 };
197 });
198 runtime.assertLog(['Post Message']);
199
200 runtime.fireMessageEvent();
201 runtime.assertLog([
202 'Message Event',
203 'Task',
204 gate(flags => flags.enableAlwaysYieldScheduler) ||
205 !SchedulerFeatureFlags.enableRequestPaint
206 ? gate(flags => (flags.www ? 'Yield at 10ms' : 'Yield at 5ms'))
207 : 'Yield at 0ms',
208 'Post Message',
209 ]);
210
211 runtime.fireMessageEvent();
212 runtime.assertLog(['Message Event', 'Continuation']);
213 });
214
215 it('multiple tasks', () => {
216 scheduleCallback(NormalPriority, () => {
217 runtime.log('A');
218 });
219 scheduleCallback(NormalPriority, () => {
220 runtime.log('B');
221 });
222 runtime.assertLog(['Post Message']);
223 runtime.fireMessageEvent();
224 if (gate(flags => flags.enableAlwaysYieldScheduler)) {
225 runtime.assertLog(['Message Event', 'A', 'Post Message']);
226 runtime.fireMessageEvent();
227 runtime.assertLog(['Message Event', 'B']);
228 } else {
229 runtime.assertLog(['Message Event', 'A', 'B']);
230 }
231 });
232
233 it('multiple tasks with a yield in between', () => {
234 scheduleCallback(NormalPriority, () => {
235 runtime.log('A');
236 runtime.advanceTime(4999);
237 });
238 scheduleCallback(NormalPriority, () => {
239 runtime.log('B');
240 });
241 runtime.assertLog(['Post Message']);
242 runtime.fireMessageEvent();
243 runtime.assertLog([
244 'Message Event',
245 'A',
246 // Ran out of time. Post a continuation event.
247 'Post Message',
248 ]);
249 runtime.fireMessageEvent();
250 runtime.assertLog(['Message Event', 'B']);
251 });
252
253 it('cancels tasks', () => {
254 const task = scheduleCallback(NormalPriority, () => {
255 runtime.log('Task');
256 });
257 runtime.assertLog(['Post Message']);
258 cancelCallback(task);
259 runtime.fireMessageEvent();
260 runtime.assertLog(['Message Event']);
261 });
262
263 it('throws when a task errors then continues in a new event', () => {
264 scheduleCallback(NormalPriority, () => {
265 runtime.log('Oops!');
266 throw Error('Oops!');
267 });
268 scheduleCallback(NormalPriority, () => {
269 runtime.log('Yay');
270 });
271 runtime.assertLog(['Post Message']);
272
273 expect(() => runtime.fireMessageEvent()).toThrow('Oops!');
274 runtime.assertLog(['Message Event', 'Oops!', 'Post Message']);
275
276 runtime.fireMessageEvent();
277 if (gate(flags => flags.enableAlwaysYieldScheduler)) {
278 runtime.assertLog(['Message Event', 'Post Message']);
279 runtime.fireMessageEvent();
280 }
281 runtime.assertLog(['Message Event', 'Yay']);
282 });
283
284 it('schedule new task after queue has emptied', () => {
285 scheduleCallback(NormalPriority, () => {
286 runtime.log('A');
287 });
288
289 runtime.assertLog(['Post Message']);
290 runtime.fireMessageEvent();
291 runtime.assertLog(['Message Event', 'A']);
292
293 scheduleCallback(NormalPriority, () => {
294 runtime.log('B');
295 });
296 runtime.assertLog(['Post Message']);
297 runtime.fireMessageEvent();
298 runtime.assertLog(['Message Event', 'B']);
299 });
300
301 it('schedule new task after a cancellation', () => {
302 const handle = scheduleCallback(NormalPriority, () => {
303 runtime.log('A');
304 });
305
306 runtime.assertLog(['Post Message']);
307 cancelCallback(handle);
308
309 runtime.fireMessageEvent();
310 runtime.assertLog(['Message Event']);
311
312 scheduleCallback(NormalPriority, () => {
313 runtime.log('B');
314 });
315 runtime.assertLog(['Post Message']);
316 runtime.fireMessageEvent();
317 runtime.assertLog(['Message Event', 'B']);
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 Message']);
330
331 runtime.fireMessageEvent();
332 runtime.assertLog([
333 'Message Event',
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 'Post Message',
343 ]);
344
345 // No time has elapsed
346 expect(performance.now()).toBe(0);
347
348 runtime.fireMessageEvent();
349 runtime.assertLog(['Message Event', 'Continuation Task']);
350 });
351 });