Skip to content

Commit 3ba7add

Browse files
authored
Allow async blocks in to(Error|Warn)Dev (#25338)
1 parent f197ca9 commit 3ba7add

File tree

5 files changed

+113
-52
lines changed

5 files changed

+113
-52
lines changed

packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1777,7 +1777,7 @@ describe('ReactHooksWithNoopRenderer', () => {
17771777
}, [props.count]);
17781778
return <Text text={'Count: ' + count} />;
17791779
}
1780-
expect(() =>
1780+
expect(() => {
17811781
act(() => {
17821782
ReactNoop.render(<Counter count={0} />, () =>
17831783
Scheduler.unstable_yieldValue('Sync effect'),
@@ -1787,8 +1787,8 @@ describe('ReactHooksWithNoopRenderer', () => {
17871787
'Sync effect',
17881788
]);
17891789
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');
17921792
expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
17931793
});
17941794

@@ -2648,32 +2648,32 @@ describe('ReactHooksWithNoopRenderer', () => {
26482648
}
26492649

26502650
const root1 = ReactNoop.createRoot();
2651-
expect(() =>
2651+
expect(() => {
26522652
act(() => {
26532653
root1.render(<App return={17} />);
2654-
}),
2655-
).toErrorDev([
2654+
});
2655+
}).toErrorDev([
26562656
'Warning: useEffect must not return anything besides a ' +
26572657
'function, which is used for clean-up. You returned: 17',
26582658
]);
26592659

26602660
const root2 = ReactNoop.createRoot();
2661-
expect(() =>
2661+
expect(() => {
26622662
act(() => {
26632663
root2.render(<App return={null} />);
2664-
}),
2665-
).toErrorDev([
2664+
});
2665+
}).toErrorDev([
26662666
'Warning: useEffect must not return anything besides a ' +
26672667
'function, which is used for clean-up. You returned null. If your ' +
26682668
'effect does not require clean up, return undefined (or nothing).',
26692669
]);
26702670

26712671
const root3 = ReactNoop.createRoot();
2672-
expect(() =>
2672+
expect(() => {
26732673
act(() => {
26742674
root3.render(<App return={Promise.resolve()} />);
2675-
}),
2676-
).toErrorDev([
2675+
});
2676+
}).toErrorDev([
26772677
'Warning: useEffect must not return anything besides a ' +
26782678
'function, which is used for clean-up.\n\n' +
26792679
'It looks like you wrote useEffect(async () => ...) or returned a Promise.',
@@ -3052,32 +3052,32 @@ describe('ReactHooksWithNoopRenderer', () => {
30523052
}
30533053

30543054
const root1 = ReactNoop.createRoot();
3055-
expect(() =>
3055+
expect(() => {
30563056
act(() => {
30573057
root1.render(<App return={17} />);
3058-
}),
3059-
).toErrorDev([
3058+
});
3059+
}).toErrorDev([
30603060
'Warning: useInsertionEffect must not return anything besides a ' +
30613061
'function, which is used for clean-up. You returned: 17',
30623062
]);
30633063

30643064
const root2 = ReactNoop.createRoot();
3065-
expect(() =>
3065+
expect(() => {
30663066
act(() => {
30673067
root2.render(<App return={null} />);
3068-
}),
3069-
).toErrorDev([
3068+
});
3069+
}).toErrorDev([
30703070
'Warning: useInsertionEffect must not return anything besides a ' +
30713071
'function, which is used for clean-up. You returned null. If your ' +
30723072
'effect does not require clean up, return undefined (or nothing).',
30733073
]);
30743074

30753075
const root3 = ReactNoop.createRoot();
3076-
expect(() =>
3076+
expect(() => {
30773077
act(() => {
30783078
root3.render(<App return={Promise.resolve()} />);
3079-
}),
3080-
).toErrorDev([
3079+
});
3080+
}).toErrorDev([
30813081
'Warning: useInsertionEffect must not return anything besides a ' +
30823082
'function, which is used for clean-up.\n\n' +
30833083
'It looks like you wrote useInsertionEffect(async () => ...) or returned a Promise.',
@@ -3104,11 +3104,11 @@ describe('ReactHooksWithNoopRenderer', () => {
31043104
}
31053105

31063106
const root = ReactNoop.createRoot();
3107-
expect(() =>
3107+
expect(() => {
31083108
act(() => {
31093109
root.render(<App />);
3110-
}),
3111-
).toErrorDev(['Warning: useInsertionEffect must not schedule updates.']);
3110+
});
3111+
}).toErrorDev(['Warning: useInsertionEffect must not schedule updates.']);
31123112

31133113
expect(() => {
31143114
act(() => {
@@ -3359,32 +3359,32 @@ describe('ReactHooksWithNoopRenderer', () => {
33593359
}
33603360

33613361
const root1 = ReactNoop.createRoot();
3362-
expect(() =>
3362+
expect(() => {
33633363
act(() => {
33643364
root1.render(<App return={17} />);
3365-
}),
3366-
).toErrorDev([
3365+
});
3366+
}).toErrorDev([
33673367
'Warning: useLayoutEffect must not return anything besides a ' +
33683368
'function, which is used for clean-up. You returned: 17',
33693369
]);
33703370

33713371
const root2 = ReactNoop.createRoot();
3372-
expect(() =>
3372+
expect(() => {
33733373
act(() => {
33743374
root2.render(<App return={null} />);
3375-
}),
3376-
).toErrorDev([
3375+
});
3376+
}).toErrorDev([
33773377
'Warning: useLayoutEffect must not return anything besides a ' +
33783378
'function, which is used for clean-up. You returned null. If your ' +
33793379
'effect does not require clean up, return undefined (or nothing).',
33803380
]);
33813381

33823382
const root3 = ReactNoop.createRoot();
3383-
expect(() =>
3383+
expect(() => {
33843384
act(() => {
33853385
root3.render(<App return={Promise.resolve()} />);
3386-
}),
3387-
).toErrorDev([
3386+
});
3387+
}).toErrorDev([
33883388
'Warning: useLayoutEffect must not return anything besides a ' +
33893389
'function, which is used for clean-up.\n\n' +
33903390
'It looks like you wrote useLayoutEffect(async () => ...) or returned a Promise.',

packages/react/src/__tests__/ReactCoffeeScriptClass-test.coffee

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ describe 'ReactCoffeeScriptClass', ->
133133
expect(->
134134
act ->
135135
root.render React.createElement(Foo, foo: 'foo')
136+
return
136137
).toErrorDev 'Foo: getDerivedStateFromProps() is defined as an instance method and will be ignored. Instead, declare it as a static method.'
137138

138139
it 'warns if getDerivedStateFromError is not static', ->
@@ -144,6 +145,7 @@ describe 'ReactCoffeeScriptClass', ->
144145
expect(->
145146
act ->
146147
root.render React.createElement(Foo, foo: 'foo')
148+
return
147149
).toErrorDev 'Foo: getDerivedStateFromError() is defined as an instance method and will be ignored. Instead, declare it as a static method.'
148150

149151
it 'warns if getSnapshotBeforeUpdate is static', ->
@@ -155,6 +157,7 @@ describe 'ReactCoffeeScriptClass', ->
155157
expect(->
156158
act ->
157159
root.render React.createElement(Foo, foo: 'foo')
160+
return
158161
).toErrorDev 'Foo: getSnapshotBeforeUpdate() is defined as a static method and will be ignored. Instead, declare it as an instance method.'
159162

160163
it 'warns if state not initialized before static getDerivedStateFromProps', ->
@@ -171,6 +174,7 @@ describe 'ReactCoffeeScriptClass', ->
171174
expect(->
172175
act ->
173176
root.render React.createElement(Foo, foo: 'foo')
177+
return
174178
).toErrorDev (
175179
'`Foo` uses `getDerivedStateFromProps` but its initial state is ' +
176180
'undefined. This is not recommended. Instead, define the initial state by ' +

packages/react/src/__tests__/ReactES6Class-test.js

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,9 @@ describe('ReactES6Class', () => {
149149
return <div />;
150150
}
151151
}
152-
expect(() => act(() => root.render(<Foo foo="foo" />))).toErrorDev(
152+
expect(() => {
153+
act(() => root.render(<Foo foo="foo" />));
154+
}).toErrorDev(
153155
'Foo: getDerivedStateFromProps() is defined as an instance method ' +
154156
'and will be ignored. Instead, declare it as a static method.',
155157
);
@@ -164,7 +166,9 @@ describe('ReactES6Class', () => {
164166
return <div />;
165167
}
166168
}
167-
expect(() => act(() => root.render(<Foo foo="foo" />))).toErrorDev(
169+
expect(() => {
170+
act(() => root.render(<Foo foo="foo" />));
171+
}).toErrorDev(
168172
'Foo: getDerivedStateFromError() is defined as an instance method ' +
169173
'and will be ignored. Instead, declare it as a static method.',
170174
);
@@ -177,7 +181,9 @@ describe('ReactES6Class', () => {
177181
return <div />;
178182
}
179183
}
180-
expect(() => act(() => root.render(<Foo foo="foo" />))).toErrorDev(
184+
expect(() => {
185+
act(() => root.render(<Foo foo="foo" />));
186+
}).toErrorDev(
181187
'Foo: getSnapshotBeforeUpdate() is defined as a static method ' +
182188
'and will be ignored. Instead, declare it as an instance method.',
183189
);
@@ -195,7 +201,9 @@ describe('ReactES6Class', () => {
195201
return <div className={`${this.state.foo} ${this.state.bar}`} />;
196202
}
197203
}
198-
expect(() => act(() => root.render(<Foo foo="foo" />))).toErrorDev(
204+
expect(() => {
205+
act(() => root.render(<Foo foo="foo" />));
206+
}).toErrorDev(
199207
'`Foo` uses `getDerivedStateFromProps` but its initial state is ' +
200208
'undefined. This is not recommended. Instead, define the initial state by ' +
201209
'assigning an object to `this.state` in the constructor of `Foo`. ' +

packages/use-sync-external-store/src/__tests__/useSyncExternalStoreShared-test.js

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ let useSyncExternalStoreWithSelector;
1414
let React;
1515
let ReactDOM;
1616
let ReactDOMClient;
17+
let ReactFeatureFlags;
1718
let Scheduler;
1819
let act;
1920
let useState;
@@ -48,6 +49,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
4849
React = require('react');
4950
ReactDOM = require('react-dom');
5051
ReactDOMClient = require('react-dom/client');
52+
ReactFeatureFlags = require('shared/ReactFeatureFlags');
5153
Scheduler = require('scheduler');
5254
useState = React.useState;
5355
useEffect = React.useEffect;
@@ -882,8 +884,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
882884

883885
describe('selector and isEqual error handling in extra', () => {
884886
let ErrorBoundary;
885-
beforeAll(() => {
886-
spyOnDev(console, 'warn');
887+
beforeEach(() => {
887888
ErrorBoundary = class extends React.Component {
888889
state = {error: null};
889890
static getDerivedStateFromError(error) {
@@ -929,9 +930,15 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
929930

930931
expect(container.textContent).toEqual('A');
931932

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+
);
935942
expect(container.textContent).toEqual('Malformed state');
936943
});
937944

@@ -968,9 +975,15 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
968975

969976
expect(container.textContent).toEqual('A');
970977

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+
);
974987
expect(container.textContent).toEqual('Malformed state');
975988
});
976989
});

scripts/jest/matchers/toWarnDev.js

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -152,11 +152,7 @@ const createMatcherFor = (consoleMethod, matcherName) =>
152152
// Avoid using Jest's built-in spy since it can't be removed.
153153
console[consoleMethod] = consoleSpy;
154154

155-
try {
156-
callback();
157-
} catch (error) {
158-
caughtError = error;
159-
} finally {
155+
const onFinally = () => {
160156
// Restore the unspied method so that unexpected errors fail tests.
161157
console[consoleMethod] = originalMethod;
162158

@@ -259,12 +255,52 @@ const createMatcherFor = (consoleMethod, matcherName) =>
259255
}
260256

261257
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;
262288
}
263289
} else {
264290
// Any uncaught errors or warnings should fail tests in production mode.
265-
callback();
291+
const result = callback();
266292

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+
}
268304
}
269305
};
270306

0 commit comments

Comments
 (0)