From 6cb9c3ae694dcc202ca74a3ed31c3d908344b97c Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Tue, 8 Sep 2020 10:30:34 -0500 Subject: [PATCH] Internal `act`: Flush timers at end of scope If there are any suspended fallbacks at the end of the `act` scope, force them to display by running the pending timers (i.e. `setTimeout`). The public implementation of `act` achieves the same behavior with an extra check in the work loop (`shouldForceFlushFallbacks`). Since our internal `act` needs to work in both development and production, without additional runtime checks, we instead rely on Jest's mock timers. This doesn't not affect refresh transitions, which are meant to delay indefinitely, because in that case we exit the work loop without posting a timer. --- .../src/test-utils/ReactTestUtilsAct.js | 7 +++- .../src/createReactNoop.js | 6 ++- .../src/__tests__/ReactBlocks-test.js | 18 ++++++--- .../ReactSuspenseWithNoopRenderer-test.js | 39 +++++++------------ .../src/ReactTestRenderer.js | 7 +++- 5 files changed, 42 insertions(+), 35 deletions(-) diff --git a/packages/react-dom/src/test-utils/ReactTestUtilsAct.js b/packages/react-dom/src/test-utils/ReactTestUtilsAct.js index 369981f18a074..03c6e2a8f561d 100644 --- a/packages/react-dom/src/test-utils/ReactTestUtilsAct.js +++ b/packages/react-dom/src/test-utils/ReactTestUtilsAct.js @@ -126,8 +126,11 @@ export function unstable_concurrentAct(scope: () => Thenable | void) { } function flushActWork(resolve, reject) { - // TODO: Run timers to flush suspended fallbacks - // jest.runOnlyPendingTimers(); + // Flush suspended fallbacks + /* eslint-disable no-undef */ + // $FlowFixMe - `jest` is not typed. We check for its existence above, though. + jest.runOnlyPendingTimers(); + /* eslint-enable no-undef */ enqueueTask(() => { try { const didFlushWork = Scheduler.unstable_flushAllWithoutAsserting(); diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js index f04c5479daa19..98088b261d673 100644 --- a/packages/react-noop-renderer/src/createReactNoop.js +++ b/packages/react-noop-renderer/src/createReactNoop.js @@ -1175,8 +1175,10 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { } function flushActWork(resolve, reject) { - // TODO: Run timers to flush suspended fallbacks - // jest.runOnlyPendingTimers(); + /* eslint-disable no-undef */ + // $FlowFixMe - `jest` is not typed. We check for its existence above, though. + jest.runOnlyPendingTimers(); + /* eslint-enable no-undef */ enqueueTask(() => { try { const didFlushWork = Scheduler.unstable_flushAllWithoutAsserting(); diff --git a/packages/react-reconciler/src/__tests__/ReactBlocks-test.js b/packages/react-reconciler/src/__tests__/ReactBlocks-test.js index 0d28a3359967d..18971c18a0c4a 100644 --- a/packages/react-reconciler/src/__tests__/ReactBlocks-test.js +++ b/packages/react-reconciler/src/__tests__/ReactBlocks-test.js @@ -14,6 +14,7 @@ let useState; let Suspense; let block; let readString; +let resolvePromises; let Scheduler; describe('ReactBlocks', () => { @@ -28,15 +29,16 @@ describe('ReactBlocks', () => { useState = React.useState; Suspense = React.Suspense; const cache = new Map(); + let unresolved = []; readString = function(text) { let entry = cache.get(text); if (!entry) { entry = { promise: new Promise(resolve => { - setTimeout(() => { + unresolved.push(() => { entry.resolved = true; resolve(); - }, 100); + }); }), resolved: false, }; @@ -47,6 +49,12 @@ describe('ReactBlocks', () => { } return text; }; + + resolvePromises = () => { + const res = unresolved; + unresolved = []; + res.forEach(r => r()); + }; }); // @gate experimental @@ -144,7 +152,7 @@ describe('ReactBlocks', () => { expect(ReactNoop).toMatchRenderedOutput('Loading...'); await ReactNoop.act(async () => { - jest.advanceTimersByTime(1000); + resolvePromises(); }); expect(ReactNoop).toMatchRenderedOutput(Name: Sebastian); @@ -291,7 +299,7 @@ describe('ReactBlocks', () => { ReactNoop.render(); }); await ReactNoop.act(async () => { - jest.advanceTimersByTime(1000); + resolvePromises(); }); expect(ReactNoop).toMatchRenderedOutput(Name: Sebastian); }); @@ -336,7 +344,7 @@ describe('ReactBlocks', () => { }); await ReactNoop.act(async () => { _setSuspend(false); - jest.advanceTimersByTime(1000); + resolvePromises(); }); expect(ReactNoop).toMatchRenderedOutput(Sebastian); }); diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js index 13d38e992c5e0..cbcdf8b4f6f57 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js @@ -575,7 +575,7 @@ describe('ReactSuspenseWithNoopRenderer', () => { {shouldSuspend ? ( - + ) : ( )} @@ -2595,7 +2595,7 @@ describe('ReactSuspenseWithNoopRenderer', () => { } return ( }> - + ); } @@ -2616,8 +2616,7 @@ describe('ReactSuspenseWithNoopRenderer', () => { }); // Later we load the data. - Scheduler.unstable_advanceTime(5000); - await advanceTimers(5000); + await resolveText('A'); expect(Scheduler).toHaveYielded(['Promise resolved [A]']); expect(Scheduler).toFlushAndYield(['A']); expect(ReactNoop.getChildren()).toEqual([span('A')]); @@ -2635,8 +2634,7 @@ describe('ReactSuspenseWithNoopRenderer', () => { }); // Later we load the data. - Scheduler.unstable_advanceTime(3000); - await advanceTimers(3000); + await resolveText('B'); expect(Scheduler).toHaveYielded(['Promise resolved [B]']); expect(Scheduler).toFlushAndYield(['B']); expect(ReactNoop.getChildren()).toEqual([span('B')]); @@ -2754,12 +2752,14 @@ describe('ReactSuspenseWithNoopRenderer', () => { ); }); + // TODO: This test is specifically about avoided commits that suspend for a + // JND. We may remove this behavior. it("suspended commit remains suspended even if there's another update at same expiration", async () => { // Regression test function App({text}) { return ( - + ); } @@ -2768,34 +2768,28 @@ describe('ReactSuspenseWithNoopRenderer', () => { await ReactNoop.act(async () => { root.render(); }); + expect(Scheduler).toHaveYielded(['Suspend! [Initial]']); // Resolve initial render await ReactNoop.act(async () => { - Scheduler.unstable_advanceTime(2000); - await advanceTimers(2000); + await resolveText('Initial'); }); - expect(Scheduler).toHaveYielded([ - 'Suspend! [Initial]', - 'Promise resolved [Initial]', - 'Initial', - ]); + expect(Scheduler).toHaveYielded(['Promise resolved [Initial]', 'Initial']); expect(root).toMatchRenderedOutput(); - // Update. Since showing a fallback would hide content that's already - // visible, it should suspend for a bit without committing. await ReactNoop.act(async () => { + // Update. Since showing a fallback would hide content that's already + // visible, it should suspend for a JND without committing. root.render(); - expect(Scheduler).toFlushAndYield(['Suspend! [First update]']); + // Should not display a fallback expect(root).toMatchRenderedOutput(); - }); - // Update again. This should also suspend for a bit. - await ReactNoop.act(async () => { + // Update again. This should also suspend for a JND. root.render(); - expect(Scheduler).toFlushAndYield(['Suspend! [Second update]']); + // Should not display a fallback expect(root).toMatchRenderedOutput(); }); @@ -3989,9 +3983,6 @@ describe('ReactSuspenseWithNoopRenderer', () => { await ReactNoop.act(async () => { root.render(); }); - // TODO: `act` should have already flushed the placeholder, so this - // runAllTimers call should be unnecessary. - jest.runAllTimers(); expect(Scheduler).toHaveYielded(['Suspend! [Async]', 'Loading...']); expect(root).toMatchRenderedOutput( <> diff --git a/packages/react-test-renderer/src/ReactTestRenderer.js b/packages/react-test-renderer/src/ReactTestRenderer.js index 616e38faaabae..ea2a07fd53768 100644 --- a/packages/react-test-renderer/src/ReactTestRenderer.js +++ b/packages/react-test-renderer/src/ReactTestRenderer.js @@ -684,8 +684,11 @@ function unstable_concurrentAct(scope: () => Thenable | void) { } function flushActWork(resolve, reject) { - // TODO: Run timers to flush suspended fallbacks - // jest.runOnlyPendingTimers(); + // Flush suspended fallbacks + /* eslint-disable no-undef */ + // $FlowFixMe - `jest` is not typed. We check for its existence above, though. + jest.runOnlyPendingTimers(); + /* eslint-enable no-undef */ enqueueTask(() => { try { const didFlushWork = Scheduler.unstable_flushAllWithoutAsserting();