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

Delete flushSuspenseFallbacksInTests flag (#18596)

* Move renderer `act` to work loop * Delete `flushSuspenseFallbacksInTests` This was meant to be a temporary hack to unblock the `act` work, but it quickly spread throughout our tests. What it's meant to do is force fallbacks to flush inside `act` even in Concurrent Mode. It does this by wrapping the `setTimeout` call in a check to see if it's in an `act` context. If so, it skips the delay and immediately commits the fallback. Really this is only meant for our internal React tests that need to incrementally render. Nobody outside our team (and Relay) needs to do that, yet. Even if/when we do support that, it may or may not be with the same `flushAndYield` pattern we use internally. However, even for our internal purposes, the behavior isn't right because a really common reason we flush work incrementally is to make assertions on the "suspended" state, before the fallback has committed. There's no way to do that from inside `act` with the behavior of this flag, because it causes the fallback to immediately commit. This has led us to *not* use `act` in a lot of our tests, or to write code that doesn't match what would actually happen in a real environment. What we really want is for the fallbacks to be flushed at the *end` of the `act` scope. Not within it. This only affects the noop and test renderer versions of `act`, which are implemented inside the reconciler. Whereas `ReactTestUtils.act` is implemented in "userspace" for backwards compatibility. This is fine because we didn't have any DOM Suspense tests that relied on this flag; they all use test renderer or noop. In the future, we'll probably want to move always use the reconciler implementation of `act`. It will not affect the prod bundle, because we currently only plan to support `act` in dev. Though we still haven't completely figured that out. However, regardless of whether we support a production `act` for users, we'll still need to write internal React tests in production mode. For that use case, we'll likely add our own internal version of `act` that assumes a mock Scheduler and might rely on hacks that don't 100% align up with the public one.

Andrew Clark committed Apr 13, 2020 at 20:02 UTC b928fc030ad296eadab4a29b13e1af49a6286e60
18 files changed +721 -676
packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.internal.js
+20 -11
@@ -14,7 +14,6 @@
14 const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils');
15
16 let React;
17 -let ReactFeatureFlags;
17 let ReactDOM;
18 let ReactDOMServer;
19 let ReactTestUtils;
@@ -39,9 +38,6 @@ function initModules() {
38 // Reset warning cache.
39 jest.resetModuleRegistry();
40
42 - ReactFeatureFlags = require('shared/ReactFeatureFlags');
43 -
44 - ReactFeatureFlags.flushSuspenseFallbacksInTests = false;
41 React = require('react');
42 ReactDOM = require('react-dom');
43 ReactDOMServer = require('react-dom/server');
@@ -1281,13 +1277,26 @@ describe('ReactDOMServerHooks', () => {
1277
1278 // State update should trigger the ID to update, which changes the props
1279 // of ChildWithID. This should cause ChildWithID to hydrate before Children
1284 - expect(Scheduler).toFlushAndYieldThrough([
1285 - 'Child with ID',
1286 - 'Child with ID',
1287 - 'Child with ID',
1288 - 'Child One',
1289 - 'Child Two',
1290 - ]);
1280 +
1281 + expect(Scheduler).toFlushAndYieldThrough(
1282 + __DEV__
1283 + ? [
1284 + 'Child with ID',
1285 + // Fallbacks are immdiately committed in TestUtils version
1286 + // of act
1287 + // 'Child with ID',
1288 + // 'Child with ID',
1289 + 'Child One',
1290 + 'Child Two',
1291 + ]
1292 + : [
1293 + 'Child with ID',
1294 + 'Child with ID',
1295 + 'Child with ID',
1296 + 'Child One',
1297 + 'Child Two',
1298 + ],
1299 + );
1300
1301 expect(child1Ref.current).toBe(null);
1302 expect(childWithIDRef.current).toEqual(
packages/react-reconciler/src/ReactFiberReconciler.new.js
+3 -186
@@ -18,7 +18,7 @@ import type {
18 } from './ReactFiberHostConfig';
19 import type {RendererInspectionConfig} from './ReactFiberHostConfig';
20 import {FundamentalComponent} from './ReactWorkTags';
21 -import type {ReactNodeList, Thenable} from 'shared/ReactTypes';
21 +import type {ReactNodeList} from 'shared/ReactTypes';
22 import type {ExpirationTime} from './ReactFiberExpirationTime.new';
23 import type {SuspenseState} from './ReactFiberSuspenseComponent.new';
24
@@ -64,6 +64,7 @@ import {
64 warnIfNotScopedWithMatchingAct,
65 warnIfUnmockedScheduler,
66 IsThisRendererActing,
67 + act,
68 } from './ReactFiberWorkLoop.new';
69 import {createUpdate, enqueueUpdate} from './ReactUpdateQueue.new';
70 import {getStackByFiberInDevAndProd} from './ReactFiberComponentStack';
@@ -85,11 +86,6 @@ import {
86 findHostInstancesForRefresh,
87 } from './ReactFiberHotReloading.new';
88
88 -// used by isTestEnvironment builds
89 -import enqueueTask from 'shared/enqueueTask';
90 -import * as Scheduler from 'scheduler';
91 -// end isTestEnvironment imports
92 -
89 export {createPortal} from './ReactPortal';
90
91 type OpaqueRoot = FiberRoot;
@@ -308,6 +304,7 @@ export {
304 flushSync,
305 flushPassiveEffects,
306 IsThisRendererActing,
307 + act,
308 };
309
310 export function getPublicRootInstance(
@@ -547,183 +544,3 @@ export function injectIntoDevTools(devToolsConfig: DevToolsConfig): boolean {
544 getCurrentFiber: __DEV__ ? getCurrentFiberForDevTools : null,
545 });
546 }
550 -
551 -const {IsSomeRendererActing} = ReactSharedInternals;
552 -const isSchedulerMocked =
553 - typeof Scheduler.unstable_flushAllWithoutAsserting === 'function';
554 -const flushWork =
555 - Scheduler.unstable_flushAllWithoutAsserting ||
556 - function() {
557 - let didFlushWork = false;
558 - while (flushPassiveEffects()) {
559 - didFlushWork = true;
560 - }
561 -
562 - return didFlushWork;
563 - };
564 -
565 -function flushWorkAndMicroTasks(onDone: (err: ?Error) => void) {
566 - try {
567 - flushWork();
568 - enqueueTask(() => {
569 - if (flushWork()) {
570 - flushWorkAndMicroTasks(onDone);
571 - } else {
572 - onDone();
573 - }
574 - });
575 - } catch (err) {
576 - onDone(err);
577 - }
578 -}
579 -
580 -// we track the 'depth' of the act() calls with this counter,
581 -// so we can tell if any async act() calls try to run in parallel.
582 -
583 -let actingUpdatesScopeDepth = 0;
584 -let didWarnAboutUsingActInProd = false;
585 -
586 -// eslint-disable-next-line no-inner-declarations
587 -export function act(callback: () => Thenable<mixed>): Thenable<void> {
588 - if (!__DEV__) {
589 - if (didWarnAboutUsingActInProd === false) {
590 - didWarnAboutUsingActInProd = true;
591 - // eslint-disable-next-line react-internal/no-production-logging
592 - console.error(
593 - 'act(...) is not supported in production builds of React, and might not behave as expected.',
594 - );
595 - }
596 - }
597 -
598 - const previousActingUpdatesScopeDepth = actingUpdatesScopeDepth;
599 - actingUpdatesScopeDepth++;
600 -
601 - const previousIsSomeRendererActing = IsSomeRendererActing.current;
602 - const previousIsThisRendererActing = IsThisRendererActing.current;
603 - IsSomeRendererActing.current = true;
604 - IsThisRendererActing.current = true;
605 -
606 - function onDone() {
607 - actingUpdatesScopeDepth--;
608 - IsSomeRendererActing.current = previousIsSomeRendererActing;
609 - IsThisRendererActing.current = previousIsThisRendererActing;
610 - if (__DEV__) {
611 - if (actingUpdatesScopeDepth > previousActingUpdatesScopeDepth) {
612 - // if it's _less than_ previousActingUpdatesScopeDepth, then we can assume the 'other' one has warned
613 - console.error(
614 - 'You seem to have overlapping act() calls, this is not supported. ' +
615 - 'Be sure to await previous act() calls before making a new one. ',
616 - );
617 - }
618 - }
619 - }
620 -
621 - let result;
622 - try {
623 - result = batchedUpdates(callback);
624 - } catch (error) {
625 - // on sync errors, we still want to 'cleanup' and decrement actingUpdatesScopeDepth
626 - onDone();
627 - throw error;
628 - }
629 -
630 - if (
631 - result !== null &&
632 - typeof result === 'object' &&
633 - typeof result.then === 'function'
634 - ) {
635 - // setup a boolean that gets set to true only
636 - // once this act() call is await-ed
637 - let called = false;
638 - if (__DEV__) {
639 - if (typeof Promise !== 'undefined') {
640 - //eslint-disable-next-line no-undef
641 - Promise.resolve()
642 - .then(() => {})
643 - .then(() => {
644 - if (called === false) {
645 - console.error(
646 - 'You called act(async () => ...) without await. ' +
647 - 'This could lead to unexpected testing behaviour, interleaving multiple act ' +
648 - 'calls and mixing their scopes. You should - await act(async () => ...);',
649 - );
650 - }
651 - });
652 - }
653 - }
654 -
655 - // in the async case, the returned thenable runs the callback, flushes
656 - // effects and microtasks in a loop until flushPassiveEffects() === false,
657 - // and cleans up
658 - return {
659 - then(resolve, reject) {
660 - called = true;
661 - result.then(
662 - () => {
663 - if (
664 - actingUpdatesScopeDepth > 1 ||
665 - (isSchedulerMocked === true &&
666 - previousIsSomeRendererActing === true)
667 - ) {
668 - onDone();
669 - resolve();
670 - return;
671 - }
672 - // we're about to exit the act() scope,
673 - // now's the time to flush tasks/effects
674 - flushWorkAndMicroTasks((err: ?Error) => {
675 - onDone();
676 - if (err) {
677 - reject(err);
678 - } else {
679 - resolve();
680 - }
681 - });
682 - },
683 - err => {
684 - onDone();
685 - reject(err);
686 - },
687 - );
688 - },
689 - };
690 - } else {
691 - if (__DEV__) {
692 - if (result !== undefined) {
693 - console.error(
694 - 'The callback passed to act(...) function ' +
695 - 'must return undefined, or a Promise. You returned %s',
696 - result,
697 - );
698 - }
699 - }
700 -
701 - // flush effects until none remain, and cleanup
702 - try {
703 - if (
704 - actingUpdatesScopeDepth === 1 &&
705 - (isSchedulerMocked === false || previousIsSomeRendererActing === false)
706 - ) {
707 - // we're about to exit the act() scope,
708 - // now's the time to flush effects
709 - flushWork();
710 - }
711 - onDone();
712 - } catch (err) {
713 - onDone();
714 - throw err;
715 - }
716 -
717 - // in the sync case, the returned thenable only warns *if* await-ed
718 - return {
719 - then(resolve) {
720 - if (__DEV__) {
721 - console.error(
722 - 'Do not await the result of calling act(...) with sync logic, it is not a Promise.',
723 - );
724 - }
725 - resolve();
726 - },
727 - };
728 - }
729 -}
packages/react-reconciler/src/ReactFiberReconciler.old.js
+3 -186
@@ -18,7 +18,7 @@ import type {
18 } from './ReactFiberHostConfig';
19 import type {RendererInspectionConfig} from './ReactFiberHostConfig';
20 import {FundamentalComponent} from './ReactWorkTags';
21 -import type {ReactNodeList, Thenable} from 'shared/ReactTypes';
21 +import type {ReactNodeList} from 'shared/ReactTypes';
22 import type {ExpirationTime} from './ReactFiberExpirationTime.old';
23 import type {SuspenseState} from './ReactFiberSuspenseComponent.old';
24
@@ -64,6 +64,7 @@ import {
64 warnIfNotScopedWithMatchingAct,
65 warnIfUnmockedScheduler,
66 IsThisRendererActing,
67 + act,
68 } from './ReactFiberWorkLoop.old';
69 import {createUpdate, enqueueUpdate} from './ReactUpdateQueue.old';
70 import {getStackByFiberInDevAndProd} from './ReactFiberComponentStack';
@@ -85,11 +86,6 @@ import {
86 findHostInstancesForRefresh,
87 } from './ReactFiberHotReloading.old';
88
88 -// used by isTestEnvironment builds
89 -import enqueueTask from 'shared/enqueueTask';
90 -import * as Scheduler from 'scheduler';
91 -// end isTestEnvironment imports
92 -
89 export {createPortal} from './ReactPortal';
90
91 type OpaqueRoot = FiberRoot;
@@ -308,6 +304,7 @@ export {
304 flushSync,
305 flushPassiveEffects,
306 IsThisRendererActing,
307 + act,
308 };
309
310 export function getPublicRootInstance(
@@ -547,183 +544,3 @@ export function injectIntoDevTools(devToolsConfig: DevToolsConfig): boolean {
544 getCurrentFiber: __DEV__ ? getCurrentFiberForDevTools : null,
545 });
546 }
550 -
551 -const {IsSomeRendererActing} = ReactSharedInternals;
552 -const isSchedulerMocked =
553 - typeof Scheduler.unstable_flushAllWithoutAsserting === 'function';
554 -const flushWork =
555 - Scheduler.unstable_flushAllWithoutAsserting ||
556 - function() {
557 - let didFlushWork = false;
558 - while (flushPassiveEffects()) {
559 - didFlushWork = true;
560 - }
561 -
562 - return didFlushWork;
563 - };
564 -
565 -function flushWorkAndMicroTasks(onDone: (err: ?Error) => void) {
566 - try {
567 - flushWork();
568 - enqueueTask(() => {
569 - if (flushWork()) {
570 - flushWorkAndMicroTasks(onDone);
571 - } else {
572 - onDone();
573 - }
574 - });
575 - } catch (err) {
576 - onDone(err);
577 - }
578 -}
579 -
580 -// we track the 'depth' of the act() calls with this counter,
581 -// so we can tell if any async act() calls try to run in parallel.
582 -
583 -let actingUpdatesScopeDepth = 0;
584 -let didWarnAboutUsingActInProd = false;
585 -
586 -// eslint-disable-next-line no-inner-declarations
587 -export function act(callback: () => Thenable<mixed>): Thenable<void> {
588 - if (!__DEV__) {
589 - if (didWarnAboutUsingActInProd === false) {
590 - didWarnAboutUsingActInProd = true;
591 - // eslint-disable-next-line react-internal/no-production-logging
592 - console.error(
593 - 'act(...) is not supported in production builds of React, and might not behave as expected.',
594 - );
595 - }
596 - }
597 -
598 - const previousActingUpdatesScopeDepth = actingUpdatesScopeDepth;
599 - actingUpdatesScopeDepth++;
600 -
601 - const previousIsSomeRendererActing = IsSomeRendererActing.current;
602 - const previousIsThisRendererActing = IsThisRendererActing.current;
603 - IsSomeRendererActing.current = true;
604 - IsThisRendererActing.current = true;
605 -
606 - function onDone() {
607 - actingUpdatesScopeDepth--;
608 - IsSomeRendererActing.current = previousIsSomeRendererActing;
609 - IsThisRendererActing.current = previousIsThisRendererActing;
610 - if (__DEV__) {
611 - if (actingUpdatesScopeDepth > previousActingUpdatesScopeDepth) {
612 - // if it's _less than_ previousActingUpdatesScopeDepth, then we can assume the 'other' one has warned
613 - console.error(
614 - 'You seem to have overlapping act() calls, this is not supported. ' +
615 - 'Be sure to await previous act() calls before making a new one. ',
616 - );
617 - }
618 - }
619 - }
620 -
621 - let result;
622 - try {
623 - result = batchedUpdates(callback);
624 - } catch (error) {
625 - // on sync errors, we still want to 'cleanup' and decrement actingUpdatesScopeDepth
626 - onDone();
627 - throw error;
628 - }
629 -
630 - if (
631 - result !== null &&
632 - typeof result === 'object' &&
633 - typeof result.then === 'function'
634 - ) {
635 - // setup a boolean that gets set to true only
636 - // once this act() call is await-ed
637 - let called = false;
638 - if (__DEV__) {
639 - if (typeof Promise !== 'undefined') {
640 - //eslint-disable-next-line no-undef
641 - Promise.resolve()
642 - .then(() => {})
643 - .then(() => {
644 - if (called === false) {
645 - console.error(
646 - 'You called act(async () => ...) without await. ' +
647 - 'This could lead to unexpected testing behaviour, interleaving multiple act ' +
648 - 'calls and mixing their scopes. You should - await act(async () => ...);',
649 - );
650 - }
651 - });
652 - }
653 - }
654 -
655 - // in the async case, the returned thenable runs the callback, flushes
656 - // effects and microtasks in a loop until flushPassiveEffects() === false,
657 - // and cleans up
658 - return {
659 - then(resolve, reject) {
660 - called = true;
661 - result.then(
662 - () => {
663 - if (
664 - actingUpdatesScopeDepth > 1 ||
665 - (isSchedulerMocked === true &&
666 - previousIsSomeRendererActing === true)
667 - ) {
668 - onDone();
669 - resolve();
670 - return;
671 - }
672 - // we're about to exit the act() scope,
673 - // now's the time to flush tasks/effects
674 - flushWorkAndMicroTasks((err: ?Error) => {
675 - onDone();
676 - if (err) {
677 - reject(err);
678 - } else {
679 - resolve();
680 - }
681 - });
682 - },
683 - err => {
684 - onDone();
685 - reject(err);
686 - },
687 - );
688 - },
689 - };
690 - } else {
691 - if (__DEV__) {
692 - if (result !== undefined) {
693 - console.error(
694 - 'The callback passed to act(...) function ' +
695 - 'must return undefined, or a Promise. You returned %s',
696 - result,
697 - );
698 - }
699 - }
700 -
701 - // flush effects until none remain, and cleanup
702 - try {
703 - if (
704 - actingUpdatesScopeDepth === 1 &&
705 - (isSchedulerMocked === false || previousIsSomeRendererActing === false)
706 - ) {
707 - // we're about to exit the act() scope,
708 - // now's the time to flush effects
709 - flushWork();
710 - }
711 - onDone();
712 - } catch (err) {
713 - onDone();
714 - throw err;
715 - }
716 -
717 - // in the sync case, the returned thenable only warns *if* await-ed
718 - return {
719 - then(resolve) {
720 - if (__DEV__) {
721 - console.error(
722 - 'Do not await the result of calling act(...) with sync logic, it is not a Promise.',
723 - );
724 - }
725 - resolve();
726 - },
727 - };
728 - }
729 -}
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+231 -17
@@ -7,7 +7,7 @@
7 * @flow
8 */
9
10 -import type {Wakeable} from 'shared/ReactTypes';
10 +import type {Thenable, Wakeable} from 'shared/ReactTypes';
11 import type {Fiber, FiberRoot} from './ReactInternalTypes';
12 import type {ExpirationTime} from './ReactFiberExpirationTime.new';
13 import type {ReactPriorityLevel} from './ReactInternalTypes';
@@ -26,7 +26,6 @@ import {
26 enableProfilerCommitHooks,
27 enableSchedulerTracing,
28 warnAboutUnmockedScheduler,
29 - flushSuspenseFallbacksInTests,
29 disableSchedulerTimeoutBasedOnReactExpirationTime,
30 } from 'shared/ReactFeatureFlags';
31 import ReactSharedInternals from 'shared/ReactSharedInternals';
@@ -180,6 +179,9 @@ import {
179 } from 'shared/ReactErrorUtils';
180 import {onCommitRoot} from './ReactFiberDevToolsHook.new';
181
182 +// Used by `act`
183 +import enqueueTask from 'shared/enqueueTask';
184 +
185 const ceil = Math.ceil;
186
187 const {
@@ -739,11 +741,7 @@ function finishConcurrentRender(
741 if (
742 hasNotProcessedNewUpdates &&
743 // do not delay if we're inside an act() scope
742 - !(
743 - __DEV__ &&
744 - flushSuspenseFallbacksInTests &&
745 - IsThisRendererActing.current
746 - )
744 + !shouldForceFlushFallbacksInDEV()
745 ) {
746 // If we have not processed any new updates during this pass, then
747 // this is either a retry of an existing fallback state or a
@@ -802,11 +800,7 @@ function finishConcurrentRender(
800
801 if (
802 // do not delay if we're inside an act() scope
805 - !(
806 - __DEV__ &&
807 - flushSuspenseFallbacksInTests &&
808 - IsThisRendererActing.current
809 - )
803 + !shouldForceFlushFallbacksInDEV()
804 ) {
805 // We're suspended in a state that should be avoided. We'll try to
806 // avoid committing it for as long as the timeouts let us.
@@ -878,11 +872,7 @@ function finishConcurrentRender(
872 // The work completed. Ready to commit.
873 if (
874 // do not delay if we're inside an act() scope
881 - !(
882 - __DEV__ &&
883 - flushSuspenseFallbacksInTests &&
884 - IsThisRendererActing.current
885 - ) &&
875 + !shouldForceFlushFallbacksInDEV() &&
876 workInProgressRootLatestProcessedEventTime !== Sync &&
877 workInProgressRootCanSuspendUsingConfig !== null
878 ) {
@@ -3197,3 +3187,227 @@ function finishPendingInteractions(root, committedExpirationTime) {
3187 );
3188 }
3189 }
3190 +
3191 +// `act` testing API
3192 +//
3193 +// TODO: This is mostly a copy-paste from the legacy `act`, which does not have
3194 +// access to the same internals that we do here. Some trade offs in the
3195 +// implementation no longer make sense.
3196 +
3197 +let isFlushingAct = false;
3198 +let isInsideThisAct = false;
3199 +
3200 +// TODO: Yes, this is confusing. See above comment. We'll refactor it.
3201 +function shouldForceFlushFallbacksInDEV() {
3202 + if (!__DEV__) {
3203 + // Never force flush in production. This function should get stripped out.
3204 + return false;
3205 + }
3206 + // `IsThisRendererActing.current` is used by ReactTestUtils version of `act`.
3207 + if (IsThisRendererActing.current) {
3208 + // `isInsideAct` is only used by the reconciler implementation of `act`.
3209 + // We don't want to flush suspense fallbacks until the end.
3210 + return !isInsideThisAct;
3211 + }
3212 + // Flush callbacks at the end.
3213 + return isFlushingAct;
3214 +}
3215 +
3216 +const flushMockScheduler = Scheduler.unstable_flushAllWithoutAsserting;
3217 +const isSchedulerMocked = typeof flushMockScheduler === 'function';
3218 +
3219 +// Returns whether additional work was scheduled. Caller should keep flushing
3220 +// until there's no work left.
3221 +function flushActWork(): boolean {
3222 + if (flushMockScheduler !== undefined) {
3223 + const prevIsFlushing = isFlushingAct;
3224 + isFlushingAct = true;
3225 + try {
3226 + return flushMockScheduler();
3227 + } finally {
3228 + isFlushingAct = prevIsFlushing;
3229 + }
3230 + } else {
3231 + // No mock scheduler available. However, the only type of pending work is
3232 + // passive effects, which we control. So we can flush that.
3233 + const prevIsFlushing = isFlushingAct;
3234 + isFlushingAct = true;
3235 + try {
3236 + let didFlushWork = false;
3237 + while (flushPassiveEffects()) {
3238 + didFlushWork = true;
3239 + }
3240 + return didFlushWork;
3241 + } finally {
3242 + isFlushingAct = prevIsFlushing;
3243 + }
3244 + }
3245 +}
3246 +
3247 +function flushWorkAndMicroTasks(onDone: (err: ?Error) => void) {
3248 + try {
3249 + flushActWork();
3250 + enqueueTask(() => {
3251 + if (flushActWork()) {
3252 + flushWorkAndMicroTasks(onDone);
3253 + } else {
3254 + onDone();
3255 + }
3256 + });
3257 + } catch (err) {
3258 + onDone(err);
3259 + }
3260 +}
3261 +
3262 +// we track the 'depth' of the act() calls with this counter,
3263 +// so we can tell if any async act() calls try to run in parallel.
3264 +
3265 +let actingUpdatesScopeDepth = 0;
3266 +let didWarnAboutUsingActInProd = false;
3267 +
3268 +export function act(callback: () => Thenable<mixed>): Thenable<void> {
3269 + if (!__DEV__) {
3270 + if (didWarnAboutUsingActInProd === false) {
3271 + didWarnAboutUsingActInProd = true;
3272 + // eslint-disable-next-line react-internal/no-production-logging
3273 + console.error(
3274 + 'act(...) is not supported in production builds of React, and might not behave as expected.',
3275 + );
3276 + }
3277 + }
3278 +
3279 + const previousActingUpdatesScopeDepth = actingUpdatesScopeDepth;
3280 + actingUpdatesScopeDepth++;
3281 +
3282 + const previousIsSomeRendererActing = IsSomeRendererActing.current;
3283 + const previousIsThisRendererActing = IsThisRendererActing.current;
3284 + const previousIsInsideThisAct = isInsideThisAct;
3285 + IsSomeRendererActing.current = true;
3286 + IsThisRendererActing.current = true;
3287 + isInsideThisAct = true;
3288 +
3289 + function onDone() {
3290 + actingUpdatesScopeDepth--;
3291 + IsSomeRendererActing.current = previousIsSomeRendererActing;
3292 + IsThisRendererActing.current = previousIsThisRendererActing;
3293 + isInsideThisAct = previousIsInsideThisAct;
3294 + if (__DEV__) {
3295 + if (actingUpdatesScopeDepth > previousActingUpdatesScopeDepth) {
3296 + // if it's _less than_ previousActingUpdatesScopeDepth, then we can assume the 'other' one has warned
3297 + console.error(
3298 + 'You seem to have overlapping act() calls, this is not supported. ' +
3299 + 'Be sure to await previous act() calls before making a new one. ',
3300 + );
3301 + }
3302 + }
3303 + }
3304 +
3305 + let result;
3306 + try {
3307 + result = batchedUpdates(callback);
3308 + } catch (error) {
3309 + // on sync errors, we still want to 'cleanup' and decrement actingUpdatesScopeDepth
3310 + onDone();
3311 + throw error;
3312 + }
3313 +
3314 + if (
3315 + result !== null &&
3316 + typeof result === 'object' &&
3317 + typeof result.then === 'function'
3318 + ) {
3319 + // setup a boolean that gets set to true only
3320 + // once this act() call is await-ed
3321 + let called = false;
3322 + if (__DEV__) {
3323 + if (typeof Promise !== 'undefined') {
3324 + //eslint-disable-next-line no-undef
3325 + Promise.resolve()
3326 + .then(() => {})
3327 + .then(() => {
3328 + if (called === false) {
3329 + console.error(
3330 + 'You called act(async () => ...) without await. ' +
3331 + 'This could lead to unexpected testing behaviour, interleaving multiple act ' +
3332 + 'calls and mixing their scopes. You should - await act(async () => ...);',
3333 + );
3334 + }
3335 + });
3336 + }
3337 + }
3338 +
3339 + // in the async case, the returned thenable runs the callback, flushes
3340 + // effects and microtasks in a loop until flushPassiveEffects() === false,
3341 + // and cleans up
3342 + return {
3343 + then(resolve, reject) {
3344 + called = true;
3345 + result.then(
3346 + () => {
3347 + if (
3348 + actingUpdatesScopeDepth > 1 ||
3349 + (isSchedulerMocked === true &&
3350 + previousIsSomeRendererActing === true)
3351 + ) {
3352 + onDone();
3353 + resolve();
3354 + return;
3355 + }
3356 + // we're about to exit the act() scope,
3357 + // now's the time to flush tasks/effects
3358 + flushWorkAndMicroTasks((err: ?Error) => {
3359 + onDone();
3360 + if (err) {
3361 + reject(err);
3362 + } else {
3363 + resolve();
3364 + }
3365 + });
3366 + },
3367 + err => {
3368 + onDone();
3369 + reject(err);
3370 + },
3371 + );
3372 + },
3373 + };
3374 + } else {
3375 + if (__DEV__) {
3376 + if (result !== undefined) {
3377 + console.error(
3378 + 'The callback passed to act(...) function ' +
3379 + 'must return undefined, or a Promise. You returned %s',
3380 + result,
3381 + );
3382 + }
3383 + }
3384 +
3385 + // flush effects until none remain, and cleanup
3386 + try {
3387 + if (
3388 + actingUpdatesScopeDepth === 1 &&
3389 + (isSchedulerMocked === false || previousIsSomeRendererActing === false)
3390 + ) {
3391 + // we're about to exit the act() scope,
3392 + // now's the time to flush effects
3393 + flushActWork();
3394 + }
3395 + onDone();
3396 + } catch (err) {
3397 + onDone();
3398 + throw err;
3399 + }
3400 +
3401 + // in the sync case, the returned thenable only warns *if* await-ed
3402 + return {
3403 + then(resolve) {
3404 + if (__DEV__) {
3405 + console.error(
3406 + 'Do not await the result of calling act(...) with sync logic, it is not a Promise.',
3407 + );
3408 + }
3409 + resolve();
3410 + },
3411 + };
3412 + }
3413 +}
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+231 -17
@@ -7,7 +7,7 @@
7 * @flow
8 */
9
10 -import type {Wakeable} from 'shared/ReactTypes';
10 +import type {Thenable, Wakeable} from 'shared/ReactTypes';
11 import type {Fiber, FiberRoot} from './ReactInternalTypes';
12 import type {ExpirationTime} from './ReactFiberExpirationTime.old';
13 import type {ReactPriorityLevel} from './ReactInternalTypes';
@@ -26,7 +26,6 @@ import {
26 enableProfilerCommitHooks,
27 enableSchedulerTracing,
28 warnAboutUnmockedScheduler,
29 - flushSuspenseFallbacksInTests,
29 disableSchedulerTimeoutBasedOnReactExpirationTime,
30 } from 'shared/ReactFeatureFlags';
31 import ReactSharedInternals from 'shared/ReactSharedInternals';
@@ -178,6 +177,9 @@ import {
177 } from 'shared/ReactErrorUtils';
178 import {onCommitRoot} from './ReactFiberDevToolsHook.old';
179
180 +// Used by `act`
181 +import enqueueTask from 'shared/enqueueTask';
182 +
183 const ceil = Math.ceil;
184
185 const {
@@ -732,11 +734,7 @@ function finishConcurrentRender(
734 if (
735 hasNotProcessedNewUpdates &&
736 // do not delay if we're inside an act() scope
735 - !(
736 - __DEV__ &&
737 - flushSuspenseFallbacksInTests &&
738 - IsThisRendererActing.current
739 - )
737 + !shouldForceFlushFallbacksInDEV()
738 ) {
739 // If we have not processed any new updates during this pass, then
740 // this is either a retry of an existing fallback state or a
@@ -795,11 +793,7 @@ function finishConcurrentRender(
793
794 if (
795 // do not delay if we're inside an act() scope
798 - !(
799 - __DEV__ &&
800 - flushSuspenseFallbacksInTests &&
801 - IsThisRendererActing.current
802 - )
796 + !shouldForceFlushFallbacksInDEV()
797 ) {
798 // We're suspended in a state that should be avoided. We'll try to
799 // avoid committing it for as long as the timeouts let us.
@@ -886,11 +880,7 @@ function finishConcurrentRender(
880 // The work completed. Ready to commit.
881 if (
882 // do not delay if we're inside an act() scope
889 - !(
890 - __DEV__ &&
891 - flushSuspenseFallbacksInTests &&
892 - IsThisRendererActing.current
893 - ) &&
883 + !shouldForceFlushFallbacksInDEV() &&
884 workInProgressRootLatestProcessedExpirationTime !== Sync &&
885 workInProgressRootCanSuspendUsingConfig !== null
886 ) {
@@ -3219,3 +3209,227 @@ function finishPendingInteractions(root, committedExpirationTime) {
3209 );
3210 }
3211 }
3212 +
3213 +// `act` testing API
3214 +//
3215 +// TODO: This is mostly a copy-paste from the legacy `act`, which does not have
3216 +// access to the same internals that we do here. Some trade offs in the
3217 +// implementation no longer make sense.
3218 +
3219 +let isFlushingAct = false;
3220 +let isInsideThisAct = false;
3221 +
3222 +// TODO: Yes, this is confusing. See above comment. We'll refactor it.
3223 +function shouldForceFlushFallbacksInDEV() {
3224 + if (!__DEV__) {
3225 + // Never force flush in production. This function should get stripped out.
3226 + return false;
3227 + }
3228 + // `IsThisRendererActing.current` is used by ReactTestUtils version of `act`.
3229 + if (IsThisRendererActing.current) {
3230 + // `isInsideAct` is only used by the reconciler implementation of `act`.
3231 + // We don't want to flush suspense fallbacks until the end.
3232 + return !isInsideThisAct;
3233 + }
3234 + // Flush callbacks at the end.
3235 + return isFlushingAct;
3236 +}
3237 +
3238 +const flushMockScheduler = Scheduler.unstable_flushAllWithoutAsserting;
3239 +const isSchedulerMocked = typeof flushMockScheduler === 'function';
3240 +
3241 +// Returns whether additional work was scheduled. Caller should keep flushing
3242 +// until there's no work left.
3243 +function flushActWork(): boolean {
3244 + if (flushMockScheduler !== undefined) {
3245 + const prevIsFlushing = isFlushingAct;
3246 + isFlushingAct = true;
3247 + try {
3248 + return flushMockScheduler();
3249 + } finally {
3250 + isFlushingAct = prevIsFlushing;
3251 + }
3252 + } else {
3253 + // No mock scheduler available. However, the only type of pending work is
3254 + // passive effects, which we control. So we can flush that.
3255 + const prevIsFlushing = isFlushingAct;
3256 + isFlushingAct = true;
3257 + try {
3258 + let didFlushWork = false;
3259 + while (flushPassiveEffects()) {
3260 + didFlushWork = true;
3261 + }
3262 + return didFlushWork;
3263 + } finally {
3264 + isFlushingAct = prevIsFlushing;
3265 + }
3266 + }
3267 +}
3268 +
3269 +function flushWorkAndMicroTasks(onDone: (err: ?Error) => void) {
3270 + try {
3271 + flushActWork();
3272 + enqueueTask(() => {
3273 + if (flushActWork()) {
3274 + flushWorkAndMicroTasks(onDone);
3275 + } else {
3276 + onDone();
3277 + }
3278 + });
3279 + } catch (err) {
3280 + onDone(err);
3281 + }
3282 +}
3283 +
3284 +// we track the 'depth' of the act() calls with this counter,
3285 +// so we can tell if any async act() calls try to run in parallel.
3286 +
3287 +let actingUpdatesScopeDepth = 0;
3288 +let didWarnAboutUsingActInProd = false;
3289 +
3290 +export function act(callback: () => Thenable<mixed>): Thenable<void> {
3291 + if (!__DEV__) {
3292 + if (didWarnAboutUsingActInProd === false) {
3293 + didWarnAboutUsingActInProd = true;
3294 + // eslint-disable-next-line react-internal/no-production-logging
3295 + console.error(
3296 + 'act(...) is not supported in production builds of React, and might not behave as expected.',
3297 + );
3298 + }
3299 + }
3300 +
3301 + const previousActingUpdatesScopeDepth = actingUpdatesScopeDepth;
3302 + actingUpdatesScopeDepth++;
3303 +
3304 + const previousIsSomeRendererActing = IsSomeRendererActing.current;
3305 + const previousIsThisRendererActing = IsThisRendererActing.current;
3306 + const previousIsInsideThisAct = isInsideThisAct;
3307 + IsSomeRendererActing.current = true;
3308 + IsThisRendererActing.current = true;
3309 + isInsideThisAct = true;
3310 +
3311 + function onDone() {
3312 + actingUpdatesScopeDepth--;
3313 + IsSomeRendererActing.current = previousIsSomeRendererActing;
3314 + IsThisRendererActing.current = previousIsThisRendererActing;
3315 + isInsideThisAct = previousIsInsideThisAct;
3316 + if (__DEV__) {
3317 + if (actingUpdatesScopeDepth > previousActingUpdatesScopeDepth) {
3318 + // if it's _less than_ previousActingUpdatesScopeDepth, then we can assume the 'other' one has warned
3319 + console.error(
3320 + 'You seem to have overlapping act() calls, this is not supported. ' +
3321 + 'Be sure to await previous act() calls before making a new one. ',
3322 + );
3323 + }
3324 + }
3325 + }
3326 +
3327 + let result;
3328 + try {
3329 + result = batchedUpdates(callback);
3330 + } catch (error) {
3331 + // on sync errors, we still want to 'cleanup' and decrement actingUpdatesScopeDepth
3332 + onDone();
3333 + throw error;
3334 + }
3335 +
3336 + if (
3337 + result !== null &&
3338 + typeof result === 'object' &&
3339 + typeof result.then === 'function'
3340 + ) {
3341 + // setup a boolean that gets set to true only
3342 + // once this act() call is await-ed
3343 + let called = false;
3344 + if (__DEV__) {
3345 + if (typeof Promise !== 'undefined') {
3346 + //eslint-disable-next-line no-undef
3347 + Promise.resolve()
3348 + .then(() => {})
3349 + .then(() => {
3350 + if (called === false) {
3351 + console.error(
3352 + 'You called act(async () => ...) without await. ' +
3353 + 'This could lead to unexpected testing behaviour, interleaving multiple act ' +
3354 + 'calls and mixing their scopes. You should - await act(async () => ...);',
3355 + );
3356 + }
3357 + });
3358 + }
3359 + }
3360 +
3361 + // in the async case, the returned thenable runs the callback, flushes
3362 + // effects and microtasks in a loop until flushPassiveEffects() === false,
3363 + // and cleans up
3364 + return {
3365 + then(resolve, reject) {
3366 + called = true;
3367 + result.then(
3368 + () => {
3369 + if (
3370 + actingUpdatesScopeDepth > 1 ||
3371 + (isSchedulerMocked === true &&
3372 + previousIsSomeRendererActing === true)
3373 + ) {
3374 + onDone();
3375 + resolve();
3376 + return;
3377 + }
3378 + // we're about to exit the act() scope,
3379 + // now's the time to flush tasks/effects
3380 + flushWorkAndMicroTasks((err: ?Error) => {
3381 + onDone();
3382 + if (err) {
3383 + reject(err);
3384 + } else {
3385 + resolve();
3386 + }
3387 + });
3388 + },
3389 + err => {
3390 + onDone();
3391 + reject(err);
3392 + },
3393 + );
3394 + },
3395 + };
3396 + } else {
3397 + if (__DEV__) {
3398 + if (result !== undefined) {
3399 + console.error(
3400 + 'The callback passed to act(...) function ' +
3401 + 'must return undefined, or a Promise. You returned %s',
3402 + result,
3403 + );
3404 + }
3405 + }
3406 +
3407 + // flush effects until none remain, and cleanup
3408 + try {
3409 + if (
3410 + actingUpdatesScopeDepth === 1 &&
3411 + (isSchedulerMocked === false || previousIsSomeRendererActing === false)
3412 + ) {
3413 + // we're about to exit the act() scope,
3414 + // now's the time to flush effects
3415 + flushActWork();
3416 + }
3417 + onDone();
3418 + } catch (err) {
3419 + onDone();
3420 + throw err;
3421 + }
3422 +
3423 + // in the sync case, the returned thenable only warns *if* await-ed
3424 + return {
3425 + then(resolve) {
3426 + if (__DEV__) {
3427 + console.error(
3428 + 'Do not await the result of calling act(...) with sync logic, it is not a Promise.',
3429 + );
3430 + }
3431 + resolve();
3432 + },
3433 + };
3434 + }
3435 +}
packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js
+104 -97
@@ -44,7 +44,6 @@ describe('ReactHooksWithNoopRenderer', () => {
44 ReactFeatureFlags = require('shared/ReactFeatureFlags');
45
46 ReactFeatureFlags.enableSchedulerTracing = true;
47 - ReactFeatureFlags.flushSuspenseFallbacksInTests = false;
47 ReactFeatureFlags.enableProfilerTimer = true;
48 deferPassiveEffectCleanupDuringUnmount =
49 ReactFeatureFlags.deferPassiveEffectCleanupDuringUnmount;
@@ -659,7 +658,7 @@ describe('ReactHooksWithNoopRenderer', () => {
658 expect(ReactNoop.getChildren()).toEqual([span(22)]);
659 });
660
662 - it('discards render phase updates if something suspends', () => {
661 + it('discards render phase updates if something suspends', async () => {
662 const thenable = {then() {}};
663 function Foo({signal}) {
664 return (
@@ -747,19 +746,20 @@ describe('ReactHooksWithNoopRenderer', () => {
746 await ReactNoop.act(async () => {
747 root.render(<Foo signal={false} />);
748 setLabel('B');
750 - });
751 - expect(Scheduler).toHaveYielded(['Suspend!']);
752 - expect(root).toMatchRenderedOutput(<span prop="A:0" />);
749
754 - // Rendering again should suspend again.
755 - root.render(<Foo signal={false} />);
756 - expect(Scheduler).toFlushAndYield(['Suspend!']);
750 + expect(Scheduler).toFlushAndYield(['Suspend!']);
751 + expect(root).toMatchRenderedOutput(<span prop="A:0" />);
752
758 - // Flip the signal back to "cancel" the update. However, the update to
759 - // label should still proceed. It shouldn't have been dropped.
760 - root.render(<Foo signal={true} />);
761 - expect(Scheduler).toFlushAndYield(['B:0']);
762 - expect(root).toMatchRenderedOutput(<span prop="B:0" />);
753 + // Rendering again should suspend again.
754 + root.render(<Foo signal={false} />);
755 + expect(Scheduler).toFlushAndYield(['Suspend!']);
756 +
757 + // Flip the signal back to "cancel" the update. However, the update to
758 + // label should still proceed. It shouldn't have been dropped.
759 + root.render(<Foo signal={true} />);
760 + expect(Scheduler).toFlushAndYield(['B:0']);
761 + expect(root).toMatchRenderedOutput(<span prop="B:0" />);
762 + });
763 });
764
765 it('regression: render phase updates cause lower pri work to be dropped', async () => {
@@ -2755,39 +2755,40 @@ describe('ReactHooksWithNoopRenderer', () => {
2755 span('Before... Pending: false'),
2756 ]);
2757
2758 - act(() => {
2758 + await act(async () => {
2759 Scheduler.unstable_runWithPriority(
2760 Scheduler.unstable_UserBlockingPriority,
2761 transition,
2762 );
2763 - });
2764 - Scheduler.unstable_advanceTime(500);
2765 - await advanceTimers(500);
2766 - expect(Scheduler).toHaveYielded([
2767 - 'Before... Pending: true',
2768 - 'Suspend! [After... Pending: false]',
2769 - 'Loading... Pending: false',
2770 - ]);
2771 - expect(ReactNoop.getChildren()).toEqual([
2772 - span('Before... Pending: true'),
2773 - ]);
2763
2775 - Scheduler.unstable_advanceTime(1000);
2776 - await advanceTimers(1000);
2777 - expect(ReactNoop.getChildren()).toEqual([
2778 - hiddenSpan('Before... Pending: true'),
2779 - span('Loading... Pending: false'),
2780 - ]);
2764 + expect(Scheduler).toFlushAndYield([
2765 + 'Before... Pending: true',
2766 + 'Suspend! [After... Pending: false]',
2767 + 'Loading... Pending: false',
2768 + ]);
2769 + expect(ReactNoop.getChildren()).toEqual([
2770 + span('Before... Pending: true'),
2771 + ]);
2772 + Scheduler.unstable_advanceTime(500);
2773 + await advanceTimers(500);
2774
2782 - Scheduler.unstable_advanceTime(500);
2783 - await advanceTimers(500);
2784 - expect(Scheduler).toHaveYielded([
2785 - 'Promise resolved [After... Pending: false]',
2786 - ]);
2787 - expect(Scheduler).toFlushAndYield(['After... Pending: false']);
2788 - expect(ReactNoop.getChildren()).toEqual([
2789 - span('After... Pending: false'),
2790 - ]);
2775 + Scheduler.unstable_advanceTime(1000);
2776 + await advanceTimers(1000);
2777 + expect(ReactNoop.getChildren()).toEqual([
2778 + hiddenSpan('Before... Pending: true'),
2779 + span('Loading... Pending: false'),
2780 + ]);
2781 +
2782 + Scheduler.unstable_advanceTime(500);
2783 + await advanceTimers(500);
2784 + expect(Scheduler).toHaveYielded([
2785 + 'Promise resolved [After... Pending: false]',
2786 + ]);
2787 + expect(Scheduler).toFlushAndYield(['After... Pending: false']);
2788 + expect(ReactNoop.getChildren()).toEqual([
2789 + span('After... Pending: false'),
2790 + ]);
2791 + });
2792 });
2793 // @gate experimental
2794 it('delays showing loading state until after busyDelayMs + busyMinDurationMs', async () => {
@@ -2820,51 +2821,54 @@ describe('ReactHooksWithNoopRenderer', () => {
2821 span('Before... Pending: false'),
2822 ]);
2823
2823 - act(() => {
2824 + await act(async () => {
2825 Scheduler.unstable_runWithPriority(
2826 Scheduler.unstable_UserBlockingPriority,
2827 transition,
2828 );
2828 - });
2829 - Scheduler.unstable_advanceTime(1000);
2830 - await advanceTimers(1000);
2831 - expect(Scheduler).toHaveYielded([
2832 - 'Before... Pending: true',
2833 - 'Suspend! [After... Pending: false]',
2834 - 'Loading... Pending: false',
2835 - ]);
2836 - expect(ReactNoop.getChildren()).toEqual([
2837 - span('Before... Pending: true'),
2838 - ]);
2829
2840 - // Resolve the promise. The whole tree has now completed. However,
2841 - // because we exceeded the busy threshold, we won't commit the
2842 - // result yet.
2843 - Scheduler.unstable_advanceTime(1000);
2844 - await advanceTimers(1000);
2845 - expect(Scheduler).toHaveYielded([
2846 - 'Promise resolved [After... Pending: false]',
2847 - ]);
2848 - expect(Scheduler).toFlushAndYield(['After... Pending: false']);
2849 - expect(ReactNoop.getChildren()).toEqual([
2850 - span('Before... Pending: true'),
2851 - ]);
2830 + expect(Scheduler).toFlushAndYield([
2831 + 'Before... Pending: true',
2832 + 'Suspend! [After... Pending: false]',
2833 + 'Loading... Pending: false',
2834 + ]);
2835 + expect(ReactNoop.getChildren()).toEqual([
2836 + span('Before... Pending: true'),
2837 + ]);
2838
2853 - // Advance time until just before the `busyMinDuration` threshold.
2854 - Scheduler.unstable_advanceTime(999);
2855 - await advanceTimers(999);
2856 - expect(ReactNoop.getChildren()).toEqual([
2857 - span('Before... Pending: true'),
2858 - ]);
2839 + Scheduler.unstable_advanceTime(1000);
2840 + await advanceTimers(1000);
2841
2860 - // Advance time just a bit more. Now we complete the transition.
2861 - Scheduler.unstable_advanceTime(300);
2862 - await advanceTimers(300);
2863 - expect(ReactNoop.getChildren()).toEqual([
2864 - span('After... Pending: false'),
2865 - ]);
2842 + // Resolve the promise. The whole tree has now completed. However,
2843 + // because we exceeded the busy threshold, we won't commit the
2844 + // result yet.
2845 + Scheduler.unstable_advanceTime(1000);
2846 + await advanceTimers(1000);
2847 + expect(Scheduler).toHaveYielded([
2848 + 'Promise resolved [After... Pending: false]',
2849 + ]);
2850 + expect(Scheduler).toFlushAndYield(['After... Pending: false']);
2851 + expect(ReactNoop.getChildren()).toEqual([
2852 + span('Before... Pending: true'),
2853 + ]);
2854 +
2855 + // Advance time until just before the `busyMinDuration` threshold.
2856 + Scheduler.unstable_advanceTime(999);
2857 + await advanceTimers(999);
2858 + expect(ReactNoop.getChildren()).toEqual([
2859 + span('Before... Pending: true'),
2860 + ]);
2861 +
2862 + // Advance time just a bit more. Now we complete the transition.
2863 + Scheduler.unstable_advanceTime(300);
2864 + await advanceTimers(300);
2865 + expect(ReactNoop.getChildren()).toEqual([
2866 + span('After... Pending: false'),
2867 + ]);
2868 + });
2869 });
2870 });
2871 +
2872 describe('useDeferredValue', () => {
2873 // @gate experimental
2874 it('defers text value until specified timeout', async () => {
@@ -2902,39 +2906,42 @@ describe('ReactHooksWithNoopRenderer', () => {
2906 expect(Scheduler).toFlushAndYield(['A']);
2907 expect(ReactNoop.getChildren()).toEqual([span('A'), span('A')]);
2908
2905 - act(() => {
2909 + await act(async () => {
2910 _setText('B');
2911 + expect(Scheduler).toFlushAndYield([
2912 + 'B',
2913 + 'A',
2914 + 'B',
2915 + 'Suspend! [B]',
2916 + 'Loading',
2917 + ]);
2918 + expect(Scheduler).toFlushAndYield([]);
2919 + expect(ReactNoop.getChildren()).toEqual([span('B'), span('A')]);
2920 });
2908 - expect(Scheduler).toHaveYielded([
2909 - 'B',
2910 - 'A',
2911 - 'B',
2912 - 'Suspend! [B]',
2913 - 'Loading',
2914 - ]);
2915 - expect(Scheduler).toFlushAndYield([]);
2916 - expect(ReactNoop.getChildren()).toEqual([span('B'), span('A')]);
2921
2918 - Scheduler.unstable_advanceTime(250);
2919 - await advanceTimers(250);
2920 - expect(Scheduler).toFlushAndYield([]);
2922 + await act(async () => {
2923 + Scheduler.unstable_advanceTime(250);
2924 + await advanceTimers(250);
2925 + });
2926 + expect(Scheduler).toHaveYielded([]);
2927 expect(ReactNoop.getChildren()).toEqual([span('B'), span('A')]);
2928
2923 - Scheduler.unstable_advanceTime(500);
2924 - await advanceTimers(500);
2929 + await act(async () => {
2930 + Scheduler.unstable_advanceTime(500);
2931 + await advanceTimers(500);
2932 + });
2933 + expect(Scheduler).toHaveYielded([]);
2934 expect(ReactNoop.getChildren()).toEqual([
2935 span('B'),
2936 hiddenSpan('A'),
2937 span('Loading'),
2938 ]);
2939
2931 - Scheduler.unstable_advanceTime(250);
2932 - await advanceTimers(250);
2933 - expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
2934 -
2935 - act(() => {
2936 - expect(Scheduler).toFlushAndYield(['B']);
2940 + await act(async () => {
2941 + Scheduler.unstable_advanceTime(250);
2942 + await advanceTimers(250);
2943 });
2944 + expect(Scheduler).toHaveYielded(['Promise resolved [B]', 'B']);
2945 expect(ReactNoop.getChildren()).toEqual([span('B'), span('B')]);
2946 });
2947 });
packages/react-reconciler/src/__tests__/ReactSuspenseList-test.internal.js
+15 -29
@@ -2469,35 +2469,21 @@ describe('ReactSuspenseList', () => {
2469
2470 jest.runAllTimers();
2471
2472 - expect(Scheduler).toHaveYielded(
2473 - __DEV__
2474 - ? [
2475 - // First attempt at high pri.
2476 - 'Suspend! [A]',
2477 - 'Loading A',
2478 - // Re-render at forced.
2479 - 'Suspend! [A]',
2480 - 'Loading A',
2481 - // We auto-commit this on DEV.
2482 - // Try again on low-pri.
2483 - 'Suspend! [A]',
2484 - 'Loading A',
2485 - ]
2486 - : [
2487 - // First attempt at high pri.
2488 - 'Suspend! [A]',
2489 - 'Loading A',
2490 - // Re-render at forced.
2491 - 'Suspend! [A]',
2492 - 'Loading A',
2493 - // We didn't commit so retry at low-pri.
2494 - 'Suspend! [A]',
2495 - 'Loading A',
2496 - // Re-render at forced.
2497 - 'Suspend! [A]',
2498 - 'Loading A',
2499 - ],
2500 - );
2472 + expect(Scheduler).toHaveYielded([
2473 + // First attempt at high pri.
2474 + 'Suspend! [A]',
2475 + 'Loading A',
2476 + // Re-render at forced.
2477 + 'Suspend! [A]',
2478 + 'Loading A',
2479 + // We auto-commit this on DEV.
2480 + // Try again on low-pri.
2481 + 'Suspend! [A]',
2482 + 'Loading A',
2483 + // Re-render at forced.
2484 + 'Suspend! [A]',
2485 + 'Loading A',
2486 + ]);
2487
2488 expect(ReactNoop).toMatchRenderedOutput(<span>Loading A</span>);
2489
packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js
+114 -114
@@ -17,7 +17,6 @@ describe('ReactSuspenseWithNoopRenderer', () => {
17 ReactFeatureFlags = require('shared/ReactFeatureFlags');
18
19 ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
20 - ReactFeatureFlags.flushSuspenseFallbacksInTests = false;
20 React = require('react');
21 Fragment = React.Fragment;
22 ReactNoop = require('react-noop-renderer');
@@ -3069,25 +3068,26 @@ describe('ReactSuspenseWithNoopRenderer', () => {
3068 setText('C');
3069 },
3070 );
3072 - });
3073 - expect(Scheduler).toHaveYielded([
3074 - // First we attempt the high pri update. It suspends.
3075 - 'Suspend! [B]',
3076 - 'Loading...',
3077 - ]);
3071
3079 - // Commit the placeholder to unblock the Idle update.
3080 - await advanceTimers(250);
3081 - expect(root).toMatchRenderedOutput(
3082 - <>
3083 - <span hidden={true} prop="A" />
3084 - <span prop="Loading..." />
3085 - </>,
3086 - );
3072 + expect(Scheduler).toFlushAndYield([
3073 + // First we attempt the high pri update. It suspends.
3074 + 'Suspend! [B]',
3075 + 'Loading...',
3076 + ]);
3077
3088 - // Now flush the remaining work. The Idle update successfully finishes.
3089 - expect(Scheduler).toFlushAndYield(['C']);
3090 - expect(root).toMatchRenderedOutput(<span prop="C" />);
3078 + // Commit the placeholder to unblock the Idle update.
3079 + await advanceTimers(250);
3080 + expect(root).toMatchRenderedOutput(
3081 + <>
3082 + <span hidden={true} prop="A" />
3083 + <span prop="Loading..." />
3084 + </>,
3085 + );
3086 +
3087 + // Now flush the remaining work. The Idle update successfully finishes.
3088 + expect(Scheduler).toFlushAndYield(['C']);
3089 + expect(root).toMatchRenderedOutput(<span prop="C" />);
3090 + });
3091 },
3092 );
3093
@@ -3126,48 +3126,47 @@ describe('ReactSuspenseWithNoopRenderer', () => {
3126 expect(Scheduler).toHaveYielded(['A']);
3127 expect(root).toMatchRenderedOutput(<span prop="A" />);
3128
3129 - // Schedule an update inside the Suspense boundary that suspends.
3129 await ReactNoop.act(async () => {
3130 + // Schedule an update inside the Suspense boundary that suspends.
3131 setAppText('B');
3132 - });
3133 - expect(Scheduler).toHaveYielded(['Suspend! [B]', 'Loading...']);
3134 - // Commit the placeholder
3135 - await advanceTimers(250);
3136 - expect(root).toMatchRenderedOutput(
3137 - <>
3138 - <span hidden={true} prop="A" />
3139 - <span prop="Loading..." />
3140 - </>,
3141 - );
3132 + expect(Scheduler).toFlushAndYield(['Suspend! [B]', 'Loading...']);
3133
3143 - // Schedule a high pri update on the boundary, and a lower pri update
3144 - // on the fallback. We're testing to make sure the fallback can still
3145 - // update even though the primary tree is suspended.
3146 - await ReactNoop.act(async () => {
3134 + // Commit the placeholder
3135 + await advanceTimers(250);
3136 + expect(root).toMatchRenderedOutput(
3137 + <>
3138 + <span hidden={true} prop="A" />
3139 + <span prop="Loading..." />
3140 + </>,
3141 + );
3142 +
3143 + // Schedule a high pri update on the boundary, and a lower pri update
3144 + // on the fallback. We're testing to make sure the fallback can still
3145 + // update even though the primary tree is suspended.{
3146 ReactNoop.discreteUpdates(() => {
3147 setAppText('C');
3148 });
3149 setFallbackText('Still loading...');
3151 - });
3150
3153 - expect(Scheduler).toHaveYielded([
3154 - // First try to render the high pri update. We won't try to re-render
3155 - // the suspended tree during this pass, because it still has unfinished
3156 - // updates at a lower priority.
3157 - 'Loading...',
3151 + expect(Scheduler).toFlushAndYield([
3152 + // First try to render the high pri update. We won't try to re-render
3153 + // the suspended tree during this pass, because it still has unfinished
3154 + // updates at a lower priority.
3155 + 'Loading...',
3156
3159 - // Now try the suspended update again. It's still suspended.
3160 - 'Suspend! [C]',
3157 + // Now try the suspended update again. It's still suspended.
3158 + 'Suspend! [C]',
3159
3162 - // Then complete the update to the fallback.
3163 - 'Still loading...',
3164 - ]);
3165 - expect(root).toMatchRenderedOutput(
3166 - <>
3167 - <span hidden={true} prop="A" />
3168 - <span prop="Still loading..." />
3169 - </>,
3170 - );
3160 + // Then complete the update to the fallback.
3161 + 'Still loading...',
3162 + ]);
3163 + expect(root).toMatchRenderedOutput(
3164 + <>
3165 + <span hidden={true} prop="A" />
3166 + <span prop="Still loading..." />
3167 + </>,
3168 + );
3169 + });
3170 },
3171 );
3172
@@ -3693,54 +3692,53 @@ describe('ReactSuspenseWithNoopRenderer', () => {
3692 setTextA('A2');
3693 setTextB('B2');
3694 });
3696 - });
3697 - expect(Scheduler).toHaveYielded([
3698 - 'B',
3699 - 'Suspend! [A1]',
3700 - 'Loading...',
3695
3702 - 'Suspend! [A2]',
3703 - 'Loading...',
3704 - 'Suspend! [B2]',
3705 - 'Loading...',
3706 - ]);
3707 - expect(root).toMatchRenderedOutput(
3708 - <>
3709 - <span prop="A" />
3710 - <span prop="B" />
3711 - </>,
3712 - );
3696 + expect(Scheduler).toFlushAndYield([
3697 + 'B',
3698 + 'Suspend! [A1]',
3699 + 'Loading...',
3700
3714 - await ReactNoop.act(async () => {
3715 - resolveText('A1');
3716 - });
3717 - expect(Scheduler).toHaveYielded([
3718 - 'Promise resolved [A1]',
3719 - 'A1',
3720 - 'Suspend! [A2]',
3721 - 'Loading...',
3722 - 'Suspend! [B2]',
3723 - 'Loading...',
3724 - ]);
3725 - expect(root).toMatchRenderedOutput(
3726 - <>
3727 - <span prop="A1" />
3728 - <span prop="B" />
3729 - </>,
3730 - );
3701 + 'Suspend! [A2]',
3702 + 'Loading...',
3703 + 'Suspend! [B2]',
3704 + 'Loading...',
3705 + ]);
3706 + expect(root).toMatchRenderedOutput(
3707 + <>
3708 + <span prop="A" />
3709 + <span prop="B" />
3710 + </>,
3711 + );
3712
3732 - // Commit the placeholder
3733 - Scheduler.unstable_advanceTime(20000);
3734 - await advanceTimers(20000);
3713 + await resolveText('A1');
3714 + expect(Scheduler).toHaveYielded(['Promise resolved [A1]']);
3715 + expect(Scheduler).toFlushAndYield([
3716 + 'A1',
3717 + 'Suspend! [A2]',
3718 + 'Loading...',
3719 + 'Suspend! [B2]',
3720 + 'Loading...',
3721 + ]);
3722 + expect(root).toMatchRenderedOutput(
3723 + <>
3724 + <span prop="A1" />
3725 + <span prop="B" />
3726 + </>,
3727 + );
3728
3736 - expect(root).toMatchRenderedOutput(
3737 - <>
3738 - <span hidden={true} prop="A1" />
3739 - <span prop="Loading..." />
3740 - <span hidden={true} prop="B" />
3741 - <span prop="Loading..." />
3742 - </>,
3743 - );
3729 + // Commit the placeholder
3730 + Scheduler.unstable_advanceTime(20000);
3731 + await advanceTimers(20000);
3732 +
3733 + expect(root).toMatchRenderedOutput(
3734 + <>
3735 + <span hidden={true} prop="A1" />
3736 + <span prop="Loading..." />
3737 + <span hidden={true} prop="B" />
3738 + <span prop="Loading..." />
3739 + </>,
3740 + );
3741 + });
3742 });
3743
3744 // Regression: https://github.com/facebook/react/issues/18486
@@ -3842,27 +3840,29 @@ describe('ReactSuspenseWithNoopRenderer', () => {
3840 // Resolve "a". But "b" is still pending.
3841 await ReactNoop.act(async () => {
3842 await resolveText('a');
3845 - });
3846 - expect(Scheduler).toHaveYielded([
3847 - 'Promise resolved [a]',
3848 - 'Pending...',
3849 - 'a',
3850 - 'Suspend! [b]',
3851 - 'Loading...',
3852 - ]);
3853 - expect(root).toMatchRenderedOutput(
3854 - <>
3855 - <span prop="Pending..." />
3856 - <span prop="a" />
3857 - </>,
3858 - );
3843
3860 - // Resolve "b". This should remove the pending state.
3861 - await ReactNoop.act(async () => {
3862 - await resolveText('b');
3844 + expect(Scheduler).toHaveYielded(['Promise resolved [a]']);
3845 + expect(Scheduler).toFlushAndYield([
3846 + 'Pending...',
3847 + 'a',
3848 + 'Suspend! [b]',
3849 + 'Loading...',
3850 + ]);
3851 + expect(root).toMatchRenderedOutput(
3852 + <>
3853 + <span prop="Pending..." />
3854 + <span prop="a" />
3855 + </>,
3856 + );
3857 +
3858 + // Resolve "b". This should remove the pending state.
3859 + await ReactNoop.act(async () => {
3860 + await resolveText('b');
3861 + });
3862 + expect(Scheduler).toHaveYielded(['Promise resolved [b]']);
3863 + expect(Scheduler).toFlushAndYield(['b']);
3864 + // The bug was that the pending state got stuck forever.
3865 + expect(root).toMatchRenderedOutput(<span prop="b" />);
3866 });
3864 - expect(Scheduler).toHaveYielded(['Promise resolved [b]', 'b']);
3865 - // The bug was that the pending state got stuck forever.
3866 - expect(root).toMatchRenderedOutput(<span prop="b" />);
3867 });
3868 });
packages/react-reconciler/src/__tests__/ReactTransition-test.internal.js
-6
@@ -10,7 +10,6 @@
10
11 'use strict';
12
13 -let ReactFeatureFlags;
13 let React;
14 let ReactNoop;
15 let Scheduler;
@@ -22,11 +21,6 @@ let act;
21 describe('ReactTransition', () => {
22 beforeEach(() => {
23 jest.resetModules();
25 -
26 - ReactFeatureFlags = require('shared/ReactFeatureFlags');
27 -
28 - ReactFeatureFlags.enableSchedulerTracing = true;
29 - ReactFeatureFlags.flushSuspenseFallbacksInTests = false;
24 React = require('react');
25 ReactNoop = require('react-noop-renderer');
26 Scheduler = require('scheduler');
packages/react-reconciler/src/__tests__/useMutableSource-test.internal.js
-1
@@ -26,7 +26,6 @@ function loadModules() {
26 ReactFeatureFlags = require('shared/ReactFeatureFlags');
27
28 ReactFeatureFlags.enableSchedulerTracing = true;
29 - ReactFeatureFlags.flushSuspenseFallbacksInTests = false;
29 ReactFeatureFlags.enableProfilerTimer = true;
30 React = require('react');
31 ReactNoop = require('react-noop-renderer');
packages/shared/ReactFeatureFlags.js
-4
@@ -62,10 +62,6 @@ export const enableUseEventAPI = false;
62 // Till then, we warn about the missing mock, but still fallback to a legacy mode compatible version
63 export const warnAboutUnmockedScheduler = false;
64
65 -// For tests, we flush suspense fallbacks in an act scope;
66 -// *except* in some of our own tests, where we test incremental loading states.
67 -export const flushSuspenseFallbacksInTests = true;
68 -
65 // Add a callback property to suspense to notify which promises are currently
66 // in the update queue. This allows reporting and tracing of what is causing
67 // the user to see a loading state.
packages/shared/forks/ReactFeatureFlags.native-fb.js
-1
@@ -28,7 +28,6 @@ export const enableFundamentalAPI = false;
28 export const enableScopeAPI = false;
29 export const enableUseEventAPI = false;
30 export const warnAboutUnmockedScheduler = true;
31 -export const flushSuspenseFallbacksInTests = true;
31 export const enableSuspenseCallback = false;
32 export const warnAboutDefaultPropsOnFunctionComponents = false;
33 export const warnAboutStringRefs = false;
packages/shared/forks/ReactFeatureFlags.native-oss.js
-1
@@ -27,7 +27,6 @@ export const enableFundamentalAPI = false;
27 export const enableScopeAPI = false;
28 export const enableUseEventAPI = false;
29 export const warnAboutUnmockedScheduler = false;
30 -export const flushSuspenseFallbacksInTests = true;
30 export const enableSuspenseCallback = false;
31 export const warnAboutDefaultPropsOnFunctionComponents = false;
32 export const warnAboutStringRefs = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.js
-1
@@ -27,7 +27,6 @@ export const enableFundamentalAPI = false;
27 export const enableScopeAPI = false;
28 export const enableUseEventAPI = false;
29 export const warnAboutUnmockedScheduler = false;
30 -export const flushSuspenseFallbacksInTests = true;
30 export const enableSuspenseCallback = false;
31 export const warnAboutDefaultPropsOnFunctionComponents = false;
32 export const warnAboutStringRefs = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
-1
@@ -27,7 +27,6 @@ export const enableFundamentalAPI = false;
27 export const enableScopeAPI = true;
28 export const enableUseEventAPI = true;
29 export const warnAboutUnmockedScheduler = true;
30 -export const flushSuspenseFallbacksInTests = true;
30 export const enableSuspenseCallback = true;
31 export const warnAboutDefaultPropsOnFunctionComponents = false;
32 export const warnAboutStringRefs = false;
packages/shared/forks/ReactFeatureFlags.testing.js
-1
@@ -27,7 +27,6 @@ export const enableFundamentalAPI = false;
27 export const enableScopeAPI = false;
28 export const enableUseEventAPI = false;
29 export const warnAboutUnmockedScheduler = false;
30 -export const flushSuspenseFallbacksInTests = true;
30 export const enableSuspenseCallback = false;
31 export const warnAboutDefaultPropsOnFunctionComponents = false;
32 export const warnAboutStringRefs = false;
packages/shared/forks/ReactFeatureFlags.testing.www.js
-1
@@ -27,7 +27,6 @@ export const enableFundamentalAPI = false;
27 export const enableScopeAPI = true;
28 export const enableUseEventAPI = true;
29 export const warnAboutUnmockedScheduler = true;
30 -export const flushSuspenseFallbacksInTests = true;
30 export const enableSuspenseCallback = true;
31 export const warnAboutDefaultPropsOnFunctionComponents = false;
32 export const warnAboutStringRefs = false;
packages/shared/forks/ReactFeatureFlags.www.js
-2
@@ -64,8 +64,6 @@ export const warnAboutUnmockedScheduler = true;
64
65 export const enableSuspenseCallback = true;
66
67 -export const flushSuspenseFallbacksInTests = true;
68 -
67 export const disableTextareaChildren = __EXPERIMENTAL__;
68
69 export const warnUnstableRenderSubtreeIntoContainer = false;