@samitouri / QOS-React-1 / commits / 03e4ec2d0f

[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 });