Skip to content

Commit 6b86533

Browse files
authored
[assert helpers] react-reconciler (facebook#31986)
Based off: facebook#31984
1 parent 83be48b commit 6b86533

17 files changed

+1101
-638
lines changed

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

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ let startTransition;
1414
let waitForPaint;
1515
let waitFor;
1616
let assertLog;
17+
let assertConsoleErrorDev;
1718

1819
describe('Activity', () => {
1920
beforeEach(() => {
@@ -37,6 +38,7 @@ describe('Activity', () => {
3738
waitForPaint = InternalTestUtils.waitForPaint;
3839
waitFor = InternalTestUtils.waitFor;
3940
assertLog = InternalTestUtils.assertLog;
41+
assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev;
4042
});
4143

4244
function Text(props) {
@@ -784,11 +786,14 @@ describe('Activity', () => {
784786
// would be null because it was nulled out when it was deleted, but there
785787
// was no null check before we accessed it. A weird edge case but we must
786788
// account for it.
787-
expect(() => {
788-
setState('Updated');
789-
}).toErrorDev(
790-
"Can't perform a React state update on a component that hasn't mounted yet",
791-
);
789+
setState('Updated');
790+
assertConsoleErrorDev([
791+
"Can't perform a React state update on a component that hasn't mounted yet. " +
792+
'This indicates that you have a side-effect in your render function that ' +
793+
'asynchronously later calls tries to update the component. ' +
794+
'Move this work to useEffect instead.\n' +
795+
' in Child (at **)',
796+
]);
792797
});
793798
expect(root).toMatchRenderedOutput(null);
794799
});

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

Lines changed: 120 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ let Suspense;
1818
let startTransition;
1919
let getCacheForType;
2020
let caches;
21+
let assertConsoleErrorDev;
2122

2223
// These tests are mostly concerned with concurrent roots. The legacy root
2324
// behavior is covered by other older test suites and is unchanged from
@@ -38,6 +39,7 @@ describe('act warnings', () => {
3839
const InternalTestUtils = require('internal-test-utils');
3940
waitForAll = InternalTestUtils.waitForAll;
4041
assertLog = InternalTestUtils.assertLog;
42+
assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev;
4143
});
4244

4345
function createTextCache() {
@@ -171,9 +173,21 @@ describe('act warnings', () => {
171173

172174
// Flag is true. Warn.
173175
await withActEnvironment(true, async () => {
174-
expect(() => setState(2)).toErrorDev(
175-
'An update to App inside a test was not wrapped in act',
176-
);
176+
setState(2);
177+
assertConsoleErrorDev([
178+
'An update to App inside a test was not wrapped in act(...).\n' +
179+
'\n' +
180+
'When testing, code that causes React state updates should be wrapped into act(...):\n' +
181+
'\n' +
182+
'act(() => {\n' +
183+
' /* fire events that update state */\n' +
184+
'});\n' +
185+
'/* assert on the output */\n' +
186+
'\n' +
187+
"This ensures that you're testing the behavior the user would see in the browser. " +
188+
'Learn more at https://react.dev/link/wrap-tests-with-act\n' +
189+
' in App (at **)',
190+
]);
177191
await waitForAll([2]);
178192
expect(root).toMatchRenderedOutput('2');
179193
});
@@ -202,12 +216,11 @@ describe('act warnings', () => {
202216

203217
// Default behavior. Flag is undefined. Warn.
204218
expect(global.IS_REACT_ACT_ENVIRONMENT).toBe(undefined);
205-
expect(() => {
206-
act(() => {
207-
setState(1);
208-
});
209-
}).toErrorDev(
210-
'The current testing environment is not configured to support act(...)',
219+
act(() => {
220+
setState(1);
221+
});
222+
assertConsoleErrorDev(
223+
['The current testing environment is not configured to support act(...)'],
211224
{withoutStack: true},
212225
);
213226
assertLog([1]);
@@ -224,12 +237,13 @@ describe('act warnings', () => {
224237

225238
// Flag is false. Warn.
226239
await withActEnvironment(false, () => {
227-
expect(() => {
228-
act(() => {
229-
setState(1);
230-
});
231-
}).toErrorDev(
232-
'The current testing environment is not configured to support act(...)',
240+
act(() => {
241+
setState(1);
242+
});
243+
assertConsoleErrorDev(
244+
[
245+
'The current testing environment is not configured to support act(...)',
246+
],
233247
{withoutStack: true},
234248
);
235249
assertLog([1]);
@@ -240,10 +254,23 @@ describe('act warnings', () => {
240254
it('warns if root update is not wrapped', async () => {
241255
await withActEnvironment(true, () => {
242256
const root = ReactNoop.createRoot();
243-
expect(() => root.render('Hi')).toErrorDev(
244-
// TODO: Better error message that doesn't make it look like "Root" is
245-
// the name of a custom component
246-
'An update to Root inside a test was not wrapped in act(...)',
257+
root.render('Hi');
258+
assertConsoleErrorDev(
259+
[
260+
// TODO: Better error message that doesn't make it look like "Root" is
261+
// the name of a custom component
262+
'An update to Root inside a test was not wrapped in act(...).\n' +
263+
'\n' +
264+
'When testing, code that causes React state updates should be wrapped into act(...):\n' +
265+
'\n' +
266+
'act(() => {\n' +
267+
' /* fire events that update state */\n' +
268+
'});\n' +
269+
'/* assert on the output */\n' +
270+
'\n' +
271+
"This ensures that you're testing the behavior the user would see in the browser. " +
272+
'Learn more at https://react.dev/link/wrap-tests-with-act',
273+
],
247274
{withoutStack: true},
248275
);
249276
});
@@ -265,9 +292,21 @@ describe('act warnings', () => {
265292
act(() => {
266293
root.render(<App />);
267294
});
268-
expect(() => app.setState({count: 1})).toErrorDev(
269-
'An update to App inside a test was not wrapped in act(...)',
270-
);
295+
app.setState({count: 1});
296+
assertConsoleErrorDev([
297+
'An update to App inside a test was not wrapped in act(...).\n' +
298+
'\n' +
299+
'When testing, code that causes React state updates should be wrapped into act(...):\n' +
300+
'\n' +
301+
'act(() => {\n' +
302+
' /* fire events that update state */\n' +
303+
'});\n' +
304+
'/* assert on the output */\n' +
305+
'\n' +
306+
"This ensures that you're testing the behavior the user would see in the browser. " +
307+
'Learn more at https://react.dev/link/wrap-tests-with-act\n' +
308+
' in App (at **)',
309+
]);
271310
});
272311
});
273312

@@ -288,9 +327,21 @@ describe('act warnings', () => {
288327

289328
// Even though this update is synchronous, we should still fire a warning,
290329
// because it could have spawned additional asynchronous work
291-
expect(() => ReactNoop.flushSync(() => setState(1))).toErrorDev(
292-
'An update to App inside a test was not wrapped in act(...)',
293-
);
330+
ReactNoop.flushSync(() => setState(1));
331+
assertConsoleErrorDev([
332+
'An update to App inside a test was not wrapped in act(...).\n' +
333+
'\n' +
334+
'When testing, code that causes React state updates should be wrapped into act(...):\n' +
335+
'\n' +
336+
'act(() => {\n' +
337+
' /* fire events that update state */\n' +
338+
'});\n' +
339+
'/* assert on the output */\n' +
340+
'\n' +
341+
"This ensures that you're testing the behavior the user would see in the browser. " +
342+
'Learn more at https://react.dev/link/wrap-tests-with-act\n' +
343+
' in App (at **)',
344+
]);
294345

295346
assertLog([1]);
296347
expect(root).toMatchRenderedOutput('1');
@@ -322,12 +373,36 @@ describe('act warnings', () => {
322373
expect(root).toMatchRenderedOutput('Loading...');
323374

324375
// This is a retry, not a ping, because we already showed a fallback.
325-
expect(() => resolveText('Async')).toErrorDev(
376+
resolveText('Async');
377+
assertConsoleErrorDev(
326378
[
327-
'A suspended resource finished loading inside a test, but the event ' +
328-
'was not wrapped in act(...)',
329-
330-
...(gate('enableSiblingPrerendering') ? ['not wrapped in act'] : []),
379+
'A suspended resource finished loading inside a test, but the event was not wrapped in act(...).\n' +
380+
'\n' +
381+
'When testing, code that resolves suspended data should be wrapped into act(...):\n' +
382+
'\n' +
383+
'act(() => {\n' +
384+
' /* finish loading suspended data */\n' +
385+
'});\n' +
386+
'/* assert on the output */\n' +
387+
'\n' +
388+
"This ensures that you're testing the behavior the user would see in the browser. " +
389+
'Learn more at https://react.dev/link/wrap-tests-with-act',
390+
391+
...(gate('enableSiblingPrerendering')
392+
? [
393+
'A suspended resource finished loading inside a test, but the event was not wrapped in act(...).\n' +
394+
'\n' +
395+
'When testing, code that resolves suspended data should be wrapped into act(...):\n' +
396+
'\n' +
397+
'act(() => {\n' +
398+
' /* finish loading suspended data */\n' +
399+
'});\n' +
400+
'/* assert on the output */\n' +
401+
'\n' +
402+
"This ensures that you're testing the behavior the user would see in the browser. " +
403+
'Learn more at https://react.dev/link/wrap-tests-with-act',
404+
]
405+
: []),
331406
],
332407

333408
{withoutStack: true},
@@ -363,9 +438,21 @@ describe('act warnings', () => {
363438
expect(root).toMatchRenderedOutput('(empty)');
364439

365440
// This is a ping, not a retry, because no fallback is showing.
366-
expect(() => resolveText('Async')).toErrorDev(
367-
'A suspended resource finished loading inside a test, but the event ' +
368-
'was not wrapped in act(...)',
441+
resolveText('Async');
442+
assertConsoleErrorDev(
443+
[
444+
'A suspended resource finished loading inside a test, but the event was not wrapped in act(...).\n' +
445+
'\n' +
446+
'When testing, code that resolves suspended data should be wrapped into act(...):\n' +
447+
'\n' +
448+
'act(() => {\n' +
449+
' /* finish loading suspended data */\n' +
450+
'});\n' +
451+
'/* assert on the output */\n' +
452+
'\n' +
453+
"This ensures that you're testing the behavior the user would see in the browser. " +
454+
'Learn more at https://react.dev/link/wrap-tests-with-act',
455+
],
369456
{withoutStack: true},
370457
);
371458
});

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

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ let useTransition;
77
let useState;
88
let useOptimistic;
99
let textCache;
10+
let assertConsoleErrorDev;
1011

1112
describe('ReactAsyncActions', () => {
1213
beforeEach(() => {
@@ -21,6 +22,8 @@ describe('ReactAsyncActions', () => {
2122
Scheduler = require('scheduler');
2223
act = require('internal-test-utils').act;
2324
assertLog = require('internal-test-utils').assertLog;
25+
assertConsoleErrorDev =
26+
require('internal-test-utils').assertConsoleErrorDev;
2427
useTransition = React.useTransition;
2528
useState = React.useState;
2629
useOptimistic = React.useOptimistic;
@@ -1231,15 +1234,16 @@ describe('ReactAsyncActions', () => {
12311234
assertLog(['A']);
12321235
expect(root).toMatchRenderedOutput(<div>A</div>);
12331236

1234-
await expect(async () => {
1235-
await act(() => {
1236-
setLoadingProgress('25%');
1237-
startTransition(() => setText('B'));
1238-
});
1239-
}).toErrorDev(
1240-
'An optimistic state update occurred outside a transition or ' +
1241-
'action. To fix, move the update to an action, or wrap ' +
1242-
'with startTransition.',
1237+
await act(() => {
1238+
setLoadingProgress('25%');
1239+
startTransition(() => setText('B'));
1240+
});
1241+
assertConsoleErrorDev(
1242+
[
1243+
'An optimistic state update occurred outside a transition or ' +
1244+
'action. To fix, move the update to an action, or wrap ' +
1245+
'with startTransition.',
1246+
],
12431247
{withoutStack: true},
12441248
);
12451249
assertLog(['Loading... (25%)', 'A', 'B']);

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

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
let React;
1313
let ReactNoop;
1414
let waitForAll;
15+
let assertConsoleErrorDev;
1516

1617
describe('ReactFragment', () => {
1718
beforeEach(function () {
@@ -22,6 +23,7 @@ describe('ReactFragment', () => {
2223

2324
const InternalTestUtils = require('internal-test-utils');
2425
waitForAll = InternalTestUtils.waitForAll;
26+
assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev;
2527
});
2628

2729
it('should render a single child via noop renderer', async () => {
@@ -740,9 +742,22 @@ describe('ReactFragment', () => {
740742
await waitForAll([]);
741743

742744
ReactNoop.render(<Foo condition={false} />);
743-
await expect(async () => await waitForAll([])).toErrorDev(
744-
'Each child in a list should have a unique "key" prop.',
745-
);
745+
await waitForAll([]);
746+
assertConsoleErrorDev([
747+
gate('enableOwnerStacks')
748+
? 'Each child in a list should have a unique "key" prop.\n' +
749+
'\n' +
750+
'Check the render method of `div`. ' +
751+
'It was passed a child from Foo. ' +
752+
'See https://react.dev/link/warning-keys for more information.\n' +
753+
' in Foo (at **)'
754+
: 'Each child in a list should have a unique "key" prop.\n' +
755+
'\n' +
756+
'Check the render method of `Foo`. ' +
757+
'See https://react.dev/link/warning-keys for more information.\n' +
758+
' in Stateful (at **)\n' +
759+
' in Foo (at **)',
760+
]);
746761

747762
expect(ops).toEqual([]);
748763
expect(ReactNoop).toMatchRenderedOutput(
@@ -937,9 +952,16 @@ describe('ReactFragment', () => {
937952
}
938953

939954
ReactNoop.render(<Foo condition={true} />);
940-
await expect(async () => await waitForAll([])).toErrorDev(
941-
'Each child in a list should have a unique "key" prop.',
942-
);
955+
await waitForAll([]);
956+
assertConsoleErrorDev([
957+
'Each child in a list should have a unique "key" prop.\n' +
958+
'\n' +
959+
'Check the top-level render call using <Foo>. ' +
960+
'It was passed a child from Foo. ' +
961+
'See https://react.dev/link/warning-keys for more information.\n' +
962+
' in span (at **)\n' +
963+
' in Foo (at **)',
964+
]);
943965

944966
ReactNoop.render(<Foo condition={false} />);
945967
// The key warning gets deduped because it's in the same component.

0 commit comments

Comments
 (0)