[assert helpers] react-dom (pt3) (#31983)
moar assert helpers this finishes all of react-dom except the server integration tests which are tricky to convert
Ricky committed
Jan 5, 2025 at 17:10 UTC
03e4ec2d0fe7cd854d28634ba035dc8996ff244d
28 files changed
+2056
-1461
packages/react-dom/src/__tests__/ReactDOMInvalidARIAHook-test.js
+31
-22
@@ -14,12 +14,15 @@ describe('ReactDOMInvalidARIAHook', () => {
14
let ReactDOMClient;
15
let mountComponent;
16
let act;
17
+ let assertConsoleErrorDev;
18
19
beforeEach(() => {
20
jest.resetModules();
21
React = require('react');
22
ReactDOMClient = require('react-dom/client');
23
act = require('internal-test-utils').act;
24
+ assertConsoleErrorDev =
25
+ require('internal-test-utils').assertConsoleErrorDev;
26
27
mountComponent = async function (props) {
28
const container = document.createElement('div');
@@ -35,46 +38,52 @@ describe('ReactDOMInvalidARIAHook', () => {
38
await mountComponent({'aria-label': 'Bumble bees'});
39
});
40
it('should warn for one invalid aria-* prop', async () => {
38
- await expect(() => mountComponent({'aria-badprop': 'maybe'})).toErrorDev(
41
+ await mountComponent({'aria-badprop': 'maybe'});
42
+ assertConsoleErrorDev([
43
'Invalid aria prop `aria-badprop` on <div> tag. ' +
40
- 'For details, see https://react.dev/link/invalid-aria-props',
41
- );
44
+ 'For details, see https://react.dev/link/invalid-aria-props\n' +
45
+ ' in div (at **)',
46
+ ]);
47
});
48
it('should warn for many invalid aria-* props', async () => {
44
- await expect(() =>
45
- mountComponent({
46
- 'aria-badprop': 'Very tall trees',
47
- 'aria-malprop': 'Turbulent seas',
48
- }),
49
- ).toErrorDev(
49
+ await mountComponent({
50
+ 'aria-badprop': 'Very tall trees',
51
+ 'aria-malprop': 'Turbulent seas',
52
+ });
53
+ assertConsoleErrorDev([
54
'Invalid aria props `aria-badprop`, `aria-malprop` on <div> ' +
51
- 'tag. For details, see https://react.dev/link/invalid-aria-props',
52
- );
55
+ 'tag. For details, see https://react.dev/link/invalid-aria-props\n' +
56
+ ' in div (at **)',
57
+ ]);
58
});
59
it('should warn for an improperly cased aria-* prop', async () => {
60
// The valid attribute name is aria-haspopup.
56
- await expect(() => mountComponent({'aria-hasPopup': 'true'})).toErrorDev(
61
+ await mountComponent({'aria-hasPopup': 'true'});
62
+ assertConsoleErrorDev([
63
'Unknown ARIA attribute `aria-hasPopup`. ' +
58
- 'Did you mean `aria-haspopup`?',
59
- );
64
+ 'Did you mean `aria-haspopup`?\n' +
65
+ ' in div (at **)',
66
+ ]);
67
});
68
69
it('should warn for use of recognized camel case aria attributes', async () => {
70
// The valid attribute name is aria-haspopup.
64
- await expect(() => mountComponent({ariaHasPopup: 'true'})).toErrorDev(
71
+ await mountComponent({ariaHasPopup: 'true'});
72
+ assertConsoleErrorDev([
73
'Invalid ARIA attribute `ariaHasPopup`. ' +
66
- 'Did you mean `aria-haspopup`?',
67
- );
74
+ 'Did you mean `aria-haspopup`?\n' +
75
+ ' in div (at **)',
76
+ ]);
77
});
78
79
it('should warn for use of unrecognized camel case aria attributes', async () => {
80
// The valid attribute name is aria-haspopup.
72
- await expect(() =>
73
- mountComponent({ariaSomethingInvalid: 'true'}),
74
- ).toErrorDev(
81
+ await mountComponent({ariaSomethingInvalid: 'true'});
82
+ assertConsoleErrorDev([
83
'Invalid ARIA attribute `ariaSomethingInvalid`. ARIA ' +
76
- 'attributes follow the pattern aria-* and must be lowercase.',
77
- );
84
+ 'attributes follow the pattern aria-* and must be lowercase.\n' +
85
+ ' in div (at **)',
86
+ ]);
87
});
88
});
89
});
packages/react-dom/src/__tests__/ReactDOMLegacyComponentTree-test.internal.js
+19
-10
@@ -13,10 +13,13 @@ describe('ReactDOMComponentTree', () => {
13
let React;
14
let ReactDOM;
15
let container;
16
+ let assertConsoleErrorDev;
17
18
beforeEach(() => {
19
React = require('react');
20
ReactDOM = require('react-dom');
21
+ assertConsoleErrorDev =
22
+ require('internal-test-utils').assertConsoleErrorDev;
23
24
container = document.createElement('div');
25
document.body.appendChild(container);
@@ -31,11 +34,14 @@ describe('ReactDOMComponentTree', () => {
34
it('finds instance of node that is attempted to be unmounted', () => {
35
const component = <div />;
36
const node = ReactDOM.render(<div>{component}</div>, container);
34
- expect(() => ReactDOM.unmountComponentAtNode(node)).toErrorDev(
35
- "unmountComponentAtNode(): The node you're attempting to unmount " +
36
- 'was rendered by React and is not a top-level container. You may ' +
37
- 'have accidentally passed in a React root node instead of its ' +
38
- 'container.',
37
+ ReactDOM.unmountComponentAtNode(node);
38
+ assertConsoleErrorDev(
39
+ [
40
+ "unmountComponentAtNode(): The node you're attempting to unmount " +
41
+ 'was rendered by React and is not a top-level container. You may ' +
42
+ 'have accidentally passed in a React root node instead of its ' +
43
+ 'container.',
44
+ ],
45
{withoutStack: true},
46
);
47
});
@@ -49,11 +55,14 @@ describe('ReactDOMComponentTree', () => {
55
);
56
const anotherComponent = <div />;
57
const instance = ReactDOM.render(component, container);
52
- expect(() => ReactDOM.render(anotherComponent, instance)).toErrorDev(
53
- 'Replacing React-rendered children with a new root ' +
54
- 'component. If you intended to update the children of this node, ' +
55
- 'you should instead have the existing children update their state ' +
56
- 'and render the new components instead of calling ReactDOM.render.',
58
+ ReactDOM.render(anotherComponent, instance);
59
+ assertConsoleErrorDev(
60
+ [
61
+ 'Replacing React-rendered children with a new root ' +
62
+ 'component. If you intended to update the children of this node, ' +
63
+ 'you should instead have the existing children update their state ' +
64
+ 'and render the new components instead of calling ReactDOM.render.',
65
+ ],
66
{withoutStack: true},
67
);
68
});
packages/react-dom/src/__tests__/ReactDOMLegacyFiber-test.js
+45
-37
@@ -13,11 +13,14 @@ const React = require('react');
13
const ReactDOM = require('react-dom');
14
const PropTypes = require('prop-types');
15
let act;
16
+let assertConsoleErrorDev;
17
describe('ReactDOMLegacyFiber', () => {
18
let container;
19
20
beforeEach(() => {
21
act = require('internal-test-utils').act;
22
+ assertConsoleErrorDev =
23
+ require('internal-test-utils').assertConsoleErrorDev;
24
container = document.createElement('div');
25
document.body.appendChild(container);
26
});
@@ -786,9 +789,8 @@ describe('ReactDOMLegacyFiber', () => {
789
}
790
}
791
789
- expect(() => {
790
- ReactDOM.render(<Parent />, container);
791
- }).toErrorDev([
792
+ ReactDOM.render(<Parent />, container);
793
+ assertConsoleErrorDev([
794
'Parent uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
795
'Component uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
796
]);
@@ -834,10 +836,8 @@ describe('ReactDOMLegacyFiber', () => {
836
}
837
}
838
837
- let instance;
838
- expect(() => {
839
- instance = ReactDOM.render(<Parent />, container);
840
- }).toErrorDev([
839
+ const instance = ReactDOM.render(<Parent />, container);
840
+ assertConsoleErrorDev([
841
'Parent uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
842
'Component uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
843
]);
@@ -882,9 +882,8 @@ describe('ReactDOMLegacyFiber', () => {
882
}
883
}
884
885
- expect(() => {
886
- ReactDOM.render(<Parent bar="initial" />, container);
887
- }).toErrorDev([
885
+ ReactDOM.render(<Parent bar="initial" />, container);
886
+ assertConsoleErrorDev([
887
'Parent uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
888
'Component uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
889
]);
@@ -1117,11 +1116,12 @@ describe('ReactDOMLegacyFiber', () => {
1116
return <div onClick="woops" />;
1117
}
1118
}
1120
- expect(() => ReactDOM.render(<Example />, container)).toErrorDev(
1119
+ ReactDOM.render(<Example />, container);
1120
+ assertConsoleErrorDev([
1121
'Expected `onClick` listener to be a function, instead got a value of `string` type.\n' +
1122
' in div (at **)\n' +
1123
' in Example (at **)',
1124
- );
1124
+ ]);
1125
});
1126
1127
// @gate !disableLegacyMode
@@ -1131,13 +1131,14 @@ describe('ReactDOMLegacyFiber', () => {
1131
return <div onClick={false} />;
1132
}
1133
}
1134
- expect(() => ReactDOM.render(<Example />, container)).toErrorDev(
1134
+ ReactDOM.render(<Example />, container);
1135
+ assertConsoleErrorDev([
1136
'Expected `onClick` listener to be a function, instead got `false`.\n\n' +
1137
'If you used to conditionally omit it with onClick={condition && value}, ' +
1138
'pass onClick={condition ? value : undefined} instead.\n' +
1139
' in div (at **)\n' +
1140
' in Example (at **)',
1140
- );
1141
+ ]);
1142
});
1143
1144
// @gate !disableLegacyMode
@@ -1270,17 +1271,18 @@ describe('ReactDOMLegacyFiber', () => {
1271
container.innerHTML = '<div>MEOW.</div>';
1272
1273
await expect(async () => {
1273
- await expect(async () => {
1274
- await act(() => {
1275
- ReactDOM.render(<div key="2">baz</div>, container);
1276
- });
1277
- }).rejects.toThrow('The node to be removed is not a child of this node.');
1278
- }).toErrorDev(
1279
- '' +
1280
- 'It looks like the React-rendered content of this container was ' +
1281
- 'removed without using React. This is not supported and will ' +
1282
- 'cause errors. Instead, call ReactDOM.unmountComponentAtNode ' +
1283
- 'to empty a container.',
1274
+ await act(() => {
1275
+ ReactDOM.render(<div key="2">baz</div>, container);
1276
+ });
1277
+ }).rejects.toThrow('The node to be removed is not a child of this node.');
1278
+ assertConsoleErrorDev(
1279
+ [
1280
+ '' +
1281
+ 'It looks like the React-rendered content of this container was ' +
1282
+ 'removed without using React. This is not supported and will ' +
1283
+ 'cause errors. Instead, call ReactDOM.unmountComponentAtNode ' +
1284
+ 'to empty a container.',
1285
+ ],
1286
{withoutStack: true},
1287
);
1288
});
@@ -1293,12 +1295,15 @@ describe('ReactDOMLegacyFiber', () => {
1295
expect(container.innerHTML).toBe('<div>bar</div>');
1296
// then we mess with the DOM before an update
1297
container.innerHTML = '<div>MEOW.</div>';
1296
- expect(() => ReactDOM.render(<div>baz</div>, container)).toErrorDev(
1297
- '' +
1298
- 'It looks like the React-rendered content of this container was ' +
1299
- 'removed without using React. This is not supported and will ' +
1300
- 'cause errors. Instead, call ReactDOM.unmountComponentAtNode ' +
1301
- 'to empty a container.',
1298
+ ReactDOM.render(<div>baz</div>, container);
1299
+ assertConsoleErrorDev(
1300
+ [
1301
+ '' +
1302
+ 'It looks like the React-rendered content of this container was ' +
1303
+ 'removed without using React. This is not supported and will ' +
1304
+ 'cause errors. Instead, call ReactDOM.unmountComponentAtNode ' +
1305
+ 'to empty a container.',
1306
+ ],
1307
{withoutStack: true},
1308
);
1309
});
@@ -1311,12 +1316,15 @@ describe('ReactDOMLegacyFiber', () => {
1316
expect(container.innerHTML).toBe('<div>bar</div>');
1317
// then we mess with the DOM before an update
1318
container.innerHTML = '';
1314
- expect(() => ReactDOM.render(<div>baz</div>, container)).toErrorDev(
1315
- '' +
1316
- 'It looks like the React-rendered content of this container was ' +
1317
- 'removed without using React. This is not supported and will ' +
1318
- 'cause errors. Instead, call ReactDOM.unmountComponentAtNode ' +
1319
- 'to empty a container.',
1319
+ ReactDOM.render(<div>baz</div>, container);
1320
+ assertConsoleErrorDev(
1321
+ [
1322
+ '' +
1323
+ 'It looks like the React-rendered content of this container was ' +
1324
+ 'removed without using React. This is not supported and will ' +
1325
+ 'cause errors. Instead, call ReactDOM.unmountComponentAtNode ' +
1326
+ 'to empty a container.',
1327
+ ],
1328
{withoutStack: true},
1329
);
1330
});
packages/react-dom/src/__tests__/ReactDOMOption-test.js
+21
-24
@@ -14,6 +14,7 @@ describe('ReactDOMOption', () => {
14
let ReactDOMClient;
15
let ReactDOMServer;
16
let act;
17
+ let assertConsoleErrorDev;
18
19
beforeEach(() => {
20
jest.resetModules();
@@ -21,6 +22,8 @@ describe('ReactDOMOption', () => {
22
ReactDOMClient = require('react-dom/client');
23
ReactDOMServer = require('react-dom/server');
24
act = require('internal-test-utils').act;
25
+ assertConsoleErrorDev =
26
+ require('internal-test-utils').assertConsoleErrorDev;
27
});
28
29
async function renderIntoDocument(children) {
@@ -47,10 +50,8 @@ describe('ReactDOMOption', () => {
50
{1} <div /> {2}
51
</option>
52
);
50
- let container;
51
- await expect(async () => {
52
- container = await renderIntoDocument(el);
53
- }).toErrorDev(
53
+ const container = await renderIntoDocument(el);
54
+ assertConsoleErrorDev([
55
'In HTML, <div> cannot be a child of <option>.\n' +
56
'This will cause a hydration error.\n' +
57
'\n' +
@@ -62,7 +63,7 @@ describe('ReactDOMOption', () => {
63
(gate(flags => flags.enableOwnerStacks)
64
? ''
65
: '\n in option (at **)'),
65
- );
66
+ ]);
67
expect(container.firstChild.innerHTML).toBe('1 <div></div> 2');
68
await renderIntoDocument(el);
69
});
@@ -76,13 +77,12 @@ describe('ReactDOMOption', () => {
77
{1} <Foo /> {3}
78
</option>
79
);
79
- let container;
80
- await expect(async () => {
81
- container = await renderIntoDocument(el);
82
- }).toErrorDev(
80
+ const container = await renderIntoDocument(el);
81
+ assertConsoleErrorDev([
82
'Cannot infer the option value of complex children. ' +
84
- 'Pass a `value` prop or use a plain string as children to <option>.',
85
- );
83
+ 'Pass a `value` prop or use a plain string as children to <option>.\n' +
84
+ ' in option (at **)',
85
+ ]);
86
expect(container.firstChild.innerHTML).toBe('1 2 3');
87
await renderIntoDocument(el);
88
});
@@ -187,13 +187,11 @@ describe('ReactDOMOption', () => {
187
188
it('should be able to use dangerouslySetInnerHTML on option', async () => {
189
const stub = <option dangerouslySetInnerHTML={{__html: 'foobar'}} />;
190
- let container;
191
- await expect(async () => {
192
- container = await renderIntoDocument(stub);
193
- }).toErrorDev(
190
+ const container = await renderIntoDocument(stub);
191
+ assertConsoleErrorDev([
192
'Pass a `value` prop if you set dangerouslyInnerHTML so React knows which value should be selected.\n' +
193
' in option (at **)',
196
- );
194
+ ]);
195
196
expect(container.firstChild.innerHTML).toBe('foobar');
197
});
@@ -267,13 +265,12 @@ describe('ReactDOMOption', () => {
265
expect(option.textContent).toBe('BarFooBaz');
266
expect(option.selected).toBe(true);
267
270
- await expect(async () => {
271
- await act(async () => {
272
- ReactDOMClient.hydrateRoot(container, children, {
273
- onRecoverableError: () => {},
274
- });
268
+ await act(async () => {
269
+ ReactDOMClient.hydrateRoot(container, children, {
270
+ onRecoverableError: () => {},
271
});
276
- }).toErrorDev(
272
+ });
273
+ assertConsoleErrorDev([
274
'In HTML, <div> cannot be a child of <option>.\n' +
275
'This will cause a hydration error.\n' +
276
'\n' +
@@ -285,8 +282,8 @@ describe('ReactDOMOption', () => {
282
' in div (at **)' +
283
(gate(flags => flags.enableOwnerStacks)
284
? ''
288
- : '\n in option (at **)'),
289
- );
285
+ : '\n in option (at **)' + '\n in select (at **)'),
286
+ ]);
287
option = container.firstChild.firstChild;
288
289
expect(option.textContent).toBe('BarFooBaz');
packages/react-dom/src/__tests__/ReactDOMRoot-test.js
+98
-50
@@ -18,6 +18,7 @@ let act;
18
let useEffect;
19
let assertLog;
20
let waitForAll;
21
+let assertConsoleErrorDev;
22
23
describe('ReactDOMRoot', () => {
24
let container;
@@ -31,6 +32,8 @@ describe('ReactDOMRoot', () => {
32
ReactDOMServer = require('react-dom/server');
33
Scheduler = require('scheduler');
34
act = require('internal-test-utils').act;
35
+ assertConsoleErrorDev =
36
+ require('internal-test-utils').assertConsoleErrorDev;
37
useEffect = React.useEffect;
38
39
const InternalTestUtils = require('internal-test-utils');
@@ -48,9 +51,12 @@ describe('ReactDOMRoot', () => {
51
it('warns if a callback parameter is provided to render', async () => {
52
const callback = jest.fn();
53
const root = ReactDOMClient.createRoot(container);
51
- expect(() => root.render(<div>Hi</div>, callback)).toErrorDev(
52
- 'does not support the second callback argument. ' +
53
- 'To execute a side effect after rendering, declare it in a component body with useEffect().',
54
+ root.render(<div>Hi</div>, callback);
55
+ assertConsoleErrorDev(
56
+ [
57
+ 'does not support the second callback argument. ' +
58
+ 'To execute a side effect after rendering, declare it in a component body with useEffect().',
59
+ ],
60
{withoutStack: true},
61
);
62
await waitForAll([]);
@@ -63,9 +69,12 @@ describe('ReactDOMRoot', () => {
69
}
70
71
const root = ReactDOMClient.createRoot(container);
66
- expect(() => root.render(<App />, {})).toErrorDev(
67
- 'You passed a second argument to root.render(...) but it only accepts ' +
68
- 'one argument.',
72
+ root.render(<App />, {});
73
+ assertConsoleErrorDev(
74
+ [
75
+ 'You passed a second argument to root.render(...) but it only accepts ' +
76
+ 'one argument.',
77
+ ],
78
{
79
withoutStack: true,
80
},
@@ -78,10 +87,13 @@ describe('ReactDOMRoot', () => {
87
}
88
89
const root = ReactDOMClient.createRoot(container);
81
- expect(() => root.render(<App />, container)).toErrorDev(
82
- 'You passed a container to the second argument of root.render(...). ' +
83
- "You don't need to pass it again since you already passed it to create " +
84
- 'the root.',
90
+ root.render(<App />, container);
91
+ assertConsoleErrorDev(
92
+ [
93
+ 'You passed a container to the second argument of root.render(...). ' +
94
+ "You don't need to pass it again since you already passed it to create " +
95
+ 'the root.',
96
+ ],
97
{
98
withoutStack: true,
99
},
@@ -92,9 +104,12 @@ describe('ReactDOMRoot', () => {
104
const callback = jest.fn();
105
const root = ReactDOMClient.createRoot(container);
106
root.render(<div>Hi</div>);
95
- expect(() => root.unmount(callback)).toErrorDev(
96
- 'does not support a callback argument. ' +
97
- 'To execute a side effect after rendering, declare it in a component body with useEffect().',
107
+ root.unmount(callback);
108
+ assertConsoleErrorDev(
109
+ [
110
+ 'does not support a callback argument. ' +
111
+ 'To execute a side effect after rendering, declare it in a component body with useEffect().',
112
+ ],
113
{withoutStack: true},
114
);
115
await waitForAll([]);
@@ -148,8 +163,27 @@ describe('ReactDOMRoot', () => {
163
<span />
164
</div>,
165
);
151
- await expect(async () => await waitForAll([])).toErrorDev(
152
- "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.",
166
+ await waitForAll([]);
167
+ assertConsoleErrorDev(
168
+ [
169
+ "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. " +
170
+ "This won't be patched up. This can happen if a SSR-ed Client Component used:\n" +
171
+ '\n' +
172
+ "- A server/client branch `if (typeof window !== 'undefined')`.\n" +
173
+ "- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" +
174
+ "- Date formatting in a user's locale which doesn't match the server.\n" +
175
+ '- External changing data without sending a snapshot of it along with the HTML.\n' +
176
+ '- Invalid HTML tag nesting.\n' +
177
+ '\n' +
178
+ 'It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.\n' +
179
+ '\n' +
180
+ 'https://react.dev/link/hydration-mismatch\n' +
181
+ '\n' +
182
+ ' <div>\n' +
183
+ ' <span\n' +
184
+ '- className="extra"\n' +
185
+ ' >\n',
186
+ ],
187
{withoutStack: true},
188
);
189
});
@@ -183,12 +217,13 @@ describe('ReactDOMRoot', () => {
217
218
it('warns when creating two roots managing the same container', () => {
219
ReactDOMClient.createRoot(container);
186
- expect(() => {
187
- ReactDOMClient.createRoot(container);
188
- }).toErrorDev(
189
- 'You are calling ReactDOMClient.createRoot() on a container that ' +
190
- 'has already been passed to createRoot() before. Instead, call ' +
191
- 'root.render() on the existing root instead if you want to update it.',
220
+ ReactDOMClient.createRoot(container);
221
+ assertConsoleErrorDev(
222
+ [
223
+ 'You are calling ReactDOMClient.createRoot() on a container that ' +
224
+ 'has already been passed to createRoot() before. Instead, call ' +
225
+ 'root.render() on the existing root instead if you want to update it.',
226
+ ],
227
{withoutStack: true},
228
);
229
});
@@ -352,14 +387,15 @@ describe('ReactDOMRoot', () => {
387
});
388
expect(container1.textContent).toEqual('Hi');
389
355
- expect(() => {
356
- ReactDOM.flushSync(() => {
357
- root1.render(<App step={2} />);
358
- });
359
- }).toErrorDev(
360
- 'Attempted to synchronously unmount a root while React was ' +
361
- 'already rendering.',
362
- );
390
+ ReactDOM.flushSync(() => {
391
+ root1.render(<App step={2} />);
392
+ });
393
+ assertConsoleErrorDev([
394
+ 'Attempted to synchronously unmount a root while React was already rendering. ' +
395
+ 'React cannot finish unmounting the root until the current render has completed, ' +
396
+ 'which may lead to a race condition.\n' +
397
+ ' in App (at **)',
398
+ ]);
399
});
400
401
// @gate disableCommentsAsDOMContainers
@@ -378,8 +414,12 @@ describe('ReactDOMRoot', () => {
414
});
415
416
it('warn if no children passed to hydrateRoot', async () => {
381
- expect(() => ReactDOMClient.hydrateRoot(container)).toErrorDev(
382
- 'Must provide initial children as second argument to hydrateRoot.',
417
+ ReactDOMClient.hydrateRoot(container);
418
+ assertConsoleErrorDev(
419
+ [
420
+ 'Must provide initial children as second argument to hydrateRoot. ' +
421
+ 'Example usage: hydrateRoot(domContainer, <App />)',
422
+ ],
423
{withoutStack: true},
424
);
425
});
@@ -389,9 +429,15 @@ describe('ReactDOMRoot', () => {
429
return 'Child';
430
}
431
392
- expect(() => ReactDOMClient.createRoot(container, <App />)).toErrorDev(
393
- 'You passed a JSX element to createRoot. You probably meant to call ' +
394
- 'root.render instead',
432
+ ReactDOMClient.createRoot(container, <App />);
433
+ assertConsoleErrorDev(
434
+ [
435
+ 'You passed a JSX element to createRoot. You probably meant to call root.render instead. ' +
436
+ 'Example usage:\n' +
437
+ '\n' +
438
+ ' let root = createRoot(domContainer);\n' +
439
+ ' root.render(<App />);',
440
+ ],
441
{
442
withoutStack: true,
443
},
@@ -405,15 +451,16 @@ describe('ReactDOMRoot', () => {
451
452
const root = ReactDOMClient.createRoot(document.createElement('div'));
453
408
- expect(() => {
409
- ReactDOM.flushSync(() => {
410
- root.render(Component);
411
- });
412
- }).toErrorDev(
413
- 'Functions are not valid as a React child. ' +
414
- 'This may happen if you return Component instead of <Component /> from render. ' +
415
- 'Or maybe you meant to call this function rather than return it.\n' +
416
- ' root.render(Component)',
454
+ ReactDOM.flushSync(() => {
455
+ root.render(Component);
456
+ });
457
+ assertConsoleErrorDev(
458
+ [
459
+ 'Functions are not valid as a React child. ' +
460
+ 'This may happen if you return Component instead of <Component /> from render. ' +
461
+ 'Or maybe you meant to call this function rather than return it.\n' +
462
+ ' root.render(Component)',
463
+ ],
464
{withoutStack: true},
465
);
466
});
@@ -421,13 +468,14 @@ describe('ReactDOMRoot', () => {
468
it('warns when given a symbol', () => {
469
const root = ReactDOMClient.createRoot(document.createElement('div'));
470
424
- expect(() => {
425
- ReactDOM.flushSync(() => {
426
- root.render(Symbol('foo'));
427
- });
428
- }).toErrorDev(
429
- 'Symbols are not valid as a React child.\n' +
430
- ' root.render(Symbol(foo))',
471
+ ReactDOM.flushSync(() => {
472
+ root.render(Symbol('foo'));
473
+ });
474
+ assertConsoleErrorDev(
475
+ [
476
+ 'Symbols are not valid as a React child.\n' +
477
+ ' root.render(Symbol(foo))',
478
+ ],
479
{withoutStack: true},
480
);
481
});
packages/react-dom/src/__tests__/ReactDOMSelect-test.js
+432
-367
@@ -22,6 +22,7 @@ describe('ReactDOMSelect', () => {
22
let ReactDOMClient;
23
let ReactDOMServer;
24
let act;
25
+ let assertConsoleErrorDev;
26
27
const noop = function () {};
28
@@ -32,6 +33,8 @@ describe('ReactDOMSelect', () => {
33
ReactDOMClient = require('react-dom/client');
34
ReactDOMServer = require('react-dom/server');
35
act = require('internal-test-utils').act;
36
+ assertConsoleErrorDev =
37
+ require('internal-test-utils').assertConsoleErrorDev;
38
});
39
40
it('should allow setting `defaultValue`', async () => {
@@ -749,19 +752,19 @@ describe('ReactDOMSelect', () => {
752
it('should warn if value is null', async () => {
753
const container = document.createElement('div');
754
const root = ReactDOMClient.createRoot(container);
752
- await expect(async () => {
753
- await act(() => {
754
- root.render(
755
- <select value={null}>
756
- <option value="test" />
757
- </select>,
758
- );
759
- });
760
- }).toErrorDev(
755
+ await act(() => {
756
+ root.render(
757
+ <select value={null}>
758
+ <option value="test" />
759
+ </select>,
760
+ );
761
+ });
762
+ assertConsoleErrorDev([
763
'`value` prop on `select` should not be null. ' +
764
'Consider using an empty string to clear the component or `undefined` ' +
763
- 'for uncontrolled components.',
764
- );
765
+ 'for uncontrolled components.\n' +
766
+ ' in select (at **)',
767
+ ]);
768
769
await act(() => {
770
root.render(
@@ -784,14 +787,16 @@ describe('ReactDOMSelect', () => {
787
788
const container = document.createElement('div');
789
const root = ReactDOMClient.createRoot(container);
787
- await expect(async () => {
788
- await act(() => {
789
- root.render(<App />);
790
- });
791
- }).toErrorDev(
790
+ await act(() => {
791
+ root.render(<App />);
792
+ });
793
+ assertConsoleErrorDev([
794
'Use the `defaultValue` or `value` props on <select> instead of ' +
793
- 'setting `selected` on <option>.',
794
- );
795
+ 'setting `selected` on <option>.\n' +
796
+ ' in option (at **)\n' +
797
+ (gate('enableOwnerStacks') ? '' : ' in select (at **)\n') +
798
+ ' in App (at **)',
799
+ ]);
800
801
await act(() => {
802
root.render(<App />);
@@ -801,20 +806,20 @@ describe('ReactDOMSelect', () => {
806
it('should warn if value is null and multiple is true', async () => {
807
const container = document.createElement('div');
808
const root = ReactDOMClient.createRoot(container);
804
- await expect(async () => {
805
- await act(() => {
806
- root.render(
807
- <select value={null} multiple={true}>
808
- <option value="test" />
809
- </select>,
810
- );
811
- });
812
- }).toErrorDev(
809
+ await act(() => {
810
+ root.render(
811
+ <select value={null} multiple={true}>
812
+ <option value="test" />
813
+ </select>,
814
+ );
815
+ });
816
+ assertConsoleErrorDev([
817
'`value` prop on `select` should not be null. ' +
818
'Consider using an empty array when `multiple` is ' +
819
'set to `true` to clear the component or `undefined` ' +
816
- 'for uncontrolled components.',
817
- );
820
+ 'for uncontrolled components.\n' +
821
+ ' in select (at **)',
822
+ ]);
823
824
await act(() => {
825
root.render(
@@ -860,23 +865,23 @@ describe('ReactDOMSelect', () => {
865
it('should warn if value and defaultValue props are specified', async () => {
866
const container = document.createElement('div');
867
const root = ReactDOMClient.createRoot(container);
863
- await expect(async () => {
864
- await act(() => {
865
- root.render(
866
- <select value="giraffe" defaultValue="giraffe" readOnly={true}>
867
- <option value="monkey">A monkey!</option>
868
- <option value="giraffe">A giraffe!</option>
869
- <option value="gorilla">A gorilla!</option>
870
- </select>,
871
- );
872
- });
873
- }).toErrorDev(
868
+ await act(() => {
869
+ root.render(
870
+ <select value="giraffe" defaultValue="giraffe" readOnly={true}>
871
+ <option value="monkey">A monkey!</option>
872
+ <option value="giraffe">A giraffe!</option>
873
+ <option value="gorilla">A gorilla!</option>
874
+ </select>,
875
+ );
876
+ });
877
+ assertConsoleErrorDev([
878
'Select elements must be either controlled or uncontrolled ' +
879
'(specify either the value prop, or the defaultValue prop, but not ' +
880
'both). Decide between using a controlled or uncontrolled select ' +
881
'element and remove one of these props. More info: ' +
878
- 'https://react.dev/link/controlled-components',
879
- );
882
+ 'https://react.dev/link/controlled-components\n' +
883
+ ' in select (at **)',
884
+ ]);
885
886
await act(() => {
887
root.render(
@@ -1054,17 +1059,22 @@ describe('ReactDOMSelect', () => {
1059
it('treats initial Symbol value as missing', async () => {
1060
const container = document.createElement('div');
1061
const root = ReactDOMClient.createRoot(container);
1057
- await expect(async () => {
1058
- await act(() => {
1059
- root.render(
1060
- <select onChange={noop} value={Symbol('foobar')}>
1061
- <option value={Symbol('foobar')}>A Symbol!</option>
1062
- <option value="monkey">A monkey!</option>
1063
- <option value="giraffe">A giraffe!</option>
1064
- </select>,
1065
- );
1066
- });
1067
- }).toErrorDev('Invalid value for prop `value`');
1062
+ await act(() => {
1063
+ root.render(
1064
+ <select onChange={noop} value={Symbol('foobar')}>
1065
+ <option value={Symbol('foobar')}>A Symbol!</option>
1066
+ <option value="monkey">A monkey!</option>
1067
+ <option value="giraffe">A giraffe!</option>
1068
+ </select>,
1069
+ );
1070
+ });
1071
+ assertConsoleErrorDev([
1072
+ 'Invalid value for prop `value` on <option> tag. ' +
1073
+ 'Either remove it from the element, or pass a string or number value to keep it in the DOM. ' +
1074
+ 'For details, see https://react.dev/link/attribute-behavior \n' +
1075
+ ' in option (at **)' +
1076
+ (gate('enableOwnerStacks') ? '' : '\n in select (at **)'),
1077
+ ]);
1078
1079
const node = container.firstChild;
1080
expect(node.value).toBe('A Symbol!');
@@ -1073,17 +1083,22 @@ describe('ReactDOMSelect', () => {
1083
it('treats updated Symbol value as missing', async () => {
1084
const container = document.createElement('div');
1085
const root = ReactDOMClient.createRoot(container);
1076
- await expect(async () => {
1077
- await act(() => {
1078
- root.render(
1079
- <select onChange={noop} value="monkey">
1080
- <option value={Symbol('foobar')}>A Symbol!</option>
1081
- <option value="monkey">A monkey!</option>
1082
- <option value="giraffe">A giraffe!</option>
1083
- </select>,
1084
- );
1085
- });
1086
- }).toErrorDev('Invalid value for prop `value`');
1086
+ await act(() => {
1087
+ root.render(
1088
+ <select onChange={noop} value="monkey">
1089
+ <option value={Symbol('foobar')}>A Symbol!</option>
1090
+ <option value="monkey">A monkey!</option>
1091
+ <option value="giraffe">A giraffe!</option>
1092
+ </select>,
1093
+ );
1094
+ });
1095
+ assertConsoleErrorDev([
1096
+ 'Invalid value for prop `value` on <option> tag. ' +
1097
+ 'Either remove it from the element, or pass a string or number value to keep it in the DOM. ' +
1098
+ 'For details, see https://react.dev/link/attribute-behavior \n' +
1099
+ ' in option (at **)' +
1100
+ (gate('enableOwnerStacks') ? '' : '\n in select (at **)'),
1101
+ ]);
1102
1103
let node = container.firstChild;
1104
expect(node.value).toBe('monkey');
@@ -1106,17 +1121,22 @@ describe('ReactDOMSelect', () => {
1121
it('treats initial Symbol defaultValue as an empty string', async () => {
1122
const container = document.createElement('div');
1123
const root = ReactDOMClient.createRoot(container);
1109
- await expect(async () => {
1110
- await act(() => {
1111
- root.render(
1112
- <select defaultValue={Symbol('foobar')}>
1113
- <option value={Symbol('foobar')}>A Symbol!</option>
1114
- <option value="monkey">A monkey!</option>
1115
- <option value="giraffe">A giraffe!</option>
1116
- </select>,
1117
- );
1118
- });
1119
- }).toErrorDev('Invalid value for prop `value`');
1124
+ await act(() => {
1125
+ root.render(
1126
+ <select defaultValue={Symbol('foobar')}>
1127
+ <option value={Symbol('foobar')}>A Symbol!</option>
1128
+ <option value="monkey">A monkey!</option>
1129
+ <option value="giraffe">A giraffe!</option>
1130
+ </select>,
1131
+ );
1132
+ });
1133
+ assertConsoleErrorDev([
1134
+ 'Invalid value for prop `value` on <option> tag. ' +
1135
+ 'Either remove it from the element, or pass a string or number value to keep it in the DOM. ' +
1136
+ 'For details, see https://react.dev/link/attribute-behavior \n' +
1137
+ ' in option (at **)' +
1138
+ (gate('enableOwnerStacks') ? '' : '\n in select (at **)'),
1139
+ ]);
1140
1141
const node = container.firstChild;
1142
expect(node.value).toBe('A Symbol!');
@@ -1125,17 +1145,22 @@ describe('ReactDOMSelect', () => {
1145
it('treats updated Symbol defaultValue as an empty string', async () => {
1146
let container = document.createElement('div');
1147
let root = ReactDOMClient.createRoot(container);
1128
- await expect(async () => {
1129
- await act(() => {
1130
- root.render(
1131
- <select defaultValue="monkey">
1132
- <option value={Symbol('foobar')}>A Symbol!</option>
1133
- <option value="monkey">A monkey!</option>
1134
- <option value="giraffe">A giraffe!</option>
1135
- </select>,
1136
- );
1137
- });
1138
- }).toErrorDev('Invalid value for prop `value`');
1148
+ await act(() => {
1149
+ root.render(
1150
+ <select defaultValue="monkey">
1151
+ <option value={Symbol('foobar')}>A Symbol!</option>
1152
+ <option value="monkey">A monkey!</option>
1153
+ <option value="giraffe">A giraffe!</option>
1154
+ </select>,
1155
+ );
1156
+ });
1157
+ assertConsoleErrorDev([
1158
+ 'Invalid value for prop `value` on <option> tag. ' +
1159
+ 'Either remove it from the element, or pass a string or number value to keep it in the DOM. ' +
1160
+ 'For details, see https://react.dev/link/attribute-behavior \n' +
1161
+ ' in option (at **)' +
1162
+ (gate('enableOwnerStacks') ? '' : '\n in select (at **)'),
1163
+ ]);
1164
1165
let node = container.firstChild;
1166
expect(node.value).toBe('monkey');
@@ -1161,17 +1186,22 @@ describe('ReactDOMSelect', () => {
1186
it('treats initial function value as missing', async () => {
1187
const container = document.createElement('div');
1188
const root = ReactDOMClient.createRoot(container);
1164
- await expect(async () => {
1165
- await act(() => {
1166
- root.render(
1167
- <select onChange={noop} value={() => {}}>
1168
- <option value={() => {}}>A function!</option>
1169
- <option value="monkey">A monkey!</option>
1170
- <option value="giraffe">A giraffe!</option>
1171
- </select>,
1172
- );
1173
- });
1174
- }).toErrorDev('Invalid value for prop `value`');
1189
+ await act(() => {
1190
+ root.render(
1191
+ <select onChange={noop} value={() => {}}>
1192
+ <option value={() => {}}>A function!</option>
1193
+ <option value="monkey">A monkey!</option>
1194
+ <option value="giraffe">A giraffe!</option>
1195
+ </select>,
1196
+ );
1197
+ });
1198
+ assertConsoleErrorDev([
1199
+ 'Invalid value for prop `value` on <option> tag. ' +
1200
+ 'Either remove it from the element, or pass a string or number value to keep it in the DOM. ' +
1201
+ 'For details, see https://react.dev/link/attribute-behavior \n' +
1202
+ ' in option (at **)' +
1203
+ (gate('enableOwnerStacks') ? '' : '\n in select (at **)'),
1204
+ ]);
1205
1206
const node = container.firstChild;
1207
expect(node.value).toBe('A function!');
@@ -1180,17 +1210,22 @@ describe('ReactDOMSelect', () => {
1210
it('treats initial function defaultValue as an empty string', async () => {
1211
const container = document.createElement('div');
1212
const root = ReactDOMClient.createRoot(container);
1183
- await expect(async () => {
1184
- await act(() => {
1185
- root.render(
1186
- <select defaultValue={() => {}}>
1187
- <option value={() => {}}>A function!</option>
1188
- <option value="monkey">A monkey!</option>
1189
- <option value="giraffe">A giraffe!</option>
1190
- </select>,
1191
- );
1192
- });
1193
- }).toErrorDev('Invalid value for prop `value`');
1213
+ await act(() => {
1214
+ root.render(
1215
+ <select defaultValue={() => {}}>
1216
+ <option value={() => {}}>A function!</option>
1217
+ <option value="monkey">A monkey!</option>
1218
+ <option value="giraffe">A giraffe!</option>
1219
+ </select>,
1220
+ );
1221
+ });
1222
+ assertConsoleErrorDev([
1223
+ 'Invalid value for prop `value` on <option> tag. ' +
1224
+ 'Either remove it from the element, or pass a string or number value to keep it in the DOM. ' +
1225
+ 'For details, see https://react.dev/link/attribute-behavior \n' +
1226
+ ' in option (at **)' +
1227
+ (gate('enableOwnerStacks') ? '' : '\n in select (at **)'),
1228
+ ]);
1229
1230
const node = container.firstChild;
1231
expect(node.value).toBe('A function!');
@@ -1199,17 +1234,22 @@ describe('ReactDOMSelect', () => {
1234
it('treats updated function value as an empty string', async () => {
1235
const container = document.createElement('div');
1236
const root = ReactDOMClient.createRoot(container);
1202
- await expect(async () => {
1203
- await act(() => {
1204
- root.render(
1205
- <select onChange={noop} value="monkey">
1206
- <option value={() => {}}>A function!</option>
1207
- <option value="monkey">A monkey!</option>
1208
- <option value="giraffe">A giraffe!</option>
1209
- </select>,
1210
- );
1211
- });
1212
- }).toErrorDev('Invalid value for prop `value`');
1237
+ await act(() => {
1238
+ root.render(
1239
+ <select onChange={noop} value="monkey">
1240
+ <option value={() => {}}>A function!</option>
1241
+ <option value="monkey">A monkey!</option>
1242
+ <option value="giraffe">A giraffe!</option>
1243
+ </select>,
1244
+ );
1245
+ });
1246
+ assertConsoleErrorDev([
1247
+ 'Invalid value for prop `value` on <option> tag. ' +
1248
+ 'Either remove it from the element, or pass a string or number value to keep it in the DOM. ' +
1249
+ 'For details, see https://react.dev/link/attribute-behavior \n' +
1250
+ ' in option (at **)' +
1251
+ (gate('enableOwnerStacks') ? '' : '\n in select (at **)'),
1252
+ ]);
1253
1254
let node = container.firstChild;
1255
expect(node.value).toBe('monkey');
@@ -1231,17 +1271,22 @@ describe('ReactDOMSelect', () => {
1271
it('treats updated function defaultValue as an empty string', async () => {
1272
let container = document.createElement('div');
1273
let root = ReactDOMClient.createRoot(container);
1234
- await expect(async () => {
1235
- await act(() => {
1236
- root.render(
1237
- <select defaultValue="monkey">
1238
- <option value={() => {}}>A function!</option>
1239
- <option value="monkey">A monkey!</option>
1240
- <option value="giraffe">A giraffe!</option>
1241
- </select>,
1242
- );
1243
- });
1244
- }).toErrorDev('Invalid value for prop `value`');
1274
+ await act(() => {
1275
+ root.render(
1276
+ <select defaultValue="monkey">
1277
+ <option value={() => {}}>A function!</option>
1278
+ <option value="monkey">A monkey!</option>
1279
+ <option value="giraffe">A giraffe!</option>
1280
+ </select>,
1281
+ );
1282
+ });
1283
+ assertConsoleErrorDev([
1284
+ 'Invalid value for prop `value` on <option> tag. ' +
1285
+ 'Either remove it from the element, or pass a string or number value to keep it in the DOM. ' +
1286
+ 'For details, see https://react.dev/link/attribute-behavior \n' +
1287
+ ' in option (at **)' +
1288
+ (gate('enableOwnerStacks') ? '' : '\n in select (at **)'),
1289
+ ]);
1290
1291
let node = container.firstChild;
1292
expect(node.value).toBe('monkey');
@@ -1279,50 +1324,54 @@ describe('ReactDOMSelect', () => {
1324
it('throws when given a Temporal.PlainDate-like value (select)', async () => {
1325
const container = document.createElement('div');
1326
const root = ReactDOMClient.createRoot(container);
1282
- await expect(async () => {
1283
- await expect(
1284
- act(() => {
1285
- root.render(
1286
- <select onChange={noop} value={new TemporalLike()}>
1287
- <option value="2020-01-01">like a Temporal.PlainDate</option>
1288
- <option value="monkey">A monkey!</option>
1289
- <option value="giraffe">A giraffe!</option>
1290
- </select>,
1291
- );
1292
- }),
1293
- ).rejects.toThrowError(new TypeError('prod message'));
1294
- }).toErrorDev([
1327
+ await expect(
1328
+ act(() => {
1329
+ root.render(
1330
+ <select onChange={noop} value={new TemporalLike()}>
1331
+ <option value="2020-01-01">like a Temporal.PlainDate</option>
1332
+ <option value="monkey">A monkey!</option>
1333
+ <option value="giraffe">A giraffe!</option>
1334
+ </select>,
1335
+ );
1336
+ }),
1337
+ ).rejects.toThrowError(new TypeError('prod message'));
1338
+ assertConsoleErrorDev([
1339
'Form field values (value, checked, defaultValue, or defaultChecked props)' +
1340
' must be strings, not TemporalLike. ' +
1297
- 'This value must be coerced to a string before using it here.',
1341
+ 'This value must be coerced to a string before using it here.\n' +
1342
+ ' in select (at **)',
1343
'Form field values (value, checked, defaultValue, or defaultChecked props)' +
1344
' must be strings, not TemporalLike. ' +
1300
- 'This value must be coerced to a string before using it here.',
1345
+ 'This value must be coerced to a string before using it here.\n' +
1346
+ ' in select (at **)',
1347
]);
1348
});
1349
1350
it('throws when given a Temporal.PlainDate-like value (option)', async () => {
1351
const container = document.createElement('div');
1352
const root = ReactDOMClient.createRoot(container);
1307
- await expect(async () => {
1308
- await expect(
1309
- act(() => {
1310
- root.render(
1311
- <select onChange={noop} value="2020-01-01">
1312
- <option value={new TemporalLike()}>
1313
- like a Temporal.PlainDate
1314
- </option>
1315
- <option value="monkey">A monkey!</option>
1316
- <option value="giraffe">A giraffe!</option>
1317
- </select>,
1318
- );
1319
- }),
1320
- ).rejects.toThrowError(new TypeError('prod message'));
1321
- }).toErrorDev([
1353
+ await expect(
1354
+ act(() => {
1355
+ root.render(
1356
+ <select onChange={noop} value="2020-01-01">
1357
+ <option value={new TemporalLike()}>
1358
+ like a Temporal.PlainDate
1359
+ </option>
1360
+ <option value="monkey">A monkey!</option>
1361
+ <option value="giraffe">A giraffe!</option>
1362
+ </select>,
1363
+ );
1364
+ }),
1365
+ ).rejects.toThrowError(new TypeError('prod message'));
1366
+ assertConsoleErrorDev([
1367
'The provided `value` attribute is an unsupported type TemporalLike.' +
1323
- ' This value must be coerced to a string before using it here.',
1368
+ ' This value must be coerced to a string before using it here.\n' +
1369
+ ' in option (at **)' +
1370
+ (gate('enableOwnerStacks') ? '' : '\n in select (at **)'),
1371
'The provided `value` attribute is an unsupported type TemporalLike.' +
1325
- ' This value must be coerced to a string before using it here.',
1372
+ ' This value must be coerced to a string before using it here.\n' +
1373
+ ' in option (at **)' +
1374
+ (gate('enableOwnerStacks') ? '' : '\n in select (at **)'),
1375
]);
1376
});
1377
@@ -1330,25 +1379,28 @@ describe('ReactDOMSelect', () => {
1379
const container = document.createElement('div');
1380
const root = ReactDOMClient.createRoot(container);
1381
1333
- await expect(async () => {
1334
- await expect(
1335
- act(() => {
1336
- root.render(
1337
- <select onChange={noop} value={new TemporalLike()}>
1338
- <option value={new TemporalLike()}>
1339
- like a Temporal.PlainDate
1340
- </option>
1341
- <option value="monkey">A monkey!</option>
1342
- <option value="giraffe">A giraffe!</option>
1343
- </select>,
1344
- );
1345
- }),
1346
- ).rejects.toThrowError(new TypeError('prod message'));
1347
- }).toErrorDev([
1382
+ await expect(
1383
+ act(() => {
1384
+ root.render(
1385
+ <select onChange={noop} value={new TemporalLike()}>
1386
+ <option value={new TemporalLike()}>
1387
+ like a Temporal.PlainDate
1388
+ </option>
1389
+ <option value="monkey">A monkey!</option>
1390
+ <option value="giraffe">A giraffe!</option>
1391
+ </select>,
1392
+ );
1393
+ }),
1394
+ ).rejects.toThrowError(new TypeError('prod message'));
1395
+ assertConsoleErrorDev([
1396
'The provided `value` attribute is an unsupported type TemporalLike.' +
1349
- ' This value must be coerced to a string before using it here.',
1397
+ ' This value must be coerced to a string before using it here.\n' +
1398
+ ' in option (at **)' +
1399
+ (gate('enableOwnerStacks') ? '' : '\n in select (at **)'),
1400
'The provided `value` attribute is an unsupported type TemporalLike.' +
1351
- ' This value must be coerced to a string before using it here.',
1401
+ ' This value must be coerced to a string before using it here.\n' +
1402
+ ' in option (at **)' +
1403
+ (gate('enableOwnerStacks') ? '' : '\n in select (at **)'),
1404
]);
1405
});
1406
@@ -1366,23 +1418,23 @@ describe('ReactDOMSelect', () => {
1418
);
1419
});
1420
1369
- await expect(async () => {
1370
- await expect(
1371
- act(() => {
1372
- root.render(
1373
- <select onChange={noop} value={new TemporalLike()}>
1374
- <option value="2020-01-01">like a Temporal.PlainDate</option>
1375
- <option value="monkey">A monkey!</option>
1376
- <option value="giraffe">A giraffe!</option>
1377
- </select>,
1378
- );
1379
- }),
1380
- ).rejects.toThrowError(new TypeError('prod message'));
1381
- }).toErrorDev(
1421
+ await expect(
1422
+ act(() => {
1423
+ root.render(
1424
+ <select onChange={noop} value={new TemporalLike()}>
1425
+ <option value="2020-01-01">like a Temporal.PlainDate</option>
1426
+ <option value="monkey">A monkey!</option>
1427
+ <option value="giraffe">A giraffe!</option>
1428
+ </select>,
1429
+ );
1430
+ }),
1431
+ ).rejects.toThrowError(new TypeError('prod message'));
1432
+ assertConsoleErrorDev([
1433
'Form field values (value, checked, defaultValue, or defaultChecked props)' +
1434
' must be strings, not TemporalLike. ' +
1384
- 'This value must be coerced to a string before using it here.',
1385
- );
1435
+ 'This value must be coerced to a string before using it here.\n' +
1436
+ ' in select (at **)',
1437
+ ]);
1438
});
1439
1440
it('throws with updated Temporal.PlainDate-like value (option)', async () => {
@@ -1399,24 +1451,25 @@ describe('ReactDOMSelect', () => {
1451
);
1452
});
1453
1402
- await expect(async () => {
1403
- await expect(
1404
- act(() => {
1405
- root.render(
1406
- <select onChange={noop} value="2020-01-01">
1407
- <option value={new TemporalLike()}>
1408
- like a Temporal.PlainDate
1409
- </option>
1410
- <option value="monkey">A monkey!</option>
1411
- <option value="giraffe">A giraffe!</option>
1412
- </select>,
1413
- );
1414
- }),
1415
- ).rejects.toThrowError(new TypeError('prod message'));
1416
- }).toErrorDev(
1454
+ await expect(
1455
+ act(() => {
1456
+ root.render(
1457
+ <select onChange={noop} value="2020-01-01">
1458
+ <option value={new TemporalLike()}>
1459
+ like a Temporal.PlainDate
1460
+ </option>
1461
+ <option value="monkey">A monkey!</option>
1462
+ <option value="giraffe">A giraffe!</option>
1463
+ </select>,
1464
+ );
1465
+ }),
1466
+ ).rejects.toThrowError(new TypeError('prod message'));
1467
+ assertConsoleErrorDev([
1468
'The provided `value` attribute is an unsupported type TemporalLike.' +
1418
- ' This value must be coerced to a string before using it here.',
1419
- );
1469
+ ' This value must be coerced to a string before using it here.\n' +
1470
+ ' in option (at **)' +
1471
+ (gate('enableOwnerStacks') ? '' : '\n in select (at **)'),
1472
+ ]);
1473
});
1474
1475
it('throws with updated Temporal.PlainDate-like value (both)', async () => {
@@ -1433,57 +1486,60 @@ describe('ReactDOMSelect', () => {
1486
);
1487
});
1488
1436
- await expect(async () => {
1437
- await expect(
1438
- act(() => {
1439
- root.render(
1440
- <select onChange={noop} value={new TemporalLike()}>
1441
- <option value={new TemporalLike()}>
1442
- like a Temporal.PlainDate
1443
- </option>
1444
- <option value="monkey">A monkey!</option>
1445
- <option value="giraffe">A giraffe!</option>
1446
- </select>,
1447
- );
1448
- }),
1449
- ).rejects.toThrowError(
1450
- // eslint-disable-next-line no-undef
1451
- new AggregateError([
1452
- new TypeError('prod message'),
1453
- new TypeError('prod message'),
1454
- ]),
1455
- );
1456
- }).toErrorDev([
1489
+ await expect(
1490
+ act(() => {
1491
+ root.render(
1492
+ <select onChange={noop} value={new TemporalLike()}>
1493
+ <option value={new TemporalLike()}>
1494
+ like a Temporal.PlainDate
1495
+ </option>
1496
+ <option value="monkey">A monkey!</option>
1497
+ <option value="giraffe">A giraffe!</option>
1498
+ </select>,
1499
+ );
1500
+ }),
1501
+ ).rejects.toThrowError(
1502
+ // eslint-disable-next-line no-undef
1503
+ new AggregateError([
1504
+ new TypeError('prod message'),
1505
+ new TypeError('prod message'),
1506
+ ]),
1507
+ );
1508
+ assertConsoleErrorDev([
1509
'The provided `value` attribute is an unsupported type TemporalLike.' +
1458
- ' This value must be coerced to a string before using it here.',
1510
+ ' This value must be coerced to a string before using it here.\n' +
1511
+ ' in option (at **)' +
1512
+ (gate('enableOwnerStacks') ? '' : '\n in select (at **)'),
1513
'Form field values (value, checked, defaultValue, or defaultChecked props)' +
1514
' must be strings, not TemporalLike. ' +
1461
- 'This value must be coerced to a string before using it here.',
1515
+ 'This value must be coerced to a string before using it here.\n' +
1516
+ ' in select (at **)',
1517
]);
1518
});
1519
1520
it('throws when given a Temporal.PlainDate-like defaultValue (select)', async () => {
1521
const container = document.createElement('div');
1522
const root = ReactDOMClient.createRoot(container);
1468
- await expect(async () => {
1469
- await expect(
1470
- act(() => {
1471
- root.render(
1472
- <select onChange={noop} defaultValue={new TemporalLike()}>
1473
- <option value="2020-01-01">like a Temporal.PlainDate</option>
1474
- <option value="monkey">A monkey!</option>
1475
- <option value="giraffe">A giraffe!</option>
1476
- </select>,
1477
- );
1478
- }),
1479
- ).rejects.toThrowError(new TypeError('prod message'));
1480
- }).toErrorDev([
1523
+ await expect(
1524
+ act(() => {
1525
+ root.render(
1526
+ <select onChange={noop} defaultValue={new TemporalLike()}>
1527
+ <option value="2020-01-01">like a Temporal.PlainDate</option>
1528
+ <option value="monkey">A monkey!</option>
1529
+ <option value="giraffe">A giraffe!</option>
1530
+ </select>,
1531
+ );
1532
+ }),
1533
+ ).rejects.toThrowError(new TypeError('prod message'));
1534
+ assertConsoleErrorDev([
1535
'Form field values (value, checked, defaultValue, or defaultChecked props)' +
1536
' must be strings, not TemporalLike. ' +
1483
- 'This value must be coerced to a string before using it here.',
1537
+ 'This value must be coerced to a string before using it here.\n' +
1538
+ ' in select (at **)',
1539
'Form field values (value, checked, defaultValue, or defaultChecked props)' +
1540
' must be strings, not TemporalLike. ' +
1486
- 'This value must be coerced to a string before using it here.',
1541
+ 'This value must be coerced to a string before using it here.\n' +
1542
+ ' in select (at **)',
1543
]);
1544
});
1545
@@ -1491,50 +1547,56 @@ describe('ReactDOMSelect', () => {
1547
const container = document.createElement('div');
1548
const root = ReactDOMClient.createRoot(container);
1549
1494
- await expect(async () => {
1495
- await expect(
1496
- act(() => {
1497
- root.render(
1498
- <select onChange={noop} defaultValue="2020-01-01">
1499
- <option value={new TemporalLike()}>
1500
- like a Temporal.PlainDate
1501
- </option>
1502
- <option value="monkey">A monkey!</option>
1503
- <option value="giraffe">A giraffe!</option>
1504
- </select>,
1505
- );
1506
- }),
1507
- ).rejects.toThrowError(new TypeError('prod message'));
1508
- }).toErrorDev([
1550
+ await expect(
1551
+ act(() => {
1552
+ root.render(
1553
+ <select onChange={noop} defaultValue="2020-01-01">
1554
+ <option value={new TemporalLike()}>
1555
+ like a Temporal.PlainDate
1556
+ </option>
1557
+ <option value="monkey">A monkey!</option>
1558
+ <option value="giraffe">A giraffe!</option>
1559
+ </select>,
1560
+ );
1561
+ }),
1562
+ ).rejects.toThrowError(new TypeError('prod message'));
1563
+ assertConsoleErrorDev([
1564
'The provided `value` attribute is an unsupported type TemporalLike.' +
1510
- ' This value must be coerced to a string before using it here.',
1565
+ ' This value must be coerced to a string before using it here.\n' +
1566
+ ' in option (at **)' +
1567
+ (gate('enableOwnerStacks') ? '' : '\n in select (at **)'),
1568
'The provided `value` attribute is an unsupported type TemporalLike.' +
1512
- ' This value must be coerced to a string before using it here.',
1569
+ ' This value must be coerced to a string before using it here.\n' +
1570
+ ' in option (at **)' +
1571
+ (gate('enableOwnerStacks') ? '' : '\n in select (at **)'),
1572
]);
1573
});
1574
1575
it('throws when given a Temporal.PlainDate-like defaultValue (both)', async () => {
1576
const container = document.createElement('div');
1577
const root = ReactDOMClient.createRoot(container);
1519
- await expect(async () => {
1520
- await expect(
1521
- act(() => {
1522
- root.render(
1523
- <select onChange={noop} defaultValue={new TemporalLike()}>
1524
- <option value={new TemporalLike()}>
1525
- like a Temporal.PlainDate
1526
- </option>
1527
- <option value="monkey">A monkey!</option>
1528
- <option value="giraffe">A giraffe!</option>
1529
- </select>,
1530
- );
1531
- }),
1532
- ).rejects.toThrowError(new TypeError('prod message'));
1533
- }).toErrorDev([
1578
+ await expect(
1579
+ act(() => {
1580
+ root.render(
1581
+ <select onChange={noop} defaultValue={new TemporalLike()}>
1582
+ <option value={new TemporalLike()}>
1583
+ like a Temporal.PlainDate
1584
+ </option>
1585
+ <option value="monkey">A monkey!</option>
1586
+ <option value="giraffe">A giraffe!</option>
1587
+ </select>,
1588
+ );
1589
+ }),
1590
+ ).rejects.toThrowError(new TypeError('prod message'));
1591
+ assertConsoleErrorDev([
1592
'The provided `value` attribute is an unsupported type TemporalLike.' +
1535
- ' This value must be coerced to a string before using it here.',
1593
+ ' This value must be coerced to a string before using it here.\n' +
1594
+ ' in option (at **)' +
1595
+ (gate('enableOwnerStacks') ? '' : '\n in select (at **)'),
1596
'The provided `value` attribute is an unsupported type TemporalLike.' +
1537
- ' This value must be coerced to a string before using it here.',
1597
+ ' This value must be coerced to a string before using it here.\n' +
1598
+ ' in option (at **)' +
1599
+ (gate('enableOwnerStacks') ? '' : '\n in select (at **)'),
1600
]);
1601
});
1602
@@ -1553,25 +1615,26 @@ describe('ReactDOMSelect', () => {
1615
1616
container = document.createElement('div');
1617
root = ReactDOMClient.createRoot(container);
1556
- await expect(async () => {
1557
- await expect(
1558
- act(() => {
1559
- root.render(
1560
- <select onChange={noop} defaultValue={new TemporalLike()}>
1561
- <option value="2020-01-01">like a Temporal.PlainDate</option>
1562
- <option value="monkey">A monkey!</option>
1563
- <option value="giraffe">A giraffe!</option>
1564
- </select>,
1565
- );
1566
- }),
1567
- ).rejects.toThrowError(new TypeError('prod message'));
1568
- }).toErrorDev([
1618
+ await expect(
1619
+ act(() => {
1620
+ root.render(
1621
+ <select onChange={noop} defaultValue={new TemporalLike()}>
1622
+ <option value="2020-01-01">like a Temporal.PlainDate</option>
1623
+ <option value="monkey">A monkey!</option>
1624
+ <option value="giraffe">A giraffe!</option>
1625
+ </select>,
1626
+ );
1627
+ }),
1628
+ ).rejects.toThrowError(new TypeError('prod message'));
1629
+ assertConsoleErrorDev([
1630
'Form field values (value, checked, defaultValue, or defaultChecked props)' +
1631
' must be strings, not TemporalLike. ' +
1571
- 'This value must be coerced to a string before using it here.',
1632
+ 'This value must be coerced to a string before using it here.\n' +
1633
+ ' in select (at **)',
1634
'Form field values (value, checked, defaultValue, or defaultChecked props)' +
1635
' must be strings, not TemporalLike. ' +
1574
- 'This value must be coerced to a string before using it here.',
1636
+ 'This value must be coerced to a string before using it here.\n' +
1637
+ ' in select (at **)',
1638
]);
1639
});
1640
@@ -1591,25 +1654,28 @@ describe('ReactDOMSelect', () => {
1654
1655
container = document.createElement('div');
1656
root = ReactDOMClient.createRoot(container);
1594
- await expect(async () => {
1595
- await expect(
1596
- act(() => {
1597
- root.render(
1598
- <select onChange={noop} value={new TemporalLike()}>
1599
- <option value={new TemporalLike()}>
1600
- like a Temporal.PlainDate
1601
- </option>
1602
- <option value="monkey">A monkey!</option>
1603
- <option value="giraffe">A giraffe!</option>
1604
- </select>,
1605
- );
1606
- }),
1607
- ).rejects.toThrowError(new TypeError('prod message'));
1608
- }).toErrorDev([
1657
+ await expect(
1658
+ act(() => {
1659
+ root.render(
1660
+ <select onChange={noop} value={new TemporalLike()}>
1661
+ <option value={new TemporalLike()}>
1662
+ like a Temporal.PlainDate
1663
+ </option>
1664
+ <option value="monkey">A monkey!</option>
1665
+ <option value="giraffe">A giraffe!</option>
1666
+ </select>,
1667
+ );
1668
+ }),
1669
+ ).rejects.toThrowError(new TypeError('prod message'));
1670
+ assertConsoleErrorDev([
1671
'The provided `value` attribute is an unsupported type TemporalLike.' +
1610
- ' This value must be coerced to a string before using it here.',
1672
+ ' This value must be coerced to a string before using it here.\n' +
1673
+ ' in option (at **)' +
1674
+ (gate('enableOwnerStacks') ? '' : '\n in select (at **)'),
1675
'The provided `value` attribute is an unsupported type TemporalLike.' +
1612
- ' This value must be coerced to a string before using it here.',
1676
+ ' This value must be coerced to a string before using it here.\n' +
1677
+ ' in option (at **)' +
1678
+ (gate('enableOwnerStacks') ? '' : '\n in select (at **)'),
1679
]);
1680
});
1681
@@ -1681,85 +1747,84 @@ describe('ReactDOMSelect', () => {
1747
it('should warn about missing onChange if value is false', async () => {
1748
const container = document.createElement('div');
1749
const root = ReactDOMClient.createRoot(container);
1684
- await expect(async () => {
1685
- await act(() => {
1686
- root.render(
1687
- <select value={false}>
1688
- <option value="monkey">A monkey!</option>
1689
- <option value="giraffe">A giraffe!</option>
1690
- <option value="gorilla">A gorilla!</option>
1691
- </select>,
1692
- );
1693
- });
1694
- }).toErrorDev(
1750
+ await act(() => {
1751
+ root.render(
1752
+ <select value={false}>
1753
+ <option value="monkey">A monkey!</option>
1754
+ <option value="giraffe">A giraffe!</option>
1755
+ <option value="gorilla">A gorilla!</option>
1756
+ </select>,
1757
+ );
1758
+ });
1759
+ assertConsoleErrorDev([
1760
'You provided a `value` prop to a form ' +
1761
'field without an `onChange` handler. This will render a read-only ' +
1762
'field. If the field should be mutable use `defaultValue`. ' +
1698
- 'Otherwise, set `onChange`.',
1699
- );
1763
+ 'Otherwise, set `onChange`.\n in select (at **)',
1764
+ ]);
1765
});
1766
1767
it('should warn about missing onChange if value is 0', async () => {
1768
const container = document.createElement('div');
1769
const root = ReactDOMClient.createRoot(container);
1705
- await expect(async () => {
1706
- await act(() => {
1707
- root.render(
1708
- <select value={0}>
1709
- <option value="monkey">A monkey!</option>
1710
- <option value="giraffe">A giraffe!</option>
1711
- <option value="gorilla">A gorilla!</option>
1712
- </select>,
1713
- );
1714
- });
1715
- }).toErrorDev(
1770
+ await act(() => {
1771
+ root.render(
1772
+ <select value={0}>
1773
+ <option value="monkey">A monkey!</option>
1774
+ <option value="giraffe">A giraffe!</option>
1775
+ <option value="gorilla">A gorilla!</option>
1776
+ </select>,
1777
+ );
1778
+ });
1779
+ assertConsoleErrorDev([
1780
'You provided a `value` prop to a form ' +
1781
'field without an `onChange` handler. This will render a read-only ' +
1782
'field. If the field should be mutable use `defaultValue`. ' +
1719
- 'Otherwise, set `onChange`.',
1720
- );
1783
+ 'Otherwise, set `onChange`.\n' +
1784
+ ' in select (at **)',
1785
+ ]);
1786
});
1787
1788
it('should warn about missing onChange if value is "0"', async () => {
1789
const container = document.createElement('div');
1790
const root = ReactDOMClient.createRoot(container);
1726
- await expect(async () => {
1727
- await act(() => {
1728
- root.render(
1729
- <select value="0">
1730
- <option value="monkey">A monkey!</option>
1731
- <option value="giraffe">A giraffe!</option>
1732
- <option value="gorilla">A gorilla!</option>
1733
- </select>,
1734
- );
1735
- });
1736
- }).toErrorDev(
1791
+ await act(() => {
1792
+ root.render(
1793
+ <select value="0">
1794
+ <option value="monkey">A monkey!</option>
1795
+ <option value="giraffe">A giraffe!</option>
1796
+ <option value="gorilla">A gorilla!</option>
1797
+ </select>,
1798
+ );
1799
+ });
1800
+ assertConsoleErrorDev([
1801
'You provided a `value` prop to a form ' +
1802
'field without an `onChange` handler. This will render a read-only ' +
1803
'field. If the field should be mutable use `defaultValue`. ' +
1740
- 'Otherwise, set `onChange`.',
1741
- );
1804
+ 'Otherwise, set `onChange`.\n' +
1805
+ ' in select (at **)',
1806
+ ]);
1807
});
1808
1809
it('should warn about missing onChange if value is ""', async () => {
1810
const container = document.createElement('div');
1811
const root = ReactDOMClient.createRoot(container);
1747
- await expect(async () => {
1748
- await act(() => {
1749
- root.render(
1750
- <select value="">
1751
- <option value="monkey">A monkey!</option>
1752
- <option value="giraffe">A giraffe!</option>
1753
- <option value="gorilla">A gorilla!</option>
1754
- </select>,
1755
- );
1756
- });
1757
- }).toErrorDev(
1812
+ await act(() => {
1813
+ root.render(
1814
+ <select value="">
1815
+ <option value="monkey">A monkey!</option>
1816
+ <option value="giraffe">A giraffe!</option>
1817
+ <option value="gorilla">A gorilla!</option>
1818
+ </select>,
1819
+ );
1820
+ });
1821
+ assertConsoleErrorDev([
1822
'You provided a `value` prop to a form ' +
1823
'field without an `onChange` handler. This will render a read-only ' +
1824
'field. If the field should be mutable use `defaultValue`. ' +
1761
- 'Otherwise, set `onChange`.',
1762
- );
1825
+ 'Otherwise, set `onChange`.\n' +
1826
+ ' in select (at **)',
1827
+ ]);
1828
});
1829
});
1830
});
packages/react-dom/src/__tests__/ReactDOMServerLifecycles-test.js
+65
-28
@@ -13,12 +13,16 @@ const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegratio
13
14
let React;
15
let ReactDOMServer;
16
+let assertConsoleErrorDev;
17
+let assertConsoleWarnDev;
18
19
function initModules() {
20
// Reset warning cache.
21
jest.resetModules();
22
React = require('react');
23
ReactDOMServer = require('react-dom/server');
24
+ assertConsoleErrorDev = require('internal-test-utils').assertConsoleErrorDev;
25
+ assertConsoleWarnDev = require('internal-test-utils').assertConsoleWarnDev;
26
27
// Make them available to the helpers.
28
return {
@@ -115,9 +119,17 @@ describe('ReactDOMServerLifecycles', () => {
119
}
120
}
121
118
- expect(() => ReactDOMServer.renderToString(<Component />)).toErrorDev(
119
- 'Unsafe legacy lifecycles will not be called for components using new component APIs.',
120
- );
122
+ ReactDOMServer.renderToString(<Component />);
123
+ assertConsoleErrorDev([
124
+ 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n' +
125
+ '\n' +
126
+ 'Component uses getDerivedStateFromProps() but also contains the following legacy lifecycles:\n' +
127
+ ' UNSAFE_componentWillMount\n' +
128
+ '\n' +
129
+ 'The above lifecycles should be removed. Learn more about this warning here:\n' +
130
+ 'https://react.dev/link/unsafe-component-lifecycles\n' +
131
+ ' in Component (at **)',
132
+ ]);
133
});
134
135
it('should update instance.state with value returned from getDerivedStateFromProps', () => {
@@ -182,10 +194,12 @@ describe('ReactDOMServerLifecycles', () => {
194
}
195
}
196
185
- expect(() => ReactDOMServer.renderToString(<Component />)).toErrorDev(
197
+ ReactDOMServer.renderToString(<Component />);
198
+ assertConsoleErrorDev([
199
'Component.getDerivedStateFromProps(): A valid state object (or null) must ' +
187
- 'be returned. You have returned undefined.',
188
- );
200
+ 'be returned. You have returned undefined.\n' +
201
+ ' in Component (at **)',
202
+ ]);
203
204
// De-duped
205
ReactDOMServer.renderToString(<Component />);
@@ -201,12 +215,14 @@ describe('ReactDOMServerLifecycles', () => {
215
}
216
}
217
204
- expect(() => ReactDOMServer.renderToString(<Component />)).toErrorDev(
218
+ ReactDOMServer.renderToString(<Component />);
219
+ assertConsoleErrorDev([
220
'`Component` uses `getDerivedStateFromProps` but its initial state is ' +
221
'undefined. This is not recommended. Instead, define the initial state by ' +
222
'assigning an object to `this.state` in the constructor of `Component`. ' +
208
- 'This ensures that `getDerivedStateFromProps` arguments have a consistent shape.',
209
- );
223
+ 'This ensures that `getDerivedStateFromProps` arguments have a consistent shape.\n' +
224
+ ' in Component (at **)',
225
+ ]);
226
227
// De-duped
228
ReactDOMServer.renderToString(<Component />);
@@ -227,9 +243,16 @@ describe('ReactDOMServerLifecycles', () => {
243
}
244
}
245
230
- expect(() => ReactDOMServer.renderToString(<Component />)).toWarnDev(
231
- 'componentWillMount has been renamed',
232
- );
246
+ ReactDOMServer.renderToString(<Component />);
247
+ assertConsoleWarnDev([
248
+ 'componentWillMount has been renamed, and is not recommended for use. ' +
249
+ 'See https://react.dev/link/unsafe-component-lifecycles for details.\n' +
250
+ '\n' +
251
+ '* Move code from componentWillMount to componentDidMount (preferred in most cases) or the constructor.\n' +
252
+ '\n' +
253
+ 'Please update the following components: Component\n' +
254
+ ' in Component (at **)',
255
+ ]);
256
expect(log).toEqual(['componentWillMount', 'UNSAFE_componentWillMount']);
257
});
258
@@ -254,17 +277,18 @@ describe('ReactDOMServerLifecycles', () => {
277
return <div>{this.props.children + '-' + this.state.x}</div>;
278
}
279
}
257
- expect(() => {
258
- // Shouldn't be 1-3.
259
- expect(ReactDOMServer.renderToStaticMarkup(<Outer />)).toBe(
260
- '<div>1-2</div>',
261
- );
262
- }).toErrorDev(
280
+ // Shouldn't be 1-3.
281
+ expect(ReactDOMServer.renderToStaticMarkup(<Outer />)).toBe(
282
+ '<div>1-2</div>',
283
+ );
284
+ assertConsoleErrorDev([
285
'Can only update a mounting component. This ' +
286
'usually means you called setState() outside componentWillMount() on ' +
287
'the server. This is a no-op.\n\n' +
266
- 'Please check the code for the Outer component.',
267
- );
288
+ 'Please check the code for the Outer component.\n' +
289
+ (gate('enableOwnerStacks') ? '' : ' in Inner (at **)\n') +
290
+ ' in Outer (at **)',
291
+ ]);
292
});
293
294
it('should not invoke cWM if static gDSFP is present', () => {
@@ -281,9 +305,17 @@ describe('ReactDOMServerLifecycles', () => {
305
}
306
}
307
284
- expect(() => ReactDOMServer.renderToString(<Component />)).toErrorDev(
285
- 'Unsafe legacy lifecycles will not be called for components using new component APIs.',
286
- );
308
+ ReactDOMServer.renderToString(<Component />);
309
+ assertConsoleErrorDev([
310
+ 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n' +
311
+ '\n' +
312
+ 'Component uses getDerivedStateFromProps() but also contains the following legacy lifecycles:\n' +
313
+ ' componentWillMount\n' +
314
+ '\n' +
315
+ 'The above lifecycles should be removed. Learn more about this warning here:\n' +
316
+ 'https://react.dev/link/unsafe-component-lifecycles\n' +
317
+ ' in Component (at **)',
318
+ ]);
319
});
320
321
it('should warn about deprecated lifecycle hooks', () => {
@@ -294,11 +326,16 @@ describe('ReactDOMServerLifecycles', () => {
326
}
327
}
328
297
- expect(() => ReactDOMServer.renderToString(<MyComponent />)).toWarnDev(
298
- 'componentWillMount has been renamed, and is not recommended for use. See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' +
299
- '* Move code from componentWillMount to componentDidMount (preferred in most cases) or the constructor.\n\n' +
300
- 'Please update the following components: MyComponent',
301
- );
329
+ ReactDOMServer.renderToString(<MyComponent />);
330
+ assertConsoleWarnDev([
331
+ 'componentWillMount has been renamed, and is not recommended for use. ' +
332
+ 'See https://react.dev/link/unsafe-component-lifecycles for details.\n' +
333
+ '\n' +
334
+ '* Move code from componentWillMount to componentDidMount (preferred in most cases) or the constructor.\n' +
335
+ '\n' +
336
+ 'Please update the following components: MyComponent\n' +
337
+ ' in MyComponent (at **)',
338
+ ]);
339
340
// De-duped
341
ReactDOMServer.renderToString(<MyComponent />);
packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+14
-3
@@ -26,6 +26,7 @@ let waitForAll;
26
let waitFor;
27
let waitForPaint;
28
let assertLog;
29
+let assertConsoleErrorDev;
30
31
function normalizeError(msg) {
32
// Take the first sentence to make it easier to assert on.
@@ -124,6 +125,7 @@ describe('ReactDOMServerPartialHydration', () => {
125
assertLog = InternalTestUtils.assertLog;
126
waitForPaint = InternalTestUtils.waitForPaint;
127
waitFor = InternalTestUtils.waitFor;
128
+ assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev;
129
130
IdleEventPriority = require('react-reconciler/constants').IdleEventPriority;
131
});
@@ -1916,9 +1918,18 @@ describe('ReactDOMServerPartialHydration', () => {
1918
1919
// While we're part way through the hydration, we update the state.
1920
// This will schedule an update on the children of the suspense boundary.
1919
- expect(() => updateText('Hi')).toErrorDev(
1920
- "Can't perform a React state update on a component that hasn't mounted yet.",
1921
- );
1921
+ updateText('Hi');
1922
+ assertConsoleErrorDev([
1923
+ "Can't perform a React state update on a component that hasn't mounted yet. " +
1924
+ 'This indicates that you have a side-effect in your render function that ' +
1925
+ 'asynchronously later calls tries to update the component. Move this work to useEffect instead.\n' +
1926
+ (gate('enableOwnerStacks')
1927
+ ? ''
1928
+ : ' in Child (at **)\n' +
1929
+ ' in Suspense (at **)\n' +
1930
+ ' in div (at **)\n') +
1931
+ ' in App (at **)',
1932
+ ]);
1933
1934
// This will throw it away and rerender.
1935
await waitForAll(['Child']);
packages/react-dom/src/__tests__/ReactDOMShorthandCSSPropertyCollision-test.js
+45
-50
@@ -14,6 +14,7 @@ describe('ReactDOMShorthandCSSPropertyCollision', () => {
14
15
let React;
16
let ReactDOMClient;
17
+ let assertConsoleErrorDev;
18
19
beforeEach(() => {
20
jest.resetModules();
@@ -21,6 +22,8 @@ describe('ReactDOMShorthandCSSPropertyCollision', () => {
22
act = require('internal-test-utils').act;
23
React = require('react');
24
ReactDOMClient = require('react-dom/client');
25
+ assertConsoleErrorDev =
26
+ require('internal-test-utils').assertConsoleErrorDev;
27
});
28
29
it('should warn for conflicting CSS shorthand updates', async () => {
@@ -29,18 +32,17 @@ describe('ReactDOMShorthandCSSPropertyCollision', () => {
32
await act(() => {
33
root.render(<div style={{font: 'foo', fontStyle: 'bar'}} />);
34
});
32
- await expect(async () => {
33
- await act(() => {
34
- root.render(<div style={{font: 'foo'}} />);
35
- });
36
- }).toErrorDev(
35
+ await act(() => {
36
+ root.render(<div style={{font: 'foo'}} />);
37
+ });
38
+ assertConsoleErrorDev([
39
'Removing a style property during rerender (fontStyle) ' +
40
'when a conflicting property is set (font) can lead to styling ' +
41
"bugs. To avoid this, don't mix shorthand and non-shorthand " +
42
'properties for the same value; instead, replace the shorthand ' +
43
'with separate values.' +
44
'\n in div (at **)',
43
- );
45
+ ]);
46
47
// These updates are OK and don't warn:
48
await act(() => {
@@ -50,30 +52,28 @@ describe('ReactDOMShorthandCSSPropertyCollision', () => {
52
root.render(<div style={{font: 'foo', fontStyle: 'baz'}} />);
53
});
54
53
- await expect(async () => {
54
- await act(() => {
55
- root.render(<div style={{font: 'qux', fontStyle: 'baz'}} />);
56
- });
57
- }).toErrorDev(
55
+ await act(() => {
56
+ root.render(<div style={{font: 'qux', fontStyle: 'baz'}} />);
57
+ });
58
+ assertConsoleErrorDev([
59
'Updating a style property during rerender (font) when ' +
60
'a conflicting property is set (fontStyle) can lead to styling ' +
61
"bugs. To avoid this, don't mix shorthand and non-shorthand " +
62
'properties for the same value; instead, replace the shorthand ' +
63
'with separate values.' +
64
'\n in div (at **)',
64
- );
65
- await expect(async () => {
66
- await act(() => {
67
- root.render(<div style={{fontStyle: 'baz'}} />);
68
- });
69
- }).toErrorDev(
65
+ ]);
66
+ await act(() => {
67
+ root.render(<div style={{fontStyle: 'baz'}} />);
68
+ });
69
+ assertConsoleErrorDev([
70
'Removing a style property during rerender (font) when ' +
71
'a conflicting property is set (fontStyle) can lead to styling ' +
72
"bugs. To avoid this, don't mix shorthand and non-shorthand " +
73
'properties for the same value; instead, replace the shorthand ' +
74
'with separate values.' +
75
'\n in div (at **)',
76
- );
76
+ ]);
77
78
// A bit of a special case: backgroundPosition isn't technically longhand
79
// (it expands to backgroundPosition{X,Y} but so does background)
@@ -82,18 +82,17 @@ describe('ReactDOMShorthandCSSPropertyCollision', () => {
82
<div style={{background: 'yellow', backgroundPosition: 'center'}} />,
83
);
84
});
85
- await expect(async () => {
86
- await act(() => {
87
- root.render(<div style={{background: 'yellow'}} />);
88
- });
89
- }).toErrorDev(
85
+ await act(() => {
86
+ root.render(<div style={{background: 'yellow'}} />);
87
+ });
88
+ assertConsoleErrorDev([
89
'Removing a style property during rerender ' +
90
'(backgroundPosition) when a conflicting property is set ' +
91
"(background) can lead to styling bugs. To avoid this, don't mix " +
92
'shorthand and non-shorthand properties for the same value; ' +
93
'instead, replace the shorthand with separate values.' +
94
'\n in div (at **)',
96
- );
95
+ ]);
96
await act(() => {
97
root.render(
98
<div style={{background: 'yellow', backgroundPosition: 'center'}} />,
@@ -105,18 +104,17 @@ describe('ReactDOMShorthandCSSPropertyCollision', () => {
104
<div style={{background: 'green', backgroundPosition: 'top'}} />,
105
);
106
});
108
- await expect(async () => {
109
- await act(() => {
110
- root.render(<div style={{backgroundPosition: 'top'}} />);
111
- });
112
- }).toErrorDev(
107
+ await act(() => {
108
+ root.render(<div style={{backgroundPosition: 'top'}} />);
109
+ });
110
+ assertConsoleErrorDev([
111
'Removing a style property during rerender (background) ' +
112
'when a conflicting property is set (backgroundPosition) can lead ' +
113
"to styling bugs. To avoid this, don't mix shorthand and " +
114
'non-shorthand properties for the same value; instead, replace the ' +
115
'shorthand with separate values.' +
116
'\n in div (at **)',
119
- );
117
+ ]);
118
119
// A bit of an even more special case: borderLeft and borderStyle overlap.
120
await act(() => {
@@ -124,49 +122,46 @@ describe('ReactDOMShorthandCSSPropertyCollision', () => {
122
<div style={{borderStyle: 'dotted', borderLeft: '1px solid red'}} />,
123
);
124
});
127
- await expect(async () => {
128
- await act(() => {
129
- root.render(<div style={{borderLeft: '1px solid red'}} />);
130
- });
131
- }).toErrorDev(
125
+ await act(() => {
126
+ root.render(<div style={{borderLeft: '1px solid red'}} />);
127
+ });
128
+ assertConsoleErrorDev([
129
'Removing a style property during rerender (borderStyle) ' +
130
'when a conflicting property is set (borderLeft) can lead to ' +
131
"styling bugs. To avoid this, don't mix shorthand and " +
132
'non-shorthand properties for the same value; instead, replace the ' +
133
'shorthand with separate values.' +
134
'\n in div (at **)',
138
- );
139
- await expect(async () => {
140
- await act(() => {
141
- root.render(
142
- <div style={{borderStyle: 'dashed', borderLeft: '1px solid red'}} />,
143
- );
144
- });
145
- }).toErrorDev(
135
+ ]);
136
+ await act(() => {
137
+ root.render(
138
+ <div style={{borderStyle: 'dashed', borderLeft: '1px solid red'}} />,
139
+ );
140
+ });
141
+ assertConsoleErrorDev([
142
'Updating a style property during rerender (borderStyle) ' +
143
'when a conflicting property is set (borderLeft) can lead to ' +
144
"styling bugs. To avoid this, don't mix shorthand and " +
145
'non-shorthand properties for the same value; instead, replace the ' +
146
'shorthand with separate values.' +
147
'\n in div (at **)',
152
- );
148
+ ]);
149
// But setting them at the same time is OK:
150
await act(() => {
151
root.render(
152
<div style={{borderStyle: 'dotted', borderLeft: '2px solid red'}} />,
153
);
154
});
159
- await expect(async () => {
160
- await act(() => {
161
- root.render(<div style={{borderStyle: 'dotted'}} />);
162
- });
163
- }).toErrorDev(
155
+ await act(() => {
156
+ root.render(<div style={{borderStyle: 'dotted'}} />);
157
+ });
158
+ assertConsoleErrorDev([
159
'Removing a style property during rerender (borderLeft) ' +
160
'when a conflicting property is set (borderStyle) can lead to ' +
161
"styling bugs. To avoid this, don't mix shorthand and " +
162
'non-shorthand properties for the same value; instead, replace the ' +
163
'shorthand with separate values.' +
164
'\n in div (at **)',
170
- );
165
+ ]);
166
});
167
});
packages/react-dom/src/__tests__/ReactDOMSingletonComponents-test.js
+37
-9
@@ -23,6 +23,7 @@ let buffer = '';
23
let hasErrored = false;
24
let fatalError = undefined;
25
let waitForAll;
26
+let assertConsoleErrorDev;
27
28
function normalizeError(msg) {
29
// Take the first sentence to make it easier to assert on.
@@ -45,6 +46,7 @@ describe('ReactDOM HostSingleton', () => {
46
47
const InternalTestUtils = require('internal-test-utils');
48
waitForAll = InternalTestUtils.waitForAll;
49
+ assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev;
50
51
// Test Environment
52
const jsdom = new JSDOM(
@@ -162,11 +164,16 @@ describe('ReactDOM HostSingleton', () => {
164
<body />
165
</html>,
166
);
165
- await expect(async () => {
166
- await waitForAll([]);
167
- }).toErrorDev(
168
- 'You are mounting a new head component when a previous one has not first unmounted. It is an error to render more than one head component at a time and attributes and children of these components will likely fail in unpredictable ways. Please only render a single instance of <head> and if you need to mount a new one, ensure any previous ones have unmounted first',
169
- );
167
+ await waitForAll([]);
168
+ assertConsoleErrorDev([
169
+ 'You are mounting a new head component when a previous one has not first unmounted. ' +
170
+ 'It is an error to render more than one head component at a time and attributes and ' +
171
+ 'children of these components will likely fail in unpredictable ways. ' +
172
+ 'Please only render a single instance of <head> and if you need to mount a new one, ' +
173
+ 'ensure any previous ones have unmounted first.\n' +
174
+ ' in head (at **)' +
175
+ (gate('enableOwnerStacks') ? '' : '\n in html (at **)'),
176
+ ]);
177
expect(getVisibleChildren(document)).toEqual(
178
<html>
179
<head lang="es" data-foo="foo">
@@ -540,10 +547,31 @@ describe('ReactDOM HostSingleton', () => {
547
},
548
);
549
expect(hydrationErrors).toEqual([]);
543
- await expect(async () => {
544
- await waitForAll([]);
545
- }).toErrorDev(
546
- "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.",
550
+ await waitForAll([]);
551
+ assertConsoleErrorDev(
552
+ [
553
+ "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. " +
554
+ "This won't be patched up. This can happen if a SSR-ed Client Component used:\n" +
555
+ '\n' +
556
+ "- A server/client branch `if (typeof window !== 'undefined')`.\n" +
557
+ "- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" +
558
+ "- Date formatting in a user's locale which doesn't match the server.\n" +
559
+ '- External changing data without sending a snapshot of it along with the HTML.\n' +
560
+ '- Invalid HTML tag nesting.\n\nIt can also happen if the client has a browser extension installed ' +
561
+ 'which messes with the HTML before React loaded.\n' +
562
+ '\n' +
563
+ 'https://react.dev/link/hydration-mismatch\n' +
564
+ '\n' +
565
+ ' <html\n' +
566
+ '+ data-client-foo="foo"\n' +
567
+ '- data-client-foo={null}\n' +
568
+ ' >\n' +
569
+ ' <head>\n' +
570
+ ' <body\n' +
571
+ '+ data-client-baz="baz"\n' +
572
+ '- data-client-baz={null}\n' +
573
+ ' >\n',
574
+ ],
575
{withoutStack: true},
576
);
577
expect(persistentElements).toEqual([
packages/react-dom/src/__tests__/ReactDOMTextarea-test.js
+202
-181
@@ -16,6 +16,7 @@ describe('ReactDOMTextarea', () => {
16
let ReactDOMClient;
17
let ReactDOMServer;
18
let act;
19
+ let assertConsoleErrorDev;
20
21
let renderTextarea;
22
@@ -28,6 +29,8 @@ describe('ReactDOMTextarea', () => {
29
ReactDOMClient = require('react-dom/client');
30
ReactDOMServer = require('react-dom/server');
31
act = require('internal-test-utils').act;
32
+ assertConsoleErrorDev =
33
+ require('internal-test-utils').assertConsoleErrorDev;
34
35
renderTextarea = async function (component, container, root) {
36
await act(() => {
@@ -330,12 +333,12 @@ describe('ReactDOMTextarea', () => {
333
);
334
});
335
};
333
- await expect(() =>
334
- expect(test).rejects.toThrowError(new TypeError('prod message')),
335
- ).toErrorDev(
336
+ await expect(test).rejects.toThrowError(new TypeError('prod message'));
337
+ assertConsoleErrorDev([
338
'Form field values (value, checked, defaultValue, or defaultChecked props) must be ' +
337
- 'strings, not TemporalLike. This value must be coerced to a string before using it here.',
338
- );
339
+ 'strings, not TemporalLike. This value must be coerced to a string before using it here.\n' +
340
+ ' in textarea (at **',
341
+ ]);
342
});
343
344
it('should take updates to `defaultValue` for uncontrolled textarea', async () => {
@@ -437,17 +440,15 @@ describe('ReactDOMTextarea', () => {
440
it('should ignore children content', async () => {
441
const container = document.createElement('div');
442
const root = ReactDOMClient.createRoot(container);
440
- let node;
441
-
442
- await expect(async () => {
443
- node = await renderTextarea(
444
- <textarea>giraffe</textarea>,
445
- container,
446
- root,
447
- );
448
- }).toErrorDev(
449
- 'Use the `defaultValue` or `value` props instead of setting children on <textarea>.',
443
+ const node = await renderTextarea(
444
+ <textarea>giraffe</textarea>,
445
+ container,
446
+ root,
447
);
448
+ assertConsoleErrorDev([
449
+ 'Use the `defaultValue` or `value` props instead of setting children on <textarea>.\n' +
450
+ ' in textarea (at **)',
451
+ ]);
452
expect(node.value).toBe('');
453
454
await act(() => {
@@ -462,17 +463,16 @@ describe('ReactDOMTextarea', () => {
463
it('should receive defaultValue and still ignore children content', async () => {
464
const container = document.createElement('div');
465
const root = ReactDOMClient.createRoot(container);
465
- let node;
466
467
- await expect(async () => {
468
- node = await renderTextarea(
469
- <textarea defaultValue="dragon">monkey</textarea>,
470
- container,
471
- root,
472
- );
473
- }).toErrorDev(
474
- 'Use the `defaultValue` or `value` props instead of setting children on <textarea>.',
467
+ const node = await renderTextarea(
468
+ <textarea defaultValue="dragon">monkey</textarea>,
469
+ container,
470
+ root,
471
);
472
+ assertConsoleErrorDev([
473
+ 'Use the `defaultValue` or `value` props instead of setting children on <textarea>.\n' +
474
+ ' in textarea (at **)',
475
+ ]);
476
expect(node.value).toBe('dragon');
477
});
478
}
@@ -481,17 +481,16 @@ describe('ReactDOMTextarea', () => {
481
it('should treat children like `defaultValue`', async () => {
482
const container = document.createElement('div');
483
const root = ReactDOMClient.createRoot(container);
484
- let node;
484
486
- await expect(async () => {
487
- node = await renderTextarea(
488
- <textarea>giraffe</textarea>,
489
- container,
490
- root,
491
- );
492
- }).toErrorDev(
493
- 'Use the `defaultValue` or `value` props instead of setting children on <textarea>.',
485
+ const node = await renderTextarea(
486
+ <textarea>giraffe</textarea>,
487
+ container,
488
+ root,
489
);
490
+ assertConsoleErrorDev([
491
+ 'Use the `defaultValue` or `value` props instead of setting children on <textarea>.\n' +
492
+ ' in textarea (at **)',
493
+ ]);
494
495
expect(node.value).toBe('giraffe');
496
@@ -549,12 +548,15 @@ describe('ReactDOMTextarea', () => {
548
it('should ignore numbers as children', async () => {
549
const container = document.createElement('div');
550
const root = ReactDOMClient.createRoot(container);
552
- let node;
553
- await expect(async () => {
554
- node = await renderTextarea(<textarea>{17}</textarea>, container, root);
555
- }).toErrorDev(
556
- 'Use the `defaultValue` or `value` props instead of setting children on <textarea>.',
551
+ const node = await renderTextarea(
552
+ <textarea>{17}</textarea>,
553
+ container,
554
+ root,
555
);
556
+ assertConsoleErrorDev([
557
+ 'Use the `defaultValue` or `value` props instead of setting children on <textarea>.\n' +
558
+ ' in textarea (at **)',
559
+ ]);
560
expect(node.value).toBe('');
561
});
562
}
@@ -563,12 +565,15 @@ describe('ReactDOMTextarea', () => {
565
it('should allow numbers as children', async () => {
566
const container = document.createElement('div');
567
const root = ReactDOMClient.createRoot(container);
566
- let node;
567
- await expect(async () => {
568
- node = await renderTextarea(<textarea>{17}</textarea>, container, root);
569
- }).toErrorDev(
570
- 'Use the `defaultValue` or `value` props instead of setting children on <textarea>.',
568
+ const node = await renderTextarea(
569
+ <textarea>{17}</textarea>,
570
+ container,
571
+ root,
572
);
573
+ assertConsoleErrorDev([
574
+ 'Use the `defaultValue` or `value` props instead of setting children on <textarea>.\n' +
575
+ ' in textarea (at **)',
576
+ ]);
577
expect(node.value).toBe('17');
578
});
579
}
@@ -577,16 +582,15 @@ describe('ReactDOMTextarea', () => {
582
it('should ignore booleans as children', async () => {
583
const container = document.createElement('div');
584
const root = ReactDOMClient.createRoot(container);
580
- let node;
581
- await expect(async () => {
582
- node = await renderTextarea(
583
- <textarea>{false}</textarea>,
584
- container,
585
- root,
586
- );
587
- }).toErrorDev(
588
- 'Use the `defaultValue` or `value` props instead of setting children on <textarea>.',
585
+ const node = await renderTextarea(
586
+ <textarea>{false}</textarea>,
587
+ container,
588
+ root,
589
);
590
+ assertConsoleErrorDev([
591
+ 'Use the `defaultValue` or `value` props instead of setting children on <textarea>.\n' +
592
+ ' in textarea (at **)',
593
+ ]);
594
expect(node.value).toBe('');
595
});
596
}
@@ -595,16 +599,15 @@ describe('ReactDOMTextarea', () => {
599
it('should allow booleans as children', async () => {
600
const container = document.createElement('div');
601
const root = ReactDOMClient.createRoot(container);
598
- let node;
599
- await expect(async () => {
600
- node = await renderTextarea(
601
- <textarea>{false}</textarea>,
602
- container,
603
- root,
604
- );
605
- }).toErrorDev(
606
- 'Use the `defaultValue` or `value` props instead of setting children on <textarea>.',
602
+ const node = await renderTextarea(
603
+ <textarea>{false}</textarea>,
604
+ container,
605
+ root,
606
);
607
+ assertConsoleErrorDev([
608
+ 'Use the `defaultValue` or `value` props instead of setting children on <textarea>.\n' +
609
+ ' in textarea (at **)',
610
+ ]);
611
expect(node.value).toBe('false');
612
});
613
}
@@ -618,16 +621,15 @@ describe('ReactDOMTextarea', () => {
621
return 'sharkswithlasers';
622
},
623
};
621
- let node;
622
- await expect(async () => {
623
- node = await renderTextarea(
624
- <textarea>{obj}</textarea>,
625
- container,
626
- root,
627
- );
628
- }).toErrorDev(
629
- 'Use the `defaultValue` or `value` props instead of setting children on <textarea>.',
624
+ const node = await renderTextarea(
625
+ <textarea>{obj}</textarea>,
626
+ container,
627
+ root,
628
);
629
+ assertConsoleErrorDev([
630
+ 'Use the `defaultValue` or `value` props instead of setting children on <textarea>.\n' +
631
+ ' in textarea (at **)',
632
+ ]);
633
expect(node.value).toBe('');
634
});
635
}
@@ -641,16 +643,15 @@ describe('ReactDOMTextarea', () => {
643
return 'sharkswithlasers';
644
},
645
};
644
- let node;
645
- await expect(async () => {
646
- node = await renderTextarea(
647
- <textarea>{obj}</textarea>,
648
- container,
649
- root,
650
- );
651
- }).toErrorDev(
652
- 'Use the `defaultValue` or `value` props instead of setting children on <textarea>.',
646
+ const node = await renderTextarea(
647
+ <textarea>{obj}</textarea>,
648
+ container,
649
+ root,
650
);
651
+ assertConsoleErrorDev([
652
+ 'Use the `defaultValue` or `value` props instead of setting children on <textarea>.\n' +
653
+ ' in textarea (at **)',
654
+ ]);
655
expect(node.value).toBe('sharkswithlasers');
656
});
657
}
@@ -660,35 +661,36 @@ describe('ReactDOMTextarea', () => {
661
const container = document.createElement('div');
662
const root = ReactDOMClient.createRoot(container);
663
await expect(async () => {
663
- await expect(async () => {
664
- await act(() => {
665
- root.render(
666
- <textarea>
667
- {'hello'}
668
- {'there'}
669
- </textarea>,
670
- );
671
- });
672
- }).rejects.toThrow('<textarea> can only have at most one child');
673
- }).toErrorDev([
674
- 'Use the `defaultValue` or `value` props instead of setting children on <textarea>.',
675
- 'Use the `defaultValue` or `value` props instead of setting children on <textarea>.',
664
+ await act(() => {
665
+ root.render(
666
+ <textarea>
667
+ {'hello'}
668
+ {'there'}
669
+ </textarea>,
670
+ );
671
+ });
672
+ }).rejects.toThrow('<textarea> can only have at most one child');
673
+ assertConsoleErrorDev([
674
+ 'Use the `defaultValue` or `value` props instead of setting children on <textarea>.\n' +
675
+ ' in textarea (at **)',
676
+ 'Use the `defaultValue` or `value` props instead of setting children on <textarea>.\n' +
677
+ ' in textarea (at **)',
678
]);
679
680
let node;
679
- await expect(async () => {
680
- await expect(
681
- (async () =>
682
- (node = await renderTextarea(
683
- <textarea>
684
- <strong />
685
- </textarea>,
686
- container,
687
- root,
688
- )))(),
689
- ).resolves.not.toThrow();
690
- }).toErrorDev([
691
- 'Use the `defaultValue` or `value` props instead of setting children on <textarea>.',
681
+ await expect(
682
+ (async () =>
683
+ (node = await renderTextarea(
684
+ <textarea>
685
+ <strong />
686
+ </textarea>,
687
+ container,
688
+ root,
689
+ )))(),
690
+ ).resolves.not.toThrow();
691
+ assertConsoleErrorDev([
692
+ 'Use the `defaultValue` or `value` props instead of setting children on <textarea>.\n' +
693
+ ' in textarea (at **)',
694
]);
695
696
expect(node.value).toBe('[object Object]');
@@ -710,15 +712,15 @@ describe('ReactDOMTextarea', () => {
712
it('should warn if value is null', async () => {
713
const container = document.createElement('div');
714
const root = ReactDOMClient.createRoot(container);
713
- await expect(async () => {
714
- await act(() => {
715
- root.render(<textarea value={null} />);
716
- });
717
- }).toErrorDev(
715
+ await act(() => {
716
+ root.render(<textarea value={null} />);
717
+ });
718
+ assertConsoleErrorDev([
719
'`value` prop on `textarea` should not be null. ' +
720
'Consider using an empty string to clear the component or `undefined` ' +
720
- 'for uncontrolled components.',
721
- );
721
+ 'for uncontrolled components.\n' +
722
+ ' in textarea (at **)',
723
+ ]);
724
725
await act(() => {
726
root.render(<textarea value={null} />);
@@ -731,18 +733,19 @@ describe('ReactDOMTextarea', () => {
733
);
734
let container = document.createElement('div');
735
let root = ReactDOMClient.createRoot(container);
734
- await expect(async () => {
735
- await act(() => {
736
- root.render(<InvalidComponent />);
737
- });
738
- }).toErrorDev(
736
+ await act(() => {
737
+ root.render(<InvalidComponent />);
738
+ });
739
+ assertConsoleErrorDev([
740
'InvalidComponent contains a textarea with both value and defaultValue props. ' +
741
'Textarea elements must be either controlled or uncontrolled ' +
742
'(specify either the value prop, or the defaultValue prop, but not ' +
743
'both). Decide between using a controlled or uncontrolled textarea ' +
744
'and remove one of these props. More info: ' +
744
- 'https://react.dev/link/controlled-components',
745
- );
745
+ 'https://react.dev/link/controlled-components\n' +
746
+ ' in textarea (at **)\n' +
747
+ ' in InvalidComponent (at **)',
748
+ ]);
749
750
container = document.createElement('div');
751
root = ReactDOMClient.createRoot(container);
@@ -819,13 +822,15 @@ describe('ReactDOMTextarea', () => {
822
it('treats initial Symbol value as an empty string', async () => {
823
const container = document.createElement('div');
824
const root = ReactDOMClient.createRoot(container);
822
- await expect(async () => {
823
- await act(() => {
824
- root.render(
825
- <textarea value={Symbol('foobar')} onChange={() => {}} />,
826
- );
827
- });
828
- }).toErrorDev('Invalid value for prop `value`');
825
+ await act(() => {
826
+ root.render(<textarea value={Symbol('foobar')} onChange={() => {}} />);
827
+ });
828
+ assertConsoleErrorDev([
829
+ 'Invalid value for prop `value` on <textarea> tag. ' +
830
+ 'Either remove it from the element, or pass a string or number value to keep it in the DOM. ' +
831
+ 'For details, see https://react.dev/link/attribute-behavior \n' +
832
+ ' in textarea (at **)',
833
+ ]);
834
const node = container.firstChild;
835
836
expect(node.value).toBe('');
@@ -834,11 +839,13 @@ describe('ReactDOMTextarea', () => {
839
it('treats initial Symbol children as an empty string', async () => {
840
const container = document.createElement('div');
841
const root = ReactDOMClient.createRoot(container);
837
- await expect(async () => {
838
- await act(() => {
839
- root.render(<textarea onChange={() => {}}>{Symbol('foo')}</textarea>);
840
- });
841
- }).toErrorDev('Use the `defaultValue` or `value` props');
842
+ await act(() => {
843
+ root.render(<textarea onChange={() => {}}>{Symbol('foo')}</textarea>);
844
+ });
845
+ assertConsoleErrorDev([
846
+ 'Use the `defaultValue` or `value` props instead of setting children on <textarea>.\n' +
847
+ ' in textarea (at **)',
848
+ ]);
849
const node = container.firstChild;
850
851
expect(node.value).toBe('');
@@ -852,11 +859,15 @@ describe('ReactDOMTextarea', () => {
859
root.render(<textarea value="foo" onChange={() => {}} />);
860
});
861
855
- await expect(async () => {
856
- await act(() => {
857
- root.render(<textarea value={Symbol('foo')} onChange={() => {}} />);
858
- });
859
- }).toErrorDev('Invalid value for prop `value`');
862
+ await act(() => {
863
+ root.render(<textarea value={Symbol('foo')} onChange={() => {}} />);
864
+ });
865
+ assertConsoleErrorDev([
866
+ 'Invalid value for prop `value` on <textarea> tag. ' +
867
+ 'Either remove it from the element, or pass a string or number value to keep it in the DOM. ' +
868
+ 'For details, see https://react.dev/link/attribute-behavior \n' +
869
+ ' in textarea (at **)',
870
+ ]);
871
const node = container.firstChild;
872
873
expect(node.value).toBe('');
@@ -897,13 +908,17 @@ describe('ReactDOMTextarea', () => {
908
describe('When given a function value', () => {
909
it('treats initial function value as an empty string', async () => {
910
const container = document.createElement('div');
900
- await expect(async () => {
901
- const root = ReactDOMClient.createRoot(container);
911
+ const root = ReactDOMClient.createRoot(container);
912
903
- await act(() => {
904
- root.render(<textarea value={() => {}} onChange={() => {}} />);
905
- });
906
- }).toErrorDev('Invalid value for prop `value`');
913
+ await act(() => {
914
+ root.render(<textarea value={() => {}} onChange={() => {}} />);
915
+ });
916
+ assertConsoleErrorDev([
917
+ 'Invalid value for prop `value` on <textarea> tag. ' +
918
+ 'Either remove it from the element, or pass a string or number value to keep it in the DOM. ' +
919
+ 'For details, see https://react.dev/link/attribute-behavior \n' +
920
+ ' in textarea (at **)',
921
+ ]);
922
const node = container.firstChild;
923
924
expect(node.value).toBe('');
@@ -911,13 +926,15 @@ describe('ReactDOMTextarea', () => {
926
927
it('treats initial function children as an empty string', async () => {
928
const container = document.createElement('div');
914
- await expect(async () => {
915
- const root = ReactDOMClient.createRoot(container);
929
+ const root = ReactDOMClient.createRoot(container);
930
917
- await act(() => {
918
- root.render(<textarea onChange={() => {}}>{() => {}}</textarea>);
919
- });
920
- }).toErrorDev('Use the `defaultValue` or `value` props');
931
+ await act(() => {
932
+ root.render(<textarea onChange={() => {}}>{() => {}}</textarea>);
933
+ });
934
+ assertConsoleErrorDev([
935
+ 'Use the `defaultValue` or `value` props instead of setting children on <textarea>.\n' +
936
+ ' in textarea (at **)',
937
+ ]);
938
const node = container.firstChild;
939
940
expect(node.value).toBe('');
@@ -931,11 +948,15 @@ describe('ReactDOMTextarea', () => {
948
root.render(<textarea value="foo" onChange={() => {}} />);
949
});
950
934
- await expect(async () => {
935
- await act(() => {
936
- root.render(<textarea value={() => {}} onChange={() => {}} />);
937
- });
938
- }).toErrorDev('Invalid value for prop `value`');
951
+ await act(() => {
952
+ root.render(<textarea value={() => {}} onChange={() => {}} />);
953
+ });
954
+ assertConsoleErrorDev([
955
+ 'Invalid value for prop `value` on <textarea> tag. ' +
956
+ 'Either remove it from the element, or pass a string or number value to keep it in the DOM. ' +
957
+ 'For details, see https://react.dev/link/attribute-behavior \n' +
958
+ ' in textarea (at **)',
959
+ ]);
960
const node = container.firstChild;
961
962
expect(node.value).toBe('');
@@ -1054,60 +1075,60 @@ describe('ReactDOMTextarea', () => {
1075
it('should warn about missing onChange if value is false', async () => {
1076
const container = document.createElement('div');
1077
const root = ReactDOMClient.createRoot(container);
1057
- await expect(async () => {
1058
- await act(() => {
1059
- root.render(<textarea value={false} />);
1060
- });
1061
- }).toErrorDev(
1078
+ await act(() => {
1079
+ root.render(<textarea value={false} />);
1080
+ });
1081
+ assertConsoleErrorDev([
1082
'You provided a `value` prop to a form ' +
1083
'field without an `onChange` handler. This will render a read-only ' +
1084
'field. If the field should be mutable use `defaultValue`. ' +
1065
- 'Otherwise, set either `onChange` or `readOnly`.',
1066
- );
1085
+ 'Otherwise, set either `onChange` or `readOnly`.\n' +
1086
+ ' in textarea (at **)',
1087
+ ]);
1088
});
1089
1090
it('should warn about missing onChange if value is 0', async () => {
1091
const container = document.createElement('div');
1092
const root = ReactDOMClient.createRoot(container);
1072
- await expect(async () => {
1073
- await act(() => {
1074
- root.render(<textarea value={0} />);
1075
- });
1076
- }).toErrorDev(
1093
+ await act(() => {
1094
+ root.render(<textarea value={0} />);
1095
+ });
1096
+ assertConsoleErrorDev([
1097
'You provided a `value` prop to a form ' +
1098
'field without an `onChange` handler. This will render a read-only ' +
1099
'field. If the field should be mutable use `defaultValue`. ' +
1080
- 'Otherwise, set either `onChange` or `readOnly`.',
1081
- );
1100
+ 'Otherwise, set either `onChange` or `readOnly`.\n' +
1101
+ ' in textarea (at **)',
1102
+ ]);
1103
});
1104
1105
it('should warn about missing onChange if value is "0"', async () => {
1106
const container = document.createElement('div');
1107
const root = ReactDOMClient.createRoot(container);
1087
- await expect(async () => {
1088
- await act(() => {
1089
- root.render(<textarea value="0" />);
1090
- });
1091
- }).toErrorDev(
1108
+ await act(() => {
1109
+ root.render(<textarea value="0" />);
1110
+ });
1111
+ assertConsoleErrorDev([
1112
'You provided a `value` prop to a form ' +
1113
'field without an `onChange` handler. This will render a read-only ' +
1114
'field. If the field should be mutable use `defaultValue`. ' +
1095
- 'Otherwise, set either `onChange` or `readOnly`.',
1096
- );
1115
+ 'Otherwise, set either `onChange` or `readOnly`.\n' +
1116
+ ' in textarea (at **)',
1117
+ ]);
1118
});
1119
1120
it('should warn about missing onChange if value is ""', async () => {
1121
const container = document.createElement('div');
1122
const root = ReactDOMClient.createRoot(container);
1102
- await expect(async () => {
1103
- await act(() => {
1104
- root.render(<textarea value="" />);
1105
- });
1106
- }).toErrorDev(
1123
+ await act(() => {
1124
+ root.render(<textarea value="" />);
1125
+ });
1126
+ assertConsoleErrorDev([
1127
'You provided a `value` prop to a form ' +
1128
'field without an `onChange` handler. This will render a read-only ' +
1129
'field. If the field should be mutable use `defaultValue`. ' +
1110
- 'Otherwise, set either `onChange` or `readOnly`.',
1111
- );
1130
+ 'Otherwise, set either `onChange` or `readOnly`.\n' +
1131
+ ' in textarea (at **)',
1132
+ ]);
1133
});
1134
});
packages/react-dom/src/__tests__/ReactErrorBoundaries-test.internal.js
+52
-41
@@ -896,7 +896,9 @@ describe('ReactErrorBoundaries', () => {
896
);
897
});
898
assertConsoleErrorDev([
899
- 'BrokenComponentWillMountWithContext uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
899
+ 'BrokenComponentWillMountWithContext uses the legacy childContextTypes API which will soon be removed. ' +
900
+ 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
901
+ ' in BrokenComponentWillMountWithContext (at **)',
902
]);
903
expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
904
});
@@ -2366,44 +2368,53 @@ describe('ReactErrorBoundaries', () => {
2368
it('discards a bad root if the root component fails', async () => {
2369
const X = null;
2370
const Y = undefined;
2369
- let err1;
2370
- let err2;
2371
2372
- try {
2372
+ await expect(async () => {
2373
const container = document.createElement('div');
2374
const root = ReactDOMClient.createRoot(container);
2375
- await expect(
2376
- async () =>
2377
- await act(async () => {
2378
- root.render(<X />, container);
2379
- }),
2380
- ).toErrorDev(
2381
- 'React.createElement: type is invalid -- expected a string ' +
2382
- '(for built-in components) or a class/function ' +
2383
- '(for composite components) but got: null.',
2375
+ await act(async () => {
2376
+ root.render(<X />);
2377
+ });
2378
+ }).rejects.toThrow(
2379
+ 'Element type is invalid: ' +
2380
+ 'expected a string (for built-in components) or a ' +
2381
+ 'class/function (for composite components) but got: null.',
2382
+ );
2383
+
2384
+ if (!gate('enableOwnerStacks')) {
2385
+ assertConsoleErrorDev(
2386
+ [
2387
+ 'React.jsx: type is invalid -- expected a string ' +
2388
+ '(for built-in components) or a class/function ' +
2389
+ '(for composite components) but got: null.',
2390
+ ],
2391
+ {withoutStack: true},
2392
);
2385
- } catch (err) {
2386
- err1 = err;
2393
}
2388
- try {
2394
+
2395
+ await expect(async () => {
2396
const container = document.createElement('div');
2397
const root = ReactDOMClient.createRoot(container);
2391
- await expect(
2392
- async () =>
2393
- await act(async () => {
2394
- root.render(<Y />, container);
2395
- }),
2396
- ).toErrorDev(
2397
- 'React.createElement: type is invalid -- expected a string ' +
2398
- '(for built-in components) or a class/function ' +
2399
- '(for composite components) but got: undefined.',
2398
+ await act(async () => {
2399
+ root.render(<Y />);
2400
+ });
2401
+ }).rejects.toThrow(
2402
+ 'Element type is invalid: ' +
2403
+ 'expected a string (for built-in components) or a ' +
2404
+ 'class/function (for composite components) but got: undefined.',
2405
+ );
2406
+ if (!gate('enableOwnerStacks')) {
2407
+ assertConsoleErrorDev(
2408
+ [
2409
+ 'React.jsx: type is invalid -- expected a string ' +
2410
+ '(for built-in components) or a class/function ' +
2411
+ '(for composite components) but got: undefined. ' +
2412
+ "You likely forgot to export your component from the file it's defined in, " +
2413
+ 'or you might have mixed up default and named imports.',
2414
+ ],
2415
+ {withoutStack: true},
2416
);
2401
- } catch (err) {
2402
- err2 = err;
2417
}
2404
-
2405
- expect(err1.message).toMatch(/got: null/);
2406
- expect(err2.message).toMatch(/got: undefined/);
2418
});
2419
2420
it('renders empty output if error boundary does not handle the error', async () => {
@@ -2570,18 +2581,18 @@ describe('ReactErrorBoundaries', () => {
2581
2582
const container = document.createElement('div');
2583
const root = ReactDOMClient.createRoot(container);
2573
- await expect(async () => {
2574
- await act(async () => {
2575
- root.render(
2576
- <InvalidErrorBoundary>
2577
- <Throws />
2578
- </InvalidErrorBoundary>,
2579
- );
2580
- });
2581
- }).toErrorDev(
2584
+ await act(async () => {
2585
+ root.render(
2586
+ <InvalidErrorBoundary>
2587
+ <Throws />
2588
+ </InvalidErrorBoundary>,
2589
+ );
2590
+ });
2591
+ assertConsoleErrorDev([
2592
'InvalidErrorBoundary: Error boundaries should implement getDerivedStateFromError(). ' +
2583
- 'In that method, return a state update to display an error message or fallback UI.',
2584
- );
2593
+ 'In that method, return a state update to display an error message or fallback UI.\n' +
2594
+ ' in InvalidErrorBoundary (at **)',
2595
+ ]);
2596
expect(container.textContent).toBe('');
2597
});
2598
packages/react-dom/src/__tests__/ReactFunctionComponent-test.js
+55
-39
@@ -111,8 +111,14 @@ describe('ReactFunctionComponent', () => {
111
});
112
113
assertConsoleErrorDev([
114
- 'GrandParent uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
115
- 'Child uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
114
+ 'GrandParent uses the legacy childContextTypes API which will soon be removed. ' +
115
+ 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
116
+ ' in GrandParent (at **)',
117
+ 'Child uses the legacy contextTypes API which will soon be removed. ' +
118
+ 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
119
+ (gate('enableOwnerStacks') ? '' : ' in Child (at **)\n') +
120
+ ' in Parent (at **)\n' +
121
+ ' in GrandParent (at **)',
122
]);
123
124
expect(el.textContent).toBe('test');
@@ -132,15 +138,15 @@ describe('ReactFunctionComponent', () => {
138
139
const container = document.createElement('div');
140
135
- await expect(async () => {
136
- const root = ReactDOMClient.createRoot(container);
137
- await act(() => {
138
- root.render(<FunctionComponentWithChildContext />);
139
- });
140
- }).toErrorDev(
141
+ const root = ReactDOMClient.createRoot(container);
142
+ await act(() => {
143
+ root.render(<FunctionComponentWithChildContext />);
144
+ });
145
+ assertConsoleErrorDev([
146
'FunctionComponentWithChildContext: Function ' +
142
- 'components do not support getDerivedStateFromProps.',
143
- );
147
+ 'components do not support getDerivedStateFromProps.\n' +
148
+ ' in FunctionComponentWithChildContext (at **)',
149
+ ]);
150
});
151
152
it('should warn for childContextTypes on a function component', async () => {
@@ -154,14 +160,16 @@ describe('ReactFunctionComponent', () => {
160
161
const container = document.createElement('div');
162
157
- await expect(async () => {
158
- const root = ReactDOMClient.createRoot(container);
159
- await act(() => {
160
- root.render(<FunctionComponentWithChildContext name="A" />);
161
- });
162
- }).toErrorDev(
163
- 'childContextTypes cannot ' + 'be defined on a function component.',
164
- );
163
+ const root = ReactDOMClient.createRoot(container);
164
+ await act(() => {
165
+ root.render(<FunctionComponentWithChildContext name="A" />);
166
+ });
167
+ assertConsoleErrorDev([
168
+ 'childContextTypes cannot ' +
169
+ 'be defined on a function component.\n' +
170
+ ' FunctionComponentWithChildContext.childContextTypes = ...\n' +
171
+ ' in FunctionComponentWithChildContext (at **)',
172
+ ]);
173
});
174
175
it('should not throw when stateless component returns undefined', async () => {
@@ -184,16 +192,18 @@ describe('ReactFunctionComponent', () => {
192
return <div>{[<span />]}</div>;
193
}
194
187
- await expect(async () => {
188
- const container = document.createElement('div');
189
- const root = ReactDOMClient.createRoot(container);
190
- await act(() => {
191
- root.render(<Child />);
192
- });
193
- }).toErrorDev(
194
- 'Each child in a list should have a unique "key" prop.\n\n' +
195
- 'Check the render method of `Child`.',
196
- );
195
+ const container = document.createElement('div');
196
+ const root = ReactDOMClient.createRoot(container);
197
+ await act(() => {
198
+ root.render(<Child />);
199
+ });
200
+ assertConsoleErrorDev([
201
+ 'Each child in a list should have a unique "key" prop.\n' +
202
+ '\n' +
203
+ 'Check the render method of `Child`. See https://react.dev/link/warning-keys for more information.\n' +
204
+ ' in span (at **)\n' +
205
+ ' in Child (at **)',
206
+ ]);
207
});
208
209
// @gate !disableDefaultPropsExceptForClasses
@@ -203,16 +213,17 @@ describe('ReactFunctionComponent', () => {
213
}
214
Child.defaultProps = {test: 2};
215
206
- await expect(async () => {
207
- const container = document.createElement('div');
208
- const root = ReactDOMClient.createRoot(container);
216
+ const container = document.createElement('div');
217
+ const root = ReactDOMClient.createRoot(container);
218
210
- await act(() => {
211
- root.render(<Child />);
212
- });
213
- expect(container.textContent).toBe('2');
214
- }).toErrorDev([
215
- 'Child: Support for defaultProps will be removed from function components in a future major release. Use JavaScript default parameters instead.',
219
+ await act(() => {
220
+ root.render(<Child />);
221
+ });
222
+ expect(container.textContent).toBe('2');
223
+ assertConsoleErrorDev([
224
+ 'Child: Support for defaultProps will be removed from function components in a future major release. ' +
225
+ 'Use JavaScript default parameters instead.\n' +
226
+ ' in Child (at **)',
227
]);
228
});
229
@@ -244,8 +255,13 @@ describe('ReactFunctionComponent', () => {
255
root.render(<Parent />);
256
});
257
assertConsoleErrorDev([
247
- 'Parent uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
248
- 'Child uses the legacy contextTypes API which will be removed soon. Use React.createContext() with React.useContext() instead.',
258
+ 'Parent uses the legacy childContextTypes API which will soon be removed. ' +
259
+ 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
260
+ ' in Parent (at **)',
261
+ 'Child uses the legacy contextTypes API which will be removed soon. ' +
262
+ 'Use React.createContext() with React.useContext() instead. (https://react.dev/link/legacy-context)\n' +
263
+ ' in Child (at **)\n' +
264
+ ' in Parent (at **)',
265
]);
266
expect(el.textContent).toBe('en');
267
});
packages/react-dom/src/__tests__/ReactIdentity-test.js
+15
-11
@@ -12,6 +12,7 @@
12
let React;
13
let ReactDOMClient;
14
let act;
15
+let assertConsoleErrorDev;
16
17
describe('ReactIdentity', () => {
18
beforeEach(() => {
@@ -19,6 +20,8 @@ describe('ReactIdentity', () => {
20
React = require('react');
21
ReactDOMClient = require('react-dom/client');
22
act = require('internal-test-utils').act;
23
+ assertConsoleErrorDev =
24
+ require('internal-test-utils').assertConsoleErrorDev;
25
});
26
27
it('should allow key property to express identity', async () => {
@@ -313,17 +316,18 @@ describe('ReactIdentity', () => {
316
317
const el = document.createElement('div');
318
const root = ReactDOMClient.createRoot(el);
316
- await expect(() =>
317
- expect(() => {
318
- root.render(
319
- <div>
320
- <span key={new TemporalLike()} />
321
- </div>,
322
- );
323
- }).toThrowError(new TypeError('prod message')),
324
- ).toErrorDev(
325
- 'The provided key is an unsupported type TemporalLike.' +
326
- ' This value must be coerced to a string before using it here.',
319
+ await expect(() => {
320
+ root.render(
321
+ <div>
322
+ <span key={new TemporalLike()} />
323
+ </div>,
324
+ );
325
+ }).toThrowError(new TypeError('prod message'));
326
+ assertConsoleErrorDev(
327
+ [
328
+ 'The provided key is an unsupported type TemporalLike.' +
329
+ ' This value must be coerced to a string before using it here.',
330
+ ],
331
{withoutStack: true},
332
);
333
});
packages/react-dom/src/__tests__/ReactLegacyCompositeComponent-test.js
+105
-54
@@ -50,15 +50,12 @@ describe('ReactLegacyCompositeComponent', () => {
50
return <div />;
51
}
52
}
53
-
54
- let instance;
55
-
56
- expect(() => {
57
- instance = ReactDOM.render(<Component />, container);
58
- }).toErrorDev(
53
+ const instance = ReactDOM.render(<Component />, container);
54
+ assertConsoleErrorDev([
55
'Cannot update during an existing state transition (such as within ' +
60
- '`render`). Render methods should be a pure function of props and state.',
61
- );
56
+ '`render`). Render methods should be a pure function of props and state.\n' +
57
+ ' in Component (at **)',
58
+ ]);
59
60
// The setState call is queued and then executed as a second pass. This
61
// behavior is undefined though so we're free to change it to suit the
@@ -122,8 +119,16 @@ describe('ReactLegacyCompositeComponent', () => {
119
root.render(<Parent ref={current => (component = current)} />);
120
});
121
assertConsoleErrorDev([
125
- 'Child uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
126
- 'Grandchild uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
122
+ 'Child uses the legacy childContextTypes API which will soon be removed. ' +
123
+ 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
124
+ (gate('enableOwnerStacks') ? '' : ' in Child (at **)\n') +
125
+ ' in Parent (at **)',
126
+ 'Grandchild uses the legacy contextTypes API which will soon be removed. ' +
127
+ 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
128
+ (gate('enableOwnerStacks')
129
+ ? ''
130
+ : ' in Grandchild (at **)\n' + ' in Child (at **)\n') +
131
+ ' in Parent (at **)',
132
]);
133
expect(findDOMNode(component).innerHTML).toBe('bar');
134
});
@@ -190,8 +195,15 @@ describe('ReactLegacyCompositeComponent', () => {
195
expect(childInstance.context).toEqual({foo: 'bar', flag: false});
196
197
assertConsoleErrorDev([
193
- 'Parent uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
194
- 'Child uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
198
+ 'Parent uses the legacy childContextTypes API which will soon be removed. ' +
199
+ 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
200
+ ' in Parent (at **)',
201
+ 'Child uses the legacy contextTypes API which will soon be removed. ' +
202
+ 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
203
+ ' in Child (at **)' +
204
+ (gate('enableOwnerStacks')
205
+ ? ''
206
+ : '\n in Middle (at **)' + '\n in Parent (at **)'),
207
]);
208
209
await act(() => {
@@ -254,8 +266,18 @@ describe('ReactLegacyCompositeComponent', () => {
266
});
267
268
assertConsoleErrorDev([
257
- 'Parent uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
258
- 'Child uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
269
+ 'Parent uses the legacy childContextTypes API which will soon be removed. ' +
270
+ 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
271
+ (gate('enableOwnerStacks') ? '' : ' in Parent (at **)\n') +
272
+ ' in Wrapper (at **)',
273
+ 'Child uses the legacy contextTypes API which will soon be removed. ' +
274
+ 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
275
+ (gate('enableOwnerStacks')
276
+ ? ''
277
+ : ' in Child (at **)\n' +
278
+ ' in div (at **)\n' +
279
+ ' in Parent (at **)\n') +
280
+ ' in Wrapper (at **)',
281
]);
282
283
expect(wrapper.parentRef.current.state.flag).toEqual(true);
@@ -334,10 +356,22 @@ describe('ReactLegacyCompositeComponent', () => {
356
});
357
358
assertConsoleErrorDev([
337
- 'Parent uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
338
- 'Child uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
339
- 'Child uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
340
- 'Grandchild uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
359
+ 'Parent uses the legacy childContextTypes API which will soon be removed. ' +
360
+ 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
361
+ ' in Parent (at **)',
362
+ 'Child uses the legacy childContextTypes API which will soon be removed. ' +
363
+ 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
364
+ (gate('enableOwnerStacks') ? '' : ' in Child (at **)\n') +
365
+ ' in Parent (at **)',
366
+ 'Child uses the legacy contextTypes API which will soon be removed. ' +
367
+ 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
368
+ (gate('enableOwnerStacks') ? '' : ' in Child (at **)\n') +
369
+ ' in Parent (at **)',
370
+ 'Grandchild uses the legacy contextTypes API which will soon be removed. ' +
371
+ 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
372
+ (gate('enableOwnerStacks') ? '' : ' in Grandchild (at **)\n') +
373
+ ' in Child (at **)\n' +
374
+ ' in Parent (at **)',
375
]);
376
377
expect(childInstance.context).toEqual({foo: 'bar', depth: 0});
@@ -393,7 +427,9 @@ describe('ReactLegacyCompositeComponent', () => {
427
root.render(<Parent ref={current => (parentInstance = current)} />);
428
});
429
assertConsoleErrorDev([
396
- 'Parent uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
430
+ 'Parent uses the legacy childContextTypes API which will soon be removed. ' +
431
+ 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
432
+ ' in Parent (at **)',
433
]);
434
435
expect(childInstance).toBeNull();
@@ -403,7 +439,10 @@ describe('ReactLegacyCompositeComponent', () => {
439
parentInstance.setState({flag: true});
440
});
441
assertConsoleErrorDev([
406
- 'Child uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
442
+ 'Child uses the legacy contextTypes API which will soon be removed. ' +
443
+ 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
444
+ (gate('enableOwnerStacks') ? '' : ' in Child (at **)\n') +
445
+ ' in Parent (at **)',
446
]);
447
448
expect(parentInstance.state.flag).toBe(true);
@@ -465,11 +504,15 @@ describe('ReactLegacyCompositeComponent', () => {
504
}
505
506
const div = document.createElement('div');
468
- expect(() => {
469
- ReactDOM.render(<Parent cntxt="noise" />, div);
470
- }).toErrorDev([
471
- 'Parent uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
472
- 'Leaf uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
507
+ ReactDOM.render(<Parent cntxt="noise" />, div);
508
+ assertConsoleErrorDev([
509
+ 'Parent uses the legacy childContextTypes API which will soon be removed. ' +
510
+ 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
511
+ ' in Parent (at **)',
512
+ 'Leaf uses the legacy contextTypes API which will soon be removed. ' +
513
+ 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
514
+ ' in Intermediary (at **)\n' +
515
+ ' in Parent (at **)',
516
]);
517
expect(div.children[0].innerHTML).toBe('noise');
518
div.children[0].innerHTML = 'aliens';
@@ -572,25 +615,30 @@ describe('ReactLegacyCompositeComponent', () => {
615
const div = document.createElement('div');
616
617
let parentInstance = null;
575
- expect(() => {
576
- ReactDOM.render(
577
- <Parent ref={inst => (parentInstance = inst)}>
578
- <ChildWithoutContext>
579
- A1
580
- <GrandChild>A2</GrandChild>
581
- </ChildWithoutContext>
582
-
583
- <ChildWithContext>
584
- B1
585
- <GrandChild>B2</GrandChild>
586
- </ChildWithContext>
587
- </Parent>,
588
- div,
589
- );
590
- }).toErrorDev([
591
- 'Parent uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
592
- 'GrandChild uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
593
- 'ChildWithContext uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
618
+ ReactDOM.render(
619
+ <Parent ref={inst => (parentInstance = inst)}>
620
+ <ChildWithoutContext>
621
+ A1
622
+ <GrandChild>A2</GrandChild>
623
+ </ChildWithoutContext>
624
+
625
+ <ChildWithContext>
626
+ B1
627
+ <GrandChild>B2</GrandChild>
628
+ </ChildWithContext>
629
+ </Parent>,
630
+ div,
631
+ );
632
+ assertConsoleErrorDev([
633
+ 'Parent uses the legacy childContextTypes API which will soon be removed. ' +
634
+ 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
635
+ ' in Parent (at **)',
636
+ 'GrandChild uses the legacy contextTypes API which will soon be removed. ' +
637
+ 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
638
+ ' in GrandChild (at **)',
639
+ 'ChildWithContext uses the legacy contextTypes API which will soon be removed. ' +
640
+ 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
641
+ ' in ChildWithContext (at **)',
642
]);
643
644
parentInstance.setState({
@@ -774,16 +822,19 @@ describe('ReactLegacyCompositeComponent', () => {
822
}
823
824
const div = document.createElement('div');
777
- expect(() => {
778
- ReactDOM.render(
779
- <Parent>
780
- <Component />
781
- </Parent>,
782
- div,
783
- );
784
- }).toErrorDev([
785
- 'Parent uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
786
- 'Component uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
825
+ ReactDOM.render(
826
+ <Parent>
827
+ <Component />
828
+ </Parent>,
829
+ div,
830
+ );
831
+ assertConsoleErrorDev([
832
+ 'Parent uses the legacy childContextTypes API which will soon be removed. ' +
833
+ 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
834
+ ' in Parent (at **)',
835
+ 'Component uses the legacy contextTypes API which will soon be removed. ' +
836
+ 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
837
+ ' in Component (at **)',
838
]);
839
});
840
packages/react-dom/src/__tests__/ReactLegacyContextDisabled-test.internal.js
+50
-32
@@ -14,6 +14,7 @@ let ReactDOMClient;
14
let ReactDOMServer;
15
let ReactFeatureFlags;
16
let act;
17
+let assertConsoleErrorDev;
18
19
describe('ReactLegacyContextDisabled', () => {
20
beforeEach(() => {
@@ -25,6 +26,8 @@ describe('ReactLegacyContextDisabled', () => {
26
ReactFeatureFlags = require('shared/ReactFeatureFlags');
27
ReactFeatureFlags.disableLegacyContext = true;
28
act = require('internal-test-utils').act;
29
+ assertConsoleErrorDev =
30
+ require('internal-test-utils').assertConsoleErrorDev;
31
});
32
33
function formatValue(val) {
@@ -84,25 +87,33 @@ describe('ReactLegacyContextDisabled', () => {
87
88
const container = document.createElement('div');
89
const root = ReactDOMClient.createRoot(container);
87
- await expect(async () => {
88
- await act(() => {
89
- root.render(
90
- <LegacyProvider>
91
- <span>
92
- <LegacyClsConsumer />
93
- <LegacyFnConsumer />
94
- <RegularFn />
95
- </span>
96
- </LegacyProvider>,
97
- );
98
- });
99
- }).toErrorDev([
90
+ await act(() => {
91
+ root.render(
92
+ <LegacyProvider>
93
+ <span>
94
+ <LegacyClsConsumer />
95
+ <LegacyFnConsumer />
96
+ <RegularFn />
97
+ </span>
98
+ </LegacyProvider>,
99
+ );
100
+ });
101
+ assertConsoleErrorDev([
102
'LegacyProvider uses the legacy childContextTypes API which was removed in React 19. ' +
101
- 'Use React.createContext() instead.',
103
+ 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
104
+ ' in LegacyProvider (at **)',
105
'LegacyClsConsumer uses the legacy contextTypes API which was removed in React 19. ' +
103
- 'Use React.createContext() with static contextType instead.',
106
+ 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
107
+ ' in LegacyClsConsumer (at **)' +
108
+ (gate('enableOwnerStacks')
109
+ ? ''
110
+ : '\n' + ' in span (at **)\n' + ' in LegacyProvider (at **)'),
111
'LegacyFnConsumer uses the legacy contextTypes API which was removed in React 19. ' +
105
- 'Use React.createContext() with React.useContext() instead.',
112
+ 'Use React.createContext() with React.useContext() instead. (https://react.dev/link/legacy-context)\n' +
113
+ ' in LegacyFnConsumer (at **)' +
114
+ (gate('enableOwnerStacks')
115
+ ? ''
116
+ : '\n' + ' in span (at **)\n' + ' in LegacyProvider (at **)'),
117
]);
118
expect(container.textContent).toBe('{}undefinedundefined');
119
expect(lifecycleContextLog).toEqual([]);
@@ -124,25 +135,32 @@ describe('ReactLegacyContextDisabled', () => {
135
root.unmount();
136
137
// test server path.
127
- let text;
128
- expect(() => {
129
- text = ReactDOMServer.renderToString(
130
- <LegacyProvider>
131
- <span>
132
- <LegacyClsConsumer />
133
- <LegacyFnConsumer />
134
- <RegularFn />
135
- </span>
136
- </LegacyProvider>,
137
- container,
138
- );
139
- }).toErrorDev([
138
+ const text = ReactDOMServer.renderToString(
139
+ <LegacyProvider>
140
+ <span>
141
+ <LegacyClsConsumer />
142
+ <LegacyFnConsumer />
143
+ <RegularFn />
144
+ </span>
145
+ </LegacyProvider>,
146
+ container,
147
+ );
148
+ assertConsoleErrorDev([
149
'LegacyProvider uses the legacy childContextTypes API which was removed in React 19. ' +
141
- 'Use React.createContext() instead.',
150
+ 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
151
+ ' in LegacyProvider (at **)',
152
'LegacyClsConsumer uses the legacy contextTypes API which was removed in React 19. ' +
143
- 'Use React.createContext() with static contextType instead.',
153
+ 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
154
+ ' in LegacyClsConsumer (at **)' +
155
+ (gate('enableOwnerStacks')
156
+ ? ''
157
+ : '\n' + ' in span (at **)\n' + ' in LegacyProvider (at **)'),
158
'LegacyFnConsumer uses the legacy contextTypes API which was removed in React 19. ' +
145
- 'Use React.createContext() with React.useContext() instead.',
159
+ 'Use React.createContext() with React.useContext() instead. (https://react.dev/link/legacy-context)\n' +
160
+ ' in LegacyFnConsumer (at **)' +
161
+ (gate('enableOwnerStacks')
162
+ ? ''
163
+ : '\n' + ' in span (at **)\n' + ' in LegacyProvider (at **)'),
164
]);
165
expect(text).toBe('<span>{}<!-- -->undefined<!-- -->undefined</span>');
166
expect(lifecycleContextLog).toEqual([{}, {}, {}]);
packages/react-dom/src/__tests__/ReactLegacyErrorBoundaries-test.internal.js
+16
-13
@@ -850,7 +850,9 @@ describe('ReactLegacyErrorBoundaries', () => {
850
container,
851
);
852
assertConsoleErrorDev([
853
- 'BrokenComponentWillMountWithContext uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
853
+ 'BrokenComponentWillMountWithContext uses the legacy childContextTypes API which will soon be removed. ' +
854
+ 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
855
+ ' in BrokenComponentWillMountWithContext (at **)',
856
]);
857
expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
858
});
@@ -2142,19 +2144,20 @@ describe('ReactLegacyErrorBoundaries', () => {
2144
// @gate !disableLegacyMode
2145
it('renders empty output if error boundary does not handle the error', async () => {
2146
const container = document.createElement('div');
2145
- expect(() => {
2146
- ReactDOM.render(
2147
- <div>
2148
- Sibling
2149
- <NoopErrorBoundary>
2150
- <BrokenRender />
2151
- </NoopErrorBoundary>
2152
- </div>,
2153
- container,
2154
- );
2155
- }).toErrorDev(
2156
- 'ErrorBoundary: Error boundaries should implement getDerivedStateFromError()',
2147
+ ReactDOM.render(
2148
+ <div>
2149
+ Sibling
2150
+ <NoopErrorBoundary>
2151
+ <BrokenRender />
2152
+ </NoopErrorBoundary>
2153
+ </div>,
2154
+ container,
2155
);
2156
+ assertConsoleErrorDev([
2157
+ 'NoopErrorBoundary: Error boundaries should implement getDerivedStateFromError(). ' +
2158
+ 'In that method, return a state update to display an error message or fallback UI.\n' +
2159
+ ' in NoopErrorBoundary (at **)',
2160
+ ]);
2161
expect(container.firstChild.textContent).toBe('Sibling');
2162
expect(log).toEqual([
2163
'NoopErrorBoundary constructor',
packages/react-dom/src/__tests__/ReactLegacyMount-test.js
+50
-36
@@ -15,6 +15,7 @@ let React;
15
let ReactDOM;
16
let ReactDOMClient;
17
let waitForAll;
18
+let assertConsoleErrorDev;
19
20
describe('ReactMount', () => {
21
beforeEach(() => {
@@ -26,6 +27,7 @@ describe('ReactMount', () => {
27
28
const InternalTestUtils = require('internal-test-utils');
29
waitForAll = InternalTestUtils.waitForAll;
30
+ assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev;
31
});
32
33
describe('unmountComponentAtNode', () => {
@@ -63,14 +65,15 @@ describe('ReactMount', () => {
65
}
66
}
67
66
- expect(() => {
67
- const container = document.createElement('div');
68
- ReactDOM.render(Component, container);
69
- }).toErrorDev(
70
- 'Functions are not valid as a React child. ' +
71
- 'This may happen if you return Component instead of <Component /> from render. ' +
72
- 'Or maybe you meant to call this function rather than return it.\n' +
73
- ' root.render(Component)',
68
+ const container = document.createElement('div');
69
+ ReactDOM.render(Component, container);
70
+ assertConsoleErrorDev(
71
+ [
72
+ 'Functions are not valid as a React child. ' +
73
+ 'This may happen if you return Component instead of <Component /> from render. ' +
74
+ 'Or maybe you meant to call this function rather than return it.\n' +
75
+ ' root.render(Component)',
76
+ ],
77
{withoutStack: true},
78
);
79
});
@@ -168,11 +171,14 @@ describe('ReactMount', () => {
171
// Test that blasting away children throws a warning
172
const rootNode = container.firstChild;
173
171
- expect(() => ReactDOM.render(<span />, rootNode)).toErrorDev(
172
- 'Replacing React-rendered children with a new ' +
173
- 'root component. If you intended to update the children of this node, ' +
174
- 'you should instead have the existing children update their state and ' +
175
- 'render the new components instead of calling ReactDOM.render.',
174
+ ReactDOM.render(<span />, rootNode);
175
+ assertConsoleErrorDev(
176
+ [
177
+ 'Replacing React-rendered children with a new ' +
178
+ 'root component. If you intended to update the children of this node, ' +
179
+ 'you should instead have the existing children update their state and ' +
180
+ 'render the new components instead of calling ReactDOM.render.',
181
+ ],
182
{withoutStack: true},
183
);
184
});
@@ -197,9 +203,12 @@ describe('ReactMount', () => {
203
// Make sure ReactDOM and ReactDOMOther are different copies
204
expect(ReactDOM).not.toEqual(ReactDOMOther);
205
200
- expect(() => ReactDOMOther.unmountComponentAtNode(container)).toErrorDev(
201
- "unmountComponentAtNode(): The node you're attempting to unmount " +
202
- 'was rendered by another copy of React.',
206
+ ReactDOMOther.unmountComponentAtNode(container);
207
+ assertConsoleErrorDev(
208
+ [
209
+ "unmountComponentAtNode(): The node you're attempting to unmount " +
210
+ 'was rendered by another copy of React.',
211
+ ],
212
{withoutStack: true},
213
);
214
@@ -351,16 +360,18 @@ describe('ReactMount', () => {
360
root.render(<div>Hi</div>);
361
await waitForAll([]);
362
expect(container.textContent).toEqual('Hi');
354
- expect(() => {
355
- ReactDOM.render(<div>Bye</div>, container);
356
- }).toErrorDev(
363
+ ReactDOM.render(<div>Bye</div>, container);
364
+ assertConsoleErrorDev(
365
[
366
// We care about this warning:
367
'You are calling ReactDOM.render() on a container that was previously ' +
368
'passed to ReactDOMClient.createRoot(). This is not supported. ' +
369
'Did you mean to call root.render(element)?',
370
// This is more of a symptom but restructuring the code to avoid it isn't worth it:
363
- 'Replacing React-rendered children with a new root component.',
371
+ 'Replacing React-rendered children with a new root component. ' +
372
+ 'If you intended to update the children of this node, ' +
373
+ 'you should instead have the existing children update their state ' +
374
+ 'and render the new components instead of calling ReactDOM.render.',
375
],
376
{withoutStack: true},
377
);
@@ -376,16 +387,16 @@ describe('ReactMount', () => {
387
root.render(<div>Hi</div>);
388
await waitForAll([]);
389
expect(container.textContent).toEqual('Hi');
379
- let unmounted = false;
380
- expect(() => {
381
- unmounted = ReactDOM.unmountComponentAtNode(container);
382
- }).toErrorDev(
390
+ const unmounted = ReactDOM.unmountComponentAtNode(container);
391
+ assertConsoleErrorDev(
392
[
393
// We care about this warning:
394
'You are calling ReactDOM.unmountComponentAtNode() on a container that was previously ' +
395
'passed to ReactDOMClient.createRoot(). This is not supported. Did you mean to call root.unmount()?',
396
// This is more of a symptom but restructuring the code to avoid it isn't worth it:
388
- "The node you're attempting to unmount was rendered by React and is not a top-level container.",
397
+ 'unmountComponentAtNode(): ' +
398
+ "The node you're attempting to unmount was rendered by React and is not a top-level container. " +
399
+ 'Instead, have the parent component update its state and rerender in order to remove this component.',
400
],
401
{withoutStack: true},
402
);
@@ -407,14 +418,16 @@ describe('ReactMount', () => {
418
root.render(<div>Hi</div>);
419
await waitForAll([]);
420
expect(container.textContent).toEqual('Hi');
410
- let unmounted = false;
411
- expect(() => {
412
- unmounted = ReactDOM.unmountComponentAtNode(container);
413
- }).toErrorDev(
421
+ const unmounted = ReactDOM.unmountComponentAtNode(container);
422
+ assertConsoleErrorDev(
423
[
415
- 'Did you mean to call root.unmount()?',
424
+ 'You are calling ReactDOM.unmountComponentAtNode() on a container ' +
425
+ 'that was previously passed to ReactDOMClient.createRoot(). ' +
426
+ 'This is not supported. Did you mean to call root.unmount()?',
427
// This is more of a symptom but restructuring the code to avoid it isn't worth it:
417
- "The node you're attempting to unmount was rendered by React and is not a top-level container.",
428
+ 'unmountComponentAtNode(): ' +
429
+ "The node you're attempting to unmount was rendered by React and is not a top-level container. " +
430
+ 'Instead, have the parent component update its state and rerender in order to remove this component.',
431
],
432
{withoutStack: true},
433
);
@@ -430,11 +443,12 @@ describe('ReactMount', () => {
443
it('warns when passing legacy container to createRoot()', () => {
444
const container = document.createElement('div');
445
ReactDOM.render(<div>Hi</div>, container);
433
- expect(() => {
434
- ReactDOMClient.createRoot(container);
435
- }).toErrorDev(
436
- 'You are calling ReactDOMClient.createRoot() on a container that was previously ' +
437
- 'passed to ReactDOM.render(). This is not supported.',
446
+ ReactDOMClient.createRoot(container);
447
+ assertConsoleErrorDev(
448
+ [
449
+ 'You are calling ReactDOMClient.createRoot() on a container that was previously ' +
450
+ 'passed to ReactDOM.render(). This is not supported.',
451
+ ],
452
{withoutStack: true},
453
);
454
});
packages/react-dom/src/__tests__/ReactLegacyUpdates-test.js
+61
-51
@@ -15,6 +15,7 @@ let findDOMNode;
15
let act;
16
let Scheduler;
17
let assertLog;
18
+let assertConsoleErrorDev;
19
20
// Copy of ReactUpdates using ReactDOM.render and ReactDOM.unstable_batchedUpdates.
21
// Can be deleted when we remove both.
@@ -27,6 +28,8 @@ describe('ReactLegacyUpdates', () => {
28
ReactDOM.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE
29
.findDOMNode;
30
act = require('internal-test-utils').act;
31
+ assertConsoleErrorDev =
32
+ require('internal-test-utils').assertConsoleErrorDev;
33
Scheduler = require('scheduler');
34
35
const InternalTestUtils = require('internal-test-utils');
@@ -906,35 +909,37 @@ describe('ReactLegacyUpdates', () => {
909
let component = ReactDOM.render(<A />, container);
910
911
await expect(async () => {
909
- await expect(async () => {
910
- await act(() => {
911
- component.setState({}, 'no');
912
- });
913
- }).rejects.toThrowError(
914
- 'Invalid argument passed as callback. Expected a function. Instead ' +
915
- 'received: no',
916
- );
917
- }).toErrorDev(
918
- 'Expected the last optional `callback` argument to be ' +
919
- 'a function. Instead received: no.',
920
- {withoutStack: 1},
912
+ await act(() => {
913
+ component.setState({}, 'no');
914
+ });
915
+ }).rejects.toThrowError(
916
+ 'Invalid argument passed as callback. Expected a function. ' +
917
+ 'Instead received: no',
918
+ );
919
+ assertConsoleErrorDev(
920
+ [
921
+ 'Expected the last optional `callback` argument to be ' +
922
+ 'a function. Instead received: no.',
923
+ ],
924
+ {withoutStack: true},
925
);
926
927
container = document.createElement('div');
928
component = ReactDOM.render(<A />, container);
929
await expect(async () => {
926
- await expect(async () => {
927
- await act(() => {
928
- component.setState({}, {foo: 'bar'});
929
- });
930
- }).rejects.toThrowError(
931
- 'Invalid argument passed as callback. Expected a function. Instead ' +
932
- 'received: [object Object]',
933
- );
934
- }).toErrorDev(
935
- 'Expected the last optional `callback` argument to be ' +
936
- "a function. Instead received: { foo: 'bar' }.",
937
- {withoutStack: 1},
930
+ await act(() => {
931
+ component.setState({}, {foo: 'bar'});
932
+ });
933
+ }).rejects.toThrowError(
934
+ 'Invalid argument passed as callback. Expected a function. Instead ' +
935
+ 'received: [object Object]',
936
+ );
937
+ assertConsoleErrorDev(
938
+ [
939
+ 'Expected the last optional `callback` argument to be a function. ' +
940
+ "Instead received: { foo: 'bar' }.",
941
+ ],
942
+ {withoutStack: true},
943
);
944
// Make sure the warning is deduplicated and doesn't fire again
945
container = document.createElement('div');
@@ -968,34 +973,36 @@ describe('ReactLegacyUpdates', () => {
973
let component = ReactDOM.render(<A />, container);
974
975
await expect(async () => {
971
- await expect(async () => {
972
- await act(() => {
973
- component.forceUpdate('no');
974
- });
975
- }).rejects.toThrowError(
976
- 'Invalid argument passed as callback. Expected a function. Instead ' +
977
- 'received: no',
978
- );
979
- }).toErrorDev(
980
- 'Expected the last optional `callback` argument to be ' +
981
- 'a function. Instead received: no.',
982
- {withoutStack: 1},
976
+ await act(() => {
977
+ component.forceUpdate('no');
978
+ });
979
+ }).rejects.toThrowError(
980
+ 'Invalid argument passed as callback. Expected a function. Instead ' +
981
+ 'received: no',
982
+ );
983
+ assertConsoleErrorDev(
984
+ [
985
+ 'Expected the last optional `callback` argument to be a function. ' +
986
+ 'Instead received: no.',
987
+ ],
988
+ {withoutStack: true},
989
);
990
container = document.createElement('div');
991
component = ReactDOM.render(<A />, container);
992
await expect(async () => {
987
- await expect(async () => {
988
- await act(() => {
989
- component.forceUpdate({foo: 'bar'});
990
- });
991
- }).rejects.toThrowError(
992
- 'Invalid argument passed as callback. Expected a function. Instead ' +
993
- 'received: [object Object]',
994
- );
995
- }).toErrorDev(
996
- 'Expected the last optional `callback` argument to be ' +
997
- "a function. Instead received: { foo: 'bar' }.",
998
- {withoutStack: 1},
993
+ await act(() => {
994
+ component.forceUpdate({foo: 'bar'});
995
+ });
996
+ }).rejects.toThrowError(
997
+ 'Invalid argument passed as callback. Expected a function. Instead ' +
998
+ 'received: [object Object]',
999
+ );
1000
+ assertConsoleErrorDev(
1001
+ [
1002
+ 'Expected the last optional `callback` argument to be a function. ' +
1003
+ "Instead received: { foo: 'bar' }.",
1004
+ ],
1005
+ {withoutStack: true},
1006
);
1007
// Make sure the warning is deduplicated and doesn't fire again
1008
container = document.createElement('div');
@@ -1318,9 +1325,12 @@ describe('ReactLegacyUpdates', () => {
1325
}
1326
1327
const container = document.createElement('div');
1321
- expect(() => ReactDOM.render(<Foo />, container)).toErrorDev(
1322
- 'Cannot update during an existing state transition',
1323
- );
1328
+ ReactDOM.render(<Foo />, container);
1329
+ assertConsoleErrorDev([
1330
+ 'Cannot update during an existing state transition (such as within `render`). ' +
1331
+ 'Render methods should be a pure function of props and state.\n' +
1332
+ ' in Foo (at **)',
1333
+ ]);
1334
expect(ops).toEqual(['base: 0, memoized: 0', 'base: 1, memoized: 1']);
1335
});
1336
packages/react-dom/src/__tests__/ReactMountDestruction-test.js
+18
-10
@@ -13,6 +13,8 @@ const React = require('react');
13
const ReactDOM = require('react-dom');
14
const ReactDOMClient = require('react-dom/client');
15
const act = require('internal-test-utils').act;
16
+const assertConsoleErrorDev =
17
+ require('internal-test-utils').assertConsoleErrorDev;
18
19
describe('ReactMount', () => {
20
it('should destroy a react root upon request', async () => {
@@ -63,11 +65,14 @@ describe('ReactMount', () => {
65
66
// Test that unmounting at a root node gives a helpful warning
67
const rootDiv = mainContainerDiv.firstChild;
66
- expect(() => ReactDOM.unmountComponentAtNode(rootDiv)).toErrorDev(
67
- "unmountComponentAtNode(): The node you're attempting to " +
68
- 'unmount was rendered by React and is not a top-level container. You ' +
69
- 'may have accidentally passed in a React root node instead of its ' +
70
- 'container.',
68
+ ReactDOM.unmountComponentAtNode(rootDiv);
69
+ assertConsoleErrorDev(
70
+ [
71
+ "unmountComponentAtNode(): The node you're attempting to " +
72
+ 'unmount was rendered by React and is not a top-level container. You ' +
73
+ 'may have accidentally passed in a React root node instead of its ' +
74
+ 'container.',
75
+ ],
76
{withoutStack: true},
77
);
78
});
@@ -88,11 +93,14 @@ describe('ReactMount', () => {
93
94
// Test that unmounting at a non-root node gives a different warning
95
const nonRootDiv = mainContainerDiv.firstChild.firstChild;
91
- expect(() => ReactDOM.unmountComponentAtNode(nonRootDiv)).toErrorDev(
92
- "unmountComponentAtNode(): The node you're attempting to " +
93
- 'unmount was rendered by React and is not a top-level container. ' +
94
- 'Instead, have the parent component update its state and rerender in ' +
95
- 'order to remove this component.',
96
+ ReactDOM.unmountComponentAtNode(nonRootDiv);
97
+ assertConsoleErrorDev(
98
+ [
99
+ "unmountComponentAtNode(): The node you're attempting to " +
100
+ 'unmount was rendered by React and is not a top-level container. ' +
101
+ 'Instead, have the parent component update its state and rerender in ' +
102
+ 'order to remove this component.',
103
+ ],
104
{withoutStack: true},
105
);
106
});
packages/react-dom/src/__tests__/ReactMultiChild-test.js
+30
-37
@@ -13,12 +13,15 @@ describe('ReactMultiChild', () => {
13
let React;
14
let ReactDOMClient;
15
let act;
16
+ let assertConsoleErrorDev;
17
18
beforeEach(() => {
19
jest.resetModules();
20
React = require('react');
21
ReactDOMClient = require('react-dom/client');
22
act = require('internal-test-utils').act;
23
+ assertConsoleErrorDev =
24
+ require('internal-test-utils').assertConsoleErrorDev;
25
});
26
27
describe('reconciliation', () => {
@@ -216,12 +219,10 @@ describe('ReactMultiChild', () => {
219
root.render(<Parent>{[<div key="1" />]}</Parent>);
220
});
221
219
- await expect(
220
- async () =>
221
- await act(async () => {
222
- root.render(<Parent>{[<div key="1" />, <div key="1" />]}</Parent>);
223
- }),
224
- ).toErrorDev(
222
+ await act(async () => {
223
+ root.render(<Parent>{[<div key="1" />, <div key="1" />]}</Parent>);
224
+ });
225
+ assertConsoleErrorDev([
226
'Encountered two children with the same key, `1`. ' +
227
'Keys should be unique so that components maintain their identity ' +
228
'across updates. Non-unique keys may cause children to be ' +
@@ -234,7 +235,7 @@ describe('ReactMultiChild', () => {
235
'\n in WrapperComponent (at **)' +
236
'\n in div (at **)' +
237
'\n in Parent (at **)'),
237
- );
238
+ ]);
239
});
240
241
it('should warn for duplicated iterable keys with component stack info', async () => {
@@ -278,16 +279,12 @@ describe('ReactMultiChild', () => {
279
root.render(<Parent>{createIterable([<div key="1" />])}</Parent>);
280
});
281
281
- await expect(
282
- async () =>
283
- await act(async () => {
284
- root.render(
285
- <Parent>
286
- {createIterable([<div key="1" />, <div key="1" />])}
287
- </Parent>,
288
- );
289
- }),
290
- ).toErrorDev(
282
+ await act(async () => {
283
+ root.render(
284
+ <Parent>{createIterable([<div key="1" />, <div key="1" />])}</Parent>,
285
+ );
286
+ });
287
+ assertConsoleErrorDev([
288
'Encountered two children with the same key, `1`. ' +
289
'Keys should be unique so that components maintain their identity ' +
290
'across updates. Non-unique keys may cause children to be ' +
@@ -300,7 +297,7 @@ describe('ReactMultiChild', () => {
297
'\n in WrapperComponent (at **)' +
298
'\n in div (at **)' +
299
'\n in Parent (at **)'),
303
- );
300
+ ]);
301
});
302
});
303
@@ -321,17 +318,15 @@ describe('ReactMultiChild', () => {
318
}
319
const container = document.createElement('div');
320
const root = ReactDOMClient.createRoot(container);
324
- await expect(
325
- async () =>
326
- await act(async () => {
327
- root.render(<Parent />);
328
- }),
329
- ).toErrorDev(
321
+ await act(async () => {
322
+ root.render(<Parent />);
323
+ });
324
+ assertConsoleErrorDev([
325
'Using Maps as children is not supported. ' +
326
'Use an array of keyed ReactElements instead.\n' +
327
' in div (at **)\n' +
328
' in Parent (at **)',
334
- );
329
+ ]);
330
});
331
332
it('should NOT warn for using generator functions as components', async () => {
@@ -362,11 +357,10 @@ describe('ReactMultiChild', () => {
357
358
const container = document.createElement('div');
359
const root = ReactDOMClient.createRoot(container);
365
- await expect(async () => {
366
- await act(async () => {
367
- root.render(<Foo />);
368
- });
369
- }).toErrorDev(
360
+ await act(async () => {
361
+ root.render(<Foo />);
362
+ });
363
+ assertConsoleErrorDev([
364
'Using Iterators as children is unsupported and will likely yield ' +
365
'unexpected results because enumerating a generator mutates it. ' +
366
'You may convert it to an array with `Array.from()` or the ' +
@@ -374,7 +368,7 @@ describe('ReactMultiChild', () => {
368
'Iterable that can iterate multiple times over the same items.\n' +
369
' in div (at **)\n' +
370
' in Foo (at **)',
377
- );
371
+ ]);
372
373
expect(container.textContent).toBe('HelloWorld');
374
@@ -407,18 +401,17 @@ describe('ReactMultiChild', () => {
401
402
const container = document.createElement('div');
403
const root = ReactDOMClient.createRoot(container);
410
- await expect(async () => {
411
- await act(async () => {
412
- root.render(<Foo />);
413
- });
414
- }).toErrorDev(
404
+ await act(async () => {
405
+ root.render(<Foo />);
406
+ });
407
+ assertConsoleErrorDev([
408
'Using Iterators as children is unsupported and will likely yield ' +
409
'unexpected results because enumerating a generator mutates it. ' +
410
'You may convert it to an array with `Array.from()` or the ' +
411
'`[...spread]` operator before rendering. You can also use an ' +
412
'Iterable that can iterate multiple times over the same items.\n' +
413
' in Foo (at **)',
421
- );
414
+ ]);
415
416
expect(container.textContent).toBe('HelloWorld');
417
packages/react-dom/src/__tests__/ReactMultiChildText-test.js
+96
-91
@@ -80,98 +80,103 @@ describe('ReactMultiChildText', () => {
80
jest.setTimeout(30000);
81
82
it('should correctly handle all possible children for render and update', async () => {
83
- await expect(async () => {
84
- // prettier-ignore
85
- await testAllPermutations([
86
- // basic values
87
- undefined, [],
88
- null, [],
89
- false, [],
90
- true, [],
91
- 0, '0',
92
- 1.2, '1.2',
93
- '', [],
94
- 'foo', 'foo',
95
-
96
- [], [],
97
- [undefined], [],
98
- [null], [],
99
- [false], [],
100
- [true], [],
101
- [0], ['0'],
102
- [1.2], ['1.2'],
103
- [''], [],
104
- ['foo'], ['foo'],
105
- [<div />], [<div />],
106
-
107
- // two adjacent values
108
- [true, 0], ['0'],
109
- [0, 0], ['0', '0'],
110
- [1.2, 0], ['1.2', '0'],
111
- [0, ''], ['0', ''],
112
- ['foo', 0], ['foo', '0'],
113
- [0, <div />], ['0', <div />],
114
-
115
- [true, 1.2], ['1.2'],
116
- [1.2, 0], ['1.2', '0'],
117
- [1.2, 1.2], ['1.2', '1.2'],
118
- [1.2, ''], ['1.2', ''],
119
- ['foo', 1.2], ['foo', '1.2'],
120
- [1.2, <div />], ['1.2', <div />],
121
-
122
- [true, ''], [''],
123
- ['', 0], ['', '0'],
124
- [1.2, ''], ['1.2', ''],
125
- ['', ''], ['', ''],
126
- ['foo', ''], ['foo', ''],
127
- ['', <div />], ['', <div />],
128
-
129
- [true, 'foo'], ['foo'],
130
- ['foo', 0], ['foo', '0'],
131
- [1.2, 'foo'], ['1.2', 'foo'],
132
- ['foo', ''], ['foo', ''],
133
- ['foo', 'foo'], ['foo', 'foo'],
134
- ['foo', <div />], ['foo', <div />],
135
-
136
- // values separated by an element
137
- [true, <div />, true], [<div />],
138
- [1.2, <div />, 1.2], ['1.2', <div />, '1.2'],
139
- ['', <div />, ''], ['', <div />, ''],
140
- ['foo', <div />, 'foo'], ['foo', <div />, 'foo'],
141
-
142
- [true, 1.2, <div />, '', 'foo'], ['1.2', <div />, '', 'foo'],
143
- [1.2, '', <div />, 'foo', true], ['1.2', '', <div />, 'foo'],
144
- ['', 'foo', <div />, true, 1.2], ['', 'foo', <div />, '1.2'],
145
-
146
- [true, 1.2, '', <div />, 'foo', true, 1.2], ['1.2', '', <div />, 'foo', '1.2'],
147
- ['', 'foo', true, <div />, 1.2, '', 'foo'], ['', 'foo', <div />, '1.2', '', 'foo'],
148
-
149
- // values inside arrays
150
- [[true], [true]], [],
151
- [[1.2], [1.2]], ['1.2', '1.2'],
152
- [[''], ['']], ['', ''],
153
- [['foo'], ['foo']], ['foo', 'foo'],
154
- [[<div />], [<div />]], [<div />, <div />],
155
-
156
- [[true, 1.2, <div />], '', 'foo'], ['1.2', <div />, '', 'foo'],
157
- [1.2, '', [<div />, 'foo', true]], ['1.2', '', <div />, 'foo'],
158
- ['', ['foo', <div />, true], 1.2], ['', 'foo', <div />, '1.2'],
159
-
160
- [true, [1.2, '', <div />, 'foo'], true, 1.2], ['1.2', '', <div />, 'foo', '1.2'],
161
- ['', 'foo', [true, <div />, 1.2, ''], 'foo'], ['', 'foo', <div />, '1.2', '', 'foo'],
162
-
163
- // values inside elements
164
- [<div>{true}{1.2}{<div />}</div>, '', 'foo'], [<div />, '', 'foo'],
165
- [1.2, '', <div>{<div />}{'foo'}{true}</div>], ['1.2', '', <div />],
166
- ['', <div>{'foo'}{<div />}{true}</div>, 1.2], ['', <div />, '1.2'],
167
-
168
- [true, <div>{1.2}{''}{<div />}{'foo'}</div>, true, 1.2], [<div />, '1.2'],
169
- ['', 'foo', <div>{true}{<div />}{1.2}{''}</div>, 'foo'], ['', 'foo', <div />, 'foo'],
170
- ]);
171
- }).toErrorDev([
172
- 'Each child in a list should have a unique "key" prop.',
173
- 'Each child in a list should have a unique "key" prop.',
83
+ spyOnDev(console, 'error').mockImplementation(() => {});
84
+ // prettier-ignore
85
+ await testAllPermutations([
86
+ // basic values
87
+ undefined, [],
88
+ null, [],
89
+ false, [],
90
+ true, [],
91
+ 0, '0',
92
+ 1.2, '1.2',
93
+ '', [],
94
+ 'foo', 'foo',
95
+
96
+ [], [],
97
+ [undefined], [],
98
+ [null], [],
99
+ [false], [],
100
+ [true], [],
101
+ [0], ['0'],
102
+ [1.2], ['1.2'],
103
+ [''], [],
104
+ ['foo'], ['foo'],
105
+ [<div />], [<div />],
106
+
107
+ // two adjacent values
108
+ [true, 0], ['0'],
109
+ [0, 0], ['0', '0'],
110
+ [1.2, 0], ['1.2', '0'],
111
+ [0, ''], ['0', ''],
112
+ ['foo', 0], ['foo', '0'],
113
+ [0, <div />], ['0', <div />],
114
+
115
+ [true, 1.2], ['1.2'],
116
+ [1.2, 0], ['1.2', '0'],
117
+ [1.2, 1.2], ['1.2', '1.2'],
118
+ [1.2, ''], ['1.2', ''],
119
+ ['foo', 1.2], ['foo', '1.2'],
120
+ [1.2, <div />], ['1.2', <div />],
121
+
122
+ [true, ''], [''],
123
+ ['', 0], ['', '0'],
124
+ [1.2, ''], ['1.2', ''],
125
+ ['', ''], ['', ''],
126
+ ['foo', ''], ['foo', ''],
127
+ ['', <div />], ['', <div />],
128
+
129
+ [true, 'foo'], ['foo'],
130
+ ['foo', 0], ['foo', '0'],
131
+ [1.2, 'foo'], ['1.2', 'foo'],
132
+ ['foo', ''], ['foo', ''],
133
+ ['foo', 'foo'], ['foo', 'foo'],
134
+ ['foo', <div />], ['foo', <div />],
135
+
136
+ // values separated by an element
137
+ [true, <div />, true], [<div />],
138
+ [1.2, <div />, 1.2], ['1.2', <div />, '1.2'],
139
+ ['', <div />, ''], ['', <div />, ''],
140
+ ['foo', <div />, 'foo'], ['foo', <div />, 'foo'],
141
+
142
+ [true, 1.2, <div />, '', 'foo'], ['1.2', <div />, '', 'foo'],
143
+ [1.2, '', <div />, 'foo', true], ['1.2', '', <div />, 'foo'],
144
+ ['', 'foo', <div />, true, 1.2], ['', 'foo', <div />, '1.2'],
145
+
146
+ [true, 1.2, '', <div />, 'foo', true, 1.2], ['1.2', '', <div />, 'foo', '1.2'],
147
+ ['', 'foo', true, <div />, 1.2, '', 'foo'], ['', 'foo', <div />, '1.2', '', 'foo'],
148
+
149
+ // values inside arrays
150
+ [[true], [true]], [],
151
+ [[1.2], [1.2]], ['1.2', '1.2'],
152
+ [[''], ['']], ['', ''],
153
+ [['foo'], ['foo']], ['foo', 'foo'],
154
+ [[<div />], [<div />]], [<div />, <div />],
155
+
156
+ [[true, 1.2, <div />], '', 'foo'], ['1.2', <div />, '', 'foo'],
157
+ [1.2, '', [<div />, 'foo', true]], ['1.2', '', <div />, 'foo'],
158
+ ['', ['foo', <div />, true], 1.2], ['', 'foo', <div />, '1.2'],
159
+
160
+ [true, [1.2, '', <div />, 'foo'], true, 1.2], ['1.2', '', <div />, 'foo', '1.2'],
161
+ ['', 'foo', [true, <div />, 1.2, ''], 'foo'], ['', 'foo', <div />, '1.2', '', 'foo'],
162
+
163
+ // values inside elements
164
+ [<div>{true}{1.2}{<div />}</div>, '', 'foo'], [<div />, '', 'foo'],
165
+ [1.2, '', <div>{<div />}{'foo'}{true}</div>], ['1.2', '', <div />],
166
+ ['', <div>{'foo'}{<div />}{true}</div>, 1.2], ['', <div />, '1.2'],
167
+
168
+ [true, <div>{1.2}{''}{<div />}{'foo'}</div>, true, 1.2], [<div />, '1.2'],
169
+ ['', 'foo', <div>{true}{<div />}{1.2}{''}</div>, 'foo'], ['', 'foo', <div />, 'foo'],
170
]);
171
+ if (__DEV__) {
172
+ expect(console.error).toHaveBeenCalledTimes(2);
173
+ expect(console.error.mock.calls[0][0]).toMatch(
174
+ 'Each child in a list should have a unique "key" prop.',
175
+ );
176
+ expect(console.error.mock.calls[1][0]).toMatch(
177
+ 'Each child in a list should have a unique "key" prop.',
178
+ );
179
+ }
180
});
181
182
it('should correctly handle bigint children for render and update', async () => {
packages/react-dom/src/__tests__/ReactRenderDocument-test.js
+39
-20
@@ -16,6 +16,7 @@ let ReactDOMServer;
16
let act;
17
let Scheduler;
18
let assertLog;
19
+let assertConsoleErrorDev;
20
21
function getTestDocument(markup) {
22
const doc = document.implementation.createHTMLDocument('');
@@ -48,6 +49,8 @@ describe('rendering React components at document', () => {
49
act = require('internal-test-utils').act;
50
assertLog = require('internal-test-utils').assertLog;
51
Scheduler = require('scheduler');
52
+ assertConsoleErrorDev =
53
+ require('internal-test-utils').assertConsoleErrorDev;
54
});
55
56
describe('with new explicit hydration API', () => {
@@ -269,30 +272,46 @@ describe('rendering React components at document', () => {
272
const favorSafetyOverHydrationPerf = gate(
273
flags => flags.favorSafetyOverHydrationPerf,
274
);
272
- expect(() => {
273
- ReactDOM.flushSync(() => {
274
- ReactDOMClient.hydrateRoot(
275
- testDocument,
276
- <Component text="Hello world" />,
277
- {
278
- onRecoverableError: error => {
279
- Scheduler.log(
280
- 'onRecoverableError: ' + normalizeError(error.message),
281
- );
282
- if (error.cause) {
283
- Scheduler.log(
284
- 'Cause: ' + normalizeError(error.cause.message),
285
- );
286
- }
287
- },
275
+ ReactDOM.flushSync(() => {
276
+ ReactDOMClient.hydrateRoot(
277
+ testDocument,
278
+ <Component text="Hello world" />,
279
+ {
280
+ onRecoverableError: error => {
281
+ Scheduler.log(
282
+ 'onRecoverableError: ' + normalizeError(error.message),
283
+ );
284
+ if (error.cause) {
285
+ Scheduler.log('Cause: ' + normalizeError(error.cause.message));
286
+ }
287
},
289
- );
290
- });
291
- }).toErrorDev(
288
+ },
289
+ );
290
+ });
291
+ assertConsoleErrorDev(
292
favorSafetyOverHydrationPerf
293
? []
294
: [
295
- "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.",
295
+ "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. " +
296
+ "This won't be patched up. This can happen if a SSR-ed Client Component used:\n" +
297
+ '\n' +
298
+ "- A server/client branch `if (typeof window !== 'undefined')`.\n" +
299
+ "- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" +
300
+ "- Date formatting in a user's locale which doesn't match the server.\n" +
301
+ '- External changing data without sending a snapshot of it along with the HTML.\n' +
302
+ '- Invalid HTML tag nesting.\n\nIt can also happen if the client has a browser extension ' +
303
+ 'installed which messes with the HTML before React loaded.\n' +
304
+ '\n' +
305
+ 'https://react.dev/link/hydration-mismatch\n' +
306
+ '\n' +
307
+ ' <Component text="Hello world">\n' +
308
+ ' <html>\n' +
309
+ ' <head>\n' +
310
+ ' <body>\n' +
311
+ '+ Hello world\n' +
312
+ '- Goodbye world\n' +
313
+ '+ Hello world\n' +
314
+ '- Goodbye world\n',
315
],
316
{withoutStack: true},
317
);
packages/react-dom/src/__tests__/ReactServerRendering-test.js
+126
-86
@@ -14,6 +14,7 @@ let React;
14
let ReactDOMServer;
15
let PropTypes;
16
let ReactSharedInternals;
17
+let assertConsoleErrorDev;
18
19
describe('ReactDOMServer', () => {
20
beforeEach(() => {
@@ -21,6 +22,8 @@ describe('ReactDOMServer', () => {
22
React = require('react');
23
PropTypes = require('prop-types');
24
ReactDOMServer = require('react-dom/server');
25
+ assertConsoleErrorDev =
26
+ require('internal-test-utils').assertConsoleErrorDev;
27
ReactSharedInternals =
28
React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
29
});
@@ -159,15 +162,18 @@ describe('ReactDOMServer', () => {
162
});
163
164
it('should not crash on poisoned hasOwnProperty', () => {
162
- let html;
163
- expect(
164
- () =>
165
- (html = ReactDOMServer.renderToString(
166
- <div hasOwnProperty="poison">
167
- <span unknown="test" />
168
- </div>,
169
- )),
170
- ).toErrorDev(['React does not recognize the `hasOwnProperty` prop']);
165
+ const html = ReactDOMServer.renderToString(
166
+ <div hasOwnProperty="poison">
167
+ <span unknown="test" />
168
+ </div>,
169
+ );
170
+ assertConsoleErrorDev([
171
+ 'React does not recognize the `hasOwnProperty` prop on a DOM element. ' +
172
+ 'If you intentionally want it to appear in the DOM as a custom attribute, ' +
173
+ 'spell it as lowercase `hasownproperty` instead. ' +
174
+ 'If you accidentally passed it from a parent component, remove it from the DOM element.\n' +
175
+ ' in div (at **)',
176
+ ]);
177
expect(html).toContain('<span unknown="test">');
178
});
179
});
@@ -371,16 +377,18 @@ describe('ReactDOMServer', () => {
377
text: PropTypes.string,
378
};
379
374
- let markup;
375
- expect(() => {
376
- markup = ReactDOMServer.renderToStaticMarkup(
377
- <ContextProvider>
378
- <Component />
379
- </ContextProvider>,
380
- );
381
- }).toErrorDev([
382
- 'ContextProvider uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
383
- 'Component uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
380
+ const markup = ReactDOMServer.renderToStaticMarkup(
381
+ <ContextProvider>
382
+ <Component />
383
+ </ContextProvider>,
384
+ );
385
+ assertConsoleErrorDev([
386
+ 'ContextProvider uses the legacy childContextTypes API which will soon be removed. ' +
387
+ 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
388
+ ' in ContextProvider (at **)',
389
+ 'Component uses the legacy contextTypes API which will soon be removed. ' +
390
+ 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
391
+ ' in Component (at **)',
392
]);
393
expect(markup).toContain('hello, world');
394
});
@@ -597,10 +605,15 @@ describe('ReactDOMServer', () => {
605
}
606
607
ReactDOMServer.renderToString(<Foo />);
600
- expect(() => jest.runOnlyPendingTimers()).toErrorDev(
601
- 'Can only update a mounting component.' +
602
- ' This usually means you called setState() outside componentWillMount() on the server.' +
603
- ' This is a no-op.\n\nPlease check the code for the Foo component.',
608
+ jest.runOnlyPendingTimers();
609
+ assertConsoleErrorDev(
610
+ [
611
+ 'Can only update a mounting component. ' +
612
+ 'This usually means you called setState() outside componentWillMount() on the server. ' +
613
+ 'This is a no-op.\n' +
614
+ '\n' +
615
+ 'Please check the code for the Foo component.',
616
+ ],
617
{withoutStack: true},
618
);
619
@@ -625,10 +638,15 @@ describe('ReactDOMServer', () => {
638
}
639
640
ReactDOMServer.renderToString(<Baz />);
628
- expect(() => jest.runOnlyPendingTimers()).toErrorDev(
629
- 'Can only update a mounting component. ' +
630
- 'This usually means you called forceUpdate() outside componentWillMount() on the server. ' +
631
- 'This is a no-op.\n\nPlease check the code for the Baz component.',
641
+ jest.runOnlyPendingTimers();
642
+ assertConsoleErrorDev(
643
+ [
644
+ 'Can only update a mounting component. ' +
645
+ 'This usually means you called forceUpdate() outside componentWillMount() on the server. ' +
646
+ 'This is a no-op.\n' +
647
+ '\n' +
648
+ 'Please check the code for the Baz component.',
649
+ ],
650
{withoutStack: true},
651
);
652
const markup = ReactDOMServer.renderToStaticMarkup(<Baz />);
@@ -722,41 +740,50 @@ describe('ReactDOMServer', () => {
740
// Make sure namespace passes through composites
741
return <g>{props.children}</g>;
742
}
725
- expect(() =>
726
- ReactDOMServer.renderToStaticMarkup(
727
- <div>
728
- <inPUT />
729
- <svg>
730
- <CompositeG>
731
- <linearGradient />
732
- <foreignObject>
733
- {/* back to HTML */}
734
- <iFrame />
735
- </foreignObject>
736
- </CompositeG>
737
- </svg>
738
- </div>,
739
- ),
740
- ).toErrorDev([
743
+ ReactDOMServer.renderToStaticMarkup(
744
+ <div>
745
+ <inPUT />
746
+ <svg>
747
+ <CompositeG>
748
+ <linearGradient />
749
+ <foreignObject>
750
+ {/* back to HTML */}
751
+ <iFrame />
752
+ </foreignObject>
753
+ </CompositeG>
754
+ </svg>
755
+ </div>,
756
+ );
757
+ assertConsoleErrorDev([
758
'<inPUT /> is using incorrect casing. ' +
759
'Use PascalCase for React components, ' +
743
- 'or lowercase for HTML elements.',
760
+ 'or lowercase for HTML elements.\n' +
761
+ ' in inPUT (at **)' +
762
+ (gate('enableOwnerStacks') ? '' : '\n in div (at **)'),
763
// linearGradient doesn't warn
764
'<iFrame /> is using incorrect casing. ' +
765
'Use PascalCase for React components, ' +
747
- 'or lowercase for HTML elements.',
766
+ 'or lowercase for HTML elements.\n' +
767
+ ' in iFrame (at **)' +
768
+ (gate('enableOwnerStacks')
769
+ ? ''
770
+ : '\n in foreignObject (at **)' +
771
+ '\n in g (at **)' +
772
+ '\n in CompositeG (at **)' +
773
+ '\n in svg (at **)' +
774
+ '\n in div (at **)'),
775
]);
776
});
777
778
it('should warn about contentEditable and children', () => {
752
- expect(() =>
753
- ReactDOMServer.renderToString(<div contentEditable={true} children="" />),
754
- ).toErrorDev(
779
+ ReactDOMServer.renderToString(<div contentEditable={true} children="" />);
780
+ assertConsoleErrorDev([
781
'A component is `contentEditable` and contains `children` ' +
782
'managed by React. It is now your responsibility to guarantee that ' +
783
'none of those nodes are unexpectedly modified or duplicated. This ' +
758
- 'is probably not intentional.\n in div (at **)',
759
- );
784
+ 'is probably not intentional.\n' +
785
+ ' in div (at **)',
786
+ ]);
787
});
788
789
it('should warn when server rendering a class with a render method that does not extend React.Component', () => {
@@ -766,15 +793,15 @@ describe('ReactDOMServer', () => {
793
}
794
}
795
769
- expect(() => {
770
- expect(() =>
771
- ReactDOMServer.renderToString(<ClassWithRenderNotExtended />),
772
- ).toThrow(TypeError);
773
- }).toErrorDev(
796
+ expect(() =>
797
+ ReactDOMServer.renderToString(<ClassWithRenderNotExtended />),
798
+ ).toThrow(TypeError);
799
+ assertConsoleErrorDev([
800
'The <ClassWithRenderNotExtended /> component appears to have a render method, ' +
801
"but doesn't extend React.Component. This is likely to cause errors. " +
776
- 'Change ClassWithRenderNotExtended to extend React.Component instead.',
777
- );
802
+ 'Change ClassWithRenderNotExtended to extend React.Component instead.\n' +
803
+ ' in ClassWithRenderNotExtended (at **)',
804
+ ]);
805
806
// Test deduplication
807
expect(() => {
@@ -839,7 +866,8 @@ describe('ReactDOMServer', () => {
866
);
867
}
868
842
- expect(() => ReactDOMServer.renderToString(<App />)).toErrorDev([
869
+ ReactDOMServer.renderToString(<App />);
870
+ assertConsoleErrorDev([
871
'Invalid ARIA attribute `ariaTypo`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
872
(gate(flags => flags.enableOwnerStacks)
873
? ' in span (at **)\n' +
@@ -897,7 +925,8 @@ describe('ReactDOMServer', () => {
925
);
926
}
927
900
- expect(() => ReactDOMServer.renderToString(<App />)).toErrorDev([
928
+ ReactDOMServer.renderToString(<App />);
929
+ assertConsoleErrorDev([
930
// ReactDOMServer(App > div > span)
931
'Invalid ARIA attribute `ariaTypo`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
932
(gate(flags => flags.enableOwnerStacks)
@@ -907,12 +936,23 @@ describe('ReactDOMServer', () => {
936
' in App (at **)'),
937
// ReactDOMServer(App > div > Child) >>> ReactDOMServer(App2) >>> ReactDOMServer(blink)
938
'Invalid ARIA attribute `ariaTypo2`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
910
- ' in blink (at **)',
939
+ (gate(flags => flags.enableOwnerStacks)
940
+ ? ' in blink (at **)\n' +
941
+ ' in App2 (at **)\n' +
942
+ ' in Child (at **)\n' +
943
+ ' in App (at **)'
944
+ : ' in blink (at **)'),
945
// ReactDOMServer(App > div > Child) >>> ReactDOMServer(App2 > Child2 > span)
946
'Invalid ARIA attribute `ariaTypo3`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
913
- ' in span (at **)\n' +
914
- ' in Child2 (at **)\n' +
915
- ' in App2 (at **)',
947
+ (gate(flags => flags.enableOwnerStacks)
948
+ ? ' in span (at **)\n' +
949
+ ' in Child2 (at **)\n' +
950
+ ' in App2 (at **)\n' +
951
+ ' in Child (at **)\n' +
952
+ ' in App (at **)'
953
+ : ' in span (at **)\n' +
954
+ ' in Child2 (at **)\n' +
955
+ ' in App2 (at **)'),
956
// ReactDOMServer(App > div > Child > span)
957
'Invalid ARIA attribute `ariaTypo4`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
958
(gate(flags => flags.enableOwnerStacks)
@@ -943,13 +983,13 @@ describe('ReactDOMServer', () => {
983
}
984
}
985
946
- expect(() => {
947
- ReactDOMServer.renderToString(<ComponentA />);
948
- }).toErrorDev(
986
+ ReactDOMServer.renderToString(<ComponentA />);
987
+ assertConsoleErrorDev([
988
'ComponentA defines an invalid contextType. ' +
989
'contextType should point to the Context object returned by React.createContext(). ' +
951
- 'Did you accidentally pass the Context.Consumer instead?',
952
- );
990
+ 'Did you accidentally pass the Context.Consumer instead?\n' +
991
+ ' in ComponentA (at **)',
992
+ ]);
993
994
// Warnings should be deduped by component type
995
ReactDOMServer.renderToString(<ComponentA />);
@@ -988,17 +1028,17 @@ describe('ReactDOMServer', () => {
1028
}
1029
1030
expect(() => {
991
- expect(() => {
992
- ReactDOMServer.renderToString(<Foo />);
993
- }).toThrow("Cannot read property 'world' of undefined");
994
- }).toErrorDev(
1031
+ ReactDOMServer.renderToString(<Foo />);
1032
+ }).toThrow("Cannot read property 'world' of undefined");
1033
+ assertConsoleErrorDev([
1034
'Foo defines an invalid contextType. ' +
1035
'contextType should point to the Context object returned by React.createContext(). ' +
1036
'However, it is set to undefined. ' +
1037
'This can be caused by a typo or by mixing up named and default imports. ' +
1038
'This can also happen due to a circular dependency, ' +
1000
- 'so try moving the createContext() call to a separate file.',
1001
- );
1039
+ 'so try moving the createContext() call to a separate file.\n' +
1040
+ ' in Foo (at **)',
1041
+ ]);
1042
});
1043
1044
it('should warn when class contextType is an object', () => {
@@ -1014,14 +1054,14 @@ describe('ReactDOMServer', () => {
1054
}
1055
1056
expect(() => {
1017
- expect(() => {
1018
- ReactDOMServer.renderToString(<Foo />);
1019
- }).toThrow("Cannot read property 'hello' of undefined");
1020
- }).toErrorDev(
1057
+ ReactDOMServer.renderToString(<Foo />);
1058
+ }).toThrow("Cannot read property 'hello' of undefined");
1059
+ assertConsoleErrorDev([
1060
'Foo defines an invalid contextType. ' +
1061
'contextType should point to the Context object returned by React.createContext(). ' +
1023
- 'However, it is set to an object with keys {x, y}.',
1024
- );
1062
+ 'However, it is set to an object with keys {x, y}.\n' +
1063
+ ' in Foo (at **)',
1064
+ ]);
1065
});
1066
1067
it('should warn when class contextType is a primitive', () => {
@@ -1033,14 +1073,14 @@ describe('ReactDOMServer', () => {
1073
}
1074
1075
expect(() => {
1036
- expect(() => {
1037
- ReactDOMServer.renderToString(<Foo />);
1038
- }).toThrow("Cannot read property 'world' of undefined");
1039
- }).toErrorDev(
1076
+ ReactDOMServer.renderToString(<Foo />);
1077
+ }).toThrow("Cannot read property 'world' of undefined");
1078
+ assertConsoleErrorDev([
1079
'Foo defines an invalid contextType. ' +
1080
'contextType should point to the Context object returned by React.createContext(). ' +
1042
- 'However, it is set to a string.',
1043
- );
1081
+ 'However, it is set to a string.\n' +
1082
+ ' in Foo (at **)',
1083
+ ]);
1084
});
1085
1086
describe('custom element server rendering', () => {
packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js
+186
-79
@@ -17,6 +17,8 @@ let ReactDOMServer;
17
let ReactDOMServerBrowser;
18
let waitForAll;
19
let act;
20
+let assertConsoleErrorDev;
21
+let assertConsoleWarnDev;
22
23
// These tests rely both on ReactDOMServer and ReactDOM.
24
// If a test only needs ReactDOMServer, put it in ReactServerRendering-test instead.
@@ -32,6 +34,8 @@ describe('ReactDOMServerHydration', () => {
34
const InternalTestUtils = require('internal-test-utils');
35
waitForAll = InternalTestUtils.waitForAll;
36
act = InternalTestUtils.act;
37
+ assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev;
38
+ assertConsoleWarnDev = InternalTestUtils.assertConsoleWarnDev;
39
});
40
41
it('should have the correct mounting behavior', async () => {
@@ -126,26 +130,40 @@ describe('ReactDOMServerHydration', () => {
130
const favorSafetyOverHydrationPerf = gate(
131
flags => flags.favorSafetyOverHydrationPerf,
132
);
129
- await expect(async () => {
130
- root = await act(() => {
131
- return ReactDOMClient.hydrateRoot(
132
- element,
133
- <TestComponent
134
- name="y"
135
- ref={current => {
136
- instance = current;
137
- }}
138
- />,
139
- {
140
- onRecoverableError: error => {},
141
- },
142
- );
143
- });
144
- }).toErrorDev(
133
+ root = await act(() => {
134
+ return ReactDOMClient.hydrateRoot(
135
+ element,
136
+ <TestComponent
137
+ name="y"
138
+ ref={current => {
139
+ instance = current;
140
+ }}
141
+ />,
142
+ {
143
+ onRecoverableError: error => {},
144
+ },
145
+ );
146
+ });
147
+ assertConsoleErrorDev(
148
favorSafetyOverHydrationPerf
149
? []
150
: [
148
- "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.",
151
+ "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. " +
152
+ "This won't be patched up. This can happen if a SSR-ed Client Component used:\n" +
153
+ '\n' +
154
+ "- A server/client branch `if (typeof window !== 'undefined')`.\n" +
155
+ "- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" +
156
+ "- Date formatting in a user's locale which doesn't match the server.\n" +
157
+ '- External changing data without sending a snapshot of it along with the HTML.\n' +
158
+ '- Invalid HTML tag nesting.\n\nIt can also happen if the client has a browser extension ' +
159
+ 'installed which messes with the HTML before React loaded.\n' +
160
+ '\n' +
161
+ 'https://react.dev/link/hydration-mismatch\n' +
162
+ '\n' +
163
+ ' <TestComponent name="y" ref={function ref}>\n' +
164
+ ' <span ref={{current:null}} onClick={function}>\n' +
165
+ '+ y\n' +
166
+ '- x\n',
167
],
168
{withoutStack: true},
169
);
@@ -224,21 +242,34 @@ describe('ReactDOMServerHydration', () => {
242
const favorSafetyOverHydrationPerf = gate(
243
flags => flags.favorSafetyOverHydrationPerf,
244
);
227
- await expect(async () => {
228
- await act(() => {
229
- ReactDOMClient.hydrateRoot(
230
- element,
231
- <button autoFocus={false} onFocus={onFocusAfterHydration}>
232
- client
233
- </button>,
234
- {onRecoverableError: error => {}},
235
- );
236
- });
237
- }).toErrorDev(
245
+ await act(() => {
246
+ ReactDOMClient.hydrateRoot(
247
+ element,
248
+ <button autoFocus={false} onFocus={onFocusAfterHydration}>
249
+ client
250
+ </button>,
251
+ {onRecoverableError: error => {}},
252
+ );
253
+ });
254
+ assertConsoleErrorDev(
255
favorSafetyOverHydrationPerf
256
? []
257
: [
241
- "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.",
258
+ "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. " +
259
+ "This won't be patched up. This can happen if a SSR-ed Client Component used:\n" +
260
+ '\n' +
261
+ "- A server/client branch `if (typeof window !== 'undefined')`.\n" +
262
+ "- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" +
263
+ "- Date formatting in a user's locale which doesn't match the server.\n" +
264
+ '- External changing data without sending a snapshot of it along with the HTML.\n' +
265
+ '- Invalid HTML tag nesting.\n\nIt can also happen if the client has a browser extension ' +
266
+ 'installed which messes with the HTML before React loaded.\n' +
267
+ '\n' +
268
+ 'https://react.dev/link/hydration-mismatch\n' +
269
+ '\n' +
270
+ ' <button autoFocus={false} onFocus={function mockConstructor}>\n' +
271
+ '+ client\n' +
272
+ '- server\n',
273
],
274
{withoutStack: true},
275
);
@@ -255,17 +286,37 @@ describe('ReactDOMServerHydration', () => {
286
expect(element.firstChild.style.textDecoration).toBe('none');
287
expect(element.firstChild.style.color).toBe('black');
288
258
- await expect(async () => {
259
- await act(() => {
260
- ReactDOMClient.hydrateRoot(
261
- element,
262
- <div
263
- style={{textDecoration: 'none', color: 'white', height: '10px'}}
264
- />,
265
- );
266
- });
267
- }).toErrorDev(
268
- "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.",
289
+ await act(() => {
290
+ ReactDOMClient.hydrateRoot(
291
+ element,
292
+ <div
293
+ style={{textDecoration: 'none', color: 'white', height: '10px'}}
294
+ />,
295
+ );
296
+ });
297
+ assertConsoleErrorDev(
298
+ [
299
+ "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. " +
300
+ "This won't be patched up. This can happen if a SSR-ed Client Component used:\n" +
301
+ '\n' +
302
+ "- A server/client branch `if (typeof window !== 'undefined')`.\n" +
303
+ "- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" +
304
+ "- Date formatting in a user's locale which doesn't match the server.\n" +
305
+ '- External changing data without sending a snapshot of it along with the HTML.\n' +
306
+ '- Invalid HTML tag nesting.\n\nIt can also happen if the client has a browser extension ' +
307
+ 'installed which messes with the HTML before React loaded.\n' +
308
+ '\n' +
309
+ 'https://react.dev/link/hydration-mismatch\n' +
310
+ '\n' +
311
+ ' <div\n style={{\n+ textDecoration: "none"\n' +
312
+ '+ color: "white"\n' +
313
+ '- color: "black"\n' +
314
+ '+ height: "10px"\n' +
315
+ '- height: "10px"\n' +
316
+ '- text-decoration: "none"\n' +
317
+ ' }}\n' +
318
+ ' >\n',
319
+ ],
320
{withoutStack: true},
321
);
322
});
@@ -303,17 +354,39 @@ describe('ReactDOMServerHydration', () => {
354
element.innerHTML =
355
'<div style="text-decoration: none; color: black; height: 10px;"></div>';
356
306
- await expect(async () => {
307
- await act(() => {
308
- ReactDOMClient.hydrateRoot(
309
- element,
310
- <div
311
- style={{textDecoration: 'none', color: 'black', height: '10px'}}
312
- />,
313
- );
314
- });
315
- }).toErrorDev(
316
- "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.",
357
+ await act(() => {
358
+ ReactDOMClient.hydrateRoot(
359
+ element,
360
+ <div
361
+ style={{textDecoration: 'none', color: 'black', height: '10px'}}
362
+ />,
363
+ );
364
+ });
365
+ assertConsoleErrorDev(
366
+ [
367
+ "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. " +
368
+ "This won't be patched up. This can happen if a SSR-ed Client Component used:\n" +
369
+ '\n' +
370
+ "- A server/client branch `if (typeof window !== 'undefined')`.\n" +
371
+ "- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" +
372
+ "- Date formatting in a user's locale which doesn't match the server.\n" +
373
+ '- External changing data without sending a snapshot of it along with the HTML.\n' +
374
+ '- Invalid HTML tag nesting.\n\nIt can also happen if the client has a browser extension ' +
375
+ 'installed which messes with the HTML before React loaded.\n' +
376
+ '\n' +
377
+ 'https://react.dev/link/hydration-mismatch\n' +
378
+ '\n' +
379
+ ' <div\n' +
380
+ ' style={{\n' +
381
+ '+ textDecoration: "none"\n' +
382
+ '+ color: "black"\n' +
383
+ '- color: "black"\n' +
384
+ '+ height: "10px"\n' +
385
+ '- height: "10px"\n' +
386
+ '- text-decoration: "none"\n' +
387
+ ' }}\n' +
388
+ ' >\n',
389
+ ],
390
{withoutStack: true},
391
);
392
});
@@ -347,18 +420,38 @@ describe('ReactDOMServerHydration', () => {
420
);
421
422
const element = document.createElement('div');
350
- expect(() => {
351
- element.innerHTML = ReactDOMServer.renderToString(markup);
352
- }).toWarnDev('componentWillMount has been renamed');
423
+ element.innerHTML = ReactDOMServer.renderToString(markup);
424
+ assertConsoleWarnDev([
425
+ 'componentWillMount has been renamed, and is not recommended for use. ' +
426
+ 'See https://react.dev/link/unsafe-component-lifecycles for details.\n' +
427
+ '\n' +
428
+ '* Move code from componentWillMount to componentDidMount (preferred in most cases) or the constructor.\n' +
429
+ '\n' +
430
+ 'Please update the following components: ComponentWithWarning\n' +
431
+ ' in ComponentWithWarning (at **)',
432
+ ]);
433
expect(element.textContent).toBe('Hi');
434
355
- await expect(async () => {
356
- await act(() => {
357
- ReactDOMClient.hydrateRoot(element, markup);
358
- });
359
- }).toWarnDev('componentWillMount has been renamed', {
360
- withoutStack: true,
435
+ await act(() => {
436
+ ReactDOMClient.hydrateRoot(element, markup);
437
});
438
+ assertConsoleWarnDev(
439
+ [
440
+ 'componentWillMount has been renamed, and is not recommended for use. ' +
441
+ 'See https://react.dev/link/unsafe-component-lifecycles for details.\n' +
442
+ '\n' +
443
+ '* Move code with side effects to componentDidMount, and set initial state in the constructor.\n' +
444
+ '* Rename componentWillMount to UNSAFE_componentWillMount to suppress this warning in non-strict mode. ' +
445
+ 'In React 18.x, only the UNSAFE_ name will work. ' +
446
+ 'To rename all deprecated lifecycles to their new names, ' +
447
+ 'you can run `npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n' +
448
+ '\n' +
449
+ 'Please update the following components: ComponentWithWarning',
450
+ ],
451
+ {
452
+ withoutStack: true,
453
+ },
454
+ );
455
expect(element.textContent).toBe('Hi');
456
});
457
@@ -531,21 +624,35 @@ describe('ReactDOMServerHydration', () => {
624
const favorSafetyOverHydrationPerf = gate(
625
flags => flags.favorSafetyOverHydrationPerf,
626
);
534
- await expect(async () => {
535
- await act(() => {
536
- ReactDOMClient.hydrateRoot(
537
- domElement,
538
- <div dangerouslySetInnerHTML={undefined}>
539
- <p>client</p>
540
- </div>,
541
- {onRecoverableError: error => {}},
542
- );
543
- });
544
- }).toErrorDev(
627
+ await act(() => {
628
+ ReactDOMClient.hydrateRoot(
629
+ domElement,
630
+ <div dangerouslySetInnerHTML={undefined}>
631
+ <p>client</p>
632
+ </div>,
633
+ {onRecoverableError: error => {}},
634
+ );
635
+ });
636
+ assertConsoleErrorDev(
637
favorSafetyOverHydrationPerf
638
? []
639
: [
548
- "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.",
640
+ "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. " +
641
+ "This won't be patched up. This can happen if a SSR-ed Client Component used:\n" +
642
+ '\n' +
643
+ "- A server/client branch `if (typeof window !== 'undefined')`.\n" +
644
+ "- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" +
645
+ "- Date formatting in a user's locale which doesn't match the server.\n" +
646
+ '- External changing data without sending a snapshot of it along with the HTML.\n' +
647
+ '- Invalid HTML tag nesting.\n\nIt can also happen if the client has a browser extension ' +
648
+ 'installed which messes with the HTML before React loaded.\n' +
649
+ '\n' +
650
+ 'https://react.dev/link/hydration-mismatch\n' +
651
+ '\n' +
652
+ ' <div dangerouslySetInnerHTML={undefined}>\n' +
653
+ ' <p>\n' +
654
+ '+ client\n' +
655
+ '- server\n',
656
],
657
{withoutStack: true},
658
);
@@ -593,13 +700,13 @@ describe('ReactDOMServerHydration', () => {
700
const jsx = React.createElement('my-custom-element', props);
701
const element = document.createElement('div');
702
element.innerHTML = ReactDOMServer.renderToString(jsx);
596
- await expect(async () => {
597
- await act(() => {
598
- ReactDOMClient.hydrateRoot(element, jsx);
599
- });
600
- }).toErrorDev(
601
- `Assignment to read-only property will result in a no-op: \`${readOnlyProperty}\``,
602
- );
703
+ await act(() => {
704
+ ReactDOMClient.hydrateRoot(element, jsx);
705
+ });
706
+ assertConsoleErrorDev([
707
+ `Assignment to read-only property will result in a no-op: \`${readOnlyProperty}\`
708
+ in my-custom-element (at **)`,
709
+ ]);
710
}
711
});
712
packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js
+60
-10
@@ -13,6 +13,7 @@ let Scheduler;
13
let act;
14
let container;
15
let assertLog;
16
+let assertConsoleErrorDev;
17
18
jest.useRealTimers();
19
@@ -88,6 +89,7 @@ function runActTests(render, unmount, rerender) {
89
90
const InternalTestUtils = require('internal-test-utils');
91
assertLog = InternalTestUtils.assertLog;
92
+ assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev;
93
94
container = document.createElement('div');
95
document.body.appendChild(container);
@@ -205,8 +207,20 @@ function runActTests(render, unmount, rerender) {
207
render(<App />, container);
208
});
209
208
- expect(() => setValue(1)).toErrorDev([
209
- 'An update to App inside a test was not wrapped in act(...).',
210
+ setValue(1);
211
+ assertConsoleErrorDev([
212
+ 'An update to App inside a test was not wrapped in act(...).\n' +
213
+ '\n' +
214
+ 'When testing, code that causes React state updates should be wrapped into act(...):\n' +
215
+ '\n' +
216
+ 'act(() => {\n' +
217
+ ' /* fire events that update state */\n' +
218
+ '});\n' +
219
+ '/* assert on the output */\n' +
220
+ '\n' +
221
+ "This ensures that you're testing the behavior the user would see in the browser. " +
222
+ 'Learn more at https://react.dev/link/wrap-tests-with-act\n' +
223
+ ' in App (at **)',
224
]);
225
});
226
@@ -232,8 +246,20 @@ function runActTests(render, unmount, rerender) {
246
rerender(<App defaultValue={0} />, container);
247
});
248
235
- expect(() => setValue(1)).toErrorDev([
236
- 'An update to App inside a test was not wrapped in act(...).',
249
+ setValue(1);
250
+ assertConsoleErrorDev([
251
+ 'An update to App inside a test was not wrapped in act(...).\n' +
252
+ '\n' +
253
+ 'When testing, code that causes React state updates should be wrapped into act(...):\n' +
254
+ '\n' +
255
+ 'act(() => {\n' +
256
+ ' /* fire events that update state */\n' +
257
+ '});\n' +
258
+ '/* assert on the output */\n' +
259
+ '\n' +
260
+ "This ensures that you're testing the behavior the user would see in the browser. " +
261
+ 'Learn more at https://react.dev/link/wrap-tests-with-act\n' +
262
+ ' in App (at **)',
263
]);
264
});
265
@@ -251,9 +277,21 @@ function runActTests(render, unmount, rerender) {
277
});
278
279
// First show that it does warn
254
- expect(() => setState(1)).toErrorDev(
255
- 'An update to App inside a test was not wrapped in act(...)',
256
- );
280
+ setState(1);
281
+ assertConsoleErrorDev([
282
+ 'An update to App inside a test was not wrapped in act(...).\n' +
283
+ '\n' +
284
+ 'When testing, code that causes React state updates should be wrapped into act(...):\n' +
285
+ '\n' +
286
+ 'act(() => {\n' +
287
+ ' /* fire events that update state */\n' +
288
+ '});\n' +
289
+ '/* assert on the output */\n' +
290
+ '\n' +
291
+ "This ensures that you're testing the behavior the user would see in the browser. " +
292
+ 'Learn more at https://react.dev/link/wrap-tests-with-act\n' +
293
+ ' in App (at **)',
294
+ ]);
295
296
// Now do the same thing again, but disable with the environment flag
297
const prevIsActEnvironment = global.IS_REACT_ACT_ENVIRONMENT;
@@ -266,9 +304,21 @@ function runActTests(render, unmount, rerender) {
304
305
// When the flag is restored to its previous value, it should start
306
// warning again. This shows that React reads the flag each time.
269
- expect(() => setState(3)).toErrorDev(
270
- 'An update to App inside a test was not wrapped in act(...)',
271
- );
307
+ setState(3);
308
+ assertConsoleErrorDev([
309
+ 'An update to App inside a test was not wrapped in act(...).\n' +
310
+ '\n' +
311
+ 'When testing, code that causes React state updates should be wrapped into act(...):\n' +
312
+ '\n' +
313
+ 'act(() => {\n' +
314
+ ' /* fire events that update state */\n' +
315
+ '});\n' +
316
+ '/* assert on the output */\n' +
317
+ '\n' +
318
+ "This ensures that you're testing the behavior the user would see in the browser. " +
319
+ 'Learn more at https://react.dev/link/wrap-tests-with-act\n' +
320
+ ' in App (at **)',
321
+ ]);
322
});
323
324
describe('fake timers', () => {
packages/react-dom/src/__tests__/ReactUpdates-test.js
+78
-63
@@ -18,6 +18,7 @@ let Scheduler;
18
let waitForAll;
19
let waitFor;
20
let assertLog;
21
+let assertConsoleErrorDev;
22
23
describe('ReactUpdates', () => {
24
beforeEach(() => {
@@ -29,6 +30,8 @@ describe('ReactUpdates', () => {
30
ReactDOM.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE
31
.findDOMNode;
32
act = require('internal-test-utils').act;
33
+ assertConsoleErrorDev =
34
+ require('internal-test-utils').assertConsoleErrorDev;
35
Scheduler = require('scheduler');
36
37
const InternalTestUtils = require('internal-test-utils');
@@ -1039,38 +1042,42 @@ describe('ReactUpdates', () => {
1042
root.render(<A ref={current => (component = current)} />);
1043
});
1044
1042
- await expect(
1043
- expect(async () => {
1044
- await act(() => {
1045
- component.setState({}, 'no');
1046
- });
1047
- }).toErrorDev(
1048
- 'Expected the last optional `callback` argument to be ' +
1049
- 'a function. Instead received: no.',
1050
- ),
1051
- ).rejects.toThrowError(
1045
+ await expect(async () => {
1046
+ await act(() => {
1047
+ component.setState({}, 'no');
1048
+ });
1049
+ }).rejects.toThrowError(
1050
'Invalid argument passed as callback. Expected a function. Instead ' +
1051
'received: no',
1052
);
1053
+ assertConsoleErrorDev(
1054
+ [
1055
+ 'Expected the last optional `callback` argument to be ' +
1056
+ 'a function. Instead received: no.',
1057
+ ],
1058
+ {withoutStack: true},
1059
+ );
1060
container = document.createElement('div');
1061
root = ReactDOMClient.createRoot(container);
1062
await act(() => {
1063
root.render(<A ref={current => (component = current)} />);
1064
});
1065
1061
- await expect(
1062
- expect(async () => {
1063
- await act(() => {
1064
- component.setState({}, {foo: 'bar'});
1065
- });
1066
- }).toErrorDev(
1067
- 'Expected the last optional `callback` argument to be ' +
1068
- 'a function. Instead received: [object Object].',
1069
- ),
1070
- ).rejects.toThrowError(
1066
+ await expect(async () => {
1067
+ await act(() => {
1068
+ component.setState({}, {foo: 'bar'});
1069
+ });
1070
+ }).rejects.toThrowError(
1071
'Invalid argument passed as callback. Expected a function. Instead ' +
1072
'received: [object Object]',
1073
);
1074
+ assertConsoleErrorDev(
1075
+ [
1076
+ 'Expected the last optional `callback` argument to be ' +
1077
+ "a function. Instead received: { foo: 'bar' }.",
1078
+ ],
1079
+ {withoutStack: true},
1080
+ );
1081
container = document.createElement('div');
1082
root = ReactDOMClient.createRoot(container);
1083
await act(() => {
@@ -1108,38 +1115,42 @@ describe('ReactUpdates', () => {
1115
root.render(<A ref={current => (component = current)} />);
1116
});
1117
1111
- await expect(
1112
- expect(async () => {
1113
- await act(() => {
1114
- component.forceUpdate('no');
1115
- });
1116
- }).toErrorDev(
1117
- 'Expected the last optional `callback` argument to be ' +
1118
- 'a function. Instead received: no.',
1119
- ),
1120
- ).rejects.toThrowError(
1118
+ await expect(async () => {
1119
+ await act(() => {
1120
+ component.forceUpdate('no');
1121
+ });
1122
+ }).rejects.toThrowError(
1123
'Invalid argument passed as callback. Expected a function. Instead ' +
1124
'received: no',
1125
);
1126
+ assertConsoleErrorDev(
1127
+ [
1128
+ 'Expected the last optional `callback` argument to be ' +
1129
+ 'a function. Instead received: no.',
1130
+ ],
1131
+ {withoutStack: true},
1132
+ );
1133
container = document.createElement('div');
1134
root = ReactDOMClient.createRoot(container);
1135
await act(() => {
1136
root.render(<A ref={current => (component = current)} />);
1137
});
1138
1130
- await expect(
1131
- expect(async () => {
1132
- await act(() => {
1133
- component.forceUpdate({foo: 'bar'});
1134
- });
1135
- }).toErrorDev(
1136
- 'Expected the last optional `callback` argument to be ' +
1137
- 'a function. Instead received: [object Object].',
1138
- ),
1139
- ).rejects.toThrowError(
1139
+ await expect(async () => {
1140
+ await act(() => {
1141
+ component.forceUpdate({foo: 'bar'});
1142
+ });
1143
+ }).rejects.toThrowError(
1144
'Invalid argument passed as callback. Expected a function. Instead ' +
1145
'received: [object Object]',
1146
);
1147
+ assertConsoleErrorDev(
1148
+ [
1149
+ 'Expected the last optional `callback` argument to be ' +
1150
+ "a function. Instead received: { foo: 'bar' }.",
1151
+ ],
1152
+ {withoutStack: true},
1153
+ );
1154
// Make sure the warning is deduplicated and doesn't fire again
1155
container = document.createElement('div');
1156
root = ReactDOMClient.createRoot(container);
@@ -1351,11 +1362,14 @@ describe('ReactUpdates', () => {
1362
1363
const container = document.createElement('div');
1364
const root = ReactDOMClient.createRoot(container);
1354
- await expect(async () => {
1355
- await act(() => {
1356
- root.render(<Foo />);
1357
- });
1358
- }).toErrorDev('Cannot update during an existing state transition');
1365
+ await act(() => {
1366
+ root.render(<Foo />);
1367
+ });
1368
+ assertConsoleErrorDev([
1369
+ 'Cannot update during an existing state transition (such as within `render`). ' +
1370
+ 'Render methods should be a pure function of props and state.\n' +
1371
+ ' in Foo (at **)',
1372
+ ]);
1373
1374
assertLog(['base: 0, memoized: 0', 'base: 1, memoized: 1']);
1375
});
@@ -1798,12 +1812,14 @@ describe('ReactUpdates', () => {
1812
const root = ReactDOMClient.createRoot(container);
1813
1814
await expect(async () => {
1801
- await expect(async () => {
1802
- await act(() => ReactDOM.flushSync(() => root.render(<App />)));
1803
- }).rejects.toThrow('Maximum update depth exceeded');
1804
- }).toErrorDev(
1805
- 'Cannot update a component (`App`) while rendering a different component (`Child`)',
1806
- );
1815
+ await act(() => ReactDOM.flushSync(() => root.render(<App />)));
1816
+ }).rejects.toThrow('Maximum update depth exceeded');
1817
+ assertConsoleErrorDev([
1818
+ 'Cannot update a component (`App`) while rendering a different component (`Child`). ' +
1819
+ 'To locate the bad setState() call inside `Child`, ' +
1820
+ 'follow the stack trace as described in https://react.dev/link/setstate-in-render\n' +
1821
+ ' in App (at **)',
1822
+ ]);
1823
});
1824
1825
it("does not infinite loop if there's an async render phase update on another component", async () => {
@@ -1827,18 +1843,17 @@ describe('ReactUpdates', () => {
1843
const root = ReactDOMClient.createRoot(container);
1844
1845
await expect(async () => {
1830
- let error;
1831
- try {
1832
- await act(() => {
1833
- React.startTransition(() => root.render(<App />));
1834
- });
1835
- } catch (e) {
1836
- error = e;
1837
- }
1838
- expect(error.message).toMatch('Maximum update depth exceeded');
1839
- }).toErrorDev(
1840
- 'Cannot update a component (`App`) while rendering a different component (`Child`)',
1841
- );
1846
+ await act(() => {
1847
+ React.startTransition(() => root.render(<App />));
1848
+ });
1849
+ }).rejects.toThrow('Maximum update depth exceeded');
1850
+
1851
+ assertConsoleErrorDev([
1852
+ 'Cannot update a component (`App`) while rendering a different component (`Child`). ' +
1853
+ 'To locate the bad setState() call inside `Child`, ' +
1854
+ 'follow the stack trace as described in https://react.dev/link/setstate-in-render\n' +
1855
+ ' in App (at **)',
1856
+ ]);
1857
});
1858
1859
// TODO: Replace this branch with @gate pragmas
packages/react-dom/src/__tests__/validateDOMNesting-test.js
+10
-7
@@ -12,6 +12,8 @@
12
const React = require('react');
13
const ReactDOM = require('react-dom');
14
const ReactDOMClient = require('react-dom/client');
15
+const assertConsoleErrorDev =
16
+ require('internal-test-utils').assertConsoleErrorDev;
17
18
function expectWarnings(tags, warnings = [], withoutStack = 0) {
19
tags = [...tags];
@@ -31,13 +33,13 @@ function expectWarnings(tags, warnings = [], withoutStack = 0) {
33
34
const root = ReactDOMClient.createRoot(container);
35
if (warnings.length) {
34
- expect(() => {
35
- ReactDOM.flushSync(() => {
36
- root.render(element);
37
- });
38
- }).toErrorDev(warnings, {
39
- withoutStack,
36
+ ReactDOM.flushSync(() => {
37
+ root.render(element);
38
});
39
+ assertConsoleErrorDev(
40
+ warnings,
41
+ withoutStack > 0 ? {withoutStack} : undefined,
42
+ );
43
}
44
}
45
@@ -164,7 +166,8 @@ describe('validateDOMNesting', () => {
166
' in body (at **)\n' +
167
' in foreignObject (at **)',
168
'You are mounting a new body component when a previous one has not first unmounted. It is an error to render more than one body component at a time and attributes and children of these components will likely fail in unpredictable ways. Please only render a single instance of <body> and if you need to mount a new one, ensure any previous ones have unmounted first.\n' +
167
- ' in body (at **)',
169
+ ' in body (at **)\n' +
170
+ ' in foreignObject (at **)',
171
],
172
);
173
});