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

Expand act warning to cover all APIs that might schedule React work (#22607)

* Move isActEnvironment check to function that warns I'm about to fork the behavior in legacy roots versus concurrent roots even further, so I'm lifting this up so I only have to fork once. * Lift `mode` check, too Similar to previous commit. I only want to check this once. Not for performance reasons, but so the logic is easier to follow. * Expand act warning to include non-hook APIs In a test environment, React warns if an update isn't wrapped with act — but only if the update originates from a hook API, like useState. We did it this way for backwards compatibility with tests that were written before the act API was introduced. Those tests didn't require act, anyway, because in a legacy root, all tasks are synchronous except for `useEffect`. However, in a concurrent root, nearly every task is asynchronous. Even tasks that are synchronous may spawn additional asynchronous work. So all updates need to be wrapped with act, regardless of whether they originate from a hook, a class, a root, or any other type of component. This commit expands the act warning to include any API that triggers an update. It does not currently account for renders that are caused by a Suspense promise resolving; those are modelled slightly differently from updates. I'll fix that in the next step. I also removed the check for whether an update is batched. It shouldn't matter, because even a batched update can spawn asynchronous work, which needs to be flushed by act. This change only affects concurrent roots. The behavior in legacy roots is the same. * Expand act warning to include Suspense resolutions For the same reason we warn when an update is not wrapped with act, we should warn if a Suspense promise resolution is not wrapped with act. Both "pings" and "retries". Legacy root behavior is unchanged.

Andrew Clark committed Oct 21, 2021 at 18:31 UTC d5b6b4b865ebf13a1eaf2342d623101056e5e197
13 files changed +578 -240
packages/react-debug-tools/src/__tests__/ReactDevToolsHooksIntegration-test.js
+16 -12
@@ -20,6 +20,8 @@ describe('React hooks DevTools integration', () => {
20 let scheduleUpdate;
21 let setSuspenseHandler;
22
23 + global.IS_REACT_ACT_ENVIRONMENT = true;
24 +
25 beforeEach(() => {
26 global.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
27 inject: injected => {
@@ -64,7 +66,7 @@ describe('React hooks DevTools integration', () => {
66 expect(stateHook.isStateEditable).toBe(true);
67
68 if (__DEV__) {
67 - overrideHookState(fiber, stateHook.id, [], 10);
69 + act(() => overrideHookState(fiber, stateHook.id, [], 10));
70 expect(renderer.toJSON()).toEqual({
71 type: 'div',
72 props: {},
@@ -116,7 +118,7 @@ describe('React hooks DevTools integration', () => {
118 expect(reducerHook.isStateEditable).toBe(true);
119
120 if (__DEV__) {
119 - overrideHookState(fiber, reducerHook.id, ['foo'], 'def');
121 + act(() => overrideHookState(fiber, reducerHook.id, ['foo'], 'def'));
122 expect(renderer.toJSON()).toEqual({
123 type: 'div',
124 props: {},
@@ -164,13 +166,12 @@ describe('React hooks DevTools integration', () => {
166 expect(stateHook.isStateEditable).toBe(true);
167
168 if (__DEV__) {
167 - overrideHookState(fiber, stateHook.id, ['count'], 10);
169 + act(() => overrideHookState(fiber, stateHook.id, ['count'], 10));
170 expect(renderer.toJSON()).toEqual({
171 type: 'div',
172 props: {},
173 children: ['count:', '10'],
174 });
173 -
175 act(() => setStateFn(state => ({count: state.count + 1})));
176 expect(renderer.toJSON()).toEqual({
177 type: 'div',
@@ -233,7 +234,8 @@ describe('React hooks DevTools integration', () => {
234 }
235 });
236
236 - it('should support overriding suspense in concurrent mode', () => {
237 + // @gate __DEV__
238 + it('should support overriding suspense in concurrent mode', async () => {
239 if (__DEV__) {
240 // Lock the first render
241 setSuspenseHandler(() => true);
@@ -243,13 +245,15 @@ describe('React hooks DevTools integration', () => {
245 return 'Done';
246 }
247
246 - const renderer = ReactTestRenderer.create(
247 - <div>
248 - <React.Suspense fallback={'Loading'}>
249 - <MyComponent />
250 - </React.Suspense>
251 - </div>,
252 - {unstable_isConcurrent: true},
248 + const renderer = await act(() =>
249 + ReactTestRenderer.create(
250 + <div>
251 + <React.Suspense fallback={'Loading'}>
252 + <MyComponent />
253 + </React.Suspense>
254 + </div>,
255 + {unstable_isConcurrent: true},
256 + ),
257 );
258
259 expect(Scheduler).toFlushAndYield([]);
packages/react-devtools-scheduling-profiler/src/import-worker/__tests__/preprocessData-test.internal.js
+7 -5
@@ -17,8 +17,6 @@ import {
17 } from '../../constants';
18 import REACT_VERSION from 'shared/ReactVersion';
19
20 -global.IS_REACT_ACT_ENVIRONMENT = true;
21 -
20 describe('getLanesFromTransportDecimalBitmask', () => {
21 it('should return array of lane numbers from bitmask string', () => {
22 expect(getLanesFromTransportDecimalBitmask('1')).toEqual([0]);
@@ -210,6 +208,8 @@ describe('preprocessData', () => {
208 tid = 0;
209 pid = 0;
210 startTime = 0;
211 +
212 + global.IS_REACT_ACT_ENVIRONMENT = true;
213 });
214
215 afterEach(() => {
@@ -1251,7 +1251,7 @@ describe('preprocessData', () => {
1251
1252 testMarks.push(...createUserTimingData(clearedMarks));
1253
1254 - const data = await preprocessData(testMarks);
1254 + const data = await act(() => preprocessData(testMarks));
1255 expect(data.suspenseEvents).toHaveLength(1);
1256 expect(data.suspenseEvents[0].promiseName).toBe('Testing displayName');
1257 }
@@ -1367,6 +1367,8 @@ describe('preprocessData', () => {
1367
1368 const root = ReactDOM.createRoot(document.createElement('div'));
1369
1370 + // Temporarily turn off the act environment, since we're intentionally using Scheduler instead.
1371 + global.IS_REACT_ACT_ENVIRONMENT = false;
1372 React.startTransition(() => {
1373 // Start rendering an async update (but don't finish).
1374 root.render(
@@ -1837,7 +1839,7 @@ describe('preprocessData', () => {
1839
1840 testMarks.push(...createUserTimingData(clearedMarks));
1841
1840 - const data = await preprocessData(testMarks);
1842 + const data = await act(() => preprocessData(testMarks));
1843 expect(data.suspenseEvents).toHaveLength(1);
1844 expect(data.suspenseEvents[0].warning).toMatchInlineSnapshot(
1845 `"A component suspended during an update which caused a fallback to be shown. Consider using the Transition API to avoid hiding components after they've been mounted."`,
@@ -1895,7 +1897,7 @@ describe('preprocessData', () => {
1897
1898 testMarks.push(...createUserTimingData(clearedMarks));
1899
1898 - const data = await preprocessData(testMarks);
1900 + const data = await act(() => preprocessData(testMarks));
1901 expect(data.suspenseEvents).toHaveLength(1);
1902 expect(data.suspenseEvents[0].warning).toBe(null);
1903 }
packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js
+29 -9
@@ -32,18 +32,31 @@ describe('ReactTestUtils.act()', () => {
32 let concurrentRoot = null;
33 const renderConcurrent = (el, dom) => {
34 concurrentRoot = ReactDOM.createRoot(dom);
35 - concurrentRoot.render(el);
35 + if (__DEV__) {
36 + act(() => concurrentRoot.render(el));
37 + } else {
38 + concurrentRoot.render(el);
39 + }
40 };
41
42 const unmountConcurrent = _dom => {
39 - if (concurrentRoot !== null) {
40 - concurrentRoot.unmount();
41 - concurrentRoot = null;
43 + if (__DEV__) {
44 + act(() => {
45 + if (concurrentRoot !== null) {
46 + concurrentRoot.unmount();
47 + concurrentRoot = null;
48 + }
49 + });
50 + } else {
51 + if (concurrentRoot !== null) {
52 + concurrentRoot.unmount();
53 + concurrentRoot = null;
54 + }
55 }
56 };
57
58 const rerenderConcurrent = el => {
46 - concurrentRoot.render(el);
59 + act(() => concurrentRoot.render(el));
60 };
61
62 runActTests(
@@ -98,22 +111,29 @@ describe('ReactTestUtils.act()', () => {
111 ]);
112 });
113
114 + // @gate __DEV__
115 it('does not warn in concurrent mode', () => {
116 const root = ReactDOM.createRoot(document.createElement('div'));
103 - root.render(<App />);
117 + act(() => root.render(<App />));
118 Scheduler.unstable_flushAll();
119 });
120
121 it('warns in concurrent mode if root is strict', () => {
122 + // TODO: We don't need this error anymore in concurrent mode because
123 + // effects can only be scheduled as the result of an update, and we now
124 + // enforce all updates must be wrapped with act, not just hook updates.
125 expect(() => {
126 const root = ReactDOM.createRoot(document.createElement('div'), {
127 unstable_strictMode: true,
128 });
129 root.render(<App />);
113 - Scheduler.unstable_flushAll();
114 - }).toErrorDev([
130 + }).toErrorDev(
131 + 'An update to Root inside a test was not wrapped in act(...)',
132 + {withoutStack: true},
133 + );
134 + expect(() => Scheduler.unstable_flushAll()).toErrorDev(
135 'An update to App ran an effect, but was not wrapped in act(...)',
116 - ]);
136 + );
137 });
138 });
139 });
packages/react-reconciler/src/ReactFiberAct.new.js
+29 -25
@@ -12,11 +12,32 @@ import type {Fiber} from './ReactFiber.new';
12 import ReactSharedInternals from 'shared/ReactSharedInternals';
13
14 import {warnsIfNotActing} from './ReactFiberHostConfig';
15 -import {ConcurrentMode} from './ReactTypeOfMode';
15
16 const {ReactCurrentActQueue} = ReactSharedInternals;
17
19 -export function isActEnvironment(fiber: Fiber) {
18 +export function isLegacyActEnvironment(fiber: Fiber) {
19 + if (__DEV__) {
20 + // Legacy mode. We preserve the behavior of React 17's act. It assumes an
21 + // act environment whenever `jest` is defined, but you can still turn off
22 + // spurious warnings by setting IS_REACT_ACT_ENVIRONMENT explicitly
23 + // to false.
24 +
25 + const isReactActEnvironmentGlobal =
26 + // $FlowExpectedError – Flow doesn't know about IS_REACT_ACT_ENVIRONMENT global
27 + typeof IS_REACT_ACT_ENVIRONMENT !== 'undefined'
28 + ? IS_REACT_ACT_ENVIRONMENT
29 + : undefined;
30 +
31 + // $FlowExpectedError - Flow doesn't know about jest
32 + const jestIsDefined = typeof jest !== 'undefined';
33 + return (
34 + warnsIfNotActing && jestIsDefined && isReactActEnvironmentGlobal !== false
35 + );
36 + }
37 + return false;
38 +}
39 +
40 +export function isConcurrentActEnvironment() {
41 if (__DEV__) {
42 const isReactActEnvironmentGlobal =
43 // $FlowExpectedError – Flow doesn't know about IS_REACT_ACT_ENVIRONMENT global
@@ -24,31 +45,14 @@ export function isActEnvironment(fiber: Fiber) {
45 ? IS_REACT_ACT_ENVIRONMENT
46 : undefined;
47
27 - if (fiber.mode & ConcurrentMode) {
28 - if (
29 - !isReactActEnvironmentGlobal &&
30 - ReactCurrentActQueue.current !== null
31 - ) {
32 - // TODO: Include link to relevant documentation page.
33 - console.error(
34 - 'The current testing environment is not configured to support ' +
35 - 'act(...)',
36 - );
37 - }
38 - return isReactActEnvironmentGlobal;
39 - } else {
40 - // Legacy mode. We preserve the behavior of React 17's act. It assumes an
41 - // act environment whenever `jest` is defined, but you can still turn off
42 - // spurious warnings by setting IS_REACT_ACT_ENVIRONMENT explicitly
43 - // to false.
44 - // $FlowExpectedError - Flow doesn't know about jest
45 - const jestIsDefined = typeof jest !== 'undefined';
46 - return (
47 - warnsIfNotActing &&
48 - jestIsDefined &&
49 - isReactActEnvironmentGlobal !== false
48 + if (!isReactActEnvironmentGlobal && ReactCurrentActQueue.current !== null) {
49 + // TODO: Include link to relevant documentation page.
50 + console.error(
51 + 'The current testing environment is not configured to support ' +
52 + 'act(...)',
53 );
54 }
55 + return isReactActEnvironmentGlobal;
56 }
57 return false;
58 }
packages/react-reconciler/src/ReactFiberAct.old.js
+29 -25
@@ -12,11 +12,32 @@ import type {Fiber} from './ReactFiber.old';
12 import ReactSharedInternals from 'shared/ReactSharedInternals';
13
14 import {warnsIfNotActing} from './ReactFiberHostConfig';
15 -import {ConcurrentMode} from './ReactTypeOfMode';
15
16 const {ReactCurrentActQueue} = ReactSharedInternals;
17
19 -export function isActEnvironment(fiber: Fiber) {
18 +export function isLegacyActEnvironment(fiber: Fiber) {
19 + if (__DEV__) {
20 + // Legacy mode. We preserve the behavior of React 17's act. It assumes an
21 + // act environment whenever `jest` is defined, but you can still turn off
22 + // spurious warnings by setting IS_REACT_ACT_ENVIRONMENT explicitly
23 + // to false.
24 +
25 + const isReactActEnvironmentGlobal =
26 + // $FlowExpectedError – Flow doesn't know about IS_REACT_ACT_ENVIRONMENT global
27 + typeof IS_REACT_ACT_ENVIRONMENT !== 'undefined'
28 + ? IS_REACT_ACT_ENVIRONMENT
29 + : undefined;
30 +
31 + // $FlowExpectedError - Flow doesn't know about jest
32 + const jestIsDefined = typeof jest !== 'undefined';
33 + return (
34 + warnsIfNotActing && jestIsDefined && isReactActEnvironmentGlobal !== false
35 + );
36 + }
37 + return false;
38 +}
39 +
40 +export function isConcurrentActEnvironment() {
41 if (__DEV__) {
42 const isReactActEnvironmentGlobal =
43 // $FlowExpectedError – Flow doesn't know about IS_REACT_ACT_ENVIRONMENT global
@@ -24,31 +45,14 @@ export function isActEnvironment(fiber: Fiber) {
45 ? IS_REACT_ACT_ENVIRONMENT
46 : undefined;
47
27 - if (fiber.mode & ConcurrentMode) {
28 - if (
29 - !isReactActEnvironmentGlobal &&
30 - ReactCurrentActQueue.current !== null
31 - ) {
32 - // TODO: Include link to relevant documentation page.
33 - console.error(
34 - 'The current testing environment is not configured to support ' +
35 - 'act(...)',
36 - );
37 - }
38 - return isReactActEnvironmentGlobal;
39 - } else {
40 - // Legacy mode. We preserve the behavior of React 17's act. It assumes an
41 - // act environment whenever `jest` is defined, but you can still turn off
42 - // spurious warnings by setting IS_REACT_ACT_ENVIRONMENT explicitly
43 - // to false.
44 - // $FlowExpectedError - Flow doesn't know about jest
45 - const jestIsDefined = typeof jest !== 'undefined';
46 - return (
47 - warnsIfNotActing &&
48 - jestIsDefined &&
49 - isReactActEnvironmentGlobal !== false
48 + if (!isReactActEnvironmentGlobal && ReactCurrentActQueue.current !== null) {
49 + // TODO: Include link to relevant documentation page.
50 + console.error(
51 + 'The current testing environment is not configured to support ' +
52 + 'act(...)',
53 );
54 }
55 + return isReactActEnvironmentGlobal;
56 }
57 return false;
58 }
packages/react-reconciler/src/ReactFiberHooks.new.js
+2 -19
@@ -83,7 +83,6 @@ import {
83 requestUpdateLane,
84 requestEventTime,
85 warnIfNotCurrentlyActingEffectsInDEV,
86 - warnIfNotCurrentlyActingUpdatesInDev,
86 markSkippedUpdateLanes,
87 isInterleavedUpdate,
88 } from './ReactFiberWorkLoop.new';
@@ -118,7 +117,6 @@ import {
117 } from './ReactUpdateQueue.new';
118 import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.new';
119 import {warnOnSubscriptionInsideStartTransition} from 'shared/ReactFeatureFlags';
121 -import {isActEnvironment} from './ReactFiberAct.new';
120
121 const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
122
@@ -1679,9 +1677,7 @@ function mountEffect(
1677 deps: Array<mixed> | void | null,
1678 ): void {
1679 if (__DEV__) {
1682 - if (isActEnvironment(currentlyRenderingFiber)) {
1683 - warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber);
1684 - }
1680 + warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber);
1681 }
1682 if (
1683 __DEV__ &&
@@ -1709,9 +1705,7 @@ function updateEffect(
1705 deps: Array<mixed> | void | null,
1706 ): void {
1707 if (__DEV__) {
1712 - if (isActEnvironment(currentlyRenderingFiber)) {
1713 - warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber);
1714 - }
1708 + warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber);
1709 }
1710 return updateEffectImpl(PassiveEffect, HookPassive, create, deps);
1711 }
@@ -2196,12 +2190,6 @@ function dispatchReducerAction<S, A>(
2190 enqueueRenderPhaseUpdate(queue, update);
2191 } else {
2192 enqueueUpdate(fiber, queue, update, lane);
2199 -
2200 - if (__DEV__) {
2201 - if (isActEnvironment(fiber)) {
2202 - warnIfNotCurrentlyActingUpdatesInDev(fiber);
2203 - }
2204 - }
2193 const eventTime = requestEventTime();
2194 const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
2195 if (root !== null) {
@@ -2282,11 +2270,6 @@ function dispatchSetState<S, A>(
2270 }
2271 }
2272 }
2285 - if (__DEV__) {
2286 - if (isActEnvironment(fiber)) {
2287 - warnIfNotCurrentlyActingUpdatesInDev(fiber);
2288 - }
2289 - }
2273 const eventTime = requestEventTime();
2274 const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
2275 if (root !== null) {
packages/react-reconciler/src/ReactFiberHooks.old.js
+2 -19
@@ -83,7 +83,6 @@ import {
83 requestUpdateLane,
84 requestEventTime,
85 warnIfNotCurrentlyActingEffectsInDEV,
86 - warnIfNotCurrentlyActingUpdatesInDev,
86 markSkippedUpdateLanes,
87 isInterleavedUpdate,
88 } from './ReactFiberWorkLoop.old';
@@ -118,7 +117,6 @@ import {
117 } from './ReactUpdateQueue.old';
118 import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.old';
119 import {warnOnSubscriptionInsideStartTransition} from 'shared/ReactFeatureFlags';
121 -import {isActEnvironment} from './ReactFiberAct.old';
120
121 const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
122
@@ -1679,9 +1677,7 @@ function mountEffect(
1677 deps: Array<mixed> | void | null,
1678 ): void {
1679 if (__DEV__) {
1682 - if (isActEnvironment(currentlyRenderingFiber)) {
1683 - warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber);
1684 - }
1680 + warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber);
1681 }
1682 if (
1683 __DEV__ &&
@@ -1709,9 +1705,7 @@ function updateEffect(
1705 deps: Array<mixed> | void | null,
1706 ): void {
1707 if (__DEV__) {
1712 - if (isActEnvironment(currentlyRenderingFiber)) {
1713 - warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber);
1714 - }
1708 + warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber);
1709 }
1710 return updateEffectImpl(PassiveEffect, HookPassive, create, deps);
1711 }
@@ -2196,12 +2190,6 @@ function dispatchReducerAction<S, A>(
2190 enqueueRenderPhaseUpdate(queue, update);
2191 } else {
2192 enqueueUpdate(fiber, queue, update, lane);
2199 -
2200 - if (__DEV__) {
2201 - if (isActEnvironment(fiber)) {
2202 - warnIfNotCurrentlyActingUpdatesInDev(fiber);
2203 - }
2204 - }
2193 const eventTime = requestEventTime();
2194 const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
2195 if (root !== null) {
@@ -2282,11 +2270,6 @@ function dispatchSetState<S, A>(
2270 }
2271 }
2272 }
2285 - if (__DEV__) {
2286 - if (isActEnvironment(fiber)) {
2287 - warnIfNotCurrentlyActingUpdatesInDev(fiber);
2288 - }
2289 - }
2273 const eventTime = requestEventTime();
2274 const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
2275 if (root !== null) {
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+65 -6
@@ -236,6 +236,10 @@ import {
236 } from './ReactFiberDevToolsHook.new';
237 import {onCommitRoot as onCommitRootTestSelector} from './ReactTestSelectors';
238 import {releaseCache} from './ReactFiberCacheComponent.new';
239 +import {
240 + isLegacyActEnvironment,
241 + isConcurrentActEnvironment,
242 +} from './ReactFiberAct.new';
243
244 const ceil = Math.ceil;
245
@@ -493,6 +497,8 @@ export function scheduleUpdateOnFiber(
497 }
498 }
499
500 + warnIfUpdatesNotWrappedWithActDEV(fiber);
501 +
502 if (enableProfilerTimer && enableProfilerNestedUpdateScheduledHook) {
503 if (
504 (executionContext & CommitContext) !== NoContext &&
@@ -2402,6 +2408,8 @@ export function pingSuspendedRoot(
2408 const eventTime = requestEventTime();
2409 markRootPinged(root, pingedLanes, eventTime);
2410
2411 + warnIfSuspenseResolutionNotWrappedWithActDEV(root);
2412 +
2413 if (
2414 workInProgressRoot === root &&
2415 isSubsetOfLanes(workInProgressRootRenderLanes, pingedLanes)
@@ -2854,7 +2862,12 @@ function shouldForceFlushFallbacksInDEV() {
2862
2863 export function warnIfNotCurrentlyActingEffectsInDEV(fiber: Fiber): void {
2864 if (__DEV__) {
2865 + const isActEnvironment =
2866 + fiber.mode & ConcurrentMode
2867 + ? isConcurrentActEnvironment()
2868 + : isLegacyActEnvironment(fiber);
2869 if (
2870 + isActEnvironment &&
2871 (fiber.mode & StrictLegacyMode) !== NoMode &&
2872 ReactCurrentActQueue.current === null
2873 ) {
@@ -2875,12 +2888,36 @@ export function warnIfNotCurrentlyActingEffectsInDEV(fiber: Fiber): void {
2888 }
2889 }
2890
2878 -function warnIfNotCurrentlyActingUpdatesInDEV(fiber: Fiber): void {
2891 +function warnIfUpdatesNotWrappedWithActDEV(fiber: Fiber): void {
2892 if (__DEV__) {
2880 - if (
2881 - executionContext === NoContext &&
2882 - ReactCurrentActQueue.current === null
2883 - ) {
2893 + if (fiber.mode & ConcurrentMode) {
2894 + if (!isConcurrentActEnvironment()) {
2895 + // Not in an act environment. No need to warn.
2896 + return;
2897 + }
2898 + } else {
2899 + // Legacy mode has additional cases where we suppress a warning.
2900 + if (!isLegacyActEnvironment(fiber)) {
2901 + // Not in an act environment. No need to warn.
2902 + return;
2903 + }
2904 + if (executionContext !== NoContext) {
2905 + // Legacy mode doesn't warn if the update is batched, i.e.
2906 + // batchedUpdates or flushSync.
2907 + return;
2908 + }
2909 + if (
2910 + fiber.tag !== FunctionComponent &&
2911 + fiber.tag !== ForwardRef &&
2912 + fiber.tag !== SimpleMemoComponent
2913 + ) {
2914 + // For backwards compatibility with pre-hooks code, legacy mode only
2915 + // warns for updates that originate from a hook.
2916 + return;
2917 + }
2918 + }
2919 +
2920 + if (ReactCurrentActQueue.current === null) {
2921 const previousFiber = ReactCurrentFiberCurrent;
2922 try {
2923 setCurrentDebugFiberInDEV(fiber);
@@ -2908,4 +2945,26 @@ function warnIfNotCurrentlyActingUpdatesInDEV(fiber: Fiber): void {
2945 }
2946 }
2947
2911 -export const warnIfNotCurrentlyActingUpdatesInDev = warnIfNotCurrentlyActingUpdatesInDEV;
2948 +function warnIfSuspenseResolutionNotWrappedWithActDEV(root: FiberRoot): void {
2949 + if (__DEV__) {
2950 + if (
2951 + root.tag !== LegacyRoot &&
2952 + isConcurrentActEnvironment() &&
2953 + ReactCurrentActQueue.current === null
2954 + ) {
2955 + console.error(
2956 + 'A suspended resource finished loading inside a test, but the event ' +
2957 + 'was not wrapped in act(...).\n\n' +
2958 + 'When testing, code that resolves suspended data should be wrapped ' +
2959 + 'into act(...):\n\n' +
2960 + 'act(() => {\n' +
2961 + ' /* finish loading suspended data */\n' +
2962 + '});\n' +
2963 + '/* assert on the output */\n\n' +
2964 + "This ensures that you're testing the behavior the user would see " +
2965 + 'in the browser.' +
2966 + ' Learn more at https://reactjs.org/link/wrap-tests-with-act',
2967 + );
2968 + }
2969 + }
2970 +}
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+65 -6
@@ -236,6 +236,10 @@ import {
236 } from './ReactFiberDevToolsHook.old';
237 import {onCommitRoot as onCommitRootTestSelector} from './ReactTestSelectors';
238 import {releaseCache} from './ReactFiberCacheComponent.old';
239 +import {
240 + isLegacyActEnvironment,
241 + isConcurrentActEnvironment,
242 +} from './ReactFiberAct.old';
243
244 const ceil = Math.ceil;
245
@@ -493,6 +497,8 @@ export function scheduleUpdateOnFiber(
497 }
498 }
499
500 + warnIfUpdatesNotWrappedWithActDEV(fiber);
501 +
502 if (enableProfilerTimer && enableProfilerNestedUpdateScheduledHook) {
503 if (
504 (executionContext & CommitContext) !== NoContext &&
@@ -2402,6 +2408,8 @@ export function pingSuspendedRoot(
2408 const eventTime = requestEventTime();
2409 markRootPinged(root, pingedLanes, eventTime);
2410
2411 + warnIfSuspenseResolutionNotWrappedWithActDEV(root);
2412 +
2413 if (
2414 workInProgressRoot === root &&
2415 isSubsetOfLanes(workInProgressRootRenderLanes, pingedLanes)
@@ -2854,7 +2862,12 @@ function shouldForceFlushFallbacksInDEV() {
2862
2863 export function warnIfNotCurrentlyActingEffectsInDEV(fiber: Fiber): void {
2864 if (__DEV__) {
2865 + const isActEnvironment =
2866 + fiber.mode & ConcurrentMode
2867 + ? isConcurrentActEnvironment()
2868 + : isLegacyActEnvironment(fiber);
2869 if (
2870 + isActEnvironment &&
2871 (fiber.mode & StrictLegacyMode) !== NoMode &&
2872 ReactCurrentActQueue.current === null
2873 ) {
@@ -2875,12 +2888,36 @@ export function warnIfNotCurrentlyActingEffectsInDEV(fiber: Fiber): void {
2888 }
2889 }
2890
2878 -function warnIfNotCurrentlyActingUpdatesInDEV(fiber: Fiber): void {
2891 +function warnIfUpdatesNotWrappedWithActDEV(fiber: Fiber): void {
2892 if (__DEV__) {
2880 - if (
2881 - executionContext === NoContext &&
2882 - ReactCurrentActQueue.current === null
2883 - ) {
2893 + if (fiber.mode & ConcurrentMode) {
2894 + if (!isConcurrentActEnvironment()) {
2895 + // Not in an act environment. No need to warn.
2896 + return;
2897 + }
2898 + } else {
2899 + // Legacy mode has additional cases where we suppress a warning.
2900 + if (!isLegacyActEnvironment(fiber)) {
2901 + // Not in an act environment. No need to warn.
2902 + return;
2903 + }
2904 + if (executionContext !== NoContext) {
2905 + // Legacy mode doesn't warn if the update is batched, i.e.
2906 + // batchedUpdates or flushSync.
2907 + return;
2908 + }
2909 + if (
2910 + fiber.tag !== FunctionComponent &&
2911 + fiber.tag !== ForwardRef &&
2912 + fiber.tag !== SimpleMemoComponent
2913 + ) {
2914 + // For backwards compatibility with pre-hooks code, legacy mode only
2915 + // warns for updates that originate from a hook.
2916 + return;
2917 + }
2918 + }
2919 +
2920 + if (ReactCurrentActQueue.current === null) {
2921 const previousFiber = ReactCurrentFiberCurrent;
2922 try {
2923 setCurrentDebugFiberInDEV(fiber);
@@ -2908,4 +2945,26 @@ function warnIfNotCurrentlyActingUpdatesInDEV(fiber: Fiber): void {
2945 }
2946 }
2947
2911 -export const warnIfNotCurrentlyActingUpdatesInDev = warnIfNotCurrentlyActingUpdatesInDEV;
2948 +function warnIfSuspenseResolutionNotWrappedWithActDEV(root: FiberRoot): void {
2949 + if (__DEV__) {
2950 + if (
2951 + root.tag !== LegacyRoot &&
2952 + isConcurrentActEnvironment() &&
2953 + ReactCurrentActQueue.current === null
2954 + ) {
2955 + console.error(
2956 + 'A suspended resource finished loading inside a test, but the event ' +
2957 + 'was not wrapped in act(...).\n\n' +
2958 + 'When testing, code that resolves suspended data should be wrapped ' +
2959 + 'into act(...):\n\n' +
2960 + 'act(() => {\n' +
2961 + ' /* finish loading suspended data */\n' +
2962 + '});\n' +
2963 + '/* assert on the output */\n\n' +
2964 + "This ensures that you're testing the behavior the user would see " +
2965 + 'in the browser.' +
2966 + ' Learn more at https://reactjs.org/link/wrap-tests-with-act',
2967 + );
2968 + }
2969 + }
2970 +}
packages/react-reconciler/src/__tests__/DebugTracing-test.internal.js
+92 -111
@@ -56,21 +56,17 @@ describe('DebugTracing', () => {
56 expect(logs).toEqual([]);
57 });
58
59 + // @gate build === 'development'
60 // @gate experimental || www
61 it('should not log anything for concurrent render without suspends or state updates', () => {
61 - ReactTestRenderer.create(
62 - <React.unstable_DebugTracingMode>
63 - <div />
64 - </React.unstable_DebugTracingMode>,
65 - {unstable_isConcurrent: true},
62 + ReactTestRenderer.act(() =>
63 + ReactTestRenderer.create(
64 + <React.unstable_DebugTracingMode>
65 + <div />
66 + </React.unstable_DebugTracingMode>,
67 + {unstable_isConcurrent: true},
68 + ),
69 );
67 -
68 - expect(logs).toEqual([]);
69 -
70 - logs.splice(0);
71 -
72 - expect(Scheduler).toFlushUntilNextPaint([]);
73 -
70 expect(logs).toEqual([]);
71 });
72
@@ -81,12 +77,14 @@ describe('DebugTracing', () => {
77 throw fakeSuspensePromise;
78 }
79
84 - ReactTestRenderer.create(
85 - <React.unstable_DebugTracingMode>
86 - <React.Suspense fallback={null}>
87 - <Example />
88 - </React.Suspense>
89 - </React.unstable_DebugTracingMode>,
80 + ReactTestRenderer.act(() =>
81 + ReactTestRenderer.create(
82 + <React.unstable_DebugTracingMode>
83 + <React.Suspense fallback={null}>
84 + <Example />
85 + </React.Suspense>
86 + </React.unstable_DebugTracingMode>,
87 + ),
88 );
89
90 expect(logs).toEqual([
@@ -142,26 +140,33 @@ describe('DebugTracing', () => {
140
141 // @gate experimental && build === 'development' && enableDebugTracing
142 it('should log concurrent render with suspense', async () => {
145 - const fakeSuspensePromise = Promise.resolve(true);
143 + let isResolved = false;
144 + let resolveFakeSuspensePromise;
145 + const fakeSuspensePromise = new Promise(resolve => {
146 + resolveFakeSuspensePromise = () => {
147 + resolve();
148 + isResolved = true;
149 + };
150 + });
151 +
152 function Example() {
147 - throw fakeSuspensePromise;
153 + if (!isResolved) {
154 + throw fakeSuspensePromise;
155 + }
156 + return null;
157 }
158
150 - ReactTestRenderer.create(
151 - <React.unstable_DebugTracingMode>
152 - <React.Suspense fallback={null}>
153 - <Example />
154 - </React.Suspense>
155 - </React.unstable_DebugTracingMode>,
156 - {unstable_isConcurrent: true},
159 + ReactTestRenderer.act(() =>
160 + ReactTestRenderer.create(
161 + <React.unstable_DebugTracingMode>
162 + <React.Suspense fallback={null}>
163 + <Example />
164 + </React.Suspense>
165 + </React.unstable_DebugTracingMode>,
166 + {unstable_isConcurrent: true},
167 + ),
168 );
169
159 - expect(logs).toEqual([]);
160 -
161 - logs.splice(0);
162 -
163 - expect(Scheduler).toFlushUntilNextPaint([]);
164 -
170 expect(logs).toEqual([
171 `group: ⚛️ render (${DEFAULT_LANE_STRING})`,
172 'log: ⚛️ Example suspended',
@@ -170,7 +175,7 @@ describe('DebugTracing', () => {
175
176 logs.splice(0);
177
173 - await fakeSuspensePromise;
178 + await ReactTestRenderer.act(async () => await resolveFakeSuspensePromise());
179 expect(logs).toEqual(['log: ⚛️ Example resolved']);
180 });
181
@@ -186,34 +191,23 @@ describe('DebugTracing', () => {
191 return children;
192 }
193
189 - ReactTestRenderer.create(
190 - <React.unstable_DebugTracingMode>
191 - <Wrapper>
192 - <React.Suspense fallback={null} unstable_expectedLoadTime={1}>
193 - <Example />
194 - </React.Suspense>
195 - </Wrapper>
196 - </React.unstable_DebugTracingMode>,
197 - {unstable_isConcurrent: true},
194 + ReactTestRenderer.act(() =>
195 + ReactTestRenderer.create(
196 + <React.unstable_DebugTracingMode>
197 + <Wrapper>
198 + <React.Suspense fallback={null} unstable_expectedLoadTime={1}>
199 + <Example />
200 + </React.Suspense>
201 + </Wrapper>
202 + </React.unstable_DebugTracingMode>,
203 + {unstable_isConcurrent: true},
204 + ),
205 );
206
200 - expect(logs).toEqual([]);
201 -
202 - logs.splice(0);
203 -
204 - expect(Scheduler).toFlushUntilNextPaint([]);
205 -
207 expect(logs).toEqual([
208 `group: ⚛️ render (${DEFAULT_LANE_STRING})`,
209 'log: <Wrapper/>',
210 `groupEnd: ⚛️ render (${DEFAULT_LANE_STRING})`,
210 - ]);
211 -
212 - logs.splice(0);
213 -
214 - expect(Scheduler).toFlushUntilNextPaint([]);
215 -
216 - expect(logs).toEqual([
211 `group: ⚛️ render (${RETRY_LANE_STRING})`,
212 'log: <Example/>',
213 `groupEnd: ⚛️ render (${RETRY_LANE_STRING})`,
@@ -232,19 +226,15 @@ describe('DebugTracing', () => {
226 }
227 }
228
235 - ReactTestRenderer.create(
236 - <React.unstable_DebugTracingMode>
237 - <Example />
238 - </React.unstable_DebugTracingMode>,
239 - {unstable_isConcurrent: true},
229 + ReactTestRenderer.act(() =>
230 + ReactTestRenderer.create(
231 + <React.unstable_DebugTracingMode>
232 + <Example />
233 + </React.unstable_DebugTracingMode>,
234 + {unstable_isConcurrent: true},
235 + ),
236 );
237
242 - expect(logs).toEqual([]);
243 -
244 - logs.splice(0);
245 -
246 - expect(Scheduler).toFlushUntilNextPaint([]);
247 -
238 expect(logs).toEqual([
239 `group: ⚛️ commit (${DEFAULT_LANE_STRING})`,
240 `group: ⚛️ layout effects (${DEFAULT_LANE_STRING})`,
@@ -266,19 +256,15 @@ describe('DebugTracing', () => {
256 }
257 }
258
269 - ReactTestRenderer.create(
270 - <React.unstable_DebugTracingMode>
271 - <Example />
272 - </React.unstable_DebugTracingMode>,
273 - {unstable_isConcurrent: true},
274 - );
275 -
276 - expect(logs).toEqual([]);
277 -
278 - logs.splice(0);
279 -
259 expect(() => {
281 - expect(Scheduler).toFlushUntilNextPaint([]);
260 + ReactTestRenderer.act(() =>
261 + ReactTestRenderer.create(
262 + <React.unstable_DebugTracingMode>
263 + <Example />
264 + </React.unstable_DebugTracingMode>,
265 + {unstable_isConcurrent: true},
266 + ),
267 + );
268 }).toErrorDev('Cannot update during an existing state transition');
269
270 expect(logs).toEqual([
@@ -298,19 +284,15 @@ describe('DebugTracing', () => {
284 return didMount;
285 }
286
301 - ReactTestRenderer.create(
302 - <React.unstable_DebugTracingMode>
303 - <Example />
304 - </React.unstable_DebugTracingMode>,
305 - {unstable_isConcurrent: true},
287 + ReactTestRenderer.act(() =>
288 + ReactTestRenderer.create(
289 + <React.unstable_DebugTracingMode>
290 + <Example />
291 + </React.unstable_DebugTracingMode>,
292 + {unstable_isConcurrent: true},
293 + ),
294 );
295
308 - expect(logs).toEqual([]);
309 -
310 - logs.splice(0);
311 -
312 - expect(Scheduler).toFlushUntilNextPaint([]);
313 -
296 expect(logs).toEqual([
297 `group: ⚛️ commit (${DEFAULT_LANE_STRING})`,
298 `group: ⚛️ layout effects (${DEFAULT_LANE_STRING})`,
@@ -378,19 +360,15 @@ describe('DebugTracing', () => {
360 return null;
361 }
362
381 - ReactTestRenderer.create(
382 - <React.unstable_DebugTracingMode>
383 - <Example />
384 - </React.unstable_DebugTracingMode>,
385 - {unstable_isConcurrent: true},
363 + ReactTestRenderer.act(() =>
364 + ReactTestRenderer.create(
365 + <React.unstable_DebugTracingMode>
366 + <Example />
367 + </React.unstable_DebugTracingMode>,
368 + {unstable_isConcurrent: true},
369 + ),
370 );
371
388 - expect(logs).toEqual([]);
389 -
390 - logs.splice(0);
391 -
392 - expect(Scheduler).toFlushUntilNextPaint([]);
393 -
372 expect(logs).toEqual([
373 `group: ⚛️ render (${DEFAULT_LANE_STRING})`,
374 'log: Hello from user code',
@@ -398,6 +376,7 @@ describe('DebugTracing', () => {
376 ]);
377 });
378
379 + // @gate build === 'development'
380 // @gate experimental || www
381 it('should not log anything outside of a unstable_DebugTracingMode subtree', () => {
382 function ExampleThatCascades() {
@@ -417,16 +396,18 @@ describe('DebugTracing', () => {
396 return null;
397 }
398
420 - ReactTestRenderer.create(
421 - <React.Fragment>
422 - <ExampleThatCascades />
423 - <React.Suspense fallback={null}>
424 - <ExampleThatSuspends />
425 - </React.Suspense>
426 - <React.unstable_DebugTracingMode>
427 - <Example />
428 - </React.unstable_DebugTracingMode>
429 - </React.Fragment>,
399 + ReactTestRenderer.act(() =>
400 + ReactTestRenderer.create(
401 + <React.Fragment>
402 + <ExampleThatCascades />
403 + <React.Suspense fallback={null}>
404 + <ExampleThatSuspends />
405 + </React.Suspense>
406 + <React.unstable_DebugTracingMode>
407 + <Example />
408 + </React.unstable_DebugTracingMode>
409 + </React.Fragment>,
410 + ),
411 );
412
413 expect(logs).toEqual([]);
packages/react-reconciler/src/__tests__/ReactActWarnings-test.js
+234 -3
@@ -12,6 +12,10 @@ let Scheduler;
12 let ReactNoop;
13 let useState;
14 let act;
15 +let Suspense;
16 +let startTransition;
17 +let getCacheForType;
18 +let caches;
19
20 // These tests are mostly concerned with concurrent roots. The legacy root
21 // behavior is covered by other older test suites and is unchanged from
@@ -24,11 +28,110 @@ describe('act warnings', () => {
28 ReactNoop = require('react-noop-renderer');
29 act = React.unstable_act;
30 useState = React.useState;
31 + Suspense = React.Suspense;
32 + startTransition = React.startTransition;
33 + getCacheForType = React.unstable_getCacheForType;
34 + caches = [];
35 });
36
29 - function Text(props) {
30 - Scheduler.unstable_yieldValue(props.text);
31 - return props.text;
37 + function createTextCache() {
38 + const data = new Map();
39 + const version = caches.length + 1;
40 + const cache = {
41 + version,
42 + data,
43 + resolve(text) {
44 + const record = data.get(text);
45 + if (record === undefined) {
46 + const newRecord = {
47 + status: 'resolved',
48 + value: text,
49 + };
50 + data.set(text, newRecord);
51 + } else if (record.status === 'pending') {
52 + const thenable = record.value;
53 + record.status = 'resolved';
54 + record.value = text;
55 + thenable.pings.forEach(t => t());
56 + }
57 + },
58 + reject(text, error) {
59 + const record = data.get(text);
60 + if (record === undefined) {
61 + const newRecord = {
62 + status: 'rejected',
63 + value: error,
64 + };
65 + data.set(text, newRecord);
66 + } else if (record.status === 'pending') {
67 + const thenable = record.value;
68 + record.status = 'rejected';
69 + record.value = error;
70 + thenable.pings.forEach(t => t());
71 + }
72 + },
73 + };
74 + caches.push(cache);
75 + return cache;
76 + }
77 +
78 + function readText(text) {
79 + const textCache = getCacheForType(createTextCache);
80 + const record = textCache.data.get(text);
81 + if (record !== undefined) {
82 + switch (record.status) {
83 + case 'pending':
84 + Scheduler.unstable_yieldValue(`Suspend! [${text}]`);
85 + throw record.value;
86 + case 'rejected':
87 + Scheduler.unstable_yieldValue(`Error! [${text}]`);
88 + throw record.value;
89 + case 'resolved':
90 + return textCache.version;
91 + }
92 + } else {
93 + Scheduler.unstable_yieldValue(`Suspend! [${text}]`);
94 +
95 + const thenable = {
96 + pings: [],
97 + then(resolve) {
98 + if (newRecord.status === 'pending') {
99 + thenable.pings.push(resolve);
100 + } else {
101 + Promise.resolve().then(() => resolve(newRecord.value));
102 + }
103 + },
104 + };
105 +
106 + const newRecord = {
107 + status: 'pending',
108 + value: thenable,
109 + };
110 + textCache.data.set(text, newRecord);
111 +
112 + throw thenable;
113 + }
114 + }
115 +
116 + function Text({text}) {
117 + Scheduler.unstable_yieldValue(text);
118 + return text;
119 + }
120 +
121 + function AsyncText({text}) {
122 + readText(text);
123 + Scheduler.unstable_yieldValue(text);
124 + return text;
125 + }
126 +
127 + function resolveText(text) {
128 + if (caches.length === 0) {
129 + throw Error('Cache does not exist.');
130 + } else {
131 + // Resolve the most recently created cache. An older cache can by
132 + // resolved with `caches[index].resolve(text)`.
133 + caches[caches.length - 1].resolve(text);
134 + }
135 }
136
137 function withActEnvironment(value, scope) {
@@ -127,4 +230,132 @@ describe('act warnings', () => {
230 expect(root).toMatchRenderedOutput('1');
231 });
232 });
233 +
234 + test('warns if root update is not wrapped', () => {
235 + withActEnvironment(true, () => {
236 + const root = ReactNoop.createRoot();
237 + expect(() => root.render('Hi')).toErrorDev(
238 + // TODO: Better error message that doesn't make it look like "Root" is
239 + // the name of a custom component
240 + 'An update to Root inside a test was not wrapped in act(...)',
241 + {withoutStack: true},
242 + );
243 + });
244 + });
245 +
246 + // @gate __DEV__
247 + test('warns if class update is not wrapped', () => {
248 + let app;
249 + class App extends React.Component {
250 + state = {count: 0};
251 + render() {
252 + app = this;
253 + return <Text text={this.state.count} />;
254 + }
255 + }
256 +
257 + withActEnvironment(true, () => {
258 + const root = ReactNoop.createRoot();
259 + act(() => {
260 + root.render(<App />);
261 + });
262 + expect(() => app.setState({count: 1})).toErrorDev(
263 + 'An update to App inside a test was not wrapped in act(...)',
264 + );
265 + });
266 + });
267 +
268 + // @gate __DEV__
269 + test('warns even if update is synchronous', () => {
270 + let setState;
271 + function App() {
272 + const [state, _setState] = useState(0);
273 + setState = _setState;
274 + return <Text text={state} />;
275 + }
276 +
277 + withActEnvironment(true, () => {
278 + const root = ReactNoop.createRoot();
279 + act(() => root.render(<App />));
280 + expect(Scheduler).toHaveYielded([0]);
281 + expect(root).toMatchRenderedOutput('0');
282 +
283 + // Even though this update is synchronous, we should still fire a warning,
284 + // because it could have spawned additional asynchronous work
285 + expect(() => ReactNoop.flushSync(() => setState(1))).toErrorDev(
286 + 'An update to App inside a test was not wrapped in act(...)',
287 + );
288 +
289 + expect(Scheduler).toHaveYielded([1]);
290 + expect(root).toMatchRenderedOutput('1');
291 + });
292 + });
293 +
294 + // @gate __DEV__
295 + // @gate enableCache
296 + test('warns if Suspense retry is not wrapped', () => {
297 + function App() {
298 + return (
299 + <Suspense fallback={<Text text="Loading..." />}>
300 + <AsyncText text="Async" />
301 + </Suspense>
302 + );
303 + }
304 +
305 + withActEnvironment(true, () => {
306 + const root = ReactNoop.createRoot();
307 + act(() => {
308 + root.render(<App />);
309 + });
310 + expect(Scheduler).toHaveYielded(['Suspend! [Async]', 'Loading...']);
311 + expect(root).toMatchRenderedOutput('Loading...');
312 +
313 + // This is a retry, not a ping, because we already showed a fallback.
314 + expect(() =>
315 + resolveText('Async'),
316 + ).toErrorDev(
317 + 'A suspended resource finished loading inside a test, but the event ' +
318 + 'was not wrapped in act(...)',
319 + {withoutStack: true},
320 + );
321 + });
322 + });
323 +
324 + // @gate __DEV__
325 + // @gate enableCache
326 + test('warns if Suspense ping is not wrapped', () => {
327 + function App({showMore}) {
328 + return (
329 + <Suspense fallback={<Text text="Loading..." />}>
330 + {showMore ? <AsyncText text="Async" /> : <Text text="(empty)" />}
331 + </Suspense>
332 + );
333 + }
334 +
335 + withActEnvironment(true, () => {
336 + const root = ReactNoop.createRoot();
337 + act(() => {
338 + root.render(<App showMore={false} />);
339 + });
340 + expect(Scheduler).toHaveYielded(['(empty)']);
341 + expect(root).toMatchRenderedOutput('(empty)');
342 +
343 + act(() => {
344 + startTransition(() => {
345 + root.render(<App showMore={true} />);
346 + });
347 + });
348 + expect(Scheduler).toHaveYielded(['Suspend! [Async]', 'Loading...']);
349 + expect(root).toMatchRenderedOutput('(empty)');
350 +
351 + // This is a ping, not a retry, because no fallback is showing.
352 + expect(() =>
353 + resolveText('Async'),
354 + ).toErrorDev(
355 + 'A suspended resource finished loading inside a test, but the event ' +
356 + 'was not wrapped in act(...)',
357 + {withoutStack: true},
358 + );
359 + });
360 + });
361 });
packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js
+2
@@ -28,6 +28,8 @@ describe('ReactFiberHostContext', () => {
28 .DefaultEventPriority;
29 });
30
31 + global.IS_REACT_ACT_ENVIRONMENT = true;
32 +
33 // @gate __DEV__
34 it('works with null host context', async () => {
35 let creates = 0;
packages/react-reconciler/src/__tests__/ReactIsomorphicAct-test.js
+6
@@ -23,12 +23,17 @@ describe('isomorphic act()', () => {
23 act = React.unstable_act;
24 });
25
26 + beforeEach(() => {
27 + global.IS_REACT_ACT_ENVIRONMENT = true;
28 + });
29 +
30 // @gate __DEV__
31 test('bypasses queueMicrotask', async () => {
32 const root = ReactNoop.createRoot();
33
34 // First test what happens without wrapping in act. This update would
35 // normally be queued in a microtask.
36 + global.IS_REACT_ACT_ENVIRONMENT = false;
37 ReactNoop.unstable_runWithPriority(DiscreteEventPriority, () => {
38 root.render('A');
39 });
@@ -40,6 +45,7 @@ describe('isomorphic act()', () => {
45
46 // Now do the same thing but wrap the update with `act`. No
47 // `await` necessary.
48 + global.IS_REACT_ACT_ENVIRONMENT = true;
49 act(() => {
50 ReactNoop.unstable_runWithPriority(DiscreteEventPriority, () => {
51 root.render('B');