From a8875eab7f78a453d22370d1061a8bb3cd672b9d Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 10 Mar 2023 11:06:28 -0500 Subject: [PATCH] Update more tests to not rely on sync queuing (#26358) This fixes a handful of tests that were accidentally relying on React synchronously queuing work in the Scheduler after a setState. Usually this is because they use a lower level SchedulerMock method instead of either `act` or one of the `waitFor` helpers. In some cases, the solution is to switch to those APIs. In other cases, if we're intentionally testing some lower level behavior, we might have to be a bit more clever. Co-authored-by: Tianyu Yao --- .../ReactDevToolsHooksIntegration-test.js | 51 +++--- .../ReactHooksInspectionIntegration-test.js | 8 +- .../src/__tests__/ReactDOMFiberAsync-test.js | 123 +++++++------ .../src/__tests__/ReactDOMFizzServer-test.js | 42 ++--- .../src/__tests__/ReactDOMHooks-test.js | 12 +- .../ReactDOMNativeEventHeuristic-test.js | 98 +++++------ .../src/__tests__/ReactDOMRoot-test.js | 64 +++---- ...DOMServerPartialHydration-test.internal.js | 162 +++++++----------- ...MServerSelectiveHydration-test.internal.js | 159 +++++++++-------- .../ReactServerRenderingHydration-test.js | 23 ++- .../src/__tests__/ReactTestUtilsAct-test.js | 1 - packages/react-noop-renderer/src/ReactNoop.js | 3 +- .../src/ReactNoopPersistent.js | 3 +- .../src/createReactNoop.js | 35 ++-- .../__tests__/DebugTracing-test.internal.js | 24 ++- .../src/__tests__/ReactExpiration-test.js | 62 +++---- .../src/__tests__/ReactIncremental-test.js | 2 +- .../ReactIncrementalSideEffects-test.js | 14 +- .../ReactIncrementalUpdatesMinimalism-test.js | 44 +++-- .../ReactPersistentUpdatesMinimalism-test.js | 43 +++-- .../src/__tests__/ReactScope-test.internal.js | 18 +- .../__tests__/ReactSuspenseCallback-test.js | 46 +++-- .../ReactSuspenseWithNoopRenderer-test.js | 12 +- .../src/__tests__/ReactTestRenderer-test.js | 13 +- .../src/__tests__/ReactStrictMode-test.js | 43 +++-- 25 files changed, 550 insertions(+), 555 deletions(-) diff --git a/packages/react-debug-tools/src/__tests__/ReactDevToolsHooksIntegration-test.js b/packages/react-debug-tools/src/__tests__/ReactDevToolsHooksIntegration-test.js index 4497c01e740a8..5da6b8de2a42e 100644 --- a/packages/react-debug-tools/src/__tests__/ReactDevToolsHooksIntegration-test.js +++ b/packages/react-debug-tools/src/__tests__/ReactDevToolsHooksIntegration-test.js @@ -14,7 +14,6 @@ describe('React hooks DevTools integration', () => { let React; let ReactDebugTools; let ReactTestRenderer; - let Scheduler; let act; let overrideHookState; let scheduleUpdate; @@ -40,7 +39,6 @@ describe('React hooks DevTools integration', () => { React = require('react'); ReactDebugTools = require('react-debug-tools'); ReactTestRenderer = require('react-test-renderer'); - Scheduler = require('scheduler'); const InternalTestUtils = require('internal-test-utils'); waitForAll = InternalTestUtils.waitForAll; @@ -48,7 +46,7 @@ describe('React hooks DevTools integration', () => { act = ReactTestRenderer.act; }); - it('should support editing useState hooks', () => { + it('should support editing useState hooks', async () => { let setCountFn; function MyComponent() { @@ -70,14 +68,14 @@ describe('React hooks DevTools integration', () => { expect(stateHook.isStateEditable).toBe(true); if (__DEV__) { - act(() => overrideHookState(fiber, stateHook.id, [], 10)); + await act(() => overrideHookState(fiber, stateHook.id, [], 10)); expect(renderer.toJSON()).toEqual({ type: 'div', props: {}, children: ['count:', '10'], }); - act(() => setCountFn(count => count + 1)); + await act(() => setCountFn(count => count + 1)); expect(renderer.toJSON()).toEqual({ type: 'div', props: {}, @@ -86,7 +84,7 @@ describe('React hooks DevTools integration', () => { } }); - it('should support editable useReducer hooks', () => { + it('should support editable useReducer hooks', async () => { const initialData = {foo: 'abc', bar: 123}; function reducer(state, action) { @@ -122,14 +120,14 @@ describe('React hooks DevTools integration', () => { expect(reducerHook.isStateEditable).toBe(true); if (__DEV__) { - act(() => overrideHookState(fiber, reducerHook.id, ['foo'], 'def')); + await act(() => overrideHookState(fiber, reducerHook.id, ['foo'], 'def')); expect(renderer.toJSON()).toEqual({ type: 'div', props: {}, children: ['foo:', 'def', ', bar:', '123'], }); - act(() => dispatchFn({type: 'swap'})); + await act(() => dispatchFn({type: 'swap'})); expect(renderer.toJSON()).toEqual({ type: 'div', props: {}, @@ -140,7 +138,7 @@ describe('React hooks DevTools integration', () => { // This test case is based on an open source bug report: // https://github.com/facebookincubator/redux-react-hook/issues/34#issuecomment-466693787 - it('should handle interleaved stateful hooks (e.g. useState) and non-stateful hooks (e.g. useContext)', () => { + it('should handle interleaved stateful hooks (e.g. useState) and non-stateful hooks (e.g. useContext)', async () => { const MyContext = React.createContext(1); let setStateFn; @@ -170,13 +168,13 @@ describe('React hooks DevTools integration', () => { expect(stateHook.isStateEditable).toBe(true); if (__DEV__) { - act(() => overrideHookState(fiber, stateHook.id, ['count'], 10)); + await act(() => overrideHookState(fiber, stateHook.id, ['count'], 10)); expect(renderer.toJSON()).toEqual({ type: 'div', props: {}, children: ['count:', '10'], }); - act(() => setStateFn(state => ({count: state.count + 1}))); + await act(() => setStateFn(state => ({count: state.count + 1}))); expect(renderer.toJSON()).toEqual({ type: 'div', props: {}, @@ -185,7 +183,7 @@ describe('React hooks DevTools integration', () => { } }); - it('should support overriding suspense in legacy mode', () => { + it('should support overriding suspense in legacy mode', async () => { if (__DEV__) { // Lock the first render setSuspenseHandler(() => true); @@ -206,32 +204,32 @@ describe('React hooks DevTools integration', () => { if (__DEV__) { // First render was locked expect(renderer.toJSON().children).toEqual(['Loading']); - act(() => scheduleUpdate(fiber)); // Re-render + await act(() => scheduleUpdate(fiber)); // Re-render expect(renderer.toJSON().children).toEqual(['Loading']); // Release the lock setSuspenseHandler(() => false); - act(() => scheduleUpdate(fiber)); // Re-render + await act(() => scheduleUpdate(fiber)); // Re-render expect(renderer.toJSON().children).toEqual(['Done']); - act(() => scheduleUpdate(fiber)); // Re-render + await act(() => scheduleUpdate(fiber)); // Re-render expect(renderer.toJSON().children).toEqual(['Done']); // Lock again setSuspenseHandler(() => true); - act(() => scheduleUpdate(fiber)); // Re-render + await act(() => scheduleUpdate(fiber)); // Re-render expect(renderer.toJSON().children).toEqual(['Loading']); // Release the lock again setSuspenseHandler(() => false); - act(() => scheduleUpdate(fiber)); // Re-render + await act(() => scheduleUpdate(fiber)); // Re-render expect(renderer.toJSON().children).toEqual(['Done']); // Ensure it checks specific fibers. setSuspenseHandler(f => f === fiber || f === fiber.alternate); - act(() => scheduleUpdate(fiber)); // Re-render + await act(() => scheduleUpdate(fiber)); // Re-render expect(renderer.toJSON().children).toEqual(['Loading']); setSuspenseHandler(f => f !== fiber && f !== fiber.alternate); - act(() => scheduleUpdate(fiber)); // Re-render + await act(() => scheduleUpdate(fiber)); // Re-render expect(renderer.toJSON().children).toEqual(['Done']); } else { expect(renderer.toJSON().children).toEqual(['Done']); @@ -267,33 +265,32 @@ describe('React hooks DevTools integration', () => { if (__DEV__) { // First render was locked expect(renderer.toJSON().children).toEqual(['Loading']); - act(() => scheduleUpdate(fiber)); // Re-render + await act(() => scheduleUpdate(fiber)); // Re-render expect(renderer.toJSON().children).toEqual(['Loading']); // Release the lock setSuspenseHandler(() => false); - act(() => scheduleUpdate(fiber)); // Re-render - Scheduler.unstable_flushAll(); + await act(() => scheduleUpdate(fiber)); // Re-render expect(renderer.toJSON().children).toEqual(['Done']); - act(() => scheduleUpdate(fiber)); // Re-render + await act(() => scheduleUpdate(fiber)); // Re-render expect(renderer.toJSON().children).toEqual(['Done']); // Lock again setSuspenseHandler(() => true); - act(() => scheduleUpdate(fiber)); // Re-render + await act(() => scheduleUpdate(fiber)); // Re-render expect(renderer.toJSON().children).toEqual(['Loading']); // Release the lock again setSuspenseHandler(() => false); - act(() => scheduleUpdate(fiber)); // Re-render + await act(() => scheduleUpdate(fiber)); // Re-render expect(renderer.toJSON().children).toEqual(['Done']); // Ensure it checks specific fibers. setSuspenseHandler(f => f === fiber || f === fiber.alternate); - act(() => scheduleUpdate(fiber)); // Re-render + await act(() => scheduleUpdate(fiber)); // Re-render expect(renderer.toJSON().children).toEqual(['Loading']); setSuspenseHandler(f => f !== fiber && f !== fiber.alternate); - act(() => scheduleUpdate(fiber)); // Re-render + await act(() => scheduleUpdate(fiber)); // Re-render expect(renderer.toJSON().children).toEqual(['Done']); } else { expect(renderer.toJSON().children).toEqual(['Done']); diff --git a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js index 2c7597a6551dd..b026f7edb2605 100644 --- a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js +++ b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js @@ -12,7 +12,6 @@ let React; let ReactTestRenderer; -let Scheduler; let ReactDebugTools; let act; @@ -21,7 +20,6 @@ describe('ReactHooksInspectionIntegration', () => { jest.resetModules(); React = require('react'); ReactTestRenderer = require('react-test-renderer'); - Scheduler = require('scheduler'); act = require('internal-test-utils').act; ReactDebugTools = require('react-debug-tools'); }); @@ -890,10 +888,8 @@ describe('ReactHooksInspectionIntegration', () => { , ); - await LazyFoo; - - expect(() => { - Scheduler.unstable_flushAll(); + await expect(async () => { + await act(async () => await LazyFoo); }).toErrorDev([ 'Foo: Support for defaultProps will be removed from function components in a future major release. Use JavaScript default parameters instead.', ]); diff --git a/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js b/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js index 79d70b4bf2437..c8d445264da9b 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js @@ -16,6 +16,7 @@ let ReactDOMClient; let Scheduler; let act; let waitForAll; +let assertLog; const setUntrackedInputValue = Object.getOwnPropertyDescriptor( HTMLInputElement.prototype, @@ -36,6 +37,7 @@ describe('ReactDOMFiberAsync', () => { const InternalTestUtils = require('internal-test-utils'); waitForAll = InternalTestUtils.waitForAll; + assertLog = InternalTestUtils.assertLog; document.body.appendChild(container); }); @@ -154,7 +156,7 @@ describe('ReactDOMFiberAsync', () => { }); describe('concurrent mode', () => { - it('does not perform deferred updates synchronously', () => { + it('does not perform deferred updates synchronously', async () => { const inputRef = React.createRef(); const asyncValueRef = React.createRef(); const syncValueRef = React.createRef(); @@ -164,7 +166,7 @@ describe('ReactDOMFiberAsync', () => { handleChange = e => { const nextValue = e.target.value; - requestIdleCallback(() => { + React.startTransition(() => { this.setState({ asyncValue: nextValue, }); @@ -191,38 +193,41 @@ describe('ReactDOMFiberAsync', () => { } } const root = ReactDOMClient.createRoot(container); - root.render(); - Scheduler.unstable_flushAll(); + await act(() => root.render()); expect(asyncValueRef.current.textContent).toBe(''); expect(syncValueRef.current.textContent).toBe(''); - setUntrackedInputValue.call(inputRef.current, 'hello'); - inputRef.current.dispatchEvent(new MouseEvent('input', {bubbles: true})); - // Should only flush non-deferred update. - expect(asyncValueRef.current.textContent).toBe(''); - expect(syncValueRef.current.textContent).toBe('hello'); + await act(() => { + setUntrackedInputValue.call(inputRef.current, 'hello'); + inputRef.current.dispatchEvent( + new MouseEvent('input', {bubbles: true}), + ); + // Should only flush non-deferred update. + expect(asyncValueRef.current.textContent).toBe(''); + expect(syncValueRef.current.textContent).toBe('hello'); + }); // Should flush both updates now. - jest.runAllTimers(); - Scheduler.unstable_flushAll(); expect(asyncValueRef.current.textContent).toBe('hello'); expect(syncValueRef.current.textContent).toBe('hello'); }); - it('top-level updates are concurrent', () => { + it('top-level updates are concurrent', async () => { const root = ReactDOMClient.createRoot(container); - root.render(
Hi
); - expect(container.textContent).toEqual(''); - Scheduler.unstable_flushAll(); + await act(() => { + root.render(
Hi
); + expect(container.textContent).toEqual(''); + }); expect(container.textContent).toEqual('Hi'); - root.render(
Bye
); - expect(container.textContent).toEqual('Hi'); - Scheduler.unstable_flushAll(); + await act(() => { + root.render(
Bye
); + expect(container.textContent).toEqual('Hi'); + }); expect(container.textContent).toEqual('Bye'); }); - it('deep updates (setState) are concurrent', () => { + it('deep updates (setState) are concurrent', async () => { let instance; class Component extends React.Component { state = {step: 0}; @@ -233,19 +238,21 @@ describe('ReactDOMFiberAsync', () => { } const root = ReactDOMClient.createRoot(container); - root.render(); - expect(container.textContent).toEqual(''); - Scheduler.unstable_flushAll(); - expect(container.textContent).toEqual('0'); - instance.setState({step: 1}); + await act(() => { + root.render(); + expect(container.textContent).toEqual(''); + }); expect(container.textContent).toEqual('0'); - Scheduler.unstable_flushAll(); + + await act(() => { + instance.setState({step: 1}); + expect(container.textContent).toEqual('0'); + }); expect(container.textContent).toEqual('1'); }); - it('flushSync flushes updates before end of the tick', () => { - const ops = []; + it('flushSync flushes updates before end of the tick', async () => { let instance; class Component extends React.Component { @@ -254,7 +261,7 @@ describe('ReactDOMFiberAsync', () => { this.setState(state => ({text: state.text + val})); } componentDidUpdate() { - ops.push(this.state.text); + Scheduler.log(this.state.text); } render() { instance = this; @@ -263,12 +270,11 @@ describe('ReactDOMFiberAsync', () => { } const root = ReactDOMClient.createRoot(container); - root.render(); - Scheduler.unstable_flushAll(); + await act(() => root.render()); // Updates are async by default instance.push('A'); - expect(ops).toEqual([]); + assertLog([]); expect(container.textContent).toEqual(''); ReactDOM.flushSync(() => { @@ -276,39 +282,32 @@ describe('ReactDOMFiberAsync', () => { instance.push('C'); // Not flushed yet expect(container.textContent).toEqual(''); - expect(ops).toEqual([]); + assertLog([]); }); // Only the active updates have flushed if (gate(flags => flags.enableUnifiedSyncLane)) { expect(container.textContent).toEqual('ABC'); - expect(ops).toEqual(['ABC']); + assertLog(['ABC']); } else { expect(container.textContent).toEqual('BC'); - expect(ops).toEqual(['BC']); + assertLog(['BC']); } - if (gate(flags => flags.enableUnifiedSyncLane)) { - instance.push('D'); - expect(container.textContent).toEqual('ABC'); - expect(ops).toEqual(['ABC']); - } else { + await act(() => { instance.push('D'); - expect(container.textContent).toEqual('BC'); - expect(ops).toEqual(['BC']); - } - - // Flush the async updates - Scheduler.unstable_flushAll(); + if (gate(flags => flags.enableUnifiedSyncLane)) { + expect(container.textContent).toEqual('ABC'); + } else { + expect(container.textContent).toEqual('BC'); + } + assertLog([]); + }); + assertLog(['ABCD']); expect(container.textContent).toEqual('ABCD'); - if (gate(flags => flags.enableUnifiedSyncLane)) { - expect(ops).toEqual(['ABC', 'ABCD']); - } else { - expect(ops).toEqual(['BC', 'ABCD']); - } }); // @gate www - it('flushControlled flushes updates before yielding to browser', () => { + it('flushControlled flushes updates before yielding to browser', async () => { let inst; class Counter extends React.Component { state = {counter: 0}; @@ -320,14 +319,14 @@ describe('ReactDOMFiberAsync', () => { } } const root = ReactDOMClient.createRoot(container); - root.render(); - Scheduler.unstable_flushAll(); + await act(() => root.render()); expect(container.textContent).toEqual('0'); // Test that a normal update is async - inst.increment(); - expect(container.textContent).toEqual('0'); - Scheduler.unstable_flushAll(); + await act(() => { + inst.increment(); + expect(container.textContent).toEqual('0'); + }); expect(container.textContent).toEqual('1'); const ops = []; @@ -566,7 +565,7 @@ describe('ReactDOMFiberAsync', () => { }); }); - it('regression test: does not drop passive effects across roots (#17066)', () => { + it('regression test: does not drop passive effects across roots (#17066)', async () => { const {useState, useEffect} = React; function App({label}) { @@ -585,11 +584,11 @@ describe('ReactDOMFiberAsync', () => { const containerB = document.createElement('div'); const containerC = document.createElement('div'); - ReactDOM.render(, containerA); - ReactDOM.render(, containerB); - ReactDOM.render(, containerC); - - Scheduler.unstable_flushAll(); + await act(() => { + ReactDOM.render(, containerA); + ReactDOM.render(, containerB); + ReactDOM.render(, containerC); + }); expect(containerA.textContent).toEqual('Finished'); expect(containerB.textContent).toEqual('Finished'); diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index 766cdc66a3459..d4c5a0bf8f298 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -555,7 +555,7 @@ describe('ReactDOMFizzServer', () => { expect(loggedErrors).toEqual([]); expect(bootstrapped).toBe(true); - Scheduler.unstable_flushAll(); + await waitForAll([]); // We're still loading because we're waiting for the server to stream more content. expect(getVisibleChildren(container)).toEqual(
Loading...
); @@ -677,7 +677,7 @@ describe('ReactDOMFizzServer', () => { errors.push({error, errorInfo}); }, }); - Scheduler.unstable_flushAll(); + await waitForAll([]); // We're still loading because we're waiting for the server to stream more content. expect(getVisibleChildren(container)).toEqual(
Loading...
); @@ -773,7 +773,7 @@ describe('ReactDOMFizzServer', () => { errors.push({error, errorInfo}); }, }); - Scheduler.unstable_flushAll(); + await waitForAll([]); expect(getVisibleChildren(container)).toEqual(
Hello World
); @@ -841,7 +841,7 @@ describe('ReactDOMFizzServer', () => { errors.push({error, errorInfo}); }, }); - Scheduler.unstable_flushAll(); + await waitForAll([]); expect(getVisibleChildren(container)).toEqual(
Loading...
); @@ -931,7 +931,7 @@ describe('ReactDOMFizzServer', () => { expect(bootstrapped).toBe(true); // Attempt to hydrate the content. - Scheduler.unstable_flushAll(); + await waitForAll([]); // We're still loading because we're waiting for the server to stream more content. expect(getVisibleChildren(container)).toEqual(
Loading...
); @@ -952,7 +952,7 @@ describe('ReactDOMFizzServer', () => { // But it is not yet hydrated. expect(ref.current).toBe(null); - Scheduler.unstable_flushAll(); + await waitForAll([]); // Now it's hydrated. expect(ref.current).toBe(h1); @@ -1011,7 +1011,7 @@ describe('ReactDOMFizzServer', () => { // Attempt to hydrate the content. ReactDOMClient.hydrateRoot(container, ); - Scheduler.unstable_flushAll(); + await waitForAll([]); // We're still loading because we're waiting for the server to stream more content. expect(getVisibleChildren(container)).toEqual(
Loading...
); @@ -1029,7 +1029,7 @@ describe('ReactDOMFizzServer', () => { expect(ref.current).toBe(null); // Flush the hydration. - Scheduler.unstable_flushAll(); + await waitForAll([]); // Hydrating should've generated an error and replaced the suspense boundary. expect(getVisibleChildren(container)).toEqual(Error Message); @@ -1082,7 +1082,7 @@ describe('ReactDOMFizzServer', () => { container, , ); - Scheduler.unstable_flushAll(); + await waitForAll([]); // We're not hydrated yet. expect(ref.current).toBe(null); @@ -1095,7 +1095,7 @@ describe('ReactDOMFizzServer', () => { // Add more rows before we've hydrated the first two. root.render(); - Scheduler.unstable_flushAll(); + await waitForAll([]); // We're not hydrated yet. expect(ref.current).toBe(null); @@ -1113,7 +1113,7 @@ describe('ReactDOMFizzServer', () => { await resolveText('A'); }); - Scheduler.unstable_flushAll(); + await waitForAll([]); expect(getVisibleChildren(container)).toEqual([ A, @@ -1160,7 +1160,7 @@ describe('ReactDOMFizzServer', () => { errors.push({error, errorInfo}); }, }); - Scheduler.unstable_flushAll(); + await waitForAll([]); // We're still loading because we're waiting for the server to stream more content. expect(getVisibleChildren(container)).toEqual(
Loading...
); @@ -1192,7 +1192,7 @@ describe('ReactDOMFizzServer', () => { // We now resolve it on the client. resolveText('Hello'); - Scheduler.unstable_flushAll(); + await waitForAll([]); // The client rendered HTML is now in place. expect(getVisibleChildren(container)).toEqual( @@ -1866,7 +1866,7 @@ describe('ReactDOMFizzServer', () => { errors.push({error, errorInfo}); }, }); - Scheduler.unstable_flushAll(); + await waitForAll([]); // We're still loading because we're waiting for the server to stream more content. expect(getVisibleChildren(container)).toEqual('Loading root...'); @@ -1881,7 +1881,7 @@ describe('ReactDOMFizzServer', () => { expect(loggedErrors).toEqual([theError]); // We still can't render it on the client because we haven't unblocked the parent. - Scheduler.unstable_flushAll(); + await waitForAll([]); expect(getVisibleChildren(container)).toEqual('Loading root...'); // Unblock the loading state @@ -2067,7 +2067,7 @@ describe('ReactDOMFizzServer', () => { let root; await act(async () => { root = ReactDOMClient.hydrateRoot(container, ); - Scheduler.unstable_flushAll(); + await waitForAll([]); await jest.runAllTimers(); }); @@ -2085,7 +2085,7 @@ describe('ReactDOMFizzServer', () => { await act(async () => { // Trigger update by changing isClient to true root.render(); - Scheduler.unstable_flushAll(); + await waitForAll([]); await jest.runAllTimers(); }); @@ -2664,7 +2664,7 @@ describe('ReactDOMFizzServer', () => { React.startTransition(() => { root.render(); }); - Scheduler.unstable_flushAll(); + await waitForAll([]); jest.runAllTimers(); const clientFallback2 = container.getElementsByTagName('p')[0]; expect(clientFallback2).toBe(serverFallback); @@ -2767,7 +2767,7 @@ describe('ReactDOMFizzServer', () => { // However, an update may have changed the fallback props. In that case we have to // actually force it to re-render on the client and throw away the server one. root.render(); - Scheduler.unstable_flushAll(); + await waitForAll([]); jest.runAllTimers(); assertLog([ '[c!] The server could not finish this Suspense boundary, ' + @@ -3717,7 +3717,7 @@ describe('ReactDOMFizzServer', () => { Scheduler.log('Logged recoverable error: ' + error.message); }, }); - Scheduler.unstable_flushAll(); + await waitForAll([]); expect(getVisibleChildren(container)).toEqual(
@@ -3794,7 +3794,7 @@ describe('ReactDOMFizzServer', () => { Scheduler.log('Logged recoverable error: ' + error.message); }, }); - Scheduler.unstable_flushAll(); + await waitForAll([]); expect(getVisibleChildren(container)).toEqual(
diff --git a/packages/react-dom/src/__tests__/ReactDOMHooks-test.js b/packages/react-dom/src/__tests__/ReactDOMHooks-test.js index c8695de60b7e5..ad8fadad1ff97 100644 --- a/packages/react-dom/src/__tests__/ReactDOMHooks-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMHooks-test.js @@ -12,8 +12,8 @@ let React; let ReactDOM; let ReactDOMClient; -let Scheduler; let act; +let waitForAll; describe('ReactDOMHooks', () => { let container; @@ -24,8 +24,8 @@ describe('ReactDOMHooks', () => { React = require('react'); ReactDOM = require('react-dom'); ReactDOMClient = require('react-dom/client'); - Scheduler = require('scheduler'); act = require('internal-test-utils').act; + waitForAll = require('internal-test-utils').waitForAll; container = document.createElement('div'); document.body.appendChild(container); @@ -35,7 +35,7 @@ describe('ReactDOMHooks', () => { document.body.removeChild(container); }); - it('can ReactDOM.render() from useEffect', () => { + it('can ReactDOM.render() from useEffect', async () => { const container2 = document.createElement('div'); const container3 = document.createElement('div'); @@ -61,7 +61,7 @@ describe('ReactDOMHooks', () => { expect(container.textContent).toBe('1'); expect(container2.textContent).toBe(''); expect(container3.textContent).toBe(''); - Scheduler.unstable_flushAll(); + await waitForAll([]); expect(container.textContent).toBe('1'); expect(container2.textContent).toBe('2'); expect(container3.textContent).toBe('3'); @@ -70,7 +70,7 @@ describe('ReactDOMHooks', () => { expect(container.textContent).toBe('2'); expect(container2.textContent).toBe('2'); // Not flushed yet expect(container3.textContent).toBe('3'); // Not flushed yet - Scheduler.unstable_flushAll(); + await waitForAll([]); expect(container.textContent).toBe('2'); expect(container2.textContent).toBe('4'); expect(container3.textContent).toBe('6'); @@ -132,7 +132,7 @@ describe('ReactDOMHooks', () => { const root = ReactDOMClient.createRoot(container); root.render(); - Scheduler.unstable_flushAll(); + await waitForAll([]); inputRef.current.value = 'abc'; await act(() => { diff --git a/packages/react-dom/src/__tests__/ReactDOMNativeEventHeuristic-test.js b/packages/react-dom/src/__tests__/ReactDOMNativeEventHeuristic-test.js index 9f82e730f8137..60c78035e2223 100644 --- a/packages/react-dom/src/__tests__/ReactDOMNativeEventHeuristic-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMNativeEventHeuristic-test.js @@ -82,13 +82,13 @@ describe('ReactDOMNativeEventHeuristic-test', () => { expect(disableButton.tagName).toBe('BUTTON'); // Dispatch a click event on the Disable-button. - const firstEvent = document.createEvent('Event'); - firstEvent.initEvent('click', true, true); - dispatchAndSetCurrentEvent(disableButton, firstEvent); - + await act(async () => { + const firstEvent = document.createEvent('Event'); + firstEvent.initEvent('click', true, true); + dispatchAndSetCurrentEvent(disableButton, firstEvent); + }); // Discrete events should be flushed in a microtask. // Verify that the second button was removed. - await null; expect(submitButtonRef.current).toBe(null); // We'll assume that the browser won't let the user click it. }); @@ -130,9 +130,8 @@ describe('ReactDOMNativeEventHeuristic-test', () => { } const root = ReactDOMClient.createRoot(container); - root.render(
); // Flush - Scheduler.unstable_flushAll(); + await act(() => root.render()); const disableButton = disableButtonRef.current; expect(disableButton.tagName).toBe('BUTTON'); @@ -140,20 +139,22 @@ describe('ReactDOMNativeEventHeuristic-test', () => { // Dispatch a click event on the Disable-button. const firstEvent = document.createEvent('Event'); firstEvent.initEvent('click', true, true); - dispatchAndSetCurrentEvent(disableButton, firstEvent); + await act(() => { + dispatchAndSetCurrentEvent(disableButton, firstEvent); - // There should now be a pending update to disable the form. - // This should not have flushed yet since it's in concurrent mode. - const submitButton = submitButtonRef.current; - expect(submitButton.tagName).toBe('BUTTON'); + // There should now be a pending update to disable the form. + // This should not have flushed yet since it's in concurrent mode. + const submitButton = submitButtonRef.current; + expect(submitButton.tagName).toBe('BUTTON'); - // Discrete events should be flushed in a microtask. - await null; + // Flush the discrete event + ReactDOM.flushSync(); - // Now let's dispatch an event on the submit button. - const secondEvent = document.createEvent('Event'); - secondEvent.initEvent('click', true, true); - dispatchAndSetCurrentEvent(submitButton, secondEvent); + // Now let's dispatch an event on the submit button. + const secondEvent = document.createEvent('Event'); + secondEvent.initEvent('click', true, true); + dispatchAndSetCurrentEvent(submitButton, secondEvent); + }); // Therefore the form should never have been submitted. expect(formSubmitted).toBe(false); @@ -190,30 +191,30 @@ describe('ReactDOMNativeEventHeuristic-test', () => { } const root = ReactDOMClient.createRoot(container); - root.render(); - // Flush - Scheduler.unstable_flushAll(); + await act(() => root.render()); const enableButton = enableButtonRef.current; expect(enableButton.tagName).toBe('BUTTON'); // Dispatch a click event on the Enable-button. - const firstEvent = document.createEvent('Event'); - firstEvent.initEvent('click', true, true); - dispatchAndSetCurrentEvent(enableButton, firstEvent); + await act(() => { + const firstEvent = document.createEvent('Event'); + firstEvent.initEvent('click', true, true); + dispatchAndSetCurrentEvent(enableButton, firstEvent); - // There should now be a pending update to enable the form. - // This should not have flushed yet since it's in concurrent mode. - const submitButton = submitButtonRef.current; - expect(submitButton.tagName).toBe('BUTTON'); + // There should now be a pending update to enable the form. + // This should not have flushed yet since it's in concurrent mode. + const submitButton = submitButtonRef.current; + expect(submitButton.tagName).toBe('BUTTON'); - // Discrete events should be flushed in a microtask. - await null; + // Flush discrete updates + ReactDOM.flushSync(); - // Now let's dispatch an event on the submit button. - const secondEvent = document.createEvent('Event'); - secondEvent.initEvent('click', true, true); - dispatchAndSetCurrentEvent(submitButton, secondEvent); + // Now let's dispatch an event on the submit button. + const secondEvent = document.createEvent('Event'); + secondEvent.initEvent('click', true, true); + dispatchAndSetCurrentEvent(submitButton, secondEvent); + }); // Therefore the form should have been submitted. expect(formSubmitted).toBe(true); @@ -342,12 +343,11 @@ describe('ReactDOMNativeEventHeuristic-test', () => { }); expect(container.textContent).toEqual('Count: 0'); - const pressEvent = document.createEvent('Event'); - pressEvent.initEvent('click', true, true); - dispatchAndSetCurrentEvent(target.current, pressEvent); - // Intentionally not using `act` so we can observe in between the press - // event and the microtask, without batching. - await null; + await act(async () => { + const pressEvent = document.createEvent('Event'); + pressEvent.initEvent('click', true, true); + dispatchAndSetCurrentEvent(target.current, pressEvent); + }); // If this is 2, that means the `setCount` calls were not batched. expect(container.textContent).toEqual('Count: 1'); }); @@ -383,17 +383,13 @@ describe('ReactDOMNativeEventHeuristic-test', () => { }); expect(container.textContent).toEqual('Count: 0'); - const pressEvent = document.createEvent('Event'); - pressEvent.initEvent('click', true, true); - dispatchAndSetCurrentEvent(target, pressEvent); - - assertLog(['Count: 0 [after batchedUpdates]']); - expect(container.textContent).toEqual('Count: 0'); - - // Intentionally not using `act` so we can observe in between the click - // event and the microtask, without batching. - await null; - + await act(async () => { + const pressEvent = document.createEvent('Event'); + pressEvent.initEvent('click', true, true); + dispatchAndSetCurrentEvent(target, pressEvent); + assertLog(['Count: 0 [after batchedUpdates]']); + expect(container.textContent).toEqual('Count: 0'); + }); expect(container.textContent).toEqual('Count: 1'); }); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMRoot-test.js b/packages/react-dom/src/__tests__/ReactDOMRoot-test.js index 3352d72c8b91f..c601691c96873 100644 --- a/packages/react-dom/src/__tests__/ReactDOMRoot-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMRoot-test.js @@ -18,6 +18,7 @@ let act; let useEffect; let assertLog; let waitFor; +let waitForAll; describe('ReactDOMRoot', () => { let container; @@ -36,12 +37,13 @@ describe('ReactDOMRoot', () => { const InternalTestUtils = require('internal-test-utils'); assertLog = InternalTestUtils.assertLog; waitFor = InternalTestUtils.waitFor; + waitForAll = InternalTestUtils.waitForAll; }); - it('renders children', () => { + it('renders children', async () => { const root = ReactDOMClient.createRoot(container); root.render(
Hi
); - Scheduler.unstable_flushAll(); + await waitForAll([]); expect(container.textContent).toEqual('Hi'); }); @@ -65,7 +67,7 @@ describe('ReactDOMRoot', () => { ); }); - it('warns if a callback parameter is provided to render', () => { + it('warns if a callback parameter is provided to render', async () => { const callback = jest.fn(); const root = ReactDOMClient.createRoot(container); expect(() => root.render(
Hi
, callback)).toErrorDev( @@ -73,7 +75,7 @@ describe('ReactDOMRoot', () => { 'To execute a side effect after rendering, declare it in a component body with useEffect().', {withoutStack: true}, ); - Scheduler.unstable_flushAll(); + await waitForAll([]); expect(callback).not.toHaveBeenCalled(); }); @@ -108,7 +110,7 @@ describe('ReactDOMRoot', () => { ); }); - it('warns if a callback parameter is provided to unmount', () => { + it('warns if a callback parameter is provided to unmount', async () => { const callback = jest.fn(); const root = ReactDOMClient.createRoot(container); root.render(
Hi
); @@ -117,17 +119,17 @@ describe('ReactDOMRoot', () => { 'To execute a side effect after rendering, declare it in a component body with useEffect().', {withoutStack: true}, ); - Scheduler.unstable_flushAll(); + await waitForAll([]); expect(callback).not.toHaveBeenCalled(); }); - it('unmounts children', () => { + it('unmounts children', async () => { const root = ReactDOMClient.createRoot(container); root.render(
Hi
); - Scheduler.unstable_flushAll(); + await waitForAll([]); expect(container.textContent).toEqual('Hi'); root.unmount(); - Scheduler.unstable_flushAll(); + await waitForAll([]); expect(container.textContent).toEqual(''); }); @@ -151,7 +153,7 @@ describe('ReactDOMRoot', () => {
, ); - Scheduler.unstable_flushAll(); + await waitForAll([]); const container2 = document.createElement('div'); container2.innerHTML = markup; @@ -161,7 +163,9 @@ describe('ReactDOMRoot', () => {
, ); - expect(() => Scheduler.unstable_flushAll()).toErrorDev('Extra attributes'); + await expect(async () => await waitForAll([])).toErrorDev( + 'Extra attributes', + ); }); it('clears existing children with legacy API', async () => { @@ -181,7 +185,7 @@ describe('ReactDOMRoot', () => { , container, ); - Scheduler.unstable_flushAll(); + await waitForAll([]); expect(container.textContent).toEqual('dc'); }); @@ -194,7 +198,7 @@ describe('ReactDOMRoot', () => { d , ); - Scheduler.unstable_flushAll(); + await waitForAll([]); expect(container.textContent).toEqual('cd'); root.render(
@@ -202,7 +206,7 @@ describe('ReactDOMRoot', () => { c
, ); - Scheduler.unstable_flushAll(); + await waitForAll([]); expect(container.textContent).toEqual('dc'); }); @@ -212,10 +216,10 @@ describe('ReactDOMRoot', () => { }).toThrow('createRoot(...): Target container is not a DOM element.'); }); - it('warns when rendering with legacy API into createRoot() container', () => { + it('warns when rendering with legacy API into createRoot() container', async () => { const root = ReactDOMClient.createRoot(container); root.render(
Hi
); - Scheduler.unstable_flushAll(); + await waitForAll([]); expect(container.textContent).toEqual('Hi'); expect(() => { ReactDOM.render(
Bye
, container); @@ -230,15 +234,15 @@ describe('ReactDOMRoot', () => { ], {withoutStack: true}, ); - Scheduler.unstable_flushAll(); + await waitForAll([]); // This works now but we could disallow it: expect(container.textContent).toEqual('Bye'); }); - it('warns when hydrating with legacy API into createRoot() container', () => { + it('warns when hydrating with legacy API into createRoot() container', async () => { const root = ReactDOMClient.createRoot(container); root.render(
Hi
); - Scheduler.unstable_flushAll(); + await waitForAll([]); expect(container.textContent).toEqual('Hi'); expect(() => { ReactDOM.hydrate(
Hi
, container); @@ -264,10 +268,10 @@ describe('ReactDOMRoot', () => { assertLog(['callback']); }); - it('warns when unmounting with legacy API (no previous content)', () => { + it('warns when unmounting with legacy API (no previous content)', async () => { const root = ReactDOMClient.createRoot(container); root.render(
Hi
); - Scheduler.unstable_flushAll(); + await waitForAll([]); expect(container.textContent).toEqual('Hi'); let unmounted = false; expect(() => { @@ -283,20 +287,20 @@ describe('ReactDOMRoot', () => { {withoutStack: true}, ); expect(unmounted).toBe(false); - Scheduler.unstable_flushAll(); + await waitForAll([]); expect(container.textContent).toEqual('Hi'); root.unmount(); - Scheduler.unstable_flushAll(); + await waitForAll([]); expect(container.textContent).toEqual(''); }); - it('warns when unmounting with legacy API (has previous content)', () => { + it('warns when unmounting with legacy API (has previous content)', async () => { // Currently createRoot().render() doesn't clear this. container.appendChild(document.createElement('div')); // The rest is the same as test above. const root = ReactDOMClient.createRoot(container); root.render(
Hi
); - Scheduler.unstable_flushAll(); + await waitForAll([]); expect(container.textContent).toEqual('Hi'); let unmounted = false; expect(() => { @@ -310,10 +314,10 @@ describe('ReactDOMRoot', () => { {withoutStack: true}, ); expect(unmounted).toBe(false); - Scheduler.unstable_flushAll(); + await waitForAll([]); expect(container.textContent).toEqual('Hi'); root.unmount(); - Scheduler.unstable_flushAll(); + await waitForAll([]); expect(container.textContent).toEqual(''); }); @@ -340,10 +344,10 @@ describe('ReactDOMRoot', () => { ); }); - it('does not warn when creating second root after first one is unmounted', () => { + it('does not warn when creating second root after first one is unmounted', async () => { const root = ReactDOMClient.createRoot(container); root.unmount(); - Scheduler.unstable_flushAll(); + await waitForAll([]); ReactDOMClient.createRoot(container); // No warning }); @@ -368,7 +372,7 @@ describe('ReactDOMRoot', () => { it('warns if updating a root that has had its contents removed', async () => { const root = ReactDOMClient.createRoot(container); root.render(
Hi
); - Scheduler.unstable_flushAll(); + await waitForAll([]); container.innerHTML = ''; if (gate(flags => flags.enableFloat || flags.enableHostSingletons)) { diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js index 61565a175839a..7d98dbd87dc78 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js @@ -180,8 +180,7 @@ describe('ReactDOMServerPartialHydration', () => { // hydrating anyway. suspend = true; ReactDOMClient.hydrateRoot(container, ); - Scheduler.unstable_flushAll(); - jest.runAllTimers(); + await waitForAll([]); expect(ref.current).toBe(null); @@ -189,8 +188,7 @@ describe('ReactDOMServerPartialHydration', () => { suspend = false; resolve(); await promise; - Scheduler.unstable_flushAll(); - jest.runAllTimers(); + await waitForAll([]); // We should now have hydrated with a ref on the existing span. expect(ref.current).toBe(span); @@ -237,8 +235,7 @@ describe('ReactDOMServerPartialHydration', () => { Scheduler.log(error.message); }, }); - Scheduler.unstable_flushAll(); - jest.runAllTimers(); + await waitForAll([]); // Expect the server-generated HTML to stay intact. expect(container.textContent).toBe('HelloHello'); @@ -247,8 +244,7 @@ describe('ReactDOMServerPartialHydration', () => { suspend = false; resolve(); await promise; - Scheduler.unstable_flushAll(); - jest.runAllTimers(); + await waitForAll([]); // Hydration should not change anything. expect(container.textContent).toBe('HelloHello'); }); @@ -478,8 +474,7 @@ describe('ReactDOMServerPartialHydration', () => { container.innerHTML = finalHTML; ReactDOMClient.hydrateRoot(container, ); - Scheduler.unstable_flushAll(); - jest.runAllTimers(); + await waitForAll([]); expect(container.innerHTML).toContain('
Sibling
'); }); @@ -583,11 +578,9 @@ describe('ReactDOMServerPartialHydration', () => { ReactDOMClient.hydrateRoot(container, ); }); - resolve(); - await promise; - Scheduler.unstable_flushAll(); - await null; - jest.runAllTimers(); + await act(() => { + resolve(); + }); expect(container.innerHTML).toContain('A'); expect(container.innerHTML).not.toContain('B'); @@ -689,12 +682,13 @@ describe('ReactDOMServerPartialHydration', () => { // On the client we don't have all data yet but we want to start // hydrating anyway. suspend = true; - const root = ReactDOMClient.hydrateRoot(container, , { - onDeleted(node) { - deleted.push(node); - }, + const root = await act(() => { + return ReactDOMClient.hydrateRoot(container, , { + onDeleted(node) { + deleted.push(node); + }, + }); }); - Scheduler.unstable_flushAll(); expect(deleted.length).toBe(0); @@ -775,11 +769,7 @@ describe('ReactDOMServerPartialHydration', () => { // Resolving the promise should render the final content. suspend = false; - resolve(); - await promise; - Scheduler.unstable_flushAll(); - await null; - jest.runAllTimers(); + await act(() => resolve()); // We should now have hydrated with a ref on the existing span. expect(container.textContent).toBe('Hello'); @@ -939,8 +929,7 @@ describe('ReactDOMServerPartialHydration', () => { container, , ); - Scheduler.unstable_flushAll(); - jest.runAllTimers(); + await waitForAll([]); expect(ref.current).toBe(null); expect(span.textContent).toBe('Hello'); @@ -1012,8 +1001,7 @@ describe('ReactDOMServerPartialHydration', () => { container, , ); - Scheduler.unstable_flushAll(); - jest.runAllTimers(); + await waitForAll([]); expect(ref.current).toBe(null); expect(span.textContent).toBe('Hello'); @@ -1029,8 +1017,7 @@ describe('ReactDOMServerPartialHydration', () => { await promise; // This should first complete the hydration and then flush the update onto the hydrated state. - Scheduler.unstable_flushAll(); - jest.runAllTimers(); + await waitForAll([]); // The new span should be the same since we should have successfully hydrated // before changing it. @@ -1090,8 +1077,7 @@ describe('ReactDOMServerPartialHydration', () => { }, }, ); - Scheduler.unstable_flushAll(); - jest.runAllTimers(); + await waitForAll([]); expect(ref.current).toBe(null); @@ -1169,8 +1155,7 @@ describe('ReactDOMServerPartialHydration', () => { }, }, ); - Scheduler.unstable_flushAll(); - jest.runAllTimers(); + await waitForAll([]); const span = container.getElementsByTagName('span')[0]; expect(ref.current).toBe(span); @@ -1246,8 +1231,7 @@ describe('ReactDOMServerPartialHydration', () => { }, }, ); - Scheduler.unstable_flushAll(); - jest.runAllTimers(); + await waitForAll([]); expect(ref.current).toBe(null); @@ -1268,8 +1252,7 @@ describe('ReactDOMServerPartialHydration', () => { await promise; }); - Scheduler.unstable_flushAll(); - jest.runAllTimers(); + await waitForAll([]); const span = container.getElementsByTagName('span')[0]; expect(span.textContent).toBe('Hi'); @@ -1320,29 +1303,23 @@ describe('ReactDOMServerPartialHydration', () => { container, , ); - Scheduler.unstable_flushAll(); - jest.runAllTimers(); + await waitForAll([]); expect(ref.current).toBe(null); expect(container.textContent).toBe('Hello'); // Render an update with a long timeout. React.startTransition(() => root.render()); - // This shouldn't force the fallback yet. - Scheduler.unstable_flushAll(); + await waitForAll([]); expect(ref.current).toBe(null); expect(container.textContent).toBe('Hello'); // Resolving the promise so that rendering can complete. - suspend = false; - resolve(); - await promise; - // This should first complete the hydration and then flush the update onto the hydrated state. - Scheduler.unstable_flushAll(); - jest.runAllTimers(); + suspend = false; + await act(() => resolve()); // The new span should be the same since we should have successfully hydrated // before changing it. @@ -1477,8 +1454,7 @@ describe('ReactDOMServerPartialHydration', () => { , ); - Scheduler.unstable_flushAll(); - jest.runAllTimers(); + await waitForAll([]); expect(ref.current).toBe(null); expect(span.textContent).toBe('Hello'); @@ -1563,8 +1539,7 @@ describe('ReactDOMServerPartialHydration', () => { }, }, ); - Scheduler.unstable_flushAll(); - jest.runAllTimers(); + await waitForAll([]); expect(ref.current).toBe(null); @@ -1868,12 +1843,10 @@ describe('ReactDOMServerPartialHydration', () => { const spanB = container.getElementsByTagName('span')[1]; - const root = ReactDOMClient.hydrateRoot( - container, - , - ); suspend = true; - Scheduler.unstable_flushAll(); + const root = await act(() => + ReactDOMClient.hydrateRoot(container, ), + ); // We're not hydrated yet. expect(ref.current).toBe(null); @@ -1951,13 +1924,10 @@ describe('ReactDOMServerPartialHydration', () => { const spanA = container.getElementsByTagName('span')[0]; - const root = ReactDOMClient.hydrateRoot( - container, - , - ); - suspend = true; - Scheduler.unstable_flushAll(); + const root = await act(() => + ReactDOMClient.hydrateRoot(container, ), + ); // We're not hydrated yet. expect(ref.current).toBe(null); @@ -2241,8 +2211,7 @@ describe('ReactDOMServerPartialHydration', () => { // On the client we don't have all data yet but we want to start // hydrating anyway. ReactDOMClient.hydrateRoot(container, ); - Scheduler.unstable_flushAll(); - jest.runAllTimers(); + await waitForAll([]); expect(ref1.current).toBe(span1); expect(ref2.current).toBe(span2); @@ -2303,8 +2272,7 @@ describe('ReactDOMServerPartialHydration', () => { }, }, ); - Scheduler.unstable_flushAll(); - jest.runAllTimers(); + await waitForAll([]); expect(ref.current).toBe(null); expect(span.textContent).toBe('Hello'); @@ -2389,8 +2357,7 @@ describe('ReactDOMServerPartialHydration', () => { // hydrating anyway. suspend = true; ReactDOMClient.hydrateRoot(container, ); - Scheduler.unstable_flushAll(); - jest.runAllTimers(); + await waitForAll([]); expect(container.textContent).toBe('Click meHello'); @@ -2482,8 +2449,7 @@ describe('ReactDOMServerPartialHydration', () => { // This should be delayed. expect(onEvent).toHaveBeenCalledTimes(0); - Scheduler.unstable_flushAll(); - jest.runAllTimers(); + await waitForAll([]); // We're now partially hydrated. await act(() => { @@ -2574,8 +2540,7 @@ describe('ReactDOMServerPartialHydration', () => { // This should be delayed. expect(clicks).toBe(0); - Scheduler.unstable_flushAll(); - jest.runAllTimers(); + await waitForAll([]); // We're now partially hydrated. await act(() => { @@ -2667,8 +2632,7 @@ describe('ReactDOMServerPartialHydration', () => { // This should be delayed. expect(onEvent).toHaveBeenCalledTimes(0); - Scheduler.unstable_flushAll(); - jest.runAllTimers(); + await waitForAll([]); // We're now partially hydrated. await act(() => { @@ -2748,8 +2712,7 @@ describe('ReactDOMServerPartialHydration', () => { // hydrating anyway. suspend = true; ReactDOMClient.hydrateRoot(container, ); - Scheduler.unstable_flushAll(); - jest.runAllTimers(); + await waitForAll([]); // We're now partially hydrated. await act(() => { @@ -2823,8 +2786,7 @@ describe('ReactDOMServerPartialHydration', () => { // We're going to use a different root as a parent. // This lets us detect whether an event goes through React's event system. const parentRoot = ReactDOMClient.createRoot(parentContainer); - parentRoot.render(); - Scheduler.unstable_flushAll(); + await act(() => parentRoot.render()); childSlotRef.current.appendChild(childContainer); @@ -2835,9 +2797,7 @@ describe('ReactDOMServerPartialHydration', () => { suspend = true; // Hydrate asynchronously. - ReactDOMClient.hydrateRoot(childContainer, ); - jest.runAllTimers(); - Scheduler.unstable_flushAll(); + await act(() => ReactDOMClient.hydrateRoot(childContainer, )); // The Suspense boundary is not yet hydrated. await act(() => { @@ -2935,8 +2895,7 @@ describe('ReactDOMServerPartialHydration', () => { suspend2 = true; ReactDOMClient.hydrateRoot(container, ); - Scheduler.unstable_flushAll(); - jest.runAllTimers(); + await waitForAll([]); dispatchMouseEvent(appDiv, null); dispatchMouseEvent(firstSpan, appDiv); @@ -2950,8 +2909,7 @@ describe('ReactDOMServerPartialHydration', () => { resolve2(); await promise2; - Scheduler.unstable_flushAll(); - jest.runAllTimers(); + await waitForAll([]); // We've unblocked the current hover target so we should be // able to replay it now. @@ -2962,8 +2920,7 @@ describe('ReactDOMServerPartialHydration', () => { resolve1(); await promise1; - Scheduler.unstable_flushAll(); - jest.runAllTimers(); + await waitForAll([]); expect(ops).toEqual(['Mouse Enter Second']); @@ -3109,8 +3066,7 @@ describe('ReactDOMServerPartialHydration', () => { // hydrating anyway. suspend = true; ReactDOMClient.hydrateRoot(container, ); - Scheduler.unstable_flushAll(); - jest.runAllTimers(); + await waitForAll([]); expect(container.textContent).toBe('Click meHello'); @@ -3173,13 +3129,14 @@ describe('ReactDOMServerPartialHydration', () => { const span = container.getElementsByTagName('span')[0]; expect(span.innerHTML).toBe('Hidden child'); - ReactDOMClient.hydrateRoot(container, , { - onRecoverableError(error) { - Scheduler.log('Log recoverable error: ' + error.message); - }, - }); + await act(() => + ReactDOMClient.hydrateRoot(container, , { + onRecoverableError(error) { + Scheduler.log('Log recoverable error: ' + error.message); + }, + }), + ); - Scheduler.unstable_flushAll(); expect(ref.current).toBe(span); expect(span.innerHTML).toBe('Hidden child'); }); @@ -3206,8 +3163,7 @@ describe('ReactDOMServerPartialHydration', () => { const span = container.getElementsByTagName('span')[0]; expect(span.innerHTML).toBe('Hidden child'); - ReactDOMClient.hydrateRoot(container, ); - Scheduler.unstable_flushAll(); + await act(() => ReactDOMClient.hydrateRoot(container, )); expect(ref.current).toBe(span); expect(span.innerHTML).toBe('Hidden child'); }); @@ -3231,8 +3187,7 @@ describe('ReactDOMServerPartialHydration', () => { const span = container.getElementsByTagName('span')[0]; - ReactDOMClient.hydrateRoot(container, ); - Scheduler.unstable_flushAll(); + await act(() => ReactDOMClient.hydrateRoot(container, )); expect(ref.current).toBe(span); expect(ref.current.innerHTML).toBe('Hidden child'); }); @@ -3335,14 +3290,13 @@ describe('ReactDOMServerPartialHydration', () => { }); function itHydratesWithoutMismatch(msg, App) { - it('hydrates without mismatch ' + msg, () => { + it('hydrates without mismatch ' + msg, async () => { const container = document.createElement('div'); document.body.appendChild(container); const finalHTML = ReactDOMServer.renderToString(); container.innerHTML = finalHTML; - ReactDOMClient.hydrateRoot(container, ); - Scheduler.unstable_flushAll(); + await act(() => ReactDOMClient.hydrateRoot(container, )); }); } diff --git a/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js index ede800787b044..275c3e8fdefd7 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js @@ -763,18 +763,22 @@ describe('ReactDOMServerSelectiveHydration', () => { // Nothing has been hydrated so far. assertLog([]); - // Click D - dispatchMouseHoverEvent(spanD, null); - dispatchClickEvent(spanD); - // Hover over B and then C. - dispatchMouseHoverEvent(spanB, spanD); - dispatchMouseHoverEvent(spanC, spanB); - assertLog(['App']); - await act(async () => { + + await act(() => { + // Click D + dispatchMouseHoverEvent(spanD, null); + dispatchClickEvent(spanD); + + // Hover over B and then C. + dispatchMouseHoverEvent(spanB, spanD); + dispatchMouseHoverEvent(spanC, spanB); + + assertLog(['App']); + suspend = false; resolve(); - await promise; }); + if ( gate( flags => @@ -914,19 +918,18 @@ describe('ReactDOMServerSelectiveHydration', () => { // Nothing has been hydrated so far. assertLog([]); - // Click D - dispatchMouseHoverEvent(spanD, null); - dispatchClickEvent(spanD); - // Hover over B and then C. - dispatchMouseHoverEvent(spanB, spanD); - dispatchMouseHoverEvent(spanC, spanB); + await act(async () => { + // Click D + dispatchMouseHoverEvent(spanD, null); + dispatchClickEvent(spanD); + // Hover over B and then C. + dispatchMouseHoverEvent(spanB, spanD); + dispatchMouseHoverEvent(spanC, spanB); - assertLog(['App']); + assertLog(['App']); - await act(async () => { suspend = false; resolve(); - await promise; }); if ( @@ -998,18 +1001,24 @@ describe('ReactDOMServerSelectiveHydration', () => { let InnerScheduler; let innerDiv; + let OuterTestUtils; + let InnerTestUtils; + beforeEach(async () => { document.body.innerHTML = ''; jest.resetModules(); let OuterReactDOMClient; let InnerReactDOMClient; + jest.isolateModules(() => { OuterReactDOMClient = require('react-dom/client'); OuterScheduler = require('scheduler'); + OuterTestUtils = require('internal-test-utils'); }); jest.isolateModules(() => { InnerReactDOMClient = require('react-dom/client'); InnerScheduler = require('scheduler'); + InnerTestUtils = require('internal-test-utils'); }); expect(OuterReactDOMClient).not.toBe(InnerReactDOMClient); @@ -1092,19 +1101,21 @@ describe('ReactDOMServerSelectiveHydration', () => { const innerHTML = ReactDOMServer.renderToString(); innerContainer.innerHTML = innerHTML; - expect(OuterScheduler.unstable_clearLog()).toEqual(['Outer']); - expect(InnerScheduler.unstable_clearLog()).toEqual(['Inner']); + OuterTestUtils.assertLog(['Outer']); + InnerTestUtils.assertLog(['Inner']); suspendOuter = true; suspendInner = true; - OuterReactDOMClient.hydrateRoot(outerContainer, ); - InnerReactDOMClient.hydrateRoot(innerContainer, ); + await OuterTestUtils.act(() => + OuterReactDOMClient.hydrateRoot(outerContainer, ), + ); + await InnerTestUtils.act(() => + InnerReactDOMClient.hydrateRoot(innerContainer, ), + ); - OuterScheduler.unstable_flushAllWithoutAsserting(); - InnerScheduler.unstable_flushAllWithoutAsserting(); - expect(OuterScheduler.unstable_clearLog()).toEqual(['Suspend Outer']); - expect(InnerScheduler.unstable_clearLog()).toEqual(['Suspend Inner']); + OuterTestUtils.assertLog(['Suspend Outer']); + InnerTestUtils.assertLog(['Suspend Inner']); innerDiv = document.querySelector('#inner'); @@ -1117,7 +1128,7 @@ describe('ReactDOMServerSelectiveHydration', () => { InnerScheduler.unstable_flushAllWithoutAsserting(); }); - expect(OuterScheduler.unstable_clearLog()).toEqual(['Suspend Outer']); + OuterTestUtils.assertLog(['Suspend Outer']); if ( gate( flags => @@ -1126,10 +1137,10 @@ describe('ReactDOMServerSelectiveHydration', () => { ) { // InnerApp doesn't see the event because OuterApp calls stopPropagation in // capture phase since the event is blocked on suspended component - expect(InnerScheduler.unstable_clearLog()).toEqual([]); + InnerTestUtils.assertLog([]); } else { // no stopPropagation - expect(InnerScheduler.unstable_clearLog()).toEqual(['Suspend Inner']); + InnerTestUtils.assertLog(['Suspend Inner']); } assertLog([]); @@ -1142,51 +1153,39 @@ describe('ReactDOMServerSelectiveHydration', () => { it('Inner hydrates first then Outer', async () => { dispatchMouseHoverEvent(innerDiv); - await act(async () => { - resolveInner(); - await innerPromise; - jest.runAllTimers(); - Scheduler.unstable_flushAllWithoutAsserting(); - OuterScheduler.unstable_flushAllWithoutAsserting(); - InnerScheduler.unstable_flushAllWithoutAsserting(); + await InnerTestUtils.act(async () => { + await OuterTestUtils.act(() => { + resolveInner(); + }); }); - expect(OuterScheduler.unstable_clearLog()).toEqual(['Suspend Outer']); + OuterTestUtils.assertLog(['Suspend Outer']); // Inner App renders because it is unblocked - expect(InnerScheduler.unstable_clearLog()).toEqual(['Inner']); + InnerTestUtils.assertLog(['Inner']); // No event is replayed yet assertLog([]); dispatchMouseHoverEvent(innerDiv); - expect(OuterScheduler.unstable_clearLog()).toEqual([]); - expect(InnerScheduler.unstable_clearLog()).toEqual([]); + OuterTestUtils.assertLog([]); + InnerTestUtils.assertLog([]); // No event is replayed yet assertLog([]); - await act(async () => { - resolveOuter(); - await outerPromise; - jest.runAllTimers(); - Scheduler.unstable_flushAllWithoutAsserting(); - OuterScheduler.unstable_flushAllWithoutAsserting(); - InnerScheduler.unstable_flushAllWithoutAsserting(); + await InnerTestUtils.act(async () => { + await OuterTestUtils.act(() => { + resolveOuter(); + + // Nothing happens to inner app yet. + // Its blocked on the outer app replaying the event + InnerTestUtils.assertLog([]); + // Outer hydrates and schedules Replay + OuterTestUtils.waitFor(['Outer']); + // No event is replayed yet + assertLog([]); + }); }); - // Nothing happens to inner app yet. - // Its blocked on the outer app replaying the event - expect(InnerScheduler.unstable_clearLog()).toEqual([]); - // Outer hydrates and schedules Replay - expect(OuterScheduler.unstable_clearLog()).toEqual(['Outer']); - // No event is replayed yet - assertLog([]); - // fire scheduled Replay - await act(() => { - jest.runAllTimers(); - Scheduler.unstable_flushAllWithoutAsserting(); - OuterScheduler.unstable_flushAllWithoutAsserting(); - InnerScheduler.unstable_flushAllWithoutAsserting(); - }); // First Inner Mouse Enter fires then Outer Mouse Enter assertLog(['Inner Mouse Enter', 'Outer Mouse Enter']); @@ -1205,9 +1204,9 @@ describe('ReactDOMServerSelectiveHydration', () => { }); // Outer resolves and scheduled replay - expect(OuterScheduler.unstable_clearLog()).toEqual(['Outer']); + OuterTestUtils.assertLog(['Outer']); // Inner App is still blocked - expect(InnerScheduler.unstable_clearLog()).toEqual([]); + InnerTestUtils.assertLog([]); // Replay outer event await act(() => { @@ -1219,12 +1218,12 @@ describe('ReactDOMServerSelectiveHydration', () => { // Inner is still blocked so when Outer replays the event in capture phase // inner ends up caling stopPropagation assertLog([]); - expect(OuterScheduler.unstable_clearLog()).toEqual([]); - expect(InnerScheduler.unstable_clearLog()).toEqual(['Suspend Inner']); + OuterTestUtils.assertLog([]); + InnerTestUtils.assertLog(['Suspend Inner']); dispatchMouseHoverEvent(innerDiv); - expect(OuterScheduler.unstable_clearLog()).toEqual([]); - expect(InnerScheduler.unstable_clearLog()).toEqual([]); + OuterTestUtils.assertLog([]); + InnerTestUtils.assertLog([]); assertLog([]); await act(async () => { @@ -1236,9 +1235,9 @@ describe('ReactDOMServerSelectiveHydration', () => { }); // Inner hydrates - expect(InnerScheduler.unstable_clearLog()).toEqual(['Inner']); + InnerTestUtils.assertLog(['Inner']); // Outer was hydrated earlier - expect(OuterScheduler.unstable_clearLog()).toEqual([]); + OuterTestUtils.assertLog([]); await act(() => { Scheduler.unstable_flushAllWithoutAsserting(); @@ -1297,18 +1296,17 @@ describe('ReactDOMServerSelectiveHydration', () => { ReactDOMClient.hydrateRoot(container, ); const childDiv = container.firstElementChild; - dispatchMouseHoverEvent(childDiv); - // Not hydrated so event is saved for replay and stopPropagation is called - assertLog([]); + await act(async () => { + dispatchMouseHoverEvent(childDiv); - resolve(); - Scheduler.unstable_flushNumberOfYields(1); - assertLog(['Child']); + // Not hydrated so event is saved for replay and stopPropagation is called + assertLog([]); + + resolve(); + await waitFor(['Child']); - Scheduler.unstable_scheduleCallback( - Scheduler.unstable_ImmediatePriority, - () => { + ReactDOM.flushSync(() => { container.removeChild(childDiv); const container2 = document.createElement('div'); @@ -1316,9 +1314,8 @@ describe('ReactDOMServerSelectiveHydration', () => { Scheduler.log('container2 mouse over'); }); container2.appendChild(childDiv); - }, - ); - Scheduler.unstable_flushAllWithoutAsserting(); + }); + }); // Even though the tree is remove the event is still dispatched with native event handler // on the container firing. diff --git a/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js b/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js index 03e53fc21ae63..d6a06842492e1 100644 --- a/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js +++ b/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js @@ -15,8 +15,8 @@ let ReactDOM; let ReactDOMClient; let ReactDOMServer; let ReactDOMServerBrowser; -let Scheduler; let waitForAll; +let act; // These tests rely both on ReactDOMServer and ReactDOM. // If a test only needs ReactDOMServer, put it in ReactServerRendering-test instead. @@ -28,10 +28,10 @@ describe('ReactDOMServerHydration', () => { ReactDOMClient = require('react-dom/client'); ReactDOMServer = require('react-dom/server'); ReactDOMServerBrowser = require('react-dom/server.browser'); - Scheduler = require('scheduler'); const InternalTestUtils = require('internal-test-utils'); waitForAll = InternalTestUtils.waitForAll; + act = InternalTestUtils.act; }); it('should have the correct mounting behavior (new hydrate API)', () => { @@ -403,25 +403,22 @@ describe('ReactDOMServerHydration', () => { ReactDOM.hydrate(, element); expect(element.textContent).toBe('Hello loading'); - jest.runAllTimers(); - await Promise.resolve(); - Scheduler.unstable_flushAll(); - await null; + // Resolve Lazy component + await act(() => jest.runAllTimers()); expect(element.textContent).toBe('Hello world'); }); - it('does not re-enter hydration after committing the first one', () => { + it('does not re-enter hydration after committing the first one', async () => { const finalHTML = ReactDOMServer.renderToString(
); const container = document.createElement('div'); container.innerHTML = finalHTML; - const root = ReactDOMClient.hydrateRoot(container,
); - Scheduler.unstable_flushAll(); - root.render(null); - Scheduler.unstable_flushAll(); + const root = await act(() => + ReactDOMClient.hydrateRoot(container,
), + ); + await act(() => root.render(null)); // This should not reenter hydration state and therefore not trigger hydration // warnings. - root.render(
); - Scheduler.unstable_flushAll(); + await act(() => root.render(
)); }); it('Suspense + hydration in legacy mode', () => { diff --git a/packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js b/packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js index e7f75a41e44bf..e68914e085988 100644 --- a/packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js +++ b/packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js @@ -108,7 +108,6 @@ describe('ReactTestUtils.act()', () => { it('does not warn in concurrent mode', () => { const root = ReactDOMClient.createRoot(document.createElement('div')); act(() => root.render()); - Scheduler.unstable_flushAll(); }); }); }); diff --git a/packages/react-noop-renderer/src/ReactNoop.js b/packages/react-noop-renderer/src/ReactNoop.js index f7f750cd30652..72d735692672f 100644 --- a/packages/react-noop-renderer/src/ReactNoop.js +++ b/packages/react-noop-renderer/src/ReactNoop.js @@ -35,7 +35,8 @@ export const { unmountRootWithID, findInstance, flushNextYield, - flushWithHostCounters, + startTrackingHostCounters, + stopTrackingHostCounters, expire, flushExpired, batchedUpdates, diff --git a/packages/react-noop-renderer/src/ReactNoopPersistent.js b/packages/react-noop-renderer/src/ReactNoopPersistent.js index c4bce280f216f..f5b74a72f653c 100644 --- a/packages/react-noop-renderer/src/ReactNoopPersistent.js +++ b/packages/react-noop-renderer/src/ReactNoopPersistent.js @@ -35,7 +35,8 @@ export const { unmountRootWithID, findInstance, flushNextYield, - flushWithHostCounters, + startTrackingHostCounters, + stopTrackingHostCounters, expire, flushExpired, batchedUpdates, diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js index 3ea1836ce2055..67634ab1d58fa 100644 --- a/packages/react-noop-renderer/src/createReactNoop.js +++ b/packages/react-noop-renderer/src/createReactNoop.js @@ -983,7 +983,13 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { return Scheduler.unstable_clearLog(); }, - flushWithHostCounters(fn: () => void): + startTrackingHostCounters(): void { + hostDiffCounter = 0; + hostUpdateCounter = 0; + hostCloneCounter = 0; + }, + + stopTrackingHostCounters(): | { hostDiffCounter: number, hostUpdateCounter: number, @@ -992,25 +998,20 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { hostDiffCounter: number, hostCloneCounter: number, } { + const result = useMutation + ? { + hostDiffCounter, + hostUpdateCounter, + } + : { + hostDiffCounter, + hostCloneCounter, + }; hostDiffCounter = 0; hostUpdateCounter = 0; hostCloneCounter = 0; - try { - Scheduler.unstable_flushAll(); - return useMutation - ? { - hostDiffCounter, - hostUpdateCounter, - } - : { - hostDiffCounter, - hostCloneCounter, - }; - } finally { - hostDiffCounter = 0; - hostUpdateCounter = 0; - hostCloneCounter = 0; - } + + return result; }, expire: Scheduler.unstable_advanceTime, diff --git a/packages/react-reconciler/src/__tests__/DebugTracing-test.internal.js b/packages/react-reconciler/src/__tests__/DebugTracing-test.internal.js index a5b20ada90283..39d42d254f610 100644 --- a/packages/react-reconciler/src/__tests__/DebugTracing-test.internal.js +++ b/packages/react-reconciler/src/__tests__/DebugTracing-test.internal.js @@ -13,6 +13,7 @@ describe('DebugTracing', () => { let React; let ReactTestRenderer; let waitForPaint; + let waitForAll; let logs; @@ -29,6 +30,7 @@ describe('DebugTracing', () => { ReactTestRenderer = require('react-test-renderer'); const InternalTestUtils = require('internal-test-utils'); waitForPaint = InternalTestUtils.waitForPaint; + waitForAll = InternalTestUtils.waitForAll; logs = []; @@ -73,9 +75,20 @@ describe('DebugTracing', () => { // @gate experimental && build === 'development' && enableDebugTracing it('should log sync render with suspense', async () => { - const fakeSuspensePromise = Promise.resolve(true); + let resolveFakeSuspensePromise; + let didResolve = false; + const fakeSuspensePromise = new Promise(resolve => { + resolveFakeSuspensePromise = () => { + didResolve = true; + resolve(); + }; + }); + function Example() { - throw fakeSuspensePromise; + if (!didResolve) { + throw fakeSuspensePromise; + } + return null; } ReactTestRenderer.act(() => @@ -96,7 +109,9 @@ describe('DebugTracing', () => { logs.splice(0); - await fakeSuspensePromise; + resolveFakeSuspensePromise(); + await waitForAll([]); + expect(logs).toEqual(['log: ⚛️ Example resolved']); }); @@ -387,7 +402,8 @@ describe('DebugTracing', () => { return didMount; } - const fakeSuspensePromise = new Promise(() => {}); + const fakeSuspensePromise = {then() {}}; + function ExampleThatSuspends() { throw fakeSuspensePromise; } diff --git a/packages/react-reconciler/src/__tests__/ReactExpiration-test.js b/packages/react-reconciler/src/__tests__/ReactExpiration-test.js index e1a8d08bcc7cd..ae734e19711af 100644 --- a/packages/react-reconciler/src/__tests__/ReactExpiration-test.js +++ b/packages/react-reconciler/src/__tests__/ReactExpiration-test.js @@ -113,35 +113,30 @@ describe('ReactExpiration', () => { } } - function flushNextRenderIfExpired() { - // This will start rendering the next level of work. If the work hasn't - // expired yet, React will exit without doing anything. If it has expired, - // it will schedule a sync task. - Scheduler.unstable_flushExpired(); - // Flush the sync task. - ReactNoop.flushSync(); - } - - it('increases priority of updates as time progresses', () => { + it('increases priority of updates as time progresses', async () => { + ReactNoop.render(); React.startTransition(() => { - ReactNoop.render(); + ReactNoop.render(); }); - expect(ReactNoop).toMatchRenderedOutput(null); + await waitFor(['Step 1']); + + expect(ReactNoop).toMatchRenderedOutput('Step 1'); // Nothing has expired yet because time hasn't advanced. - flushNextRenderIfExpired(); - expect(ReactNoop).toMatchRenderedOutput(null); + Scheduler.unstable_flushExpired(); + expect(ReactNoop).toMatchRenderedOutput('Step 1'); // Advance time a bit, but not enough to expire the low pri update. ReactNoop.expire(4500); - flushNextRenderIfExpired(); - expect(ReactNoop).toMatchRenderedOutput(null); + Scheduler.unstable_flushExpired(); + expect(ReactNoop).toMatchRenderedOutput('Step 1'); - // Advance by another second. Now the update should expire and flush. + // Advance by a little bit more. Now the update should expire and flush. ReactNoop.expire(500); - flushNextRenderIfExpired(); - expect(ReactNoop).toMatchRenderedOutput(); + Scheduler.unstable_flushExpired(); + assertLog(['Step 2']); + expect(ReactNoop).toMatchRenderedOutput('Step 2'); }); it('two updates of like priority in the same event always flush within the same batch', async () => { @@ -344,7 +339,7 @@ describe('ReactExpiration', () => { Scheduler.unstable_advanceTime(10000); - flushNextRenderIfExpired(); + Scheduler.unstable_flushExpired(); assertLog(['D', 'E']); expect(root).toMatchRenderedOutput('ABCDE'); }); @@ -374,17 +369,21 @@ describe('ReactExpiration', () => { Scheduler.unstable_advanceTime(10000); - flushNextRenderIfExpired(); + Scheduler.unstable_flushExpired(); assertLog(['D', 'E']); expect(root).toMatchRenderedOutput('ABCDE'); }); - it('should measure expiration times relative to module initialization', () => { + it('should measure expiration times relative to module initialization', async () => { // Tests an implementation detail where expiration times are computed using // bitwise operations. jest.resetModules(); Scheduler = require('scheduler'); + const InternalTestUtils = require('internal-test-utils'); + waitFor = InternalTestUtils.waitFor; + assertLog = InternalTestUtils.assertLog; + // Before importing the renderer, advance the current time by a number // larger than the maximum allowed for bitwise operations. const maxSigned31BitInt = 1073741823; @@ -393,22 +392,25 @@ describe('ReactExpiration', () => { // Now import the renderer. On module initialization, it will read the // current time. ReactNoop = require('react-noop-renderer'); + React = require('react'); + ReactNoop.render(); React.startTransition(() => { - ReactNoop.render('Hi'); + ReactNoop.render(); }); + await waitFor(['Step 1']); // The update should not have expired yet. - flushNextRenderIfExpired(); + Scheduler.unstable_flushExpired(); assertLog([]); - expect(ReactNoop).toMatchRenderedOutput(null); + expect(ReactNoop).toMatchRenderedOutput('Step 1'); // Advance the time some more to expire the update. Scheduler.unstable_advanceTime(10000); - flushNextRenderIfExpired(); - assertLog([]); - expect(ReactNoop).toMatchRenderedOutput('Hi'); + Scheduler.unstable_flushExpired(); + assertLog(['Step 2']); + expect(ReactNoop).toMatchRenderedOutput('Step 2'); }); it('should measure callback timeout relative to current time, not start-up time', () => { @@ -422,14 +424,14 @@ describe('ReactExpiration', () => { React.startTransition(() => { ReactNoop.render('Hi'); }); - flushNextRenderIfExpired(); + Scheduler.unstable_flushExpired(); assertLog([]); expect(ReactNoop).toMatchRenderedOutput(null); // Advancing by ~5 seconds should be sufficient to expire the update. (I // used a slightly larger number to allow for possible rounding.) Scheduler.unstable_advanceTime(6000); - flushNextRenderIfExpired(); + Scheduler.unstable_flushExpired(); assertLog([]); expect(ReactNoop).toMatchRenderedOutput('Hi'); }); diff --git a/packages/react-reconciler/src/__tests__/ReactIncremental-test.js b/packages/react-reconciler/src/__tests__/ReactIncremental-test.js index b90572aed72df..885955273c967 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncremental-test.js +++ b/packages/react-reconciler/src/__tests__/ReactIncremental-test.js @@ -79,7 +79,7 @@ describe('ReactIncremental', () => { ReactNoop.render(, () => Scheduler.log('callback')); }); // Do one step of work. - expect(ReactNoop.flushNextYield()).toEqual(['Foo']); + await waitFor(['Foo']); // Do the rest of the work. await waitForAll(['Bar', 'Bar', 'callback']); diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalSideEffects-test.js b/packages/react-reconciler/src/__tests__/ReactIncrementalSideEffects-test.js index 0ed01d9471a16..b47897e54d92e 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncrementalSideEffects-test.js +++ b/packages/react-reconciler/src/__tests__/ReactIncrementalSideEffects-test.js @@ -15,6 +15,7 @@ let ReactNoop; let Scheduler; let waitForAll; let waitFor; +let waitForPaint; describe('ReactIncrementalSideEffects', () => { beforeEach(() => { @@ -27,6 +28,7 @@ describe('ReactIncrementalSideEffects', () => { const InternalTestUtils = require('internal-test-utils'); waitForAll = InternalTestUtils.waitForAll; waitFor = InternalTestUtils.waitFor; + waitForPaint = InternalTestUtils.waitForPaint; }); // Note: This is based on a similar component we use in www. We can delete @@ -694,25 +696,25 @@ describe('ReactIncrementalSideEffects', () => { it('can update a completed tree before it has a chance to commit', async () => { function Foo(props) { - Scheduler.log('Foo'); + Scheduler.log('Foo ' + props.step); return ; } React.startTransition(() => { ReactNoop.render(); }); // This should be just enough to complete the tree without committing it - await waitFor(['Foo']); + await waitFor(['Foo 1']); expect(ReactNoop.getChildrenAsJSX()).toEqual(null); // To confirm, perform one more unit of work. The tree should now // be flushed. - ReactNoop.flushNextYield(); + await waitForPaint([]); expect(ReactNoop.getChildrenAsJSX()).toEqual(); React.startTransition(() => { ReactNoop.render(); }); // This should be just enough to complete the tree without committing it - await waitFor(['Foo']); + await waitFor(['Foo 2']); expect(ReactNoop.getChildrenAsJSX()).toEqual(); // This time, before we commit the tree, we update the root component with // new props @@ -723,11 +725,11 @@ describe('ReactIncrementalSideEffects', () => { expect(ReactNoop.getChildrenAsJSX()).toEqual(); // Now let's commit. We already had a commit that was pending, which will // render 2. - ReactNoop.flushNextYield(); + await waitForPaint([]); expect(ReactNoop.getChildrenAsJSX()).toEqual(); // If we flush the rest of the work, we should get another commit that // renders 3. If it renders 2 again, that means an update was dropped. - await waitForAll([]); + await waitForAll(['Foo 3']); expect(ReactNoop.getChildrenAsJSX()).toEqual(); }); diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalUpdatesMinimalism-test.js b/packages/react-reconciler/src/__tests__/ReactIncrementalUpdatesMinimalism-test.js index 1d32c0d24a26b..ddbe7eb8e7011 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncrementalUpdatesMinimalism-test.js +++ b/packages/react-reconciler/src/__tests__/ReactIncrementalUpdatesMinimalism-test.js @@ -12,15 +12,18 @@ let React; let ReactNoop; +let act; describe('ReactIncrementalUpdatesMinimalism', () => { beforeEach(() => { jest.resetModules(); React = require('react'); ReactNoop = require('react-noop-renderer'); + + act = require('internal-test-utils').act; }); - it('should render a simple component', () => { + it('should render a simple component', async () => { function Child() { return
Hello World
; } @@ -29,20 +32,22 @@ describe('ReactIncrementalUpdatesMinimalism', () => { return ; } - ReactNoop.render(); - expect(ReactNoop.flushWithHostCounters()).toEqual({ + ReactNoop.startTrackingHostCounters(); + await act(() => ReactNoop.render()); + expect(ReactNoop.stopTrackingHostCounters()).toEqual({ hostDiffCounter: 0, hostUpdateCounter: 0, }); - ReactNoop.render(); - expect(ReactNoop.flushWithHostCounters()).toEqual({ + ReactNoop.startTrackingHostCounters(); + await act(() => ReactNoop.render()); + expect(ReactNoop.stopTrackingHostCounters()).toEqual({ hostDiffCounter: 1, hostUpdateCounter: 1, }); }); - it('should not diff referentially equal host elements', () => { + it('should not diff referentially equal host elements', async () => { function Leaf(props) { return ( @@ -67,20 +72,22 @@ describe('ReactIncrementalUpdatesMinimalism', () => { return ; } - ReactNoop.render(); - expect(ReactNoop.flushWithHostCounters()).toEqual({ + ReactNoop.startTrackingHostCounters(); + await act(() => ReactNoop.render()); + expect(ReactNoop.stopTrackingHostCounters()).toEqual({ hostDiffCounter: 0, hostUpdateCounter: 0, }); - ReactNoop.render(); - expect(ReactNoop.flushWithHostCounters()).toEqual({ + ReactNoop.startTrackingHostCounters(); + await act(() => ReactNoop.render()); + expect(ReactNoop.stopTrackingHostCounters()).toEqual({ hostDiffCounter: 0, hostUpdateCounter: 0, }); }); - it('should not diff parents of setState targets', () => { + it('should not diff parents of setState targets', async () => { let childInst; function Leaf(props) { @@ -118,14 +125,16 @@ describe('ReactIncrementalUpdatesMinimalism', () => { ); } - ReactNoop.render(); - expect(ReactNoop.flushWithHostCounters()).toEqual({ + ReactNoop.startTrackingHostCounters(); + await act(() => ReactNoop.render()); + expect(ReactNoop.stopTrackingHostCounters()).toEqual({ hostDiffCounter: 0, hostUpdateCounter: 0, }); - childInst.setState({name: 'Robin'}); - expect(ReactNoop.flushWithHostCounters()).toEqual({ + ReactNoop.startTrackingHostCounters(); + await act(() => childInst.setState({name: 'Robin'})); + expect(ReactNoop.stopTrackingHostCounters()).toEqual({ // Child > div // Child > Leaf > span // Child > Leaf > span > b @@ -137,8 +146,9 @@ describe('ReactIncrementalUpdatesMinimalism', () => { hostUpdateCounter: 4, }); - ReactNoop.render(); - expect(ReactNoop.flushWithHostCounters()).toEqual({ + ReactNoop.startTrackingHostCounters(); + await act(() => ReactNoop.render()); + expect(ReactNoop.stopTrackingHostCounters()).toEqual({ // Parent > section // Parent > section > div // Parent > section > div > Leaf > span diff --git a/packages/react-reconciler/src/__tests__/ReactPersistentUpdatesMinimalism-test.js b/packages/react-reconciler/src/__tests__/ReactPersistentUpdatesMinimalism-test.js index da7eed71afa93..4e17597481c5c 100644 --- a/packages/react-reconciler/src/__tests__/ReactPersistentUpdatesMinimalism-test.js +++ b/packages/react-reconciler/src/__tests__/ReactPersistentUpdatesMinimalism-test.js @@ -12,15 +12,17 @@ let React; let ReactNoopPersistent; +let act; describe('ReactPersistentUpdatesMinimalism', () => { beforeEach(() => { jest.resetModules(); React = require('react'); ReactNoopPersistent = require('react-noop-renderer/persistent'); + act = require('internal-test-utils').act; }); - it('should render a simple component', () => { + it('should render a simple component', async () => { function Child() { return
Hello World
; } @@ -29,20 +31,22 @@ describe('ReactPersistentUpdatesMinimalism', () => { return ; } - ReactNoopPersistent.render(); - expect(ReactNoopPersistent.flushWithHostCounters()).toEqual({ + ReactNoopPersistent.startTrackingHostCounters(); + await act(() => ReactNoopPersistent.render()); + expect(ReactNoopPersistent.stopTrackingHostCounters()).toEqual({ hostDiffCounter: 0, hostCloneCounter: 0, }); - ReactNoopPersistent.render(); - expect(ReactNoopPersistent.flushWithHostCounters()).toEqual({ + ReactNoopPersistent.startTrackingHostCounters(); + await act(() => ReactNoopPersistent.render()); + expect(ReactNoopPersistent.stopTrackingHostCounters()).toEqual({ hostDiffCounter: 1, hostCloneCounter: 1, }); }); - it('should not diff referentially equal host elements', () => { + it('should not diff referentially equal host elements', async () => { function Leaf(props) { return ( @@ -67,20 +71,22 @@ describe('ReactPersistentUpdatesMinimalism', () => { return ; } - ReactNoopPersistent.render(); - expect(ReactNoopPersistent.flushWithHostCounters()).toEqual({ + ReactNoopPersistent.startTrackingHostCounters(); + await act(() => ReactNoopPersistent.render()); + expect(ReactNoopPersistent.stopTrackingHostCounters()).toEqual({ hostDiffCounter: 0, hostCloneCounter: 0, }); - ReactNoopPersistent.render(); - expect(ReactNoopPersistent.flushWithHostCounters()).toEqual({ + ReactNoopPersistent.startTrackingHostCounters(); + await act(() => ReactNoopPersistent.render()); + expect(ReactNoopPersistent.stopTrackingHostCounters()).toEqual({ hostDiffCounter: 0, hostCloneCounter: 0, }); }); - it('should not diff parents of setState targets', () => { + it('should not diff parents of setState targets', async () => { let childInst; function Leaf(props) { @@ -118,14 +124,16 @@ describe('ReactPersistentUpdatesMinimalism', () => { ); } - ReactNoopPersistent.render(); - expect(ReactNoopPersistent.flushWithHostCounters()).toEqual({ + ReactNoopPersistent.startTrackingHostCounters(); + await act(() => ReactNoopPersistent.render()); + expect(ReactNoopPersistent.stopTrackingHostCounters()).toEqual({ hostDiffCounter: 0, hostCloneCounter: 0, }); - childInst.setState({name: 'Robin'}); - expect(ReactNoopPersistent.flushWithHostCounters()).toEqual({ + ReactNoopPersistent.startTrackingHostCounters(); + await act(() => childInst.setState({name: 'Robin'})); + expect(ReactNoopPersistent.stopTrackingHostCounters()).toEqual({ // section > div > Child > div // section > div > Child > Leaf > span // section > div > Child > Leaf > span > b @@ -138,8 +146,9 @@ describe('ReactPersistentUpdatesMinimalism', () => { hostCloneCounter: 5, }); - ReactNoopPersistent.render(); - expect(ReactNoopPersistent.flushWithHostCounters()).toEqual({ + ReactNoopPersistent.startTrackingHostCounters(); + await act(() => ReactNoopPersistent.render()); + expect(ReactNoopPersistent.stopTrackingHostCounters()).toEqual({ // Parent > section // Parent > section > div // Parent > section > div > Leaf > span diff --git a/packages/react-reconciler/src/__tests__/ReactScope-test.internal.js b/packages/react-reconciler/src/__tests__/ReactScope-test.internal.js index 7afa353b0298b..66ae53a757d66 100644 --- a/packages/react-reconciler/src/__tests__/ReactScope-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactScope-test.internal.js @@ -12,7 +12,7 @@ let React; let ReactFeatureFlags; let ReactDOMServer; -let Scheduler; +let act; describe('ReactScope', () => { beforeEach(() => { @@ -20,7 +20,9 @@ describe('ReactScope', () => { ReactFeatureFlags = require('shared/ReactFeatureFlags'); ReactFeatureFlags.enableScopeAPI = true; React = require('react'); - Scheduler = require('scheduler'); + + const InternalTestUtils = require('internal-test-utils'); + act = InternalTestUtils.act; }); describe('ReactDOM', () => { @@ -314,9 +316,7 @@ describe('ReactScope', () => { // On the client we don't have all data yet but we want to start // hydrating anyway. suspend = true; - ReactDOMClient.hydrateRoot(container2, ); - Scheduler.unstable_flushAll(); - jest.runAllTimers(); + await act(() => ReactDOMClient.hydrateRoot(container2, )); // This should not cause a runtime exception, see: // https://github.com/facebook/react/pull/18184 @@ -325,10 +325,10 @@ describe('ReactScope', () => { // Resolving the promise should continue hydration suspend = false; - resolve(); - await promise; - Scheduler.unstable_flushAll(); - jest.runAllTimers(); + await act(async () => { + resolve(); + await promise; + }); // We should now have hydrated with a ref on the existing span. expect(ref.current).toBe(span); diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseCallback-test.js b/packages/react-reconciler/src/__tests__/ReactSuspenseCallback-test.js index 011657cd065a8..1f612eae67b90 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseCallback-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseCallback-test.js @@ -11,7 +11,6 @@ let React; let ReactNoop; -let Scheduler; let waitForAll; describe('ReactSuspense', () => { @@ -20,7 +19,6 @@ describe('ReactSuspense', () => { React = require('react'); ReactNoop = require('react-noop-renderer'); - Scheduler = require('scheduler'); const InternalTestUtils = require('internal-test-utils'); waitForAll = InternalTestUtils.waitForAll; @@ -44,32 +42,32 @@ describe('ReactSuspense', () => { return {promise, resolve, PromiseComp}; } - if (__DEV__) { - // @gate www - it('check type', () => { - const {PromiseComp} = createThenable(); + // Warning don't fire in production, so this test passes in prod even if + // the suspenseCallback feature is not enabled + // @gate www || !__DEV__ + it('check type', async () => { + const {PromiseComp} = createThenable(); - const elementBadType = ( - - - - ); + const elementBadType = ( + + + + ); - ReactNoop.render(elementBadType); - expect(() => Scheduler.unstable_flushAll()).toErrorDev([ - 'Warning: Unexpected type for suspenseCallback.', - ]); + ReactNoop.render(elementBadType); + await expect(async () => await waitForAll([])).toErrorDev([ + 'Warning: Unexpected type for suspenseCallback.', + ]); - const elementMissingCallback = ( - - - - ); + const elementMissingCallback = ( + + + + ); - ReactNoop.render(elementMissingCallback); - expect(() => Scheduler.unstable_flushAll()).toErrorDev([]); - }); - } + ReactNoop.render(elementMissingCallback); + await expect(async () => await waitForAll([])).toErrorDev([]); + }); // @gate www it('1 then 0 suspense callback', async () => { diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js index e430b9ef3536d..4d3abff868cb8 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js @@ -8,6 +8,7 @@ let waitForAll; let assertLog; let waitForPaint; let Suspense; +let startTransition; let getCacheForType; let caches; @@ -23,6 +24,7 @@ describe('ReactSuspenseWithNoopRenderer', () => { Scheduler = require('scheduler'); act = require('internal-test-utils').act; Suspense = React.Suspense; + startTransition = React.startTransition; const InternalTestUtils = require('internal-test-utils'); waitFor = InternalTestUtils.waitFor; waitForAll = InternalTestUtils.waitForAll; @@ -3224,9 +3226,15 @@ describe('ReactSuspenseWithNoopRenderer', () => { ReactNoop.discreteUpdates(() => { setText('B'); }); - // Update to a value that has already resolved + startTransition(() => { + setText('C'); + }); + // Assert that neither update has happened yet. Both the high pri and + // low pri updates are in the queue. + assertLog([]); + + // Resolve this before starting to render so that C doesn't suspend. await resolveText('C'); - setText('C'); }); assertLog([ // First we attempt the high pri update. It suspends. diff --git a/packages/react-test-renderer/src/__tests__/ReactTestRenderer-test.js b/packages/react-test-renderer/src/__tests__/ReactTestRenderer-test.js index a55b046fee980..44f58434c2b47 100644 --- a/packages/react-test-renderer/src/__tests__/ReactTestRenderer-test.js +++ b/packages/react-test-renderer/src/__tests__/ReactTestRenderer-test.js @@ -31,9 +31,18 @@ describe('ReactTestRenderer', () => { it('should warn if used to render a ReactDOM portal', () => { const container = document.createElement('div'); expect(() => { - expect(() => { + try { ReactTestRenderer.create(ReactDOM.createPortal('foo', container)); - }).toThrow(); + } catch (e) { + // TODO: After the update throws, a subsequent render is scheduled to + // unmount the whole tree. This update also causes an error, and this + // happens in a separate task. Flush this error now and capture it, to + // prevent it from firing asynchronously and causing the Jest test + // to fail. + expect(() => Scheduler.unstable_flushAll()).toThrow( + '.children.indexOf is not a function', + ); + } }).toErrorDev('An invalid container has been provided.', { withoutStack: true, }); diff --git a/packages/react/src/__tests__/ReactStrictMode-test.js b/packages/react/src/__tests__/ReactStrictMode-test.js index e99fe83eacf75..800607dd956ed 100644 --- a/packages/react/src/__tests__/ReactStrictMode-test.js +++ b/packages/react/src/__tests__/ReactStrictMode-test.js @@ -13,7 +13,6 @@ let React; let ReactDOM; let ReactDOMClient; let ReactDOMServer; -let Scheduler; let PropTypes; let act; let useMemo; @@ -525,10 +524,10 @@ describe('Concurrent Mode', () => { React = require('react'); ReactDOM = require('react-dom'); ReactDOMClient = require('react-dom/client'); - Scheduler = require('scheduler'); + act = require('internal-test-utils').act; }); - it('should warn about unsafe legacy lifecycle methods anywhere in a StrictMode tree', () => { + it('should warn about unsafe legacy lifecycle methods anywhere in a StrictMode tree', async () => { function StrictRoot() { return ( @@ -571,8 +570,9 @@ describe('Concurrent Mode', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - root.render(); - expect(() => Scheduler.unstable_flushAll()).toErrorDev( + await expect( + async () => await act(() => root.render()), + ).toErrorDev( [ /* eslint-disable max-len */ `Warning: Using UNSAFE_componentWillMount in strict mode is not recommended and may indicate bugs in your code. See https://reactjs.org/link/unsafe-component-lifecycles for details. @@ -597,11 +597,10 @@ Please update the following components: App`, ); // Dedupe - root.render(); - Scheduler.unstable_flushAll(); + await act(() => root.render()); }); - it('should coalesce warnings by lifecycle name', () => { + it('should coalesce warnings by lifecycle name', async () => { function StrictRoot() { return ( @@ -633,10 +632,11 @@ Please update the following components: App`, const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - root.render(); - expect(() => { - expect(() => Scheduler.unstable_flushAll()).toErrorDev( + await expect(async () => { + await expect( + async () => await act(() => root.render()), + ).toErrorDev( [ /* eslint-disable max-len */ `Warning: Using UNSAFE_componentWillMount in strict mode is not recommended and may indicate bugs in your code. See https://reactjs.org/link/unsafe-component-lifecycles for details. @@ -686,11 +686,10 @@ Please update the following components: Parent`, {withoutStack: true}, ); // Dedupe - root.render(); - Scheduler.unstable_flushAll(); + await act(() => root.render()); }); - it('should warn about components not present during the initial render', () => { + it('should warn about components not present during the initial render', async () => { function StrictRoot({foo}) { return {foo ? : }; } @@ -709,23 +708,23 @@ Please update the following components: Parent`, const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - root.render(); - expect(() => Scheduler.unstable_flushAll()).toErrorDev( + await expect(async () => { + await act(() => root.render()); + }).toErrorDev( 'Using UNSAFE_componentWillMount in strict mode is not recommended', {withoutStack: true}, ); - root.render(); - expect(() => Scheduler.unstable_flushAll()).toErrorDev( + await expect(async () => { + await act(() => root.render()); + }).toErrorDev( 'Using UNSAFE_componentWillMount in strict mode is not recommended', {withoutStack: true}, ); // Dedupe - root.render(); - Scheduler.unstable_flushAll(); - root.render(); - Scheduler.unstable_flushAll(); + await act(() => root.render()); + await act(() => root.render()); }); it('should also warn inside of "strict" mode trees', () => {