`act()` - s / flushPassiveEffects / Scheduler.unstable_flushWithoutYielding (#15591)

* s/flushPassiveEffects/unstable_flushWithoutYielding

a first crack at flushing the scheduler manually from inside act(). uses unstable_flushWithoutYielding(). The tests that changed, mostly replaced toFlushAndYield(...) with toHaveYielded(). For some tests that tested the state of the tree before flushing effects (but still after updates), I replaced act() with bacthedUpdates().

* ugh lint

* pass build, flushPassiveEffects returns nothing now

* pass test-fire

* flush all work (not just effects), add a compatibility mode

of note, unstable_flushWithoutYielding now returns a boolean much like flushPassiveEffects

* umd build for scheduler/unstable_mock, pass the fixture with it

* add a comment to Shcduler.umd.js for why we're exporting unstable_flushWithoutYielding

* run testsutilsact tests in both sync/concurrent modes

* augh lint

* use a feature flag for the missing mock scheduler warning

I also tried writing a test for it, but couldn't get the scheduler to unmock. included the failing test.

* Update ReactTestUtilsAct-test.js

- pass the mock scheduler warning test,
- rewrite some tests to use Scheduler.yieldValue
- structure concurrent/legacy suites neatly

* pass failing tests in batchedmode-test

* fix pretty/lint/import errors

* pass test-build

* nit: pull .create(null) out of the act() call
This commit is contained in:
Sunil Pai 2019-05-16 17:12:36 +01:00 committed by GitHub
parent aad5a264d2
commit d278a3ff8b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 662 additions and 514 deletions

View File

@ -8,6 +8,8 @@ coverage
# production # production
build build
public/scheduler-unstable_mock.development.js
public/scheduler-unstable_mock.production.min.js
public/react.development.js public/react.development.js
public/react.production.min.js public/react.production.min.js
public/react-dom.development.js public/react-dom.development.js

View File

@ -18,7 +18,7 @@
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "start": "react-scripts start",
"prestart": "cp ../../build/node_modules/react/umd/react.development.js ../../build/node_modules/react-dom/umd/react-dom.development.js ../../build/node_modules/react/umd/react.production.min.js ../../build/node_modules/react-dom/umd/react-dom.production.min.js ../../build/node_modules/react-dom/umd/react-dom-server.browser.development.js ../../build/node_modules/react-dom/umd/react-dom-server.browser.production.min.js ../../build/node_modules/react-dom/umd/react-dom-test-utils.development.js ../../build/node_modules/react-dom/umd/react-dom-test-utils.production.min.js public/", "prestart": "cp ../../build/node_modules/scheduler/umd/scheduler-unstable_mock.development.js ../../build/node_modules/scheduler/umd/scheduler-unstable_mock.production.min.js ../../build/node_modules/react/umd/react.development.js ../../build/node_modules/react-dom/umd/react-dom.development.js ../../build/node_modules/react/umd/react.production.min.js ../../build/node_modules/react-dom/umd/react-dom.production.min.js ../../build/node_modules/react-dom/umd/react-dom-server.browser.development.js ../../build/node_modules/react-dom/umd/react-dom-server.browser.production.min.js ../../build/node_modules/react-dom/umd/react-dom-test-utils.development.js ../../build/node_modules/react-dom/umd/react-dom-test-utils.production.min.js public/",
"build": "react-scripts build && cp build/index.html build/200.html", "build": "react-scripts build && cp build/index.html build/200.html",
"test": "react-scripts test --env=jsdom", "test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject" "eject": "react-scripts eject"

View File

@ -7,7 +7,11 @@
this page tests whether act runs properly in a browser. this page tests whether act runs properly in a browser.
<br/> <br/>
your console should say "5" your console should say "5"
<script src='scheduler-unstable_mock.development.js'></script>
<script src='react.development.js'></script> <script src='react.development.js'></script>
<script type="text/javascript">
window.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler = window.SchedulerMock
</script>
<script src='react-dom.development.js'></script> <script src='react-dom.development.js'></script>
<script src='react-dom-test-utils.development.js'></script> <script src='react-dom-test-utils.development.js'></script>
<script> <script>

View File

@ -0,0 +1,28 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/
let ReactFeatureFlags;
let act;
describe('mocked scheduler', () => {
beforeEach(() => {
jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.warnAboutMissingMockScheduler = true;
jest.unmock('scheduler');
act = require('react-dom/test-utils').act;
});
it("should warn when the scheduler isn't mocked", () => {
expect(() => act(() => {})).toWarnDev(
[
'Starting from React v17, the "scheduler" module will need to be mocked',
],
{withoutStack: true},
);
});
});

View File

@ -11,6 +11,7 @@ let React;
let ReactDOM; let ReactDOM;
let ReactTestUtils; let ReactTestUtils;
let SchedulerTracing; let SchedulerTracing;
let Scheduler;
let act; let act;
let container; let container;
@ -25,175 +26,386 @@ function sleep(period) {
} }
describe('ReactTestUtils.act()', () => { describe('ReactTestUtils.act()', () => {
beforeEach(() => { // first we run all the tests with concurrent mode
jest.resetModules(); let concurrentRoot;
React = require('react'); function renderConcurrent(el, dom) {
ReactDOM = require('react-dom'); concurrentRoot = ReactDOM.unstable_createRoot(dom);
ReactTestUtils = require('react-dom/test-utils'); concurrentRoot.render(el);
SchedulerTracing = require('scheduler/tracing'); }
act = ReactTestUtils.act; function unmountConcurrent(_dom) {
container = document.createElement('div'); if (concurrentRoot !== null) {
document.body.appendChild(container); concurrentRoot.unmount();
}); concurrentRoot = null;
afterEach(() => { }
ReactDOM.unmountComponentAtNode(container); }
document.body.removeChild(container); runActTests('concurrent mode', renderConcurrent, unmountConcurrent);
});
describe('sync', () => { // and then in sync mode
it('can use act to flush effects', () => { function renderSync(el, dom) {
function App(props) { ReactDOM.render(el, dom);
React.useEffect(props.callback); }
return null; function unmountSync(dom) {
} ReactDOM.unmountComponentAtNode(dom);
}
runActTests('legacy sync mode', renderSync, unmountSync);
});
let calledLog = []; function runActTests(label, render, unmount) {
act(() => { describe(label, () => {
ReactDOM.render( beforeEach(() => {
<App jest.resetModules();
callback={() => { React = require('react');
calledLog.push(calledLog.length); ReactDOM = require('react-dom');
}} ReactTestUtils = require('react-dom/test-utils');
/>, SchedulerTracing = require('scheduler/tracing');
document.createElement('div'), Scheduler = require('scheduler');
); act = ReactTestUtils.act;
}); container = document.createElement('div');
document.body.appendChild(container);
expect(calledLog).toEqual([0]);
}); });
afterEach(() => {
unmount(container);
document.body.removeChild(container);
});
describe('sync', () => {
it('can use act to flush effects', () => {
function App() {
React.useEffect(() => {
Scheduler.yieldValue(100);
});
return null;
}
it('flushes effects on every call', () => { act(() => {
function App(props) { render(<App />, container);
let [ctr, setCtr] = React.useState(0);
React.useEffect(() => {
props.callback(ctr);
}); });
return (
<button id="button" onClick={() => setCtr(x => x + 1)}>
{ctr}
</button>
);
}
let calledCounter = 0; expect(Scheduler).toHaveYielded([100]);
act(() => {
ReactDOM.render(
<App
callback={val => {
calledCounter = val;
}}
/>,
container,
);
}); });
const button = document.getElementById('button');
function click() {
button.dispatchEvent(new MouseEvent('click', {bubbles: true}));
}
act(() => { it('flushes effects on every call', () => {
click(); function App() {
click(); let [ctr, setCtr] = React.useState(0);
click(); React.useEffect(() => {
Scheduler.yieldValue(ctr);
});
return (
<button id="button" onClick={() => setCtr(x => x + 1)}>
{ctr}
</button>
);
}
act(() => {
render(<App />, container);
});
expect(Scheduler).toHaveYielded([0]);
const button = container.querySelector('#button');
function click() {
button.dispatchEvent(new MouseEvent('click', {bubbles: true}));
}
act(() => {
click();
click();
click();
});
// it consolidates the 3 updates, then fires the effect
expect(Scheduler).toHaveYielded([3]);
act(click);
expect(Scheduler).toHaveYielded([4]);
act(click);
expect(Scheduler).toHaveYielded([5]);
expect(button.innerHTML).toBe('5');
}); });
expect(calledCounter).toBe(3);
act(click);
expect(calledCounter).toBe(4);
act(click);
expect(calledCounter).toBe(5);
expect(button.innerHTML).toBe('5');
});
it('should flush effects recursively', () => { it("should keep flushing effects until the're done", () => {
function App() { function App() {
let [ctr, setCtr] = React.useState(0); let [ctr, setCtr] = React.useState(0);
React.useEffect(() => { React.useEffect(() => {
if (ctr < 5) { if (ctr < 5) {
setCtr(x => x + 1); setCtr(x => x + 1);
}
});
return ctr;
}
act(() => {
render(<App />, container);
});
expect(container.innerHTML).toBe('5');
});
it('warns if a setState is called outside of act(...)', () => {
let setValue = null;
function App() {
let [value, _setValue] = React.useState(0);
setValue = _setValue;
return value;
}
act(() => {
render(<App />, container);
});
expect(() => setValue(1)).toWarnDev([
'An update to App inside a test was not wrapped in act(...).',
]);
});
describe('fake timers', () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it('lets a ticker update', () => {
function App() {
let [toggle, setToggle] = React.useState(0);
React.useEffect(() => {
let timeout = setTimeout(() => {
setToggle(1);
}, 200);
return () => clearTimeout(timeout);
}, []);
return toggle;
} }
});
return ctr;
}
act(() => { act(() => {
ReactDOM.render(<App />, container); render(<App />, container);
});
act(() => {
jest.runAllTimers();
});
expect(container.innerHTML).toBe('1');
});
it('can use the async version to catch microtasks', async () => {
function App() {
let [toggle, setToggle] = React.useState(0);
React.useEffect(() => {
// just like the previous test, except we
// use a promise and schedule the update
// after it resolves
sleep(200).then(() => setToggle(1));
}, []);
return toggle;
}
act(() => {
render(<App />, container);
});
await act(async () => {
jest.runAllTimers();
});
expect(container.innerHTML).toBe('1');
});
it('can handle cascading promises with fake timers', async () => {
// this component triggers an effect, that waits a tick,
// then sets state. repeats this 5 times.
function App() {
let [state, setState] = React.useState(0);
async function ticker() {
await null;
setState(x => x + 1);
}
React.useEffect(
() => {
ticker();
},
[Math.min(state, 4)],
);
return state;
}
await act(async () => {
render(<App />, container);
});
// all 5 ticks present and accounted for
expect(container.innerHTML).toBe('5');
});
it('flushes immediate re-renders with act', () => {
function App() {
let [ctr, setCtr] = React.useState(0);
React.useEffect(() => {
if (ctr === 0) {
setCtr(1);
}
const timeout = setTimeout(() => setCtr(2), 1000);
return () => clearTimeout(timeout);
});
return ctr;
}
act(() => {
render(<App />, container);
// Since effects haven't been flushed yet, this does not advance the timer
jest.runAllTimers();
});
expect(container.innerHTML).toBe('1');
act(() => {
jest.runAllTimers();
});
expect(container.innerHTML).toBe('2');
});
}); });
expect(container.innerHTML).toBe('5'); it('warns if you return a value inside act', () => {
}); expect(() => act(() => null)).toWarnDev(
[
it('detects setState being called outside of act(...)', () => { 'The callback passed to act(...) function must return undefined, or a Promise.',
let setValue = null; ],
function App() { {withoutStack: true},
let [value, _setValue] = React.useState(0); );
setValue = _setValue; expect(() => act(() => 123)).toWarnDev(
return ( [
<button id="button" onClick={() => setValue(2)}> 'The callback passed to act(...) function must return undefined, or a Promise.',
{value} ],
</button> {withoutStack: true},
);
});
it('warns if you try to await a sync .act call', () => {
expect(() => act(() => {}).then(() => {})).toWarnDev(
[
'Do not await the result of calling act(...) with sync logic, it is not a Promise.',
],
{withoutStack: true},
); );
}
let button;
act(() => {
ReactDOM.render(<App />, container);
button = container.querySelector('#button');
button.dispatchEvent(new MouseEvent('click', {bubbles: true}));
}); });
expect(button.innerHTML).toBe('2');
expect(() => setValue(1)).toWarnDev([
'An update to App inside a test was not wrapped in act(...).',
]);
}); });
describe('fake timers', () => { describe('asynchronous tests', () => {
beforeEach(() => { it('can handle timers', async () => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it('lets a ticker update', () => {
function App() { function App() {
let [toggle, setToggle] = React.useState(0); let [ctr, setCtr] = React.useState(0);
function doSomething() {
setTimeout(() => {
setCtr(1);
}, 50);
}
React.useEffect(() => { React.useEffect(() => {
let timeout = setTimeout(() => { doSomething();
setToggle(1);
}, 200);
return () => clearTimeout(timeout);
}, []); }, []);
return toggle; return ctr;
} }
act(() => { act(() => {
ReactDOM.render(<App />, container); render(<App />, container);
});
act(() => {
jest.runAllTimers();
});
expect(container.innerHTML).toBe('1');
});
it('can use the async version to catch microtasks', async () => {
function App() {
let [toggle, setToggle] = React.useState(0);
React.useEffect(() => {
// just like the previous test, except we
// use a promise and schedule the update
// after it resolves
sleep(200).then(() => setToggle(1));
}, []);
return toggle;
}
act(() => {
ReactDOM.render(<App />, container);
}); });
await act(async () => { await act(async () => {
jest.runAllTimers(); await sleep(100);
}); });
expect(container.innerHTML).toBe('1'); expect(container.innerHTML).toBe('1');
}); });
it('can handle cascading promises with fake timers', async () => {
it('can handle async/await', async () => {
function App() {
let [ctr, setCtr] = React.useState(0);
async function someAsyncFunction() {
// queue a bunch of promises to be sure they all flush
await null;
await null;
await null;
setCtr(1);
}
React.useEffect(() => {
someAsyncFunction();
}, []);
return ctr;
}
await act(async () => {
act(() => {
render(<App />, container);
});
// pending promises will close before this ends
});
expect(container.innerHTML).toEqual('1');
});
it('warns if you do not await an act call', async () => {
spyOnDevAndProd(console, 'error');
act(async () => {});
// it's annoying that we have to wait a tick before this warning comes in
await sleep(0);
if (__DEV__) {
expect(console.error.calls.count()).toEqual(1);
expect(console.error.calls.argsFor(0)[0]).toMatch(
'You called act(async () => ...) without await.',
);
}
});
it('warns if you try to interleave multiple act calls', async () => {
spyOnDevAndProd(console, 'error');
// let's try to cheat and spin off a 'thread' with an act call
(async () => {
await act(async () => {
await sleep(50);
});
})();
await act(async () => {
await sleep(100);
});
await sleep(150);
if (__DEV__) {
expect(console.error).toHaveBeenCalledTimes(1);
}
});
it('commits and effects are guaranteed to be flushed', async () => {
function App() {
let [state, setState] = React.useState(0);
async function something() {
await null;
setState(1);
}
React.useEffect(() => {
something();
}, []);
React.useEffect(() => {
Scheduler.yieldValue(state);
});
return state;
}
await act(async () => {
act(() => {
render(<App />, container);
});
expect(container.innerHTML).toBe('0');
expect(Scheduler).toHaveYielded([0]);
});
// this may seem odd, but it matches user behaviour -
// a flash of "0" followed by "1"
expect(container.innerHTML).toBe('1');
expect(Scheduler).toHaveYielded([1]);
});
it('propagates errors', async () => {
let err;
try {
await act(async () => {
await sleep(100);
throw new Error('some error');
});
} catch (_err) {
err = _err;
} finally {
expect(err instanceof Error).toBe(true);
expect(err.message).toBe('some error');
}
});
it('can handle cascading promises', async () => {
// this component triggers an effect, that waits a tick, // this component triggers an effect, that waits a tick,
// then sets state. repeats this 5 times. // then sets state. repeats this 5 times.
function App() { function App() {
@ -204,303 +416,100 @@ describe('ReactTestUtils.act()', () => {
} }
React.useEffect( React.useEffect(
() => { () => {
Scheduler.yieldValue(state);
ticker(); ticker();
}, },
[Math.min(state, 4)], [Math.min(state, 4)],
); );
return state; return state;
} }
const el = document.createElement('div');
await act(async () => {
ReactDOM.render(<App />, el);
});
await act(async () => {
render(<App />, container);
});
// all 5 ticks present and accounted for // all 5 ticks present and accounted for
expect(el.innerHTML).toBe('5'); expect(Scheduler).toHaveYielded([0, 1, 2, 3, 4]);
expect(container.innerHTML).toBe('5');
}); });
it('flushes immediate re-renders with act', () => { });
function App() {
let [ctr, setCtr] = React.useState(0); describe('interaction tracing', () => {
React.useEffect(() => { if (__DEV__) {
if (ctr === 0) { it('should correctly trace interactions for sync roots', () => {
setCtr(1); let expectedInteraction;
}
const timeout = setTimeout(() => setCtr(2), 1000); const Component = jest.fn(() => {
return () => clearTimeout(timeout); expect(expectedInteraction).toBeDefined();
const interactions = SchedulerTracing.unstable_getCurrent();
expect(interactions.size).toBe(1);
expect(interactions).toContain(expectedInteraction);
return null;
}); });
return ctr;
}
act(() => { act(() => {
ReactDOM.render(<App />, container); SchedulerTracing.unstable_trace(
// Since the effects won't be flushed yet, this does not advance the timer 'mount traced inside act',
jest.runAllTimers(); performance.now(),
}); () => {
const interactions = SchedulerTracing.unstable_getCurrent();
expect(interactions.size).toBe(1);
expectedInteraction = Array.from(interactions)[0];
expect(container.innerHTML).toBe('1'); render(<Component />, container);
},
);
});
act(() => { act(() => {
jest.runAllTimers(); SchedulerTracing.unstable_trace(
}); 'update traced inside act',
performance.now(),
() => {
const interactions = SchedulerTracing.unstable_getCurrent();
expect(interactions.size).toBe(1);
expectedInteraction = Array.from(interactions)[0];
expect(container.innerHTML).toBe('2'); render(<Component />, container);
}); },
}); );
});
it('warns if you return a value inside act', () => { const secondContainer = document.createElement('div');
expect(() => act(() => null)).toWarnDev(
[
'The callback passed to act(...) function must return undefined, or a Promise.',
],
{withoutStack: true},
);
expect(() => act(() => 123)).toWarnDev(
[
'The callback passed to act(...) function must return undefined, or a Promise.',
],
{withoutStack: true},
);
});
it('warns if you try to await an .act call', () => {
expect(() => act(() => {}).then(() => {})).toWarnDev(
[
'Do not await the result of calling act(...) with sync logic, it is not a Promise.',
],
{withoutStack: true},
);
});
});
describe('asynchronous tests', () => {
it('can handle timers', async () => {
function App() {
let [ctr, setCtr] = React.useState(0);
function doSomething() {
setTimeout(() => {
setCtr(1);
}, 50);
}
React.useEffect(() => {
doSomething();
}, []);
return ctr;
}
const el = document.createElement('div');
await act(async () => {
act(() => {
ReactDOM.render(<App />, el);
});
await sleep(100);
expect(el.innerHTML).toBe('1');
});
});
it('can handle async/await', async () => {
function App() {
let [ctr, setCtr] = React.useState(0);
async function someAsyncFunction() {
// queue a bunch of promises to be sure they all flush
await null;
await null;
await null;
setCtr(1);
}
React.useEffect(() => {
someAsyncFunction();
}, []);
return ctr;
}
const el = document.createElement('div');
await act(async () => {
act(() => {
ReactDOM.render(<App />, el);
});
// pending promises will close before this ends
});
expect(el.innerHTML).toEqual('1');
});
it('warns if you do not await an act call', async () => {
spyOnDevAndProd(console, 'error');
act(async () => {});
// it's annoying that we have to wait a tick before this warning comes in
await sleep(0);
if (__DEV__) {
expect(console.error.calls.count()).toEqual(1);
expect(console.error.calls.argsFor(0)[0]).toMatch(
'You called act(async () => ...) without await.',
);
}
});
it('warns if you try to interleave multiple act calls', async () => {
spyOnDevAndProd(console, 'error');
// let's try to cheat and spin off a 'thread' with an act call
(async () => {
await act(async () => {
await sleep(50);
});
})();
await act(async () => {
await sleep(100);
});
await sleep(150);
if (__DEV__) {
expect(console.error).toHaveBeenCalledTimes(1);
}
});
it('commits and effects are guaranteed to be flushed', async () => {
function App(props) {
let [state, setState] = React.useState(0);
async function something() {
await null;
setState(1);
}
React.useEffect(() => {
something();
}, []);
React.useEffect(() => {
props.callback();
});
return state;
}
let ctr = 0;
const div = document.createElement('div');
await act(async () => {
act(() => {
ReactDOM.render(<App callback={() => ctr++} />, div);
});
expect(div.innerHTML).toBe('0');
expect(ctr).toBe(1);
});
// this may seem odd, but it matches user behaviour -
// a flash of "0" followed by "1"
expect(div.innerHTML).toBe('1');
expect(ctr).toBe(2);
});
it('propagates errors', async () => {
let err;
try {
await act(async () => {
throw new Error('some error');
});
} catch (_err) {
err = _err;
} finally {
expect(err instanceof Error).toBe(true);
expect(err.message).toBe('some error');
}
});
it('can handle cascading promises', async () => {
// this component triggers an effect, that waits a tick,
// then sets state. repeats this 5 times.
function App() {
let [state, setState] = React.useState(0);
async function ticker() {
await null;
setState(x => x + 1);
}
React.useEffect(
() => {
ticker();
},
[Math.min(state, 4)],
);
return state;
}
const el = document.createElement('div');
await act(async () => {
ReactDOM.render(<App />, el);
});
// all 5 ticks present and accounted for
expect(el.innerHTML).toBe('5');
});
});
describe('interaction tracing', () => {
if (__DEV__) {
it('should correctly trace interactions for sync roots', () => {
let expectedInteraction;
const Component = jest.fn(() => {
expect(expectedInteraction).toBeDefined();
const interactions = SchedulerTracing.unstable_getCurrent();
expect(interactions.size).toBe(1);
expect(interactions).toContain(expectedInteraction);
return null;
});
act(() => {
SchedulerTracing.unstable_trace( SchedulerTracing.unstable_trace(
'mount traced inside act', 'mount traced outside act',
performance.now(), performance.now(),
() => { () => {
const interactions = SchedulerTracing.unstable_getCurrent(); act(() => {
expect(interactions.size).toBe(1); const interactions = SchedulerTracing.unstable_getCurrent();
expectedInteraction = Array.from(interactions)[0]; expect(interactions.size).toBe(1);
expectedInteraction = Array.from(interactions)[0];
ReactDOM.render(<Component />, container); render(<Component />, secondContainer);
});
}, },
); );
});
act(() => {
SchedulerTracing.unstable_trace( SchedulerTracing.unstable_trace(
'update traced inside act', 'update traced outside act',
performance.now(), performance.now(),
() => { () => {
const interactions = SchedulerTracing.unstable_getCurrent(); act(() => {
expect(interactions.size).toBe(1); const interactions = SchedulerTracing.unstable_getCurrent();
expectedInteraction = Array.from(interactions)[0]; expect(interactions.size).toBe(1);
expectedInteraction = Array.from(interactions)[0];
ReactDOM.render(<Component />, container); render(<Component />, secondContainer);
});
}, },
); );
expect(Component).toHaveBeenCalledTimes(4);
unmount(secondContainer);
}); });
}
const secondContainer = document.createElement('div'); });
SchedulerTracing.unstable_trace(
'mount traced outside act',
performance.now(),
() => {
act(() => {
const interactions = SchedulerTracing.unstable_getCurrent();
expect(interactions.size).toBe(1);
expectedInteraction = Array.from(interactions)[0];
ReactDOM.render(<Component />, secondContainer);
});
},
);
SchedulerTracing.unstable_trace(
'update traced outside act',
performance.now(),
() => {
act(() => {
const interactions = SchedulerTracing.unstable_getCurrent();
expect(interactions.size).toBe(1);
expectedInteraction = Array.from(interactions)[0];
ReactDOM.render(<Component />, secondContainer);
});
},
);
expect(Component).toHaveBeenCalledTimes(4);
});
}
}); });
}); }

