Allow async blocks in `to(Error|Warn)Dev` (#25338)
Sebastian Silbermann committed
Dec 1, 2022 at 11:26 UTC
3ba7add608ec8338cbcaea26d5935401853d2959
5 files changed
+113
-52
packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js
+33
-33
@@ -1777,7 +1777,7 @@ describe('ReactHooksWithNoopRenderer', () => {
1777
}, [props.count]);
1778
return <Text text={'Count: ' + count} />;
1779
}
1780
- expect(() =>
1780
+ expect(() => {
1781
act(() => {
1782
ReactNoop.render(<Counter count={0} />, () =>
1783
Scheduler.unstable_yieldValue('Sync effect'),
@@ -1787,8 +1787,8 @@ describe('ReactHooksWithNoopRenderer', () => {
1787
'Sync effect',
1788
]);
1789
expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]);
1790
- }),
1791
- ).toErrorDev('flushSync was called from inside a lifecycle method');
1790
+ });
1791
+ }).toErrorDev('flushSync was called from inside a lifecycle method');
1792
expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1793
});
1794
@@ -2648,32 +2648,32 @@ describe('ReactHooksWithNoopRenderer', () => {
2648
}
2649
2650
const root1 = ReactNoop.createRoot();
2651
- expect(() =>
2651
+ expect(() => {
2652
act(() => {
2653
root1.render(<App return={17} />);
2654
- }),
2655
- ).toErrorDev([
2654
+ });
2655
+ }).toErrorDev([
2656
'Warning: useEffect must not return anything besides a ' +
2657
'function, which is used for clean-up. You returned: 17',
2658
]);
2659
2660
const root2 = ReactNoop.createRoot();
2661
- expect(() =>
2661
+ expect(() => {
2662
act(() => {
2663
root2.render(<App return={null} />);
2664
- }),
2665
- ).toErrorDev([
2664
+ });
2665
+ }).toErrorDev([
2666
'Warning: useEffect must not return anything besides a ' +
2667
'function, which is used for clean-up. You returned null. If your ' +
2668
'effect does not require clean up, return undefined (or nothing).',
2669
]);
2670
2671
const root3 = ReactNoop.createRoot();
2672
- expect(() =>
2672
+ expect(() => {
2673
act(() => {
2674
root3.render(<App return={Promise.resolve()} />);
2675
- }),
2676
- ).toErrorDev([
2675
+ });
2676
+ }).toErrorDev([
2677
'Warning: useEffect must not return anything besides a ' +
2678
'function, which is used for clean-up.\n\n' +
2679
'It looks like you wrote useEffect(async () => ...) or returned a Promise.',
@@ -3052,32 +3052,32 @@ describe('ReactHooksWithNoopRenderer', () => {
3052
}
3053
3054
const root1 = ReactNoop.createRoot();
3055
- expect(() =>
3055
+ expect(() => {
3056
act(() => {
3057
root1.render(<App return={17} />);
3058
- }),
3059
- ).toErrorDev([
3058
+ });
3059
+ }).toErrorDev([
3060
'Warning: useInsertionEffect must not return anything besides a ' +
3061
'function, which is used for clean-up. You returned: 17',
3062
]);
3063
3064
const root2 = ReactNoop.createRoot();
3065
- expect(() =>
3065
+ expect(() => {
3066
act(() => {
3067
root2.render(<App return={null} />);
3068
- }),
3069
- ).toErrorDev([
3068
+ });
3069
+ }).toErrorDev([
3070
'Warning: useInsertionEffect must not return anything besides a ' +
3071
'function, which is used for clean-up. You returned null. If your ' +
3072
'effect does not require clean up, return undefined (or nothing).',
3073
]);
3074
3075
const root3 = ReactNoop.createRoot();
3076
- expect(() =>
3076
+ expect(() => {
3077
act(() => {
3078
root3.render(<App return={Promise.resolve()} />);
3079
- }),
3080
- ).toErrorDev([
3079
+ });
3080
+ }).toErrorDev([
3081
'Warning: useInsertionEffect must not return anything besides a ' +
3082
'function, which is used for clean-up.\n\n' +
3083
'It looks like you wrote useInsertionEffect(async () => ...) or returned a Promise.',
@@ -3104,11 +3104,11 @@ describe('ReactHooksWithNoopRenderer', () => {
3104
}
3105
3106
const root = ReactNoop.createRoot();
3107
- expect(() =>
3107
+ expect(() => {
3108
act(() => {
3109
root.render(<App />);
3110
- }),
3111
- ).toErrorDev(['Warning: useInsertionEffect must not schedule updates.']);
3110
+ });
3111
+ }).toErrorDev(['Warning: useInsertionEffect must not schedule updates.']);
3112
3113
expect(() => {
3114
act(() => {
@@ -3359,32 +3359,32 @@ describe('ReactHooksWithNoopRenderer', () => {
3359
}
3360
3361
const root1 = ReactNoop.createRoot();
3362
- expect(() =>
3362
+ expect(() => {
3363
act(() => {
3364
root1.render(<App return={17} />);
3365
- }),
3366
- ).toErrorDev([
3365
+ });
3366
+ }).toErrorDev([
3367
'Warning: useLayoutEffect must not return anything besides a ' +
3368
'function, which is used for clean-up. You returned: 17',
3369
]);
3370
3371
const root2 = ReactNoop.createRoot();
3372
- expect(() =>
3372
+ expect(() => {
3373
act(() => {
3374
root2.render(<App return={null} />);
3375
- }),
3376
- ).toErrorDev([
3375
+ });
3376
+ }).toErrorDev([
3377
'Warning: useLayoutEffect must not return anything besides a ' +
3378
'function, which is used for clean-up. You returned null. If your ' +
3379
'effect does not require clean up, return undefined (or nothing).',
3380
]);
3381
3382
const root3 = ReactNoop.createRoot();
3383
- expect(() =>
3383
+ expect(() => {
3384
act(() => {
3385
root3.render(<App return={Promise.resolve()} />);
3386
- }),
3387
- ).toErrorDev([
3386
+ });
3387
+ }).toErrorDev([
3388
'Warning: useLayoutEffect must not return anything besides a ' +
3389
'function, which is used for clean-up.\n\n' +
3390
'It looks like you wrote useLayoutEffect(async () => ...) or returned a Promise.',
packages/react/src/__tests__/ReactCoffeeScriptClass-test.coffee
+4
@@ -133,6 +133,7 @@ describe 'ReactCoffeeScriptClass', ->
133
expect(->
134
act ->
135
root.render React.createElement(Foo, foo: 'foo')
136
+ return
137
).toErrorDev 'Foo: getDerivedStateFromProps() is defined as an instance method and will be ignored. Instead, declare it as a static method.'
138
139
it 'warns if getDerivedStateFromError is not static', ->
@@ -144,6 +145,7 @@ describe 'ReactCoffeeScriptClass', ->
145
expect(->
146
act ->
147
root.render React.createElement(Foo, foo: 'foo')
148
+ return
149
).toErrorDev 'Foo: getDerivedStateFromError() is defined as an instance method and will be ignored. Instead, declare it as a static method.'
150
151
it 'warns if getSnapshotBeforeUpdate is static', ->
@@ -155,6 +157,7 @@ describe 'ReactCoffeeScriptClass', ->
157
expect(->
158
act ->
159
root.render React.createElement(Foo, foo: 'foo')
160
+ return
161
).toErrorDev 'Foo: getSnapshotBeforeUpdate() is defined as a static method and will be ignored. Instead, declare it as an instance method.'
162
163
it 'warns if state not initialized before static getDerivedStateFromProps', ->
@@ -171,6 +174,7 @@ describe 'ReactCoffeeScriptClass', ->
174
expect(->
175
act ->
176
root.render React.createElement(Foo, foo: 'foo')
177
+ return
178
).toErrorDev (
179
'`Foo` uses `getDerivedStateFromProps` but its initial state is ' +
180
'undefined. This is not recommended. Instead, define the initial state by ' +
packages/react/src/__tests__/ReactES6Class-test.js
+12
-4
@@ -149,7 +149,9 @@ describe('ReactES6Class', () => {
149
return <div />;
150
}
151
}
152
- expect(() => act(() => root.render(<Foo foo="foo" />))).toErrorDev(
152
+ expect(() => {
153
+ act(() => root.render(<Foo foo="foo" />));
154
+ }).toErrorDev(
155
'Foo: getDerivedStateFromProps() is defined as an instance method ' +
156
'and will be ignored. Instead, declare it as a static method.',
157
);
@@ -164,7 +166,9 @@ describe('ReactES6Class', () => {
166
return <div />;
167
}
168
}
167
- expect(() => act(() => root.render(<Foo foo="foo" />))).toErrorDev(
169
+ expect(() => {
170
+ act(() => root.render(<Foo foo="foo" />));
171
+ }).toErrorDev(
172
'Foo: getDerivedStateFromError() is defined as an instance method ' +
173
'and will be ignored. Instead, declare it as a static method.',
174
);
@@ -177,7 +181,9 @@ describe('ReactES6Class', () => {
181
return <div />;
182
}
183
}
180
- expect(() => act(() => root.render(<Foo foo="foo" />))).toErrorDev(
184
+ expect(() => {
185
+ act(() => root.render(<Foo foo="foo" />));
186
+ }).toErrorDev(
187
'Foo: getSnapshotBeforeUpdate() is defined as a static method ' +
188
'and will be ignored. Instead, declare it as an instance method.',
189
);
@@ -195,7 +201,9 @@ describe('ReactES6Class', () => {
201
return <div className={`${this.state.foo} ${this.state.bar}`} />;
202
}
203
}
198
- expect(() => act(() => root.render(<Foo foo="foo" />))).toErrorDev(
204
+ expect(() => {
205
+ act(() => root.render(<Foo foo="foo" />));
206
+ }).toErrorDev(
207
'`Foo` uses `getDerivedStateFromProps` but its initial state is ' +
208
'undefined. This is not recommended. Instead, define the initial state by ' +
209
'assigning an object to `this.state` in the constructor of `Foo`. ' +
packages/use-sync-external-store/src/__tests__/useSyncExternalStoreShared-test.js
+21
-8
@@ -14,6 +14,7 @@ let useSyncExternalStoreWithSelector;
14
let React;
15
let ReactDOM;
16
let ReactDOMClient;
17
+let ReactFeatureFlags;
18
let Scheduler;
19
let act;
20
let useState;
@@ -48,6 +49,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
49
React = require('react');
50
ReactDOM = require('react-dom');
51
ReactDOMClient = require('react-dom/client');
52
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
53
Scheduler = require('scheduler');
54
useState = React.useState;
55
useEffect = React.useEffect;
@@ -882,8 +884,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
884
885
describe('selector and isEqual error handling in extra', () => {
886
let ErrorBoundary;
885
- beforeAll(() => {
886
- spyOnDev(console, 'warn');
887
+ beforeEach(() => {
888
ErrorBoundary = class extends React.Component {
889
state = {error: null};
890
static getDerivedStateFromError(error) {
@@ -929,9 +930,15 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
930
931
expect(container.textContent).toEqual('A');
932
932
- await act(() => {
933
- store.set({});
934
- });
933
+ await expect(async () => {
934
+ await act(async () => {
935
+ store.set({});
936
+ });
937
+ }).toWarnDev(
938
+ ReactFeatureFlags.enableUseRefAccessWarning
939
+ ? ['Warning: App: Unsafe read of a mutable value during render.']
940
+ : [],
941
+ );
942
expect(container.textContent).toEqual('Malformed state');
943
});
944
@@ -968,9 +975,15 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
975
976
expect(container.textContent).toEqual('A');
977
971
- await act(() => {
972
- store.set({});
973
- });
978
+ await expect(async () => {
979
+ await act(() => {
980
+ store.set({});
981
+ });
982
+ }).toWarnDev(
983
+ ReactFeatureFlags.enableUseRefAccessWarning
984
+ ? ['Warning: App: Unsafe read of a mutable value during render.']
985
+ : [],
986
+ );
987
expect(container.textContent).toEqual('Malformed state');
988
});
989
});
scripts/jest/matchers/toWarnDev.js
+43
-7
@@ -152,11 +152,7 @@ const createMatcherFor = (consoleMethod, matcherName) =>
152
// Avoid using Jest's built-in spy since it can't be removed.
153
console[consoleMethod] = consoleSpy;
154
155
- try {
156
- callback();
157
- } catch (error) {
158
- caughtError = error;
159
- } finally {
155
+ const onFinally = () => {
156
// Restore the unspied method so that unexpected errors fail tests.
157
console[consoleMethod] = originalMethod;
158
@@ -259,12 +255,52 @@ const createMatcherFor = (consoleMethod, matcherName) =>
255
}
256
257
return {pass: true};
258
+ };
259
+
260
+ let returnPromise = null;
261
+ try {
262
+ const result = callback();
263
+
264
+ if (
265
+ typeof result === 'object' &&
266
+ result !== null &&
267
+ typeof result.then === 'function'
268
+ ) {
269
+ // `act` returns a thenable that can't be chained.
270
+ // Once `act(async () => {}).then(() => {}).then(() => {})` works
271
+ // we can just return `result.then(onFinally, error => ...)`
272
+ returnPromise = new Promise((resolve, reject) => {
273
+ result.then(
274
+ () => {
275
+ resolve(onFinally());
276
+ },
277
+ error => {
278
+ caughtError = error;
279
+ return resolve(onFinally());
280
+ }
281
+ );
282
+ });
283
+ }
284
+ } catch (error) {
285
+ caughtError = error;
286
+ } finally {
287
+ return returnPromise === null ? onFinally() : returnPromise;
288
}
289
} else {
290
// Any uncaught errors or warnings should fail tests in production mode.
265
- callback();
291
+ const result = callback();
292
267
- return {pass: true};
293
+ if (
294
+ typeof result === 'object' &&
295
+ result !== null &&
296
+ typeof result.then === 'function'
297
+ ) {
298
+ return result.then(() => {
299
+ return {pass: true};
300
+ });
301
+ } else {
302
+ return {pass: true};
303
+ }
304
}
305
};
306