diff --git a/packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js b/packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js
index 827b3aa2f1855..c369eb97ed4c7 100644
--- a/packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js
+++ b/packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js
@@ -130,10 +130,12 @@ function runActTests(label, render, unmount) {
container = document.createElement('div');
document.body.appendChild(container);
});
+
afterEach(() => {
unmount(container);
document.body.removeChild(container);
});
+
describe('sync', () => {
it('can use act to flush effects', () => {
function App() {
@@ -240,13 +242,16 @@ function runActTests(label, render, unmount) {
'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);
@@ -268,6 +273,7 @@ function runActTests(label, render, unmount) {
expect(container.innerHTML).toBe('1');
});
+
it('can use the async version to catch microtasks', async () => {
function App() {
let [toggle, setToggle] = React.useState(0);
@@ -289,6 +295,7 @@ function runActTests(label, render, unmount) {
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.
@@ -314,6 +321,7 @@ function runActTests(label, render, unmount) {
// 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);
@@ -367,6 +375,7 @@ function runActTests(label, render, unmount) {
);
});
});
+
describe('asynchronous tests', () => {
it('works with timeouts', async () => {
function App() {
@@ -577,6 +586,7 @@ function runActTests(label, render, unmount) {
});
}
});
+
describe('error propagation', () => {
it('propagates errors - sync', () => {
let err;
diff --git a/packages/react-dom/src/__tests__/ReactTestUtilsActUnmockedScheduler-test.js b/packages/react-dom/src/__tests__/ReactTestUtilsActUnmockedScheduler-test.js
new file mode 100644
index 0000000000000..baa4f63e4ba75
--- /dev/null
+++ b/packages/react-dom/src/__tests__/ReactTestUtilsActUnmockedScheduler-test.js
@@ -0,0 +1,166 @@
+/**
+ * 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
+ */
+
+// sanity tests to make sure act() works without a mocked scheduler
+
+let React;
+let ReactDOM;
+let act;
+let container;
+let yields;
+
+function clearYields() {
+ try {
+ return yields;
+ } finally {
+ yields = [];
+ }
+}
+
+function render(el, dom) {
+ ReactDOM.render(el, dom);
+}
+
+function unmount(dom) {
+ ReactDOM.unmountComponentAtNode(dom);
+}
+
+beforeEach(() => {
+ jest.resetModules();
+ jest.unmock('scheduler');
+ yields = [];
+ React = require('react');
+ ReactDOM = require('react-dom');
+ act = require('react-dom/test-utils').act;
+ container = document.createElement('div');
+ document.body.appendChild(container);
+});
+
+afterEach(() => {
+ unmount(container);
+ document.body.removeChild(container);
+});
+
+it('can use act to flush effects', () => {
+ function App() {
+ React.useEffect(() => {
+ yields.push(100);
+ });
+ return null;
+ }
+
+ act(() => {
+ render(, container);
+ });
+
+ expect(clearYields()).toEqual([100]);
+});
+
+it('flushes effects on every call', () => {
+ function App() {
+ let [ctr, setCtr] = React.useState(0);
+ React.useEffect(() => {
+ yields.push(ctr);
+ });
+ return (
+
+ );
+ }
+
+ act(() => {
+ render(, container);
+ });
+
+ expect(clearYields()).toEqual([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(clearYields()).toEqual([3]);
+ act(click);
+ expect(clearYields()).toEqual([4]);
+ act(click);
+ expect(clearYields()).toEqual([5]);
+ expect(button.innerHTML).toEqual('5');
+});
+
+it("should keep flushing effects until the're done", () => {
+ function App() {
+ let [ctr, setCtr] = React.useState(0);
+ React.useEffect(() => {
+ if (ctr < 5) {
+ setCtr(x => x + 1);
+ }
+ });
+ return ctr;
+ }
+
+ act(() => {
+ render(, container);
+ });
+
+ expect(container.innerHTML).toEqual('5');
+});
+
+it('should flush effects only on exiting the outermost act', () => {
+ function App() {
+ React.useEffect(() => {
+ yields.push(0);
+ });
+ return null;
+ }
+ // let's nest a couple of act() calls
+ act(() => {
+ act(() => {
+ render(, container);
+ });
+ // the effect wouldn't have yielded yet because
+ // we're still inside an act() scope
+ expect(clearYields()).toEqual([]);
+ });
+ // but after exiting the last one, effects get flushed
+ expect(clearYields()).toEqual([0]);
+});
+
+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(
+ () => {
+ yields.push(state);
+ ticker();
+ },
+ [Math.min(state, 4)],
+ );
+ return state;
+ }
+
+ await act(async () => {
+ render(, container);
+ });
+ // all 5 ticks present and accounted for
+ expect(clearYields()).toEqual([0, 1, 2, 3, 4]);
+ expect(container.innerHTML).toBe('5');
+});
diff --git a/packages/react-dom/src/test-utils/ReactTestUtilsAct.js b/packages/react-dom/src/test-utils/ReactTestUtilsAct.js
index bfeb849d7dae0..a0ccce2c9c9a4 100644
--- a/packages/react-dom/src/test-utils/ReactTestUtilsAct.js
+++ b/packages/react-dom/src/test-utils/ReactTestUtilsAct.js
@@ -63,7 +63,13 @@ const flushWork =
hasWarnedAboutMissingMockScheduler = true;
}
}
- while (flushPassiveEffects()) {}
+
+ let didFlushWork = false;
+ while (flushPassiveEffects()) {
+ didFlushWork = true;
+ }
+
+ return didFlushWork;
};
function flushWorkAndMicroTasks(onDone: (err: ?Error) => void) {
diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js
index 1f868e4fcda17..451c3c4dccc88 100644
--- a/packages/react-noop-renderer/src/createReactNoop.js
+++ b/packages/react-noop-renderer/src/createReactNoop.js
@@ -619,7 +619,13 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
hasWarnedAboutMissingMockScheduler = true;
}
}
- while (flushPassiveEffects()) {}
+
+ let didFlushWork = false;
+ while (flushPassiveEffects()) {
+ didFlushWork = true;
+ }
+
+ return didFlushWork;
};
function flushWorkAndMicroTasks(onDone: (err: ?Error) => void) {
diff --git a/packages/react-test-renderer/src/ReactTestRendererAct.js b/packages/react-test-renderer/src/ReactTestRendererAct.js
index 4b6dd2864e226..83408301a6994 100644
--- a/packages/react-test-renderer/src/ReactTestRendererAct.js
+++ b/packages/react-test-renderer/src/ReactTestRendererAct.js
@@ -44,7 +44,13 @@ const flushWork =
hasWarnedAboutMissingMockScheduler = true;
}
}
- while (flushPassiveEffects()) {}
+
+ let didFlushWork = false;
+ while (flushPassiveEffects()) {
+ didFlushWork = true;
+ }
+
+ return didFlushWork;
};
function flushWorkAndMicroTasks(onDone: (err: ?Error) => void) {