diff --git a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js index 78c7e0ee21b44..61e3505eb1758 100644 --- a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js +++ b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js @@ -984,7 +984,7 @@ describe('ReactHooksInspectionIntegration', () => { children: ['count: ', '1'], }); - act(incrementCount); + await act(async () => incrementCount()); expect(renderer.toJSON()).toEqual({ type: 'div', props: {}, diff --git a/packages/react-devtools-shared/src/__tests__/inspectedElement-test.js b/packages/react-devtools-shared/src/__tests__/inspectedElement-test.js index 6e86f42a74a12..2ec03ab8588ab 100644 --- a/packages/react-devtools-shared/src/__tests__/inspectedElement-test.js +++ b/packages/react-devtools-shared/src/__tests__/inspectedElement-test.js @@ -1069,8 +1069,8 @@ describe('InspectedElement', () => { }); async function loadPath(path) { - TestUtilsAct(() => { - TestRendererAct(() => { + await TestUtilsAct(async () => { + await TestRendererAct(async () => { inspectElementPath(path); jest.runOnlyPendingTimers(); }); @@ -1224,8 +1224,8 @@ describe('InspectedElement', () => { }); async function loadPath(path) { - TestUtilsAct(() => { - TestRendererAct(() => { + await TestUtilsAct(async () => { + await TestRendererAct(async () => { inspectElementPath(path); jest.runOnlyPendingTimers(); }); @@ -1306,8 +1306,8 @@ describe('InspectedElement', () => { }); async function loadPath(path) { - TestUtilsAct(() => { - TestRendererAct(() => { + await TestUtilsAct(async () => { + await TestRendererAct(async () => { inspectElementPath(path); jest.runOnlyPendingTimers(); }); @@ -1375,8 +1375,8 @@ describe('InspectedElement', () => { } `); - TestRendererAct(() => { - TestUtilsAct(() => { + await TestRendererAct(async () => { + await TestUtilsAct(async () => { legacyRender( { }); async function loadPath(path) { - TestUtilsAct(() => { - TestRendererAct(() => { + await TestUtilsAct(async () => { + await TestRendererAct(async () => { inspectElementPath(path); jest.runOnlyPendingTimers(); }); @@ -1513,8 +1513,8 @@ describe('InspectedElement', () => { } `); - TestRendererAct(() => { - TestUtilsAct(() => { + await TestRendererAct(async () => { + await TestUtilsAct(async () => { legacyRender( { }); async function loadPath(path) { - TestUtilsAct(() => { - TestRendererAct(() => { + await TestUtilsAct(async () => { + await TestRendererAct(async () => { inspectElementPath(path); jest.runOnlyPendingTimers(); }); @@ -1618,7 +1618,7 @@ describe('InspectedElement', () => { } `); - TestUtilsAct(() => { + await TestUtilsAct(async () => { legacyRender( { expect(inspectedElement.props).toMatchInlineSnapshot(` { "nestedObject": { - "a": { - "b": { - "value": 2, - }, - "value": 2, + "a": Dehydrated { + "preview_short": {…}, + "preview_long": {b: {…}, value: 2}, }, "value": 2, }, @@ -2833,7 +2831,7 @@ describe('InspectedElement', () => { }; const toggleError = async forceError => { await withErrorsOrWarningsIgnored(['ErrorBoundary'], async () => { - await TestUtilsAct(() => { + await TestUtilsAct(async () => { bridge.send('overrideError', { id: targetErrorBoundaryID, rendererID: store.getRendererIDForElement(targetErrorBoundaryID), @@ -2842,7 +2840,7 @@ describe('InspectedElement', () => { }); }); - TestUtilsAct(() => { + await TestUtilsAct(async () => { jest.runOnlyPendingTimers(); }); }; diff --git a/packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js b/packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js index 4d8fe6e414ede..14e928ba82efc 100644 --- a/packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js +++ b/packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js @@ -19,10 +19,8 @@ describe('Store component filters', () => { let utils; let internalAct; - const act = (callback: Function) => { - internalAct(() => { - callback(); - }); + const act = async (callback: Function) => { + internalAct(callback); jest.runAllTimers(); // Flush Bridge operations }; @@ -42,15 +40,15 @@ describe('Store component filters', () => { }); // @reactVersion >= 16.0 - it('should throw if filters are updated while profiling', () => { - act(() => store.profilerStore.startProfiling()); + it('should throw if filters are updated while profiling', async () => { + await act(async () => store.profilerStore.startProfiling()); expect(() => (store.componentFilters = [])).toThrow( 'Cannot modify filter preferences while profiling', ); }); // @reactVersion >= 16.0 - it('should support filtering by element type', () => { + it('should support filtering by element type', async () => { class ClassComponent extends React.Component<{children: React$Node}> { render() { return
{this.props.children}
; @@ -58,7 +56,7 @@ describe('Store component filters', () => { } const FunctionComponent = () =>
Hi
; - act(() => + await act(async () => legacyRender( @@ -74,8 +72,8 @@ describe('Store component filters', () => {
`); - act( - () => + await act( + async () => (store.componentFilters = [ utils.createElementTypeFilter(Types.ElementTypeHostComponent), ]), @@ -86,8 +84,8 @@ describe('Store component filters', () => { `); - act( - () => + await act( + async () => (store.componentFilters = [ utils.createElementTypeFilter(Types.ElementTypeClass), ]), @@ -99,8 +97,8 @@ describe('Store component filters', () => {
`); - act( - () => + await act( + async () => (store.componentFilters = [ utils.createElementTypeFilter(Types.ElementTypeClass), utils.createElementTypeFilter(Types.ElementTypeFunction), @@ -112,8 +110,8 @@ describe('Store component filters', () => {
`); - act( - () => + await act( + async () => (store.componentFilters = [ utils.createElementTypeFilter(Types.ElementTypeClass, false), utils.createElementTypeFilter(Types.ElementTypeFunction, false), @@ -127,7 +125,7 @@ describe('Store component filters', () => {
`); - act(() => (store.componentFilters = [])); + await act(async () => (store.componentFilters = [])); expect(store).toMatchInlineSnapshot(` [root] ▾ @@ -138,18 +136,20 @@ describe('Store component filters', () => { }); // @reactVersion >= 16.0 - it('should ignore invalid ElementTypeRoot filter', () => { + it('should ignore invalid ElementTypeRoot filter', async () => { const Component = () =>
Hi
; - act(() => legacyRender(, document.createElement('div'))); + await act(async () => + legacyRender(, document.createElement('div')), + ); expect(store).toMatchInlineSnapshot(` [root] ▾
`); - act( - () => + await act( + async () => (store.componentFilters = [ utils.createElementTypeFilter(Types.ElementTypeRoot), ]), @@ -163,13 +163,13 @@ describe('Store component filters', () => { }); // @reactVersion >= 16.2 - it('should filter by display name', () => { + it('should filter by display name', async () => { const Text = ({label}) => label; const Foo = () => ; const Bar = () => ; const Baz = () => ; - act(() => + await act(async () => legacyRender( @@ -189,8 +189,9 @@ describe('Store component filters', () => { `); - act( - () => (store.componentFilters = [utils.createDisplayNameFilter('Foo')]), + await act( + async () => + (store.componentFilters = [utils.createDisplayNameFilter('Foo')]), ); expect(store).toMatchInlineSnapshot(` [root] @@ -201,7 +202,10 @@ describe('Store component filters', () => { `); - act(() => (store.componentFilters = [utils.createDisplayNameFilter('Ba')])); + await act( + async () => + (store.componentFilters = [utils.createDisplayNameFilter('Ba')]), + ); expect(store).toMatchInlineSnapshot(` [root] ▾ @@ -210,8 +214,9 @@ describe('Store component filters', () => { `); - act( - () => (store.componentFilters = [utils.createDisplayNameFilter('B.z')]), + await act( + async () => + (store.componentFilters = [utils.createDisplayNameFilter('B.z')]), ); expect(store).toMatchInlineSnapshot(` [root] @@ -224,18 +229,20 @@ describe('Store component filters', () => { }); // @reactVersion >= 16.0 - it('should filter by path', () => { + it('should filter by path', async () => { const Component = () =>
Hi
; - act(() => legacyRender(, document.createElement('div'))); + await act(async () => + legacyRender(, document.createElement('div')), + ); expect(store).toMatchInlineSnapshot(` [root] ▾
`); - act( - () => + await act( + async () => (store.componentFilters = [ utils.createLocationFilter(__filename.replace(__dirname, '')), ]), @@ -243,8 +250,8 @@ describe('Store component filters', () => { expect(store).toMatchInlineSnapshot(`[root]`); - act( - () => + await act( + async () => (store.componentFilters = [ utils.createLocationFilter('this:is:a:made:up:path'), ]), @@ -258,14 +265,14 @@ describe('Store component filters', () => { }); // @reactVersion >= 16.0 - it('should filter HOCs', () => { + it('should filter HOCs', async () => { const Component = () =>
Hi
; const Foo = () => ; Foo.displayName = 'Foo(Component)'; const Bar = () => ; Bar.displayName = 'Bar(Foo(Component))'; - act(() => legacyRender(, document.createElement('div'))); + await act(async () => legacyRender(, document.createElement('div'))); expect(store).toMatchInlineSnapshot(` [root] ▾ [Bar][Foo] @@ -274,14 +281,18 @@ describe('Store component filters', () => {
`); - act(() => (store.componentFilters = [utils.createHOCFilter(true)])); + await act( + async () => (store.componentFilters = [utils.createHOCFilter(true)]), + ); expect(store).toMatchInlineSnapshot(` [root] ▾
`); - act(() => (store.componentFilters = [utils.createHOCFilter(false)])); + await act( + async () => (store.componentFilters = [utils.createHOCFilter(false)]), + ); expect(store).toMatchInlineSnapshot(` [root] ▾ [Bar][Foo] @@ -292,29 +303,31 @@ describe('Store component filters', () => { }); // @reactVersion >= 16.0 - it('should not send a bridge update if the set of enabled filters has not changed', () => { - act(() => (store.componentFilters = [utils.createHOCFilter(true)])); + it('should not send a bridge update if the set of enabled filters has not changed', async () => { + await act( + async () => (store.componentFilters = [utils.createHOCFilter(true)]), + ); bridge.addListener('updateComponentFilters', componentFilters => { throw Error('Unexpected component update'); }); - act( - () => + await act( + async () => (store.componentFilters = [ utils.createHOCFilter(false), utils.createHOCFilter(true), ]), ); - act( - () => + await act( + async () => (store.componentFilters = [ utils.createHOCFilter(true), utils.createLocationFilter('abc', false), ]), ); - act( - () => + await act( + async () => (store.componentFilters = [ utils.createHOCFilter(true), utils.createElementTypeFilter(Types.ElementTypeHostComponent, false), @@ -323,7 +336,7 @@ describe('Store component filters', () => { }); // @reactVersion >= 18.0 - it('should not break when Suspense nodes are filtered from the tree', () => { + it('should not break when Suspense nodes are filtered from the tree', async () => { const promise = new Promise(() => {}); const Loading = () =>
Loading...
; @@ -346,7 +359,9 @@ describe('Store component filters', () => { ]; const container = document.createElement('div'); - act(() => legacyRender(, container)); + await act(async () => + legacyRender(, container), + ); expect(store).toMatchInlineSnapshot(` [root] ▾ @@ -354,15 +369,21 @@ describe('Store component filters', () => {
`); - act(() => legacyRender(, container)); + await act(async () => + legacyRender(, container), + ); expect(store).toMatchInlineSnapshot(` + ✕ 1, ⚠ 0 [root] ▾ `); - act(() => legacyRender(, container)); + await act(async () => + legacyRender(, container), + ); expect(store).toMatchInlineSnapshot(` + ✕ 2, ⚠ 0 [root] ▾ @@ -372,7 +393,7 @@ describe('Store component filters', () => { describe('inline errors and warnings', () => { // @reactVersion >= 17.0 - it('only counts for unfiltered components', () => { + it('only counts for unfiltered components', async () => { function ComponentWithWarning() { console.warn('test-only: render warning'); return null; @@ -395,31 +416,29 @@ describe('Store component filters', () => { require('react-dom'); const container = document.createElement('div'); + await act( + async () => + (store.componentFilters = [ + utils.createDisplayNameFilter('Warning'), + utils.createDisplayNameFilter('Error'), + ]), + ); utils.withErrorsOrWarningsIgnored(['test-only:'], () => { - act( - () => - (store.componentFilters = [ - utils.createDisplayNameFilter('Warning'), - utils.createDisplayNameFilter('Error'), - ]), - ); - act(() => - legacyRender( - - - - - , - container, - ), + legacyRender( + + + + + , + container, ); }); - expect(store).toMatchInlineSnapshot(`[root]`); + expect(store).toMatchInlineSnapshot(``); expect(store.errorCount).toBe(0); expect(store.warningCount).toBe(0); - act(() => (store.componentFilters = [])); + await act(async () => (store.componentFilters = [])); expect(store).toMatchInlineSnapshot(` ✕ 2, ⚠ 2 [root] @@ -428,8 +447,8 @@ describe('Store component filters', () => { ✕⚠ `); - act( - () => + await act( + async () => (store.componentFilters = [utils.createDisplayNameFilter('Warning')]), ); expect(store).toMatchInlineSnapshot(` @@ -438,8 +457,8 @@ describe('Store component filters', () => { ✕ `); - act( - () => + await act( + async () => (store.componentFilters = [utils.createDisplayNameFilter('Error')]), ); expect(store).toMatchInlineSnapshot(` @@ -448,8 +467,8 @@ describe('Store component filters', () => { ⚠ `); - act( - () => + await act( + async () => (store.componentFilters = [ utils.createDisplayNameFilter('Warning'), utils.createDisplayNameFilter('Error'), @@ -459,7 +478,7 @@ describe('Store component filters', () => { expect(store.errorCount).toBe(0); expect(store.warningCount).toBe(0); - act(() => (store.componentFilters = [])); + await act(async () => (store.componentFilters = [])); expect(store).toMatchInlineSnapshot(` ✕ 2, ⚠ 2 [root] diff --git a/packages/react-dom/src/__tests__/utils/ReactDOMServerIntegrationTestUtils.js b/packages/react-dom/src/__tests__/utils/ReactDOMServerIntegrationTestUtils.js index 5aa5cad24842a..868c59be61a09 100644 --- a/packages/react-dom/src/__tests__/utils/ReactDOMServerIntegrationTestUtils.js +++ b/packages/react-dom/src/__tests__/utils/ReactDOMServerIntegrationTestUtils.js @@ -48,21 +48,16 @@ module.exports = function (initModules) { // ==================================== // promisified version of ReactDOM.render() - function asyncReactDOMRender(reactElement, domElement, forceHydrate) { - return new Promise(resolve => { - if (forceHydrate) { - act(() => { - ReactDOM.hydrate(reactElement, domElement); - }); - } else { - act(() => { - ReactDOM.render(reactElement, domElement); - }); - } - // We can't use the callback for resolution because that will not catch - // errors. They're thrown. - resolve(); - }); + async function asyncReactDOMRender(reactElement, domElement, forceHydrate) { + if (forceHydrate) { + await act(async () => { + ReactDOM.hydrate(reactElement, domElement); + }); + } else { + await act(async () => { + ReactDOM.render(reactElement, domElement); + }); + } } // performs fn asynchronously and expects count errors logged to console.error. // will fail the test if the count of errors logged is not equal to count. diff --git a/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js b/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js index 4f0b03f42143e..8a81d5582f6e0 100644 --- a/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js +++ b/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js @@ -2520,7 +2520,7 @@ describe('DOMPluginEventSystem', () => { }); // @gate www - it('beforeblur and afterblur are called after a focused element is suspended', () => { + it('beforeblur and afterblur are called after a focused element is suspended', async () => { const log = []; // We have to persist here because we want to read relatedTarget later. const onAfterBlur = jest.fn(e => { @@ -2575,7 +2575,7 @@ describe('DOMPluginEventSystem', () => { const root = ReactDOMClient.createRoot(container2); - act(() => { + await act(async () => { root.render(); }); jest.runAllTimers(); @@ -2587,7 +2587,7 @@ describe('DOMPluginEventSystem', () => { expect(onAfterBlur).toHaveBeenCalledTimes(0); suspend = true; - act(() => { + await act(async () => { root.render(); }); jest.runAllTimers(); @@ -2604,7 +2604,7 @@ describe('DOMPluginEventSystem', () => { }); // @gate www - it('beforeblur should skip handlers from a deleted subtree after the focused element is suspended', () => { + it('beforeblur should skip handlers from a deleted subtree after the focused element is suspended', async () => { const onBeforeBlur = jest.fn(); const innerRef = React.createRef(); const innerRef2 = React.createRef(); @@ -2661,7 +2661,7 @@ describe('DOMPluginEventSystem', () => { const root = ReactDOMClient.createRoot(container2); - act(() => { + await act(async () => { root.render(); }); jest.runAllTimers(); @@ -2672,7 +2672,7 @@ describe('DOMPluginEventSystem', () => { expect(onBeforeBlur).toHaveBeenCalledTimes(0); suspend = true; - act(() => { + await act(async () => { root.render(); }); jest.runAllTimers(); @@ -2684,17 +2684,17 @@ describe('DOMPluginEventSystem', () => { }); // @gate www - it('regression: does not fire beforeblur/afterblur if target is already hidden', () => { + it('regression: does not fire beforeblur/afterblur if target is already hidden', async () => { const Suspense = React.Suspense; let suspend = false; - const promise = Promise.resolve(); + const fakePromise = {then() {}}; const setBeforeBlurHandle = ReactDOM.unstable_createEventHandle('beforeblur'); const innerRef = React.createRef(); function Child() { if (suspend) { - throw promise; + throw fakePromise; } return ; } @@ -2726,7 +2726,7 @@ describe('DOMPluginEventSystem', () => { document.body.appendChild(container2); const root = ReactDOMClient.createRoot(container2); - act(() => { + await act(async () => { root.render(); }); @@ -2737,7 +2737,7 @@ describe('DOMPluginEventSystem', () => { // Suspend. This hides the input node, causing it to lose focus. suspend = true; - act(() => { + await act(async () => { root.render(); }); diff --git a/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js b/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js index df5af903a7216..dc4ef2135f281 100644 --- a/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js @@ -20,6 +20,7 @@ let ReactDOMServer; let act; let assertLog; let waitForAll; +let waitForThrow; describe('ReactHooks', () => { beforeEach(() => { @@ -36,6 +37,7 @@ describe('ReactHooks', () => { const InternalTestUtils = require('internal-test-utils'); assertLog = InternalTestUtils.assertLog; waitForAll = InternalTestUtils.waitForAll; + waitForThrow = InternalTestUtils.waitForThrow; }); if (__DEV__) { @@ -90,7 +92,7 @@ describe('ReactHooks', () => { expect(root).toMatchRenderedOutput('0, 0'); // Normal update - act(() => { + await act(async () => { setCounter1(1); setCounter2(1); }); @@ -98,12 +100,12 @@ describe('ReactHooks', () => { assertLog(['Parent: 1, 1', 'Child: 1, 1', 'Effect: 1, 1']); // Update that bails out. - act(() => setCounter1(1)); + await act(async () => setCounter1(1)); assertLog(['Parent: 1, 1']); // This time, one of the state updates but the other one doesn't. So we // can't bail out. - act(() => { + await act(async () => { setCounter1(1); setCounter2(2); }); @@ -111,7 +113,7 @@ describe('ReactHooks', () => { assertLog(['Parent: 1, 2', 'Child: 1, 2', 'Effect: 1, 2']); // Lots of updates that eventually resolve to the current values. - act(() => { + await act(async () => { setCounter1(9); setCounter2(3); setCounter1(4); @@ -125,7 +127,7 @@ describe('ReactHooks', () => { assertLog(['Parent: 1, 2']); // prepare to check SameValue - act(() => { + await act(async () => { setCounter1(0 / -1); setCounter2(NaN); }); @@ -133,7 +135,7 @@ describe('ReactHooks', () => { assertLog(['Parent: 0, NaN', 'Child: 0, NaN', 'Effect: 0, NaN']); // check if re-setting to negative 0 / NaN still bails out - act(() => { + await act(async () => { setCounter1(0 / -1); setCounter2(NaN); setCounter2(Infinity); @@ -143,7 +145,7 @@ describe('ReactHooks', () => { assertLog(['Parent: 0, NaN']); // check if changing negative 0 to positive 0 does not bail out - act(() => { + await act(async () => { setCounter1(0); }); assertLog(['Parent: 0, NaN', 'Child: 0, NaN', 'Effect: 0, NaN']); @@ -178,7 +180,7 @@ describe('ReactHooks', () => { expect(root).toMatchRenderedOutput('0, 0 (light)'); // Normal update - act(() => { + await act(async () => { setCounter1(1); setCounter2(1); }); @@ -186,12 +188,12 @@ describe('ReactHooks', () => { assertLog(['Parent: 1, 1 (light)', 'Child: 1, 1 (light)']); // Update that bails out. - act(() => setCounter1(1)); + await act(async () => setCounter1(1)); assertLog(['Parent: 1, 1 (light)']); // This time, one of the state updates but the other one doesn't. So we // can't bail out. - act(() => { + await act(async () => { setCounter1(1); setCounter2(2); }); @@ -200,7 +202,7 @@ describe('ReactHooks', () => { // Updates bail out, but component still renders because props // have changed - act(() => { + await act(async () => { setCounter1(1); setCounter2(2); root.update(); @@ -209,7 +211,7 @@ describe('ReactHooks', () => { assertLog(['Parent: 1, 2 (dark)', 'Child: 1, 2 (dark)']); // Both props and state bail out - act(() => { + await act(async () => { setCounter1(1); setCounter2(2); root.update(); @@ -235,8 +237,8 @@ describe('ReactHooks', () => { await waitForAll(['Count: 0']); expect(root).toMatchRenderedOutput('0'); - expect(() => { - act(() => + await expect(async () => { + await act(async () => setCounter(1, () => { throw new Error('Expected to ignore the callback.'); }), @@ -269,8 +271,8 @@ describe('ReactHooks', () => { await waitForAll(['Count: 0']); expect(root).toMatchRenderedOutput('0'); - expect(() => { - act(() => + await expect(async () => { + await act(async () => dispatch(1, () => { throw new Error('Expected to ignore the callback.'); }), @@ -321,7 +323,7 @@ describe('ReactHooks', () => { return ; } const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true}); - act(() => { + await act(async () => { root.update( @@ -344,18 +346,18 @@ describe('ReactHooks', () => { expect(root).toMatchRenderedOutput('0 (light)'); // Normal update - act(() => setCounter(1)); + await act(async () => setCounter(1)); assertLog(['Parent: 1 (light)', 'Child: 1 (light)', 'Effect: 1 (light)']); expect(root).toMatchRenderedOutput('1 (light)'); // Update that doesn't change state, so it bails out - act(() => setCounter(1)); + await act(async () => setCounter(1)); assertLog(['Parent: 1 (light)']); expect(root).toMatchRenderedOutput('1 (light)'); // Update that doesn't change state, but the context changes, too, so it // can't bail out - act(() => { + await act(async () => { setCounter(1); setTheme('dark'); }); @@ -394,7 +396,7 @@ describe('ReactHooks', () => { expect(root).toMatchRenderedOutput('0'); // Normal update - act(() => setCounter(1)); + await act(async () => setCounter(1)); assertLog(['Parent: 1', 'Child: 1', 'Effect: 1']); expect(root).toMatchRenderedOutput('1'); @@ -402,30 +404,30 @@ describe('ReactHooks', () => { // because the alternate fiber has pending update priority, so we have to // enter the render phase before we can bail out. But we bail out before // rendering the child, and we don't fire any effects. - act(() => setCounter(1)); + await act(async () => setCounter(1)); assertLog(['Parent: 1']); expect(root).toMatchRenderedOutput('1'); // Update to the same state again. This times, neither fiber has pending // update priority, so we can bail out before even entering the render phase. - act(() => setCounter(1)); + await act(async () => setCounter(1)); await waitForAll([]); expect(root).toMatchRenderedOutput('1'); // This changes the state to something different so it renders normally. - act(() => setCounter(2)); + await act(async () => setCounter(2)); assertLog(['Parent: 2', 'Child: 2', 'Effect: 2']); expect(root).toMatchRenderedOutput('2'); // prepare to check SameValue - act(() => { + await act(async () => { setCounter(0); }); assertLog(['Parent: 0', 'Child: 0', 'Effect: 0']); expect(root).toMatchRenderedOutput('0'); // Update to the same state for the first time to flush the queue - act(() => { + await act(async () => { setCounter(0); }); @@ -433,14 +435,14 @@ describe('ReactHooks', () => { expect(root).toMatchRenderedOutput('0'); // Update again to the same state. Should bail out. - act(() => { + await act(async () => { setCounter(0); }); await waitForAll([]); expect(root).toMatchRenderedOutput('0'); // Update to a different state (positive 0 to negative 0) - act(() => { + await act(async () => { setCounter(0 / -1); }); assertLog(['Parent: 0', 'Child: 0', 'Effect: 0']); @@ -615,7 +617,7 @@ describe('ReactHooks', () => { ]); }); - it('warns if deps is not an array', () => { + it('warns if deps is not an array', async () => { const {useEffect, useLayoutEffect, useMemo, useCallback} = React; function App(props) { @@ -626,8 +628,8 @@ describe('ReactHooks', () => { return null; } - expect(() => { - act(() => { + await expect(async () => { + await act(async () => { ReactTestRenderer.create(); }); }).toErrorDev([ @@ -640,8 +642,8 @@ describe('ReactHooks', () => { 'Warning: useCallback received a final argument that is not an array (instead, received `string`). ' + 'When specified, the final argument must be an array.', ]); - expect(() => { - act(() => { + await expect(async () => { + await act(async () => { ReactTestRenderer.create(); }); }).toErrorDev([ @@ -654,8 +656,8 @@ describe('ReactHooks', () => { 'Warning: useCallback received a final argument that is not an array (instead, received `number`). ' + 'When specified, the final argument must be an array.', ]); - expect(() => { - act(() => { + await expect(async () => { + await act(async () => { ReactTestRenderer.create(); }); }).toErrorDev([ @@ -669,7 +671,7 @@ describe('ReactHooks', () => { 'When specified, the final argument must be an array.', ]); - act(() => { + await act(async () => { ReactTestRenderer.create(); ReactTestRenderer.create(); ReactTestRenderer.create(); @@ -695,7 +697,7 @@ describe('ReactHooks', () => { ReactTestRenderer.create(); }); - it('does not forget render phase useState updates inside an effect', () => { + it('does not forget render phase useState updates inside an effect', async () => { const {useState, useEffect} = React; function Counter() { @@ -712,13 +714,13 @@ describe('ReactHooks', () => { } const root = ReactTestRenderer.create(null); - act(() => { + await act(async () => { root.update(); }); expect(root).toMatchRenderedOutput('4'); }); - it('does not forget render phase useReducer updates inside an effect with hoisted reducer', () => { + it('does not forget render phase useReducer updates inside an effect with hoisted reducer', async () => { const {useReducer, useEffect} = React; const reducer = x => x + 1; @@ -736,13 +738,13 @@ describe('ReactHooks', () => { } const root = ReactTestRenderer.create(null); - act(() => { + await act(async () => { root.update(); }); expect(root).toMatchRenderedOutput('4'); }); - it('does not forget render phase useReducer updates inside an effect with inline reducer', () => { + it('does not forget render phase useReducer updates inside an effect with inline reducer', async () => { const {useReducer, useEffect} = React; function Counter() { @@ -759,7 +761,7 @@ describe('ReactHooks', () => { } const root = ReactTestRenderer.create(null); - act(() => { + await act(async () => { root.update(); }); expect(root).toMatchRenderedOutput('4'); @@ -914,7 +916,7 @@ describe('ReactHooks', () => { }); // Throws because there's no runtime cost for being strict here. - it('throws when reading context inside useEffect', () => { + it('throws when reading context inside useEffect', async () => { const {useEffect, createContext} = React; const ReactCurrentDispatcher = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED @@ -928,14 +930,11 @@ describe('ReactHooks', () => { return null; } - expect(() => { - act(() => { - ReactTestRenderer.create(); - }); - }).toThrow( + await act(async () => { + ReactTestRenderer.create(); // The exact message doesn't matter, just make sure we don't allow this - 'Context can only be read while React is rendering', - ); + await waitForThrow('Context can only be read while React is rendering'); + }); }); // Throws because there's no runtime cost for being strict here. @@ -1070,7 +1069,7 @@ describe('ReactHooks', () => { ); }); - it('resets warning internal state when interrupted by an error', () => { + it('resets warning internal state when interrupted by an error', async () => { const ReactCurrentDispatcher = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED .ReactCurrentDispatcher; @@ -1132,7 +1131,7 @@ describe('ReactHooks', () => { } // Verify it doesn't think we're still inside a Hook. // Should have no warnings. - act(() => { + await act(async () => { ReactTestRenderer.create(); }); @@ -1473,7 +1472,7 @@ describe('ReactHooks', () => { .replace('use', '') .replace('Helper', ''); - it(`warns on using differently ordered hooks (${hookNameA}, ${hookNameB}) on subsequent renders`, () => { + it(`warns on using differently ordered hooks (${hookNameA}, ${hookNameB}) on subsequent renders`, async () => { function App(props) { /* eslint-disable no-unused-vars */ if (props.update) { @@ -1489,12 +1488,12 @@ describe('ReactHooks', () => { /* eslint-enable no-unused-vars */ } let root; - act(() => { + await act(async () => { root = ReactTestRenderer.create(); }); - expect(() => { + await expect(async () => { try { - act(() => { + await act(async () => { root.update(); }); } catch (error) { @@ -1515,7 +1514,7 @@ describe('ReactHooks', () => { // further warnings for this component are silenced try { - act(() => { + await act(async () => { root.update(); }); } catch (error) { @@ -1525,7 +1524,7 @@ describe('ReactHooks', () => { } }); - it(`warns when more hooks (${hookNameA}, ${hookNameB}) are used during update than mount`, () => { + it(`warns when more hooks (${hookNameA}, ${hookNameB}) are used during update than mount`, async () => { function App(props) { /* eslint-disable no-unused-vars */ if (props.update) { @@ -1538,13 +1537,13 @@ describe('ReactHooks', () => { /* eslint-enable no-unused-vars */ } let root; - act(() => { + await act(async () => { root = ReactTestRenderer.create(); }); - expect(() => { + await expect(async () => { try { - act(() => { + await act(async () => { root.update(); }); } catch (error) { @@ -1579,7 +1578,7 @@ describe('ReactHooks', () => { .replace('use', '') .replace('Helper', ''); - it(`warns when fewer hooks (${hookNameA}, ${hookNameB}) are used during update than mount`, () => { + it(`warns when fewer hooks (${hookNameA}, ${hookNameB}) are used during update than mount`, async () => { function App(props) { /* eslint-disable no-unused-vars */ if (props.update) { @@ -1592,15 +1591,15 @@ describe('ReactHooks', () => { /* eslint-enable no-unused-vars */ } let root; - act(() => { + await act(async () => { root = ReactTestRenderer.create(); }); - expect(() => { - act(() => { + await act(async () => { + expect(() => { root.update(); - }); - }).toThrow('Rendered fewer hooks than expected.'); + }).toThrow('Rendered fewer hooks than expected. '); + }); }); }); @@ -1726,7 +1725,7 @@ describe('ReactHooks', () => { }); // Regression test for https://github.com/facebook/react/issues/15057 - it('does not fire a false positive warning when previous effect unmounts the component', () => { + it('does not fire a false positive warning when previous effect unmounts the component', async () => { const {useState, useEffect} = React; let globalListener; @@ -1762,7 +1761,7 @@ describe('ReactHooks', () => { return null; } - act(() => { + await act(async () => { ReactTestRenderer.create(); }); @@ -1912,7 +1911,7 @@ describe('ReactHooks', () => { } let root; - act(() => { + await act(async () => { root = ReactTestRenderer.create( @@ -1921,7 +1920,7 @@ describe('ReactHooks', () => { }); expect(root).toMatchRenderedOutput('Throw!'); - act(() => setShouldThrow(true)); + await act(async () => setShouldThrow(true)); expect(root).toMatchRenderedOutput('Error!'); }); }); diff --git a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js index 7325a3cf79b32..4d1668263c8d7 100644 --- a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js +++ b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js @@ -294,11 +294,11 @@ describe('ReactHooksWithNoopRenderer', () => { await waitForAll(['Count: 0']); expect(ReactNoop).toMatchRenderedOutput(); - act(() => counter.current.updateCount(1)); + await act(async () => counter.current.updateCount(1)); assertLog(['Count: 1']); expect(ReactNoop).toMatchRenderedOutput(); - act(() => counter.current.updateCount(count => count + 10)); + await act(async () => counter.current.updateCount(count => count + 10)); assertLog(['Count: 11']); expect(ReactNoop).toMatchRenderedOutput(); }); @@ -318,7 +318,7 @@ describe('ReactHooksWithNoopRenderer', () => { await waitForAll(['getInitialState', 'Count: 42']); expect(ReactNoop).toMatchRenderedOutput(); - act(() => counter.current.updateCount(7)); + await act(async () => counter.current.updateCount(7)); assertLog(['Count: 7']); expect(ReactNoop).toMatchRenderedOutput(); }); @@ -336,10 +336,10 @@ describe('ReactHooksWithNoopRenderer', () => { await waitForAll(['Count: 0']); expect(ReactNoop).toMatchRenderedOutput(); - act(() => counter.current.updateCount(7)); + await act(async () => counter.current.updateCount(7)); assertLog(['Count: 7']); - act(() => counter.current.updateLabel('Total')); + await act(async () => counter.current.updateLabel('Total')); assertLog(['Total: 7']); }); @@ -356,13 +356,13 @@ describe('ReactHooksWithNoopRenderer', () => { const firstUpdater = updater; - act(() => firstUpdater(1)); + await act(async () => firstUpdater(1)); assertLog(['Count: 1']); expect(ReactNoop).toMatchRenderedOutput(); const secondUpdater = updater; - act(() => firstUpdater(count => count + 10)); + await act(async () => firstUpdater(count => count + 10)); assertLog(['Count: 11']); expect(ReactNoop).toMatchRenderedOutput(); @@ -381,7 +381,7 @@ describe('ReactHooksWithNoopRenderer', () => { await waitForAll([]); ReactNoop.render(null); await waitForAll([]); - act(() => _updateCount(1)); + await act(async () => _updateCount(1)); }); it('works with memo', async () => { @@ -401,7 +401,7 @@ describe('ReactHooksWithNoopRenderer', () => { await waitForAll([]); expect(ReactNoop).toMatchRenderedOutput(); - act(() => _updateCount(1)); + await act(async () => _updateCount(1)); assertLog(['Count: 1']); expect(ReactNoop).toMatchRenderedOutput(); }); @@ -637,7 +637,7 @@ describe('ReactHooksWithNoopRenderer', () => { // Test that it works on update, too. This time the log is a bit different // because we started with reducerB instead of reducerA. - act(() => { + await act(async () => { counter.current.dispatch('reset'); }); ReactNoop.render(); @@ -851,10 +851,10 @@ describe('ReactHooksWithNoopRenderer', () => { await waitForAll(['Count: 0']); expect(ReactNoop).toMatchRenderedOutput(); - act(() => counter.current.dispatch(INCREMENT)); + await act(async () => counter.current.dispatch(INCREMENT)); assertLog(['Count: 1']); expect(ReactNoop).toMatchRenderedOutput(); - act(() => { + await act(async () => { counter.current.dispatch(DECREMENT); counter.current.dispatch(DECREMENT); counter.current.dispatch(DECREMENT); @@ -893,11 +893,11 @@ describe('ReactHooksWithNoopRenderer', () => { await waitForAll(['Init', 'Count: 10']); expect(ReactNoop).toMatchRenderedOutput(); - act(() => counter.current.dispatch(INCREMENT)); + await act(async () => counter.current.dispatch(INCREMENT)); assertLog(['Count: 11']); expect(ReactNoop).toMatchRenderedOutput(); - act(() => { + await act(async () => { counter.current.dispatch(DECREMENT); counter.current.dispatch(DECREMENT); counter.current.dispatch(DECREMENT); @@ -1427,7 +1427,7 @@ describe('ReactHooksWithNoopRenderer', () => { return state; } - act(() => { + await act(async () => { ReactNoop.renderToRootWithID(, 'root', () => Scheduler.log('Sync effect'), ); @@ -1437,7 +1437,7 @@ describe('ReactHooksWithNoopRenderer', () => { ReactNoop.unmountRootWithID('root'); await waitForAll(['passive destroy']); - act(() => { + await act(async () => { updaterFunction(true); }); }); @@ -1624,7 +1624,7 @@ describe('ReactHooksWithNoopRenderer', () => { expect(ReactNoop).toMatchRenderedOutput(); // A flush sync doesn't cause the passive effects to fire. // So we haven't added the other update yet. - act(() => { + await act(async () => { ReactNoop.flushSync(() => { _updateCount(2); }); @@ -2256,7 +2256,7 @@ describe('ReactHooksWithNoopRenderer', () => { // @gate skipUnmountedBoundaries it('should use the nearest still-mounted boundary if there are no unmounted boundaries', async () => { - act(() => { + await act(async () => { ReactNoop.render( @@ -2269,7 +2269,7 @@ describe('ReactHooksWithNoopRenderer', () => { 'BrokenUseEffectCleanup useEffect', ]); - act(() => { + await act(async () => { ReactNoop.render(); }); @@ -2294,7 +2294,7 @@ describe('ReactHooksWithNoopRenderer', () => { } } - act(() => { + await act(async () => { ReactNoop.render( @@ -2308,7 +2308,7 @@ describe('ReactHooksWithNoopRenderer', () => { 'BrokenUseEffectCleanup useEffect', ]); - act(() => { + await act(async () => { ReactNoop.render( @@ -2333,7 +2333,7 @@ describe('ReactHooksWithNoopRenderer', () => { } } - act(() => { + await act(async () => { ReactNoop.render( @@ -2346,7 +2346,7 @@ describe('ReactHooksWithNoopRenderer', () => { 'BrokenUseEffectCleanup useEffect', ]); - act(() => { + await act(async () => { ReactNoop.render( @@ -2381,7 +2381,7 @@ describe('ReactHooksWithNoopRenderer', () => { } } - act(() => { + await act(async () => { ReactNoop.render(); }); @@ -2390,11 +2390,10 @@ describe('ReactHooksWithNoopRenderer', () => { 'BrokenUseEffectCleanup useEffect', ]); - expect(() => { - act(() => { - ReactNoop.render(); - }); - }).toThrow('Expected error'); + await act(async () => { + ReactNoop.render(); + await waitForThrow('Expected error'); + }); assertLog(['BrokenUseEffectCleanup useEffect destroy']); @@ -2425,7 +2424,7 @@ describe('ReactHooksWithNoopRenderer', () => { prevProps.prop === nextProps.prop; const MemoizedChild = React.memo(Child, isEqual); - act(() => { + await act(async () => { ReactNoop.render( @@ -2435,7 +2434,7 @@ describe('ReactHooksWithNoopRenderer', () => { assertLog(['render', 'layout create', 'passive create']); // Include at least one no-op (memoized) update to trigger original bug. - act(() => { + await act(async () => { ReactNoop.render( @@ -2444,7 +2443,7 @@ describe('ReactHooksWithNoopRenderer', () => { }); assertLog([]); - act(() => { + await act(async () => { ReactNoop.render( @@ -2459,7 +2458,7 @@ describe('ReactHooksWithNoopRenderer', () => { 'passive create', ]); - act(() => { + await act(async () => { ReactNoop.render(null); }); assertLog(['layout destroy', 'passive destroy']); @@ -2492,7 +2491,7 @@ describe('ReactHooksWithNoopRenderer', () => { prevProps.prop === nextProps.prop; const MemoizedChild = React.memo(Child, isEqual); - act(() => { + await act(async () => { ReactNoop.render( @@ -2502,7 +2501,7 @@ describe('ReactHooksWithNoopRenderer', () => { assertLog(['render', 'layout create', 'passive create']); // Include at least one no-op (memoized) update to trigger original bug. - act(() => { + await act(async () => { ReactNoop.render( @@ -2511,7 +2510,7 @@ describe('ReactHooksWithNoopRenderer', () => { }); assertLog([]); - act(() => { + await act(async () => { ReactNoop.render( @@ -2526,12 +2525,16 @@ describe('ReactHooksWithNoopRenderer', () => { 'passive create', ]); - act(() => { + await act(async () => { ReactNoop.render(null); }); assertLog(['layout destroy', 'passive destroy']); }); + // TODO: This test fails when skipUnmountedBoundaries is disabled. However, + // it's also rolled out to open source already and partially to www. So + // we should probably just land it. + // @gate skipUnmountedBoundaries it('assumes passive effect destroy function is either a function or undefined', async () => { function App(props) { useEffect(() => { @@ -2541,8 +2544,8 @@ describe('ReactHooksWithNoopRenderer', () => { } const root1 = ReactNoop.createRoot(); - expect(() => { - act(() => { + await expect(async () => { + await act(async () => { root1.render(); }); }).toErrorDev([ @@ -2551,8 +2554,8 @@ describe('ReactHooksWithNoopRenderer', () => { ]); const root2 = ReactNoop.createRoot(); - expect(() => { - act(() => { + await expect(async () => { + await act(async () => { root2.render(); }); }).toErrorDev([ @@ -2562,8 +2565,8 @@ describe('ReactHooksWithNoopRenderer', () => { ]); const root3 = ReactNoop.createRoot(); - expect(() => { - act(() => { + await expect(async () => { + await act(async () => { root3.render(); }); }).toErrorDev([ @@ -2573,11 +2576,10 @@ describe('ReactHooksWithNoopRenderer', () => { ]); // Error on unmount because React assumes the value is a function - expect(() => - act(() => { - root3.unmount(); - }), - ).toThrow('is not a function'); + await act(async () => { + root3.render(null); + await waitForThrow('is not a function'); + }); }); }); @@ -2921,8 +2923,8 @@ describe('ReactHooksWithNoopRenderer', () => { } const root1 = ReactNoop.createRoot(); - expect(() => { - act(() => { + await expect(async () => { + await act(async () => { root1.render(); }); }).toErrorDev([ @@ -2931,8 +2933,8 @@ describe('ReactHooksWithNoopRenderer', () => { ]); const root2 = ReactNoop.createRoot(); - expect(() => { - act(() => { + await expect(async () => { + await act(async () => { root2.render(); }); }).toErrorDev([ @@ -2942,8 +2944,8 @@ describe('ReactHooksWithNoopRenderer', () => { ]); const root3 = ReactNoop.createRoot(); - expect(() => { - act(() => { + await expect(async () => { + await act(async () => { root3.render(); }); }).toErrorDev([ @@ -2953,11 +2955,10 @@ describe('ReactHooksWithNoopRenderer', () => { ]); // Error on unmount because React assumes the value is a function - expect(() => - act(() => { - root3.unmount(); - }), - ).toThrow('is not a function'); + await act(async () => { + root3.render(null); + await waitForThrow('is not a function'); + }); }); it('warns when setState is called from insertion effect setup', async () => { @@ -2973,17 +2974,16 @@ describe('ReactHooksWithNoopRenderer', () => { } const root = ReactNoop.createRoot(); - expect(() => { - act(() => { + await expect(async () => { + await act(async () => { root.render(); }); }).toErrorDev(['Warning: useInsertionEffect must not schedule updates.']); - expect(() => { - act(() => { - root.render(); - }); - }).toThrow('No'); + await act(async () => { + root.render(); + await waitForThrow('No'); + }); // Should not warn for regular effects after throw. function NotInsertion() { @@ -2993,7 +2993,7 @@ describe('ReactHooksWithNoopRenderer', () => { }, []); return null; } - act(() => { + await act(async () => { root.render(); }); }); @@ -3013,20 +3013,19 @@ describe('ReactHooksWithNoopRenderer', () => { } const root = ReactNoop.createRoot(); - act(() => { + await act(async () => { root.render(); }); - expect(() => { - act(() => { + await expect(async () => { + await act(async () => { root.render(); }); }).toErrorDev(['Warning: useInsertionEffect must not schedule updates.']); - expect(() => { - act(() => { - root.render(); - }); - }).toThrow('No'); + await act(async () => { + root.render(); + await waitForThrow('No'); + }); // Should not warn for regular effects after throw. function NotInsertion() { @@ -3036,7 +3035,7 @@ describe('ReactHooksWithNoopRenderer', () => { }, []); return null; } - act(() => { + await act(async () => { root.render(); }); }); @@ -3204,8 +3203,8 @@ describe('ReactHooksWithNoopRenderer', () => { } const root1 = ReactNoop.createRoot(); - expect(() => { - act(() => { + await expect(async () => { + await act(async () => { root1.render(); }); }).toErrorDev([ @@ -3214,8 +3213,8 @@ describe('ReactHooksWithNoopRenderer', () => { ]); const root2 = ReactNoop.createRoot(); - expect(() => { - act(() => { + await expect(async () => { + await act(async () => { root2.render(); }); }).toErrorDev([ @@ -3225,8 +3224,8 @@ describe('ReactHooksWithNoopRenderer', () => { ]); const root3 = ReactNoop.createRoot(); - expect(() => { - act(() => { + await expect(async () => { + await act(async () => { root3.render(); }); }).toErrorDev([ @@ -3236,11 +3235,10 @@ describe('ReactHooksWithNoopRenderer', () => { ]); // Error on unmount because React assumes the value is a function - expect(() => - act(() => { - root3.unmount(); - }), - ).toThrow('is not a function'); + await act(async () => { + root3.render(null); + await waitForThrow('is not a function'); + }); }); }); @@ -3279,7 +3277,7 @@ describe('ReactHooksWithNoopRenderer', () => { , ); - act(button.current.increment); + await act(async () => button.current.increment()); assertLog([ // Button should not re-render, because its props haven't changed // 'Increment', @@ -3307,7 +3305,7 @@ describe('ReactHooksWithNoopRenderer', () => { ); // Callback should have updated - act(button.current.increment); + await act(async () => button.current.increment()); assertLog(['Count: 11']); expect(ReactNoop).toMatchRenderedOutput( <> @@ -3425,7 +3423,7 @@ describe('ReactHooksWithNoopRenderer', () => { expect(ReactNoop).toMatchRenderedOutput(); expect(counter.current.count).toBe(0); - act(() => { + await act(async () => { counter.current.dispatch(INCREMENT); }); assertLog(['Count: 1']); @@ -3455,7 +3453,7 @@ describe('ReactHooksWithNoopRenderer', () => { expect(ReactNoop).toMatchRenderedOutput(); expect(counter.current.count).toBe(0); - act(() => { + await act(async () => { counter.current.dispatch(INCREMENT); }); assertLog(['Count: 1']); @@ -3492,7 +3490,7 @@ describe('ReactHooksWithNoopRenderer', () => { expect(counter.current.count).toBe(0); expect(totalRefUpdates).toBe(1); - act(() => { + await act(async () => { counter.current.dispatch(INCREMENT); }); assertLog(['Count: 1']); @@ -3591,7 +3589,7 @@ describe('ReactHooksWithNoopRenderer', () => { ); } - act(() => { + await act(async () => { ReactNoop.render(); }); @@ -3689,7 +3687,7 @@ describe('ReactHooksWithNoopRenderer', () => { , ); - act(() => { + await act(async () => { updateA(2); updateB(3); }); @@ -3751,7 +3749,7 @@ describe('ReactHooksWithNoopRenderer', () => { ReactNoop.render(); await waitForAll(['A: 0, B: 0, C: 0']); expect(ReactNoop).toMatchRenderedOutput(); - act(() => { + await act(async () => { updateA(2); updateB(3); updateC(4); @@ -3857,7 +3855,7 @@ describe('ReactHooksWithNoopRenderer', () => { expect(ReactNoop).toMatchRenderedOutput('1'); }); - act(() => { + await act(async () => { setCounter(2); }); assertLog(['Render: 1', 'Effect: 2', 'Reducer: 2', 'Render: 2']); @@ -3892,7 +3890,7 @@ describe('ReactHooksWithNoopRenderer', () => { await waitForAll(['Render disabled: true', 'Render count: 0']); expect(ReactNoop).toMatchRenderedOutput('0'); - act(() => { + await act(async () => { // These increments should have no effect, since disabled=true increment(); increment(); @@ -3901,7 +3899,7 @@ describe('ReactHooksWithNoopRenderer', () => { assertLog(['Render disabled: true', 'Render count: 0']); expect(ReactNoop).toMatchRenderedOutput('0'); - act(() => { + await act(async () => { // Enabling the updater should *not* replay the previous increment() actions setDisabled(false); }); @@ -3941,7 +3939,7 @@ describe('ReactHooksWithNoopRenderer', () => { await waitForAll(['Render disabled: true', 'Render count: 0']); expect(ReactNoop).toMatchRenderedOutput('0'); - act(() => { + await act(async () => { // These increments should have no effect, since disabled=true increment(); increment(); @@ -3950,7 +3948,7 @@ describe('ReactHooksWithNoopRenderer', () => { assertLog(['Render count: 0']); expect(ReactNoop).toMatchRenderedOutput('0'); - act(() => { + await act(async () => { // Enabling the updater should *not* replay the previous increment() actions setDisabled(false); }); @@ -3990,7 +3988,7 @@ describe('ReactHooksWithNoopRenderer', () => { await waitForAll(['Render disabled: true', 'Render count: 0']); expect(ReactNoop).toMatchRenderedOutput('0'); - act(() => { + await act(async () => { // Although the increment happens first (and would seem to do nothing since disabled=true), // because these calls are in a batch the parent updates first. This should cause the child // to re-render with disabled=false and *then* process the increment action, which now @@ -4085,7 +4083,7 @@ describe('ReactHooksWithNoopRenderer', () => { ]); expect(ReactNoop).toMatchRenderedOutput('0'); - act(() => dispatch()); + await act(async () => dispatch()); assertLog(['Step: 5, Shadow: 5']); expect(ReactNoop).toMatchRenderedOutput('5'); }); @@ -4110,10 +4108,10 @@ describe('ReactHooksWithNoopRenderer', () => { return `${a ? 'A' : 'a'}${b ? 'B' : 'b'}${c ? 'C' : 'c'}`; } - act(() => ReactNoop.render()); + await act(async () => ReactNoop.render()); expect(ReactNoop).toMatchRenderedOutput('abc'); - act(() => { + await act(async () => { updateA(true); // This update should not get dropped. updateC(true); @@ -4282,25 +4280,25 @@ describe('ReactHooksWithNoopRenderer', () => { return ; } - act(() => { + await act(async () => { ReactNoop.render(); }); assertLog(['Render: 0', 'Effect: 0']); - act(() => { + await act(async () => { handleClick(); }); assertLog(['Render: 0']); - act(() => { + await act(async () => { handleClick(); }); assertLog(['Render: 0']); - act(() => { + await act(async () => { handleClick(); }); diff --git a/packages/react-reconciler/src/__tests__/ReactOffscreenStrictMode-test.js b/packages/react-reconciler/src/__tests__/ReactOffscreenStrictMode-test.js index a3f45d8aa1ac6..49d0e0c4b8959 100644 --- a/packages/react-reconciler/src/__tests__/ReactOffscreenStrictMode-test.js +++ b/packages/react-reconciler/src/__tests__/ReactOffscreenStrictMode-test.js @@ -32,8 +32,8 @@ describe('ReactOffscreenStrictMode', () => { } // @gate __DEV__ && enableOffscreen - it('should trigger strict effects when offscreen is visible', () => { - act(() => { + it('should trigger strict effects when offscreen is visible', async () => { + await act(async () => { ReactNoop.render( @@ -56,8 +56,8 @@ describe('ReactOffscreenStrictMode', () => { }); // @gate __DEV__ && enableOffscreen && useModernStrictMode - it('should not trigger strict effects when offscreen is hidden', () => { - act(() => { + it('should not trigger strict effects when offscreen is hidden', async () => { + await act(async () => { ReactNoop.render( @@ -71,7 +71,7 @@ describe('ReactOffscreenStrictMode', () => { log = []; - act(() => { + await act(async () => { ReactNoop.render( @@ -86,7 +86,7 @@ describe('ReactOffscreenStrictMode', () => { log = []; - act(() => { + await act(async () => { ReactNoop.render( @@ -109,7 +109,7 @@ describe('ReactOffscreenStrictMode', () => { log = []; - act(() => { + await act(async () => { ReactNoop.render( @@ -127,7 +127,7 @@ describe('ReactOffscreenStrictMode', () => { ]); }); - it('should not cause infinite render loop when StrictMode is used with Suspense and synchronous set states', () => { + it('should not cause infinite render loop when StrictMode is used with Suspense and synchronous set states', async () => { // This is a regression test, see https://github.com/facebook/react/pull/25179 for more details. function App() { const [state, setState] = React.useState(false); @@ -143,7 +143,7 @@ describe('ReactOffscreenStrictMode', () => { return state; } - act(() => { + await act(async () => { ReactNoop.render( @@ -193,7 +193,7 @@ describe('ReactOffscreenStrictMode', () => { return null; } - act(() => { + await act(async () => { ReactNoop.render( diff --git a/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js index e46d6deb51f73..ee3ea935ace91 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js @@ -2,12 +2,9 @@ let React; let ReactTestRenderer; let ReactFeatureFlags; let Scheduler; -let ReactCache; let Suspense; let act; - -let TextResource; -let textResourceShouldFail; +let textCache; let assertLog; let waitForPaint; @@ -24,7 +21,6 @@ describe('ReactSuspense', () => { ReactTestRenderer = require('react-test-renderer'); act = require('jest-react').act; Scheduler = require('scheduler'); - ReactCache = require('react-cache'); Suspense = React.Suspense; @@ -34,73 +30,71 @@ describe('ReactSuspense', () => { assertLog = InternalTestUtils.assertLog; waitFor = InternalTestUtils.waitFor; - TextResource = ReactCache.unstable_createResource( - ([text, ms = 0]) => { - let listeners = null; - let status = 'pending'; - let value = null; - return { - then(resolve, reject) { - switch (status) { - case 'pending': { - if (listeners === null) { - listeners = [{resolve, reject}]; - setTimeout(() => { - if (textResourceShouldFail) { - Scheduler.log(`Promise rejected [${text}]`); - status = 'rejected'; - value = new Error('Failed to load: ' + text); - listeners.forEach(listener => listener.reject(value)); - } else { - Scheduler.log(`Promise resolved [${text}]`); - status = 'resolved'; - value = text; - listeners.forEach(listener => listener.resolve(value)); - } - }, ms); - } else { - listeners.push({resolve, reject}); - } - break; - } - case 'resolved': { - resolve(value); - break; - } - case 'rejected': { - reject(value); - break; - } - } - }, - }; - }, - ([text, ms]) => text, - ); - textResourceShouldFail = false; + textCache = new Map(); }); - function Text(props) { - Scheduler.log(props.text); - return props.text; + function resolveText(text) { + const record = textCache.get(text); + if (record === undefined) { + const newRecord = { + status: 'resolved', + value: text, + }; + textCache.set(text, newRecord); + } else if (record.status === 'pending') { + const thenable = record.value; + record.status = 'resolved'; + record.value = text; + thenable.pings.forEach(t => t()); + } } - function AsyncText(props) { - const text = props.text; - try { - TextResource.read([props.text, props.ms]); - Scheduler.log(text); - return text; - } catch (promise) { - if (typeof promise.then === 'function') { - Scheduler.log(`Suspend! [${text}]`); - } else { - Scheduler.log(`Error! [${text}]`); + function readText(text) { + const record = textCache.get(text); + if (record !== undefined) { + switch (record.status) { + case 'pending': + Scheduler.log(`Suspend! [${text}]`); + throw record.value; + case 'rejected': + throw record.value; + case 'resolved': + return record.value; } - throw promise; + } else { + Scheduler.log(`Suspend! [${text}]`); + const thenable = { + pings: [], + then(resolve) { + if (newRecord.status === 'pending') { + thenable.pings.push(resolve); + } else { + Promise.resolve().then(() => resolve(newRecord.value)); + } + }, + }; + + const newRecord = { + status: 'pending', + value: thenable, + }; + textCache.set(text, newRecord); + + throw thenable; } } + function Text({text}) { + Scheduler.log(text); + return text; + } + + function AsyncText({text}) { + readText(text); + Scheduler.log(text); + return text; + } + it('suspends rendering and continues later', async () => { function Bar(props) { Scheduler.log('Bar'); @@ -146,16 +140,10 @@ describe('ReactSuspense', () => { ]); expect(root).toMatchRenderedOutput(null); - // Flush some of the time - jest.advanceTimersByTime(50); - // Still nothing... await waitForAll([]); expect(root).toMatchRenderedOutput(null); - // Flush the promise completely - jest.advanceTimersByTime(50); - // Renders successfully - assertLog(['Promise resolved [A]']); + await resolveText('A'); await waitForAll(['Foo', 'Bar', 'A', 'B']); expect(root).toMatchRenderedOutput('AB'); }); @@ -184,19 +172,15 @@ describe('ReactSuspense', () => { ]); expect(root).toMatchRenderedOutput('Loading A...Loading B...'); - // Advance time by enough that the first Suspense's promise resolves and - // switches back to the normal view. The second Suspense should still - // show the placeholder - jest.advanceTimersByTime(5000); - // TODO: Should we throw if you forget to call toHaveYielded? - assertLog(['Promise resolved [A]']); + // Resolve first Suspense's promise and switch back to the normal view. The + // second Suspense should still show the placeholder + await resolveText('A'); await waitForAll(['A']); expect(root).toMatchRenderedOutput('ALoading B...'); - // Advance time by enough that the second Suspense's promise resolves - // and switches back to the normal view - jest.advanceTimersByTime(1000); - assertLog(['Promise resolved [B]']); + // Resolve the second Suspense's promise resolves and switche back to the + // normal view + await resolveText('B'); await waitForAll(['B']); expect(root).toMatchRenderedOutput('AB'); }); @@ -284,11 +268,6 @@ describe('ReactSuspense', () => { ); } - // Committing fallbacks should be throttled. - // First, advance some time to skip the first threshold. - jest.advanceTimersByTime(600); - Scheduler.unstable_advanceTime(600); - const root = ReactTestRenderer.create(, { unstable_isConcurrent: true, }); @@ -302,10 +281,7 @@ describe('ReactSuspense', () => { ]); expect(root).toMatchRenderedOutput('Loading...'); - // Resolve A. - jest.advanceTimersByTime(200); - Scheduler.unstable_advanceTime(200); - assertLog(['Promise resolved [A]']); + await resolveText('A'); await waitForAll(['A', 'Suspend! [B]', 'Loading more...']); // By this point, we have enough info to show "A" and "Loading more..." @@ -313,15 +289,12 @@ describe('ReactSuspense', () => { // showing the inner fallback hoping that B will resolve soon enough. expect(root).toMatchRenderedOutput('Loading...'); - // Resolve B. - jest.advanceTimersByTime(100); - Scheduler.unstable_advanceTime(100); - assertLog(['Promise resolved [B]']); - // By this point, B has resolved. // We're still showing the outer fallback. + await resolveText('B'); expect(root).toMatchRenderedOutput('Loading...'); await waitForAll(['A', 'B']); + // Then contents of both should pop in together. expect(root).toMatchRenderedOutput('AB'); }); @@ -339,11 +312,6 @@ describe('ReactSuspense', () => { ); } - // Committing fallbacks should be throttled. - // First, advance some time to skip the first threshold. - jest.advanceTimersByTime(600); - Scheduler.unstable_advanceTime(600); - const root = ReactTestRenderer.create(, { unstable_isConcurrent: true, }); @@ -357,27 +325,20 @@ describe('ReactSuspense', () => { ]); expect(root).toMatchRenderedOutput('Loading...'); - // Resolve A. - jest.advanceTimersByTime(200); - Scheduler.unstable_advanceTime(200); - assertLog(['Promise resolved [A]']); + await resolveText('A'); await waitForAll(['A', 'Suspend! [B]', 'Loading more...']); // By this point, we have enough info to show "A" and "Loading more..." // However, we've just shown the outer fallback. So we'll delay // showing the inner fallback hoping that B will resolve soon enough. expect(root).toMatchRenderedOutput('Loading...'); - - // Wait some more. B is still not resolving. + // But if we wait a bit longer, eventually we'll give up and show a + // fallback. The exact value here isn't important. It's a JND ("Just + // Noticeable Difference"). jest.advanceTimersByTime(500); - Scheduler.unstable_advanceTime(500); - // Give up and render A with a spinner for B. expect(root).toMatchRenderedOutput('ALoading more...'); - // Resolve B. - jest.advanceTimersByTime(500); - Scheduler.unstable_advanceTime(500); - assertLog(['Promise resolved [B]']); + await resolveText('B'); await waitForAll(['B']); expect(root).toMatchRenderedOutput('AB'); }); @@ -423,18 +384,7 @@ describe('ReactSuspense', () => { const MemoizedChild = memo(function MemoizedChild() { const text = useContext(ValueContext); - try { - TextResource.read([text, 1000]); - Scheduler.log(text); - return text; - } catch (promise) { - if (typeof promise.then === 'function') { - Scheduler.log(`Suspend! [${text}]`); - } else { - Scheduler.log(`Error! [${text}]`); - } - throw promise; - } + return ; }); let setValue; @@ -455,17 +405,15 @@ describe('ReactSuspense', () => { unstable_isConcurrent: true, }); await waitForAll(['Suspend! [default]', 'Loading...']); - jest.advanceTimersByTime(1000); - assertLog(['Promise resolved [default]']); + await resolveText('default'); await waitForAll(['default']); expect(root).toMatchRenderedOutput('default'); - act(() => setValue('new value')); + await act(async () => setValue('new value')); assertLog(['Suspend! [new value]', 'Loading...']); - jest.advanceTimersByTime(1000); - assertLog(['Promise resolved [new value]']); + await resolveText('new value'); await waitForAll(['new value']); expect(root).toMatchRenderedOutput('new value'); }); @@ -478,18 +426,7 @@ describe('ReactSuspense', () => { const MemoizedChild = memo( function MemoizedChild() { const text = useContext(ValueContext); - try { - TextResource.read([text, 1000]); - Scheduler.log(text); - return text; - } catch (promise) { - if (typeof promise.then === 'function') { - Scheduler.log(`Suspend! [${text}]`); - } else { - Scheduler.log(`Error! [${text}]`); - } - throw promise; - } + return ; }, function areEqual(prevProps, nextProps) { return true; @@ -514,17 +451,15 @@ describe('ReactSuspense', () => { unstable_isConcurrent: true, }); await waitForAll(['Suspend! [default]', 'Loading...']); - jest.advanceTimersByTime(1000); - assertLog(['Promise resolved [default]']); + await resolveText('default'); await waitForAll(['default']); expect(root).toMatchRenderedOutput('default'); - act(() => setValue('new value')); + await act(async () => setValue('new value')); assertLog(['Suspend! [new value]', 'Loading...']); - jest.advanceTimersByTime(1000); - assertLog(['Promise resolved [new value]']); + await resolveText('new value'); await waitForAll(['new value']); expect(root).toMatchRenderedOutput('new value'); }); @@ -536,18 +471,7 @@ describe('ReactSuspense', () => { function MemoizedChild() { const text = useContext(ValueContext); - try { - TextResource.read([text, 1000]); - Scheduler.log(text); - return text; - } catch (promise) { - if (typeof promise.then === 'function') { - Scheduler.log(`Suspend! [${text}]`); - } else { - Scheduler.log(`Error! [${text}]`); - } - throw promise; - } + return ; } let setValue; @@ -571,17 +495,15 @@ describe('ReactSuspense', () => { }, ); await waitForAll(['Suspend! [default]', 'Loading...']); - jest.advanceTimersByTime(1000); - assertLog(['Promise resolved [default]']); + await resolveText('default'); await waitForAll(['default']); expect(root).toMatchRenderedOutput('default'); - act(() => setValue('new value')); + await act(async () => setValue('new value')); assertLog(['Suspend! [new value]', 'Loading...']); - jest.advanceTimersByTime(1000); - assertLog(['Promise resolved [new value]']); + await resolveText('new value'); await waitForAll(['new value']); expect(root).toMatchRenderedOutput('new value'); }); @@ -593,18 +515,7 @@ describe('ReactSuspense', () => { const MemoizedChild = forwardRef(() => { const text = useContext(ValueContext); - try { - TextResource.read([text, 1000]); - Scheduler.log(text); - return text; - } catch (promise) { - if (typeof promise.then === 'function') { - Scheduler.log(`Suspend! [${text}]`); - } else { - Scheduler.log(`Error! [${text}]`); - } - throw promise; - } + return ; }); let setValue; @@ -628,17 +539,15 @@ describe('ReactSuspense', () => { }, ); await waitForAll(['Suspend! [default]', 'Loading...']); - jest.advanceTimersByTime(1000); - assertLog(['Promise resolved [default]']); + await resolveText('default'); await waitForAll(['default']); expect(root).toMatchRenderedOutput('default'); - act(() => setValue('new value')); + await act(async () => setValue('new value')); assertLog(['Suspend! [new value]', 'Loading...']); - jest.advanceTimersByTime(1000); - assertLog(['Promise resolved [new value]']); + await resolveText('new value'); await waitForAll(['new value']); expect(root).toMatchRenderedOutput('new value'); }); @@ -674,12 +583,17 @@ describe('ReactSuspense', () => { await waitForAll(['Child 1', 'create layout']); expect(root).toMatchRenderedOutput('Child 1'); - act(() => { + await act(async () => { _setShow(true); }); - assertLog(['Child 1', 'Suspend! [Child 2]', 'Loading...']); - jest.advanceTimersByTime(1000); - assertLog(['destroy layout', 'Promise resolved [Child 2]']); + assertLog([ + 'Child 1', + 'Suspend! [Child 2]', + 'Loading...', + 'destroy layout', + ]); + + await resolveText('Child 2'); await waitForAll(['Child 1', 'Child 2', 'create layout']); expect(root).toMatchRenderedOutput(['Child 1', 'Child 2'].join('')); }); @@ -715,20 +629,8 @@ describe('ReactSuspense', () => { } render() { instance = this; - const text = `${this.props.text}:${this.state.step}`; - const ms = this.props.ms; - try { - TextResource.read([text, ms]); - Scheduler.log(text); - return text; - } catch (promise) { - if (typeof promise.then === 'function') { - Scheduler.log(`Suspend! [${text}]`); - } else { - Scheduler.log(`Error! [${text}]`); - } - throw promise; - } + const text = readText(`${this.props.text}:${this.state.step}`); + return ; } } @@ -758,9 +660,7 @@ describe('ReactSuspense', () => { ]); expect(root).toMatchRenderedOutput('Loading...'); - jest.advanceTimersByTime(100); - - assertLog(['Promise resolved [B:1]']); + await resolveText('B:1'); await waitForPaint([ 'B:1', 'Unmount [Loading...]', @@ -773,9 +673,7 @@ describe('ReactSuspense', () => { assertLog(['Suspend! [B:2]', 'Loading...', 'Mount [Loading...]']); expect(root).toMatchRenderedOutput('Loading...'); - jest.advanceTimersByTime(100); - - assertLog(['Promise resolved [B:2]']); + await resolveText('B:2'); await waitForPaint(['B:2', 'Unmount [Loading...]', 'Update [B:2]']); expect(root).toMatchRenderedOutput('AB:2C'); }); @@ -803,9 +701,7 @@ describe('ReactSuspense', () => { assertLog(['Stateful: 1', 'Suspend! [A]', 'Loading...']); - jest.advanceTimersByTime(1000); - - assertLog(['Promise resolved [A]']); + await resolveText('A'); await waitForPaint(['A']); expect(root).toMatchRenderedOutput('Stateful: 1A'); @@ -817,9 +713,7 @@ describe('ReactSuspense', () => { assertLog(['Stateful: 2', 'Suspend! [B]']); expect(root).toMatchRenderedOutput('Loading...'); - jest.advanceTimersByTime(1000); - - assertLog(['Promise resolved [B]']); + await resolveText('B'); await waitForPaint(['B']); expect(root).toMatchRenderedOutput('Stateful: 2B'); }); @@ -855,9 +749,7 @@ describe('ReactSuspense', () => { assertLog(['Stateful: 1', 'Suspend! [A]', 'Loading...']); - jest.advanceTimersByTime(1000); - - assertLog(['Promise resolved [A]']); + await resolveText('A'); await waitForPaint(['A']); expect(root).toMatchRenderedOutput('Stateful: 1A'); @@ -876,9 +768,7 @@ describe('ReactSuspense', () => { ]); expect(root).toMatchRenderedOutput('Loading...'); - jest.advanceTimersByTime(1000); - - assertLog(['Promise resolved [B]']); + await resolveText('B'); await waitForPaint(['B']); expect(root).toMatchRenderedOutput('Stateful: 2B'); }); @@ -889,20 +779,7 @@ describe('ReactSuspense', () => { Scheduler.log('will unmount'); } render() { - const text = this.props.text; - const ms = this.props.ms; - try { - TextResource.read([text, ms]); - Scheduler.log(text); - return text; - } catch (promise) { - if (typeof promise.then === 'function') { - Scheduler.log(`Suspend! [${text}]`); - } else { - Scheduler.log(`Error! [${text}]`); - } - throw promise; - } + return ; } } @@ -932,18 +809,7 @@ describe('ReactSuspense', () => { Scheduler.log('Did commit: ' + text); }, [text]); - try { - TextResource.read([props.text, props.ms]); - Scheduler.log(text); - return text; - } catch (promise) { - if (typeof promise.then === 'function') { - Scheduler.log(`Suspend! [${text}]`); - } else { - Scheduler.log(`Error! [${text}]`); - } - throw promise; - } + return ; } function App({text}) { @@ -956,9 +822,7 @@ describe('ReactSuspense', () => { ReactTestRenderer.create(); assertLog(['Suspend! [A]', 'Loading...']); - jest.advanceTimersByTime(500); - - assertLog(['Promise resolved [A]']); + await resolveText('A'); await waitForPaint(['A', 'Did commit: A']); }); @@ -986,15 +850,16 @@ describe('ReactSuspense', () => { // Initial render await waitForAll(['Suspend! [Step: 1]', 'Loading...']); - jest.advanceTimersByTime(1000); - assertLog(['Promise resolved [Step: 1]']); + + await resolveText('Step: 1'); await waitForAll(['Step: 1']); expect(root).toMatchRenderedOutput('Step: 1'); // Update that suspends - instance.setState({step: 2}); - await waitForAll(['Suspend! [Step: 2]', 'Loading...']); - jest.advanceTimersByTime(500); + await act(async () => { + instance.setState({step: 2}); + }); + assertLog(['Suspend! [Step: 2]', 'Loading...']); expect(root).toMatchRenderedOutput('Loading...'); // Update while still suspended @@ -1002,8 +867,8 @@ describe('ReactSuspense', () => { await waitForAll(['Suspend! [Step: 3]']); expect(root).toMatchRenderedOutput('Loading...'); - jest.advanceTimersByTime(1000); - assertLog(['Promise resolved [Step: 2]', 'Promise resolved [Step: 3]']); + await resolveText('Step: 2'); + await resolveText('Step: 3'); await waitForAll(['Step: 3']); expect(root).toMatchRenderedOutput('Step: 3'); }); @@ -1040,23 +905,17 @@ describe('ReactSuspense', () => { ]); await waitForAll([]); - jest.advanceTimersByTime(1000); - - assertLog(['Promise resolved [Child 1]']); + await resolveText('Child 1'); await waitForPaint([ 'Child 1', 'Suspend! [Child 2]', 'Suspend! [Child 3]', ]); - jest.advanceTimersByTime(1000); - - assertLog(['Promise resolved [Child 2]']); + await resolveText('Child 2'); await waitForPaint(['Child 2', 'Suspend! [Child 3]']); - jest.advanceTimersByTime(1000); - - assertLog(['Promise resolved [Child 3]']); + await resolveText('Child 3'); await waitForPaint(['Child 3']); expect(root).toMatchRenderedOutput( ['Child 1', 'Child 2', 'Child 3'].join(''), @@ -1083,11 +942,12 @@ describe('ReactSuspense', () => { 'Suspend! [Child 2]', 'Loading...', ]); - jest.advanceTimersByTime(1000); - assertLog(['Promise resolved [Child 1]']); + await resolveText('Child 1'); await waitForAll(['Child 1', 'Suspend! [Child 2]']); + jest.advanceTimersByTime(6000); - assertLog(['Promise resolved [Child 2]']); + + await resolveText('Child 2'); await waitForAll(['Child 1', 'Child 2']); expect(root).toMatchRenderedOutput(['Child 1', 'Child 2'].join('')); }); @@ -1111,32 +971,29 @@ describe('ReactSuspense', () => { const root = ReactTestRenderer.create(); assertLog(['Suspend! [Tab: 0]', ' + sibling', 'Loading...']); expect(root).toMatchRenderedOutput('Loading...'); - jest.advanceTimersByTime(1000); - assertLog(['Promise resolved [Tab: 0]']); + await resolveText('Tab: 0'); await waitForPaint(['Tab: 0']); expect(root).toMatchRenderedOutput('Tab: 0 + sibling'); - act(() => setTab(1)); + await act(async () => setTab(1)); assertLog(['Suspend! [Tab: 1]', ' + sibling', 'Loading...']); expect(root).toMatchRenderedOutput('Loading...'); - jest.advanceTimersByTime(1000); - assertLog(['Promise resolved [Tab: 1]']); + await resolveText('Tab: 1'); await waitForPaint(['Tab: 1']); expect(root).toMatchRenderedOutput('Tab: 1 + sibling'); - act(() => setTab(2)); + await act(async () => setTab(2)); assertLog(['Suspend! [Tab: 2]', ' + sibling', 'Loading...']); expect(root).toMatchRenderedOutput('Loading...'); - jest.advanceTimersByTime(1000); - assertLog(['Promise resolved [Tab: 2]']); + await resolveText('Tab: 2'); await waitForPaint(['Tab: 2']); expect(root).toMatchRenderedOutput('Tab: 2 + sibling'); }); - it('does not warn if an mounted component is pinged', async () => { + it('does not warn if a mounted component is pinged', async () => { const {useState} = React; const root = ReactTestRenderer.create(null); @@ -1146,18 +1003,7 @@ describe('ReactSuspense', () => { const [step, _setStep] = useState(0); setStep = _setStep; const fullText = `${text}:${step}`; - try { - TextResource.read([fullText, ms]); - Scheduler.log(fullText); - return fullText; - } catch (promise) { - if (typeof promise.then === 'function') { - Scheduler.log(`Suspend! [${fullText}]`); - } else { - Scheduler.log(`Error! [${fullText}]`); - } - throw promise; - } + return ; } root.update( @@ -1167,19 +1013,18 @@ describe('ReactSuspense', () => { ); assertLog(['Suspend! [A:0]', 'Loading...']); - jest.advanceTimersByTime(1000); - assertLog(['Promise resolved [A:0]']); + await resolveText('A:0'); await waitForPaint(['A:0']); expect(root).toMatchRenderedOutput('A:0'); - act(() => setStep(1)); + await act(async () => setStep(1)); assertLog(['Suspend! [A:1]', 'Loading...']); expect(root).toMatchRenderedOutput('Loading...'); - root.update(null); - await waitForAll([]); - jest.advanceTimersByTime(1000); + await act(async () => { + root.update(null); + }); }); it('memoizes promise listeners per thread ID to prevent redundant renders', async () => { @@ -1199,10 +1044,7 @@ describe('ReactSuspense', () => { assertLog(['Suspend! [A]', 'Suspend! [B]', 'Suspend! [C]', 'Loading...']); - // Resolve A - jest.advanceTimersByTime(1000); - - assertLog(['Promise resolved [A]']); + await resolveText('A'); await waitForPaint([ 'A', // The promises for B and C have now been thrown twice @@ -1210,10 +1052,7 @@ describe('ReactSuspense', () => { 'Suspend! [C]', ]); - // Resolve B - jest.advanceTimersByTime(1000); - - assertLog(['Promise resolved [B]']); + await resolveText('B'); await waitForPaint([ // Even though the promise for B was thrown twice, we should only // re-render once. @@ -1222,10 +1061,7 @@ describe('ReactSuspense', () => { 'Suspend! [C]', ]); - // Resolve C - jest.advanceTimersByTime(1000); - - assertLog(['Promise resolved [C]']); + await resolveText('C'); await waitForPaint([ // Even though the promise for C was thrown three times, we should only // re-render once. @@ -1233,7 +1069,7 @@ describe('ReactSuspense', () => { ]); }); - it('#14162', () => { + it('#14162', async () => { const {lazy} = React; function Hello() { @@ -1267,8 +1103,9 @@ describe('ReactSuspense', () => { const root = ReactTestRenderer.create(null); - root.update(); - jest.advanceTimersByTime(1000); + await act(async () => { + root.update(); + }); }); it('updates memoized child of suspense component when context updates (simple memo)', async () => { @@ -1278,18 +1115,7 @@ describe('ReactSuspense', () => { const MemoizedChild = memo(function MemoizedChild() { const text = useContext(ValueContext); - try { - TextResource.read([text, 1000]); - Scheduler.log(text); - return text; - } catch (promise) { - if (typeof promise.then === 'function') { - Scheduler.log(`Suspend! [${text}]`); - } else { - Scheduler.log(`Error! [${text}]`); - } - throw promise; - } + return ; }); let setValue; @@ -1308,17 +1134,15 @@ describe('ReactSuspense', () => { const root = ReactTestRenderer.create(); assertLog(['Suspend! [default]', 'Loading...']); - jest.advanceTimersByTime(1000); - assertLog(['Promise resolved [default]']); + await resolveText('default'); await waitForPaint(['default']); expect(root).toMatchRenderedOutput('default'); - act(() => setValue('new value')); + await act(async () => setValue('new value')); assertLog(['Suspend! [new value]', 'Loading...']); - jest.advanceTimersByTime(1000); - assertLog(['Promise resolved [new value]']); + await resolveText('new value'); await waitForPaint(['new value']); expect(root).toMatchRenderedOutput('new value'); }); @@ -1331,18 +1155,7 @@ describe('ReactSuspense', () => { const MemoizedChild = memo( function MemoizedChild() { const text = useContext(ValueContext); - try { - TextResource.read([text, 1000]); - Scheduler.log(text); - return text; - } catch (promise) { - if (typeof promise.then === 'function') { - Scheduler.log(`Suspend! [${text}]`); - } else { - Scheduler.log(`Error! [${text}]`); - } - throw promise; - } + return ; }, function areEqual(prevProps, nextProps) { return true; @@ -1365,17 +1178,15 @@ describe('ReactSuspense', () => { const root = ReactTestRenderer.create(); assertLog(['Suspend! [default]', 'Loading...']); - jest.advanceTimersByTime(1000); - assertLog(['Promise resolved [default]']); + await resolveText('default'); await waitForPaint(['default']); expect(root).toMatchRenderedOutput('default'); - act(() => setValue('new value')); + await act(async () => setValue('new value')); assertLog(['Suspend! [new value]', 'Loading...']); - jest.advanceTimersByTime(1000); - assertLog(['Promise resolved [new value]']); + await resolveText('new value'); await waitForPaint(['new value']); expect(root).toMatchRenderedOutput('new value'); }); @@ -1387,18 +1198,7 @@ describe('ReactSuspense', () => { function MemoizedChild() { const text = useContext(ValueContext); - try { - TextResource.read([text, 1000]); - Scheduler.log(text); - return text; - } catch (promise) { - if (typeof promise.then === 'function') { - Scheduler.log(`Suspend! [${text}]`); - } else { - Scheduler.log(`Error! [${text}]`); - } - throw promise; - } + return ; } let setValue; @@ -1421,17 +1221,15 @@ describe('ReactSuspense', () => { , ); assertLog(['Suspend! [default]', 'Loading...']); - jest.advanceTimersByTime(1000); - assertLog(['Promise resolved [default]']); + await resolveText('default'); await waitForPaint(['default']); expect(root).toMatchRenderedOutput('default'); - act(() => setValue('new value')); + await act(async () => setValue('new value')); assertLog(['Suspend! [new value]', 'Loading...']); - jest.advanceTimersByTime(1000); - assertLog(['Promise resolved [new value]']); + await resolveText('new value'); await waitForPaint(['new value']); expect(root).toMatchRenderedOutput('new value'); }); @@ -1443,18 +1241,7 @@ describe('ReactSuspense', () => { const MemoizedChild = forwardRef(function MemoizedChild() { const text = useContext(ValueContext); - try { - TextResource.read([text, 1000]); - Scheduler.log(text); - return text; - } catch (promise) { - if (typeof promise.then === 'function') { - Scheduler.log(`Suspend! [${text}]`); - } else { - Scheduler.log(`Error! [${text}]`); - } - throw promise; - } + return ; }); let setValue; @@ -1473,22 +1260,20 @@ describe('ReactSuspense', () => { const root = ReactTestRenderer.create(); assertLog(['Suspend! [default]', 'Loading...']); - jest.advanceTimersByTime(1000); - assertLog(['Promise resolved [default]']); + await resolveText('default'); await waitForPaint(['default']); expect(root).toMatchRenderedOutput('default'); - act(() => setValue('new value')); + await act(async () => setValue('new value')); assertLog(['Suspend! [new value]', 'Loading...']); - jest.advanceTimersByTime(1000); - assertLog(['Promise resolved [new value]']); + await resolveText('new value'); await waitForPaint(['new value']); expect(root).toMatchRenderedOutput('new value'); }); - it('updates context consumer within child of suspended suspense component when context updates', () => { + it('updates context consumer within child of suspended suspense component when context updates', async () => { const {createContext, useState} = React; const ValueContext = createContext(null); @@ -1531,11 +1316,11 @@ describe('ReactSuspense', () => { assertLog(['Received context value [default]', 'default']); expect(root).toMatchRenderedOutput('default'); - act(() => setValue('new value')); + await act(async () => setValue('new value')); assertLog(['Received context value [new value]', 'Loading...']); expect(root).toMatchRenderedOutput('Loading...'); - act(() => setValue('default')); + await act(async () => setValue('default')); assertLog(['Received context value [default]', 'default']); expect(root).toMatchRenderedOutput('default'); }); diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseEffectsSemantics-test.js b/packages/react-reconciler/src/__tests__/ReactSuspenseEffectsSemantics-test.js index 71cb898077f36..914f58c2bdb23 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseEffectsSemantics-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseEffectsSemantics-test.js @@ -17,6 +17,7 @@ let caches; let seededCache; let ErrorBoundary; let waitForAll; +let waitFor; let assertLog; // TODO: These tests don't pass in persistent mode yet. Need to implement. @@ -35,6 +36,7 @@ describe('ReactSuspenseEffectsSemantics', () => { const InternalTestUtils = require('internal-test-utils'); waitForAll = InternalTestUtils.waitForAll; + waitFor = InternalTestUtils.waitFor; assertLog = InternalTestUtils.assertLog; caches = []; @@ -372,7 +374,7 @@ describe('ReactSuspenseEffectsSemantics', () => { } // Mount and suspend. - act(() => { + await act(async () => { ReactNoop.renderLegacySyncRoot( @@ -473,7 +475,7 @@ describe('ReactSuspenseEffectsSemantics', () => { } // Mount - act(() => { + await act(async () => { ReactNoop.renderLegacySyncRoot(); }); assertLog([ @@ -499,7 +501,7 @@ describe('ReactSuspenseEffectsSemantics', () => { ); // Schedule an update that causes React to suspend. - act(() => { + await act(async () => { ReactNoop.renderLegacySyncRoot( @@ -629,46 +631,46 @@ describe('ReactSuspenseEffectsSemantics', () => { ); // Schedule an update that causes React to suspend. - act(() => { + await act(async () => { ReactNoop.render( , ); - }); - assertLog([ - 'App render', - 'Text:Inside:Before render', - 'Suspend:Async', - 'Text:Inside:After render', - 'Text:Fallback render', - 'Text:Outside render', - ]); - expect(ReactNoop).toMatchRenderedOutput( - <> - - - - , - ); + await waitFor([ + 'App render', + 'Text:Inside:Before render', + 'Suspend:Async', + 'Text:Inside:After render', + 'Text:Fallback render', + 'Text:Outside render', + ]); + expect(ReactNoop).toMatchRenderedOutput( + <> + + + + , + ); - await advanceTimers(1000); + await jest.runAllTimers(); - // Timing out should commit the fallback and destroy inner layout effects. - assertLog([ - 'Text:Inside:Before destroy layout', - 'Text:Inside:After destroy layout', - 'Text:Fallback create layout', - ]); - await waitForAll(['Text:Fallback create passive']); - expect(ReactNoop).toMatchRenderedOutput( - <> -