@samitouri / QOS-React / commits / 00748c53e1

Add new mock build of Scheduler with flush, yield API (#14964)

* Add new mock build of Scheduler with flush, yield API Test environments need a way to take control of the Scheduler queue and incrementally flush work. Our current tests accomplish this either using dynamic injection, or by using Jest's fake timers feature. Both of these options are fragile and rely too much on implementation details. In this new approach, we have a separate build of Scheduler that is specifically designed for test environments. We mock the default implementation like we would any other module; in our case, via Jest. This special build has methods like `flushAll` and `yieldValue` that control when work is flushed. These methods are based on equivalent methods we've been using to write incremental React tests. Eventually we may want to migrate the React tests to interact with the mock Scheduler directly, instead of going through the host config like we currently do. For now, I'm using our custom static injection infrastructure to create the two builds of Scheduler — a default build for DOM (which falls back to a naive timer based implementation), and the new mock build. I did it this way because it allows me to share most of the implementation, which isn't specific to a host environment — e.g. everything related to the priority queue. It may be better to duplicate the shared code instead, especially considering that future environments (like React Native) may have entirely forked implementations. I'd prefer to wait until the implementation stabilizes before worrying about that, but I'm open to changing this now if we decide it's important enough. * Mock Scheduler in bundle tests, too * Remove special case by making regex more restrictive

Andrew Clark committed Feb 26, 2019 at 20:51 UTC 00748c53e183952696157088a858352cc77b0010
31 files changed +1074 -1002
packages/jest-mock-scheduler/npm/index.js
+1 -5
@@ -1,7 +1,3 @@
1 'use strict';
2
3 -if (process.env.NODE_ENV === 'production') {
4 - module.exports = require('./cjs/jest-mock-scheduler.production.min.js');
5 -} else {
6 - module.exports = require('./cjs/jest-mock-scheduler.development.js');
7 -}
3 +module.exports = require('scheduler/unstable_mock');
packages/jest-mock-scheduler/src/JestMockScheduler.js deleted
-61
@@ -1,61 +0,0 @@
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 -
8 -// Max 31 bit integer. The max integer size in V8 for 32-bit systems.
9 -// Math.pow(2, 30) - 1
10 -// 0b111111111111111111111111111111
11 -const maxSigned31BitInt = 1073741823;
12 -
13 -export function mockRestore() {
14 - delete global._schedMock;
15 -}
16 -
17 -let callback = null;
18 -let currentTime = -1;
19 -
20 -function flushCallback(didTimeout, ms) {
21 - if (callback !== null) {
22 - let cb = callback;
23 - callback = null;
24 - try {
25 - currentTime = ms;
26 - cb(didTimeout);
27 - } finally {
28 - currentTime = -1;
29 - }
30 - }
31 -}
32 -
33 -function requestHostCallback(cb, ms) {
34 - if (currentTime !== -1) {
35 - // Protect against re-entrancy.
36 - setTimeout(requestHostCallback, 0, cb, ms);
37 - } else {
38 - callback = cb;
39 - setTimeout(flushCallback, ms, true, ms);
40 - setTimeout(flushCallback, maxSigned31BitInt, false, maxSigned31BitInt);
41 - }
42 -}
43 -
44 -function cancelHostCallback() {
45 - callback = null;
46 -}
47 -
48 -function shouldYieldToHost() {
49 - return false;
50 -}
51 -
52 -function getCurrentTime() {
53 - return currentTime === -1 ? 0 : currentTime;
54 -}
55 -
56 -global._schedMock = [
57 - requestHostCallback,
58 - cancelHostCallback,
59 - shouldYieldToHost,
60 - getCurrentTime,
61 -];
packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.internal.js
+19 -38
@@ -13,6 +13,7 @@ const React = require('react');
13 let ReactFeatureFlags = require('shared/ReactFeatureFlags');
14
15 let ReactDOM;
16 +let Scheduler;
17
18 const ConcurrentMode = React.unstable_ConcurrentMode;
19
@@ -25,33 +26,10 @@ describe('ReactDOMFiberAsync', () => {
26 let container;
27
28 beforeEach(() => {
28 - // TODO pull this into helper method, reduce repetition.
29 - // mock the browser APIs which are used in schedule:
30 - // - requestAnimationFrame should pass the DOMHighResTimeStamp argument
31 - // - calling 'window.postMessage' should actually fire postmessage handlers
32 - global.requestAnimationFrame = function(cb) {
33 - return setTimeout(() => {
34 - cb(Date.now());
35 - });
36 - };
37 - const originalAddEventListener = global.addEventListener;
38 - let postMessageCallback;
39 - global.addEventListener = function(eventName, callback, useCapture) {
40 - if (eventName === 'message') {
41 - postMessageCallback = callback;
42 - } else {
43 - originalAddEventListener(eventName, callback, useCapture);
44 - }
45 - };
46 - global.postMessage = function(messageKey, targetOrigin) {
47 - const postMessageEvent = {source: window, data: messageKey};
48 - if (postMessageCallback) {
49 - postMessageCallback(postMessageEvent);
50 - }
51 - };
29 jest.resetModules();
30 container = document.createElement('div');
31 ReactDOM = require('react-dom');
32 + Scheduler = require('scheduler');
33
34 document.body.appendChild(container);
35 });
@@ -124,6 +102,7 @@ describe('ReactDOMFiberAsync', () => {
102
103 // Should flush both updates now.
104 jest.runAllTimers();
105 + Scheduler.flushAll();
106 expect(asyncValueRef.current.textContent).toBe('hello');
107 expect(syncValueRef.current.textContent).toBe('hello');
108 });
@@ -133,6 +112,7 @@ describe('ReactDOMFiberAsync', () => {
112 jest.resetModules();
113 ReactFeatureFlags = require('shared/ReactFeatureFlags');
114 ReactDOM = require('react-dom');
115 + Scheduler = require('scheduler');
116 });
117
118 it('renders synchronously', () => {
@@ -160,18 +140,19 @@ describe('ReactDOMFiberAsync', () => {
140 ReactFeatureFlags = require('shared/ReactFeatureFlags');
141 ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
142 ReactDOM = require('react-dom');
143 + Scheduler = require('scheduler');
144 });
145
146 it('createRoot makes the entire tree async', () => {
147 const root = ReactDOM.unstable_createRoot(container);
148 root.render(<div>Hi</div>);
149 expect(container.textContent).toEqual('');
169 - jest.runAllTimers();
150 + Scheduler.flushAll();
151 expect(container.textContent).toEqual('Hi');
152
153 root.render(<div>Bye</div>);
154 expect(container.textContent).toEqual('Hi');
174 - jest.runAllTimers();
155 + Scheduler.flushAll();
156 expect(container.textContent).toEqual('Bye');
157 });
158
@@ -188,12 +169,12 @@ describe('ReactDOMFiberAsync', () => {
169 const root = ReactDOM.unstable_createRoot(container);
170 root.render(<Component />);
171 expect(container.textContent).toEqual('');
191 - jest.runAllTimers();
172 + Scheduler.flushAll();
173 expect(container.textContent).toEqual('0');
174
175 instance.setState({step: 1});
176 expect(container.textContent).toEqual('0');
196 - jest.runAllTimers();
177 + Scheduler.flushAll();
178 expect(container.textContent).toEqual('1');
179 });
180
@@ -213,11 +194,11 @@ describe('ReactDOMFiberAsync', () => {
194 </ConcurrentMode>,
195 container,
196 );
216 - jest.runAllTimers();
197 + Scheduler.flushAll();
198
199 instance.setState({step: 1});
200 expect(container.textContent).toEqual('0');
220 - jest.runAllTimers();
201 + Scheduler.flushAll();
202 expect(container.textContent).toEqual('1');
203 });
204
@@ -239,11 +220,11 @@ describe('ReactDOMFiberAsync', () => {
220 </div>,
221 container,
222 );
242 - jest.runAllTimers();
223 + Scheduler.flushAll();
224
225 instance.setState({step: 1});
226 expect(container.textContent).toEqual('0');
246 - jest.runAllTimers();
227 + Scheduler.flushAll();
228 expect(container.textContent).toEqual('1');
229 });
230
@@ -369,7 +350,7 @@ describe('ReactDOMFiberAsync', () => {
350 </ConcurrentMode>,
351 container,
352 );
372 - jest.runAllTimers();
353 + Scheduler.flushAll();
354
355 // Updates are async by default
356 instance.push('A');
@@ -392,7 +373,7 @@ describe('ReactDOMFiberAsync', () => {
373 expect(ops).toEqual(['BC']);
374
375 // Flush the async updates
395 - jest.runAllTimers();
376 + Scheduler.flushAll();
377 expect(container.textContent).toEqual('ABCD');
378 expect(ops).toEqual(['BC', 'ABCD']);
379 });
@@ -419,7 +400,7 @@ describe('ReactDOMFiberAsync', () => {
400 // Test that a normal update is async
401 inst.increment();
402 expect(container.textContent).toEqual('0');
422 - jest.runAllTimers();
403 + Scheduler.flushAll();
404 expect(container.textContent).toEqual('1');
405
406 let ops = [];
@@ -525,7 +506,7 @@ describe('ReactDOMFiberAsync', () => {
506 const root = ReactDOM.unstable_createRoot(container);
507 root.render(<Form />);
508 // Flush
528 - jest.runAllTimers();
509 + Scheduler.flushAll();
510
511 let disableButton = disableButtonRef.current;
512 expect(disableButton.tagName).toBe('BUTTON');
@@ -592,7 +573,7 @@ describe('ReactDOMFiberAsync', () => {
573 const root = ReactDOM.unstable_createRoot(container);
574 root.render(<Form />);
575 // Flush
595 - jest.runAllTimers();
576 + Scheduler.flushAll();
577
578 let disableButton = disableButtonRef.current;
579 expect(disableButton.tagName).toBe('BUTTON');
@@ -652,7 +633,7 @@ describe('ReactDOMFiberAsync', () => {
633 const root = ReactDOM.unstable_createRoot(container);
634 root.render(<Form />);
635 // Flush
655 - jest.runAllTimers();
636 + Scheduler.flushAll();
637
638 let enableButton = enableButtonRef.current;
639 expect(enableButton.tagName).toBe('BUTTON');
packages/react-dom/src/__tests__/ReactDOMHooks-test.js
+6 -4
@@ -11,6 +11,7 @@
11
12 let React;
13 let ReactDOM;
14 +let Scheduler;
15
16 describe('ReactDOMHooks', () => {
17 let container;
@@ -20,6 +21,7 @@ describe('ReactDOMHooks', () => {
21
22 React = require('react');
23 ReactDOM = require('react-dom');
24 + Scheduler = require('scheduler');
25
26 container = document.createElement('div');
27 document.body.appendChild(container);
@@ -55,7 +57,7 @@ describe('ReactDOMHooks', () => {
57 expect(container.textContent).toBe('1');
58 expect(container2.textContent).toBe('');
59 expect(container3.textContent).toBe('');
58 - jest.runAllTimers();
60 + Scheduler.flushAll();
61 expect(container.textContent).toBe('1');
62 expect(container2.textContent).toBe('2');
63 expect(container3.textContent).toBe('3');
@@ -64,7 +66,7 @@ describe('ReactDOMHooks', () => {
66 expect(container.textContent).toBe('2');
67 expect(container2.textContent).toBe('2'); // Not flushed yet
68 expect(container3.textContent).toBe('3'); // Not flushed yet
67 - jest.runAllTimers();
69 + Scheduler.flushAll();
70 expect(container.textContent).toBe('2');
71 expect(container2.textContent).toBe('4');
72 expect(container3.textContent).toBe('6');
@@ -166,14 +168,14 @@ describe('ReactDOMHooks', () => {
168 </React.unstable_ConcurrentMode>,
169 );
170
169 - jest.runAllTimers();
171 + Scheduler.flushAll();
172
173 inputRef.current.value = 'abc';
174 inputRef.current.dispatchEvent(
175 new Event('input', {bubbles: true, cancelable: true}),
176 );
177
176 - jest.runAllTimers();
178 + Scheduler.flushAll();
179
180 expect(labelRef.current.innerHTML).toBe('abc');
181 });
packages/react-dom/src/__tests__/ReactDOMRoot-test.js
+29 -67
@@ -12,74 +12,36 @@
12 let React = require('react');
13 let ReactDOM = require('react-dom');
14 let ReactDOMServer = require('react-dom/server');
15 +let Scheduler = require('scheduler');
16 let ConcurrentMode = React.unstable_ConcurrentMode;
17
18 describe('ReactDOMRoot', () => {
19 let container;
20
20 - let advanceCurrentTime;
21 -
21 beforeEach(() => {
23 - container = document.createElement('div');
24 - // TODO pull this into helper method, reduce repetition.
25 - // mock the browser APIs which are used in schedule:
26 - // - requestAnimationFrame should pass the DOMHighResTimeStamp argument
27 - // - calling 'window.postMessage' should actually fire postmessage handlers
28 - // - must allow artificially changing time returned by Date.now
29 - // Performance.now is not supported in the test environment
30 - const originalDateNow = Date.now;
31 - let advancedTime = null;
32 - global.Date.now = function() {
33 - if (advancedTime) {
34 - return originalDateNow() + advancedTime;
35 - }
36 - return originalDateNow();
37 - };
38 - advanceCurrentTime = function(amount) {
39 - advancedTime = amount;
40 - };
41 - global.requestAnimationFrame = function(cb) {
42 - return setTimeout(() => {
43 - cb(Date.now());
44 - });
45 - };
46 - const originalAddEventListener = global.addEventListener;
47 - let postMessageCallback;
48 - global.addEventListener = function(eventName, callback, useCapture) {
49 - if (eventName === 'message') {
50 - postMessageCallback = callback;
51 - } else {
52 - originalAddEventListener(eventName, callback, useCapture);
53 - }
54 - };
55 - global.postMessage = function(messageKey, targetOrigin) {
56 - const postMessageEvent = {source: window, data: messageKey};
57 - if (postMessageCallback) {
58 - postMessageCallback(postMessageEvent);
59 - }
60 - };
61 -
22 jest.resetModules();
23 + container = document.createElement('div');
24 React = require('react');
25 ReactDOM = require('react-dom');
26 ReactDOMServer = require('react-dom/server');
27 + Scheduler = require('scheduler');
28 ConcurrentMode = React.unstable_ConcurrentMode;
29 });
30
31 it('renders children', () => {
32 const root = ReactDOM.unstable_createRoot(container);
33 root.render(<div>Hi</div>);
72 - jest.runAllTimers();
34 + Scheduler.flushAll();
35 expect(container.textContent).toEqual('Hi');
36 });
37
38 it('unmounts children', () => {
39 const root = ReactDOM.unstable_createRoot(container);
40 root.render(<div>Hi</div>);
79 - jest.runAllTimers();
41 + Scheduler.flushAll();
42 expect(container.textContent).toEqual('Hi');
43 root.unmount();
82 - jest.runAllTimers();
44 + Scheduler.flushAll();
45 expect(container.textContent).toEqual('');
46 });
47
@@ -91,7 +53,7 @@ describe('ReactDOMRoot', () => {
53 ops.push('inside callback: ' + container.textContent);
54 });
55 ops.push('before committing: ' + container.textContent);
94 - jest.runAllTimers();
56 + Scheduler.flushAll();
57 ops.push('after committing: ' + container.textContent);
58 expect(ops).toEqual([
59 'before committing: ',
@@ -104,7 +66,7 @@ describe('ReactDOMRoot', () => {
66 it('resolves `work.then` callback synchronously if the work already committed', () => {
67 const root = ReactDOM.unstable_createRoot(container);
68 const work = root.render(<ConcurrentMode>Hi</ConcurrentMode>);
107 - jest.runAllTimers();
69 + Scheduler.flushAll();
70 let ops = [];
71 work.then(() => {
72 ops.push('inside callback');
@@ -132,7 +94,7 @@ describe('ReactDOMRoot', () => {
94 <span />
95 </div>,
96 );
135 - jest.runAllTimers();
97 + Scheduler.flushAll();
98
99 // Accepts `hydrate` option
100 const container2 = document.createElement('div');
@@ -143,7 +105,7 @@ describe('ReactDOMRoot', () => {
105 <span />
106 </div>,
107 );
146 - expect(jest.runAllTimers).toWarnDev('Extra attributes', {
108 + expect(() => Scheduler.flushAll()).toWarnDev('Extra attributes', {
109 withoutStack: true,
110 });
111 });
@@ -157,7 +119,7 @@ describe('ReactDOMRoot', () => {
119 <span>d</span>
120 </div>,
121 );
160 - jest.runAllTimers();
122 + Scheduler.flushAll();
123 expect(container.textContent).toEqual('abcd');
124 root.render(
125 <div>
@@ -165,7 +127,7 @@ describe('ReactDOMRoot', () => {
127 <span>c</span>
128 </div>,
129 );
168 - jest.runAllTimers();
130 + Scheduler.flushAll();
131 expect(container.textContent).toEqual('abdc');
132 });
133
@@ -201,7 +163,7 @@ describe('ReactDOMRoot', () => {
163 </ConcurrentMode>,
164 );
165
204 - jest.runAllTimers();
166 + Scheduler.flushAll();
167
168 // Hasn't updated yet
169 expect(container.textContent).toEqual('');
@@ -230,7 +192,7 @@ describe('ReactDOMRoot', () => {
192 const batch = root.createBatch();
193 batch.render(<Foo>Hi</Foo>);
194 // Flush all async work.
233 - jest.runAllTimers();
195 + Scheduler.flushAll();
196 // Root should complete without committing.
197 expect(ops).toEqual(['Foo']);
198 expect(container.textContent).toEqual('');
@@ -248,7 +210,7 @@ describe('ReactDOMRoot', () => {
210 const batch = root.createBatch();
211 batch.render(<ConcurrentMode>Foo</ConcurrentMode>);
212
251 - jest.runAllTimers();
213 + Scheduler.flushAll();
214
215 // Hasn't updated yet
216 expect(container.textContent).toEqual('');
@@ -288,7 +250,7 @@ describe('ReactDOMRoot', () => {
250 const root = ReactDOM.unstable_createRoot(container);
251 root.render(<ConcurrentMode>1</ConcurrentMode>);
252
291 - advanceCurrentTime(2000);
253 + Scheduler.advanceTime(2000);
254 // This batch has a later expiration time than the earlier update.
255 const batch = root.createBatch();
256
@@ -296,7 +258,7 @@ describe('ReactDOMRoot', () => {
258 batch.commit();
259 expect(container.textContent).toEqual('');
260
299 - jest.runAllTimers();
261 + Scheduler.flushAll();
262 expect(container.textContent).toEqual('1');
263 });
264
@@ -323,7 +285,7 @@ describe('ReactDOMRoot', () => {
285 batch1.render(1);
286
287 // This batch has a later expiration time
326 - advanceCurrentTime(2000);
288 + Scheduler.advanceTime(2000);
289 const batch2 = root.createBatch();
290 batch2.render(2);
291
@@ -342,7 +304,7 @@ describe('ReactDOMRoot', () => {
304 batch1.render(1);
305
306 // This batch has a later expiration time
345 - advanceCurrentTime(2000);
307 + Scheduler.advanceTime(2000);
308 const batch2 = root.createBatch();
309 batch2.render(2);
310
@@ -352,7 +314,7 @@ describe('ReactDOMRoot', () => {
314 expect(container.textContent).toEqual('2');
315
316 batch1.commit();
355 - jest.runAllTimers();
317 + Scheduler.flushAll();
318 expect(container.textContent).toEqual('1');
319 });
320
@@ -378,7 +340,7 @@ describe('ReactDOMRoot', () => {
340 it('warns when rendering with legacy API into createRoot() container', () => {
341 const root = ReactDOM.unstable_createRoot(container);
342 root.render(<div>Hi</div>);
381 - jest.runAllTimers();
343 + Scheduler.flushAll();
344 expect(container.textContent).toEqual('Hi');
345 expect(() => {
346 ReactDOM.render(<div>Bye</div>, container);
@@ -393,7 +355,7 @@ describe('ReactDOMRoot', () => {
355 ],
356 {withoutStack: true},
357 );
396 - jest.runAllTimers();
358 + Scheduler.flushAll();
359 // This works now but we could disallow it:
360 expect(container.textContent).toEqual('Bye');
361 });
@@ -401,7 +363,7 @@ describe('ReactDOMRoot', () => {
363 it('warns when hydrating with legacy API into createRoot() container', () => {
364 const root = ReactDOM.unstable_createRoot(container);
365 root.render(<div>Hi</div>);
404 - jest.runAllTimers();
366 + Scheduler.flushAll();
367 expect(container.textContent).toEqual('Hi');
368 expect(() => {
369 ReactDOM.hydrate(<div>Hi</div>, container);
@@ -421,7 +383,7 @@ describe('ReactDOMRoot', () => {
383 it('warns when unmounting with legacy API (no previous content)', () => {
384 const root = ReactDOM.unstable_createRoot(container);
385 root.render(<div>Hi</div>);
424 - jest.runAllTimers();
386 + Scheduler.flushAll();
387 expect(container.textContent).toEqual('Hi');
388 let unmounted = false;
389 expect(() => {
@@ -437,10 +399,10 @@ describe('ReactDOMRoot', () => {
399 {withoutStack: true},
400 );
401 expect(unmounted).toBe(false);
440 - jest.runAllTimers();
402 + Scheduler.flushAll();
403 expect(container.textContent).toEqual('Hi');
404 root.unmount();
443 - jest.runAllTimers();
405 + Scheduler.flushAll();
406 expect(container.textContent).toEqual('');
407 });
408
@@ -450,17 +412,17 @@ describe('ReactDOMRoot', () => {
412 // The rest is the same as test above.
413 const root = ReactDOM.unstable_createRoot(container);
414 root.render(<div>Hi</div>);
453 - jest.runAllTimers();
415 + Scheduler.flushAll();
416 expect(container.textContent).toEqual('Hi');
417 let unmounted = false;
418 expect(() => {
419 unmounted = ReactDOM.unmountComponentAtNode(container);
420 }).toWarnDev('Did you mean to call root.unmount()?', {withoutStack: true});
421 expect(unmounted).toBe(false);
460 - jest.runAllTimers();
422 + Scheduler.flushAll();
423 expect(container.textContent).toEqual('Hi');
424 root.unmount();
463 - jest.runAllTimers();
425 + Scheduler.flushAll();
426 expect(container.textContent).toEqual('');
427 });
428
packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+26 -1
@@ -12,6 +12,7 @@
12 let React;
13 let ReactDOM;
14 let ReactDOMServer;
15 +let Scheduler;
16 let ReactFeatureFlags;
17 let Suspense;
18 let act;
@@ -27,6 +28,7 @@ describe('ReactDOMServerPartialHydration', () => {
28 ReactDOM = require('react-dom');
29 act = require('react-dom/test-utils').act;
30 ReactDOMServer = require('react-dom/server');
31 + Scheduler = require('scheduler');
32 Suspense = React.Suspense;
33 });
34
@@ -72,6 +74,7 @@ describe('ReactDOMServerPartialHydration', () => {
74 suspend = true;
75 let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
76 root.render(<App />);
77 + Scheduler.flushAll();
78 jest.runAllTimers();
79
80 expect(ref.current).toBe(null);
@@ -80,6 +83,7 @@ describe('ReactDOMServerPartialHydration', () => {
83 suspend = false;
84 resolve();
85 await promise;
86 + Scheduler.flushAll();
87 jest.runAllTimers();
88
89 // We should now have hydrated with a ref on the existing span.
@@ -238,6 +242,7 @@ describe('ReactDOMServerPartialHydration', () => {
242 suspend = true;
243 let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
244 root.render(<App text="Hello" className="hello" />);
245 + Scheduler.flushAll();
246 jest.runAllTimers();
247
248 expect(ref.current).toBe(null);
@@ -253,6 +258,7 @@ describe('ReactDOMServerPartialHydration', () => {
258
259 // Flushing both of these in the same batch won't be able to hydrate so we'll
260 // probably throw away the existing subtree.
261 + Scheduler.flushAll();
262 jest.runAllTimers();
263
264 // Pick up the new span. In an ideal implementation this might be the same span
@@ -305,15 +311,17 @@ describe('ReactDOMServerPartialHydration', () => {
311 suspend = true;
312 let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
313 root.render(<App text="Hello" className="hello" />);
314 + Scheduler.flushAll();
315 jest.runAllTimers();
316
317 expect(ref.current).toBe(null);
318
319 // Render an update, but leave it still suspended.
320 root.render(<App text="Hi" className="hi" />);
321 + Scheduler.flushAll();
322 + jest.runAllTimers();
323
324 // Flushing now should delete the existing content and show the fallback.
316 - jest.runAllTimers();
325
326 expect(container.getElementsByTagName('span').length).toBe(0);
327 expect(ref.current).toBe(null);
@@ -324,6 +332,7 @@ describe('ReactDOMServerPartialHydration', () => {
332 resolve();
333 await promise;
334
335 + Scheduler.flushAll();
336 jest.runAllTimers();
337
338 let span = container.getElementsByTagName('span')[0];
@@ -375,6 +384,7 @@ describe('ReactDOMServerPartialHydration', () => {
384 suspend = true;
385 let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
386 root.render(<App text="Hello" className="hello" />);
387 + Scheduler.flushAll();
388 jest.runAllTimers();
389
390 expect(ref.current).toBe(null);
@@ -383,6 +393,7 @@ describe('ReactDOMServerPartialHydration', () => {
393 root.render(<App text="Hi" className="hi" />);
394
395 // Flushing now should delete the existing content and show the fallback.
396 + Scheduler.flushAll();
397 jest.runAllTimers();
398
399 expect(container.getElementsByTagName('span').length).toBe(0);
@@ -394,6 +405,7 @@ describe('ReactDOMServerPartialHydration', () => {
405 resolve();
406 await promise;
407
408 + Scheduler.flushAll();
409 jest.runAllTimers();
410
411 let span = container.getElementsByTagName('span')[0];
@@ -444,6 +456,7 @@ describe('ReactDOMServerPartialHydration', () => {
456 suspend = true;
457 let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
458 root.render(<App text="Hello" className="hello" />);
459 + Scheduler.flushAll();
460 jest.runAllTimers();
461
462 expect(ref.current).toBe(null);
@@ -452,6 +465,7 @@ describe('ReactDOMServerPartialHydration', () => {
465 root.render(<App text="Hi" className="hi" />);
466
467 // Flushing now should delete the existing content and show the fallback.
468 + Scheduler.flushAll();
469 jest.runAllTimers();
470
471 expect(container.getElementsByTagName('span').length).toBe(0);
@@ -463,6 +477,7 @@ describe('ReactDOMServerPartialHydration', () => {
477 resolve();
478 await promise;
479
480 + Scheduler.flushAll();
481 jest.runAllTimers();
482
483 let span = container.getElementsByTagName('span')[0];
@@ -522,6 +537,7 @@ describe('ReactDOMServerPartialHydration', () => {
537 <App />
538 </Context.Provider>,
539 );
540 + Scheduler.flushAll();
541 jest.runAllTimers();
542
543 expect(ref.current).toBe(null);
@@ -541,6 +557,7 @@ describe('ReactDOMServerPartialHydration', () => {
557
558 // Flushing both of these in the same batch won't be able to hydrate so we'll
559 // probably throw away the existing subtree.
560 + Scheduler.flushAll();
561 jest.runAllTimers();
562
563 // Pick up the new span. In an ideal implementation this might be the same span
@@ -603,6 +620,7 @@ describe('ReactDOMServerPartialHydration', () => {
620 <App />
621 </Context.Provider>,
622 );
623 + Scheduler.flushAll();
624 jest.runAllTimers();
625
626 expect(ref.current).toBe(null);
@@ -615,6 +633,7 @@ describe('ReactDOMServerPartialHydration', () => {
633 );
634
635 // Flushing now should delete the existing content and show the fallback.
636 + Scheduler.flushAll();
637 jest.runAllTimers();
638
639 expect(container.getElementsByTagName('span').length).toBe(0);
@@ -626,6 +645,7 @@ describe('ReactDOMServerPartialHydration', () => {
645 resolve();
646 await promise;
647
648 + Scheduler.flushAll();
649 jest.runAllTimers();
650
651 let span = container.getElementsByTagName('span')[0];
@@ -674,6 +694,7 @@ describe('ReactDOMServerPartialHydration', () => {
694 suspend = false;
695 let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
696 root.render(<App />);
697 + Scheduler.flushAll();
698 jest.runAllTimers();
699
700 expect(container.textContent).toBe('Hello');
@@ -746,6 +767,7 @@ describe('ReactDOMServerPartialHydration', () => {
767 suspend = false;
768 let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
769 root.render(<App />);
770 + Scheduler.flushAll();
771 jest.runAllTimers();
772
773 // We're still loading because we're waiting for the server to stream more content.
@@ -761,6 +783,7 @@ describe('ReactDOMServerPartialHydration', () => {
783 // But it is not yet hydrated.
784 expect(ref.current).toBe(null);
785
786 + Scheduler.flushAll();
787 jest.runAllTimers();
788
789 // Now it's hydrated.
@@ -837,6 +860,7 @@ describe('ReactDOMServerPartialHydration', () => {
860 suspend = false;
861 let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
862 root.render(<App />);
863 + Scheduler.flushAll();
864 jest.runAllTimers();
865
866 // We're still loading because we're waiting for the server to stream more content.
@@ -850,6 +874,7 @@ describe('ReactDOMServerPartialHydration', () => {
874 expect(container.textContent).toBe('Loading...');
875 expect(ref.current).toBe(null);
876
877 + Scheduler.flushAll();
878 jest.runAllTimers();
879
880 // Hydrating should've generated an error and replaced the suspense boundary.
packages/react-dom/src/__tests__/ReactErrorBoundaries-test.internal.js
+3 -2
@@ -13,6 +13,7 @@ let PropTypes;
13 let React;
14 let ReactDOM;
15 let ReactFeatureFlags;
16 +let Scheduler;
17
18 describe('ReactErrorBoundaries', () => {
19 let log;
@@ -44,6 +45,7 @@ describe('ReactErrorBoundaries', () => {
45 ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
46 ReactDOM = require('react-dom');
47 React = require('react');
48 + Scheduler = require('scheduler');
49
50 log = [];
51
@@ -1839,9 +1841,8 @@ describe('ReactErrorBoundaries', () => {
1841 expect(container.firstChild.textContent).toBe('Initial value');
1842 log.length = 0;
1843
1842 - jest.runAllTimers();
1843 -
1844 // Flush passive effects and handle the error
1845 + Scheduler.flushAll();
1846 expect(log).toEqual([
1847 'BrokenUseEffect useEffect [!]',
1848 // Handle the error
packages/react-dom/src/events/__tests__/ChangeEventPlugin-test.internal.js
+9 -7
@@ -12,6 +12,7 @@
12 const React = require('react');
13 let ReactDOM = require('react-dom');
14 let ReactFeatureFlags;
15 +let Scheduler;
16
17 const setUntrackedChecked = Object.getOwnPropertyDescriptor(
18 HTMLInputElement.prototype,
@@ -484,6 +485,7 @@ describe('ChangeEventPlugin', () => {
485 ReactFeatureFlags = require('shared/ReactFeatureFlags');
486 ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
487 ReactDOM = require('react-dom');
488 + Scheduler = require('scheduler');
489 });
490 it('text input', () => {
491 const root = ReactDOM.unstable_createRoot(container);
@@ -515,7 +517,7 @@ describe('ChangeEventPlugin', () => {
517 expect(ops).toEqual([]);
518 expect(input).toBe(undefined);
519 // Flush callbacks.
518 - jest.runAllTimers();
520 + Scheduler.flushAll();
521 expect(ops).toEqual(['render: initial']);
522 expect(input.value).toBe('initial');
523
@@ -565,7 +567,7 @@ describe('ChangeEventPlugin', () => {
567 expect(ops).toEqual([]);
568 expect(input).toBe(undefined);
569 // Flush callbacks.
568 - jest.runAllTimers();
570 + Scheduler.flushAll();
571 expect(ops).toEqual(['render: false']);
572 expect(input.checked).toBe(false);
573
@@ -581,7 +583,7 @@ describe('ChangeEventPlugin', () => {
583
584 // Now let's make sure we're using the controlled value.
585 root.render(<ControlledInput reverse={true} />);
584 - jest.runAllTimers();
586 + Scheduler.flushAll();
587
588 ops = [];
589
@@ -624,7 +626,7 @@ describe('ChangeEventPlugin', () => {
626 expect(ops).toEqual([]);
627 expect(textarea).toBe(undefined);
628 // Flush callbacks.
627 - jest.runAllTimers();
629 + Scheduler.flushAll();
630 expect(ops).toEqual(['render: initial']);
631 expect(textarea.value).toBe('initial');
632
@@ -675,7 +677,7 @@ describe('ChangeEventPlugin', () => {
677 expect(ops).toEqual([]);
678 expect(input).toBe(undefined);
679 // Flush callbacks.
678 - jest.runAllTimers();
680 + Scheduler.flushAll();
681 expect(ops).toEqual(['render: initial']);
682 expect(input.value).toBe('initial');
683
@@ -726,7 +728,7 @@ describe('ChangeEventPlugin', () => {
728 expect(ops).toEqual([]);
729 expect(input).toBe(undefined);
730 // Flush callbacks.
729 - jest.runAllTimers();
731 + Scheduler.flushAll();
732 expect(ops).toEqual(['render: initial']);
733 expect(input.value).toBe('initial');
734
@@ -741,7 +743,7 @@ describe('ChangeEventPlugin', () => {
743 expect(input.value).toBe('initial');
744
745 // Flush callbacks.
744 - jest.runAllTimers();
746 + Scheduler.flushAll();
747 // Now the click update has flushed.
748 expect(ops).toEqual(['render: ']);
749 expect(input.value).toBe('');
packages/react-dom/src/events/__tests__/SimpleEventPlugin-test.internal.js
+8 -29
@@ -13,6 +13,7 @@ describe('SimpleEventPlugin', function() {
13 let React;
14 let ReactDOM;
15 let ReactFeatureFlags;
16 + let Scheduler;
17
18 let onClick;
19 let container;
@@ -35,33 +36,10 @@ describe('SimpleEventPlugin', function() {
36 }
37
38 beforeEach(function() {
38 - // TODO pull this into helper method, reduce repetition.
39 - // mock the browser APIs which are used in schedule:
40 - // - requestAnimationFrame should pass the DOMHighResTimeStamp argument
41 - // - calling 'window.postMessage' should actually fire postmessage handlers
42 - global.requestAnimationFrame = function(cb) {
43 - return setTimeout(() => {
44 - cb(Date.now());
45 - });
46 - };
47 - const originalAddEventListener = global.addEventListener;
48 - let postMessageCallback;
49 - global.addEventListener = function(eventName, callback, useCapture) {
50 - if (eventName === 'message') {
51 - postMessageCallback = callback;
52 - } else {
53 - originalAddEventListener(eventName, callback, useCapture);
54 - }
55 - };
56 - global.postMessage = function(messageKey, targetOrigin) {
57 - const postMessageEvent = {source: window, data: messageKey};
58 - if (postMessageCallback) {
59 - postMessageCallback(postMessageEvent);
60 - }
61 - };
39 jest.resetModules();
40 React = require('react');
41 ReactDOM = require('react-dom');
42 + Scheduler = require('scheduler');
43
44 onClick = jest.fn();
45 });
@@ -258,6 +236,7 @@ describe('SimpleEventPlugin', function() {
236 ReactFeatureFlags = require('shared/ReactFeatureFlags');
237 ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
238 ReactDOM = require('react-dom');
239 + Scheduler = require('scheduler');
240 });
241
242 it('flushes pending interactive work before extracting event handler', () => {
@@ -296,7 +275,7 @@ describe('SimpleEventPlugin', function() {
275 expect(ops).toEqual([]);
276 expect(button).toBe(undefined);
277 // Flush async work
299 - jest.runAllTimers();
278 + Scheduler.flushAll();
279 expect(ops).toEqual(['render button: enabled']);
280
281 ops = [];
@@ -336,7 +315,7 @@ describe('SimpleEventPlugin', function() {
315 click();
316 click();
317 click();
339 - jest.runAllTimers();
318 + Scheduler.flushAll();
319 expect(ops).toEqual([]);
320 });
321
@@ -367,7 +346,7 @@ describe('SimpleEventPlugin', function() {
346 // Should not have flushed yet because it's async
347 expect(button).toBe(undefined);
348 // Flush async work
370 - jest.runAllTimers();
349 + Scheduler.flushAll();
350 expect(button.textContent).toEqual('Count: 0');
351
352 function click() {
@@ -390,7 +369,7 @@ describe('SimpleEventPlugin', function() {
369 click();
370
371 // Flush the remaining work
393 - jest.runAllTimers();
372 + Scheduler.flushAll();
373 // The counter should equal the total number of clicks
374 expect(button.textContent).toEqual('Count: 7');
375 });
@@ -459,7 +438,7 @@ describe('SimpleEventPlugin', function() {
438 click();
439
440 // Flush the remaining work
462 - jest.runAllTimers();
441 + Scheduler.flushAll();
442 // Both counters should equal the total number of clicks
443 expect(button.textContent).toEqual('High-pri count: 7, Low-pri count: 7');
444 });
packages/react/src/__tests__/ReactProfilerDOM-test.internal.js
+10 -30
@@ -13,34 +13,9 @@ let React;
13 let ReactFeatureFlags;
14 let ReactDOM;
15 let SchedulerTracing;
16 +let Scheduler;
17 let ReactCache;
18
18 -function initEnvForAsyncTesting() {
19 - // Boilerplate copied from ReactDOMRoot-test
20 - // TODO pull this into helper method, reduce repetition.
21 - // TODO remove `requestAnimationFrame` when upgrading to Jest 24 with Lolex
22 - global.requestAnimationFrame = function(cb) {
23 - return setTimeout(() => {
24 - cb(Date.now());
25 - });
26 - };
27 - const originalAddEventListener = global.addEventListener;
28 - let postMessageCallback;
29 - global.addEventListener = function(eventName, callback, useCapture) {
30 - if (eventName === 'message') {
31 - postMessageCallback = callback;
32 - } else {
33 - originalAddEventListener(eventName, callback, useCapture);
34 - }
35 - };
36 - global.postMessage = function(messageKey, targetOrigin) {
37 - const postMessageEvent = {source: window, data: messageKey};
38 - if (postMessageCallback) {
39 - postMessageCallback(postMessageEvent);
40 - }
41 - };
42 -}
43 -
19 function loadModules() {
20 ReactFeatureFlags = require('shared/ReactFeatureFlags');
21 ReactFeatureFlags.debugRenderPhaseSideEffects = false;
@@ -51,6 +26,7 @@ function loadModules() {
26 React = require('react');
27 SchedulerTracing = require('scheduler/tracing');
28 ReactDOM = require('react-dom');
29 + Scheduler = require('scheduler');
30 ReactCache = require('react-cache');
31 }
32
@@ -61,7 +37,6 @@ describe('ProfilerDOM', () => {
37 let onInteractionTraced;
38
39 beforeEach(() => {
64 - initEnvForAsyncTesting();
40 loadModules();
41
42 onInteractionScheduledWorkCompleted = jest.fn();
@@ -114,7 +89,7 @@ describe('ProfilerDOM', () => {
89 batch = root.createBatch();
90 batch.render(
91 <React.Suspense maxDuration={100} fallback={<Text text="Loading..." />}>
117 - <AsyncText text="Text" ms={200} />
92 + <AsyncText text="Text" ms={2000} />
93 </React.Suspense>,
94 );
95 batch.then(
@@ -125,9 +100,12 @@ describe('ProfilerDOM', () => {
100 expect(onInteractionTraced).toHaveBeenCalledTimes(1);
101 expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
102
103 + jest.runAllTimers();
104 +
105 resourcePromise.then(
106 SchedulerTracing.unstable_wrap(() => {
107 jest.runAllTimers();
108 + Scheduler.flushAll();
109
110 expect(element.textContent).toBe('Text');
111 expect(onInteractionTraced).toHaveBeenCalledTimes(1);
@@ -154,6 +132,8 @@ describe('ProfilerDOM', () => {
132 );
133 }),
134 );
135 +
136 + Scheduler.flushAll();
137 });
138
139 expect(onInteractionTraced).toHaveBeenCalledTimes(1);
@@ -161,7 +141,7 @@ describe('ProfilerDOM', () => {
141 interaction,
142 );
143 expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
164 -
165 - jest.runAllTimers();
144 + Scheduler.flushAll();
145 + jest.advanceTimersByTime(500);
146 });
147 });
packages/scheduler/npm/unstable_mock.js new
+7
@@ -0,0 +1,7 @@
1 +'use strict';
2 +
3 +if (process.env.NODE_ENV === 'production') {
4 + module.exports = require('./cjs/scheduler-unstable_mock.production.min.js');
5 +} else {
6 + module.exports = require('./cjs/scheduler-unstable_mock.development.js');
7 +}
packages/scheduler/package.json
+1
@@ -27,6 +27,7 @@
27 "index.js",
28 "tracing.js",
29 "tracing-profiling.js",
30 + "unstable_mock.js",
31 "cjs/",
32 "umd/"
33 ],
packages/scheduler/src/Scheduler.js
+6 -272
@@ -9,6 +9,12 @@
9 /* eslint-disable no-var */
10
11 import {enableSchedulerDebugging} from './SchedulerFeatureFlags';
12 +import {
13 + requestHostCallback,
14 + cancelHostCallback,
15 + shouldYieldToHost,
16 + getCurrentTime,
17 +} from './SchedulerHostConfig';
18
19 // TODO: Use symbols?
20 var ImmediatePriority = 1;
@@ -47,9 +53,6 @@ var isExecutingCallback = false;
53
54 var isHostCallbackScheduled = false;
55
50 -var hasNativePerformanceNow =
51 - typeof performance === 'object' && typeof performance.now === 'function';
52 -
56 function ensureHostCallbackIsScheduled() {
57 if (isExecutingCallback) {
58 // Don't schedule work yet; wait until the next time we yield.
@@ -443,275 +446,6 @@ function unstable_shouldYield() {
446 );
447 }
448
446 -// The remaining code is essentially a polyfill for requestIdleCallback. It
447 -// works by scheduling a requestAnimationFrame, storing the time for the start
448 -// of the frame, then scheduling a postMessage which gets scheduled after paint.
449 -// Within the postMessage handler do as much work as possible until time + frame
450 -// rate. By separating the idle call into a separate event tick we ensure that
451 -// layout, paint and other browser work is counted against the available time.
452 -// The frame rate is dynamically adjusted.
453 -
454 -// We capture a local reference to any global, in case it gets polyfilled after
455 -// this module is initially evaluated. We want to be using a
456 -// consistent implementation.
457 -var localDate = Date;
458 -
459 -// This initialization code may run even on server environments if a component
460 -// just imports ReactDOM (e.g. for findDOMNode). Some environments might not
461 -// have setTimeout or clearTimeout. However, we always expect them to be defined
462 -// on the client. https://github.com/facebook/react/pull/13088
463 -var localSetTimeout = typeof setTimeout === 'function' ? setTimeout : undefined;
464 -var localClearTimeout =
465 - typeof clearTimeout === 'function' ? clearTimeout : undefined;
466 -
467 -// We don't expect either of these to necessarily be defined, but we will error
468 -// later if they are missing on the client.
469 -var localRequestAnimationFrame =
470 - typeof requestAnimationFrame === 'function'
471 - ? requestAnimationFrame
472 - : undefined;
473 -var localCancelAnimationFrame =
474 - typeof cancelAnimationFrame === 'function' ? cancelAnimationFrame : undefined;
475 -
476 -var getCurrentTime;
477 -
478 -// requestAnimationFrame does not run when the tab is in the background. If
479 -// we're backgrounded we prefer for that work to happen so that the page
480 -// continues to load in the background. So we also schedule a 'setTimeout' as
481 -// a fallback.
482 -// TODO: Need a better heuristic for backgrounded work.
483 -var ANIMATION_FRAME_TIMEOUT = 100;
484 -var rAFID;
485 -var rAFTimeoutID;
486 -var requestAnimationFrameWithTimeout = function(callback) {
487 - // schedule rAF and also a setTimeout
488 - rAFID = localRequestAnimationFrame(function(timestamp) {
489 - // cancel the setTimeout
490 - localClearTimeout(rAFTimeoutID);
491 - callback(timestamp);
492 - });
493 - rAFTimeoutID = localSetTimeout(function() {
494 - // cancel the requestAnimationFrame
495 - localCancelAnimationFrame(rAFID);
496 - callback(getCurrentTime());
497 - }, ANIMATION_FRAME_TIMEOUT);
498 -};
499 -
500 -if (hasNativePerformanceNow) {
501 - var Performance = performance;
502 - getCurrentTime = function() {
503 - return Performance.now();
504 - };
505 -} else {
506 - getCurrentTime = function() {
507 - return localDate.now();
508 - };
509 -}
510 -
511 -var requestHostCallback;
512 -var cancelHostCallback;
513 -var shouldYieldToHost;
514 -
515 -var globalValue = null;
516 -if (typeof window !== 'undefined') {
517 - globalValue = window;
518 -} else if (typeof global !== 'undefined') {
519 - globalValue = global;
520 -}
521 -
522 -if (globalValue && globalValue._schedMock) {
523 - // Dynamic injection, only for testing purposes.
524 - var globalImpl = globalValue._schedMock;
525 - requestHostCallback = globalImpl[0];
526 - cancelHostCallback = globalImpl[1];
527 - shouldYieldToHost = globalImpl[2];
528 - getCurrentTime = globalImpl[3];
529 -} else if (
530 - // If Scheduler runs in a non-DOM environment, it falls back to a naive
531 - // implementation using setTimeout.
532 - typeof window === 'undefined' ||
533 - // Check if MessageChannel is supported, too.
534 - typeof MessageChannel !== 'function'
535 -) {
536 - // If this accidentally gets imported in a non-browser environment, e.g. JavaScriptCore,
537 - // fallback to a naive implementation.
538 - var _callback = null;
539 - var _flushCallback = function(didTimeout) {
540 - if (_callback !== null) {
541 - try {
542 - _callback(didTimeout);
543 - } finally {
544 - _callback = null;
545 - }
546 - }
547 - };
548 - requestHostCallback = function(cb, ms) {
549 - if (_callback !== null) {
550 - // Protect against re-entrancy.
551 - setTimeout(requestHostCallback, 0, cb);
552 - } else {
553 - _callback = cb;
554 - setTimeout(_flushCallback, 0, false);
555 - }
556 - };
557 - cancelHostCallback = function() {
558 - _callback = null;
559 - };
560 - shouldYieldToHost = function() {
561 - return false;
562 - };
563 -} else {
564 - if (typeof console !== 'undefined') {
565 - // TODO: Remove fb.me link
566 - if (typeof localRequestAnimationFrame !== 'function') {
567 - console.error(
568 - "This browser doesn't support requestAnimationFrame. " +
569 - 'Make sure that you load a ' +
570 - 'polyfill in older browsers. https://fb.me/react-polyfills',
571 - );
572 - }
573 - if (typeof localCancelAnimationFrame !== 'function') {
574 - console.error(
575 - "This browser doesn't support cancelAnimationFrame. " +
576 - 'Make sure that you load a ' +
577 - 'polyfill in older browsers. https://fb.me/react-polyfills',
578 - );
579 - }
580 - }
581 -
582 - var scheduledHostCallback = null;
583 - var isMessageEventScheduled = false;
584 - var timeoutTime = -1;
585 -
586 - var isAnimationFrameScheduled = false;
587 -
588 - var isFlushingHostCallback = false;
589 -
590 - var frameDeadline = 0;
591 - // We start out assuming that we run at 30fps but then the heuristic tracking
592 - // will adjust this value to a faster fps if we get more frequent animation
593 - // frames.
594 - var previousFrameTime = 33;
595 - var activeFrameTime = 33;
596 -
597 - shouldYieldToHost = function() {
598 - return frameDeadline <= getCurrentTime();
599 - };
600 -
601 - // We use the postMessage trick to defer idle work until after the repaint.
602 - var channel = new MessageChannel();
603 - var port = channel.port2;
604 - channel.port1.onmessage = function(event) {
605 - isMessageEventScheduled = false;
606 -
607 - var prevScheduledCallback = scheduledHostCallback;
608 - var prevTimeoutTime = timeoutTime;
609 - scheduledHostCallback = null;
610 - timeoutTime = -1;
611 -
612 - var currentTime = getCurrentTime();
613 -
614 - var didTimeout = false;
615 - if (frameDeadline - currentTime <= 0) {
616 - // There's no time left in this idle period. Check if the callback has
617 - // a timeout and whether it's been exceeded.
618 - if (prevTimeoutTime !== -1 && prevTimeoutTime <= currentTime) {
619 - // Exceeded the timeout. Invoke the callback even though there's no
620 - // time left.
621 - didTimeout = true;
622 - } else {
623 - // No timeout.
624 - if (!isAnimationFrameScheduled) {
625 - // Schedule another animation callback so we retry later.
626 - isAnimationFrameScheduled = true;
627 - requestAnimationFrameWithTimeout(animationTick);
628 - }
629 - // Exit without invoking the callback.
630 - scheduledHostCallback = prevScheduledCallback;
631 - timeoutTime = prevTimeoutTime;
632 - return;
633 - }
634 - }
635 -
636 - if (prevScheduledCallback !== null) {
637 - isFlushingHostCallback = true;
638 - try {
639 - prevScheduledCallback(didTimeout);
640 - } finally {
641 - isFlushingHostCallback = false;
642 - }
643 - }
644 - };
645 -
646 - var animationTick = function(rafTime) {
647 - if (scheduledHostCallback !== null) {
648 - // Eagerly schedule the next animation callback at the beginning of the
649 - // frame. If the scheduler queue is not empty at the end of the frame, it
650 - // will continue flushing inside that callback. If the queue *is* empty,
651 - // then it will exit immediately. Posting the callback at the start of the
652 - // frame ensures it's fired within the earliest possible frame. If we
653 - // waited until the end of the frame to post the callback, we risk the
654 - // browser skipping a frame and not firing the callback until the frame
655 - // after that.
656 - requestAnimationFrameWithTimeout(animationTick);
657 - } else {
658 - // No pending work. Exit.
659 - isAnimationFrameScheduled = false;
660 - return;
661 - }
662 -
663 - var nextFrameTime = rafTime - frameDeadline + activeFrameTime;
664 - if (
665 - nextFrameTime < activeFrameTime &&
666 - previousFrameTime < activeFrameTime
667 - ) {
668 - if (nextFrameTime < 8) {
669 - // Defensive coding. We don't support higher frame rates than 120hz.
670 - // If the calculated frame time gets lower than 8, it is probably a bug.
671 - nextFrameTime = 8;
672 - }
673 - // If one frame goes long, then the next one can be short to catch up.
674 - // If two frames are short in a row, then that's an indication that we
675 - // actually have a higher frame rate than what we're currently optimizing.
676 - // We adjust our heuristic dynamically accordingly. For example, if we're
677 - // running on 120hz display or 90hz VR display.
678 - // Take the max of the two in case one of them was an anomaly due to
679 - // missed frame deadlines.
680 - activeFrameTime =
681 - nextFrameTime < previousFrameTime ? previousFrameTime : nextFrameTime;
682 - } else {
683 - previousFrameTime = nextFrameTime;
684 - }
685 - frameDeadline = rafTime + activeFrameTime;
686 - if (!isMessageEventScheduled) {
687 - isMessageEventScheduled = true;
688 - port.postMessage(undefined);
689 - }
690 - };
691 -
692 - requestHostCallback = function(callback, absoluteTimeout) {
693 - scheduledHostCallback = callback;
694 - timeoutTime = absoluteTimeout;
695 - if (isFlushingHostCallback || absoluteTimeout < 0) {
696 - // Don't wait for the next frame. Continue working ASAP, in a new event.
697 - port.postMessage(undefined);
698 - } else if (!isAnimationFrameScheduled) {
699 - // If rAF didn't already schedule one, we need to schedule a frame.
700 - // TODO: If this rAF doesn't materialize because the browser throttles, we
701 - // might want to still have setTimeout trigger rIC as a backup to ensure
702 - // that we keep performing work.
703 - isAnimationFrameScheduled = true;
704 - requestAnimationFrameWithTimeout(animationTick);
705 - }
706 - };
707 -
708 - cancelHostCallback = function() {
709 - scheduledHostCallback = null;
710 - isMessageEventScheduled = false;
711 - timeoutTime = -1;
712 - };
713 -}
714 -
449 export {
450 ImmediatePriority as unstable_ImmediatePriority,
451 UserBlockingPriority as unstable_UserBlockingPriority,
packages/scheduler/src/SchedulerHostConfig.js renamed
+3 -1
@@ -3,6 +3,8 @@
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 + * @flow
8 */
9
8 -export * from './src/JestMockScheduler';
10 +throw new Error('This module must be shimmed by a specific build.');
packages/scheduler/src/__tests__/Scheduler-test.js
+164 -290
@@ -9,6 +9,7 @@
9
10 'use strict';
11
12 +let Scheduler;
13 let runWithPriority;
14 let ImmediatePriority;
15 let UserBlockingPriority;
@@ -18,211 +19,44 @@ let cancelCallback;
19 let wrapCallback;
20 let getCurrentPriorityLevel;
21 let shouldYield;
21 -let flushWork;
22 -let advanceTime;
23 -let doWork;
24 -let yieldedValues;
25 -let yieldValue;
26 -let clearYieldedValues;
22
23 describe('Scheduler', () => {
24 beforeEach(() => {
30 - jest.useFakeTimers();
25 jest.resetModules();
32 -
33 - const JestMockScheduler = require('jest-mock-scheduler');
34 - JestMockScheduler.mockRestore();
35 -
36 - let _flushWork = null;
37 - let isFlushing = false;
38 - let timeoutID = -1;
39 - let endOfFrame = -1;
40 - let hasMicrotask = false;
41 -
42 - let currentTime = 0;
43 -
44 - flushWork = frameSize => {
45 - if (isFlushing) {
46 - throw new Error('Already flushing work.');
47 - }
48 - if (frameSize === null || frameSize === undefined) {
49 - frameSize = Infinity;
50 - }
51 - if (_flushWork === null) {
52 - throw new Error('No work is scheduled.');
53 - }
54 - timeoutID = -1;
55 - endOfFrame = currentTime + frameSize;
56 - try {
57 - isFlushing = true;
58 - _flushWork(false);
59 - } finally {
60 - isFlushing = false;
61 - endOfFrame = -1;
62 - if (hasMicrotask) {
63 - onTimeout();
64 - }
65 - }
66 - const yields = yieldedValues;
67 - yieldedValues = [];
68 - return yields;
69 - };
70 -
71 - advanceTime = ms => {
72 - currentTime += ms;
73 - jest.advanceTimersByTime(ms);
74 - };
75 -
76 - doWork = (label, timeCost) => {
77 - if (typeof timeCost !== 'number') {
78 - throw new Error('Second arg must be a number.');
79 - }
80 - advanceTime(timeCost);
81 - yieldValue(label);
82 - };
83 -
84 - yieldedValues = [];
85 - yieldValue = value => {
86 - yieldedValues.push(value);
87 - };
88 -
89 - clearYieldedValues = () => {
90 - const yields = yieldedValues;
91 - yieldedValues = [];
92 - return yields;
93 - };
94 -
95 - function onTimeout() {
96 - if (_flushWork === null) {
97 - return;
98 - }
99 - if (isFlushing) {
100 - hasMicrotask = true;
101 - } else {
102 - try {
103 - isFlushing = true;
104 - _flushWork(true);
105 - } finally {
106 - hasMicrotask = false;
107 - isFlushing = false;
108 - }
109 - }
110 - }
111 -
112 - function requestHostCallback(fw, absoluteTimeout) {
113 - if (_flushWork !== null) {
114 - throw new Error('Work is already scheduled.');
115 - }
116 - _flushWork = fw;
117 - timeoutID = setTimeout(onTimeout, absoluteTimeout - currentTime);
118 - }
119 - function cancelHostCallback() {
120 - if (_flushWork === null) {
121 - throw new Error('No work is scheduled.');
122 - }
123 - _flushWork = null;
124 - clearTimeout(timeoutID);
125 - }
126 - function shouldYieldToHost() {
127 - return endOfFrame <= currentTime;
128 - }
129 - function getCurrentTime() {
130 - return currentTime;
131 - }
132 -
133 - // Override host implementation
134 - delete global.performance;
135 - global.Date.now = () => {
136 - return currentTime;
137 - };
138 -
139 - window._schedMock = [
140 - requestHostCallback,
141 - cancelHostCallback,
142 - shouldYieldToHost,
143 - getCurrentTime,
144 - ];
145 -
146 - const Schedule = require('scheduler');
147 - runWithPriority = Schedule.unstable_runWithPriority;
148 - ImmediatePriority = Schedule.unstable_ImmediatePriority;
149 - UserBlockingPriority = Schedule.unstable_UserBlockingPriority;
150 - NormalPriority = Schedule.unstable_NormalPriority;
151 - scheduleCallback = Schedule.unstable_scheduleCallback;
152 - cancelCallback = Schedule.unstable_cancelCallback;
153 - wrapCallback = Schedule.unstable_wrapCallback;
154 - getCurrentPriorityLevel = Schedule.unstable_getCurrentPriorityLevel;
155 - shouldYield = Schedule.unstable_shouldYield;
26 + jest.mock('scheduler', () => require('scheduler/unstable_mock'));
27 +
28 + Scheduler = require('scheduler');
29 +
30 + runWithPriority = Scheduler.unstable_runWithPriority;
31 + ImmediatePriority = Scheduler.unstable_ImmediatePriority;
32 + UserBlockingPriority = Scheduler.unstable_UserBlockingPriority;
33 + NormalPriority = Scheduler.unstable_NormalPriority;
34 + scheduleCallback = Scheduler.unstable_scheduleCallback;
35 + cancelCallback = Scheduler.unstable_cancelCallback;
36 + wrapCallback = Scheduler.unstable_wrapCallback;
37 + getCurrentPriorityLevel = Scheduler.unstable_getCurrentPriorityLevel;
38 + shouldYield = Scheduler.unstable_shouldYield;
39 });
40
41 it('flushes work incrementally', () => {
159 - scheduleCallback(() => doWork('A', 100));
160 - scheduleCallback(() => doWork('B', 200));
161 - scheduleCallback(() => doWork('C', 300));
162 - scheduleCallback(() => doWork('D', 400));
163 -
164 - expect(flushWork(300)).toEqual(['A', 'B']);
165 - expect(flushWork(300)).toEqual(['C']);
166 - expect(flushWork(400)).toEqual(['D']);
167 - });
168 -
169 - it('flushes work until framesize reached', () => {
170 - scheduleCallback(() => doWork('A1_100', 100));
171 - scheduleCallback(() => doWork('A2_200', 200));
172 - scheduleCallback(() => doWork('B1_100', 100));
173 - scheduleCallback(() => doWork('B2_200', 200));
174 - scheduleCallback(() => doWork('C1_300', 300));
175 - scheduleCallback(() => doWork('C2_300', 300));
176 - scheduleCallback(() => doWork('D_3000', 3000));
177 - scheduleCallback(() => doWork('E1_300', 300));
178 - scheduleCallback(() => doWork('E2_200', 200));
179 - scheduleCallback(() => doWork('F1_200', 200));
180 - scheduleCallback(() => doWork('F2_200', 200));
181 - scheduleCallback(() => doWork('F3_300', 300));
182 - scheduleCallback(() => doWork('F4_500', 500));
183 - scheduleCallback(() => doWork('F5_200', 200));
184 - scheduleCallback(() => doWork('F6_20', 20));
185 -
186 - expect(Date.now()).toEqual(0);
187 - // No time left after A1_100 and A2_200 are run
188 - expect(flushWork(300)).toEqual(['A1_100', 'A2_200']);
189 - expect(Date.now()).toEqual(300);
190 - // B2_200 is started as there is still time left after B1_100
191 - expect(flushWork(101)).toEqual(['B1_100', 'B2_200']);
192 - expect(Date.now()).toEqual(600);
193 - // C1_300 is started as there is even a little frame time
194 - expect(flushWork(1)).toEqual(['C1_300']);
195 - expect(Date.now()).toEqual(900);
196 - // C2_300 is started even though there is no frame time
197 - expect(flushWork(0)).toEqual(['C2_300']);
198 - expect(Date.now()).toEqual(1200);
199 - // D_3000 is very slow, but won't affect next flushes (if no
200 - // timeouts happen)
201 - expect(flushWork(100)).toEqual(['D_3000']);
202 - expect(Date.now()).toEqual(4200);
203 - expect(flushWork(400)).toEqual(['E1_300', 'E2_200']);
204 - expect(Date.now()).toEqual(4700);
205 - // Default timeout is 5000, so during F2_200, work will timeout and are done
206 - // in reverse, including F2_200
207 - expect(flushWork(1000)).toEqual([
208 - 'F1_200',
209 - 'F2_200',
210 - 'F3_300',
211 - 'F4_500',
212 - 'F5_200',
213 - 'F6_20',
214 - ]);
215 - expect(Date.now()).toEqual(6120);
42 + scheduleCallback(() => Scheduler.yieldValue('A'));
43 + scheduleCallback(() => Scheduler.yieldValue('B'));
44 + scheduleCallback(() => Scheduler.yieldValue('C'));
45 + scheduleCallback(() => Scheduler.yieldValue('D'));
46 +
47 + expect(Scheduler).toFlushAndYieldThrough(['A', 'B']);
48 + expect(Scheduler).toFlushAndYieldThrough(['C']);
49 + expect(Scheduler).toFlushAndYield(['D']);
50 });
51
52 it('cancels work', () => {
219 - scheduleCallback(() => doWork('A', 100));
220 - const callbackHandleB = scheduleCallback(() => doWork('B', 200));
221 - scheduleCallback(() => doWork('C', 300));
53 + scheduleCallback(() => Scheduler.yieldValue('A'));
54 + const callbackHandleB = scheduleCallback(() => Scheduler.yieldValue('B'));
55 + scheduleCallback(() => Scheduler.yieldValue('C'));
56
57 cancelCallback(callbackHandleB);
58
225 - expect(flushWork()).toEqual([
59 + expect(Scheduler).toFlushAndYield([
60 'A',
61 // B should have been cancelled
62 'C',
@@ -230,86 +64,100 @@ describe('Scheduler', () => {
64 });
65
66 it('executes the highest priority callbacks first', () => {
233 - scheduleCallback(() => doWork('A', 100));
234 - scheduleCallback(() => doWork('B', 100));
67 + scheduleCallback(() => Scheduler.yieldValue('A'));
68 + scheduleCallback(() => Scheduler.yieldValue('B'));
69
70 // Yield before B is flushed
237 - expect(flushWork(100)).toEqual(['A']);
71 + expect(Scheduler).toFlushAndYieldThrough(['A']);
72
73 runWithPriority(UserBlockingPriority, () => {
240 - scheduleCallback(() => doWork('C', 100));
241 - scheduleCallback(() => doWork('D', 100));
74 + scheduleCallback(() => Scheduler.yieldValue('C'));
75 + scheduleCallback(() => Scheduler.yieldValue('D'));
76 });
77
78 // C and D should come first, because they are higher priority
245 - expect(flushWork()).toEqual(['C', 'D', 'B']);
79 + expect(Scheduler).toFlushAndYield(['C', 'D', 'B']);
80 });
81
82 it('expires work', () => {
249 - scheduleCallback(didTimeout =>
250 - doWork(`A (did timeout: ${didTimeout})`, 100),
251 - );
83 + scheduleCallback(didTimeout => {
84 + Scheduler.advanceTime(100);
85 + Scheduler.yieldValue(`A (did timeout: ${didTimeout})`);
86 + });
87 runWithPriority(UserBlockingPriority, () => {
253 - scheduleCallback(didTimeout =>
254 - doWork(`B (did timeout: ${didTimeout})`, 100),
255 - );
88 + scheduleCallback(didTimeout => {
89 + Scheduler.advanceTime(100);
90 + Scheduler.yieldValue(`B (did timeout: ${didTimeout})`);
91 + });
92 });
93 runWithPriority(UserBlockingPriority, () => {
258 - scheduleCallback(didTimeout =>
259 - doWork(`C (did timeout: ${didTimeout})`, 100),
260 - );
94 + scheduleCallback(didTimeout => {
95 + Scheduler.advanceTime(100);
96 + Scheduler.yieldValue(`C (did timeout: ${didTimeout})`);
97 + });
98 });
99
100 // Advance time, but not by enough to expire any work
264 - advanceTime(249);
265 - expect(clearYieldedValues()).toEqual([]);
101 + Scheduler.advanceTime(249);
102 + expect(Scheduler).toHaveYielded([]);
103
104 // Schedule a few more callbacks
268 - scheduleCallback(didTimeout =>
269 - doWork(`D (did timeout: ${didTimeout})`, 100),
270 - );
271 - scheduleCallback(didTimeout =>
272 - doWork(`E (did timeout: ${didTimeout})`, 100),
273 - );
105 + scheduleCallback(didTimeout => {
106 + Scheduler.advanceTime(100);
107 + Scheduler.yieldValue(`D (did timeout: ${didTimeout})`);
108 + });
109 + scheduleCallback(didTimeout => {
110 + Scheduler.advanceTime(100);
111 + Scheduler.yieldValue(`E (did timeout: ${didTimeout})`);
112 + });
113
114 // Advance by just a bit more to expire the user blocking callbacks
276 - advanceTime(1);
277 - expect(clearYieldedValues()).toEqual([
115 + Scheduler.advanceTime(1);
116 + expect(Scheduler).toHaveYielded([
117 'B (did timeout: true)',
118 'C (did timeout: true)',
119 ]);
120
121 // Expire A
283 - advanceTime(4600);
284 - expect(clearYieldedValues()).toEqual(['A (did timeout: true)']);
122 + Scheduler.advanceTime(4600);
123 + expect(Scheduler).toHaveYielded(['A (did timeout: true)']);
124
125 // Flush the rest without expiring
287 - expect(flushWork()).toEqual([
126 + expect(Scheduler).toFlushAndYield([
127 'D (did timeout: false)',
289 - 'E (did timeout: false)',
128 + 'E (did timeout: true)',
129 ]);
130 });
131
132 it('has a default expiration of ~5 seconds', () => {
294 - scheduleCallback(() => doWork('A', 100));
133 + scheduleCallback(() => Scheduler.yieldValue('A'));
134
296 - advanceTime(4999);
297 - expect(clearYieldedValues()).toEqual([]);
135 + Scheduler.advanceTime(4999);
136 + expect(Scheduler).toHaveYielded([]);
137
299 - advanceTime(1);
300 - expect(clearYieldedValues()).toEqual(['A']);
138 + Scheduler.advanceTime(1);
139 + expect(Scheduler).toHaveYielded(['A']);
140 });
141
142 it('continues working on same task after yielding', () => {
304 - scheduleCallback(() => doWork('A', 100));
305 - scheduleCallback(() => doWork('B', 100));
143 + scheduleCallback(() => {
144 + Scheduler.advanceTime(100);
145 + Scheduler.yieldValue('A');
146 + });
147 + scheduleCallback(() => {
148 + Scheduler.advanceTime(100);
149 + Scheduler.yieldValue('B');
150 + });
151
152 + let didYield = false;
153 const tasks = [['C1', 100], ['C2', 100], ['C3', 100]];
154 const C = () => {
155 while (tasks.length > 0) {
310 - doWork(...tasks.shift());
156 + const [label, ms] = tasks.shift();
157 + Scheduler.advanceTime(ms);
158 + Scheduler.yieldValue(label);
159 if (shouldYield()) {
312 - yieldValue('Yield!');
160 + didYield = true;
161 return C;
162 }
163 }
@@ -317,21 +165,32 @@ describe('Scheduler', () => {
165
166 scheduleCallback(C);
167
320 - scheduleCallback(() => doWork('D', 100));
321 - scheduleCallback(() => doWork('E', 100));
168 + scheduleCallback(() => {
169 + Scheduler.advanceTime(100);
170 + Scheduler.yieldValue('D');
171 + });
172 + scheduleCallback(() => {
173 + Scheduler.advanceTime(100);
174 + Scheduler.yieldValue('E');
175 + });
176
323 - expect(flushWork(300)).toEqual(['A', 'B', 'C1', 'Yield!']);
177 + // Flush, then yield while in the middle of C.
178 + expect(didYield).toBe(false);
179 + expect(Scheduler).toFlushAndYieldThrough(['A', 'B', 'C1']);
180 + expect(didYield).toBe(true);
181
325 - expect(flushWork()).toEqual(['C2', 'C3', 'D', 'E']);
182 + // When we resume, we should continue working on C.
183 + expect(Scheduler).toFlushAndYield(['C2', 'C3', 'D', 'E']);
184 });
185
186 it('continuation callbacks inherit the expiration of the previous callback', () => {
187 const tasks = [['A', 125], ['B', 124], ['C', 100], ['D', 100]];
188 const work = () => {
189 while (tasks.length > 0) {
332 - doWork(...tasks.shift());
190 + const [label, ms] = tasks.shift();
191 + Scheduler.advanceTime(ms);
192 + Scheduler.yieldValue(label);
193 if (shouldYield()) {
334 - yieldValue('Yield!');
194 return work;
195 }
196 }
@@ -341,50 +200,56 @@ describe('Scheduler', () => {
200 runWithPriority(UserBlockingPriority, () => scheduleCallback(work));
201
202 // Flush until just before the expiration time
344 - expect(flushWork(249)).toEqual(['A', 'B', 'Yield!']);
203 + expect(Scheduler).toFlushAndYieldThrough(['A', 'B']);
204
205 // Advance time by just a bit more. This should expire all the remaining work.
347 - advanceTime(1);
348 - expect(clearYieldedValues()).toEqual(['C', 'D']);
206 + Scheduler.advanceTime(1);
207 + expect(Scheduler).toHaveYielded(['C', 'D']);
208 });
209
210 it('nested callbacks inherit the priority of the currently executing callback', () => {
211 runWithPriority(UserBlockingPriority, () => {
212 scheduleCallback(() => {
354 - doWork('Parent callback', 100);
213 + Scheduler.advanceTime(100);
214 + Scheduler.yieldValue('Parent callback');
215 scheduleCallback(() => {
356 - doWork('Nested callback', 100);
216 + Scheduler.advanceTime(100);
217 + Scheduler.yieldValue('Nested callback');
218 });
219 });
220 });
221
361 - expect(flushWork(100)).toEqual(['Parent callback']);
222 + expect(Scheduler).toFlushAndYieldThrough(['Parent callback']);
223
224 // The nested callback has user-blocking priority, so it should
225 // expire quickly.
365 - advanceTime(250 + 100);
366 - expect(clearYieldedValues()).toEqual(['Nested callback']);
226 + Scheduler.advanceTime(250 + 100);
227 + expect(Scheduler).toHaveYielded(['Nested callback']);
228 });
229
230 it('continuations are interrupted by higher priority work', () => {
231 const tasks = [['A', 100], ['B', 100], ['C', 100], ['D', 100]];
232 const work = () => {
233 while (tasks.length > 0) {
373 - doWork(...tasks.shift());
234 + const [label, ms] = tasks.shift();
235 + Scheduler.advanceTime(ms);
236 + Scheduler.yieldValue(label);
237 if (tasks.length > 0 && shouldYield()) {
375 - yieldValue('Yield!');
238 return work;
239 }
240 }
241 };
242 scheduleCallback(work);
381 - expect(flushWork(100)).toEqual(['A', 'Yield!']);
243 + expect(Scheduler).toFlushAndYieldThrough(['A']);
244
245 runWithPriority(UserBlockingPriority, () => {
384 - scheduleCallback(() => doWork('High pri', 100));
246 + scheduleCallback(() => {
247 + Scheduler.advanceTime(100);
248 + Scheduler.yieldValue('High pri');
249 + });
250 });
251
387 - expect(flushWork()).toEqual(['High pri', 'B', 'C', 'D']);
252 + expect(Scheduler).toFlushAndYield(['High pri', 'B', 'C', 'D']);
253 });
254
255 it(
@@ -395,22 +260,27 @@ describe('Scheduler', () => {
260 const work = () => {
261 while (tasks.length > 0) {
262 const task = tasks.shift();
398 - doWork(...task);
263 + const [label, ms] = task;
264 + Scheduler.advanceTime(ms);
265 + Scheduler.yieldValue(label);
266 if (task[0] === 'B') {
267 // Schedule high pri work from inside another callback
401 - yieldValue('Schedule high pri');
268 + Scheduler.yieldValue('Schedule high pri');
269 runWithPriority(UserBlockingPriority, () =>
403 - scheduleCallback(() => doWork('High pri', 100)),
270 + scheduleCallback(() => {
271 + Scheduler.advanceTime(100);
272 + Scheduler.yieldValue('High pri');
273 + }),
274 );
275 }
276 if (tasks.length > 0 && shouldYield()) {
407 - yieldValue('Yield!');
277 + Scheduler.yieldValue('Yield!');
278 return work;
279 }
280 }
281 };
282 scheduleCallback(work);
413 - expect(flushWork()).toEqual([
283 + expect(Scheduler).toFlushAndYield([
284 'A',
285 'B',
286 'Schedule high pri',
@@ -427,19 +297,19 @@ describe('Scheduler', () => {
297
298 it('immediate callbacks fire at the end of outermost event', () => {
299 runWithPriority(ImmediatePriority, () => {
430 - scheduleCallback(() => yieldValue('A'));
431 - scheduleCallback(() => yieldValue('B'));
300 + scheduleCallback(() => Scheduler.yieldValue('A'));
301 + scheduleCallback(() => Scheduler.yieldValue('B'));
302 // Nested event
303 runWithPriority(ImmediatePriority, () => {
434 - scheduleCallback(() => yieldValue('C'));
304 + scheduleCallback(() => Scheduler.yieldValue('C'));
305 // Nothing should have fired yet
436 - expect(clearYieldedValues()).toEqual([]);
306 + expect(Scheduler).toHaveYielded([]);
307 });
308 // Nothing should have fired yet
439 - expect(clearYieldedValues()).toEqual([]);
309 + expect(Scheduler).toHaveYielded([]);
310 });
311 // The callbacks were called at the end of the outer event
442 - expect(clearYieldedValues()).toEqual(['A', 'B', 'C']);
312 + expect(Scheduler).toHaveYielded(['A', 'B', 'C']);
313 });
314
315 it('wrapped callbacks have same signature as original callback', () => {
@@ -450,7 +320,8 @@ describe('Scheduler', () => {
320 it('wrapped callbacks inherit the current priority', () => {
321 const wrappedCallback = wrapCallback(() => {
322 scheduleCallback(() => {
453 - doWork('Normal', 100);
323 + Scheduler.advanceTime(100);
324 + Scheduler.yieldValue('Normal');
325 });
326 });
327 const wrappedInteractiveCallback = runWithPriority(
@@ -458,7 +329,8 @@ describe('Scheduler', () => {
329 () =>
330 wrapCallback(() => {
331 scheduleCallback(() => {
461 - doWork('User-blocking', 100);
332 + Scheduler.advanceTime(100);
333 + Scheduler.yieldValue('User-blocking');
334 });
335 }),
336 );
@@ -468,19 +340,20 @@ describe('Scheduler', () => {
340 // This should schedule an user-blocking callback
341 wrappedInteractiveCallback();
342
471 - advanceTime(249);
472 - expect(clearYieldedValues()).toEqual([]);
473 - advanceTime(1);
474 - expect(clearYieldedValues()).toEqual(['User-blocking']);
343 + Scheduler.advanceTime(249);
344 + expect(Scheduler).toHaveYielded([]);
345 + Scheduler.advanceTime(1);
346 + expect(Scheduler).toHaveYielded(['User-blocking']);
347
476 - advanceTime(10000);
477 - expect(clearYieldedValues()).toEqual(['Normal']);
348 + Scheduler.advanceTime(10000);
349 + expect(Scheduler).toHaveYielded(['Normal']);
350 });
351
352 it('wrapped callbacks inherit the current priority even when nested', () => {
353 const wrappedCallback = wrapCallback(() => {
354 scheduleCallback(() => {
483 - doWork('Normal', 100);
355 + Scheduler.advanceTime(100);
356 + Scheduler.yieldValue('Normal');
357 });
358 });
359 const wrappedInteractiveCallback = runWithPriority(
@@ -488,7 +361,8 @@ describe('Scheduler', () => {
361 () =>
362 wrapCallback(() => {
363 scheduleCallback(() => {
491 - doWork('User-blocking', 100);
364 + Scheduler.advanceTime(100);
365 + Scheduler.yieldValue('User-blocking');
366 });
367 }),
368 );
@@ -500,66 +374,66 @@ describe('Scheduler', () => {
374 wrappedInteractiveCallback();
375 });
376
503 - advanceTime(249);
504 - expect(clearYieldedValues()).toEqual([]);
505 - advanceTime(1);
506 - expect(clearYieldedValues()).toEqual(['User-blocking']);
377 + Scheduler.advanceTime(249);
378 + expect(Scheduler).toHaveYielded([]);
379 + Scheduler.advanceTime(1);
380 + expect(Scheduler).toHaveYielded(['User-blocking']);
381
508 - advanceTime(10000);
509 - expect(clearYieldedValues()).toEqual(['Normal']);
382 + Scheduler.advanceTime(10000);
383 + expect(Scheduler).toHaveYielded(['Normal']);
384 });
385
386 it('immediate callbacks fire at the end of callback', () => {
387 const immediateCallback = runWithPriority(ImmediatePriority, () =>
388 wrapCallback(() => {
515 - scheduleCallback(() => yieldValue('callback'));
389 + scheduleCallback(() => Scheduler.yieldValue('callback'));
390 }),
391 );
392 immediateCallback();
393
394 // The callback was called at the end of the outer event
521 - expect(clearYieldedValues()).toEqual(['callback']);
395 + expect(Scheduler).toHaveYielded(['callback']);
396 });
397
398 it("immediate callbacks fire even if there's an error", () => {
399 expect(() => {
400 runWithPriority(ImmediatePriority, () => {
401 scheduleCallback(() => {
528 - yieldValue('A');
402 + Scheduler.yieldValue('A');
403 throw new Error('Oops A');
404 });
405 scheduleCallback(() => {
532 - yieldValue('B');
406 + Scheduler.yieldValue('B');
407 });
408 scheduleCallback(() => {
535 - yieldValue('C');
409 + Scheduler.yieldValue('C');
410 throw new Error('Oops C');
411 });
412 });
413 }).toThrow('Oops A');
414
541 - expect(clearYieldedValues()).toEqual(['A']);
415 + expect(Scheduler).toHaveYielded(['A']);
416
417 // B and C flush in a subsequent event. That way, the second error is not
418 // swallowed.
545 - expect(() => flushWork(0)).toThrow('Oops C');
546 - expect(clearYieldedValues()).toEqual(['B', 'C']);
419 + expect(() => Scheduler.unstable_flushExpired()).toThrow('Oops C');
420 + expect(Scheduler).toHaveYielded(['B', 'C']);
421 });
422
423 it('exposes the current priority level', () => {
550 - yieldValue(getCurrentPriorityLevel());
424 + Scheduler.yieldValue(getCurrentPriorityLevel());
425 runWithPriority(ImmediatePriority, () => {
552 - yieldValue(getCurrentPriorityLevel());
426 + Scheduler.yieldValue(getCurrentPriorityLevel());
427 runWithPriority(NormalPriority, () => {
554 - yieldValue(getCurrentPriorityLevel());
428 + Scheduler.yieldValue(getCurrentPriorityLevel());
429 runWithPriority(UserBlockingPriority, () => {
556 - yieldValue(getCurrentPriorityLevel());
430 + Scheduler.yieldValue(getCurrentPriorityLevel());
431 });
432 });
559 - yieldValue(getCurrentPriorityLevel());
433 + Scheduler.yieldValue(getCurrentPriorityLevel());
434 });
435
562 - expect(clearYieldedValues()).toEqual([
436 + expect(Scheduler).toHaveYielded([
437 NormalPriority,
438 ImmediatePriority,
439 NormalPriority,
packages/scheduler/src/__tests__/SchedulerDOM-test.js
+7 -2
@@ -89,8 +89,13 @@ describe('SchedulerDOM', () => {
89 };
90 jest.resetModules();
91
92 - const JestMockScheduler = require('jest-mock-scheduler');
93 - JestMockScheduler.mockRestore();
92 + // Un-mock scheduler
93 + jest.mock('scheduler', () => require.requireActual('scheduler'));
94 + jest.mock('scheduler/src/SchedulerHostConfig', () =>
95 + require.requireActual(
96 + 'scheduler/src/forks/SchedulerHostConfig.default.js',
97 + ),
98 + );
99
100 Scheduler = require('scheduler');
101 });
packages/scheduler/src/__tests__/SchedulerNoDOM-test.js
+10 -33
@@ -19,10 +19,17 @@ describe('SchedulerNoDOM', () => {
19 // implementation using setTimeout. This only meant to be used for testing
20 // purposes, like with jest's fake timer API.
21 beforeEach(() => {
22 - jest.useFakeTimers();
22 jest.resetModules();
24 - // Delete addEventListener to force us into the fallback mode.
25 - window.addEventListener = undefined;
23 + jest.useFakeTimers();
24 +
25 + // Un-mock scheduler
26 + jest.mock('scheduler', () => require.requireActual('scheduler'));
27 + jest.mock('scheduler/src/SchedulerHostConfig', () =>
28 + require.requireActual(
29 + 'scheduler/src/forks/SchedulerHostConfig.default.js',
30 + ),
31 + );
32 +
33 const Scheduler = require('scheduler');
34 scheduleCallback = Scheduler.unstable_scheduleCallback;
35 runWithPriority = Scheduler.unstable_runWithPriority;
@@ -69,36 +76,6 @@ describe('SchedulerNoDOM', () => {
76 expect(log).toEqual(['C', 'D', 'A', 'B']);
77 });
78
72 - it('advanceTimersByTime expires callbacks incrementally', () => {
73 - let log = [];
74 -
75 - scheduleCallback(() => {
76 - log.push('A');
77 - });
78 - scheduleCallback(() => {
79 - log.push('B');
80 - });
81 - runWithPriority(UserBlockingPriority, () => {
82 - scheduleCallback(() => {
83 - log.push('C');
84 - });
85 - scheduleCallback(() => {
86 - log.push('D');
87 - });
88 - });
89 -
90 - expect(log).toEqual([]);
91 - jest.advanceTimersByTime(249);
92 - expect(log).toEqual([]);
93 - jest.advanceTimersByTime(1);
94 - expect(log).toEqual(['C', 'D']);
95 -
96 - log = [];
97 -
98 - jest.runAllTimers();
99 - expect(log).toEqual(['A', 'B']);
100 - });
101 -
79 it('calls immediate callbacks immediately', () => {
80 let log = [];
81
packages/scheduler/src/__tests__/SchedulerUMDBundle-test.internal.js
+7
@@ -14,6 +14,13 @@ describe('Scheduling UMD bundle', () => {
14 global.__UMD__ = true;
15
16 jest.resetModules();
17 +
18 + jest.mock('scheduler', () => require.requireActual('scheduler'));
19 + jest.mock('scheduler/src/SchedulerHostConfig', () =>
20 + require.requireActual(
21 + 'scheduler/src/forks/SchedulerHostConfig.default.js',
22 + ),
23 + );
24 });
25
26 function filterPrivateKeys(name) {
packages/scheduler/src/forks/SchedulerHostConfig.default.js new
+264
@@ -0,0 +1,264 @@
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 +
8 +// The DOM Scheduler implementation is similar to requestIdleCallback. It
9 +// works by scheduling a requestAnimationFrame, storing the time for the start
10 +// of the frame, then scheduling a postMessage which gets scheduled after paint.
11 +// Within the postMessage handler do as much work as possible until time + frame
12 +// rate. By separating the idle call into a separate event tick we ensure that
13 +// layout, paint and other browser work is counted against the available time.
14 +// The frame rate is dynamically adjusted.
15 +
16 +export let requestHostCallback;
17 +export let cancelHostCallback;
18 +export let shouldYieldToHost;
19 +export let getCurrentTime;
20 +
21 +const hasNativePerformanceNow =
22 + typeof performance === 'object' && typeof performance.now === 'function';
23 +
24 +// We capture a local reference to any global, in case it gets polyfilled after
25 +// this module is initially evaluated. We want to be using a
26 +// consistent implementation.
27 +const localDate = Date;
28 +
29 +// This initialization code may run even on server environments if a component
30 +// just imports ReactDOM (e.g. for findDOMNode). Some environments might not
31 +// have setTimeout or clearTimeout. However, we always expect them to be defined
32 +// on the client. https://github.com/facebook/react/pull/13088
33 +const localSetTimeout =
34 + typeof setTimeout === 'function' ? setTimeout : undefined;
35 +const localClearTimeout =
36 + typeof clearTimeout === 'function' ? clearTimeout : undefined;
37 +
38 +// We don't expect either of these to necessarily be defined, but we will error
39 +// later if they are missing on the client.
40 +const localRequestAnimationFrame =
41 + typeof requestAnimationFrame === 'function'
42 + ? requestAnimationFrame
43 + : undefined;
44 +const localCancelAnimationFrame =
45 + typeof cancelAnimationFrame === 'function' ? cancelAnimationFrame : undefined;
46 +
47 +// requestAnimationFrame does not run when the tab is in the background. If
48 +// we're backgrounded we prefer for that work to happen so that the page
49 +// continues to load in the background. So we also schedule a 'setTimeout' as
50 +// a fallback.
51 +// TODO: Need a better heuristic for backgrounded work.
52 +const ANIMATION_FRAME_TIMEOUT = 100;
53 +let rAFID;
54 +let rAFTimeoutID;
55 +const requestAnimationFrameWithTimeout = function(callback) {
56 + // schedule rAF and also a setTimeout
57 + rAFID = localRequestAnimationFrame(function(timestamp) {
58 + // cancel the setTimeout
59 + localClearTimeout(rAFTimeoutID);
60 + callback(timestamp);
61 + });
62 + rAFTimeoutID = localSetTimeout(function() {
63 + // cancel the requestAnimationFrame
64 + localCancelAnimationFrame(rAFID);
65 + callback(getCurrentTime());
66 + }, ANIMATION_FRAME_TIMEOUT);
67 +};
68 +
69 +if (hasNativePerformanceNow) {
70 + const Performance = performance;
71 + getCurrentTime = function() {
72 + return Performance.now();
73 + };
74 +} else {
75 + getCurrentTime = function() {
76 + return localDate.now();
77 + };
78 +}
79 +
80 +if (
81 + // If Scheduler runs in a non-DOM environment, it falls back to a naive
82 + // implementation using setTimeout.
83 + typeof window === 'undefined' ||
84 + // Check if MessageChannel is supported, too.
85 + typeof MessageChannel !== 'function'
86 +) {
87 + // If this accidentally gets imported in a non-browser environment, e.g. JavaScriptCore,
88 + // fallback to a naive implementation.
89 + let _callback = null;
90 + const _flushCallback = function(didTimeout) {
91 + if (_callback !== null) {
92 + try {
93 + _callback(didTimeout);
94 + } finally {
95 + _callback = null;
96 + }
97 + }
98 + };
99 + requestHostCallback = function(cb, ms) {
100 + if (_callback !== null) {
101 + // Protect against re-entrancy.
102 + setTimeout(requestHostCallback, 0, cb);
103 + } else {
104 + _callback = cb;
105 + setTimeout(_flushCallback, 0, false);
106 + }
107 + };
108 + cancelHostCallback = function() {
109 + _callback = null;
110 + };
111 + shouldYieldToHost = function() {
112 + return false;
113 + };
114 +} else {
115 + if (typeof console !== 'undefined') {
116 + // TODO: Remove fb.me link
117 + if (typeof localRequestAnimationFrame !== 'function') {
118 + console.error(
119 + "This browser doesn't support requestAnimationFrame. " +
120 + 'Make sure that you load a ' +
121 + 'polyfill in older browsers. https://fb.me/react-polyfills',
122 + );
123 + }
124 + if (typeof localCancelAnimationFrame !== 'function') {
125 + console.error(
126 + "This browser doesn't support cancelAnimationFrame. " +
127 + 'Make sure that you load a ' +
128 + 'polyfill in older browsers. https://fb.me/react-polyfills',
129 + );
130 + }
131 + }
132 +
133 + let scheduledHostCallback = null;
134 + let isMessageEventScheduled = false;
135 + let timeoutTime = -1;
136 +
137 + let isAnimationFrameScheduled = false;
138 +
139 + let isFlushingHostCallback = false;
140 +
141 + let frameDeadline = 0;
142 + // We start out assuming that we run at 30fps but then the heuristic tracking
143 + // will adjust this value to a faster fps if we get more frequent animation
144 + // frames.
145 + let previousFrameTime = 33;
146 + let activeFrameTime = 33;
147 +
148 + shouldYieldToHost = function() {
149 + return frameDeadline <= getCurrentTime();
150 + };
151 +
152 + // We use the postMessage trick to defer idle work until after the repaint.
153 + const channel = new MessageChannel();
154 + const port = channel.port2;
155 + channel.port1.onmessage = function(event) {
156 + isMessageEventScheduled = false;
157 +
158 + const prevScheduledCallback = scheduledHostCallback;
159 + const prevTimeoutTime = timeoutTime;
160 + scheduledHostCallback = null;
161 + timeoutTime = -1;
162 +
163 + const currentTime = getCurrentTime();
164 +
165 + let didTimeout = false;
166 + if (frameDeadline - currentTime <= 0) {
167 + // There's no time left in this idle period. Check if the callback has
168 + // a timeout and whether it's been exceeded.
169 + if (prevTimeoutTime !== -1 && prevTimeoutTime <= currentTime) {
170 + // Exceeded the timeout. Invoke the callback even though there's no
171 + // time left.
172 + didTimeout = true;
173 + } else {
174 + // No timeout.
175 + if (!isAnimationFrameScheduled) {
176 + // Schedule another animation callback so we retry later.
177 + isAnimationFrameScheduled = true;
178 + requestAnimationFrameWithTimeout(animationTick);
179 + }
180 + // Exit without invoking the callback.
181 + scheduledHostCallback = prevScheduledCallback;
182 + timeoutTime = prevTimeoutTime;
183 + return;
184 + }
185 + }
186 +
187 + if (prevScheduledCallback !== null) {
188 + isFlushingHostCallback = true;
189 + try {
190 + prevScheduledCallback(didTimeout);
191 + } finally {
192 + isFlushingHostCallback = false;
193 + }
194 + }
195 + };
196 +
197 + const animationTick = function(rafTime) {
198 + if (scheduledHostCallback !== null) {
199 + // Eagerly schedule the next animation callback at the beginning of the
200 + // frame. If the scheduler queue is not empty at the end of the frame, it
201 + // will continue flushing inside that callback. If the queue *is* empty,
202 + // then it will exit immediately. Posting the callback at the start of the
203 + // frame ensures it's fired within the earliest possible frame. If we
204 + // waited until the end of the frame to post the callback, we risk the
205 + // browser skipping a frame and not firing the callback until the frame
206 + // after that.
207 + requestAnimationFrameWithTimeout(animationTick);
208 + } else {
209 + // No pending work. Exit.
210 + isAnimationFrameScheduled = false;
211 + return;
212 + }
213 +
214 + let nextFrameTime = rafTime - frameDeadline + activeFrameTime;
215 + if (
216 + nextFrameTime < activeFrameTime &&
217 + previousFrameTime < activeFrameTime
218 + ) {
219 + if (nextFrameTime < 8) {
220 + // Defensive coding. We don't support higher frame rates than 120hz.
221 + // If the calculated frame time gets lower than 8, it is probably a bug.
222 + nextFrameTime = 8;
223 + }
224 + // If one frame goes long, then the next one can be short to catch up.
225 + // If two frames are short in a row, then that's an indication that we
226 + // actually have a higher frame rate than what we're currently optimizing.
227 + // We adjust our heuristic dynamically accordingly. For example, if we're
228 + // running on 120hz display or 90hz VR display.
229 + // Take the max of the two in case one of them was an anomaly due to
230 + // missed frame deadlines.
231 + activeFrameTime =
232 + nextFrameTime < previousFrameTime ? previousFrameTime : nextFrameTime;
233 + } else {
234 + previousFrameTime = nextFrameTime;
235 + }
236 + frameDeadline = rafTime + activeFrameTime;
237 + if (!isMessageEventScheduled) {
238 + isMessageEventScheduled = true;
239 + port.postMessage(undefined);
240 + }
241 + };
242 +
243 + requestHostCallback = function(callback, absoluteTimeout) {
244 + scheduledHostCallback = callback;
245 + timeoutTime = absoluteTimeout;
246 + if (isFlushingHostCallback || absoluteTimeout < 0) {
247 + // Don't wait for the next frame. Continue working ASAP, in a new event.
248 + port.postMessage(undefined);
249 + } else if (!isAnimationFrameScheduled) {
250 + // If rAF didn't already schedule one, we need to schedule a frame.
251 + // TODO: If this rAF doesn't materialize because the browser throttles, we
252 + // might want to still have setTimeout trigger rIC as a backup to ensure
253 + // that we keep performing work.
254 + isAnimationFrameScheduled = true;
255 + requestAnimationFrameWithTimeout(animationTick);
256 + }
257 + };
258 +
259 + cancelHostCallback = function() {
260 + scheduledHostCallback = null;
261 + isMessageEventScheduled = false;
262 + timeoutTime = -1;
263 + };
264 +}
packages/scheduler/src/forks/SchedulerHostConfig.mock.js new
+167
@@ -0,0 +1,167 @@
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 + * @flow
8 + */
9 +
10 +let currentTime: number = 0;
11 +let scheduledCallback: (boolean => void) | null = null;
12 +let scheduledCallbackExpiration: number = -1;
13 +let yieldedValues: Array<mixed> | null = null;
14 +let expectedNumberOfYields: number = -1;
15 +let didStop: boolean = false;
16 +let isFlushing: boolean = false;
17 +
18 +export function requestHostCallback(
19 + callback: boolean => void,
20 + expiration: number,
21 +) {
22 + scheduledCallback = callback;
23 + scheduledCallbackExpiration = expiration;
24 +}
25 +
26 +export function cancelHostCallback(): void {
27 + scheduledCallback = null;
28 + scheduledCallbackExpiration = -1;
29 +}
30 +
31 +export function shouldYieldToHost(): boolean {
32 + if (
33 + (expectedNumberOfYields !== -1 &&
34 + yieldedValues !== null &&
35 + yieldedValues.length >= expectedNumberOfYields) ||
36 + (scheduledCallbackExpiration !== -1 &&
37 + scheduledCallbackExpiration <= currentTime)
38 + ) {
39 + // We yielded at least as many values as expected. Stop flushing.
40 + didStop = true;
41 + return true;
42 + }
43 + return false;
44 +}
45 +
46 +export function getCurrentTime(): number {
47 + return currentTime;
48 +}
49 +
50 +export function reset() {
51 + if (isFlushing) {
52 + throw new Error('Cannot reset while already flushing work.');
53 + }
54 + currentTime = 0;
55 + scheduledCallback = null;
56 + scheduledCallbackExpiration = -1;
57 + yieldedValues = null;
58 + expectedNumberOfYields = -1;
59 + didStop = false;
60 + isFlushing = false;
61 +}
62 +
63 +// Should only be used via an assertion helper that inspects the yielded values.
64 +export function unstable_flushNumberOfYields(count: number): void {
65 + if (isFlushing) {
66 + throw new Error('Already flushing work.');
67 + }
68 + expectedNumberOfYields = count;
69 + isFlushing = true;
70 + try {
71 + while (scheduledCallback !== null && !didStop) {
72 + const cb = scheduledCallback;
73 + scheduledCallback = null;
74 + const didTimeout =
75 + scheduledCallbackExpiration !== -1 &&
76 + scheduledCallbackExpiration <= currentTime;
77 + cb(didTimeout);
78 + }
79 + } finally {
80 + expectedNumberOfYields = -1;
81 + didStop = false;
82 + isFlushing = false;
83 + }
84 +}
85 +
86 +export function unstable_flushExpired() {
87 + if (isFlushing) {
88 + throw new Error('Already flushing work.');
89 + }
90 + if (scheduledCallback !== null) {
91 + const cb = scheduledCallback;
92 + scheduledCallback = null;
93 + isFlushing = true;
94 + try {
95 + cb(true);
96 + } finally {
97 + isFlushing = false;
98 + }
99 + }
100 +}
101 +
102 +export function unstable_flushWithoutYielding(): void {
103 + if (isFlushing) {
104 + throw new Error('Already flushing work.');
105 + }
106 + isFlushing = true;
107 + try {
108 + while (scheduledCallback !== null) {
109 + const cb = scheduledCallback;
110 + scheduledCallback = null;
111 + const didTimeout =
112 + scheduledCallbackExpiration !== -1 &&
113 + scheduledCallbackExpiration <= currentTime;
114 + cb(didTimeout);
115 + }
116 + } finally {
117 + expectedNumberOfYields = -1;
118 + didStop = false;
119 + isFlushing = false;
120 + }
121 +}
122 +
123 +export function unstable_clearYields(): Array<mixed> {
124 + if (yieldedValues === null) {
125 + return [];
126 + }
127 + const values = yieldedValues;
128 + yieldedValues = null;
129 + return values;
130 +}
131 +
132 +export function flushAll(): void {
133 + if (yieldedValues !== null) {
134 + throw new Error(
135 + 'Log is not empty. Assert on the log of yielded values before ' +
136 + 'flushing additional work.',
137 + );
138 + }
139 + unstable_flushWithoutYielding();
140 + if (yieldedValues !== null) {
141 + throw new Error(
142 + 'While flushing work, something yielded a value. Use an ' +
143 + 'assertion helper to assert on the log of yielded values, e.g. ' +
144 + 'expect(Scheduler).toFlushAndYield([...])',
145 + );
146 + }
147 +}
148 +
149 +export function yieldValue(value: mixed): void {
150 + if (yieldedValues === null) {
151 + yieldedValues = [value];
152 + } else {
153 + yieldedValues.push(value);
154 + }
155 +}
156 +
157 +export function advanceTime(ms: number) {
158 + currentTime += ms;
159 + // If the host callback timed out, flush the expired work.
160 + if (
161 + !isFlushing &&
162 + scheduledCallbackExpiration !== -1 &&
163 + scheduledCallbackExpiration <= currentTime
164 + ) {
165 + unstable_flushExpired();
166 + }
167 +}
packages/scheduler/unstable_mock.js new
+20
@@ -0,0 +1,20 @@
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 +
8 +'use strict';
9 +
10 +export * from './src/Scheduler';
11 +
12 +export {
13 + unstable_flushWithoutYielding,
14 + unstable_flushNumberOfYields,
15 + unstable_flushExpired,
16 + unstable_clearYields,
17 + flushAll,
18 + yieldValue,
19 + advanceTime,
20 +} from './src/SchedulerHostConfig.js';
packages/shared/__tests__/ReactDOMFrameScheduling-test.js
+12 -5
@@ -10,6 +10,18 @@
10 'use strict';
11
12 describe('ReactDOMFrameScheduling', () => {
13 + beforeEach(() => {
14 + jest.resetModules();
15 +
16 + // Un-mock scheduler
17 + jest.mock('scheduler', () => require.requireActual('scheduler'));
18 + jest.mock('scheduler/src/SchedulerHostConfig', () =>
19 + require.requireActual(
20 + 'scheduler/src/forks/SchedulerHostConfig.default.js',
21 + ),
22 + );
23 + });
24 +
25 it('warns when requestAnimationFrame is not polyfilled in the browser', () => {
26 const previousRAF = global.requestAnimationFrame;
27 const previousMessageChannel = global.MessageChannel;
@@ -21,11 +33,6 @@ describe('ReactDOMFrameScheduling', () => {
33 port2: {},
34 };
35 };
24 - jest.resetModules();
25 -
26 - const JestMockScheduler = require('jest-mock-scheduler');
27 - JestMockScheduler.mockRestore();
28 -
36 spyOnDevAndProd(console, 'error');
37 require('react-dom');
38 expect(console.error.calls.count()).toEqual(1);
scripts/jest/config.build.js
+5 -1
@@ -35,7 +35,7 @@ packages.forEach(name => {
35 moduleNameMapper[`^${name}$`] = `<rootDir>/build/node_modules/${name}`;
36 // Named entry points
37 moduleNameMapper[
38 - `^${name}/(.*)$`
38 + `^${name}\/([^\/]+)$`
39 ] = `<rootDir>/build/node_modules/${name}/$1`;
40 });
41
@@ -46,4 +46,8 @@ module.exports = Object.assign({}, baseConfig, {
46 testPathIgnorePatterns: ['/node_modules/', '-test.internal.js$'],
47 // Exclude the build output from transforms
48 transformIgnorePatterns: ['/node_modules/', '<rootDir>/build/'],
49 + setupFiles: [
50 + ...baseConfig.setupFiles,
51 + require.resolve('./setupTests.build.js'),
52 + ],
53 });
scripts/jest/matchers/reactTestMatchers.js
+20
@@ -1,6 +1,7 @@
1 'use strict';
2
3 const JestReact = require('jest-react');
4 +const SchedulerMatchers = require('./schedulerTestMatchers');
5
6 function captureAssertion(fn) {
7 // Trick to use a Jest matcher inside another Jest matcher. `fn` contains an
@@ -18,6 +19,10 @@ function captureAssertion(fn) {
19 return {pass: true};
20 }
21
22 +function isScheduler(obj) {
23 + return typeof obj.unstable_scheduleCallback === 'function';
24 +}
25 +
26 function isReactNoop(obj) {
27 return typeof obj.hasScheduledCallback === 'function';
28 }
@@ -33,6 +38,9 @@ function assertYieldsWereCleared(ReactNoop) {
38 }
39
40 function toFlushAndYield(ReactNoop, expectedYields) {
41 + if (isScheduler(ReactNoop)) {
42 + return SchedulerMatchers.toFlushAndYield(ReactNoop, expectedYields);
43 + }
44 if (!isReactNoop(ReactNoop)) {
45 return JestReact.unstable_toFlushAndYield(ReactNoop, expectedYields);
46 }
@@ -44,6 +52,9 @@ function toFlushAndYield(ReactNoop, expectedYields) {
52 }
53
54 function toFlushAndYieldThrough(ReactNoop, expectedYields) {
55 + if (isScheduler(ReactNoop)) {
56 + return SchedulerMatchers.toFlushAndYieldThrough(ReactNoop, expectedYields);
57 + }
58 if (!isReactNoop(ReactNoop)) {
59 return JestReact.unstable_toFlushAndYieldThrough(ReactNoop, expectedYields);
60 }
@@ -57,6 +68,9 @@ function toFlushAndYieldThrough(ReactNoop, expectedYields) {
68 }
69
70 function toFlushWithoutYielding(ReactNoop) {
71 + if (isScheduler(ReactNoop)) {
72 + return SchedulerMatchers.toFlushWithoutYielding(ReactNoop);
73 + }
74 if (!isReactNoop(ReactNoop)) {
75 return JestReact.unstable_toFlushWithoutYielding(ReactNoop);
76 }
@@ -64,6 +78,9 @@ function toFlushWithoutYielding(ReactNoop) {
78 }
79
80 function toHaveYielded(ReactNoop, expectedYields) {
81 + if (isScheduler(ReactNoop)) {
82 + return SchedulerMatchers.toHaveYielded(ReactNoop, expectedYields);
83 + }
84 if (!isReactNoop(ReactNoop)) {
85 return JestReact.unstable_toHaveYielded(ReactNoop, expectedYields);
86 }
@@ -74,6 +91,9 @@ function toHaveYielded(ReactNoop, expectedYields) {
91 }
92
93 function toFlushAndThrow(ReactNoop, ...rest) {
94 + if (isScheduler(ReactNoop)) {
95 + return SchedulerMatchers.toFlushAndThrow(ReactNoop, ...rest);
96 + }
97 if (!isReactNoop(ReactNoop)) {
98 return JestReact.unstable_toFlushAndThrow(ReactNoop, ...rest);
99 }
scripts/jest/matchers/schedulerTestMatchers.js new
+73
@@ -0,0 +1,73 @@
1 +'use strict';
2 +
3 +function captureAssertion(fn) {
4 + // Trick to use a Jest matcher inside another Jest matcher. `fn` contains an
5 + // assertion; if it throws, we capture the error and return it, so the stack
6 + // trace presented to the user points to the original assertion in the
7 + // test file.
8 + try {
9 + fn();
10 + } catch (error) {
11 + return {
12 + pass: false,
13 + message: () => error.message,
14 + };
15 + }
16 + return {pass: true};
17 +}
18 +
19 +function assertYieldsWereCleared(Scheduler) {
20 + const actualYields = Scheduler.unstable_clearYields();
21 + if (actualYields.length !== 0) {
22 + throw new Error(
23 + 'Log of yielded values is not empty. ' +
24 + 'Call expect(Scheduler).toHaveYielded(...) first.'
25 + );
26 + }
27 +}
28 +
29 +function toFlushAndYield(Scheduler, expectedYields) {
30 + assertYieldsWereCleared(Scheduler);
31 + Scheduler.unstable_flushWithoutYielding();
32 + const actualYields = Scheduler.unstable_clearYields();
33 + return captureAssertion(() => {
34 + expect(actualYields).toEqual(expectedYields);
35 + });
36 +}
37 +
38 +function toFlushAndYieldThrough(Scheduler, expectedYields) {
39 + assertYieldsWereCleared(Scheduler);
40 + Scheduler.unstable_flushNumberOfYields(expectedYields.length);
41 + const actualYields = Scheduler.unstable_clearYields();
42 + return captureAssertion(() => {
43 + expect(actualYields).toEqual(expectedYields);
44 + });
45 +}
46 +
47 +function toFlushWithoutYielding(Scheduler) {
48 + return toFlushAndYield(Scheduler, []);
49 +}
50 +
51 +function toHaveYielded(Scheduler, expectedYields) {
52 + return captureAssertion(() => {
53 + const actualYields = Scheduler.unstable_clearYields();
54 + expect(actualYields).toEqual(expectedYields);
55 + });
56 +}
57 +
58 +function toFlushAndThrow(Scheduler, ...rest) {
59 + assertYieldsWereCleared(Scheduler);
60 + return captureAssertion(() => {
61 + expect(() => {
62 + Scheduler.unstable_flushWithoutYielding();
63 + }).toThrow(...rest);
64 + });
65 +}
66 +
67 +module.exports = {
68 + toFlushAndYield,
69 + toFlushAndYieldThrough,
70 + toFlushWithoutYielding,
71 + toHaveYielded,
72 + toFlushAndThrow,
73 +};
scripts/jest/setupHostConfigs.js
+5
@@ -111,3 +111,8 @@ inlinedHostConfigs.forEach(rendererInfo => {
111 jest.mock('shared/ReactSharedInternals', () =>
112 require.requireActual('react/src/ReactSharedInternals')
113 );
114 +
115 +jest.mock('scheduler', () => require.requireActual('scheduler/unstable_mock'));
116 +jest.mock('scheduler/src/SchedulerHostConfig', () =>
117 + require.requireActual('scheduler/src/forks/SchedulerHostConfig.mock.js')
118 +);
scripts/jest/setupTests.build.js new
+6
@@ -0,0 +1,6 @@
1 +'use strict';
2 +
3 +jest.mock('scheduler', () => require.requireActual('scheduler/unstable_mock'));
4 +jest.mock('scheduler/src/SchedulerHostConfig', () =>
5 + require.requireActual('scheduler/src/forks/SchedulerHostConfig.mock.js')
6 +);
scripts/jest/setupTests.js
-2
@@ -49,8 +49,6 @@ if (process.env.REACT_CLASS_EQUIVALENCE_TEST) {
49 ...require('./matchers/reactTestMatchers'),
50 });
51
52 - require('jest-mock-scheduler');
53 -
52 // We have a Babel transform that inserts guards against infinite loops.
53 // If a loop runs for too many iterations, we throw an error and set this
54 // global variable. The global lets us detect an infinite loop even if
scripts/rollup/bundles.js
+6 -6
@@ -453,21 +453,21 @@ const bundles = [
453 externals: [],
454 },
455
456 - /******* Jest React (experimental) *******/
456 + /******* React Scheduler Mock (experimental) *******/
457 {
458 bundleTypes: [NODE_DEV, NODE_PROD, FB_WWW_DEV, FB_WWW_PROD],
459 moduleType: ISOMORPHIC,
460 - entry: 'jest-react',
461 - global: 'JestReact',
460 + entry: 'scheduler/unstable_mock',
461 + global: 'SchedulerMock',
462 externals: [],
463 },
464
465 - /******* Jest Scheduler (experimental) *******/
465 + /******* Jest React (experimental) *******/
466 {
467 bundleTypes: [NODE_DEV, NODE_PROD, FB_WWW_DEV, FB_WWW_PROD],
468 moduleType: ISOMORPHIC,
469 - entry: 'jest-mock-scheduler',
470 - global: 'JestMockScheduler',
469 + entry: 'jest-react',
470 + global: 'JestReact',
471 externals: [],
472 },
473
scripts/rollup/forks.js
+10 -4
@@ -159,16 +159,22 @@ const forks = Object.freeze({
159
160 'scheduler/src/SchedulerFeatureFlags': (bundleType, entry, dependencies) => {
161 if (
162 - entry === 'scheduler' &&
163 - (bundleType === FB_WWW_DEV ||
164 - bundleType === FB_WWW_PROD ||
165 - bundleType === FB_WWW_PROFILING)
162 + bundleType === FB_WWW_DEV ||
163 + bundleType === FB_WWW_PROD ||
164 + bundleType === FB_WWW_PROFILING
165 ) {
166 return 'scheduler/src/forks/SchedulerFeatureFlags.www.js';
167 }
168 return 'scheduler/src/SchedulerFeatureFlags';
169 },
170
171 + 'scheduler/src/SchedulerHostConfig': (bundleType, entry, dependencies) => {
172 + if (entry === 'scheduler/unstable_mock') {
173 + return 'scheduler/src/forks/SchedulerHostConfig.mock';
174 + }
175 + return 'scheduler/src/forks/SchedulerHostConfig.default';
176 + },
177 +
178 // This logic is forked on www to fork the formatting function.
179 'shared/invariant': (bundleType, entry) => {
180 switch (bundleType) {
scripts/rollup/results.json
+170 -142
@@ -4,22 +4,22 @@
4 "filename": "react.development.js",
5 "bundleType": "UMD_DEV",
6 "packageName": "react",
7 - "size": 101989,
8 - "gzip": 26428
7 + "size": 102032,
8 + "gzip": 26457
9 },
10 {
11 "filename": "react.production.min.js",
12 "bundleType": "UMD_PROD",
13 "packageName": "react",
14 - "size": 12548,
15 - "gzip": 4823
14 + "size": 12564,
15 + "gzip": 4826
16 },
17 {
18 "filename": "react.development.js",
19 "bundleType": "NODE_DEV",
20 "packageName": "react",
21 - "size": 63522,
22 - "gzip": 17094
21 + "size": 63704,
22 + "gzip": 17181
23 },
24 {
25 "filename": "react.production.min.js",
@@ -46,29 +46,29 @@
46 "filename": "react-dom.development.js",
47 "bundleType": "UMD_DEV",
48 "packageName": "react-dom",
49 - "size": 783692,
50 - "gzip": 178609
49 + "size": 791682,
50 + "gzip": 179962
51 },
52 {
53 "filename": "react-dom.production.min.js",
54 "bundleType": "UMD_PROD",
55 "packageName": "react-dom",
56 - "size": 107842,
57 - "gzip": 34729
56 + "size": 107808,
57 + "gzip": 34704
58 },
59 {
60 "filename": "react-dom.development.js",
61 "bundleType": "NODE_DEV",
62 "packageName": "react-dom",
63 - "size": 778167,
64 - "gzip": 177083
63 + "size": 786187,
64 + "gzip": 178415
65 },
66 {
67 "filename": "react-dom.production.min.js",
68 "bundleType": "NODE_PROD",
69 "packageName": "react-dom",
70 - "size": 108009,
71 - "gzip": 34209
70 + "size": 108031,
71 + "gzip": 34186
72 },
73 {
74 "filename": "ReactDOM-dev.js",
@@ -165,29 +165,29 @@
165 "filename": "react-dom-server.browser.development.js",
166 "bundleType": "UMD_DEV",
167 "packageName": "react-dom",
168 - "size": 129798,
169 - "gzip": 34602
168 + "size": 130425,
169 + "gzip": 34746
170 },
171 {
172 "filename": "react-dom-server.browser.production.min.js",
173 "bundleType": "UMD_PROD",
174 "packageName": "react-dom",
175 - "size": 19117,
176 - "gzip": 7343
175 + "size": 19319,
176 + "gzip": 7379
177 },
178 {
179 "filename": "react-dom-server.browser.development.js",
180 "bundleType": "NODE_DEV",
181 "packageName": "react-dom",
182 - "size": 125836,
183 - "gzip": 33642
182 + "size": 126463,
183 + "gzip": 33795
184 },
185 {
186 "filename": "react-dom-server.browser.production.min.js",
187 "bundleType": "NODE_PROD",
188 "packageName": "react-dom",
189 - "size": 19037,
190 - "gzip": 7325
189 + "size": 19239,
190 + "gzip": 7368
191 },
192 {
193 "filename": "ReactDOMServer-dev.js",
@@ -207,43 +207,43 @@
207 "filename": "react-dom-server.node.development.js",
208 "bundleType": "NODE_DEV",
209 "packageName": "react-dom",
210 - "size": 127943,
211 - "gzip": 34197
210 + "size": 128570,
211 + "gzip": 34348
212 },
213 {
214 "filename": "react-dom-server.node.production.min.js",
215 "bundleType": "NODE_PROD",
216 "packageName": "react-dom",
217 - "size": 19930,
218 - "gzip": 7641
217 + "size": 20132,
218 + "gzip": 7685
219 },
220 {
221 "filename": "react-art.development.js",
222 "bundleType": "UMD_DEV",
223 "packageName": "react-art",
224 - "size": 554932,
225 - "gzip": 120585
224 + "size": 562387,
225 + "gzip": 121860
226 },
227 {
228 "filename": "react-art.production.min.js",
229 "bundleType": "UMD_PROD",
230 "packageName": "react-art",
231 - "size": 99655,
232 - "gzip": 30575
231 + "size": 99616,
232 + "gzip": 30538
233 },
234 {
235 "filename": "react-art.development.js",
236 "bundleType": "NODE_DEV",
237 "packageName": "react-art",
238 - "size": 484327,
239 - "gzip": 102945
238 + "size": 491818,
239 + "gzip": 104175
240 },
241 {
242 "filename": "react-art.production.min.js",
243 "bundleType": "NODE_PROD",
244 "packageName": "react-art",
245 - "size": 63856,
246 - "gzip": 19480
245 + "size": 63873,
246 + "gzip": 19446
247 },
248 {
249 "filename": "ReactART-dev.js",
@@ -291,29 +291,29 @@
291 "filename": "react-test-renderer.development.js",
292 "bundleType": "UMD_DEV",
293 "packageName": "react-test-renderer",
294 - "size": 496691,
295 - "gzip": 105367
294 + "size": 503952,
295 + "gzip": 106548
296 },
297 {
298 "filename": "react-test-renderer.production.min.js",
299 "bundleType": "UMD_PROD",
300 "packageName": "react-test-renderer",
301 - "size": 65252,
302 - "gzip": 19980
301 + "size": 65222,
302 + "gzip": 19962
303 },
304 {
305 "filename": "react-test-renderer.development.js",
306 "bundleType": "NODE_DEV",
307 "packageName": "react-test-renderer",
308 - "size": 490997,
309 - "gzip": 104031
308 + "size": 498258,
309 + "gzip": 105207
310 },
311 {
312 "filename": "react-test-renderer.production.min.js",
313 "bundleType": "NODE_PROD",
314 "packageName": "react-test-renderer",
315 - "size": 64908,
316 - "gzip": 19647
315 + "size": 64873,
316 + "gzip": 19614
317 },
318 {
319 "filename": "ReactTestRenderer-dev.js",
@@ -361,43 +361,43 @@
361 "filename": "react-noop-renderer.development.js",
362 "bundleType": "NODE_DEV",
363 "packageName": "react-noop-renderer",
364 - "size": 32858,
365 - "gzip": 7514
364 + "size": 28240,
365 + "gzip": 6740
366 },
367 {
368 "filename": "react-noop-renderer.production.min.js",
369 "bundleType": "NODE_PROD",
370 "packageName": "react-noop-renderer",
371 - "size": 11486,
372 - "gzip": 3766
371 + "size": 10081,
372 + "gzip": 3367
373 },
374 {
375 "filename": "react-reconciler.development.js",
376 "bundleType": "NODE_DEV",
377 "packageName": "react-reconciler",
378 - "size": 481582,
379 - "gzip": 101249
378 + "size": 489120,
379 + "gzip": 102507
380 },
381 {
382 "filename": "react-reconciler.production.min.js",
383 "bundleType": "NODE_PROD",
384 "packageName": "react-reconciler",
385 - "size": 65118,
386 - "gzip": 19259
385 + "size": 65080,
386 + "gzip": 19234
387 },
388 {
389 "filename": "react-reconciler-persistent.development.js",
390 "bundleType": "NODE_DEV",
391 "packageName": "react-reconciler",
392 - "size": 479920,
393 - "gzip": 100594
392 + "size": 487276,
393 + "gzip": 101780
394 },
395 {
396 "filename": "react-reconciler-persistent.production.min.js",
397 "bundleType": "NODE_PROD",
398 "packageName": "react-reconciler",
399 - "size": 65129,
400 - "gzip": 19264
399 + "size": 65091,
400 + "gzip": 19239
401 },
402 {
403 "filename": "react-reconciler-reflection.development.js",
@@ -501,8 +501,8 @@
501 "filename": "React-dev.js",
502 "bundleType": "FB_WWW_DEV",
503 "packageName": "react",
504 - "size": 60700,
505 - "gzip": 16126
504 + "size": 60786,
505 + "gzip": 16140
506 },
507 {
508 "filename": "React-prod.js",
@@ -515,15 +515,15 @@
515 "filename": "ReactDOM-dev.js",
516 "bundleType": "FB_WWW_DEV",
517 "packageName": "react-dom",
518 - "size": 801709,
519 - "gzip": 178348
518 + "size": 809740,
519 + "gzip": 179616
520 },
521 {
522 "filename": "ReactDOM-prod.js",
523 "bundleType": "FB_WWW_PROD",
524 "packageName": "react-dom",
525 - "size": 329235,
526 - "gzip": 60001
525 + "size": 329960,
526 + "gzip": 60112
527 },
528 {
529 "filename": "ReactTestUtils-dev.js",
@@ -550,92 +550,92 @@
550 "filename": "ReactDOMServer-dev.js",
551 "bundleType": "FB_WWW_DEV",
552 "packageName": "react-dom",
553 - "size": 126805,
554 - "gzip": 33138
553 + "size": 127337,
554 + "gzip": 33235
555 },
556 {
557 "filename": "ReactDOMServer-prod.js",
558 "bundleType": "FB_WWW_PROD",
559 "packageName": "react-dom",
560 - "size": 46040,
561 - "gzip": 10601
560 + "size": 46343,
561 + "gzip": 10662
562 },
563 {
564 "filename": "ReactART-dev.js",
565 "bundleType": "FB_WWW_DEV",
566 "packageName": "react-art",
567 - "size": 493866,
568 - "gzip": 102238
567 + "size": 501297,
568 + "gzip": 103384
569 },
570 {
571 "filename": "ReactART-prod.js",
572 "bundleType": "FB_WWW_PROD",
573 "packageName": "react-art",
574 - "size": 199704,
575 - "gzip": 33787
574 + "size": 200132,
575 + "gzip": 33817
576 },
577 {
578 "filename": "ReactNativeRenderer-dev.js",
579 "bundleType": "RN_FB_DEV",
580 "packageName": "react-native-renderer",
581 - "size": 621398,
582 - "gzip": 133323
581 + "size": 631255,
582 + "gzip": 134845
583 },
584 {
585 "filename": "ReactNativeRenderer-prod.js",
586 "bundleType": "RN_FB_PROD",
587 "packageName": "react-native-renderer",
588 - "size": 252107,
589 - "gzip": 44030
588 + "size": 252735,
589 + "gzip": 44084
590 },
591 {
592 "filename": "ReactNativeRenderer-dev.js",
593 "bundleType": "RN_OSS_DEV",
594 "packageName": "react-native-renderer",
595 - "size": 621309,
596 - "gzip": 133286
595 + "size": 631168,
596 + "gzip": 134810
597 },
598 {
599 "filename": "ReactNativeRenderer-prod.js",
600 "bundleType": "RN_OSS_PROD",
601 "packageName": "react-native-renderer",
602 - "size": 252121,
603 - "gzip": 44026
602 + "size": 252749,
603 + "gzip": 44079
604 },
605 {
606 "filename": "ReactFabric-dev.js",
607 "bundleType": "RN_FB_DEV",
608 "packageName": "react-native-renderer",
609 - "size": 612032,
610 - "gzip": 130990
609 + "size": 621889,
610 + "gzip": 132504
611 },
612 {
613 "filename": "ReactFabric-prod.js",
614 "bundleType": "RN_FB_PROD",
615 "packageName": "react-native-renderer",
616 - "size": 244266,
617 - "gzip": 42532
616 + "size": 244896,
617 + "gzip": 42571
618 },
619 {
620 "filename": "ReactFabric-dev.js",
621 "bundleType": "RN_OSS_DEV",
622 "packageName": "react-native-renderer",
623 - "size": 611935,
624 - "gzip": 130940
623 + "size": 621794,
624 + "gzip": 132455
625 },
626 {
627 "filename": "ReactFabric-prod.js",
628 "bundleType": "RN_OSS_PROD",
629 "packageName": "react-native-renderer",
630 - "size": 244272,
631 - "gzip": 42522
630 + "size": 244902,
631 + "gzip": 42561
632 },
633 {
634 "filename": "ReactTestRenderer-dev.js",
635 "bundleType": "FB_WWW_DEV",
636 "packageName": "react-test-renderer",
637 - "size": 501300,
638 - "gzip": 103689
637 + "size": 508573,
638 + "gzip": 104823
639 },
640 {
641 "filename": "ReactShallowRenderer-dev.js",
@@ -676,15 +676,15 @@
676 "filename": "scheduler.development.js",
677 "bundleType": "NODE_DEV",
678 "packageName": "scheduler",
679 - "size": 23870,
680 - "gzip": 6174
679 + "size": 23505,
680 + "gzip": 6019
681 },
682 {
683 "filename": "scheduler.production.min.js",
684 "bundleType": "NODE_PROD",
685 "packageName": "scheduler",
686 - "size": 5000,
687 - "gzip": 1883
686 + "size": 4888,
687 + "gzip": 1819
688 },
689 {
690 "filename": "SimpleCacheProvider-dev.js",
@@ -704,50 +704,50 @@
704 "filename": "react-noop-renderer-persistent.development.js",
705 "bundleType": "NODE_DEV",
706 "packageName": "react-noop-renderer",
707 - "size": 32977,
708 - "gzip": 7525
707 + "size": 28359,
708 + "gzip": 6753
709 },
710 {
711 "filename": "react-noop-renderer-persistent.production.min.js",
712 "bundleType": "NODE_PROD",
713 "packageName": "react-noop-renderer",
714 - "size": 11508,
715 - "gzip": 3772
714 + "size": 10103,
715 + "gzip": 3373
716 },
717 {
718 "filename": "react-dom.profiling.min.js",
719 "bundleType": "NODE_PROFILING",
720 "packageName": "react-dom",
721 - "size": 111156,
722 - "gzip": 35048
721 + "size": 111178,
722 + "gzip": 35023
723 },
724 {
725 "filename": "ReactNativeRenderer-profiling.js",
726 "bundleType": "RN_OSS_PROFILING",
727 "packageName": "react-native-renderer",
728 - "size": 258626,
729 - "gzip": 45614
728 + "size": 259254,
729 + "gzip": 45666
730 },
731 {
732 "filename": "ReactFabric-profiling.js",
733 "bundleType": "RN_OSS_PROFILING",
734 "packageName": "react-native-renderer",
735 - "size": 250654,
736 - "gzip": 44086
735 + "size": 251284,
736 + "gzip": 44137
737 },
738 {
739 "filename": "Scheduler-dev.js",
740 "bundleType": "FB_WWW_DEV",
741 "packageName": "scheduler",
742 - "size": 24123,
743 - "gzip": 6223
742 + "size": 23758,
743 + "gzip": 6067
744 },
745 {
746 "filename": "Scheduler-prod.js",
747 "bundleType": "FB_WWW_PROD",
748 "packageName": "scheduler",
749 - "size": 14327,
750 - "gzip": 2953
749 + "size": 14025,
750 + "gzip": 2841
751 },
752 {
753 "filename": "react.profiling.min.js",
@@ -767,43 +767,43 @@
767 "filename": "ReactDOM-profiling.js",
768 "bundleType": "FB_WWW_PROFILING",
769 "packageName": "react-dom",
770 - "size": 335969,
771 - "gzip": 61519
770 + "size": 336694,
771 + "gzip": 61627
772 },
773 {
774 "filename": "ReactNativeRenderer-profiling.js",
775 "bundleType": "RN_FB_PROFILING",
776 "packageName": "react-native-renderer",
777 - "size": 258607,
778 - "gzip": 45621
777 + "size": 259235,
778 + "gzip": 45675
779 },
780 {
781 "filename": "ReactFabric-profiling.js",
782 "bundleType": "RN_FB_PROFILING",
783 "packageName": "react-native-renderer",
784 - "size": 250643,
785 - "gzip": 44092
784 + "size": 251273,
785 + "gzip": 44141
786 },
787 {
788 "filename": "react.profiling.min.js",
789 "bundleType": "UMD_PROFILING",
790 "packageName": "react",
791 - "size": 14756,
792 - "gzip": 5369
791 + "size": 14773,
792 + "gzip": 5356
793 },
794 {
795 "filename": "react-dom.profiling.min.js",
796 "bundleType": "UMD_PROFILING",
797 "packageName": "react-dom",
798 - "size": 110830,
799 - "gzip": 35633
798 + "size": 110796,
799 + "gzip": 35607
800 },
801 {
802 "filename": "scheduler-tracing.development.js",
803 "bundleType": "NODE_DEV",
804 "packageName": "scheduler",
805 - "size": 10554,
806 - "gzip": 2432
805 + "size": 10737,
806 + "gzip": 2535
807 },
808 {
809 "filename": "scheduler-tracing.production.min.js",
@@ -823,8 +823,8 @@
823 "filename": "SchedulerTracing-dev.js",
824 "bundleType": "FB_WWW_DEV",
825 "packageName": "scheduler",
826 - "size": 10121,
827 - "gzip": 2117
826 + "size": 10207,
827 + "gzip": 2134
828 },
829 {
830 "filename": "SchedulerTracing-prod.js",
@@ -928,15 +928,15 @@
928 "filename": "eslint-plugin-react-hooks.development.js",
929 "bundleType": "NODE_DEV",
930 "packageName": "eslint-plugin-react-hooks",
931 - "size": 26115,
932 - "gzip": 6005
931 + "size": 50458,
932 + "gzip": 11887
933 },
934 {
935 "filename": "eslint-plugin-react-hooks.production.min.js",
936 "bundleType": "NODE_PROD",
937 "packageName": "eslint-plugin-react-hooks",
938 - "size": 5080,
939 - "gzip": 1872
938 + "size": 12598,
939 + "gzip": 4566
940 },
941 {
942 "filename": "ReactDOMFizzServer-dev.js",
@@ -1026,71 +1026,71 @@
1026 "filename": "ESLintPluginReactHooks-dev.js",
1027 "bundleType": "FB_WWW_DEV",
1028 "packageName": "eslint-plugin-react-hooks",
1029 - "size": 27788,
1030 - "gzip": 6145
1029 + "size": 54073,
1030 + "gzip": 12239
1031 },
1032 {
1033 "filename": "react-dom-unstable-fire.development.js",
1034 "bundleType": "UMD_DEV",
1035 "packageName": "react-dom",
1036 - "size": 784046,
1037 - "gzip": 178758
1036 + "size": 792036,
1037 + "gzip": 180102
1038 },
1039 {
1040 "filename": "react-dom-unstable-fire.production.min.js",
1041 "bundleType": "UMD_PROD",
1042 "packageName": "react-dom",
1043 - "size": 107857,
1044 - "gzip": 34738
1043 + "size": 107823,
1044 + "gzip": 34713
1045 },
1046 {
1047 "filename": "react-dom-unstable-fire.profiling.min.js",
1048 "bundleType": "UMD_PROFILING",
1049 "packageName": "react-dom",
1050 - "size": 110845,
1051 - "gzip": 35642
1050 + "size": 110811,
1051 + "gzip": 35616
1052 },
1053 {
1054 "filename": "react-dom-unstable-fire.development.js",
1055 "bundleType": "NODE_DEV",
1056 "packageName": "react-dom",
1057 - "size": 778520,
1058 - "gzip": 177227
1057 + "size": 786540,
1058 + "gzip": 178557
1059 },
1060 {
1061 "filename": "react-dom-unstable-fire.production.min.js",
1062 "bundleType": "NODE_PROD",
1063 "packageName": "react-dom",
1064 - "size": 108023,
1065 - "gzip": 34220
1064 + "size": 108045,
1065 + "gzip": 34197
1066 },
1067 {
1068 "filename": "react-dom-unstable-fire.profiling.min.js",
1069 "bundleType": "NODE_PROFILING",
1070 "packageName": "react-dom",
1071 - "size": 111170,
1072 - "gzip": 35058
1071 + "size": 111192,
1072 + "gzip": 35033
1073 },
1074 {
1075 "filename": "ReactFire-dev.js",
1076 "bundleType": "FB_WWW_DEV",
1077 "packageName": "react-dom",
1078 - "size": 800900,
1079 - "gzip": 178268
1078 + "size": 808931,
1079 + "gzip": 179569
1080 },
1081 {
1082 "filename": "ReactFire-prod.js",
1083 "bundleType": "FB_WWW_PROD",
1084 "packageName": "react-dom",
1085 - "size": 317385,
1086 - "gzip": 57621
1085 + "size": 318110,
1086 + "gzip": 57721
1087 },
1088 {
1089 "filename": "ReactFire-profiling.js",
1090 "bundleType": "FB_WWW_PROFILING",
1091 "packageName": "react-dom",
1092 - "size": 324156,
1093 - "gzip": 59066
1092 + "size": 324881,
1093 + "gzip": 59172
1094 },
1095 {
1096 "filename": "jest-mock-scheduler.development.js",
@@ -1119,6 +1119,34 @@
1119 "packageName": "jest-mock-scheduler",
1120 "size": 1085,
1121 "gzip": 532
1122 + },
1123 + {
1124 + "filename": "scheduler-unstable_mock.development.js",
1125 + "bundleType": "NODE_DEV",
1126 + "packageName": "scheduler",
1127 + "size": 17926,
1128 + "gzip": 4128
1129 + },
1130 + {
1131 + "filename": "scheduler-unstable_mock.production.min.js",
1132 + "bundleType": "NODE_PROD",
1133 + "packageName": "scheduler",
1134 + "size": 4173,
1135 + "gzip": 1606
1136 + },
1137 + {
1138 + "filename": "SchedulerMock-dev.js",
1139 + "bundleType": "FB_WWW_DEV",
1140 + "packageName": "scheduler",
1141 + "size": 18170,
1142 + "gzip": 4175
1143 + },
1144 + {
1145 + "filename": "SchedulerMock-prod.js",
1146 + "bundleType": "FB_WWW_PROD",
1147 + "packageName": "scheduler",
1148 + "size": 12088,
1149 + "gzip": 2473
1150 }
1151 ]
1152 }
\ No newline at end of file