View File

@ -12,7 +12,9 @@ import type {Thenable} from 'react-reconciler/src/ReactFiberScheduler';
import warningWithoutStack from 'shared/warningWithoutStack'; import warningWithoutStack from 'shared/warningWithoutStack';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import ReactSharedInternals from 'shared/ReactSharedInternals'; import ReactSharedInternals from 'shared/ReactSharedInternals';
import {warnAboutMissingMockScheduler} from 'shared/ReactFeatureFlags';
import enqueueTask from 'shared/enqueueTask'; import enqueueTask from 'shared/enqueueTask';
import * as Scheduler from 'scheduler';
// Keep in sync with ReactDOMUnstableNativeDependencies.js // Keep in sync with ReactDOMUnstableNativeDependencies.js
// ReactDOM.js, and ReactTestUtils.js: // ReactDOM.js, and ReactTestUtils.js:
@ -40,16 +42,33 @@ const {ReactShouldWarnActingUpdates} = ReactSharedInternals;
// this implementation should be exactly the same in // this implementation should be exactly the same in
// ReactTestUtilsAct.js, ReactTestRendererAct.js, createReactNoop.js // ReactTestUtilsAct.js, ReactTestRendererAct.js, createReactNoop.js
// we track the 'depth' of the act() calls with this counter, let hasWarnedAboutMissingMockScheduler = false;
// so we can tell if any async act() calls try to run in parallel. const flushWork =
let actingUpdatesScopeDepth = 0; Scheduler.unstable_flushWithoutYielding ||
function() {
if (warnAboutMissingMockScheduler === true) {
if (hasWarnedAboutMissingMockScheduler === false) {
warningWithoutStack(
null,
'Starting from React v17, the "scheduler" module will need to be mocked ' +
'to guarantee consistent behaviour across tests and browsers. To fix this, add the following ' +
"to the top of your tests, or in your framework's global config file -\n\n" +
'As an example, for jest - \n' +
"jest.mock('scheduler', () => require.requireActual('scheduler/unstable_mock'));\n\n" +
'For more info, visit https://fb.me/react-mock-scheduler',
);
hasWarnedAboutMissingMockScheduler = true;
}
}
while (flushPassiveEffects()) {}
};
function flushEffectsAndMicroTasks(onDone: (err: ?Error) => void) { function flushWorkAndMicroTasks(onDone: (err: ?Error) => void) {
try { try {
flushPassiveEffects(); flushWork();
enqueueTask(() => { enqueueTask(() => {
if (flushPassiveEffects()) { if (flushWork()) {
flushEffectsAndMicroTasks(onDone); flushWorkAndMicroTasks(onDone);
} else { } else {
onDone(); onDone();
} }
@ -59,6 +78,11 @@ function flushEffectsAndMicroTasks(onDone: (err: ?Error) => void) {
} }
} }
// we track the 'depth' of the act() calls with this counter,
// so we can tell if any async act() calls try to run in parallel.
let actingUpdatesScopeDepth = 0;
function act(callback: () => Thenable) { function act(callback: () => Thenable) {
let previousActingUpdatesScopeDepth; let previousActingUpdatesScopeDepth;
if (__DEV__) { if (__DEV__) {
@ -119,7 +143,7 @@ function act(callback: () => Thenable) {
called = true; called = true;
result.then( result.then(
() => { () => {
flushEffectsAndMicroTasks((err: ?Error) => { flushWorkAndMicroTasks((err: ?Error) => {
onDone(); onDone();
if (err) { if (err) {
reject(err); reject(err);
@ -147,7 +171,7 @@ function act(callback: () => Thenable) {
// flush effects until none remain, and cleanup // flush effects until none remain, and cleanup
try { try {
while (flushPassiveEffects()) {} flushWork();
onDone(); onDone();
} catch (err) { } catch (err) {
onDone(); onDone();

View File

@ -32,7 +32,10 @@ import warning from 'shared/warning';
import enqueueTask from 'shared/enqueueTask'; import enqueueTask from 'shared/enqueueTask';
import ReactSharedInternals from 'shared/ReactSharedInternals'; import ReactSharedInternals from 'shared/ReactSharedInternals';
import warningWithoutStack from 'shared/warningWithoutStack'; import warningWithoutStack from 'shared/warningWithoutStack';
import {enableEventAPI} from 'shared/ReactFeatureFlags'; import {
warnAboutMissingMockScheduler,
enableEventAPI,
} from 'shared/ReactFeatureFlags';
import {ConcurrentRoot, BatchedRoot, LegacyRoot} from 'shared/ReactRootTags'; import {ConcurrentRoot, BatchedRoot, LegacyRoot} from 'shared/ReactRootTags';
type EventTargetChildElement = { type EventTargetChildElement = {
@ -652,14 +655,33 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
// this act() implementation should be exactly the same in // this act() implementation should be exactly the same in
// ReactTestUtilsAct.js, ReactTestRendererAct.js, createReactNoop.js // ReactTestUtilsAct.js, ReactTestRendererAct.js, createReactNoop.js
let actingUpdatesScopeDepth = 0; let hasWarnedAboutMissingMockScheduler = false;
const flushWork =
Scheduler.unstable_flushWithoutYielding ||
function() {
if (warnAboutMissingMockScheduler === true) {
if (hasWarnedAboutMissingMockScheduler === false) {
warningWithoutStack(
null,
'Starting from React v17, the "scheduler" module will need to be mocked ' +
'to guarantee consistent behaviour across tests and browsers. To fix this, add the following ' +
"to the top of your tests, or in your framework's global config file -\n\n" +
'As an example, for jest - \n' +
"jest.mock('scheduler', () => require.requireActual('scheduler/unstable_mock'));\n\n" +
'For more info, visit https://fb.me/react-mock-scheduler',
);
hasWarnedAboutMissingMockScheduler = true;
}
}
while (flushPassiveEffects()) {}
};
function flushEffectsAndMicroTasks(onDone: (err: ?Error) => void) { function flushWorkAndMicroTasks(onDone: (err: ?Error) => void) {
try { try {
flushPassiveEffects(); flushWork();
enqueueTask(() => { enqueueTask(() => {
if (flushPassiveEffects()) { if (flushWork()) {
flushEffectsAndMicroTasks(onDone); flushWorkAndMicroTasks(onDone);
} else { } else {
onDone(); onDone();
} }
@ -669,6 +691,11 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
} }
} }
// we track the 'depth' of the act() calls with this counter,
// so we can tell if any async act() calls try to run in parallel.
let actingUpdatesScopeDepth = 0;
function act(callback: () => Thenable) { function act(callback: () => Thenable) {
let previousActingUpdatesScopeDepth; let previousActingUpdatesScopeDepth;
if (__DEV__) { if (__DEV__) {
@ -729,7 +756,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
called = true; called = true;
result.then( result.then(
() => { () => {
flushEffectsAndMicroTasks((err: ?Error) => { flushWorkAndMicroTasks((err: ?Error) => {
onDone(); onDone();
if (err) { if (err) {
reject(err); reject(err);
@ -757,7 +784,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
// flush effects until none remain, and cleanup // flush effects until none remain, and cleanup
try { try {
while (flushPassiveEffects()) {} flushWork();
onDone(); onDone();
} catch (err) { } catch (err) {
onDone(); onDone();

View File

@ -1,7 +1,6 @@
let React; let React;
let ReactFeatureFlags; let ReactFeatureFlags;
let ReactNoop; let ReactNoop;
let act;
let Scheduler; let Scheduler;
let ReactCache; let ReactCache;
let Suspense; let Suspense;
@ -15,7 +14,6 @@ describe('ReactBatchedMode', () => {
ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false; ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
React = require('react'); React = require('react');
ReactNoop = require('react-noop-renderer'); ReactNoop = require('react-noop-renderer');
act = ReactNoop.act;
Scheduler = require('scheduler'); Scheduler = require('scheduler');
ReactCache = require('react-cache'); ReactCache = require('react-cache');
Suspense = React.Suspense; Suspense = React.Suspense;
@ -146,10 +144,10 @@ describe('ReactBatchedMode', () => {
expect(root).toMatchRenderedOutput('A0B0'); expect(root).toMatchRenderedOutput('A0B0');
// Schedule a batched update to the first sibling // Schedule a batched update to the first sibling
act(() => foo1.current.setStep(1)); ReactNoop.batchedUpdates(() => foo1.current.setStep(1));
// Before it flushes, update the second sibling inside flushSync // Before it flushes, update the second sibling inside flushSync
act(() => ReactNoop.batchedUpdates(() =>
ReactNoop.flushSync(() => { ReactNoop.flushSync(() => {
foo2.current.setStep(1); foo2.current.setStep(1);
}), }),

View File

@ -95,7 +95,7 @@ describe('ReactHooks', () => {
setCounter2(1); setCounter2(1);
}); });
expect(Scheduler).toFlushAndYield([ expect(Scheduler).toHaveYielded([
'Parent: 1, 1', 'Parent: 1, 1',
'Child: 1, 1', 'Child: 1, 1',
'Effect: 1, 1', 'Effect: 1, 1',
@ -103,7 +103,7 @@ describe('ReactHooks', () => {
// Update that bails out. // Update that bails out.
act(() => setCounter1(1)); act(() => setCounter1(1));
expect(Scheduler).toFlushAndYield(['Parent: 1, 1']); expect(Scheduler).toHaveYielded(['Parent: 1, 1']);
// This time, one of the state updates but the other one doesn't. So we // This time, one of the state updates but the other one doesn't. So we
// can't bail out. // can't bail out.
@ -112,7 +112,7 @@ describe('ReactHooks', () => {
setCounter2(2); setCounter2(2);
}); });
expect(Scheduler).toFlushAndYield([ expect(Scheduler).toHaveYielded([
'Parent: 1, 2', 'Parent: 1, 2',
'Child: 1, 2', 'Child: 1, 2',
'Effect: 1, 2', 'Effect: 1, 2',
@ -130,14 +130,15 @@ describe('ReactHooks', () => {
// Because the final values are the same as the current values, the // Because the final values are the same as the current values, the
// component bails out. // component bails out.
expect(Scheduler).toFlushAndYield(['Parent: 1, 2']); expect(Scheduler).toHaveYielded(['Parent: 1, 2']);
// prepare to check SameValue // prepare to check SameValue
act(() => { act(() => {
setCounter1(0 / -1); setCounter1(0 / -1);
setCounter2(NaN); setCounter2(NaN);
}); });
expect(Scheduler).toFlushAndYield([
expect(Scheduler).toHaveYielded([
'Parent: 0, NaN', 'Parent: 0, NaN',
'Child: 0, NaN', 'Child: 0, NaN',
'Effect: 0, NaN', 'Effect: 0, NaN',
@ -151,13 +152,13 @@ describe('ReactHooks', () => {
setCounter2(NaN); setCounter2(NaN);
}); });
expect(Scheduler).toFlushAndYield(['Parent: 0, NaN']); expect(Scheduler).toHaveYielded(['Parent: 0, NaN']);
// check if changing negative 0 to positive 0 does not bail out // check if changing negative 0 to positive 0 does not bail out
act(() => { act(() => {
setCounter1(0); setCounter1(0);
}); });
expect(Scheduler).toFlushAndYield([ expect(Scheduler).toHaveYielded([
'Parent: 0, NaN', 'Parent: 0, NaN',
'Child: 0, NaN', 'Child: 0, NaN',
'Effect: 0, NaN', 'Effect: 0, NaN',
@ -201,14 +202,14 @@ describe('ReactHooks', () => {
setCounter2(1); setCounter2(1);
}); });
expect(Scheduler).toFlushAndYield([ expect(Scheduler).toHaveYielded([
'Parent: 1, 1 (light)', 'Parent: 1, 1 (light)',
'Child: 1, 1 (light)', 'Child: 1, 1 (light)',
]); ]);
// Update that bails out. // Update that bails out.
act(() => setCounter1(1)); act(() => setCounter1(1));
expect(Scheduler).toFlushAndYield(['Parent: 1, 1 (light)']); expect(Scheduler).toHaveYielded(['Parent: 1, 1 (light)']);
// This time, one of the state updates but the other one doesn't. So we // This time, one of the state updates but the other one doesn't. So we
// can't bail out. // can't bail out.
@ -217,7 +218,7 @@ describe('ReactHooks', () => {
setCounter2(2); setCounter2(2);
}); });
expect(Scheduler).toFlushAndYield([ expect(Scheduler).toHaveYielded([
'Parent: 1, 2 (light)', 'Parent: 1, 2 (light)',
'Child: 1, 2 (light)', 'Child: 1, 2 (light)',
]); ]);
@ -227,10 +228,10 @@ describe('ReactHooks', () => {
act(() => { act(() => {
setCounter1(1); setCounter1(1);
setCounter2(2); setCounter2(2);
root.update(<Parent theme="dark" />);
}); });
root.update(<Parent theme="dark" />); expect(Scheduler).toHaveYielded([
expect(Scheduler).toFlushAndYield([
'Parent: 1, 2 (dark)', 'Parent: 1, 2 (dark)',
'Child: 1, 2 (dark)', 'Child: 1, 2 (dark)',
]); ]);
@ -239,10 +240,10 @@ describe('ReactHooks', () => {
act(() => { act(() => {
setCounter1(1); setCounter1(1);
setCounter2(2); setCounter2(2);
root.update(<Parent theme="dark" />);
}); });
root.update(<Parent theme="dark" />); expect(Scheduler).toHaveYielded(['Parent: 1, 2 (dark)']);
expect(Scheduler).toFlushAndYield(['Parent: 1, 2 (dark)']);
}); });
it('warns about setState second argument', () => { it('warns about setState second argument', () => {
@ -275,7 +276,7 @@ describe('ReactHooks', () => {
'declare it in the component body with useEffect().', 'declare it in the component body with useEffect().',
{withoutStack: true}, {withoutStack: true},
); );
expect(Scheduler).toFlushAndYield(['Count: 1']); expect(Scheduler).toHaveYielded(['Count: 1']);
expect(root).toMatchRenderedOutput('1'); expect(root).toMatchRenderedOutput('1');
}); });
@ -309,7 +310,7 @@ describe('ReactHooks', () => {
'declare it in the component body with useEffect().', 'declare it in the component body with useEffect().',
{withoutStack: true}, {withoutStack: true},
); );
expect(Scheduler).toFlushAndYield(['Count: 1']); expect(Scheduler).toHaveYielded(['Count: 1']);
expect(root).toMatchRenderedOutput('1'); expect(root).toMatchRenderedOutput('1');
}); });
@ -347,14 +348,16 @@ describe('ReactHooks', () => {
}); });
return <Child text={text} />; return <Child text={text} />;
} }
const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true}); const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true});
root.update( act(() => {
<ThemeProvider> root.update(
<Parent /> <ThemeProvider>
</ThemeProvider>, <Parent />
); </ThemeProvider>,
expect(Scheduler).toFlushAndYield([ );
});
expect(Scheduler).toHaveYielded([
'Theme: light', 'Theme: light',
'Parent: 0 (light)', 'Parent: 0 (light)',
'Child: 0 (light)', 'Child: 0 (light)',
@ -370,7 +373,7 @@ describe('ReactHooks', () => {
// Normal update // Normal update
act(() => setCounter(1)); act(() => setCounter(1));
expect(Scheduler).toFlushAndYield([ expect(Scheduler).toHaveYielded([
'Parent: 1 (light)', 'Parent: 1 (light)',
'Child: 1 (light)', 'Child: 1 (light)',
'Effect: 1 (light)', 'Effect: 1 (light)',
@ -379,7 +382,7 @@ describe('ReactHooks', () => {
// Update that doesn't change state, so it bails out // Update that doesn't change state, so it bails out
act(() => setCounter(1)); act(() => setCounter(1));
expect(Scheduler).toFlushAndYield(['Parent: 1 (light)']); expect(Scheduler).toHaveYielded(['Parent: 1 (light)']);
expect(root).toMatchRenderedOutput('1 (light)'); expect(root).toMatchRenderedOutput('1 (light)');
// Update that doesn't change state, but the context changes, too, so it // Update that doesn't change state, but the context changes, too, so it
@ -389,7 +392,7 @@ describe('ReactHooks', () => {
setTheme('dark'); setTheme('dark');
}); });
expect(Scheduler).toFlushAndYield([ expect(Scheduler).toHaveYielded([
'Theme: dark', 'Theme: dark',
'Parent: 1 (dark)', 'Parent: 1 (dark)',
'Child: 1 (dark)', 'Child: 1 (dark)',
@ -424,7 +427,7 @@ describe('ReactHooks', () => {
// Normal update // Normal update
act(() => setCounter(1)); act(() => setCounter(1));
expect(Scheduler).toFlushAndYield(['Parent: 1', 'Child: 1', 'Effect: 1']); expect(Scheduler).toHaveYielded(['Parent: 1', 'Child: 1', 'Effect: 1']);
expect(root).toMatchRenderedOutput('1'); expect(root).toMatchRenderedOutput('1');
// Update to the same state. React doesn't know if the queue is empty // Update to the same state. React doesn't know if the queue is empty
@ -432,7 +435,7 @@ describe('ReactHooks', () => {
// enter the render phase before we can bail out. But we bail out before // enter the render phase before we can bail out. But we bail out before
// rendering the child, and we don't fire any effects. // rendering the child, and we don't fire any effects.
act(() => setCounter(1)); act(() => setCounter(1));
expect(Scheduler).toFlushAndYield(['Parent: 1']); expect(Scheduler).toHaveYielded(['Parent: 1']);
expect(root).toMatchRenderedOutput('1'); expect(root).toMatchRenderedOutput('1');
// Update to the same state again. This times, neither fiber has pending // Update to the same state again. This times, neither fiber has pending
@ -443,14 +446,14 @@ describe('ReactHooks', () => {
// This changes the state to something different so it renders normally. // This changes the state to something different so it renders normally.
act(() => setCounter(2)); act(() => setCounter(2));
expect(Scheduler).toFlushAndYield(['Parent: 2', 'Child: 2', 'Effect: 2']); expect(Scheduler).toHaveYielded(['Parent: 2', 'Child: 2', 'Effect: 2']);
expect(root).toMatchRenderedOutput('2'); expect(root).toMatchRenderedOutput('2');
// prepare to check SameValue // prepare to check SameValue
act(() => { act(() => {
setCounter(0); setCounter(0);
}); });
expect(Scheduler).toFlushAndYield(['Parent: 0', 'Child: 0', 'Effect: 0']); expect(Scheduler).toHaveYielded(['Parent: 0', 'Child: 0', 'Effect: 0']);
expect(root).toMatchRenderedOutput('0'); expect(root).toMatchRenderedOutput('0');
// Update to the same state for the first time to flush the queue // Update to the same state for the first time to flush the queue
@ -458,7 +461,7 @@ describe('ReactHooks', () => {
setCounter(0); setCounter(0);
}); });
expect(Scheduler).toFlushAndYield(['Parent: 0']); expect(Scheduler).toHaveYielded(['Parent: 0']);
expect(root).toMatchRenderedOutput('0'); expect(root).toMatchRenderedOutput('0');
// Update again to the same state. Should bail out. // Update again to the same state. Should bail out.
@ -472,7 +475,7 @@ describe('ReactHooks', () => {
act(() => { act(() => {
setCounter(0 / -1); setCounter(0 / -1);
}); });
expect(Scheduler).toFlushAndYield(['Parent: 0', 'Child: 0', 'Effect: 0']); expect(Scheduler).toHaveYielded(['Parent: 0', 'Child: 0', 'Effect: 0']);
expect(root).toMatchRenderedOutput('0'); expect(root).toMatchRenderedOutput('0');
}); });
@ -503,7 +506,7 @@ describe('ReactHooks', () => {
return value; return value;
}); });
}; };
act(() => { ReactTestRenderer.unstable_batchedUpdates(() => {
update(0); update(0);
update(0); update(0);
update(0); update(0);
@ -564,7 +567,7 @@ describe('ReactHooks', () => {
}; };
// Update at normal priority // Update at normal priority
act(() => update(n => n * 100)); ReactTestRenderer.unstable_batchedUpdates(() => update(n => n * 100));
// The new state is eagerly computed. // The new state is eagerly computed.
expect(Scheduler).toHaveYielded(['Compute state (1 -> 100)']); expect(Scheduler).toHaveYielded(['Compute state (1 -> 100)']);

View File

@ -80,7 +80,7 @@ describe('ReactHooksWithNoopRenderer', () => {
expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
// Schedule some updates // Schedule some updates
act(() => { ReactNoop.batchedUpdates(() => {
counter.current.updateCount(1); counter.current.updateCount(1);
counter.current.updateCount(count => count + 10); counter.current.updateCount(count => count + 10);
}); });
@ -189,11 +189,11 @@ describe('ReactHooksWithNoopRenderer', () => {
expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
act(() => counter.current.updateCount(1)); act(() => counter.current.updateCount(1));
expect(Scheduler).toFlushAndYield(['Count: 1']); expect(Scheduler).toHaveYielded(['Count: 1']);
expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
act(() => counter.current.updateCount(count => count + 10)); act(() => counter.current.updateCount(count => count + 10));
expect(Scheduler).toFlushAndYield(['Count: 11']); expect(Scheduler).toHaveYielded(['Count: 11']);
expect(ReactNoop.getChildren()).toEqual([span('Count: 11')]); expect(ReactNoop.getChildren()).toEqual([span('Count: 11')]);
}); });
@ -213,7 +213,7 @@ describe('ReactHooksWithNoopRenderer', () => {
expect(ReactNoop.getChildren()).toEqual([span('Count: 42')]); expect(ReactNoop.getChildren()).toEqual([span('Count: 42')]);
act(() => counter.current.updateCount(7)); act(() => counter.current.updateCount(7));
expect(Scheduler).toFlushAndYield(['Count: 7']); expect(Scheduler).toHaveYielded(['Count: 7']);
expect(ReactNoop.getChildren()).toEqual([span('Count: 7')]); expect(ReactNoop.getChildren()).toEqual([span('Count: 7')]);
}); });
@ -231,10 +231,10 @@ describe('ReactHooksWithNoopRenderer', () => {
expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
act(() => counter.current.updateCount(7)); act(() => counter.current.updateCount(7));
expect(Scheduler).toFlushAndYield(['Count: 7']); expect(Scheduler).toHaveYielded(['Count: 7']);
act(() => counter.current.updateLabel('Total')); act(() => counter.current.updateLabel('Total'));
expect(Scheduler).toFlushAndYield(['Total: 7']); expect(Scheduler).toHaveYielded(['Total: 7']);
}); });
it('returns the same updater function every time', () => { it('returns the same updater function every time', () => {
@ -249,11 +249,11 @@ describe('ReactHooksWithNoopRenderer', () => {
expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
act(() => updaters[0](1)); act(() => updaters[0](1));
expect(Scheduler).toFlushAndYield(['Count: 1']); expect(Scheduler).toHaveYielded(['Count: 1']);
expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
act(() => updaters[0](count => count + 10)); act(() => updaters[0](count => count + 10));
expect(Scheduler).toFlushAndYield(['Count: 11']); expect(Scheduler).toHaveYielded(['Count: 11']);
expect(ReactNoop.getChildren()).toEqual([span('Count: 11')]); expect(ReactNoop.getChildren()).toEqual([span('Count: 11')]);
expect(updaters).toEqual([updaters[0], updaters[0], updaters[0]]); expect(updaters).toEqual([updaters[0], updaters[0], updaters[0]]);
@ -298,7 +298,7 @@ describe('ReactHooksWithNoopRenderer', () => {
expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
act(() => _updateCount(1)); act(() => _updateCount(1));
expect(Scheduler).toFlushAndYield(['Count: 1']); expect(Scheduler).toHaveYielded(['Count: 1']);
expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
}); });
}); });
@ -484,7 +484,7 @@ describe('ReactHooksWithNoopRenderer', () => {
counter.current.dispatch('reset'); counter.current.dispatch('reset');
}); });
ReactNoop.render(<Counter ref={counter} />); ReactNoop.render(<Counter ref={counter} />);
expect(Scheduler).toFlushAndYield([ expect(Scheduler).toHaveYielded([
'Render: 0', 'Render: 0',
'Render: 1', 'Render: 1',
'Render: 11', 'Render: 11',
@ -524,7 +524,7 @@ describe('ReactHooksWithNoopRenderer', () => {
expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
act(() => counter.current.dispatch(INCREMENT)); act(() => counter.current.dispatch(INCREMENT));
expect(Scheduler).toFlushAndYield(['Count: 1']); expect(Scheduler).toHaveYielded(['Count: 1']);
expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
act(() => { act(() => {
counter.current.dispatch(DECREMENT); counter.current.dispatch(DECREMENT);
@ -532,7 +532,7 @@ describe('ReactHooksWithNoopRenderer', () => {
counter.current.dispatch(DECREMENT); counter.current.dispatch(DECREMENT);
}); });
expect(Scheduler).toFlushAndYield(['Count: -2']); expect(Scheduler).toHaveYielded(['Count: -2']);
expect(ReactNoop.getChildren()).toEqual([span('Count: -2')]); expect(ReactNoop.getChildren()).toEqual([span('Count: -2')]);
}); });
@ -566,7 +566,7 @@ describe('ReactHooksWithNoopRenderer', () => {
expect(ReactNoop.getChildren()).toEqual([span('Count: 10')]); expect(ReactNoop.getChildren()).toEqual([span('Count: 10')]);
act(() => counter.current.dispatch(INCREMENT)); act(() => counter.current.dispatch(INCREMENT));
expect(Scheduler).toFlushAndYield(['Count: 11']); expect(Scheduler).toHaveYielded(['Count: 11']);
expect(ReactNoop.getChildren()).toEqual([span('Count: 11')]); expect(ReactNoop.getChildren()).toEqual([span('Count: 11')]);
act(() => { act(() => {
@ -575,7 +575,7 @@ describe('ReactHooksWithNoopRenderer', () => {
counter.current.dispatch(DECREMENT); counter.current.dispatch(DECREMENT);
}); });
expect(Scheduler).toFlushAndYield(['Count: 8']); expect(Scheduler).toHaveYielded(['Count: 8']);
expect(ReactNoop.getChildren()).toEqual([span('Count: 8')]); expect(ReactNoop.getChildren()).toEqual([span('Count: 8')]);
}); });
@ -600,7 +600,7 @@ describe('ReactHooksWithNoopRenderer', () => {
expect(Scheduler).toFlushAndYield(['Count: 0']); expect(Scheduler).toFlushAndYield(['Count: 0']);
expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
act(() => { ReactNoop.batchedUpdates(() => {
counter.current.dispatch(INCREMENT); counter.current.dispatch(INCREMENT);
counter.current.dispatch(INCREMENT); counter.current.dispatch(INCREMENT);
counter.current.dispatch(INCREMENT); counter.current.dispatch(INCREMENT);
@ -884,8 +884,12 @@ describe('ReactHooksWithNoopRenderer', () => {
// A discrete event forces the passive effect to be flushed -- // A discrete event forces the passive effect to be flushed --
// updateCount(1) happens first, so 2 wins. // updateCount(1) happens first, so 2 wins.
ReactNoop.interactiveUpdates(() => { ReactNoop.interactiveUpdates(() => {
act(() => _updateCount(2)); // (use batchedUpdates to silence the act() warning)
ReactNoop.batchedUpdates(() => {
_updateCount(2);
});
}); });
expect(Scheduler).toHaveYielded(['Will set count to 1']); expect(Scheduler).toHaveYielded(['Will set count to 1']);
expect(Scheduler).toFlushAndYield(['Count: 2']); expect(Scheduler).toFlushAndYield(['Count: 2']);
@ -936,7 +940,8 @@ describe('ReactHooksWithNoopRenderer', () => {
// A discrete event forces the passive effect to be flushed -- // A discrete event forces the passive effect to be flushed --
// updateCount(1) happens first, so 2 wins. // updateCount(1) happens first, so 2 wins.
ReactNoop.interactiveUpdates(() => { ReactNoop.interactiveUpdates(() => {
act(() => _updateCount(2)); // use batchedUpdates to silence the act warning
ReactNoop.batchedUpdates(() => _updateCount(2));
}); });
expect(Scheduler).toHaveYielded(['Will set count to 1']); expect(Scheduler).toHaveYielded(['Will set count to 1']);
expect(Scheduler).toFlushAndYield(['Count: 2']); expect(Scheduler).toFlushAndYield(['Count: 2']);
@ -1527,7 +1532,7 @@ describe('ReactHooksWithNoopRenderer', () => {
]); ]);
act(button.current.increment); act(button.current.increment);
expect(Scheduler).toFlushAndYield([ expect(Scheduler).toHaveYielded([
// Button should not re-render, because its props haven't changed // Button should not re-render, because its props haven't changed
// 'Increment', // 'Increment',
'Count: 1', 'Count: 1',
@ -1551,7 +1556,7 @@ describe('ReactHooksWithNoopRenderer', () => {
// Callback should have updated // Callback should have updated
act(button.current.increment); act(button.current.increment);
expect(Scheduler).toFlushAndYield(['Count: 11']); expect(Scheduler).toHaveYielded(['Count: 11']);
expect(ReactNoop.getChildren()).toEqual([ expect(ReactNoop.getChildren()).toEqual([
span('Increment'), span('Increment'),
span('Count: 11'), span('Count: 11'),
@ -1754,7 +1759,7 @@ describe('ReactHooksWithNoopRenderer', () => {
act(() => { act(() => {
counter.current.dispatch(INCREMENT); counter.current.dispatch(INCREMENT);
}); });
expect(Scheduler).toFlushAndYield(['Count: 1']); expect(Scheduler).toHaveYielded(['Count: 1']);
expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
// Intentionally not updated because of [] deps: // Intentionally not updated because of [] deps:
expect(counter.current.count).toBe(0); expect(counter.current.count).toBe(0);
@ -1784,7 +1789,7 @@ describe('ReactHooksWithNoopRenderer', () => {
act(() => { act(() => {
counter.current.dispatch(INCREMENT); counter.current.dispatch(INCREMENT);
}); });
expect(Scheduler).toFlushAndYield(['Count: 1']); expect(Scheduler).toHaveYielded(['Count: 1']);
expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
expect(counter.current.count).toBe(1); expect(counter.current.count).toBe(1);
}); });
@ -1821,7 +1826,7 @@ describe('ReactHooksWithNoopRenderer', () => {
act(() => { act(() => {
counter.current.dispatch(INCREMENT); counter.current.dispatch(INCREMENT);
}); });
expect(Scheduler).toFlushAndYield(['Count: 1']); expect(Scheduler).toHaveYielded(['Count: 1']);
expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
expect(counter.current.count).toBe(1); expect(counter.current.count).toBe(1);
expect(totalRefUpdates).toBe(2); expect(totalRefUpdates).toBe(2);
@ -1868,7 +1873,7 @@ describe('ReactHooksWithNoopRenderer', () => {
updateB(3); updateB(3);
}); });
expect(Scheduler).toFlushAndYield(['A: 2, B: 3, C: [not loaded]']); expect(Scheduler).toHaveYielded(['A: 2, B: 3, C: [not loaded]']);
expect(ReactNoop.getChildren()).toEqual([ expect(ReactNoop.getChildren()).toEqual([
span('A: 2, B: 3, C: [not loaded]'), span('A: 2, B: 3, C: [not loaded]'),
]); ]);
@ -1929,7 +1934,7 @@ describe('ReactHooksWithNoopRenderer', () => {
updateB(3); updateB(3);
updateC(4); updateC(4);
}); });
expect(Scheduler).toFlushAndYield(['A: 2, B: 3, C: 4']); expect(Scheduler).toHaveYielded(['A: 2, B: 3, C: 4']);
expect(ReactNoop.getChildren()).toEqual([span('A: 2, B: 3, C: 4')]); expect(ReactNoop.getChildren()).toEqual([span('A: 2, B: 3, C: 4')]);
ReactNoop.render(<App loadC={false} />); ReactNoop.render(<App loadC={false} />);
expect(Scheduler).toFlushAndThrow( expect(Scheduler).toFlushAndThrow(
@ -2035,7 +2040,7 @@ describe('ReactHooksWithNoopRenderer', () => {
act(() => { act(() => {
setCounter(2); setCounter(2);
}); });
expect(Scheduler).toFlushAndYield([ expect(Scheduler).toHaveYielded([
'Render: 1', 'Render: 1',
'Effect: 2', 'Effect: 2',
'Reducer: 2', 'Reducer: 2',
@ -2074,7 +2079,7 @@ describe('ReactHooksWithNoopRenderer', () => {
expect(ReactNoop).toMatchRenderedOutput('0'); expect(ReactNoop).toMatchRenderedOutput('0');
act(() => dispatch()); act(() => dispatch());
expect(Scheduler).toFlushAndYield(['Step: 5, Shadow: 5']); expect(Scheduler).toHaveYielded(['Step: 5, Shadow: 5']);
expect(ReactNoop).toMatchRenderedOutput('5'); expect(ReactNoop).toMatchRenderedOutput('5');
}); });
@ -2113,7 +2118,8 @@ describe('ReactHooksWithNoopRenderer', () => {
// Enqueuing this update forces the passive effect to be flushed -- // Enqueuing this update forces the passive effect to be flushed --
// updateCount(1) happens first, so 2 wins. // updateCount(1) happens first, so 2 wins.
act(() => _updateCount(2)); // (use batchedUpdates to silence the act() warning)
ReactNoop.batchedUpdates(() => _updateCount(2));
expect(Scheduler).toHaveYielded(['Will set count to 1']); expect(Scheduler).toHaveYielded(['Will set count to 1']);
expect(Scheduler).toFlushAndYield(['Count: 2']); expect(Scheduler).toFlushAndYield(['Count: 2']);
expect(ReactNoop.getChildren()).toEqual([span('Count: 2')]); expect(ReactNoop.getChildren()).toEqual([span('Count: 2')]);

View File

@ -104,14 +104,14 @@ describe('ReactIncrementalScheduling', () => {
ReactNoop.renderToRootWithID(<Text text="b:1" />, 'b'); ReactNoop.renderToRootWithID(<Text text="b:1" />, 'b');
ReactNoop.renderToRootWithID(<Text text="c:1" />, 'c'); ReactNoop.renderToRootWithID(<Text text="c:1" />, 'c');
}); });
expect(Scheduler).toFlushAndYield(['a:1', 'b:1', 'c:1']); expect(Scheduler).toHaveYielded(['a:1', 'b:1', 'c:1']);
expect(ReactNoop.getChildrenAsJSX('a')).toEqual('a:1'); expect(ReactNoop.getChildrenAsJSX('a')).toEqual('a:1');
expect(ReactNoop.getChildrenAsJSX('b')).toEqual('b:1'); expect(ReactNoop.getChildrenAsJSX('b')).toEqual('b:1');
expect(ReactNoop.getChildrenAsJSX('c')).toEqual('c:1'); expect(ReactNoop.getChildrenAsJSX('c')).toEqual('c:1');
// Schedule deferred work in the reverse order // Schedule deferred work in the reverse order
ReactNoop.act(() => { ReactNoop.batchedUpdates(() => {
ReactNoop.renderToRootWithID(<Text text="c:2" />, 'c'); ReactNoop.renderToRootWithID(<Text text="c:2" />, 'c');
ReactNoop.renderToRootWithID(<Text text="b:2" />, 'b'); ReactNoop.renderToRootWithID(<Text text="b:2" />, 'b');
}); });
@ -122,7 +122,7 @@ describe('ReactIncrementalScheduling', () => {
expect(ReactNoop.getChildrenAsJSX('b')).toEqual('b:1'); expect(ReactNoop.getChildrenAsJSX('b')).toEqual('b:1');
expect(ReactNoop.getChildrenAsJSX('c')).toEqual('c:2'); expect(ReactNoop.getChildrenAsJSX('c')).toEqual('c:2');
// Schedule last bit of work, it will get processed the last // Schedule last bit of work, it will get processed the last
ReactNoop.act(() => { ReactNoop.batchedUpdates(() => {
ReactNoop.renderToRootWithID(<Text text="a:2" />, 'a'); ReactNoop.renderToRootWithID(<Text text="a:2" />, 'a');
}); });
// Keep performing work in the order it was scheduled // Keep performing work in the order it was scheduled

View File

@ -42,6 +42,7 @@ describe('ReactNoop.act()', () => {
Scheduler.yieldValue('stage 1'); Scheduler.yieldValue('stage 1');
await null; await null;
Scheduler.yieldValue('stage 2'); Scheduler.yieldValue('stage 2');
await null;
setCtr(1); setCtr(1);
} }
React.useEffect(() => { React.useEffect(() => {
@ -50,13 +51,9 @@ describe('ReactNoop.act()', () => {
return ctr; return ctr;
} }
await ReactNoop.act(async () => { await ReactNoop.act(async () => {
ReactNoop.act(() => { ReactNoop.render(<App />);
ReactNoop.render(<App />);
});
await null;
expect(Scheduler).toFlushAndYield(['stage 1']);
}); });
expect(Scheduler).toHaveYielded(['stage 2']); expect(Scheduler).toHaveYielded(['stage 1', 'stage 2']);
expect(Scheduler).toFlushWithoutYielding(); expect(Scheduler).toFlushWithoutYielding();
expect(ReactNoop.getChildren()).toEqual([{text: '1', hidden: false}]); expect(ReactNoop.getChildren()).toEqual([{text: '1', hidden: false}]);
}); });

View File

@ -14,23 +14,42 @@ import {
} from 'react-reconciler/inline.test'; } from 'react-reconciler/inline.test';
import ReactSharedInternals from 'shared/ReactSharedInternals'; import ReactSharedInternals from 'shared/ReactSharedInternals';
import warningWithoutStack from 'shared/warningWithoutStack'; import warningWithoutStack from 'shared/warningWithoutStack';
import {warnAboutMissingMockScheduler} from 'shared/ReactFeatureFlags';
import enqueueTask from 'shared/enqueueTask'; import enqueueTask from 'shared/enqueueTask';
import * as Scheduler from 'scheduler';
const {ReactShouldWarnActingUpdates} = ReactSharedInternals; const {ReactShouldWarnActingUpdates} = ReactSharedInternals;
// this implementation should be exactly the same in // this implementation should be exactly the same in
// ReactTestUtilsAct.js, ReactTestRendererAct.js, createReactNoop.js // ReactTestUtilsAct.js, ReactTestRendererAct.js, createReactNoop.js
// we track the 'depth' of the act() calls with this counter, let hasWarnedAboutMissingMockScheduler = false;
// so we can tell if any async act() calls try to run in parallel. const flushWork =
let actingUpdatesScopeDepth = 0; Scheduler.unstable_flushWithoutYielding ||
function() {
if (warnAboutMissingMockScheduler === true) {
if (hasWarnedAboutMissingMockScheduler === false) {
warningWithoutStack(
null,
'Starting from React v17, the "scheduler" module will need to be mocked ' +
'to guarantee consistent behaviour across tests and browsers. To fix this, add the following ' +
"to the top of your tests, or in your framework's global config file -\n\n" +
'As an example, for jest - \n' +
"jest.mock('scheduler', () => require.requireActual('scheduler/unstable_mock'));\n\n" +
'For more info, visit https://fb.me/react-mock-scheduler',
);
hasWarnedAboutMissingMockScheduler = true;
}
}
while (flushPassiveEffects()) {}
};
function flushEffectsAndMicroTasks(onDone: (err: ?Error) => void) { function flushWorkAndMicroTasks(onDone: (err: ?Error) => void) {
try { try {
flushPassiveEffects(); flushWork();
enqueueTask(() => { enqueueTask(() => {
if (flushPassiveEffects()) { if (flushWork()) {
flushEffectsAndMicroTasks(onDone); flushWorkAndMicroTasks(onDone);
} else { } else {
onDone(); onDone();
} }
@ -40,6 +59,11 @@ function flushEffectsAndMicroTasks(onDone: (err: ?Error) => void) {
} }
} }
// we track the 'depth' of the act() calls with this counter,
// so we can tell if any async act() calls try to run in parallel.
let actingUpdatesScopeDepth = 0;
function act(callback: () => Thenable) { function act(callback: () => Thenable) {
let previousActingUpdatesScopeDepth; let previousActingUpdatesScopeDepth;
if (__DEV__) { if (__DEV__) {
@ -100,7 +124,7 @@ function act(callback: () => Thenable) {
called = true; called = true;
result.then( result.then(
() => { () => {
flushEffectsAndMicroTasks((err: ?Error) => { flushWorkAndMicroTasks((err: ?Error) => {
onDone(); onDone();
if (err) { if (err) {
reject(err); reject(err);
@ -128,7 +152,7 @@ function act(callback: () => Thenable) {
// flush effects until none remain, and cleanup // flush effects until none remain, and cleanup
try { try {
while (flushPassiveEffects()) {} flushWork();
onDone(); onDone();
} catch (err) { } catch (err) {
onDone(); onDone();

View File

@ -103,12 +103,15 @@ export function unstable_flushExpired() {
} }
} }
export function unstable_flushWithoutYielding(): void { export function unstable_flushWithoutYielding(): boolean {
if (isFlushing) { if (isFlushing) {
throw new Error('Already flushing work.'); throw new Error('Already flushing work.');
} }
isFlushing = true; isFlushing = true;
try { try {
if (scheduledCallback === null) {
return false;
}
while (scheduledCallback !== null) { while (scheduledCallback !== null) {
const cb = scheduledCallback; const cb = scheduledCallback;
scheduledCallback = null; scheduledCallback = null;
@ -117,6 +120,7 @@ export function unstable_flushWithoutYielding(): void {
scheduledCallbackExpiration <= currentTime; scheduledCallbackExpiration <= currentTime;
cb(didTimeout); cb(didTimeout);
} }
return true;
} finally { } finally {
expectedNumberOfYields = -1; expectedNumberOfYields = -1;
didStop = false; didStop = false;

View File

@ -68,5 +68,8 @@ export const enableEventAPI = false;
// New API for JSX transforms to target - https://github.com/reactjs/rfcs/pull/107 // New API for JSX transforms to target - https://github.com/reactjs/rfcs/pull/107
export const enableJSXTransformAPI = false; export const enableJSXTransformAPI = false;
// We will enforce mocking scheduler with scheduler/unstable_mock at some point. (v17?)
// Till then, we warn about the missing mock, but still fallback to a sync mode compatible version
export const warnAboutMissingMockScheduler = false;
// Temporary flag to revert the fix in #15650 // Temporary flag to revert the fix in #15650
export const revertPassiveEffectsChange = false; export const revertPassiveEffectsChange = false;

View File

@ -32,6 +32,7 @@ export const warnAboutDeprecatedLifecycles = true;
export const warnAboutDeprecatedSetNativeProps = true; export const warnAboutDeprecatedSetNativeProps = true;
export const enableEventAPI = false; export const enableEventAPI = false;
export const enableJSXTransformAPI = false; export const enableJSXTransformAPI = false;
export const warnAboutMissingMockScheduler = true;
export const revertPassiveEffectsChange = false; export const revertPassiveEffectsChange = false;
// Only used in www builds. // Only used in www builds.

View File

@ -29,6 +29,7 @@ export const enableSchedulerDebugging = false;
export const warnAboutDeprecatedSetNativeProps = false; export const warnAboutDeprecatedSetNativeProps = false;
export const enableEventAPI = false; export const enableEventAPI = false;
export const enableJSXTransformAPI = false; export const enableJSXTransformAPI = false;
export const warnAboutMissingMockScheduler = false;
export const revertPassiveEffectsChange = false; export const revertPassiveEffectsChange = false;
// Only used in www builds. // Only used in www builds.

View File

@ -29,6 +29,7 @@ export const enableSchedulerDebugging = false;
export const warnAboutDeprecatedSetNativeProps = false; export const warnAboutDeprecatedSetNativeProps = false;
export const enableEventAPI = false; export const enableEventAPI = false;
export const enableJSXTransformAPI = false; export const enableJSXTransformAPI = false;
export const warnAboutMissingMockScheduler = true;
export const revertPassiveEffectsChange = false; export const revertPassiveEffectsChange = false;
// Only used in www builds. // Only used in www builds.

View File

@ -29,6 +29,7 @@ export const enableSchedulerDebugging = false;
export const warnAboutDeprecatedSetNativeProps = false; export const warnAboutDeprecatedSetNativeProps = false;
export const enableEventAPI = false; export const enableEventAPI = false;
export const enableJSXTransformAPI = false; export const enableJSXTransformAPI = false;
export const warnAboutMissingMockScheduler = false;
export const revertPassiveEffectsChange = false; export const revertPassiveEffectsChange = false;
// Only used in www builds. // Only used in www builds.

View File

@ -30,6 +30,7 @@ export const disableJavaScriptURLs = false;
export const disableYielding = false; export const disableYielding = false;
export const enableEventAPI = true; export const enableEventAPI = true;
export const enableJSXTransformAPI = true; export const enableJSXTransformAPI = true;
export const warnAboutMissingMockScheduler = true;
// Only used in www builds. // Only used in www builds.
export function addUserTimingListener() { export function addUserTimingListener() {

View File

@ -72,6 +72,8 @@ export const enableEventAPI = true;
export const enableJSXTransformAPI = true; export const enableJSXTransformAPI = true;
export const warnAboutMissingMockScheduler = true;
// Flow magic to verify the exports of this file match the original version. // Flow magic to verify the exports of this file match the original version.
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
type Check<_X, Y: _X, X: Y = _X> = null; type Check<_X, Y: _X, X: Y = _X> = null;

View File

@ -28,6 +28,10 @@ const {
unstable_LowPriority, unstable_LowPriority,
unstable_IdlePriority, unstable_IdlePriority,
unstable_forceFrameRate, unstable_forceFrameRate,
// this doesn't actually exist on the scheduler, but it *does*
// on scheduler/unstable_mock, which we'll need inside act().
unstable_flushWithoutYielding,
} = ReactInternals.Scheduler; } = ReactInternals.Scheduler;
export { export {
@ -47,4 +51,5 @@ export {
unstable_LowPriority, unstable_LowPriority,
unstable_IdlePriority, unstable_IdlePriority,
unstable_forceFrameRate, unstable_forceFrameRate,
unstable_flushWithoutYielding,
}; };

View File

@ -418,7 +418,14 @@ const bundles = [
/******* React Scheduler Mock (experimental) *******/ /******* React Scheduler Mock (experimental) *******/
{ {
bundleTypes: [NODE_DEV, NODE_PROD, FB_WWW_DEV, FB_WWW_PROD], bundleTypes: [
UMD_DEV,
UMD_PROD,
NODE_DEV,
NODE_PROD,
FB_WWW_DEV,
FB_WWW_PROD,
],
moduleType: ISOMORPHIC, moduleType: ISOMORPHIC,
entry: 'scheduler/unstable_mock', entry: 'scheduler/unstable_mock',
global: 'SchedulerMock', global: 'SchedulerMock',