@samitouri / QOS-React-2 / commits / e1c5e8720d

warn if passive effects get queued outside of an act() call. (#15763)

* warn if passive effects get queued outside of an act() call While the code itself isn't much (it adds the warning to mountEffect() and updateEffect() in ReactFiberHooks), it does change a lot of our tests. We follow a bad-ish pattern here, which is doing asserts inside act() scopes, but it makes sense for *us* because we're testing intermediate states, and we're manually flush/yield what we need in these tests. This commit has one last failing test. Working on it. * pass lint * pass failing test, fixes another - a test was failing in ReactDOMServerIntegrationHooks while testing an effect; the behaviour of yields was different from browser and server when wrapped with act(). further, because of how we initialized modules, act() around renders wasn't working corrrectly. solved by passing in ReactTestUtils in initModules, and checking on the finally yielded values in the specific test. - in ReactUpdates, while testing an infinite recursion detection, the test needed to be wrapped in an act(), which would have caused the recusrsion error to throw. solived by rethrowing the error from inside the act(). * pass ReactDOMServerSuspense * stray todo * a better message, consistent with the state update one.

Sunil Pai committed Jun 24, 2019 at 11:18 UTC e1c5e8720d5d47974c14c125dbaf3dc608a92936
35 files changed +1005 -732
packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js
+4 -1
@@ -146,7 +146,10 @@ describe('ReactHooksInspectionIntegration', () => {
146 </div>
147 );
148 }
149 - let renderer = ReactTestRenderer.create(<Foo prop="prop" />);
149 + let renderer;
150 + act(() => {
151 + renderer = ReactTestRenderer.create(<Foo prop="prop" />);
152 + });
153
154 let childFiber = renderer.root.findByType(Foo)._currentFiber();
155
packages/react-dom/src/__tests__/ReactDOMHooks-test.js
+26 -17
@@ -53,23 +53,32 @@ describe('ReactDOMHooks', () => {
53 return 3 * n;
54 }
55
56 - ReactDOM.render(<Example1 n={1} />, container);
57 - expect(container.textContent).toBe('1');
58 - expect(container2.textContent).toBe('');
59 - expect(container3.textContent).toBe('');
60 - Scheduler.flushAll();
61 - expect(container.textContent).toBe('1');
62 - expect(container2.textContent).toBe('2');
63 - expect(container3.textContent).toBe('3');
64 -
65 - ReactDOM.render(<Example1 n={2} />, container);
66 - expect(container.textContent).toBe('2');
67 - expect(container2.textContent).toBe('2'); // Not flushed yet
68 - expect(container3.textContent).toBe('3'); // Not flushed yet
69 - Scheduler.flushAll();
70 - expect(container.textContent).toBe('2');
71 - expect(container2.textContent).toBe('4');
72 - expect(container3.textContent).toBe('6');
56 + // we explicitly catch the missing act() warnings
57 + // to simulate this tricky repro
58 + expect(() => {
59 + ReactDOM.render(<Example1 n={1} />, container);
60 + expect(container.textContent).toBe('1');
61 + expect(container2.textContent).toBe('');
62 + expect(container3.textContent).toBe('');
63 + Scheduler.flushAll();
64 + expect(container.textContent).toBe('1');
65 + expect(container2.textContent).toBe('2');
66 + expect(container3.textContent).toBe('3');
67 +
68 + ReactDOM.render(<Example1 n={2} />, container);
69 + expect(container.textContent).toBe('2');
70 + expect(container2.textContent).toBe('2'); // Not flushed yet
71 + expect(container3.textContent).toBe('3'); // Not flushed yet
72 + Scheduler.flushAll();
73 + expect(container.textContent).toBe('2');
74 + expect(container2.textContent).toBe('4');
75 + expect(container3.textContent).toBe('6');
76 + }).toWarnDev([
77 + 'An update to Example1 ran an effect',
78 + 'An update to Example2 ran an effect',
79 + 'An update to Example1 ran an effect',
80 + 'An update to Example2 ran an effect',
81 + ]);
82 });
83
84 it('should not bail out when an update is scheduled from within an event handler', () => {
packages/react-dom/src/__tests__/ReactDOMServerIntegrationAttributes-test.js
+3
@@ -13,6 +13,7 @@ const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegratio
13
14 let React;
15 let ReactDOM;
16 +let ReactTestUtils;
17 let ReactDOMServer;
18
19 function initModules() {
@@ -21,11 +22,13 @@ function initModules() {
22 React = require('react');
23 ReactDOM = require('react-dom');
24 ReactDOMServer = require('react-dom/server');
25 + ReactTestUtils = require('react-dom/test-utils');
26
27 // Make them available to the helpers.
28 return {
29 ReactDOM,
30 ReactDOMServer,
31 + ReactTestUtils,
32 };
33 }
34
packages/react-dom/src/__tests__/ReactDOMServerIntegrationBasic-test.js
+3
@@ -16,6 +16,7 @@ const TEXT_NODE_TYPE = 3;
16 let React;
17 let ReactDOM;
18 let ReactDOMServer;
19 +let ReactTestUtils;
20
21 function initModules() {
22 // Reset warning cache.
@@ -23,11 +24,13 @@ function initModules() {
24 React = require('react');
25 ReactDOM = require('react-dom');
26 ReactDOMServer = require('react-dom/server');
27 + ReactTestUtils = require('react-dom/test-utils');
28
29 // Make them available to the helpers.
30 return {
31 ReactDOM,
32 ReactDOMServer,
33 + ReactTestUtils,
34 };
35 }
36
packages/react-dom/src/__tests__/ReactDOMServerIntegrationCheckbox-test.js
+3
@@ -16,6 +16,7 @@ const {disableInputAttributeSyncing} = require('shared/ReactFeatureFlags');
16 let React;
17 let ReactDOM;
18 let ReactDOMServer;
19 +let ReactTestUtils;
20
21 function initModules() {
22 // Reset warning cache.
@@ -23,11 +24,13 @@ function initModules() {
24 React = require('react');
25 ReactDOM = require('react-dom');
26 ReactDOMServer = require('react-dom/server');
27 + ReactTestUtils = require('react-dom/test-utils');
28
29 // Make them available to the helpers.
30 return {
31 ReactDOM,
32 ReactDOMServer,
33 + ReactTestUtils,
34 };
35 }
36
packages/react-dom/src/__tests__/ReactDOMServerIntegrationClassContextType-test.js
+3
@@ -14,6 +14,7 @@ const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegratio
14 let React;
15 let ReactDOM;
16 let ReactDOMServer;
17 +let ReactTestUtils;
18
19 function initModules() {
20 // Reset warning cache.
@@ -21,11 +22,13 @@ function initModules() {
22 React = require('react');
23 ReactDOM = require('react-dom');
24 ReactDOMServer = require('react-dom/server');
25 + ReactTestUtils = require('react-dom/test-utils');
26
27 // Make them available to the helpers.
28 return {
29 ReactDOM,
30 ReactDOMServer,
31 + ReactTestUtils,
32 };
33 }
34
packages/react-dom/src/__tests__/ReactDOMServerIntegrationElements-test.js
+3
@@ -16,17 +16,20 @@ const TEXT_NODE_TYPE = 3;
16 let React;
17 let ReactDOM;
18 let ReactDOMServer;
19 +let ReactTestUtils;
20
21 function initModules() {
22 jest.resetModuleRegistry();
23 React = require('react');
24 ReactDOM = require('react-dom');
25 ReactDOMServer = require('react-dom/server');
26 + ReactTestUtils = require('react-dom/test-utils');
27
28 // Make them available to the helpers.
29 return {
30 ReactDOM,
31 ReactDOMServer,
32 + ReactTestUtils,
33 };
34 }
35
packages/react-dom/src/__tests__/ReactDOMServerIntegrationFragment-test.js
+3
@@ -14,6 +14,7 @@ const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegratio
14 let React;
15 let ReactDOM;
16 let ReactDOMServer;
17 +let ReactTestUtils;
18
19 function initModules() {
20 // Reset warning cache.
@@ -21,11 +22,13 @@ function initModules() {
22 React = require('react');
23 ReactDOM = require('react-dom');
24 ReactDOMServer = require('react-dom/server');
25 + ReactTestUtils = require('react-dom/test-utils');
26
27 // Make them available to the helpers.
28 return {
29 ReactDOM,
30 ReactDOMServer,
31 + ReactTestUtils,
32 };
33 }
34
packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.internal.js
+17 -2
@@ -17,6 +17,7 @@ let React;
17 let ReactFeatureFlags;
18 let ReactDOM;
19 let ReactDOMServer;
20 +let ReactTestUtils;
21 let useState;
22 let useReducer;
23 let useEffect;
@@ -41,6 +42,7 @@ function initModules() {
42 React = require('react');
43 ReactDOM = require('react-dom');
44 ReactDOMServer = require('react-dom/server');
45 + ReactTestUtils = require('react-dom/test-utils');
46 useState = React.useState;
47 useReducer = React.useReducer;
48 useEffect = React.useEffect;
@@ -67,6 +69,7 @@ function initModules() {
69 return {
70 ReactDOM,
71 ReactDOMServer,
72 + ReactTestUtils,
73 };
74 }
75
@@ -539,18 +542,30 @@ describe('ReactDOMServerHooks', () => {
542 });
543
544 describe('useEffect', () => {
545 + const yields = [];
546 itRenders('should ignore effects on the server', async render => {
547 function Counter(props) {
548 useEffect(() => {
545 - yieldValue('should not be invoked');
549 + yieldValue('invoked on client');
550 });
551 return <Text text={'Count: ' + props.count} />;
552 }
553 +
554 const domNode = await render(<Counter count={0} />);
550 - expect(clearYields()).toEqual(['Count: 0']);
555 + yields.push(clearYields());
556 expect(domNode.tagName).toEqual('SPAN');
557 expect(domNode.textContent).toEqual('Count: 0');
558 });
559 +
560 + it('verifies yields in order', () => {
561 + expect(yields).toEqual([
562 + ['Count: 0'], // server render
563 + ['Count: 0'], // server stream
564 + ['Count: 0', 'invoked on client'], // clean render
565 + ['Count: 0', 'invoked on client'], // hydrated render
566 + // nothing yielded for bad markup
567 + ]);
568 + });
569 });
570
571 describe('useCallback', () => {
packages/react-dom/src/__tests__/ReactDOMServerIntegrationInput-test.js
+3
@@ -16,6 +16,7 @@ const {disableInputAttributeSyncing} = require('shared/ReactFeatureFlags');
16 let React;
17 let ReactDOM;
18 let ReactDOMServer;
19 +let ReactTestUtils;
20
21 function initModules() {
22 // Reset warning cache.
@@ -23,11 +24,13 @@ function initModules() {
24 React = require('react');
25 ReactDOM = require('react-dom');
26 ReactDOMServer = require('react-dom/server');
27 + ReactTestUtils = require('react-dom/test-utils');
28
29 // Make them available to the helpers.
30 return {
31 ReactDOM,
32 ReactDOMServer,
33 + ReactTestUtils,
34 };
35 }
36
packages/react-dom/src/__tests__/ReactDOMServerIntegrationLegacyContext-test.js
+3
@@ -15,6 +15,7 @@ let PropTypes;
15 let React;
16 let ReactDOM;
17 let ReactDOMServer;
18 +let ReactTestUtils;
19
20 function initModules() {
21 // Reset warning cache.
@@ -23,11 +24,13 @@ function initModules() {
24 React = require('react');
25 ReactDOM = require('react-dom');
26 ReactDOMServer = require('react-dom/server');
27 + ReactTestUtils = require('react-dom/test-utils');
28
29 // Make them available to the helpers.
30 return {
31 ReactDOM,
32 ReactDOMServer,
33 + ReactTestUtils,
34 };
35 }
36
packages/react-dom/src/__tests__/ReactDOMServerIntegrationModes-test.js
+3
@@ -14,6 +14,7 @@ const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegratio
14 let React;
15 let ReactDOM;
16 let ReactDOMServer;
17 +let ReactTestUtils;
18
19 function initModules() {
20 // Reset warning cache.
@@ -21,11 +22,13 @@ function initModules() {
22 React = require('react');
23 ReactDOM = require('react-dom');
24 ReactDOMServer = require('react-dom/server');
25 + ReactTestUtils = require('react-dom/test-utils');
26
27 // Make them available to the helpers.
28 return {
29 ReactDOM,
30 ReactDOMServer,
31 + ReactTestUtils,
32 };
33 }
34
packages/react-dom/src/__tests__/ReactDOMServerIntegrationNewContext-test.js
+3
@@ -14,6 +14,7 @@ const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegratio
14 let React;
15 let ReactDOM;
16 let ReactDOMServer;
17 +let ReactTestUtils;
18
19 function initModules() {
20 // Reset warning cache.
@@ -21,11 +22,13 @@ function initModules() {
22 React = require('react');
23 ReactDOM = require('react-dom');
24 ReactDOMServer = require('react-dom/server');
25 + ReactTestUtils = require('react-dom/test-utils');
26
27 // Make them available to the helpers.
28 return {
29 ReactDOM,
30 ReactDOMServer,
31 + ReactTestUtils,
32 };
33 }
34
packages/react-dom/src/__tests__/ReactDOMServerIntegrationReconnecting-test.js
+3
@@ -14,6 +14,7 @@ const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegratio
14 let React;
15 let ReactDOM;
16 let ReactDOMServer;
17 +let ReactTestUtils;
18
19 function initModules() {
20 // Reset warning cache.
@@ -22,11 +23,13 @@ function initModules() {
23 React = require('react');
24 ReactDOM = require('react-dom');
25 ReactDOMServer = require('react-dom/server');
26 + ReactTestUtils = require('react-dom/test-utils');
27
28 // Make them available to the helpers.
29 return {
30 ReactDOM,
31 ReactDOMServer,
32 + ReactTestUtils,
33 };
34 }
35
packages/react-dom/src/__tests__/ReactDOMServerIntegrationRefs-test.js
+3
@@ -14,6 +14,7 @@ const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegratio
14 let React;
15 let ReactDOM;
16 let ReactDOMServer;
17 +let ReactTestUtils;
18
19 function initModules() {
20 // Reset warning cache.
@@ -21,11 +22,13 @@ function initModules() {
22 React = require('react');
23 ReactDOM = require('react-dom');
24 ReactDOMServer = require('react-dom/server');
25 + ReactTestUtils = require('react-dom/test-utils');
26
27 // Make them available to the helpers.
28 return {
29 ReactDOM,
30 ReactDOMServer,
31 + ReactTestUtils,
32 };
33 }
34
packages/react-dom/src/__tests__/ReactDOMServerIntegrationSelect-test.js
+3
@@ -14,6 +14,7 @@ const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegratio
14 let React;
15 let ReactDOM;
16 let ReactDOMServer;
17 +let ReactTestUtils;
18
19 function initModules() {
20 // Reset warning cache.
@@ -21,11 +22,13 @@ function initModules() {
22 React = require('react');
23 ReactDOM = require('react-dom');
24 ReactDOMServer = require('react-dom/server');
25 + ReactTestUtils = require('react-dom/test-utils');
26
27 // Make them available to the helpers.
28 return {
29 ReactDOM,
30 ReactDOMServer,
31 + ReactTestUtils,
32 };
33 }
34
packages/react-dom/src/__tests__/ReactDOMServerIntegrationSpecialTypes-test.js
+3
@@ -14,6 +14,7 @@ const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegratio
14 let React;
15 let ReactDOM;
16 let ReactDOMServer;
17 +let ReactTestUtils;
18 let forwardRef;
19 let memo;
20 let yieldedValues;
@@ -26,6 +27,7 @@ function initModules() {
27 React = require('react');
28 ReactDOM = require('react-dom');
29 ReactDOMServer = require('react-dom/server');
30 + ReactTestUtils = require('react-dom/test-utils');
31 forwardRef = React.forwardRef;
32 memo = React.memo;
33
@@ -43,6 +45,7 @@ function initModules() {
45 return {
46 ReactDOM,
47 ReactDOMServer,
48 + ReactTestUtils,
49 };
50 }
51
packages/react-dom/src/__tests__/ReactDOMServerIntegrationTextarea-test.js
+3
@@ -14,6 +14,7 @@ const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegratio
14 let React;
15 let ReactDOM;
16 let ReactDOMServer;
17 +let ReactTestUtils;
18
19 function initModules() {
20 // Reset warning cache.
@@ -21,11 +22,13 @@ function initModules() {
22 React = require('react');
23 ReactDOM = require('react-dom');
24 ReactDOMServer = require('react-dom/server');
25 + ReactTestUtils = require('react-dom/test-utils');
26
27 // Make them available to the helpers.
28 return {
29 ReactDOM,
30 ReactDOMServer,
31 + ReactTestUtils,
32 };
33 }
34
packages/react-dom/src/__tests__/ReactDOMServerIntegrationUntrustedURL-test.internal.js
+5
@@ -16,6 +16,7 @@ const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegratio
16 let React;
17 let ReactDOM;
18 let ReactDOMServer;
19 +let ReactTestUtils;
20
21 function runTests(itRenders, itRejectsRendering, expectToReject) {
22 itRenders('a http link with the word javascript in it', async render => {
@@ -152,11 +153,13 @@ describe('ReactDOMServerIntegration - Untrusted URLs', () => {
153 React = require('react');
154 ReactDOM = require('react-dom');
155 ReactDOMServer = require('react-dom/server');
156 + ReactTestUtils = require('react-dom/test-utils');
157
158 // Make them available to the helpers.
159 return {
160 ReactDOM,
161 ReactDOMServer,
162 + ReactTestUtils,
163 };
164 }
165
@@ -185,11 +188,13 @@ describe('ReactDOMServerIntegration - Untrusted URLs - disableJavaScriptURLs', (
188 React = require('react');
189 ReactDOM = require('react-dom');
190 ReactDOMServer = require('react-dom/server');
191 + ReactTestUtils = require('react-dom/test-utils');
192
193 // Make them available to the helpers.
194 return {
195 ReactDOM,
196 ReactDOMServer,
197 + ReactTestUtils,
198 };
199 }
200
packages/react-dom/src/__tests__/ReactDOMServerIntegrationUserInteraction-test.js
+3
@@ -14,6 +14,7 @@ const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegratio
14 let React;
15 let ReactDOM;
16 let ReactDOMServer;
17 +let ReactTestUtils;
18
19 function initModules() {
20 // Reset warning cache.
@@ -21,11 +22,13 @@ function initModules() {
22 React = require('react');
23 ReactDOM = require('react-dom');
24 ReactDOMServer = require('react-dom/server');
25 + ReactTestUtils = require('react-dom/test-utils');
26
27 // Make them available to the helpers.
28 return {
29 ReactDOM,
30 ReactDOMServer,
31 + ReactTestUtils,
32 };
33 }
34
packages/react-dom/src/__tests__/ReactDOMServerSuspense-test.internal.js
+3
@@ -15,6 +15,7 @@ let React;
15 let ReactDOM;
16 let ReactDOMServer;
17 let ReactFeatureFlags;
18 +let ReactTestUtils;
19
20 function initModules() {
21 // Reset warning cache.
@@ -26,11 +27,13 @@ function initModules() {
27 React = require('react');
28 ReactDOM = require('react-dom');
29 ReactDOMServer = require('react-dom/server');
30 + ReactTestUtils = require('react-dom/test-utils');
31
32 // Make them available to the helpers.
33 return {
34 ReactDOM,
35 ReactDOMServer,
36 + ReactTestUtils,
37 };
38 }
39
packages/react-dom/src/__tests__/ReactDOMserverIntegrationProgress-test.js
+3
@@ -14,6 +14,7 @@ const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegratio
14 let React;
15 let ReactDOM;
16 let ReactDOMServer;
17 +let ReactTestUtils;
18
19 function initModules() {
20 // Reset warning cache.
@@ -21,11 +22,13 @@ function initModules() {
22 React = require('react');
23 ReactDOM = require('react-dom');
24 ReactDOMServer = require('react-dom/server');
25 + ReactTestUtils = require('react-dom/test-utils');
26
27 // Make them available to the helpers.
28 return {
29 ReactDOM,
30 ReactDOMServer,
31 + ReactTestUtils,
32 };
33 }
34
packages/react-dom/src/__tests__/ReactErrorBoundaries-test.internal.js
+22 -21
@@ -12,8 +12,8 @@
12 let PropTypes;
13 let React;
14 let ReactDOM;
15 +let act;
16 let ReactFeatureFlags;
16 -let Scheduler;
17
18 describe('ReactErrorBoundaries', () => {
19 let log;
@@ -45,7 +45,7 @@ describe('ReactErrorBoundaries', () => {
45 ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
46 ReactDOM = require('react-dom');
47 React = require('react');
48 - Scheduler = require('scheduler');
48 + act = require('react-dom/test-utils').act;
49
50 log = [];
51
@@ -1835,25 +1835,26 @@ describe('ReactErrorBoundaries', () => {
1835
1836 it('catches errors in useEffect', () => {
1837 const container = document.createElement('div');
1838 - ReactDOM.render(
1839 - <ErrorBoundary>
1840 - <BrokenUseEffect>Initial value</BrokenUseEffect>
1841 - </ErrorBoundary>,
1842 - container,
1843 - );
1844 - expect(log).toEqual([
1845 - 'ErrorBoundary constructor',
1846 - 'ErrorBoundary componentWillMount',
1847 - 'ErrorBoundary render success',
1848 - 'BrokenUseEffect render',
1849 - 'ErrorBoundary componentDidMount',
1850 - ]);
1851 -
1852 - expect(container.firstChild.textContent).toBe('Initial value');
1853 - log.length = 0;
1854 -
1855 - // Flush passive effects and handle the error
1856 - Scheduler.flushAll();
1838 + act(() => {
1839 + ReactDOM.render(
1840 + <ErrorBoundary>
1841 + <BrokenUseEffect>Initial value</BrokenUseEffect>
1842 + </ErrorBoundary>,
1843 + container,
1844 + );
1845 + expect(log).toEqual([
1846 + 'ErrorBoundary constructor',
1847 + 'ErrorBoundary componentWillMount',
1848 + 'ErrorBoundary render success',
1849 + 'BrokenUseEffect render',
1850 + 'ErrorBoundary componentDidMount',
1851 + ]);
1852 +
1853 + expect(container.firstChild.textContent).toBe('Initial value');
1854 + log.length = 0;
1855 + });
1856 +
1857 + // verify flushed passive effects and handle the error
1858 expect(log).toEqual([
1859 'BrokenUseEffect useEffect [!]',
1860 // Handle the error
packages/react-dom/src/__tests__/ReactUpdates-test.js
+55 -41
@@ -12,6 +12,7 @@
12 let React;
13 let ReactDOM;
14 let ReactTestUtils;
15 +let act;
16 let Scheduler;
17
18 describe('ReactUpdates', () => {
@@ -20,6 +21,7 @@ describe('ReactUpdates', () => {
21 React = require('react');
22 ReactDOM = require('react-dom');
23 ReactTestUtils = require('react-dom/test-utils');
24 + act = ReactTestUtils.act;
25 Scheduler = require('scheduler');
26 });
27
@@ -1322,30 +1324,31 @@ describe('ReactUpdates', () => {
1324 }
1325
1326 const root = ReactDOM.unstable_createRoot(container);
1325 - root.render(<Foo />);
1326 - if (__DEV__) {
1327 - expect(Scheduler).toFlushAndYieldThrough([
1328 - 'Foo',
1329 - 'Foo',
1330 - 'Baz',
1331 - 'Foo#effect',
1332 - ]);
1333 - } else {
1334 - expect(Scheduler).toFlushAndYieldThrough(['Foo', 'Baz', 'Foo#effect']);
1335 - }
1336 -
1337 - const hiddenDiv = container.firstChild.firstChild;
1338 - expect(hiddenDiv.hidden).toBe(true);
1339 - expect(hiddenDiv.innerHTML).toBe('');
1340 -
1341 - // Run offscreen update
1342 - if (__DEV__) {
1343 - expect(Scheduler).toFlushAndYield(['Bar', 'Bar']);
1344 - } else {
1345 - expect(Scheduler).toFlushAndYield(['Bar']);
1346 - }
1347 - expect(hiddenDiv.hidden).toBe(true);
1348 - expect(hiddenDiv.innerHTML).toBe('<p>bar 0</p>');
1327 + let hiddenDiv;
1328 + act(() => {
1329 + root.render(<Foo />);
1330 + if (__DEV__) {
1331 + expect(Scheduler).toFlushAndYieldThrough([
1332 + 'Foo',
1333 + 'Foo',
1334 + 'Baz',
1335 + 'Foo#effect',
1336 + ]);
1337 + } else {
1338 + expect(Scheduler).toFlushAndYieldThrough(['Foo', 'Baz', 'Foo#effect']);
1339 + }
1340 + hiddenDiv = container.firstChild.firstChild;
1341 + expect(hiddenDiv.hidden).toBe(true);
1342 + expect(hiddenDiv.innerHTML).toBe('');
1343 + // Run offscreen update
1344 + if (__DEV__) {
1345 + expect(Scheduler).toFlushAndYield(['Bar', 'Bar']);
1346 + } else {
1347 + expect(Scheduler).toFlushAndYield(['Bar']);
1348 + }
1349 + expect(hiddenDiv.hidden).toBe(true);
1350 + expect(hiddenDiv.innerHTML).toBe('<p>bar 0</p>');
1351 + });
1352
1353 ReactDOM.flushSync(() => {
1354 setCounter(1);
@@ -1623,12 +1626,18 @@ describe('ReactUpdates', () => {
1626 };
1627 try {
1628 const container = document.createElement('div');
1626 - ReactDOM.render(<App />, container);
1627 - while (error === null) {
1628 - Scheduler.unstable_flushNumberOfYields(1);
1629 - }
1630 - expect(error).toContain('Warning: Maximum update depth exceeded.');
1631 - expect(stack).toContain('in NonTerminating');
1629 + expect(() => {
1630 + act(() => {
1631 + ReactDOM.render(<App />, container);
1632 + while (error === null) {
1633 + Scheduler.unstable_flushNumberOfYields(1);
1634 + }
1635 + expect(error).toContain('Warning: Maximum update depth exceeded.');
1636 + expect(stack).toContain('in NonTerminating');
1637 + // rethrow error to prevent going into an infinite loop when act() exits
1638 + throw error;
1639 + });
1640 + }).toThrow('Maximum update depth exceeded.');
1641 } finally {
1642 console.error = originalConsoleError;
1643 }
@@ -1651,7 +1660,9 @@ describe('ReactUpdates', () => {
1660 }
1661
1662 const container = document.createElement('div');
1654 - ReactDOM.render(<Terminating />, container);
1663 + act(() => {
1664 + ReactDOM.render(<Terminating />, container);
1665 + });
1666
1667 // Verify we can flush them asynchronously without warning
1668 for (let i = 0; i < LIMIT * 2; i++) {
@@ -1660,16 +1671,16 @@ describe('ReactUpdates', () => {
1671 expect(container.textContent).toBe('50');
1672
1673 // Verify restarting from 0 doesn't cross the limit
1663 - expect(() => {
1674 + act(() => {
1675 _setStep(0);
1665 - }).toWarnDev(
1666 - 'An update to Terminating inside a test was not wrapped in act',
1667 - );
1668 - expect(container.textContent).toBe('0');
1669 - for (let i = 0; i < LIMIT * 2; i++) {
1676 + // flush once to update the dom
1677 Scheduler.unstable_flushNumberOfYields(1);
1671 - }
1672 - expect(container.textContent).toBe('50');
1678 + expect(container.textContent).toBe('0');
1679 + for (let i = 0; i < LIMIT * 2; i++) {
1680 + Scheduler.unstable_flushNumberOfYields(1);
1681 + }
1682 + expect(container.textContent).toBe('50');
1683 + });
1684 });
1685
1686 it('can have many updates inside useEffect without triggering a warning', () => {
@@ -1685,8 +1696,11 @@ describe('ReactUpdates', () => {
1696 }
1697
1698 const container = document.createElement('div');
1688 - ReactDOM.render(<Terminating />, container);
1689 - expect(Scheduler).toFlushAndYield(['Done']);
1699 + act(() => {
1700 + ReactDOM.render(<Terminating />, container);
1701 + });
1702 +
1703 + expect(Scheduler).toHaveYielded(['Done']);
1704 expect(container.textContent).toBe('1000');
1705 });
1706 }
packages/react-dom/src/__tests__/utils/ReactDOMServerIntegrationTestUtils.js
+8 -3
@@ -14,9 +14,10 @@ const stream = require('stream');
14 module.exports = function(initModules) {
15 let ReactDOM;
16 let ReactDOMServer;
17 + let ReactTestUtils;
18
19 function resetModules() {
19 - ({ReactDOM, ReactDOMServer} = initModules());
20 + ({ReactDOM, ReactDOMServer, ReactTestUtils} = initModules());
21 }
22
23 function shouldUseDocument(reactElement) {
@@ -48,9 +49,13 @@ module.exports = function(initModules) {
49 function asyncReactDOMRender(reactElement, domElement, forceHydrate) {
50 return new Promise(resolve => {
51 if (forceHydrate) {
51 - ReactDOM.hydrate(reactElement, domElement);
52 + ReactTestUtils.act(() => {
53 + ReactDOM.hydrate(reactElement, domElement);
54 + });
55 } else {
53 - ReactDOM.render(reactElement, domElement);
56 + ReactTestUtils.act(() => {
57 + ReactDOM.render(reactElement, domElement);
58 + });
59 }
60 // We can't use the callback for resolution because that will not catch
61 // errors. They're thrown.
packages/react-reconciler/src/ReactFiberHooks.js
+17
@@ -35,6 +35,7 @@ import {
35 computeExpirationForFiber,
36 flushPassiveEffects,
37 requestCurrentTime,
38 + warnIfNotCurrentlyActingEffectsInDEV,
39 warnIfNotCurrentlyActingUpdatesInDev,
40 warnIfNotScopedWithMatchingAct,
41 markRenderEventTimeAndConfig,
@@ -898,6 +899,14 @@ function mountEffect(
899 create: () => (() => void) | void,
900 deps: Array<mixed> | void | null,
901 ): void {
902 + if (__DEV__) {
903 + // $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests
904 + if ('undefined' !== typeof jest) {
905 + warnIfNotCurrentlyActingEffectsInDEV(
906 + ((currentlyRenderingFiber: any): Fiber),
907 + );
908 + }
909 + }
910 return mountEffectImpl(
911 UpdateEffect | PassiveEffect,
912 UnmountPassive | MountPassive,
@@ -910,6 +919,14 @@ function updateEffect(
919 create: () => (() => void) | void,
920 deps: Array<mixed> | void | null,
921 ): void {
922 + if (__DEV__) {
923 + // $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests
924 + if ('undefined' !== typeof jest) {
925 + warnIfNotCurrentlyActingEffectsInDEV(
926 + ((currentlyRenderingFiber: any): Fiber),
927 + );
928 + }
929 + }
930 return updateEffectImpl(
931 UpdateEffect | PassiveEffect,
932 UnmountPassive | MountPassive,
packages/react-reconciler/src/ReactFiberWorkLoop.js
+23
@@ -2443,6 +2443,29 @@ export function warnIfNotScopedWithMatchingAct(fiber: Fiber): void {
2443 }
2444 }
2445
2446 +export function warnIfNotCurrentlyActingEffectsInDEV(fiber: Fiber): void {
2447 + if (__DEV__) {
2448 + if (ReactCurrentActingRendererSigil.current !== ReactActingRendererSigil) {
2449 + warningWithoutStack(
2450 + false,
2451 + 'An update to %s ran an effect, but was not wrapped in act(...).\n\n' +
2452 + 'When testing, code that causes React state updates should be ' +
2453 + 'wrapped into act(...):\n\n' +
2454 + 'act(() => {\n' +
2455 + ' /* fire events that update state */\n' +
2456 + '});\n' +
2457 + '/* assert on the output */\n\n' +
2458 + "This ensures that you're testing the behavior the user would see " +
2459 + 'in the browser.' +
2460 + ' Learn more at https://fb.me/react-wrap-tests-with-act' +
2461 + '%s',
2462 + getComponentName(fiber.type),
2463 + getStackByFiberInDevAndProd(fiber),
2464 + );
2465 + }
2466 + }
2467 +}
2468 +
2469 function warnIfNotCurrentlyActingUpdatesInDEV(fiber: Fiber): void {
2470 if (__DEV__) {
2471 if (
packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js
+58 -21
@@ -652,7 +652,9 @@ describe('ReactHooks', () => {
652 }
653
654 expect(() => {
655 - ReactTestRenderer.create(<App deps={'hello'} />);
655 + act(() => {
656 + ReactTestRenderer.create(<App deps={'hello'} />);
657 + });
658 }).toWarnDev([
659 'Warning: useEffect received a final argument that is not an array (instead, received `string`). ' +
660 'When specified, the final argument must be an array.',
@@ -664,7 +666,9 @@ describe('ReactHooks', () => {
666 'When specified, the final argument must be an array.',
667 ]);
668 expect(() => {
667 - ReactTestRenderer.create(<App deps={100500} />);
669 + act(() => {
670 + ReactTestRenderer.create(<App deps={100500} />);
671 + });
672 }).toWarnDev([
673 'Warning: useEffect received a final argument that is not an array (instead, received `number`). ' +
674 'When specified, the final argument must be an array.',
@@ -676,7 +680,9 @@ describe('ReactHooks', () => {
680 'When specified, the final argument must be an array.',
681 ]);
682 expect(() => {
679 - ReactTestRenderer.create(<App deps={{}} />);
683 + act(() => {
684 + ReactTestRenderer.create(<App deps={{}} />);
685 + });
686 }).toWarnDev([
687 'Warning: useEffect received a final argument that is not an array (instead, received `object`). ' +
688 'When specified, the final argument must be an array.',
@@ -687,9 +693,12 @@ describe('ReactHooks', () => {
693 'Warning: useCallback received a final argument that is not an array (instead, received `object`). ' +
694 'When specified, the final argument must be an array.',
695 ]);
690 - ReactTestRenderer.create(<App deps={[]} />);
691 - ReactTestRenderer.create(<App deps={null} />);
692 - ReactTestRenderer.create(<App deps={undefined} />);
696 +
697 + act(() => {
698 + ReactTestRenderer.create(<App deps={[]} />);
699 + ReactTestRenderer.create(<App deps={null} />);
700 + ReactTestRenderer.create(<App deps={undefined} />);
701 + });
702 });
703
704 it('warns if deps is not an array for useImperativeHandle', () => {
@@ -980,8 +989,11 @@ describe('ReactHooks', () => {
989 return null;
990 }
991
983 - const root = ReactTestRenderer.create(<App />);
984 - expect(() => root.update(<App />)).toThrow(
992 + expect(() => {
993 + act(() => {
994 + ReactTestRenderer.create(<App />);
995 + });
996 + }).toThrow(
997 // The exact message doesn't matter, just make sure we don't allow this
998 'Context can only be read while React is rendering',
999 );
@@ -1173,7 +1185,9 @@ describe('ReactHooks', () => {
1185 }
1186 // Verify it doesn't think we're still inside a Hook.
1187 // Should have no warnings.
1176 - ReactTestRenderer.create(<Valid />);
1188 + act(() => {
1189 + ReactTestRenderer.create(<Valid />);
1190 + });
1191
1192 // Verify warnings don't get permanently disabled.
1193 expect(() => {
@@ -1499,10 +1513,15 @@ describe('ReactHooks', () => {
1513 return null;
1514 /* eslint-enable no-unused-vars */
1515 }
1502 - let root = ReactTestRenderer.create(<App update={false} />);
1516 + let root;
1517 + act(() => {
1518 + root = ReactTestRenderer.create(<App update={false} />);
1519 + });
1520 expect(() => {
1521 try {
1505 - root.update(<App update={true} />);
1522 + act(() => {
1523 + root.update(<App update={true} />);
1524 + });
1525 } catch (error) {
1526 // Swapping certain types of hooks will cause runtime errors.
1527 // This is okay as far as this test is concerned.
@@ -1521,7 +1540,9 @@ describe('ReactHooks', () => {
1540
1541 // further warnings for this component are silenced
1542 try {
1524 - root.update(<App update={false} />);
1543 + act(() => {
1544 + root.update(<App update={false} />);
1545 + });
1546 } catch (error) {
1547 // Swapping certain types of hooks will cause runtime errors.
1548 // This is okay as far as this test is concerned.
@@ -1542,10 +1563,16 @@ describe('ReactHooks', () => {
1563 return null;
1564 /* eslint-enable no-unused-vars */
1565 }
1545 - let root = ReactTestRenderer.create(<App update={false} />);
1566 + let root;
1567 + act(() => {
1568 + root = ReactTestRenderer.create(<App update={false} />);
1569 + });
1570 +
1571 expect(() => {
1572 try {
1548 - root.update(<App update={true} />);
1573 + act(() => {
1574 + root.update(<App update={true} />);
1575 + });
1576 } catch (error) {
1577 // Swapping certain types of hooks will cause runtime errors.
1578 // This is okay as far as this test is concerned.
@@ -1604,9 +1631,15 @@ describe('ReactHooks', () => {
1631 return null;
1632 /* eslint-enable no-unused-vars */
1633 }
1607 - let root = ReactTestRenderer.create(<App update={false} />);
1634 + let root;
1635 + act(() => {
1636 + root = ReactTestRenderer.create(<App update={false} />);
1637 + });
1638 +
1639 expect(() => {
1609 - root.update(<App update={true} />);
1640 + act(() => {
1641 + root.update(<App update={true} />);
1642 + });
1643 }).toThrow('Rendered fewer hooks than expected.');
1644 });
1645 });
@@ -1767,6 +1800,7 @@ describe('ReactHooks', () => {
1800 globalListener();
1801 globalListener();
1802 }).toWarnDev([
1803 + 'An update to C ran an effect',
1804 'An update to C inside a test was not wrapped in act',
1805 'An update to C inside a test was not wrapped in act',
1806 // Note: should *not* warn about updates on unmounted component.
@@ -1908,11 +1942,14 @@ describe('ReactHooks', () => {
1942 return 'Throw!';
1943 }
1944
1911 - const root = ReactTestRenderer.create(
1912 - <ErrorBoundary>
1913 - <Thrower />
1914 - </ErrorBoundary>,
1915 - );
1945 + let root;
1946 + act(() => {
1947 + root = ReactTestRenderer.create(
1948 + <ErrorBoundary>
1949 + <Thrower />
1950 + </ErrorBoundary>,
1951 + );
1952 + });
1953
1954 expect(root).toMatchRenderedOutput('Throw!');
1955 act(() => setShouldThrow(true));
packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js
+430 -349
@@ -622,21 +622,25 @@ describe('ReactHooksWithNoopRenderer', () => {
622 });
623 return <Text text={'Count: ' + props.count} />;
624 }
625 - ReactNoop.render(<Counter count={0} />, () =>
626 - Scheduler.yieldValue('Sync effect'),
627 - );
628 - expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
629 - expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
630 - // Effects are deferred until after the commit
631 - expect(Scheduler).toFlushAndYield(['Passive effect [0]']);
625 + act(() => {
626 + ReactNoop.render(<Counter count={0} />, () =>
627 + Scheduler.yieldValue('Sync effect'),
628 + );
629 + expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
630 + expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
631 + // Effects are deferred until after the commit
632 + expect(Scheduler).toFlushAndYield(['Passive effect [0]']);
633 + });
634
633 - ReactNoop.render(<Counter count={1} />, () =>
634 - Scheduler.yieldValue('Sync effect'),
635 - );
636 - expect(Scheduler).toFlushAndYieldThrough(['Count: 1', 'Sync effect']);
637 - expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
638 - // Effects are deferred until after the commit
639 - expect(Scheduler).toFlushAndYield(['Passive effect [1]']);
635 + act(() => {
636 + ReactNoop.render(<Counter count={1} />, () =>
637 + Scheduler.yieldValue('Sync effect'),
638 + );
639 + expect(Scheduler).toFlushAndYieldThrough(['Count: 1', 'Sync effect']);
640 + expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
641 + // Effects are deferred until after the commit
642 + expect(Scheduler).toFlushAndYield(['Passive effect [1]']);
643 + });
644 });
645
646 it('flushes passive effects even with sibling deletions', () => {
@@ -653,25 +657,24 @@ describe('ReactHooksWithNoopRenderer', () => {
657 return <Text text="Passive" />;
658 }
659 let passive = <PassiveEffect key="p" />;
656 - ReactNoop.render([<LayoutEffect key="l" />, passive]);
657 - expect(Scheduler).toFlushAndYieldThrough([
658 - 'Layout',
659 - 'Passive',
660 - 'Layout effect',
661 - ]);
662 - expect(ReactNoop.getChildren()).toEqual([
663 - span('Layout'),
664 - span('Passive'),
665 - ]);
666 -
667 - // Destroying the first child shouldn't prevent the passive effect from
668 - // being executed
669 - ReactNoop.render([passive]);
670 - expect(Scheduler).toFlushAndYield(['Passive effect']);
671 - expect(ReactNoop.getChildren()).toEqual([span('Passive')]);
672 -
673 - // (No effects are left to flush.)
674 - ReactNoop.flushPassiveEffects();
660 + act(() => {
661 + ReactNoop.render([<LayoutEffect key="l" />, passive]);
662 + expect(Scheduler).toFlushAndYieldThrough([
663 + 'Layout',
664 + 'Passive',
665 + 'Layout effect',
666 + ]);
667 + expect(ReactNoop.getChildren()).toEqual([
668 + span('Layout'),
669 + span('Passive'),
670 + ]);
671 + // Destroying the first child shouldn't prevent the passive effect from
672 + // being executed
673 + ReactNoop.render([passive]);
674 + expect(Scheduler).toFlushAndYield(['Passive effect']);
675 + expect(ReactNoop.getChildren()).toEqual([span('Passive')]);
676 + });
677 + // exiting act calls flushPassiveEffects(), but there are none left to flush.
678 expect(Scheduler).toHaveYielded([]);
679 });
680
@@ -728,18 +731,20 @@ describe('ReactHooksWithNoopRenderer', () => {
731 });
732 return <Text text="Layout" />;
733 }
731 - ReactNoop.render([<PassiveEffect key="p" />, <LayoutEffect key="l" />]);
732 - expect(Scheduler).toFlushAndYield([
733 - 'Passive',
734 - 'Layout',
735 - 'Layout effect',
736 - 'Passive effect',
737 - 'New Root',
738 - ]);
739 - expect(ReactNoop.getChildren()).toEqual([
740 - span('Passive'),
741 - span('Layout'),
742 - ]);
734 + act(() => {
735 + ReactNoop.render([<PassiveEffect key="p" />, <LayoutEffect key="l" />]);
736 + expect(Scheduler).toFlushAndYield([
737 + 'Passive',
738 + 'Layout',
739 + 'Layout effect',
740 + 'Passive effect',
741 + 'New Root',
742 + ]);
743 + expect(ReactNoop.getChildren()).toEqual([
744 + span('Passive'),
745 + span('Layout'),
746 + ]);
747 + });
748 });
749
750 it(
@@ -762,25 +767,25 @@ describe('ReactHooksWithNoopRenderer', () => {
767 });
768 return <Text text={props.count} />;
769 }
765 - ReactNoop.render(<Counter count={0} />, () =>
766 - Scheduler.yieldValue('Sync effect'),
767 - );
768 - expect(Scheduler).toFlushAndYieldThrough([0, 'Sync effect']);
769 - expect(ReactNoop.getChildren()).toEqual([span(0)]);
770 -
771 - // Before the effects have a chance to flush, schedule another update
772 - ReactNoop.render(<Counter count={1} />, () =>
773 - Scheduler.yieldValue('Sync effect'),
774 - );
775 - expect(Scheduler).toFlushAndYieldThrough([
776 - // The previous effect flushes before the reconciliation
777 - 'Committed state when effect was fired: 0',
778 - 1,
779 - 'Sync effect',
780 - ]);
781 - expect(ReactNoop.getChildren()).toEqual([span(1)]);
770 + act(() => {
771 + ReactNoop.render(<Counter count={0} />, () =>
772 + Scheduler.yieldValue('Sync effect'),
773 + );
774 + expect(Scheduler).toFlushAndYieldThrough([0, 'Sync effect']);
775 + expect(ReactNoop.getChildren()).toEqual([span(0)]);
776 + // Before the effects have a chance to flush, schedule another update
777 + ReactNoop.render(<Counter count={1} />, () =>
778 + Scheduler.yieldValue('Sync effect'),
779 + );
780 + expect(Scheduler).toFlushAndYieldThrough([
781 + // The previous effect flushes before the reconciliation
782 + 'Committed state when effect was fired: 0',
783 + 1,
784 + 'Sync effect',
785 + ]);
786 + expect(ReactNoop.getChildren()).toEqual([span(1)]);
787 + });
788
783 - ReactNoop.flushPassiveEffects();
789 expect(Scheduler).toHaveYielded([
790 'Committed state when effect was fired: 1',
791 ]);
@@ -799,26 +804,30 @@ describe('ReactHooksWithNoopRenderer', () => {
804 );
805 return <Text text={'Count: ' + count} />;
806 }
802 - ReactNoop.render(<Counter count={0} />, () =>
803 - Scheduler.yieldValue('Sync effect'),
804 - );
805 - expect(Scheduler).toFlushAndYieldThrough([
806 - 'Count: (empty)',
807 - 'Sync effect',
808 - ]);
809 - expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]);
810 - ReactNoop.flushPassiveEffects();
811 - expect(Scheduler).toHaveYielded(['Schedule update [0]']);
812 - expect(Scheduler).toFlushAndYield(['Count: 0']);
807 + act(() => {
808 + ReactNoop.render(<Counter count={0} />, () =>
809 + Scheduler.yieldValue('Sync effect'),
810 + );
811 + expect(Scheduler).toFlushAndYieldThrough([
812 + 'Count: (empty)',
813 + 'Sync effect',
814 + ]);
815 + expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]);
816 + ReactNoop.flushPassiveEffects();
817 + expect(Scheduler).toHaveYielded(['Schedule update [0]']);
818 + expect(Scheduler).toFlushAndYield(['Count: 0']);
819 + });
820
814 - ReactNoop.render(<Counter count={1} />, () =>
815 - Scheduler.yieldValue('Sync effect'),
816 - );
817 - expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
818 - expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
819 - ReactNoop.flushPassiveEffects();
820 - expect(Scheduler).toHaveYielded(['Schedule update [1]']);
821 - expect(Scheduler).toFlushAndYield(['Count: 1']);
821 + act(() => {
822 + ReactNoop.render(<Counter count={1} />, () =>
823 + Scheduler.yieldValue('Sync effect'),
824 + );
825 + expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
826 + expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
827 + ReactNoop.flushPassiveEffects();
828 + expect(Scheduler).toHaveYielded(['Schedule update [1]']);
829 + expect(Scheduler).toFlushAndYield(['Count: 1']);
830 + });
831 });
832
833 it('updates have async priority even if effects are flushed early', () => {
@@ -833,32 +842,33 @@ describe('ReactHooksWithNoopRenderer', () => {
842 );
843 return <Text text={'Count: ' + count} />;
844 }
836 - ReactNoop.render(<Counter count={0} />, () =>
837 - Scheduler.yieldValue('Sync effect'),
838 - );
839 - expect(Scheduler).toFlushAndYieldThrough([
840 - 'Count: (empty)',
841 - 'Sync effect',
842 - ]);
843 - expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]);
844 -
845 - // Rendering again should flush the previous commit's effects
846 - ReactNoop.render(<Counter count={1} />, () =>
847 - Scheduler.yieldValue('Sync effect'),
848 - );
849 - expect(Scheduler).toFlushAndYieldThrough([
850 - 'Schedule update [0]',
851 - 'Count: 0',
852 - ]);
853 - expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]);
845 + act(() => {
846 + ReactNoop.render(<Counter count={0} />, () =>
847 + Scheduler.yieldValue('Sync effect'),
848 + );
849 + expect(Scheduler).toFlushAndYieldThrough([
850 + 'Count: (empty)',
851 + 'Sync effect',
852 + ]);
853 + expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]);
854
855 - expect(Scheduler).toFlushAndYieldThrough(['Sync effect']);
856 - expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
855 + // Rendering again should flush the previous commit's effects
856 + ReactNoop.render(<Counter count={1} />, () =>
857 + Scheduler.yieldValue('Sync effect'),
858 + );
859 + expect(Scheduler).toFlushAndYieldThrough([
860 + 'Schedule update [0]',
861 + 'Count: 0',
862 + ]);
863 + expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]);
864
858 - ReactNoop.flushPassiveEffects();
859 - expect(Scheduler).toHaveYielded(['Schedule update [1]']);
860 - expect(Scheduler).toFlushAndYield(['Count: 1']);
861 - expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
865 + expect(Scheduler).toFlushAndYieldThrough(['Sync effect']);
866 + expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
867 + ReactNoop.flushPassiveEffects();
868 + expect(Scheduler).toHaveYielded(['Schedule update [1]']);
869 + expect(Scheduler).toFlushAndYield(['Count: 1']);
870 + expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
871 + });
872 });
873
874 it('flushes passive effects when flushing discrete updates', () => {
@@ -873,15 +883,18 @@ describe('ReactHooksWithNoopRenderer', () => {
883 return <Text text={'Count: ' + count} />;
884 }
885
876 - ReactNoop.render(<Counter count={0} />, () =>
877 - Scheduler.yieldValue('Sync effect'),
878 - );
879 - expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
880 - expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
886 + // we explicitly wait for missing act() warnings here since
887 + // it's a lot harder to simulate this condition inside an act scope
888 + expect(() => {
889 + ReactNoop.render(<Counter count={0} />, () =>
890 + Scheduler.yieldValue('Sync effect'),
891 + );
892 + expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
893 + expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
894 + }).toWarnDev(['An update to Counter ran an effect']);
895
896 // A discrete event forces the passive effect to be flushed --
897 // updateCount(1) happens first, so 2 wins.
884 -
898 ReactNoop.flushDiscreteUpdates();
899 ReactNoop.discreteUpdates(() => {
900 // (use batchedUpdates to silence the act() warning)
@@ -890,7 +903,13 @@ describe('ReactHooksWithNoopRenderer', () => {
903 });
904 });
905 expect(Scheduler).toHaveYielded(['Will set count to 1']);
893 - expect(Scheduler).toFlushAndYield(['Count: 2']);
906 + expect(() => {
907 + expect(Scheduler).toFlushAndYield(['Count: 2']);
908 + }).toWarnDev([
909 + 'An update to Counter ran an effect',
910 + 'An update to Counter ran an effect',
911 + ]);
912 +
913 expect(ReactNoop.getChildren()).toEqual([span('Count: 2')]);
914 });
915
@@ -921,17 +940,21 @@ describe('ReactHooksWithNoopRenderer', () => {
940 }
941
942 const tracingEvent = {id: 0, name: 'hello', timestamp: 0};
924 - SchedulerTracing.unstable_trace(
925 - tracingEvent.name,
926 - tracingEvent.timestamp,
927 - () => {
928 - ReactNoop.render(<Counter count={0} />, () =>
929 - Scheduler.yieldValue('Sync effect'),
930 - );
931 - },
932 - );
933 - expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
934 - expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
943 + // we explicitly wait for missing act() warnings here since
944 + // it's a lot harder to simulate this condition inside an act scope
945 + expect(() => {
946 + SchedulerTracing.unstable_trace(
947 + tracingEvent.name,
948 + tracingEvent.timestamp,
949 + () => {
950 + ReactNoop.render(<Counter count={0} />, () =>
951 + Scheduler.yieldValue('Sync effect'),
952 + );
953 + },
954 + );
955 + expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
956 + expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
957 + }).toWarnDev(['An update to Counter ran an effect']);
958
959 expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(0);
960
@@ -939,11 +962,19 @@ describe('ReactHooksWithNoopRenderer', () => {
962 // updateCount(1) happens first, so 2 wins.
963 ReactNoop.flushDiscreteUpdates();
964 ReactNoop.discreteUpdates(() => {
942 - // use batchedUpdates to silence the act warning
943 - ReactNoop.batchedUpdates(() => _updateCount(2));
965 + // (use batchedUpdates to silence the act() warning)
966 + ReactNoop.batchedUpdates(() => {
967 + _updateCount(2);
968 + });
969 });
970 expect(Scheduler).toHaveYielded(['Will set count to 1']);
946 - expect(Scheduler).toFlushAndYield(['Count: 2']);
971 + expect(() => {
972 + expect(Scheduler).toFlushAndYield(['Count: 2']);
973 + }).toWarnDev([
974 + 'An update to Counter ran an effect',
975 + 'An update to Counter ran an effect',
976 + ]);
977 +
978 expect(ReactNoop.getChildren()).toEqual([span('Count: 2')]);
979
980 expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
@@ -971,12 +1002,14 @@ describe('ReactHooksWithNoopRenderer', () => {
1002 );
1003 return <Text text={'Count: ' + count} />;
1004 }
974 - ReactNoop.renderLegacySyncRoot(<Counter count={0} />);
975 - // Even in sync mode, effects are deferred until after paint
976 - expect(Scheduler).toHaveYielded(['Count: (empty)']);
977 - expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]);
978 - // Now fire the effects
979 - ReactNoop.flushPassiveEffects();
1005 + act(() => {
1006 + ReactNoop.renderLegacySyncRoot(<Counter count={0} />);
1007 + // Even in sync mode, effects are deferred until after paint
1008 + expect(Scheduler).toFlushAndYieldThrough(['Count: (empty)']);
1009 + expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]);
1010 + });
1011 +
1012 + // effects get fored on exiting act()
1013 // There were multiple updates, but there should only be a
1014 // single render
1015 expect(Scheduler).toHaveYielded(['Count: 0']);
@@ -998,18 +1031,19 @@ describe('ReactHooksWithNoopRenderer', () => {
1031 );
1032 return <Text text={'Count: ' + count} />;
1033 }
1001 - ReactNoop.render(<Counter count={0} />, () =>
1002 - Scheduler.yieldValue('Sync effect'),
1003 - );
1004 - expect(Scheduler).toFlushAndYieldThrough([
1005 - 'Count: (empty)',
1006 - 'Sync effect',
1007 - ]);
1008 - expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]);
1009 -
1010 - expect(() => {
1011 - ReactNoop.flushPassiveEffects();
1012 - }).toThrow('flushSync was called from inside a lifecycle method');
1034 + act(() => {
1035 + ReactNoop.render(<Counter count={0} />, () =>
1036 + Scheduler.yieldValue('Sync effect'),
1037 + );
1038 + expect(Scheduler).toFlushAndYieldThrough([
1039 + 'Count: (empty)',
1040 + 'Sync effect',
1041 + ]);
1042 + expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]);
1043 + expect(() => {
1044 + ReactNoop.flushPassiveEffects();
1045 + }).toThrow('flushSync was called from inside a lifecycle method');
1046 + });
1047 });
1048
1049 it('unmounts previous effect', () => {
@@ -1022,20 +1056,24 @@ describe('ReactHooksWithNoopRenderer', () => {
1056 });
1057 return <Text text={'Count: ' + props.count} />;
1058 }
1025 - ReactNoop.render(<Counter count={0} />, () =>
1026 - Scheduler.yieldValue('Sync effect'),
1027 - );
1028 - expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
1029 - expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1030 - ReactNoop.flushPassiveEffects();
1059 + act(() => {
1060 + ReactNoop.render(<Counter count={0} />, () =>
1061 + Scheduler.yieldValue('Sync effect'),
1062 + );
1063 + expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
1064 + expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1065 + });
1066 +
1067 expect(Scheduler).toHaveYielded(['Did create [0]']);
1068
1033 - ReactNoop.render(<Counter count={1} />, () =>
1034 - Scheduler.yieldValue('Sync effect'),
1035 - );
1036 - expect(Scheduler).toFlushAndYieldThrough(['Count: 1', 'Sync effect']);
1037 - expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1038 - ReactNoop.flushPassiveEffects();
1069 + act(() => {
1070 + ReactNoop.render(<Counter count={1} />, () =>
1071 + Scheduler.yieldValue('Sync effect'),
1072 + );
1073 + expect(Scheduler).toFlushAndYieldThrough(['Count: 1', 'Sync effect']);
1074 + expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1075 + });
1076 +
1077 expect(Scheduler).toHaveYielded(['Did destroy [0]', 'Did create [1]']);
1078 });
1079
@@ -1049,12 +1087,14 @@ describe('ReactHooksWithNoopRenderer', () => {
1087 });
1088 return <Text text={'Count: ' + props.count} />;
1089 }
1052 - ReactNoop.render(<Counter count={0} />, () =>
1053 - Scheduler.yieldValue('Sync effect'),
1054 - );
1055 - expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
1056 - expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1057 - ReactNoop.flushPassiveEffects();
1090 + act(() => {
1091 + ReactNoop.render(<Counter count={0} />, () =>
1092 + Scheduler.yieldValue('Sync effect'),
1093 + );
1094 + expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
1095 + expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1096 + });
1097 +
1098 expect(Scheduler).toHaveYielded(['Did create [0]']);
1099
1100 ReactNoop.render(null);
@@ -1072,20 +1112,24 @@ describe('ReactHooksWithNoopRenderer', () => {
1112 }, []);
1113 return <Text text={'Count: ' + props.count} />;
1114 }
1075 - ReactNoop.render(<Counter count={0} />, () =>
1076 - Scheduler.yieldValue('Sync effect'),
1077 - );
1078 - expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
1079 - expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1080 - ReactNoop.flushPassiveEffects();
1115 + act(() => {
1116 + ReactNoop.render(<Counter count={0} />, () =>
1117 + Scheduler.yieldValue('Sync effect'),
1118 + );
1119 + expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
1120 + expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1121 + });
1122 +
1123 expect(Scheduler).toHaveYielded(['Did create [0]']);
1124
1083 - ReactNoop.render(<Counter count={1} />, () =>
1084 - Scheduler.yieldValue('Sync effect'),
1085 - );
1086 - expect(Scheduler).toFlushAndYieldThrough(['Count: 1', 'Sync effect']);
1087 - expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1088 - ReactNoop.flushPassiveEffects();
1125 + act(() => {
1126 + ReactNoop.render(<Counter count={1} />, () =>
1127 + Scheduler.yieldValue('Sync effect'),
1128 + );
1129 + expect(Scheduler).toFlushAndYieldThrough(['Count: 1', 'Sync effect']);
1130 + expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1131 + });
1132 +
1133 expect(Scheduler).toHaveYielded([]);
1134
1135 ReactNoop.render(null);
@@ -1104,20 +1148,24 @@ describe('ReactHooksWithNoopRenderer', () => {
1148 useEffect(effect);
1149 return <Text text={'Count: ' + props.count} />;
1150 }
1107 - ReactNoop.render(<Counter count={0} />, () =>
1108 - Scheduler.yieldValue('Sync effect'),
1109 - );
1110 - expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
1111 - expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1112 - ReactNoop.flushPassiveEffects();
1151 + act(() => {
1152 + ReactNoop.render(<Counter count={0} />, () =>
1153 + Scheduler.yieldValue('Sync effect'),
1154 + );
1155 + expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
1156 + expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1157 + });
1158 +
1159 expect(Scheduler).toHaveYielded(['Did create']);
1160
1115 - ReactNoop.render(<Counter count={1} />, () =>
1116 - Scheduler.yieldValue('Sync effect'),
1117 - );
1118 - expect(Scheduler).toFlushAndYieldThrough(['Count: 1', 'Sync effect']);
1119 - expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1120 - ReactNoop.flushPassiveEffects();
1161 + act(() => {
1162 + ReactNoop.render(<Counter count={1} />, () =>
1163 + Scheduler.yieldValue('Sync effect'),
1164 + );
1165 + expect(Scheduler).toFlushAndYieldThrough(['Count: 1', 'Sync effect']);
1166 + expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1167 + });
1168 +
1169 expect(Scheduler).toHaveYielded(['Did destroy', 'Did create']);
1170
1171 ReactNoop.render(null);
@@ -1139,42 +1187,50 @@ describe('ReactHooksWithNoopRenderer', () => {
1187 );
1188 return <Text text={text} />;
1189 }
1142 - ReactNoop.render(<Counter label="Count" count={0} />, () =>
1143 - Scheduler.yieldValue('Sync effect'),
1144 - );
1145 - expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
1146 - ReactNoop.flushPassiveEffects();
1190 + act(() => {
1191 + ReactNoop.render(<Counter label="Count" count={0} />, () =>
1192 + Scheduler.yieldValue('Sync effect'),
1193 + );
1194 + expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
1195 + });
1196 +
1197 expect(Scheduler).toHaveYielded(['Did create [Count: 0]']);
1198 expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1199
1150 - ReactNoop.render(<Counter label="Count" count={1} />, () =>
1151 - Scheduler.yieldValue('Sync effect'),
1152 - );
1153 - // Count changed
1154 - expect(Scheduler).toFlushAndYieldThrough(['Count: 1', 'Sync effect']);
1155 - expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1156 - ReactNoop.flushPassiveEffects();
1200 + act(() => {
1201 + ReactNoop.render(<Counter label="Count" count={1} />, () =>
1202 + Scheduler.yieldValue('Sync effect'),
1203 + );
1204 + // Count changed
1205 + expect(Scheduler).toFlushAndYieldThrough(['Count: 1', 'Sync effect']);
1206 + expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1207 + });
1208 +
1209 expect(Scheduler).toHaveYielded([
1210 'Did destroy [Count: 0]',
1211 'Did create [Count: 1]',
1212 ]);
1213
1162 - ReactNoop.render(<Counter label="Count" count={1} />, () =>
1163 - Scheduler.yieldValue('Sync effect'),
1164 - );
1165 - // Nothing changed, so no effect should have fired
1166 - expect(Scheduler).toFlushAndYieldThrough(['Count: 1', 'Sync effect']);
1167 - ReactNoop.flushPassiveEffects();
1214 + act(() => {
1215 + ReactNoop.render(<Counter label="Count" count={1} />, () =>
1216 + Scheduler.yieldValue('Sync effect'),
1217 + );
1218 + // Nothing changed, so no effect should have fired
1219 + expect(Scheduler).toFlushAndYieldThrough(['Count: 1', 'Sync effect']);
1220 + });
1221 +
1222 expect(Scheduler).toHaveYielded([]);
1223 expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1224
1171 - ReactNoop.render(<Counter label="Total" count={1} />, () =>
1172 - Scheduler.yieldValue('Sync effect'),
1173 - );
1174 - // Label changed
1175 - expect(Scheduler).toFlushAndYieldThrough(['Total: 1', 'Sync effect']);
1176 - expect(ReactNoop.getChildren()).toEqual([span('Total: 1')]);
1177 - ReactNoop.flushPassiveEffects();
1225 + act(() => {
1226 + ReactNoop.render(<Counter label="Total" count={1} />, () =>
1227 + Scheduler.yieldValue('Sync effect'),
1228 + );
1229 + // Label changed
1230 + expect(Scheduler).toFlushAndYieldThrough(['Total: 1', 'Sync effect']);
1231 + expect(ReactNoop.getChildren()).toEqual([span('Total: 1')]);
1232 + });
1233 +
1234 expect(Scheduler).toHaveYielded([
1235 'Did destroy [Count: 1]',
1236 'Did create [Total: 1]',
@@ -1191,20 +1247,23 @@ describe('ReactHooksWithNoopRenderer', () => {
1247 });
1248 return <Text text={'Count: ' + props.count} />;
1249 }
1194 - ReactNoop.render(<Counter count={0} />, () =>
1195 - Scheduler.yieldValue('Sync effect'),
1196 - );
1197 - expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
1198 - expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1199 - ReactNoop.flushPassiveEffects();
1250 + act(() => {
1251 + ReactNoop.render(<Counter count={0} />, () =>
1252 + Scheduler.yieldValue('Sync effect'),
1253 + );
1254 + expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
1255 + expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1256 + });
1257 +
1258 expect(Scheduler).toHaveYielded(['Did commit 1 [0]', 'Did commit 2 [0]']);
1259
1202 - ReactNoop.render(<Counter count={1} />, () =>
1203 - Scheduler.yieldValue('Sync effect'),
1204 - );
1205 - expect(Scheduler).toFlushAndYieldThrough(['Count: 1', 'Sync effect']);
1206 - expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1207 - ReactNoop.flushPassiveEffects();
1260 + act(() => {
1261 + ReactNoop.render(<Counter count={1} />, () =>
1262 + Scheduler.yieldValue('Sync effect'),
1263 + );
1264 + expect(Scheduler).toFlushAndYieldThrough(['Count: 1', 'Sync effect']);
1265 + expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1266 + });
1267 expect(Scheduler).toHaveYielded(['Did commit 1 [1]', 'Did commit 2 [1]']);
1268 });
1269
@@ -1224,20 +1283,23 @@ describe('ReactHooksWithNoopRenderer', () => {
1283 });
1284 return <Text text={'Count: ' + props.count} />;
1285 }
1227 - ReactNoop.render(<Counter count={0} />, () =>
1228 - Scheduler.yieldValue('Sync effect'),
1229 - );
1230 - expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
1231 - expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1232 - ReactNoop.flushPassiveEffects();
1286 + act(() => {
1287 + ReactNoop.render(<Counter count={0} />, () =>
1288 + Scheduler.yieldValue('Sync effect'),
1289 + );
1290 + expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
1291 + expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1292 + });
1293 +
1294 expect(Scheduler).toHaveYielded(['Mount A [0]', 'Mount B [0]']);
1295
1235 - ReactNoop.render(<Counter count={1} />, () =>
1236 - Scheduler.yieldValue('Sync effect'),
1237 - );
1238 - expect(Scheduler).toFlushAndYieldThrough(['Count: 1', 'Sync effect']);
1239 - expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1240 - ReactNoop.flushPassiveEffects();
1296 + act(() => {
1297 + ReactNoop.render(<Counter count={1} />, () =>
1298 + Scheduler.yieldValue('Sync effect'),
1299 + );
1300 + expect(Scheduler).toFlushAndYieldThrough(['Count: 1', 'Sync effect']);
1301 + expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1302 + });
1303 expect(Scheduler).toHaveYielded([
1304 'Unmount A [0]',
1305 'Unmount B [0]',
@@ -1265,12 +1327,15 @@ describe('ReactHooksWithNoopRenderer', () => {
1327 });
1328 return <Text text={'Count: ' + props.count} />;
1329 }
1268 - ReactNoop.render(<Counter count={0} />, () =>
1269 - Scheduler.yieldValue('Sync effect'),
1270 - );
1271 - expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
1272 - expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1273 - expect(() => ReactNoop.flushPassiveEffects()).toThrow('Oops');
1330 + act(() => {
1331 + ReactNoop.render(<Counter count={0} />, () =>
1332 + Scheduler.yieldValue('Sync effect'),
1333 + );
1334 + expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
1335 + expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1336 + expect(() => ReactNoop.flushPassiveEffects()).toThrow('Oops');
1337 + });
1338 +
1339 expect(Scheduler).toHaveYielded([
1340 'Mount A [0]',
1341 'Oops!',
@@ -1301,31 +1366,35 @@ describe('ReactHooksWithNoopRenderer', () => {
1366 });
1367 return <Text text={'Count: ' + props.count} />;
1368 }
1304 - ReactNoop.render(<Counter count={0} />, () =>
1305 - Scheduler.yieldValue('Sync effect'),
1306 - );
1307 - expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
1308 - expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1309 - ReactNoop.flushPassiveEffects();
1310 - expect(Scheduler).toHaveYielded(['Mount A [0]', 'Mount B [0]']);
1369 + act(() => {
1370 + ReactNoop.render(<Counter count={0} />, () =>
1371 + Scheduler.yieldValue('Sync effect'),
1372 + );
1373 + expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
1374 + expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1375 + ReactNoop.flushPassiveEffects();
1376 + expect(Scheduler).toHaveYielded(['Mount A [0]', 'Mount B [0]']);
1377 + });
1378
1312 - // This update will trigger an errror
1313 - ReactNoop.render(<Counter count={1} />, () =>
1314 - Scheduler.yieldValue('Sync effect'),
1315 - );
1316 - expect(Scheduler).toFlushAndYieldThrough(['Count: 1', 'Sync effect']);
1317 - expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1318 - expect(() => ReactNoop.flushPassiveEffects()).toThrow('Oops');
1319 - expect(Scheduler).toHaveYielded([
1320 - 'Unmount A [0]',
1321 - 'Unmount B [0]',
1322 - 'Mount A [1]',
1323 - 'Oops!',
1324 - // Clean up effect A. There's no effect B to clean-up, because it
1325 - // never mounted.
1326 - 'Unmount A [1]',
1327 - ]);
1328 - expect(ReactNoop.getChildren()).toEqual([]);
1379 + act(() => {
1380 + // This update will trigger an errror
1381 + ReactNoop.render(<Counter count={1} />, () =>
1382 + Scheduler.yieldValue('Sync effect'),
1383 + );
1384 + expect(Scheduler).toFlushAndYieldThrough(['Count: 1', 'Sync effect']);
1385 + expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1386 + expect(() => ReactNoop.flushPassiveEffects()).toThrow('Oops');
1387 + expect(Scheduler).toHaveYielded([
1388 + 'Unmount A [0]',
1389 + 'Unmount B [0]',
1390 + 'Mount A [1]',
1391 + 'Oops!',
1392 + // Clean up effect A. There's no effect B to clean-up, because it
1393 + // never mounted.
1394 + 'Unmount A [1]',
1395 + ]);
1396 + expect(ReactNoop.getChildren()).toEqual([]);
1397 + });
1398 });
1399
1400 it('handles errors on unmount', () => {
@@ -1347,27 +1416,31 @@ describe('ReactHooksWithNoopRenderer', () => {
1416 });
1417 return <Text text={'Count: ' + props.count} />;
1418 }
1350 - ReactNoop.render(<Counter count={0} />, () =>
1351 - Scheduler.yieldValue('Sync effect'),
1352 - );
1353 - expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
1354 - expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1355 - ReactNoop.flushPassiveEffects();
1356 - expect(Scheduler).toHaveYielded(['Mount A [0]', 'Mount B [0]']);
1419 + act(() => {
1420 + ReactNoop.render(<Counter count={0} />, () =>
1421 + Scheduler.yieldValue('Sync effect'),
1422 + );
1423 + expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
1424 + expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1425 + ReactNoop.flushPassiveEffects();
1426 + expect(Scheduler).toHaveYielded(['Mount A [0]', 'Mount B [0]']);
1427 + });
1428
1358 - // This update will trigger an errror
1359 - ReactNoop.render(<Counter count={1} />, () =>
1360 - Scheduler.yieldValue('Sync effect'),
1361 - );
1362 - expect(Scheduler).toFlushAndYieldThrough(['Count: 1', 'Sync effect']);
1363 - expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1364 - expect(() => ReactNoop.flushPassiveEffects()).toThrow('Oops');
1365 - expect(Scheduler).toHaveYielded([
1366 - 'Oops!',
1367 - // B unmounts even though an error was thrown in the previous effect
1368 - 'Unmount B [0]',
1369 - ]);
1370 - expect(ReactNoop.getChildren()).toEqual([]);
1429 + act(() => {
1430 + // This update will trigger an errror
1431 + ReactNoop.render(<Counter count={1} />, () =>
1432 + Scheduler.yieldValue('Sync effect'),
1433 + );
1434 + expect(Scheduler).toFlushAndYieldThrough(['Count: 1', 'Sync effect']);
1435 + expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1436 + expect(() => ReactNoop.flushPassiveEffects()).toThrow('Oops');
1437 + expect(Scheduler).toHaveYielded([
1438 + 'Oops!',
1439 + // B unmounts even though an error was thrown in the previous effect
1440 + 'Unmount B [0]',
1441 + ]);
1442 + expect(ReactNoop.getChildren()).toEqual([]);
1443 + });
1444 });
1445
1446 it('works with memo', () => {
@@ -1470,27 +1543,27 @@ describe('ReactHooksWithNoopRenderer', () => {
1543 return null;
1544 }
1545
1473 - ReactNoop.render(<Counter count={0} />, () =>
1474 - Scheduler.yieldValue('Sync effect'),
1475 - );
1476 - expect(Scheduler).toFlushAndYieldThrough([
1477 - 'Mount layout [current: 0]',
1478 - 'Sync effect',
1479 - ]);
1480 - expect(committedText).toEqual('0');
1481 -
1482 - ReactNoop.render(<Counter count={1} />, () =>
1483 - Scheduler.yieldValue('Sync effect'),
1484 - );
1485 - expect(Scheduler).toFlushAndYieldThrough([
1486 - 'Mount normal [current: 0]',
1487 - 'Unmount layout [current: 0]',
1488 - 'Mount layout [current: 1]',
1489 - 'Sync effect',
1490 - ]);
1491 - expect(committedText).toEqual('1');
1546 + act(() => {
1547 + ReactNoop.render(<Counter count={0} />, () =>
1548 + Scheduler.yieldValue('Sync effect'),
1549 + );
1550 + expect(Scheduler).toFlushAndYieldThrough([
1551 + 'Mount layout [current: 0]',
1552 + 'Sync effect',
1553 + ]);
1554 + expect(committedText).toEqual('0');
1555 + ReactNoop.render(<Counter count={1} />, () =>
1556 + Scheduler.yieldValue('Sync effect'),
1557 + );
1558 + expect(Scheduler).toFlushAndYieldThrough([
1559 + 'Mount normal [current: 0]',
1560 + 'Unmount layout [current: 0]',
1561 + 'Mount layout [current: 1]',
1562 + 'Sync effect',
1563 + ]);
1564 + expect(committedText).toEqual('1');
1565 + });
1566
1493 - ReactNoop.flushPassiveEffects();
1567 expect(Scheduler).toHaveYielded([
1568 'Unmount normal [current: 1]',
1569 'Mount normal [current: 1]',
@@ -1684,8 +1757,10 @@ describe('ReactHooksWithNoopRenderer', () => {
1757 return null;
1758 }
1759
1687 - ReactNoop.render(<App />);
1688 - expect(Scheduler).toFlushAndYield([]);
1760 + act(() => {
1761 + ReactNoop.render(<App />);
1762 + });
1763 + expect(Scheduler).toHaveYielded([]);
1764
1765 ping(1);
1766 ping(2);
@@ -1963,28 +2038,32 @@ describe('ReactHooksWithNoopRenderer', () => {
2038 return null;
2039 }
2040
1966 - ReactNoop.render(<App showMore={false} />, () =>
1967 - Scheduler.yieldValue('Sync effect'),
1968 - );
1969 - expect(Scheduler).toFlushAndYieldThrough(['Sync effect']);
1970 - ReactNoop.flushPassiveEffects();
2041 + act(() => {
2042 + ReactNoop.render(<App showMore={false} />, () =>
2043 + Scheduler.yieldValue('Sync effect'),
2044 + );
2045 + expect(Scheduler).toFlushAndYieldThrough(['Sync effect']);
2046 + });
2047 +
2048 expect(Scheduler).toHaveYielded(['Mount A']);
2049
1973 - ReactNoop.render(<App showMore={true} />);
1974 - expect(() => {
2050 + act(() => {
2051 + ReactNoop.render(<App showMore={true} />);
2052 expect(() => {
1976 - expect(Scheduler).toFlushAndYield([]);
1977 - }).toThrow('Rendered more hooks than during the previous render');
1978 - }).toWarnDev([
1979 - 'Warning: React has detected a change in the order of Hooks called by App. ' +
1980 - 'This will lead to bugs and errors if not fixed. For more information, ' +
1981 - 'read the Rules of Hooks: https://fb.me/rules-of-hooks\n\n' +
1982 - ' Previous render Next render\n' +
1983 - ' ------------------------------------------------------\n' +
1984 - '1. useEffect useEffect\n' +
1985 - '2. undefined useEffect\n' +
1986 - ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n',
1987 - ]);
2053 + expect(() => {
2054 + expect(Scheduler).toFlushAndYield([]);
2055 + }).toThrow('Rendered more hooks than during the previous render');
2056 + }).toWarnDev([
2057 + 'Warning: React has detected a change in the order of Hooks called by App. ' +
2058 + 'This will lead to bugs and errors if not fixed. For more information, ' +
2059 + 'read the Rules of Hooks: https://fb.me/rules-of-hooks\n\n' +
2060 + ' Previous render Next render\n' +
2061 + ' ------------------------------------------------------\n' +
2062 + '1. useEffect useEffect\n' +
2063 + '2. undefined useEffect\n' +
2064 + ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n',
2065 + ]);
2066 + });
2067
2068 // Uncomment if/when we support this again
2069 // ReactNoop.flushPassiveEffects();
@@ -2026,15 +2105,17 @@ describe('ReactHooksWithNoopRenderer', () => {
2105 return count;
2106 }
2107
2029 - ReactNoop.render(<App />);
2030 - expect(Scheduler).toFlushAndYield([
2031 - 'Render: -1',
2032 - 'Effect: 1',
2033 - 'Reducer: 1',
2034 - 'Reducer: 1',
2035 - 'Render: 1',
2036 - ]);
2037 - expect(ReactNoop).toMatchRenderedOutput('1');
2108 + act(() => {
2109 + ReactNoop.render(<App />);
2110 + expect(Scheduler).toFlushAndYield([
2111 + 'Render: -1',
2112 + 'Effect: 1',
2113 + 'Reducer: 1',
2114 + 'Reducer: 1',
2115 + 'Render: 1',
2116 + ]);
2117 + expect(ReactNoop).toMatchRenderedOutput('1');
2118 + });
2119
2120 act(() => {
2121 setCounter(2);
@@ -2109,19 +2190,19 @@ describe('ReactHooksWithNoopRenderer', () => {
2190 return <Text text={'Count: ' + count} />;
2191 }
2192
2112 - ReactNoop.render(<Counter count={0} />, () =>
2113 - Scheduler.yieldValue('Sync effect'),
2114 - );
2115 - expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
2116 - expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
2117 -
2118 - // Enqueuing this update forces the passive effect to be flushed --
2119 - // updateCount(1) happens first, so 2 wins.
2120 - // (use batchedUpdates to silence the act() warning)
2121 - ReactNoop.batchedUpdates(() => _updateCount(2));
2122 - expect(Scheduler).toHaveYielded(['Will set count to 1']);
2123 - expect(Scheduler).toFlushAndYield(['Count: 2']);
2124 - expect(ReactNoop.getChildren()).toEqual([span('Count: 2')]);
2193 + act(() => {
2194 + ReactNoop.render(<Counter count={0} />, () =>
2195 + Scheduler.yieldValue('Sync effect'),
2196 + );
2197 + expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
2198 + expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
2199 + // Enqueuing this update forces the passive effect to be flushed --
2200 + // updateCount(1) happens first, so 2 wins.
2201 + act(() => _updateCount(2));
2202 + expect(Scheduler).toHaveYielded(['Will set count to 1']);
2203 + expect(Scheduler).toFlushAndYield(['Count: 2']);
2204 + expect(ReactNoop.getChildren()).toEqual([span('Count: 2')]);
2205 + });
2206 });
2207 });
2208 });
packages/react-reconciler/src/__tests__/ReactIncrementalScheduling-test.internal.js
+19 -19
@@ -111,30 +111,30 @@ describe('ReactIncrementalScheduling', () => {
111 expect(ReactNoop.getChildrenAsJSX('c')).toEqual('c:1');
112
113 // Schedule deferred work in the reverse order
114 - ReactNoop.batchedUpdates(() => {
114 + ReactNoop.act(() => {
115 ReactNoop.renderToRootWithID(<Text text="c:2" />, 'c');
116 ReactNoop.renderToRootWithID(<Text text="b:2" />, 'b');
117 - });
118 - // Ensure it starts in the order it was scheduled
119 - expect(Scheduler).toFlushAndYieldThrough(['c:2']);
117 + // Ensure it starts in the order it was scheduled
118 + expect(Scheduler).toFlushAndYieldThrough(['c:2']);
119 +
120 + expect(ReactNoop.getChildrenAsJSX('a')).toEqual('a:1');
121 + expect(ReactNoop.getChildrenAsJSX('b')).toEqual('b:1');
122 + expect(ReactNoop.getChildrenAsJSX('c')).toEqual('c:2');
123 + // Schedule last bit of work, it will get processed the last
124
121 - expect(ReactNoop.getChildrenAsJSX('a')).toEqual('a:1');
122 - expect(ReactNoop.getChildrenAsJSX('b')).toEqual('b:1');
123 - expect(ReactNoop.getChildrenAsJSX('c')).toEqual('c:2');
124 - // Schedule last bit of work, it will get processed the last
125 - ReactNoop.batchedUpdates(() => {
125 ReactNoop.renderToRootWithID(<Text text="a:2" />, 'a');
127 - });
128 - // Keep performing work in the order it was scheduled
129 - expect(Scheduler).toFlushAndYieldThrough(['b:2']);
130 - expect(ReactNoop.getChildrenAsJSX('a')).toEqual('a:1');
131 - expect(ReactNoop.getChildrenAsJSX('b')).toEqual('b:2');
132 - expect(ReactNoop.getChildrenAsJSX('c')).toEqual('c:2');
126
134 - expect(Scheduler).toFlushAndYieldThrough(['a:2']);
135 - expect(ReactNoop.getChildrenAsJSX('a')).toEqual('a:2');
136 - expect(ReactNoop.getChildrenAsJSX('b')).toEqual('b:2');
137 - expect(ReactNoop.getChildrenAsJSX('c')).toEqual('c:2');
127 + // Keep performing work in the order it was scheduled
128 + expect(Scheduler).toFlushAndYieldThrough(['b:2']);
129 + expect(ReactNoop.getChildrenAsJSX('a')).toEqual('a:1');
130 + expect(ReactNoop.getChildrenAsJSX('b')).toEqual('b:2');
131 + expect(ReactNoop.getChildrenAsJSX('c')).toEqual('c:2');
132 +
133 + expect(Scheduler).toFlushAndYieldThrough(['a:2']);
134 + expect(ReactNoop.getChildrenAsJSX('a')).toEqual('a:2');
135 + expect(ReactNoop.getChildrenAsJSX('b')).toEqual('b:2');
136 + expect(ReactNoop.getChildrenAsJSX('c')).toEqual('c:2');
137 + });
138 });
139
140 it('schedules sync updates when inside componentDidMount/Update', () => {
packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.internal.js
+156 -149
@@ -476,70 +476,74 @@ describe('ReactIncrementalUpdates', () => {
476
477 // First, as a sanity check, assert what happens when four low pri
478 // updates in separate batches are all flushed in the same callback
479 - ReactNoop.render(<App label="" />);
480 - Scheduler.advanceTime(1000);
481 - expect(Scheduler).toFlushAndYieldThrough(['Render: ']);
482 - interrupt();
483 -
484 - ReactNoop.render(<App label="he" />);
485 - Scheduler.advanceTime(1000);
486 - expect(Scheduler).toFlushAndYieldThrough(['Render: ']);
487 - interrupt();
488 -
489 - ReactNoop.render(<App label="hell" />);
490 - Scheduler.advanceTime(1000);
491 - expect(Scheduler).toFlushAndYieldThrough(['Render: ']);
492 - interrupt();
493 -
494 - ReactNoop.render(<App label="hello" />);
495 - Scheduler.advanceTime(1000);
496 - expect(Scheduler).toFlushAndYieldThrough(['Render: ']);
497 - interrupt();
498 -
499 - // Each update flushes in a separate commit.
500 - // Note: This isn't necessarily the ideal behavior. It might be better to
501 - // batch all of these updates together. The fact that they don't is an
502 - // implementation detail. The important part of this unit test is what
503 - // happens when they expire, in which case they really should be batched to
504 - // avoid blocking the main thread for a long time.
505 - expect(Scheduler).toFlushAndYield([
506 - 'Render: ',
507 - 'Commit: ',
508 - 'Render: he',
509 - 'Commit: he',
510 - 'Render: hell',
511 - 'Commit: hell',
512 - 'Render: hello',
513 - 'Commit: hello',
514 - ]);
479 + ReactNoop.act(() => {
480 + ReactNoop.render(<App label="" />);
481 + Scheduler.advanceTime(1000);
482 + expect(Scheduler).toFlushAndYieldThrough(['Render: ']);
483 + interrupt();
484 +
485 + ReactNoop.render(<App label="he" />);
486 + Scheduler.advanceTime(1000);
487 + expect(Scheduler).toFlushAndYieldThrough(['Render: ']);
488 + interrupt();
489 +
490 + ReactNoop.render(<App label="hell" />);
491 + Scheduler.advanceTime(1000);
492 + expect(Scheduler).toFlushAndYieldThrough(['Render: ']);
493 + interrupt();
494 +
495 + ReactNoop.render(<App label="hello" />);
496 + Scheduler.advanceTime(1000);
497 + expect(Scheduler).toFlushAndYieldThrough(['Render: ']);
498 + interrupt();
499 +
500 + // Each update flushes in a separate commit.
501 + // Note: This isn't necessarily the ideal behavior. It might be better to
502 + // batch all of these updates together. The fact that they don't is an
503 + // implementation detail. The important part of this unit test is what
504 + // happens when they expire, in which case they really should be batched to
505 + // avoid blocking the main thread for a long time.
506 + expect(Scheduler).toFlushAndYield([
507 + 'Render: ',
508 + 'Commit: ',
509 + 'Render: he',
510 + 'Commit: he',
511 + 'Render: hell',
512 + 'Commit: hell',
513 + 'Render: hello',
514 + 'Commit: hello',
515 + ]);
516 + });
517
516 - // Now do the same thing over again, but this time, expire all the updates
517 - // instead of flushing them normally.
518 - ReactNoop.render(<App label="" />);
519 - Scheduler.advanceTime(1000);
520 - expect(Scheduler).toFlushAndYieldThrough(['Render: ']);
521 - interrupt();
522 -
523 - ReactNoop.render(<App label="go" />);
524 - Scheduler.advanceTime(1000);
525 - expect(Scheduler).toFlushAndYieldThrough(['Render: ']);
526 - interrupt();
527 -
528 - ReactNoop.render(<App label="good" />);
529 - Scheduler.advanceTime(1000);
530 - expect(Scheduler).toFlushAndYieldThrough(['Render: ']);
531 - interrupt();
532 -
533 - ReactNoop.render(<App label="goodbye" />);
534 - Scheduler.advanceTime(1000);
535 - expect(Scheduler).toFlushAndYieldThrough(['Render: ']);
536 - interrupt();
537 -
538 - // All the updates should render and commit in a single batch.
539 - Scheduler.advanceTime(10000);
540 - expect(Scheduler).toHaveYielded(['Render: goodbye']);
541 - // Passive effect
542 - expect(Scheduler).toFlushAndYield(['Commit: goodbye']);
518 + ReactNoop.act(() => {
519 + // Now do the same thing over again, but this time, expire all the updates
520 + // instead of flushing them normally.
521 + ReactNoop.render(<App label="" />);
522 + Scheduler.advanceTime(1000);
523 + expect(Scheduler).toFlushAndYieldThrough(['Render: ']);
524 + interrupt();
525 +
526 + ReactNoop.render(<App label="go" />);
527 + Scheduler.advanceTime(1000);
528 + expect(Scheduler).toFlushAndYieldThrough(['Render: ']);
529 + interrupt();
530 +
531 + ReactNoop.render(<App label="good" />);
532 + Scheduler.advanceTime(1000);
533 + expect(Scheduler).toFlushAndYieldThrough(['Render: ']);
534 + interrupt();
535 +
536 + ReactNoop.render(<App label="goodbye" />);
537 + Scheduler.advanceTime(1000);
538 + expect(Scheduler).toFlushAndYieldThrough(['Render: ']);
539 + interrupt();
540 +
541 + // All the updates should render and commit in a single batch.
542 + Scheduler.advanceTime(10000);
543 + expect(Scheduler).toHaveYielded(['Render: goodbye']);
544 + // Passive effect
545 + expect(Scheduler).toFlushAndYield(['Commit: goodbye']);
546 + });
547 });
548
549 it('flushes all expired updates in a single batch across multiple roots', () => {
@@ -559,92 +563,95 @@ describe('ReactIncrementalUpdates', () => {
563 ReactNoop.renderToRootWithID(null, 'other-root');
564 });
565 }
566 + ReactNoop.act(() => {
567 + // First, as a sanity check, assert what happens when four low pri
568 + // updates in separate batches are all flushed in the same callback
569 + ReactNoop.renderToRootWithID(<App label="" />, 'a');
570 + ReactNoop.renderToRootWithID(<App label="" />, 'b');
571 + Scheduler.advanceTime(1000);
572 + expect(Scheduler).toFlushAndYieldThrough(['Render: ']);
573 + interrupt();
574 +
575 + ReactNoop.renderToRootWithID(<App label="he" />, 'a');
576 + ReactNoop.renderToRootWithID(<App label="he" />, 'b');
577 + Scheduler.advanceTime(1000);
578 + expect(Scheduler).toFlushAndYieldThrough(['Render: ']);
579 + interrupt();
580 +
581 + ReactNoop.renderToRootWithID(<App label="hell" />, 'a');
582 + ReactNoop.renderToRootWithID(<App label="hell" />, 'b');
583 + Scheduler.advanceTime(1000);
584 + expect(Scheduler).toFlushAndYieldThrough(['Render: ']);
585 + interrupt();
586 +
587 + ReactNoop.renderToRootWithID(<App label="hello" />, 'a');
588 + ReactNoop.renderToRootWithID(<App label="hello" />, 'b');
589 + Scheduler.advanceTime(1000);
590 + expect(Scheduler).toFlushAndYieldThrough(['Render: ']);
591 + interrupt();
592 +
593 + // Each update flushes in a separate commit.
594 + // Note: This isn't necessarily the ideal behavior. It might be better to
595 + // batch all of these updates together. The fact that they don't is an
596 + // implementation detail. The important part of this unit test is what
597 + // happens when they expire, in which case they really should be batched to
598 + // avoid blocking the main thread for a long time.
599 + expect(Scheduler).toFlushAndYield([
600 + 'Render: ',
601 + 'Commit: ',
602 + 'Render: ',
603 + 'Commit: ',
604 + 'Render: he',
605 + 'Commit: he',
606 + 'Render: he',
607 + 'Commit: he',
608 + 'Render: hell',
609 + 'Commit: hell',
610 + 'Render: hell',
611 + 'Commit: hell',
612 + 'Render: hello',
613 + 'Commit: hello',
614 + 'Render: hello',
615 + 'Commit: hello',
616 + ]);
617 + });
618
563 - // First, as a sanity check, assert what happens when four low pri
564 - // updates in separate batches are all flushed in the same callback
565 - ReactNoop.renderToRootWithID(<App label="" />, 'a');
566 - ReactNoop.renderToRootWithID(<App label="" />, 'b');
567 - Scheduler.advanceTime(1000);
568 - expect(Scheduler).toFlushAndYieldThrough(['Render: ']);
569 - interrupt();
570 -
571 - ReactNoop.renderToRootWithID(<App label="he" />, 'a');
572 - ReactNoop.renderToRootWithID(<App label="he" />, 'b');
573 - Scheduler.advanceTime(1000);
574 - expect(Scheduler).toFlushAndYieldThrough(['Render: ']);
575 - interrupt();
576 -
577 - ReactNoop.renderToRootWithID(<App label="hell" />, 'a');
578 - ReactNoop.renderToRootWithID(<App label="hell" />, 'b');
579 - Scheduler.advanceTime(1000);
580 - expect(Scheduler).toFlushAndYieldThrough(['Render: ']);
581 - interrupt();
582 -
583 - ReactNoop.renderToRootWithID(<App label="hello" />, 'a');
584 - ReactNoop.renderToRootWithID(<App label="hello" />, 'b');
585 - Scheduler.advanceTime(1000);
586 - expect(Scheduler).toFlushAndYieldThrough(['Render: ']);
587 - interrupt();
588 -
589 - // Each update flushes in a separate commit.
590 - // Note: This isn't necessarily the ideal behavior. It might be better to
591 - // batch all of these updates together. The fact that they don't is an
592 - // implementation detail. The important part of this unit test is what
593 - // happens when they expire, in which case they really should be batched to
594 - // avoid blocking the main thread for a long time.
595 - expect(Scheduler).toFlushAndYield([
596 - 'Render: ',
597 - 'Commit: ',
598 - 'Render: ',
599 - 'Commit: ',
600 - 'Render: he',
601 - 'Commit: he',
602 - 'Render: he',
603 - 'Commit: he',
604 - 'Render: hell',
605 - 'Commit: hell',
606 - 'Render: hell',
607 - 'Commit: hell',
608 - 'Render: hello',
609 - 'Commit: hello',
610 - 'Render: hello',
611 - 'Commit: hello',
612 - ]);
613 -
614 - // Now do the same thing over again, but this time, expire all the updates
615 - // instead of flushing them normally.
616 - ReactNoop.renderToRootWithID(<App label="" />, 'a');
617 - ReactNoop.renderToRootWithID(<App label="" />, 'b');
618 - Scheduler.advanceTime(1000);
619 - expect(Scheduler).toFlushAndYieldThrough(['Render: ']);
620 - interrupt();
621 -
622 - ReactNoop.renderToRootWithID(<App label="go" />, 'a');
623 - ReactNoop.renderToRootWithID(<App label="go" />, 'b');
624 - Scheduler.advanceTime(1000);
625 - expect(Scheduler).toFlushAndYieldThrough(['Render: ']);
626 - interrupt();
627 -
628 - ReactNoop.renderToRootWithID(<App label="good" />, 'a');
629 - ReactNoop.renderToRootWithID(<App label="good" />, 'b');
630 - Scheduler.advanceTime(1000);
631 - expect(Scheduler).toFlushAndYieldThrough(['Render: ']);
632 - interrupt();
633 -
634 - ReactNoop.renderToRootWithID(<App label="goodbye" />, 'a');
635 - ReactNoop.renderToRootWithID(<App label="goodbye" />, 'b');
636 - Scheduler.advanceTime(1000);
637 - expect(Scheduler).toFlushAndYieldThrough(['Render: ']);
638 - interrupt();
639 -
640 - // All the updates should render and commit in a single batch.
641 - Scheduler.advanceTime(10000);
642 - expect(Scheduler).toHaveYielded([
643 - 'Render: goodbye',
644 - 'Commit: goodbye',
645 - 'Render: goodbye',
646 - ]);
647 - // Passive effect
648 - expect(Scheduler).toFlushAndYield(['Commit: goodbye']);
619 + ReactNoop.act(() => {
620 + // Now do the same thing over again, but this time, expire all the updates
621 + // instead of flushing them normally.
622 + ReactNoop.renderToRootWithID(<App label="" />, 'a');
623 + ReactNoop.renderToRootWithID(<App label="" />, 'b');
624 + Scheduler.advanceTime(1000);
625 + expect(Scheduler).toFlushAndYieldThrough(['Render: ']);
626 + interrupt();
627 +
628 + ReactNoop.renderToRootWithID(<App label="go" />, 'a');
629 + ReactNoop.renderToRootWithID(<App label="go" />, 'b');
630 + Scheduler.advanceTime(1000);
631 + expect(Scheduler).toFlushAndYieldThrough(['Render: ']);
632 + interrupt();
633 +
634 + ReactNoop.renderToRootWithID(<App label="good" />, 'a');
635 + ReactNoop.renderToRootWithID(<App label="good" />, 'b');
636 + Scheduler.advanceTime(1000);
637 + expect(Scheduler).toFlushAndYieldThrough(['Render: ']);
638 + interrupt();
639 +
640 + ReactNoop.renderToRootWithID(<App label="goodbye" />, 'a');
641 + ReactNoop.renderToRootWithID(<App label="goodbye" />, 'b');
642 + Scheduler.advanceTime(1000);
643 + expect(Scheduler).toFlushAndYieldThrough(['Render: ']);
644 + interrupt();
645 +
646 + // All the updates should render and commit in a single batch.
647 + Scheduler.advanceTime(10000);
648 + expect(Scheduler).toHaveYielded([
649 + 'Render: goodbye',
650 + 'Commit: goodbye',
651 + 'Render: goodbye',
652 + ]);
653 + // Passive effect
654 + expect(Scheduler).toFlushAndYield(['Commit: goodbye']);
655 + });
656 });
657 });
packages/react-reconciler/src/__tests__/ReactSchedulerIntegration-test.internal.js
+13 -12
@@ -133,21 +133,22 @@ describe('ReactSchedulerIntegration', () => {
133 });
134 return null;
135 }
136 + ReactNoop.act(() => {
137 + ReactNoop.render(<ReadPriority />);
138 + expect(Scheduler).toFlushAndYield([
139 + 'Render priority: Normal',
140 + 'Passive priority: Normal',
141 + ]);
142
137 - ReactNoop.render(<ReadPriority />);
138 - expect(Scheduler).toFlushAndYield([
139 - 'Render priority: Normal',
140 - 'Passive priority: Normal',
141 - ]);
143 + runWithPriority(UserBlockingPriority, () => {
144 + ReactNoop.render(<ReadPriority />);
145 + });
146
143 - runWithPriority(UserBlockingPriority, () => {
144 - ReactNoop.render(<ReadPriority />);
147 + expect(Scheduler).toFlushAndYield([
148 + 'Render priority: UserBlocking',
149 + 'Passive priority: UserBlocking',
150 + ]);
151 });
146 -
147 - expect(Scheduler).toFlushAndYield([
148 - 'Render priority: UserBlocking',
149 - 'Passive priority: UserBlocking',
150 - ]);
152 });
153
154 it('after completing a level of work, infers priority of the next batch based on its expiration time', () => {
packages/react-refresh/src/__tests__/ReactFresh-test.js
+21 -16
@@ -2676,14 +2676,16 @@ describe('ReactFresh', () => {
2676 expect(container.firstChild.nextSibling.nextSibling).toBe(secondP);
2677
2678 // Perform a hot update that fixes the error.
2679 - patch(() => {
2680 - function Hello() {
2681 - const [x] = React.useState('');
2682 - React.useEffect(() => {}, []); // Removes the bad effect code.
2683 - x.slice(); // Doesn't throw initially.
2684 - return <h1>Fixed!</h1>;
2685 - }
2686 - $RefreshReg$(Hello, 'Hello');
2679 + act(() => {
2680 + patch(() => {
2681 + function Hello() {
2682 + const [x] = React.useState('');
2683 + React.useEffect(() => {}, []); // Removes the bad effect code.
2684 + x.slice(); // Doesn't throw initially.
2685 + return <h1>Fixed!</h1>;
2686 + }
2687 + $RefreshReg$(Hello, 'Hello');
2688 + });
2689 });
2690
2691 // This should remount the error boundary (but not anything above it).
@@ -2693,15 +2695,18 @@ describe('ReactFresh', () => {
2695
2696 // Verify next hot reload doesn't remount anything.
2697 const helloNode = container.firstChild.nextSibling;
2696 - patch(() => {
2697 - function Hello() {
2698 - const [x] = React.useState('');
2699 - React.useEffect(() => {}, []);
2700 - x.slice();
2701 - return <h1>Nice.</h1>;
2702 - }
2703 - $RefreshReg$(Hello, 'Hello');
2698 + act(() => {
2699 + patch(() => {
2700 + function Hello() {
2701 + const [x] = React.useState('');
2702 + React.useEffect(() => {}, []);
2703 + x.slice();
2704 + return <h1>Nice.</h1>;
2705 + }
2706 + $RefreshReg$(Hello, 'Hello');
2707 + });
2708 });
2709 +
2710 expect(container.firstChild.nextSibling).toBe(helloNode);
2711 expect(helloNode.textContent).toBe('Nice.');
2712 }
packages/react/src/__tests__/ReactDOMTracing-test.internal.js
+74 -74
@@ -104,34 +104,32 @@ describe('ReactDOMTracing', () => {
104 const root = ReactDOM.unstable_createRoot(container);
105 SchedulerTracing.unstable_trace('initialization', 0, () => {
106 interaction = Array.from(SchedulerTracing.unstable_getCurrent())[0];
107 + TestUtils.act(() => {
108 + root.render(
109 + <React.Profiler id="test" onRender={onRender}>
110 + <App />
111 + </React.Profiler>,
112 + );
113 + expect(onInteractionTraced).toHaveBeenCalledTimes(1);
114 + expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction(
115 + interaction,
116 + );
117 + expect(Scheduler).toFlushAndYieldThrough(['App', 'App:mount']);
118 + expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
119 + expect(onRender).toHaveBeenCalledTimes(1);
120 + expect(onRender).toHaveLastRenderedWithInteractions(
121 + new Set([interaction]),
122 + );
123 + expect(Scheduler).toFlushAndYieldThrough(['Child', 'Child:mount']);
124 + expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
125 + expect(onRender).toHaveBeenCalledTimes(2);
126 + expect(onRender).toHaveLastRenderedWithInteractions(
127 + new Set([interaction]),
128 + );
129
108 - root.render(
109 - <React.Profiler id="test" onRender={onRender}>
110 - <App />
111 - </React.Profiler>,
112 - );
130 + expect(Scheduler).toFlushAndYield(['Child', 'Child:update']);
131 + });
132 });
114 -
115 - expect(onInteractionTraced).toHaveBeenCalledTimes(1);
116 - expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction(
117 - interaction,
118 - );
119 -
120 - expect(Scheduler).toFlushAndYieldThrough(['App', 'App:mount']);
121 - expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
122 - expect(onRender).toHaveBeenCalledTimes(1);
123 - expect(onRender).toHaveLastRenderedWithInteractions(
124 - new Set([interaction]),
125 - );
126 -
127 - expect(Scheduler).toFlushAndYieldThrough(['Child', 'Child:mount']);
128 - expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
129 - expect(onRender).toHaveBeenCalledTimes(2);
130 - expect(onRender).toHaveLastRenderedWithInteractions(
131 - new Set([interaction]),
132 - );
133 -
134 - expect(Scheduler).toFlushAndYield(['Child', 'Child:update']);
133 expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
134 expect(
135 onInteractionScheduledWorkCompleted,
@@ -172,33 +170,34 @@ describe('ReactDOMTracing', () => {
170 SchedulerTracing.unstable_trace('initialization', 0, () => {
171 interaction = Array.from(SchedulerTracing.unstable_getCurrent())[0];
172
175 - root.render(
176 - <React.Profiler id="test" onRender={onRender}>
177 - <App />
178 - </React.Profiler>,
179 - );
180 - });
181 -
182 - expect(onInteractionTraced).toHaveBeenCalledTimes(1);
183 - expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction(
184 - interaction,
185 - );
173 + TestUtils.act(() => {
174 + root.render(
175 + <React.Profiler id="test" onRender={onRender}>
176 + <App />
177 + </React.Profiler>,
178 + );
179 + expect(onInteractionTraced).toHaveBeenCalledTimes(1);
180 + expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction(
181 + interaction,
182 + );
183
187 - expect(Scheduler).toFlushAndYieldThrough(['App', 'App:mount']);
188 - expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
189 - expect(onRender).toHaveBeenCalledTimes(1);
190 - expect(onRender).toHaveLastRenderedWithInteractions(
191 - new Set([interaction]),
192 - );
184 + expect(Scheduler).toFlushAndYieldThrough(['App', 'App:mount']);
185 + expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
186 + expect(onRender).toHaveBeenCalledTimes(1);
187 + expect(onRender).toHaveLastRenderedWithInteractions(
188 + new Set([interaction]),
189 + );
190
194 - expect(wrapped).not.toBeNull();
191 + expect(wrapped).not.toBeNull();
192
196 - expect(Scheduler).toFlushAndYield(['Child']);
197 - expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
198 - expect(onRender).toHaveBeenCalledTimes(2);
199 - expect(onRender).toHaveLastRenderedWithInteractions(
200 - new Set([interaction]),
201 - );
193 + expect(Scheduler).toFlushAndYield(['Child']);
194 + expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
195 + expect(onRender).toHaveBeenCalledTimes(2);
196 + expect(onRender).toHaveLastRenderedWithInteractions(
197 + new Set([interaction]),
198 + );
199 + });
200 + });
201
202 wrapped();
203 expect(onInteractionTraced).toHaveBeenCalledTimes(1);
@@ -249,34 +248,35 @@ describe('ReactDOMTracing', () => {
248 const root = ReactDOM.unstable_createRoot(container);
249 SchedulerTracing.unstable_trace('initialization', 0, () => {
250 interaction = Array.from(SchedulerTracing.unstable_getCurrent())[0];
251 + TestUtils.act(() => {
252 + root.render(
253 + <React.Profiler id="test" onRender={onRender}>
254 + <App />
255 + </React.Profiler>,
256 + );
257 + expect(onInteractionTraced).toHaveBeenCalledTimes(1);
258 + expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction(
259 + interaction,
260 + );
261
253 - root.render(
254 - <React.Profiler id="test" onRender={onRender}>
255 - <App />
256 - </React.Profiler>,
257 - );
258 - });
259 -
260 - expect(onInteractionTraced).toHaveBeenCalledTimes(1);
261 - expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction(
262 - interaction,
263 - );
262 + expect(Scheduler).toFlushAndYieldThrough(['App', 'App:mount']);
263 + expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
264 + expect(onRender).toHaveBeenCalledTimes(1);
265 + expect(onRender).toHaveLastRenderedWithInteractions(
266 + new Set([interaction]),
267 + );
268
265 - expect(Scheduler).toFlushAndYieldThrough(['App', 'App:mount']);
266 - expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
267 - expect(onRender).toHaveBeenCalledTimes(1);
268 - expect(onRender).toHaveLastRenderedWithInteractions(
269 - new Set([interaction]),
270 - );
269 + expect(Scheduler).toFlushAndYieldThrough(['Child', 'Child:mount']);
270 + expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
271 + expect(onRender).toHaveBeenCalledTimes(2);
272 + expect(onRender).toHaveLastRenderedWithInteractions(
273 + new Set([interaction]),
274 + );
275
272 - expect(Scheduler).toFlushAndYieldThrough(['Child', 'Child:mount']);
273 - expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
274 - expect(onRender).toHaveBeenCalledTimes(2);
275 - expect(onRender).toHaveLastRenderedWithInteractions(
276 - new Set([interaction]),
277 - );
276 + expect(Scheduler).toFlushAndYield(['Child', 'Child:update']);
277 + });
278 + });
279
279 - expect(Scheduler).toFlushAndYield(['Child', 'Child:update']);
280 expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
281 expect(
282 onInteractionScheduledWorkCompleted,
packages/react/src/__tests__/withComponentStack-test.js
+3 -7
@@ -44,16 +44,13 @@ describe('withComponentStack', () => {
44 let React = null;
45 let ReactTestRenderer = null;
46 let error = null;
47 - let scheduler = null;
47 let warn = null;
48
49 beforeEach(() => {
50 jest.resetModules();
52 - jest.mock('scheduler', () => require('scheduler/unstable_mock'));
51
52 React = require('react');
53 ReactTestRenderer = require('react-test-renderer');
56 - scheduler = require('scheduler');
54
55 error = React.error;
56 warn = React.warn;
@@ -178,10 +175,9 @@ describe('withComponentStack', () => {
175 });
176 return null;
177 }
181 -
182 - ReactTestRenderer.create(<Parent />);
183 -
184 - scheduler.flushAll(); // Flush passive effects
178 + ReactTestRenderer.act(() => {
179 + ReactTestRenderer.create(<Parent />);
180 + });
181
182 expectMessageAndStack(
183 'logged in child render method',