From 72c890e3123d3ea48b5d7f51bca301d7da16a8b1 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Wed, 12 Apr 2023 13:36:13 -0400 Subject: [PATCH] Convert more Suspense tests to use act (2/n) (#26610) Many of our Suspense-related tests were written before the `act` API was introduced, and use the lower level `waitFor` helpers instead. So they are less resilient to changes in implementation details than they could be. This converts some of our test suite to use `act` in more places. I found these while working on a PR to expand our fallback throttling mechanism to include all renders that result from a promise resolving, even if there are no more fallbacks in the tree. I think this covers all the remaining tests that are affected. --- .../__tests__/ReactCacheOld-test.internal.js | 60 ++-- .../src/__tests__/ReactDOMFizzServer-test.js | 14 +- .../src/__tests__/ReactDOMFloat-test.js | 4 +- .../__tests__/ReactBatching-test.internal.js | 7 +- .../ReactHooksWithNoopRenderer-test.js | 5 +- .../src/__tests__/ReactLazy-test.internal.js | 181 +++++----- .../src/__tests__/ReactMemo-test.js | 55 +-- .../__tests__/ReactSuspense-test.internal.js | 77 +++-- .../__tests__/ReactSuspenseCallback-test.js | 21 +- .../ReactSuspenseEffectsSemanticsDOM-test.js | 28 +- .../src/__tests__/ReactSuspenseList-test.js | 315 ++++++++---------- .../ReactSuspensePlaceholder-test.internal.js | 54 +-- .../ReactSuspenseWithNoopRenderer-test.js | 30 +- 13 files changed, 422 insertions(+), 429 deletions(-) diff --git a/packages/react-cache/src/__tests__/ReactCacheOld-test.internal.js b/packages/react-cache/src/__tests__/ReactCacheOld-test.internal.js index 579c835878..9463b72e14 100644 --- a/packages/react-cache/src/__tests__/ReactCacheOld-test.internal.js +++ b/packages/react-cache/src/__tests__/ReactCacheOld-test.internal.js @@ -21,6 +21,7 @@ let textResourceShouldFail; let waitForAll; let assertLog; let waitForThrow; +let act; describe('ReactCache', () => { beforeEach(() => { @@ -40,6 +41,7 @@ describe('ReactCache', () => { waitForAll = InternalTestUtils.waitForAll; assertLog = InternalTestUtils.assertLog; waitForThrow = InternalTestUtils.waitForThrow; + act = InternalTestUtils.act; TextResource = createResource( ([text, ms = 0]) => { @@ -145,11 +147,14 @@ describe('ReactCache', () => { await waitForAll(['Suspend! [Hi]', 'Loading...']); textResourceShouldFail = true; - jest.advanceTimersByTime(100); - assertLog(['Promise rejected [Hi]']); - - await waitForThrow('Failed to load: Hi'); - assertLog(['Error! [Hi]', 'Error! [Hi]']); + let error; + try { + await act(() => jest.advanceTimersByTime(100)); + } catch (e) { + error = e; + } + expect(error.message).toMatch('Failed to load: Hi'); + assertLog(['Promise rejected [Hi]', 'Error! [Hi]', 'Error! [Hi]']); // Should throw again on a subsequent read root.update(); @@ -217,9 +222,8 @@ describe('ReactCache', () => { assertLog(['Promise resolved [2]']); await waitForAll([1, 2, 'Suspend! [3]']); - jest.advanceTimersByTime(100); - assertLog(['Promise resolved [3]']); - await waitForAll([1, 2, 3]); + await act(() => jest.advanceTimersByTime(100)); + assertLog(['Promise resolved [3]', 1, 2, 3]); expect(root).toMatchRenderedOutput('123'); @@ -234,13 +238,17 @@ describe('ReactCache', () => { await waitForAll([1, 'Suspend! [4]', 'Loading...']); - jest.advanceTimersByTime(100); - assertLog(['Promise resolved [4]']); - await waitForAll([1, 4, 'Suspend! [5]']); - - jest.advanceTimersByTime(100); - assertLog(['Promise resolved [5]']); - await waitForAll([1, 4, 5]); + await act(() => jest.advanceTimersByTime(100)); + assertLog([ + 'Promise resolved [4]', + 1, + 4, + 'Suspend! [5]', + 'Promise resolved [5]', + 1, + 4, + 5, + ]); expect(root).toMatchRenderedOutput('145'); @@ -262,13 +270,18 @@ describe('ReactCache', () => { 'Suspend! [2]', 'Loading...', ]); - jest.advanceTimersByTime(100); - assertLog(['Promise resolved [2]']); - await waitForAll([1, 2, 'Suspend! [3]']); - jest.advanceTimersByTime(100); - assertLog(['Promise resolved [3]']); - await waitForAll([1, 2, 3]); + await act(() => jest.advanceTimersByTime(100)); + assertLog([ + 'Promise resolved [2]', + 1, + 2, + 'Suspend! [3]', + 'Promise resolved [3]', + 1, + 2, + 3, + ]); expect(root).toMatchRenderedOutput('123'); }); @@ -291,9 +304,8 @@ describe('ReactCache', () => { await waitForAll(['Loading...']); - jest.advanceTimersByTime(1000); - assertLog(['Promise resolved [B]', 'Promise resolved [A]']); - await waitForAll(['Result']); + await act(() => jest.advanceTimersByTime(1000)); + assertLog(['Promise resolved [B]', 'Promise resolved [A]', 'Result']); expect(root).toMatchRenderedOutput('Result'); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index 93c5eac735..da59ac66fa 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -42,6 +42,7 @@ let waitFor; let waitForAll; let assertLog; let waitForPaint; +let clientAct; function resetJSDOM(markup) { // Test Environment @@ -74,6 +75,7 @@ describe('ReactDOMFizzServer', () => { waitFor = InternalTestUtils.waitFor; waitForPaint = InternalTestUtils.waitForPaint; assertLog = InternalTestUtils.assertLog; + clientAct = InternalTestUtils.act; if (gate(flags => flags.source)) { // The `with-selector` module composes the main `use-sync-external-store` @@ -1191,8 +1193,8 @@ describe('ReactDOMFizzServer', () => { expect(getVisibleChildren(container)).toEqual(
Loading...
); // We now resolve it on the client. - resolveText('Hello'); - await waitForAll([]); + await clientAct(() => resolveText('Hello')); + assertLog([]); // The client rendered HTML is now in place. expect(getVisibleChildren(container)).toEqual( @@ -2884,10 +2886,10 @@ describe('ReactDOMFizzServer', () => { , ); - await act(() => { + await clientAct(() => { resolveText('Yay!'); }); - await waitForAll(['Yay!']); + assertLog(['Yay!']); expect(getVisibleChildren(container)).toEqual(
@@ -4310,10 +4312,10 @@ describe('ReactDOMFizzServer', () => {

Loading...

, ); - await unsuspend(); + await clientAct(() => unsuspend()); // Since our client components only throw on the very first render there are no // new throws in this pass - await waitForAll([]); + assertLog([]); expect(mockError.mock.calls).toEqual([]); expect(getVisibleChildren(container)).toEqual( diff --git a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js index d362e1b94a..28fb79c1a7 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js @@ -37,6 +37,7 @@ let waitForAll; let waitForThrow; let assertLog; let Scheduler; +let clientAct; function resetJSDOM(markup) { // Test Environment @@ -71,6 +72,7 @@ describe('ReactDOMFloat', () => { waitForAll = InternalTestUtils.waitForAll; waitForThrow = InternalTestUtils.waitForThrow; assertLog = InternalTestUtils.assertLog; + clientAct = InternalTestUtils.act; textCache = new Map(); loadCache = new Set(); @@ -1186,7 +1188,7 @@ body { // events have already fired. This requires the load to be awaited for the commit to have a chance to flush // We could change this by tracking the loadingState's fulfilled status directly on the loadingState similar // to thenables however this slightly increases the fizz runtime code size. - await loadStylesheets(); + await clientAct(() => loadStylesheets()); assertLog(['load stylesheet: foo']); expect(getMeaningfulChildren(document)).toEqual( diff --git a/packages/react-reconciler/src/__tests__/ReactBatching-test.internal.js b/packages/react-reconciler/src/__tests__/ReactBatching-test.internal.js index 15f5992109..387b6a4e55 100644 --- a/packages/react-reconciler/src/__tests__/ReactBatching-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactBatching-test.internal.js @@ -7,6 +7,7 @@ let assertLog; let ReactCache; let Suspense; let TextResource; +let act; describe('ReactBlockingMode', () => { beforeEach(() => { @@ -23,6 +24,7 @@ describe('ReactBlockingMode', () => { const InternalTestUtils = require('internal-test-utils'); waitForAll = InternalTestUtils.waitForAll; assertLog = InternalTestUtils.assertLog; + act = InternalTestUtils.act; TextResource = ReactCache.unstable_createResource( ([text, ms = 0]) => { @@ -117,9 +119,8 @@ describe('ReactBlockingMode', () => { // fallback should mount immediately. expect(root).toMatchRenderedOutput('Loading...'); - await jest.advanceTimersByTime(1000); - assertLog(['Promise resolved [B]']); - await waitForAll(['A', 'B', 'C']); + await act(() => jest.advanceTimersByTime(1000)); + assertLog(['Promise resolved [B]', 'A', 'B', 'C']); expect(root).toMatchRenderedOutput( <> A diff --git a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js index 1c7b25a2c9..5609b46225 100644 --- a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js +++ b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js @@ -3623,9 +3623,8 @@ describe('ReactHooksWithNoopRenderer', () => { , ); - await resolveText('A'); - assertLog(['Promise resolved [A]']); - await waitForAll(['A']); + await act(() => resolveText('A')); + assertLog(['Promise resolved [A]', 'A']); expect(ReactNoop).toMatchRenderedOutput( <> diff --git a/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js b/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js index bd4c534a78..a96c60bc26 100644 --- a/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js @@ -9,6 +9,7 @@ let waitFor; let waitForAll; let waitForThrow; let assertLog; +let act; let fakeModuleCache; @@ -39,6 +40,7 @@ describe('ReactLazy', () => { waitForAll = InternalTestUtils.waitForAll; waitForThrow = InternalTestUtils.waitForThrow; assertLog = InternalTestUtils.assertLog; + act = InternalTestUtils.act; fakeModuleCache = new Map(); }); @@ -104,9 +106,8 @@ describe('ReactLazy', () => { await waitForAll(['Loading...']); expect(root).not.toMatchRenderedOutput('Hi'); - await resolveFakeImport(Text); - - await waitForAll(['Hi']); + await act(() => resolveFakeImport(Text)); + assertLog(['Hi']); expect(root).toMatchRenderedOutput('Hi'); // Should not suspend on update @@ -196,9 +197,8 @@ describe('ReactLazy', () => { await waitForAll(['Foo']); expect(root).not.toMatchRenderedOutput('FooBar'); - await resolveFakeImport(Bar); - - await waitForAll(['Foo', 'Bar']); + await act(() => resolveFakeImport(Bar)); + assertLog(['Foo', 'Bar']); expect(root).toMatchRenderedOutput('FooBar'); }); @@ -207,15 +207,24 @@ describe('ReactLazy', () => { const LazyText = lazy(async () => Text); - const root = ReactTestRenderer.create( - }> - - , - { - unstable_isConcurrent: true, - }, - ); - await waitForThrow('Element type is invalid'); + const root = ReactTestRenderer.create(null, { + unstable_isConcurrent: true, + }); + + let error; + try { + await act(() => { + root.update( + }> + + , + ); + }); + } catch (e) { + error = e; + } + + expect(error.message).toMatch('Element type is invalid'); assertLog(['Loading...']); expect(root).not.toMatchRenderedOutput('Hi'); if (__DEV__) { @@ -227,20 +236,29 @@ describe('ReactLazy', () => { }); it('throws if promise rejects', async () => { + const networkError = new Error('Bad network'); const LazyText = lazy(async () => { - throw new Error('Bad network'); + throw networkError; }); - const root = ReactTestRenderer.create( - }> - - , - { - unstable_isConcurrent: true, - }, - ); + const root = ReactTestRenderer.create(null, { + unstable_isConcurrent: true, + }); - await waitForThrow('Bad network'); + let error; + try { + await act(() => { + root.update( + }> + + , + ); + }); + } catch (e) { + error = e; + } + + expect(error).toBe(networkError); assertLog(['Loading...']); expect(root).not.toMatchRenderedOutput('Hi'); }); @@ -290,14 +308,15 @@ describe('ReactLazy', () => { await waitForAll(['Suspend! [LazyChildA]', 'Loading...']); expect(root).not.toMatchRenderedOutput('AB'); - await resolveFakeImport(Child); + await act(async () => { + await resolveFakeImport(Child); - // B suspends even though it happens to share the same import as A. - // TODO: React.lazy should implement the `status` and `value` fields, so - // we can unwrap the result synchronously if it already loaded. Like `use`. - await waitFor(['A', 'Suspend! [LazyChildB]']); - - await waitForAll(['A', 'B', 'Did mount: A', 'Did mount: B']); + // B suspends even though it happens to share the same import as A. + // TODO: React.lazy should implement the `status` and `value` fields, so + // we can unwrap the result synchronously if it already loaded. Like `use`. + await waitFor(['A', 'Suspend! [LazyChildB]']); + }); + assertLog(['A', 'B', 'Did mount: A', 'Did mount: B']); expect(root).toMatchRenderedOutput('AB'); // Swap the position of A and B @@ -325,9 +344,10 @@ describe('ReactLazy', () => { await waitForAll(['Loading...']); expect(root).not.toMatchRenderedOutput('Hi'); - await resolveFakeImport(T); - - await expect(async () => await waitForAll(['Hi'])).toErrorDev( + await expect(async () => { + await act(() => resolveFakeImport(T)); + assertLog(['Hi']); + }).toErrorDev( 'Warning: T: Support for defaultProps ' + 'will be removed from function components in a future major ' + 'release. Use JavaScript default parameters instead.', @@ -380,11 +400,10 @@ describe('ReactLazy', () => { await waitForAll(['Loading...']); expect(root).not.toMatchRenderedOutput('SiblingA'); - await resolveFakeImport(LazyImpl); - - await expect( - async () => await waitForAll(['Lazy', 'Sibling', 'A']), - ).toErrorDev( + await expect(async () => { + await act(() => resolveFakeImport(LazyImpl)); + assertLog(['Lazy', 'Sibling', 'A']); + }).toErrorDev( 'Warning: LazyImpl: Support for defaultProps ' + 'will be removed from function components in a future major ' + 'release. Use JavaScript default parameters instead.', @@ -427,9 +446,8 @@ describe('ReactLazy', () => { await waitForAll(['Not lazy: 0', 'Loading...']); expect(root).not.toMatchRenderedOutput('Not lazy: 0Lazy: 0'); - await resolveFakeImport(LazyImpl); - - await waitForAll(['Lazy: 0']); + await act(() => resolveFakeImport(LazyImpl)); + assertLog(['Lazy: 0']); expect(root).toMatchRenderedOutput('Not lazy: 0Lazy: 0'); // Should bailout due to unchanged props and state @@ -473,9 +491,8 @@ describe('ReactLazy', () => { await waitForAll(['Not lazy: 0', 'Loading...']); expect(root).not.toMatchRenderedOutput('Not lazy: 0Lazy: 0'); - await resolveFakeImport(LazyImpl); - - await waitForAll(['Lazy: 0']); + await act(() => resolveFakeImport(LazyImpl)); + assertLog(['Lazy: 0']); expect(root).toMatchRenderedOutput('Not lazy: 0Lazy: 0'); // Should bailout due to shallow equal props and state @@ -551,9 +568,8 @@ describe('ReactLazy', () => { await waitForAll(['Loading...']); expect(root).not.toMatchRenderedOutput('A1'); - await resolveFakeImport(C); - - await waitForAll([ + await act(() => resolveFakeImport(C)); + assertLog([ 'constructor: A', 'getDerivedStateFromProps: A', 'A1', @@ -682,8 +698,10 @@ describe('ReactLazy', () => { await waitForAll(['Loading...']); expect(root).not.toMatchRenderedOutput('Hi Bye'); - await resolveFakeImport(T); - await expect(async () => await waitForAll(['Hi Bye'])).toErrorDev( + await expect(async () => { + await act(() => resolveFakeImport(T)); + assertLog(['Hi Bye']); + }).toErrorDev( 'Warning: T: Support for defaultProps ' + 'will be removed from function components in a future major ' + 'release. Use JavaScript default parameters instead.', @@ -806,9 +824,8 @@ describe('ReactLazy', () => { expect(root).not.toMatchRenderedOutput('22'); // Mount - await resolveFakeImport(Add); await expect(async () => { - await waitForAll([]); + await act(() => resolveFakeImport(Add)); }).toErrorDev( shouldWarnAboutFunctionDefaultProps ? [ @@ -1003,9 +1020,9 @@ describe('ReactLazy', () => { expect(root).not.toMatchRenderedOutput('Inner default text'); // Mount - await resolveFakeImport(T); await expect(async () => { - await waitForAll(['Inner default text']); + await act(() => resolveFakeImport(T)); + assertLog(['Inner default text']); }).toErrorDev([ 'T: Support for defaultProps will be removed from function components in a future major release. Use JavaScript default parameters instead.', 'The prop `text` is marked as required in `T`, but its value is `undefined`', @@ -1045,10 +1062,9 @@ describe('ReactLazy', () => { await waitForAll(['Started loading', 'Loading...']); expect(root).not.toMatchRenderedOutput(
AB
); - await resolveFakeImport(Foo); - await expect(async () => { - await waitForAll(['A', 'B']); + await act(() => resolveFakeImport(Foo)); + assertLog(['A', 'B']); }).toErrorDev(' in Text (at **)\n' + ' in Foo (at **)'); expect(root).toMatchRenderedOutput(
AB
); }); @@ -1092,12 +1108,11 @@ describe('ReactLazy', () => { expect(root).not.toMatchRenderedOutput('FooBar'); expect(ref.current).toBe(null); - await resolveFakeImport(Foo); - await waitForAll(['Foo']); + await act(() => resolveFakeImport(Foo)); + assertLog(['Foo']); - await resolveFakeImport(ForwardRefBar); - - await waitForAll(['Foo', 'forwardRef', 'Bar']); + await act(() => resolveFakeImport(ForwardRefBar)); + assertLog(['Foo', 'forwardRef', 'Bar']); expect(root).toMatchRenderedOutput('FooBar'); expect(ref.current).not.toBe(null); }); @@ -1123,9 +1138,8 @@ describe('ReactLazy', () => { expect(root).not.toMatchRenderedOutput('4'); // Mount - await resolveFakeImport(Add); await expect(async () => { - await waitForAll([]); + await act(() => resolveFakeImport(Add)); }).toErrorDev( 'Unknown: Support for defaultProps will be removed from memo components in a future major release. Use JavaScript default parameters instead.', ); @@ -1211,9 +1225,8 @@ describe('ReactLazy', () => { expect(root).not.toMatchRenderedOutput('4'); // Mount - await resolveFakeImport(Add); await expect(async () => { - await waitForAll([]); + await act(() => resolveFakeImport(Add)); }).toErrorDev([ 'Memo: Support for defaultProps will be removed from memo components in a future major release. Use JavaScript default parameters instead.', 'Unknown: Support for defaultProps will be removed from memo components in a future major release. Use JavaScript default parameters instead.', @@ -1296,8 +1309,8 @@ describe('ReactLazy', () => { await waitForAll(['Loading...']); - await resolveFakeImport(ResolvedText); - await waitForAll([]); + await act(() => resolveFakeImport(ResolvedText)); + assertLog([]); expect(componentStackMessage).toContain('in ResolvedText'); }); @@ -1405,11 +1418,11 @@ describe('ReactLazy', () => { await waitForAll(['Init A', 'Loading...']); expect(root).not.toMatchRenderedOutput('AB'); - await resolveFakeImport(ChildA); - await waitForAll(['A', 'Init B']); + await act(() => resolveFakeImport(ChildA)); + assertLog(['A', 'Init B']); - await resolveFakeImport(ChildB); - await waitForAll(['A', 'B', 'Did mount: A', 'Did mount: B']); + await act(() => resolveFakeImport(ChildB)); + assertLog(['A', 'B', 'Did mount: A', 'Did mount: B']); expect(root).toMatchRenderedOutput('AB'); // Swap the position of A and B @@ -1423,10 +1436,10 @@ describe('ReactLazy', () => { // The suspense boundary should've triggered now. expect(root).toMatchRenderedOutput('Loading...'); - await resolveB2({default: ChildB}); + await act(() => resolveB2({default: ChildB})); // We need to flush to trigger the second one to load. - await waitForAll(['Init A2', 'b', 'a', 'Did mount: b', 'Did mount: a']); + assertLog(['Init A2', 'b', 'a', 'Did mount: b', 'Did mount: a']); expect(root).toMatchRenderedOutput('ba'); }); @@ -1552,12 +1565,11 @@ describe('ReactLazy', () => { await waitForAll(['Init A', 'Loading...']); expect(root).not.toMatchRenderedOutput('AB'); - await resolveFakeImport(ChildA); + await act(() => resolveFakeImport(ChildA)); // We need to flush to trigger the B to load. - await waitForAll(['Init B']); - await resolveFakeImport(ChildB); - - await waitForAll(['A', 'B', 'Did mount: A', 'Did mount: B']); + await assertLog(['Init B']); + await act(() => resolveFakeImport(ChildB)); + assertLog(['A', 'B', 'Did mount: A', 'Did mount: B']); expect(root).toMatchRenderedOutput('AB'); // Swap the position of A and B @@ -1565,12 +1577,11 @@ describe('ReactLazy', () => { root.update(); }); await waitForAll(['Init B2', 'Loading...']); - await resolveFakeImport(ChildB2); + await act(() => resolveFakeImport(ChildB2)); // We need to flush to trigger the second one to load. - await waitForAll(['Init A2', 'Loading...']); - await resolveFakeImport(ChildA2); - - await waitForAll(['b', 'a', 'Did update: b', 'Did update: a']); + assertLog(['Init A2', 'Loading...']); + await act(() => resolveFakeImport(ChildA2)); + assertLog(['b', 'a', 'Did update: b', 'Did update: a']); expect(root).toMatchRenderedOutput('ba'); }); diff --git a/packages/react-reconciler/src/__tests__/ReactMemo-test.js b/packages/react-reconciler/src/__tests__/ReactMemo-test.js index f50dc46d50..a7b47621d5 100644 --- a/packages/react-reconciler/src/__tests__/ReactMemo-test.js +++ b/packages/react-reconciler/src/__tests__/ReactMemo-test.js @@ -106,12 +106,14 @@ describe('memo', () => { } Counter = memo(Counter); - ReactNoop.render( - }> - - , + await act(() => + ReactNoop.render( + }> + + , + ), ); - await waitForAll(['Loading...', 0]); + assertLog(['Loading...', 0]); expect(ReactNoop).toMatchRenderedOutput(); // Should bail out because props have not changed @@ -163,8 +165,8 @@ describe('memo', () => { } const parent = React.createRef(null); - ReactNoop.render(); - await waitForAll(['Loading...', 'Count: 0']); + await act(() => ReactNoop.render()); + assertLog(['Loading...', 'Count: 0']); expect(ReactNoop).toMatchRenderedOutput(); // Should bail out because props have not changed @@ -340,12 +342,14 @@ describe('memo', () => { return oldProps.count === newProps.count; }); - ReactNoop.render( - }> - - , + await act(() => + ReactNoop.render( + }> + + , + ), ); - await waitForAll(['Loading...', 0]); + assertLog(['Loading...', 0]); expect(ReactNoop).toMatchRenderedOutput(); // Should bail out because props have not changed @@ -376,12 +380,14 @@ describe('memo', () => { } const Counter = memo(CounterInner); - ReactNoop.render( - }> - - , + await act(() => + ReactNoop.render( + }> + + , + ), ); - await waitForAll(['Loading...', '0!']); + assertLog(['Loading...', '0!']); expect(ReactNoop).toMatchRenderedOutput(); // Should bail out because props have not changed @@ -427,13 +433,16 @@ describe('memo', () => { }; // The final layer uses memo() from test fixture (which might be lazy). Counter = memo(Counter); - ReactNoop.render( - }> - - , - ); + await expect(async () => { - await waitForAll(['Loading...', 15]); + await act(() => { + ReactNoop.render( + }> + + , + ); + }); + assertLog(['Loading...', 15]); }).toErrorDev([ 'Counter: Support for defaultProps will be removed from memo components in a future major release. Use JavaScript default parameters instead.', ]); diff --git a/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js index 75827aa409..aff316ae10 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js @@ -172,14 +172,14 @@ describe('ReactSuspense', () => { // 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']); + await act(() => resolveText('A')); + assertLog(['A']); expect(root).toMatchRenderedOutput('ALoading B...'); // Resolve the second Suspense's promise resolves and switche back to the // normal view - await resolveText('B'); - await waitForAll(['B']); + await act(() => resolveText('B')); + assertLog(['B']); expect(root).toMatchRenderedOutput('AB'); }); @@ -294,13 +294,10 @@ describe('ReactSuspense', () => { // showing the inner fallback hoping that B will resolve soon enough. expect(root).toMatchRenderedOutput('Loading...'); + await act(() => resolveText('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. + // The contents of both should pop in together. + assertLog(['A', 'B']); expect(root).toMatchRenderedOutput('AB'); }); @@ -337,8 +334,8 @@ describe('ReactSuspense', () => { jest.advanceTimersByTime(500); expect(root).toMatchRenderedOutput('ALoading more...'); - await resolveText('B'); - await waitForAll(['B']); + await act(() => resolveText('B')); + assertLog(['B']); expect(root).toMatchRenderedOutput('AB'); }); @@ -475,15 +472,15 @@ describe('ReactSuspense', () => { }); await waitForAll(['Suspend! [default]', 'Loading...']); - await resolveText('default'); - await waitForAll(['default']); + await act(() => resolveText('default')); + assertLog(['default']); expect(root).toMatchRenderedOutput('default'); await act(() => setValue('new value')); assertLog(['Suspend! [new value]', 'Loading...']); - await resolveText('new value'); - await waitForAll(['new value']); + await act(() => resolveText('new value')); + assertLog(['new value']); expect(root).toMatchRenderedOutput('new value'); }); @@ -521,15 +518,15 @@ describe('ReactSuspense', () => { }); await waitForAll(['Suspend! [default]', 'Loading...']); - await resolveText('default'); - await waitForAll(['default']); + await act(() => resolveText('default')); + assertLog(['default']); expect(root).toMatchRenderedOutput('default'); await act(() => setValue('new value')); assertLog(['Suspend! [new value]', 'Loading...']); - await resolveText('new value'); - await waitForAll(['new value']); + await act(() => resolveText('new value')); + assertLog(['new value']); expect(root).toMatchRenderedOutput('new value'); }); @@ -565,15 +562,15 @@ describe('ReactSuspense', () => { ); await waitForAll(['Suspend! [default]', 'Loading...']); - await resolveText('default'); - await waitForAll(['default']); + await act(() => resolveText('default')); + assertLog(['default']); expect(root).toMatchRenderedOutput('default'); await act(() => setValue('new value')); assertLog(['Suspend! [new value]', 'Loading...']); - await resolveText('new value'); - await waitForAll(['new value']); + await act(() => resolveText('new value')); + assertLog(['new value']); expect(root).toMatchRenderedOutput('new value'); }); @@ -609,15 +606,15 @@ describe('ReactSuspense', () => { ); await waitForAll(['Suspend! [default]', 'Loading...']); - await resolveText('default'); - await waitForAll(['default']); + await act(() => resolveText('default')); + assertLog(['default']); expect(root).toMatchRenderedOutput('default'); await act(() => setValue('new value')); assertLog(['Suspend! [new value]', 'Loading...']); - await resolveText('new value'); - await waitForAll(['new value']); + await act(() => resolveText('new value')); + assertLog(['new value']); expect(root).toMatchRenderedOutput('new value'); }); @@ -662,8 +659,8 @@ describe('ReactSuspense', () => { 'destroy layout', ]); - await resolveText('Child 2'); - await waitForAll(['Child 1', 'Child 2', 'create layout']); + await act(() => resolveText('Child 2')); + assertLog(['Child 1', 'Child 2', 'create layout']); expect(root).toMatchRenderedOutput(['Child 1', 'Child 2'].join('')); }); @@ -920,8 +917,8 @@ describe('ReactSuspense', () => { // Initial render await waitForAll(['Suspend! [Step: 1]', 'Loading...']); - await resolveText('Step: 1'); - await waitForAll(['Step: 1']); + await act(() => resolveText('Step: 1')); + assertLog(['Step: 1']); expect(root).toMatchRenderedOutput('Step: 1'); // Update that suspends @@ -936,9 +933,11 @@ describe('ReactSuspense', () => { await waitForAll(['Suspend! [Step: 3]']); expect(root).toMatchRenderedOutput('Loading...'); - await resolveText('Step: 2'); - await resolveText('Step: 3'); - await waitForAll(['Step: 3']); + await act(() => { + resolveText('Step: 2'); + resolveText('Step: 3'); + }); + assertLog(['Step: 3']); expect(root).toMatchRenderedOutput('Step: 3'); }); @@ -996,8 +995,8 @@ describe('ReactSuspense', () => { function App(props) { return ( }> - - + + ); } @@ -1012,8 +1011,8 @@ describe('ReactSuspense', () => { jest.advanceTimersByTime(6000); - await resolveText('Child 2'); - await waitForAll(['Child 1', 'Child 2']); + await act(() => resolveText('Child 2')); + assertLog(['Child 1', 'Child 2']); expect(root).toMatchRenderedOutput(['Child 1', 'Child 2'].join('')); }); diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseCallback-test.js b/packages/react-reconciler/src/__tests__/ReactSuspenseCallback-test.js index 5885ddb0a5..8706d9e248 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseCallback-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseCallback-test.js @@ -12,6 +12,7 @@ let React; let ReactNoop; let waitForAll; +let act; describe('ReactSuspense', () => { beforeEach(() => { @@ -22,6 +23,7 @@ describe('ReactSuspense', () => { const InternalTestUtils = require('internal-test-utils'); waitForAll = InternalTestUtils.waitForAll; + act = InternalTestUtils.act; }); function createThenable() { @@ -90,7 +92,7 @@ describe('ReactSuspense', () => { expect(ops).toEqual([new Set([promise])]); ops = []; - await resolve(); + await act(() => resolve()); await waitForAll([]); expect(ReactNoop).toMatchRenderedOutput('Done'); expect(ops).toEqual([]); @@ -129,14 +131,14 @@ describe('ReactSuspense', () => { expect(ops).toEqual([new Set([promise1])]); ops = []; - await resolve1(); + await act(() => resolve1()); ReactNoop.render(element); await waitForAll([]); expect(ReactNoop).toMatchRenderedOutput('Waiting Tier 1'); expect(ops).toEqual([new Set([promise2])]); ops = []; - await resolve2(); + await act(() => resolve2()); ReactNoop.render(element); await waitForAll([]); expect(ReactNoop).toMatchRenderedOutput('DoneDone'); @@ -218,23 +220,14 @@ describe('ReactSuspense', () => { ops1 = []; ops2 = []; - await resolve1(); - ReactNoop.render(element); - await waitForAll([]); - - // Force fallback to commit. - // TODO: Should be able to use `act` here. - jest.runAllTimers(); - + await act(() => resolve1()); expect(ReactNoop).toMatchRenderedOutput('Waiting Tier 2Done'); expect(ops1).toEqual([]); expect(ops2).toEqual([new Set([promise2])]); ops1 = []; ops2 = []; - await resolve2(); - ReactNoop.render(element); - await waitForAll([]); + await act(() => resolve2()); expect(ReactNoop).toMatchRenderedOutput('DoneDone'); expect(ops1).toEqual([]); expect(ops2).toEqual([]); diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseEffectsSemanticsDOM-test.js b/packages/react-reconciler/src/__tests__/ReactSuspenseEffectsSemanticsDOM-test.js index 12414e9e20..2a6b02acfe 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseEffectsSemanticsDOM-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseEffectsSemanticsDOM-test.js @@ -187,8 +187,8 @@ describe('ReactSuspenseEffectsSemanticsDOM', () => { }); assertLog(['Loading...']); - await resolveFakeImport(ChildA); - await waitForAll(['A', 'Ref mount: A']); + await act(() => resolveFakeImport(ChildA)); + assertLog(['A', 'Ref mount: A']); expect(container.innerHTML).toBe('A'); // Swap the position of A and B @@ -200,8 +200,8 @@ describe('ReactSuspenseEffectsSemanticsDOM', () => { 'ALoading...', ); - await resolveFakeImport(ChildB); - await waitForAll(['B', 'Ref mount: B']); + await act(() => resolveFakeImport(ChildB)); + assertLog(['B', 'Ref mount: B']); expect(container.innerHTML).toBe('B'); }); @@ -247,8 +247,8 @@ describe('ReactSuspenseEffectsSemanticsDOM', () => { }); assertLog(['Loading...']); - await resolveFakeImport(ChildA); - await waitForAll(['A', 'Did mount: A']); + await act(() => resolveFakeImport(ChildA)); + assertLog(['A', 'Did mount: A']); expect(container.innerHTML).toBe('A'); // Swap the position of A and B @@ -258,8 +258,8 @@ describe('ReactSuspenseEffectsSemanticsDOM', () => { assertLog(['Loading...', 'Will unmount: A']); expect(container.innerHTML).toBe('Loading...'); - await resolveFakeImport(ChildB); - await waitForAll(['B', 'Did mount: B']); + await act(() => resolveFakeImport(ChildB)); + assertLog(['B', 'Did mount: B']); expect(container.innerHTML).toBe('B'); }); @@ -299,8 +299,8 @@ describe('ReactSuspenseEffectsSemanticsDOM', () => { }); assertLog(['Loading...']); - await resolveFakeImport(ChildA); - await waitForAll(['A', 'Did mount: A']); + await act(() => resolveFakeImport(ChildA)); + assertLog(['A', 'Did mount: A']); expect(container.innerHTML).toBe('A'); // Swap the position of A and B @@ -366,8 +366,8 @@ describe('ReactSuspenseEffectsSemanticsDOM', () => { }); assertLog(['Loading...']); - await resolveFakeImport(ChildA); - await waitForAll(['A', 'Ref mount: A']); + await act(() => resolveFakeImport(ChildA)); + assertLog(['A', 'Ref mount: A']); expect(container.innerHTML).toBe('A'); // Swap the position of A and B @@ -429,8 +429,8 @@ describe('ReactSuspenseEffectsSemanticsDOM', () => { }); assertLog(['Loading...']); - await resolveFakeImport(ChildA); - await waitForAll(['A', 'Did mount: A']); + await act(() => resolveFakeImport(ChildA)); + assertLog(['A', 'Did mount: A']); expect(container.innerHTML).toBe('A'); // Swap the position of A and B diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js b/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js index 2b20ca53d8..bbb0b2912b 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js @@ -249,9 +249,8 @@ describe('ReactSuspenseList', () => { , ); - await C.resolve(); - - await waitForAll(['C']); + await act(() => C.resolve()); + assertLog(['C']); expect(ReactNoop).toMatchRenderedOutput( <> @@ -261,9 +260,8 @@ describe('ReactSuspenseList', () => { , ); - await B.resolve(); - - await waitForAll(['B']); + await act(() => B.resolve()); + assertLog(['B']); expect(ReactNoop).toMatchRenderedOutput( <> @@ -384,9 +382,8 @@ describe('ReactSuspenseList', () => { , ); - await B.resolve(); - - await waitForAll(['A', 'B', 'Suspend! [C]']); + await act(() => B.resolve()); + assertLog(['A', 'B', 'Suspend! [C]']); expect(ReactNoop).toMatchRenderedOutput( <> @@ -396,9 +393,8 @@ describe('ReactSuspenseList', () => { , ); - await C.resolve(); - - await waitForAll(['A', 'B', 'C']); + await act(() => C.resolve()); + assertLog(['A', 'B', 'C']); expect(ReactNoop).toMatchRenderedOutput( <> @@ -462,9 +458,8 @@ describe('ReactSuspenseList', () => { , ); - await B.resolve(); - - await waitForAll(['A', 'B', 'Suspend! [C]']); + await act(() => B.resolve()); + assertLog(['A', 'B', 'Suspend! [C]']); expect(ReactNoop).toMatchRenderedOutput( <> @@ -478,9 +473,8 @@ describe('ReactSuspenseList', () => { , ); - await C.resolve(); - - await waitForAll(['A', 'B', 'C']); + await act(() => C.resolve()); + assertLog(['A', 'B', 'C']); expect(ReactNoop).toMatchRenderedOutput( <> @@ -544,9 +538,8 @@ describe('ReactSuspenseList', () => { , ); - await C.resolve(); - - await waitForAll(['A', 'B', 'C']); + await act(() => C.resolve()); + assertLog(['A', 'B', 'C']); expect(ReactNoop).toMatchRenderedOutput( <> @@ -604,9 +597,8 @@ describe('ReactSuspenseList', () => { , ); - await C.resolve(); - - await waitForAll(['A', 'B', 'C']); + await act(() => C.resolve()); + assertLog(['A', 'B', 'C']); expect(ReactNoop).toMatchRenderedOutput( <> @@ -667,8 +659,8 @@ describe('ReactSuspenseList', () => { Loading B , ); - await B.resolve(); - await waitForAll(['A', 'B']); + await act(() => B.resolve()); + assertLog(['A', 'B']); expect(ReactNoop).toMatchRenderedOutput( <> A @@ -692,8 +684,8 @@ describe('ReactSuspenseList', () => { Loading D , ); - await D.resolve(); - await waitForAll(['C', 'D']); + await act(() => D.resolve()); + assertLog(['C', 'D']); expect(ReactNoop).toMatchRenderedOutput( <> C @@ -742,9 +734,8 @@ describe('ReactSuspenseList', () => { expect(ReactNoop).toMatchRenderedOutput(Loading); - await A.resolve(); - - await waitForAll(['A']); + await act(() => A.resolve()); + assertLog(['A']); expect(ReactNoop).toMatchRenderedOutput(A); @@ -775,9 +766,8 @@ describe('ReactSuspenseList', () => { , ); - await B.resolve(); - - await waitForAll(['B', 'Suspend! [C]']); + await act(() => B.resolve()); + assertLog(['B', 'Suspend! [C]']); // Even though we could now show B, we're still waiting on C. expect(ReactNoop).toMatchRenderedOutput( @@ -788,9 +778,8 @@ describe('ReactSuspenseList', () => { , ); - await C.resolve(); - - await waitForAll(['B', 'C']); + await act(() => C.resolve()); + assertLog(['B', 'C']); expect(ReactNoop).toMatchRenderedOutput( <> @@ -838,9 +827,8 @@ describe('ReactSuspenseList', () => { expect(ReactNoop).toMatchRenderedOutput(null); - await A.resolve(); - - await waitForAll(['A']); + await act(() => A.resolve()); + assertLog(['A']); expect(ReactNoop).toMatchRenderedOutput(A); @@ -865,16 +853,14 @@ describe('ReactSuspenseList', () => { // A is already showing content so it doesn't turn into a fallback. expect(ReactNoop).toMatchRenderedOutput(A); - await B.resolve(); - - await waitForAll(['B', 'Suspend! [C]']); + await act(() => B.resolve()); + assertLog(['B', 'Suspend! [C]']); // Even though we could now show B, we're still waiting on C. expect(ReactNoop).toMatchRenderedOutput(A); - await C.resolve(); - - await waitForAll(['B', 'C']); + await act(() => C.resolve()); + assertLog(['B', 'C']); expect(ReactNoop).toMatchRenderedOutput( <> @@ -921,9 +907,8 @@ describe('ReactSuspenseList', () => { , ); - await A.resolve(); - - await waitForAll(['A', 'Suspend! [B]']); + await act(() => A.resolve()); + assertLog(['A', 'Suspend! [B]']); expect(ReactNoop).toMatchRenderedOutput( <> @@ -933,9 +918,8 @@ describe('ReactSuspenseList', () => { , ); - await B.resolve(); - - await waitForAll(['B', 'C']); + await act(() => B.resolve()); + assertLog(['B', 'C']); expect(ReactNoop).toMatchRenderedOutput( <> @@ -982,9 +966,8 @@ describe('ReactSuspenseList', () => { , ); - await C.resolve(); - - await waitForAll(['C', 'Suspend! [B]']); + await act(() => C.resolve()); + assertLog(['C', 'Suspend! [B]']); expect(ReactNoop).toMatchRenderedOutput( <> @@ -994,9 +977,8 @@ describe('ReactSuspenseList', () => { , ); - await B.resolve(); - - await waitForAll(['B', 'A']); + await act(() => B.resolve()); + assertLog(['B', 'A']); expect(ReactNoop).toMatchRenderedOutput( <> @@ -1089,9 +1071,8 @@ describe('ReactSuspenseList', () => { , ); - await A.resolve(); - - await waitForAll(['A', 'Suspend! [C]']); + await act(() => A.resolve()); + assertLog(['A', 'Suspend! [C]']); // Even though we could show A, it is still in a fallback state because // C is not yet resolved. We need to resolve everything in the head first. @@ -1106,9 +1087,8 @@ describe('ReactSuspenseList', () => { , ); - await C.resolve(); - - await waitForAll(['A', 'C', 'Suspend! [E]']); + await act(() => C.resolve()); + assertLog(['A', 'C', 'Suspend! [E]']); // We can now resolve the full head. expect(ReactNoop).toMatchRenderedOutput( @@ -1122,9 +1102,8 @@ describe('ReactSuspenseList', () => { , ); - await E.resolve(); - - await waitForAll(['E', 'Suspend! [F]']); + await act(() => E.resolve()); + assertLog(['E', 'Suspend! [F]']); // In the tail we can resolve one-by-one. expect(ReactNoop).toMatchRenderedOutput( @@ -1138,7 +1117,18 @@ describe('ReactSuspenseList', () => { , ); - await F.resolve(); + await act(() => F.resolve()); + assertLog(['F']); + expect(ReactNoop).toMatchRenderedOutput( + <> + A + B + C + D + E + F + , + ); // We can also delete some items. ReactNoop.render( @@ -1296,9 +1286,8 @@ describe('ReactSuspenseList', () => { , ); - await D.resolve(); - - await waitForAll(['D', 'F', 'Suspend! [B]']); + await act(() => D.resolve()); + assertLog(['D', 'F', 'Suspend! [B]']); // We can now resolve the full head. expect(ReactNoop).toMatchRenderedOutput( @@ -1314,9 +1303,8 @@ describe('ReactSuspenseList', () => { , ); - await B.resolve(); - - await waitForAll(['B', 'Suspend! [A]']); + await act(() => B.resolve()); + assertLog(['B', 'Suspend! [A]']); // In the tail we can resolve one-by-one. expect(ReactNoop).toMatchRenderedOutput( @@ -1331,9 +1319,8 @@ describe('ReactSuspenseList', () => { , ); - await A.resolve(); - - await waitForAll(['A']); + await act(() => A.resolve()); + assertLog(['A']); expect(ReactNoop).toMatchRenderedOutput( <> @@ -1366,47 +1353,43 @@ describe('ReactSuspenseList', () => { } // This render is only CPU bound. Nothing suspends. - if (gate(flags => flags.enableSyncDefaultUpdates)) { + await act(async () => { React.startTransition(() => { ReactNoop.render(); }); - } else { - ReactNoop.render(); - } - await waitFor(['A']); + await waitFor(['A']); - Scheduler.unstable_advanceTime(200); - jest.advanceTimersByTime(200); + Scheduler.unstable_advanceTime(200); + jest.advanceTimersByTime(200); - await waitFor(['B']); + await waitFor(['B']); - Scheduler.unstable_advanceTime(300); - jest.advanceTimersByTime(300); + Scheduler.unstable_advanceTime(300); + jest.advanceTimersByTime(300); - // We've still not been able to show anything on the screen even though - // we have two items ready. - expect(ReactNoop).toMatchRenderedOutput(null); + // We've still not been able to show anything on the screen even though + // we have two items ready. + expect(ReactNoop).toMatchRenderedOutput(null); - // Time has now elapsed for so long that we're just going to give up - // rendering the rest of the content. So that we can at least show - // something. - await waitFor([ - 'Loading C', - 'C', // I'll flush through into the next render so that the first commits. - ]); - - expect(ReactNoop).toMatchRenderedOutput( - <> - A - B - Loading C - , - ); + // Time has now elapsed for so long that we're just going to give up + // rendering the rest of the content. So that we can at least show + // something. + await waitFor([ + 'Loading C', + 'C', // I'll flush through into the next render so that the first commits. + ]); + expect(ReactNoop).toMatchRenderedOutput( + <> + A + B + Loading C + , + ); + }); // Then we do a second pass to commit the last item. - await waitForAll([]); - + assertLog([]); expect(ReactNoop).toMatchRenderedOutput( <> A @@ -1473,9 +1456,8 @@ describe('ReactSuspenseList', () => { , ); - await C.resolve(); - - await waitForAll(['C']); + await act(() => C.resolve()); + await assertLog(['C']); expect(ReactNoop).toMatchRenderedOutput( <> @@ -1554,47 +1536,43 @@ describe('ReactSuspenseList', () => { } // This render is only CPU bound. Nothing suspends. - if (gate(flags => flags.enableSyncDefaultUpdates)) { + await act(async () => { React.startTransition(() => { ReactNoop.render(); }); - } else { - ReactNoop.render(); - } - await waitFor(['A']); + await waitFor(['A']); - Scheduler.unstable_advanceTime(200); - jest.advanceTimersByTime(200); + Scheduler.unstable_advanceTime(200); + jest.advanceTimersByTime(200); - await waitFor(['B']); + await waitFor(['B']); - Scheduler.unstable_advanceTime(300); - jest.advanceTimersByTime(300); + Scheduler.unstable_advanceTime(300); + jest.advanceTimersByTime(300); - // We've still not been able to show anything on the screen even though - // we have two items ready. - expect(ReactNoop).toMatchRenderedOutput(null); + // We've still not been able to show anything on the screen even though + // we have two items ready. + expect(ReactNoop).toMatchRenderedOutput(null); - // Time has now elapsed for so long that we're just going to give up - // rendering the rest of the content. So that we can at least show - // something. - await waitFor([ - 'Loading C', - 'C', // I'll flush through into the next render so that the first commits. - ]); - - expect(ReactNoop).toMatchRenderedOutput( - <> - A - B - Loading C - , - ); + // Time has now elapsed for so long that we're just going to give up + // rendering the rest of the content. So that we can at least show + // something. + await waitFor([ + 'Loading C', + 'C', // I'll flush through into the next render so that the first commits. + ]); + expect(ReactNoop).toMatchRenderedOutput( + <> + A + B + Loading C + , + ); + }); // Then we do a second pass to commit the last two items. - await waitForAll(['D']); - + assertLog(['D']); expect(ReactNoop).toMatchRenderedOutput( <> A @@ -2073,9 +2051,8 @@ describe('ReactSuspenseList', () => { expect(ReactNoop).toMatchRenderedOutput(A); - await B.resolve(); - - await waitForAll(['B', 'Suspend! [C]', 'Loading C']); + await act(() => B.resolve()); + assertLog(['B', 'Suspend! [C]', 'Loading C']); // Incremental loading is suspended. jest.advanceTimersByTime(500); @@ -2087,9 +2064,8 @@ describe('ReactSuspenseList', () => { , ); - await C.resolve(); - - await waitForAll(['C']); + await act(() => C.resolve()); + assertLog(['C']); expect(ReactNoop).toMatchRenderedOutput( <> @@ -2149,9 +2125,8 @@ describe('ReactSuspenseList', () => { , ); - await B.resolve(); - - await waitForAll(['A', 'B', 'C', 'D']); + await act(() => B.resolve()); + assertLog(['A', 'B', 'C', 'D']); expect(ReactNoop).toMatchRenderedOutput( <> @@ -2191,9 +2166,8 @@ describe('ReactSuspenseList', () => { expect(ReactNoop).toMatchRenderedOutput(Loading C); - await B.resolve(); - - await waitForAll(['A', 'B', 'C']); + await act(() => B.resolve()); + assertLog(['A', 'B', 'C']); expect(ReactNoop).toMatchRenderedOutput( <> @@ -2252,9 +2226,8 @@ describe('ReactSuspenseList', () => { , ); - await B.resolve(); - - await waitForAll(['B']); + await act(() => B.resolve()); + assertLog(['B']); expect(ReactNoop).toMatchRenderedOutput( <> @@ -2385,9 +2358,8 @@ describe('ReactSuspenseList', () => { , ); - await AsyncB.resolve(); - - await waitForAll(['B']); + await act(() => AsyncB.resolve()); + assertLog(['B']); expect(ReactNoop).toMatchRenderedOutput( <> @@ -2474,9 +2446,8 @@ describe('ReactSuspenseList', () => { , ); - await AsyncB.resolve(); - - await waitForAll(['B']); + await act(() => AsyncB.resolve()); + assertLog(['B']); expect(ReactNoop).toMatchRenderedOutput( <> @@ -2555,9 +2526,8 @@ describe('ReactSuspenseList', () => { expect(ReactNoop).toMatchRenderedOutput(Loading A); }); - await AsyncA.resolve(); - - await waitForAll(['A', 'B', 'C', 'D']); + await act(() => AsyncA.resolve()); + assertLog(['A', 'B', 'C', 'D']); expect(ReactNoop).toMatchRenderedOutput( <> @@ -2916,9 +2886,8 @@ describe('ReactSuspenseList', () => { // treeBaseDuration expect(onRender.mock.calls[2][3]).toBe(1 + 4 + 3 + 3); - await C.resolve(); - - await waitForAll(['C', 'Suspend! [D]']); + await act(() => C.resolve()); + assertLog(['C', 'Suspend! [D]']); expect(ReactNoop).toMatchRenderedOutput( <> A @@ -3003,10 +2972,8 @@ describe('ReactSuspenseList', () => { , ); - await A.resolve(); - - await waitForAll(['A', 'Suspend! [B]']); - + await act(() => A.resolve()); + assertLog(['A', 'Suspend! [B]']); expect(ReactNoop).toMatchRenderedOutput( <> A @@ -3015,10 +2982,8 @@ describe('ReactSuspenseList', () => { , ); - await B.resolve(); - - await waitForAll(['B', 'C']); - + await act(() => B.resolve()); + assertLog(['B', 'C']); expect(ReactNoop).toMatchRenderedOutput( <> A diff --git a/packages/react-reconciler/src/__tests__/ReactSuspensePlaceholder-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspensePlaceholder-test.internal.js index b9bb57b58a..eed0e69fe1 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspensePlaceholder-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspensePlaceholder-test.internal.js @@ -19,6 +19,7 @@ let TextResource; let textResourceShouldFail; let waitForAll; let assertLog; +let act; describe('ReactSuspensePlaceholder', () => { beforeEach(() => { @@ -39,6 +40,7 @@ describe('ReactSuspensePlaceholder', () => { const InternalTestUtils = require('internal-test-utils'); waitForAll = InternalTestUtils.waitForAll; assertLog = InternalTestUtils.assertLog; + act = InternalTestUtils.act; TextResource = ReactCache.unstable_createResource( ([text, ms = 0]) => { @@ -137,10 +139,8 @@ describe('ReactSuspensePlaceholder', () => { await waitForAll(['A', 'Suspend! [B]', 'Loading...']); expect(ReactNoop).toMatchRenderedOutput('Loading...'); - jest.advanceTimersByTime(1000); - assertLog(['Promise resolved [B]']); - - await waitForAll(['A', 'B', 'C']); + await act(() => jest.advanceTimersByTime(1000)); + assertLog(['Promise resolved [B]', 'A', 'B', 'C']); expect(ReactNoop).toMatchRenderedOutput( <> @@ -167,9 +167,8 @@ describe('ReactSuspensePlaceholder', () => { ); // Resolve the promise - jest.advanceTimersByTime(1000); - assertLog(['Promise resolved [B2]']); - await waitForAll(['B2', 'C']); + await act(() => jest.advanceTimersByTime(1000)); + assertLog(['Promise resolved [B2]', 'B2', 'C']); // Render the final update. A should still be hidden, because it was // given a `hidden` prop. @@ -200,9 +199,8 @@ describe('ReactSuspensePlaceholder', () => { expect(ReactNoop).not.toMatchRenderedOutput('ABC'); - jest.advanceTimersByTime(1000); - assertLog(['Promise resolved [B]']); - await waitForAll(['A', 'B', 'C']); + await act(() => jest.advanceTimersByTime(1000)); + assertLog(['Promise resolved [B]', 'A', 'B', 'C']); expect(ReactNoop).toMatchRenderedOutput('ABC'); // Update @@ -214,9 +212,8 @@ describe('ReactSuspensePlaceholder', () => { expect(ReactNoop).toMatchRenderedOutput('Loading...'); // Resolve the promise - jest.advanceTimersByTime(1000); - assertLog(['Promise resolved [B2]']); - await waitForAll(['A', 'B2', 'C']); + await act(() => jest.advanceTimersByTime(1000)); + assertLog(['Promise resolved [B2]', 'A', 'B2', 'C']); // Render the final update. A should still be hidden, because it was // given a `hidden` prop. @@ -245,9 +242,8 @@ describe('ReactSuspensePlaceholder', () => { expect(ReactNoop).toMatchRenderedOutput(LOADING...); - jest.advanceTimersByTime(1000); - assertLog(['Promise resolved [b]']); - await waitForAll(['a', 'b', 'c']); + await act(() => jest.advanceTimersByTime(1000)); + assertLog(['Promise resolved [b]', 'a', 'b', 'c']); expect(ReactNoop).toMatchRenderedOutput(ABC); // Update @@ -259,9 +255,8 @@ describe('ReactSuspensePlaceholder', () => { expect(ReactNoop).toMatchRenderedOutput(LOADING...); // Resolve the promise - jest.advanceTimersByTime(1000); - assertLog(['Promise resolved [b2]']); - await waitForAll(['a', 'b2', 'c']); + await act(() => jest.advanceTimersByTime(1000)); + assertLog(['Promise resolved [b2]', 'a', 'b2', 'c']); // Render the final update. A should still be hidden, because it was // given a `hidden` prop. @@ -358,9 +353,13 @@ describe('ReactSuspensePlaceholder', () => { expect(onRender.mock.calls[0][3]).toBe(10); // Resolve the pending promise. - jest.advanceTimersByTime(1000); - assertLog(['Promise resolved [Loaded]']); - await waitForAll(['Suspending', 'Loaded', 'Text']); + await act(() => jest.advanceTimersByTime(1000)); + assertLog([ + 'Promise resolved [Loaded]', + 'Suspending', + 'Loaded', + 'Text', + ]); expect(ReactNoop).toMatchRenderedOutput('LoadedText'); expect(onRender).toHaveBeenCalledTimes(2); @@ -531,9 +530,14 @@ describe('ReactSuspensePlaceholder', () => { expect(onRender).toHaveBeenCalledTimes(3); // Resolve the pending promise. - jest.advanceTimersByTime(100); - assertLog(['Promise resolved [Loaded]', 'Promise resolved [Sibling]']); - await waitForAll(['Suspending', 'Loaded', 'New', 'Sibling']); + await act(async () => { + jest.advanceTimersByTime(100); + assertLog([ + 'Promise resolved [Loaded]', + 'Promise resolved [Sibling]', + ]); + await waitForAll(['Suspending', 'Loaded', 'New', 'Sibling']); + }); expect(onRender).toHaveBeenCalledTimes(4); // When the suspending data is resolved and our final UI is rendered, diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js index cabb198f2e..29948542ec 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js @@ -481,7 +481,7 @@ describe('ReactSuspenseWithNoopRenderer', () => { return ( }> - + ); @@ -491,9 +491,8 @@ describe('ReactSuspenseWithNoopRenderer', () => { await waitForAll(['Suspend! [Result]', 'Loading...']); expect(ReactNoop).toMatchRenderedOutput(); - await rejectText('Result', new Error('Failed to load: Result')); - - await waitForAll([ + await act(() => rejectText('Result', new Error('Failed to load: Result'))); + assertLog([ 'Error! [Result]', // React retries one more time @@ -3565,20 +3564,17 @@ describe('ReactSuspenseWithNoopRenderer', () => { ); await resolveText('A1'); - await waitFor([ - 'A1', - 'Suspend! [A2]', - 'Loading...', - 'Suspend! [B2]', - 'Loading...', - ]); - expect(root).toMatchRenderedOutput( - <> - - - , - ); + await waitFor(['A1']); + }); + assertLog(['Suspend! [A2]', 'Loading...', 'Suspend! [B2]', 'Loading...']); + expect(root).toMatchRenderedOutput( + <> + + + , + ); + await act(async () => { await resolveText('A2'); await resolveText('B2'); });