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 scheduleCallback;
19
+let NormalPriority;
20
+
21
+// The Scheduler implementation uses browser APIs like `MessageChannel`,
22
+// `requestAnimationFrame`, and `setTimeout` to schedule work on the main
23
+// thread. Most of our tests treat these as implementation details; however, the
24
+// sequence and timing of these APIs are not precisely specified, and can vary
25
+// wildly 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. The only
32
+// thing it assumes is that requestAnimationFrame is passed a frame time that is
33
+// equal to or less than the time returned by performance.now. Everything else
34
+// can be controlled at will.
35
+//
36
+// It also includes Scheduler-specific invariants, e.g. only one rAF callback
37
+// can be scheduled at a time.
38
+describe('SchedulerBrowser', () => {
39
+ beforeEach(() => {
40
+ jest.resetModules();
41
+
42
+ // Un-mock scheduler
43
+ jest.mock('scheduler', () => require.requireActual('scheduler'));
44
+ jest.mock('scheduler/src/SchedulerHostConfig', () =>
45
+ require.requireActual(
46
+ 'scheduler/src/forks/SchedulerHostConfig.default.js',
47
+ ),
48
+ );
49
+
50
+ runtime = installMockBrowserRuntime();
51
+ performance = window.performance;
52
+ Scheduler = require('scheduler');
53
+ scheduleCallback = Scheduler.unstable_scheduleCallback;
54
+ NormalPriority = Scheduler.unstable_NormalPriority;
55
+ });
56
+
57
+ afterEach(() => {
58
+ if (!runtime.isLogEmpty()) {
59
+ throw Error('Test exited without clearing log.');
60
+ }
61
+ });
62
+
63
+ function installMockBrowserRuntime() {
64
+ let VSYNC_INTERVAL = 33.33;
65
+
66
+ let hasPendingMessageEvent = false;
67
+
68
+ let rAFCallbackIDCounter = 0;
69
+ let rAFCallback = null;
70
+ let isRunningRAFCallback = false;
71
+
72
+ let rICCallbackIDCounter = 0;
73
+ let rICCallback = null;
74
+
75
+ let timerIDCounter = 0;
76
+ // let timerIDs = new Map();
77
+
78
+ let eventLog = [];
79
+
80
+ const window = {};
81
+ global.window = window;
82
+
83
+ let currentTime = 0;
84
+
85
+ window.performance = {
86
+ now() {
87
+ return currentTime;
88
+ },
89
+ };
90
+ window.requestAnimationFrame = cb => {
91
+ if (rAFCallback !== null) {
92
+ throw Error('rAF already scheduled');
93
+ }
94
+ if (isRunningRAFCallback) {
95
+ log('Request Animation Frame [Reposted]');
96
+ } else {
97
+ log('Request Animation Frame');
98
+ }
99
+ rAFCallback = cb;
100
+ return rAFCallbackIDCounter++;
101
+ };
102
+ window.cancelAnimationFrame = id => {
103
+ rAFCallback = null;
104
+ };
105
+
106
+ window.requestIdleCallback = cb => {
107
+ if (rICCallback !== null) {
108
+ throw Error('rAF already scheduled');
109
+ }
110
+ log('Request Idle Callback');
111
+ rICCallback = cb;
112
+ return rICCallbackIDCounter++;
113
+ };
114
+ window.cancelIdleCallback = id => {
115
+ rICCallback = null;
116
+ };
117
+
118
+ window.setTimeout = (cb, delay) => {
119
+ const id = timerIDCounter++;
120
+ log(`Set Timer`);
121
+ // TODO
122
+ return id;
123
+ };
124
+ window.clearTimeout = id => {
125
+ // TODO
126
+ };
127
+
128
+ const port1 = {};
129
+ const port2 = {
130
+ postMessage() {
131
+ if (hasPendingMessageEvent) {
132
+ throw Error('Message event already scheduled');
133
+ }
134
+ log('Post Message');
135
+ hasPendingMessageEvent = true;
136
+ },
137
+ };
138
+ global.MessageChannel = function MessageChannel() {
139
+ this.port1 = port1;
140
+ this.port2 = port2;
141
+ };
142
+
143
+ function ensureLogIsEmpty() {
144
+ if (eventLog.length !== 0) {
145
+ throw Error('Log is not empty. Call assertLog before continuing.');
146
+ }
147
+ }
148
+ function setHardwareFrameRate(fps) {
149
+ VSYNC_INTERVAL = 1000 / fps;
150
+ }
151
+ function advanceTime(ms) {
152
+ currentTime += ms;
153
+ }
154
+ function advanceTimeToNextFrame() {
155
+ const targetTime =
156
+ Math.ceil(currentTime / VSYNC_INTERVAL) * VSYNC_INTERVAL;
157
+ if (targetTime === currentTime) {
158
+ currentTime += VSYNC_INTERVAL;
159
+ } else {
160
+ currentTime = targetTime;
161
+ }
162
+ }
163
+ function getMostRecentFrameNumber() {
164
+ return Math.floor(currentTime / VSYNC_INTERVAL);
165
+ }
166
+ function fireMessageEvent() {
167
+ ensureLogIsEmpty();
168
+ if (!hasPendingMessageEvent) {
169
+ throw Error('No message event was scheduled');
170
+ }
171
+ hasPendingMessageEvent = false;
172
+ const onMessage = port1.onmessage;
173
+ log('Message Event');
174
+ onMessage();
175
+ }
176
+ function fireAnimationFrame() {
177
+ ensureLogIsEmpty();
178
+ if (isRunningRAFCallback) {
179
+ throw Error('Cannot fire animation frame from inside another event.');
180
+ }
181
+ if (rAFCallback === null) {
182
+ throw Error('No rAF scheduled.');
183
+ }
184
+ const mostRecentFrameNumber = getMostRecentFrameNumber();
185
+ const rAFTime = mostRecentFrameNumber * VSYNC_INTERVAL;
186
+ const cb = rAFCallback;
187
+ rAFCallback = null;
188
+ log(`Animation Frame [${mostRecentFrameNumber}]`);
189
+ isRunningRAFCallback = true;
190
+ try {
191
+ cb(rAFTime);
192
+ } finally {
193
+ isRunningRAFCallback = false;
194
+ }
195
+ }
196
+ function fireRIC() {
197
+ ensureLogIsEmpty();
198
+ if (rICCallback === null) {
199
+ throw Error('No rIC scheduled.');
200
+ }
201
+ const cb = rICCallback;
202
+ rICCallback = null;
203
+ log('Idle Callback');
204
+ cb();
205
+ }
206
+ function log(val) {
207
+ eventLog.push(val);
208
+ }
209
+ function isLogEmpty() {
210
+ return eventLog.length === 0;
211
+ }
212
+ function assertLog(expected) {
213
+ const actual = eventLog;
214
+ eventLog = [];
215
+ expect(actual).toEqual(expected);
216
+ }
217
+ return {
218
+ setHardwareFrameRate,
219
+ advanceTime,
220
+ advanceTimeToNextFrame,
221
+ getMostRecentFrameNumber,
222
+ fireMessageEvent,
223
+ fireAnimationFrame,
224
+ fireRIC,
225
+ log,
226
+ isLogEmpty,
227
+ assertLog,
228
+ };
229
+ }
230
+
231
+ it('callback with continuation', () => {
232
+ scheduleCallback(NormalPriority, () => {
233
+ runtime.log('Task');
234
+ while (!Scheduler.unstable_shouldYield()) {
235
+ runtime.advanceTime(1);
236
+ }
237
+ runtime.log(`Yield at ${performance.now()}ms`);
238
+ return () => {
239
+ runtime.log('Continuation');
240
+ };
241
+ });
242
+ runtime.assertLog(['Request Animation Frame']);
243
+
244
+ runtime.fireAnimationFrame();
245
+ runtime.assertLog([
246
+ 'Animation Frame [0]',
247
+ 'Request Animation Frame [Reposted]',
248
+ 'Set Timer',
249
+ 'Post Message',
250
+ ]);
251
+ runtime.fireMessageEvent();
252
+ runtime.assertLog(['Message Event', 'Task', 'Yield at 34ms']);
253
+
254
+ runtime.fireAnimationFrame();
255
+ runtime.assertLog([
256
+ 'Animation Frame [1]',
257
+ 'Request Animation Frame [Reposted]',
258
+ 'Set Timer',
259
+ 'Post Message',
260
+ ]);
261
+
262
+ runtime.fireMessageEvent();
263
+ runtime.assertLog(['Message Event', 'Continuation']);
264
+
265
+ runtime.advanceTimeToNextFrame();
266
+ runtime.fireAnimationFrame();
267
+ runtime.assertLog(['Animation Frame [2]']);
268
+ });
269
+
270
+ it('two rAF calls in the same frame', () => {
271
+ scheduleCallback(NormalPriority, () => runtime.log('A'));
272
+ runtime.assertLog(['Request Animation Frame']);
273
+ runtime.fireAnimationFrame();
274
+ runtime.assertLog([
275
+ 'Animation Frame [0]',
276
+ 'Request Animation Frame [Reposted]',
277
+ 'Set Timer',
278
+ 'Post Message',
279
+ ]);
280
+ runtime.fireMessageEvent();
281
+ runtime.assertLog(['Message Event', 'A']);
282
+
283
+ // The Scheduler queue is now empty. We're still in frame 0.
284
+ expect(runtime.getMostRecentFrameNumber()).toBe(0);
285
+
286
+ // Post a task to Scheduler.
287
+ scheduleCallback(NormalPriority, () => runtime.log('B'));
288
+
289
+ // Did not request another animation frame, since one was already scheduled
290
+ // during the previous rAF.
291
+ runtime.assertLog([]);
292
+
293
+ // Fire the animation frame.
294
+ runtime.fireAnimationFrame();
295
+ runtime.assertLog([
296
+ 'Animation Frame [0]',
297
+ 'Request Animation Frame [Reposted]',
298
+ 'Set Timer',
299
+ 'Post Message',
300
+ ]);
301
+
302
+ runtime.fireMessageEvent();
303
+ runtime.assertLog(['Message Event', 'B']);
304
+ });
305
+
306
+ it('adjusts frame rate by measuring inteval between rAF events', () => {
307
+ runtime.setHardwareFrameRate(60);
308
+
309
+ scheduleCallback(NormalPriority, () => runtime.log('Tick'));
310
+ runtime.assertLog(['Request Animation Frame']);
311
+
312
+ // Need to measure two consecutive intervals between frames.
313
+ for (let i = 0; i < 2; i++) {
314
+ runtime.fireAnimationFrame();
315
+ runtime.assertLog([
316
+ `Animation Frame [${runtime.getMostRecentFrameNumber()}]`,
317
+ 'Request Animation Frame [Reposted]',
318
+ 'Set Timer',
319
+ 'Post Message',
320
+ ]);
321
+ runtime.fireMessageEvent();
322
+ runtime.assertLog(['Message Event', 'Tick']);
323
+ scheduleCallback(NormalPriority, () => runtime.log('Tick'));
324
+ runtime.advanceTimeToNextFrame();
325
+ }
326
+
327
+ // Scheduler should observe that it's receiving rAFs every 16.6 ms and
328
+ // adjust its frame rate accordingly. Test by blocking the thread until
329
+ // Scheduler tells us to yield. Then measure how much time has elapsed.
330
+ const start = performance.now();
331
+ scheduleCallback(NormalPriority, () => {
332
+ while (!Scheduler.unstable_shouldYield()) {
333
+ runtime.advanceTime(1);
334
+ }
335
+ });
336
+ runtime.fireAnimationFrame();
337
+ runtime.assertLog([
338
+ `Animation Frame [${runtime.getMostRecentFrameNumber()}]`,
339
+ 'Request Animation Frame [Reposted]',
340
+ 'Set Timer',
341
+ 'Post Message',
342
+ ]);
343
+ runtime.fireMessageEvent();
344
+ runtime.assertLog(['Message Event', 'Tick']);
345
+ const end = performance.now();
346
+
347
+ // Check how much time elapsed in the frame.
348
+ expect(end - start).toEqual(17);
349
+ });
350
+});