@samitouri / QOS-React-2 / commits / 3ba7add608

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