[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
This commit is contained in:
Ricky
2025-01-05 17:10:29 -05:00
committed by GitHub
parent bf883bebbc
commit 03e4ec2d0f
28 changed files with 2058 additions and 1463 deletions

View File

@@ -14,12 +14,15 @@ describe('ReactDOMInvalidARIAHook', () => {
let ReactDOMClient;
let mountComponent;
let act;
let assertConsoleErrorDev;
beforeEach(() => {
jest.resetModules();
React = require('react');
ReactDOMClient = require('react-dom/client');
act = require('internal-test-utils').act;
assertConsoleErrorDev =
require('internal-test-utils').assertConsoleErrorDev;
mountComponent = async function (props) {
const container = document.createElement('div');
@@ -35,46 +38,52 @@ describe('ReactDOMInvalidARIAHook', () => {
await mountComponent({'aria-label': 'Bumble bees'});
});
it('should warn for one invalid aria-* prop', async () => {
await expect(() => mountComponent({'aria-badprop': 'maybe'})).toErrorDev(
await mountComponent({'aria-badprop': 'maybe'});
assertConsoleErrorDev([
'Invalid aria prop `aria-badprop` on <div> tag. ' +
'For details, see https://react.dev/link/invalid-aria-props',
);
'For details, see https://react.dev/link/invalid-aria-props\n' +
' in div (at **)',
]);
});
it('should warn for many invalid aria-* props', async () => {
await expect(() =>
mountComponent({
'aria-badprop': 'Very tall trees',
'aria-malprop': 'Turbulent seas',
}),
).toErrorDev(
await mountComponent({
'aria-badprop': 'Very tall trees',
'aria-malprop': 'Turbulent seas',
});
assertConsoleErrorDev([
'Invalid aria props `aria-badprop`, `aria-malprop` on <div> ' +
'tag. For details, see https://react.dev/link/invalid-aria-props',
);
'tag. For details, see https://react.dev/link/invalid-aria-props\n' +
' in div (at **)',
]);
});
it('should warn for an improperly cased aria-* prop', async () => {
// The valid attribute name is aria-haspopup.
await expect(() => mountComponent({'aria-hasPopup': 'true'})).toErrorDev(
await mountComponent({'aria-hasPopup': 'true'});
assertConsoleErrorDev([
'Unknown ARIA attribute `aria-hasPopup`. ' +
'Did you mean `aria-haspopup`?',
);
'Did you mean `aria-haspopup`?\n' +
' in div (at **)',
]);
});
it('should warn for use of recognized camel case aria attributes', async () => {
// The valid attribute name is aria-haspopup.
await expect(() => mountComponent({ariaHasPopup: 'true'})).toErrorDev(
await mountComponent({ariaHasPopup: 'true'});
assertConsoleErrorDev([
'Invalid ARIA attribute `ariaHasPopup`. ' +
'Did you mean `aria-haspopup`?',
);
'Did you mean `aria-haspopup`?\n' +
' in div (at **)',
]);
});
it('should warn for use of unrecognized camel case aria attributes', async () => {
// The valid attribute name is aria-haspopup.
await expect(() =>
mountComponent({ariaSomethingInvalid: 'true'}),
).toErrorDev(
await mountComponent({ariaSomethingInvalid: 'true'});
assertConsoleErrorDev([
'Invalid ARIA attribute `ariaSomethingInvalid`. ARIA ' +
'attributes follow the pattern aria-* and must be lowercase.',
);
'attributes follow the pattern aria-* and must be lowercase.\n' +
' in div (at **)',
]);
});
});
});

View File

@@ -13,10 +13,13 @@ describe('ReactDOMComponentTree', () => {
let React;
let ReactDOM;
let container;
let assertConsoleErrorDev;
beforeEach(() => {
React = require('react');
ReactDOM = require('react-dom');
assertConsoleErrorDev =
require('internal-test-utils').assertConsoleErrorDev;
container = document.createElement('div');
document.body.appendChild(container);
@@ -31,11 +34,14 @@ describe('ReactDOMComponentTree', () => {
it('finds instance of node that is attempted to be unmounted', () => {
const component = <div />;
const node = ReactDOM.render(<div>{component}</div>, container);
expect(() => ReactDOM.unmountComponentAtNode(node)).toErrorDev(
"unmountComponentAtNode(): The node you're attempting to unmount " +
'was rendered by React and is not a top-level container. You may ' +
'have accidentally passed in a React root node instead of its ' +
'container.',
ReactDOM.unmountComponentAtNode(node);
assertConsoleErrorDev(
[
"unmountComponentAtNode(): The node you're attempting to unmount " +
'was rendered by React and is not a top-level container. You may ' +
'have accidentally passed in a React root node instead of its ' +
'container.',
],
{withoutStack: true},
);
});
@@ -49,11 +55,14 @@ describe('ReactDOMComponentTree', () => {
);
const anotherComponent = <div />;
const instance = ReactDOM.render(component, container);
expect(() => ReactDOM.render(anotherComponent, instance)).toErrorDev(
'Replacing React-rendered children with a new root ' +
'component. If you intended to update the children of this node, ' +
'you should instead have the existing children update their state ' +
'and render the new components instead of calling ReactDOM.render.',
ReactDOM.render(anotherComponent, instance);
assertConsoleErrorDev(
[
'Replacing React-rendered children with a new root ' +
'component. If you intended to update the children of this node, ' +
'you should instead have the existing children update their state ' +
'and render the new components instead of calling ReactDOM.render.',
],
{withoutStack: true},
);
});

View File

@@ -13,11 +13,14 @@ const React = require('react');
const ReactDOM = require('react-dom');
const PropTypes = require('prop-types');
let act;
let assertConsoleErrorDev;
describe('ReactDOMLegacyFiber', () => {
let container;
beforeEach(() => {
act = require('internal-test-utils').act;
assertConsoleErrorDev =
require('internal-test-utils').assertConsoleErrorDev;
container = document.createElement('div');
document.body.appendChild(container);
});
@@ -786,9 +789,8 @@ describe('ReactDOMLegacyFiber', () => {
}
}
expect(() => {
ReactDOM.render(<Parent />, container);
}).toErrorDev([
ReactDOM.render(<Parent />, container);
assertConsoleErrorDev([
'Parent uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
'Component uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
]);
@@ -834,10 +836,8 @@ describe('ReactDOMLegacyFiber', () => {
}
}
let instance;
expect(() => {
instance = ReactDOM.render(<Parent />, container);
}).toErrorDev([
const instance = ReactDOM.render(<Parent />, container);
assertConsoleErrorDev([
'Parent uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
'Component uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
]);
@@ -882,9 +882,8 @@ describe('ReactDOMLegacyFiber', () => {
}
}
expect(() => {
ReactDOM.render(<Parent bar="initial" />, container);
}).toErrorDev([
ReactDOM.render(<Parent bar="initial" />, container);
assertConsoleErrorDev([
'Parent uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
'Component uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
]);
@@ -1117,11 +1116,12 @@ describe('ReactDOMLegacyFiber', () => {
return <div onClick="woops" />;
}
}
expect(() => ReactDOM.render(<Example />, container)).toErrorDev(
ReactDOM.render(<Example />, container);
assertConsoleErrorDev([
'Expected `onClick` listener to be a function, instead got a value of `string` type.\n' +
' in div (at **)\n' +
' in Example (at **)',
);
]);
});
// @gate !disableLegacyMode
@@ -1131,13 +1131,14 @@ describe('ReactDOMLegacyFiber', () => {
return <div onClick={false} />;
}
}
expect(() => ReactDOM.render(<Example />, container)).toErrorDev(
ReactDOM.render(<Example />, container);
assertConsoleErrorDev([
'Expected `onClick` listener to be a function, instead got `false`.\n\n' +
'If you used to conditionally omit it with onClick={condition && value}, ' +
'pass onClick={condition ? value : undefined} instead.\n' +
' in div (at **)\n' +
' in Example (at **)',
);
]);
});
// @gate !disableLegacyMode
@@ -1270,17 +1271,18 @@ describe('ReactDOMLegacyFiber', () => {
container.innerHTML = '<div>MEOW.</div>';
await expect(async () => {
await expect(async () => {
await act(() => {
ReactDOM.render(<div key="2">baz</div>, container);
});
}).rejects.toThrow('The node to be removed is not a child of this node.');
}).toErrorDev(
'' +
'It looks like the React-rendered content of this container was ' +
'removed without using React. This is not supported and will ' +
'cause errors. Instead, call ReactDOM.unmountComponentAtNode ' +
'to empty a container.',
await act(() => {
ReactDOM.render(<div key="2">baz</div>, container);
});
}).rejects.toThrow('The node to be removed is not a child of this node.');
assertConsoleErrorDev(
[
'' +
'It looks like the React-rendered content of this container was ' +
'removed without using React. This is not supported and will ' +
'cause errors. Instead, call ReactDOM.unmountComponentAtNode ' +
'to empty a container.',
],
{withoutStack: true},
);
});
@@ -1293,12 +1295,15 @@ describe('ReactDOMLegacyFiber', () => {
expect(container.innerHTML).toBe('<div>bar</div>');
// then we mess with the DOM before an update
container.innerHTML = '<div>MEOW.</div>';
expect(() => ReactDOM.render(<div>baz</div>, container)).toErrorDev(
'' +
'It looks like the React-rendered content of this container was ' +
'removed without using React. This is not supported and will ' +
'cause errors. Instead, call ReactDOM.unmountComponentAtNode ' +
'to empty a container.',
ReactDOM.render(<div>baz</div>, container);
assertConsoleErrorDev(
[
'' +
'It looks like the React-rendered content of this container was ' +
'removed without using React. This is not supported and will ' +
'cause errors. Instead, call ReactDOM.unmountComponentAtNode ' +
'to empty a container.',
],
{withoutStack: true},
);
});
@@ -1311,12 +1316,15 @@ describe('ReactDOMLegacyFiber', () => {
expect(container.innerHTML).toBe('<div>bar</div>');
// then we mess with the DOM before an update
container.innerHTML = '';
expect(() => ReactDOM.render(<div>baz</div>, container)).toErrorDev(
'' +
'It looks like the React-rendered content of this container was ' +
'removed without using React. This is not supported and will ' +
'cause errors. Instead, call ReactDOM.unmountComponentAtNode ' +
'to empty a container.',
ReactDOM.render(<div>baz</div>, container);
assertConsoleErrorDev(
[
'' +
'It looks like the React-rendered content of this container was ' +
'removed without using React. This is not supported and will ' +
'cause errors. Instead, call ReactDOM.unmountComponentAtNode ' +
'to empty a container.',
],
{withoutStack: true},
);
});

View File

@@ -14,6 +14,7 @@ describe('ReactDOMOption', () => {
let ReactDOMClient;
let ReactDOMServer;
let act;
let assertConsoleErrorDev;
beforeEach(() => {
jest.resetModules();
@@ -21,6 +22,8 @@ describe('ReactDOMOption', () => {
ReactDOMClient = require('react-dom/client');
ReactDOMServer = require('react-dom/server');
act = require('internal-test-utils').act;
assertConsoleErrorDev =
require('internal-test-utils').assertConsoleErrorDev;
});
async function renderIntoDocument(children) {
@@ -47,10 +50,8 @@ describe('ReactDOMOption', () => {
{1} <div /> {2}
</option>
);
let container;
await expect(async () => {
container = await renderIntoDocument(el);
}).toErrorDev(
const container = await renderIntoDocument(el);
assertConsoleErrorDev([
'In HTML, <div> cannot be a child of <option>.\n' +
'This will cause a hydration error.\n' +
'\n' +
@@ -62,7 +63,7 @@ describe('ReactDOMOption', () => {
(gate(flags => flags.enableOwnerStacks)
? ''
: '\n in option (at **)'),
);
]);
expect(container.firstChild.innerHTML).toBe('1 <div></div> 2');
await renderIntoDocument(el);
});
@@ -76,13 +77,12 @@ describe('ReactDOMOption', () => {
{1} <Foo /> {3}
</option>
);
let container;
await expect(async () => {
container = await renderIntoDocument(el);
}).toErrorDev(
const container = await renderIntoDocument(el);
assertConsoleErrorDev([
'Cannot infer the option value of complex children. ' +
'Pass a `value` prop or use a plain string as children to <option>.',
);
'Pass a `value` prop or use a plain string as children to <option>.\n' +
' in option (at **)',
]);
expect(container.firstChild.innerHTML).toBe('1 2 3');
await renderIntoDocument(el);
});
@@ -187,13 +187,11 @@ describe('ReactDOMOption', () => {
it('should be able to use dangerouslySetInnerHTML on option', async () => {
const stub = <option dangerouslySetInnerHTML={{__html: 'foobar'}} />;
let container;
await expect(async () => {
container = await renderIntoDocument(stub);
}).toErrorDev(
const container = await renderIntoDocument(stub);
assertConsoleErrorDev([
'Pass a `value` prop if you set dangerouslyInnerHTML so React knows which value should be selected.\n' +
' in option (at **)',
);
]);
expect(container.firstChild.innerHTML).toBe('foobar');
});
@@ -267,13 +265,12 @@ describe('ReactDOMOption', () => {
expect(option.textContent).toBe('BarFooBaz');
expect(option.selected).toBe(true);
await expect(async () => {
await act(async () => {
ReactDOMClient.hydrateRoot(container, children, {
onRecoverableError: () => {},
});
await act(async () => {
ReactDOMClient.hydrateRoot(container, children, {
onRecoverableError: () => {},
});
}).toErrorDev(
});
assertConsoleErrorDev([
'In HTML, <div> cannot be a child of <option>.\n' +
'This will cause a hydration error.\n' +
'\n' +
@@ -285,8 +282,8 @@ describe('ReactDOMOption', () => {
' in div (at **)' +
(gate(flags => flags.enableOwnerStacks)
? ''
: '\n in option (at **)'),
);
: '\n in option (at **)' + '\n in select (at **)'),
]);
option = container.firstChild.firstChild;
expect(option.textContent).toBe('BarFooBaz');

View File

@@ -18,6 +18,7 @@ let act;
let useEffect;
let assertLog;
let waitForAll;
let assertConsoleErrorDev;
describe('ReactDOMRoot', () => {
let container;
@@ -31,6 +32,8 @@ describe('ReactDOMRoot', () => {
ReactDOMServer = require('react-dom/server');
Scheduler = require('scheduler');
act = require('internal-test-utils').act;
assertConsoleErrorDev =
require('internal-test-utils').assertConsoleErrorDev;
useEffect = React.useEffect;
const InternalTestUtils = require('internal-test-utils');
@@ -48,9 +51,12 @@ describe('ReactDOMRoot', () => {
it('warns if a callback parameter is provided to render', async () => {
const callback = jest.fn();
const root = ReactDOMClient.createRoot(container);
expect(() => root.render(<div>Hi</div>, callback)).toErrorDev(
'does not support the second callback argument. ' +
'To execute a side effect after rendering, declare it in a component body with useEffect().',
root.render(<div>Hi</div>, callback);
assertConsoleErrorDev(
[
'does not support the second callback argument. ' +
'To execute a side effect after rendering, declare it in a component body with useEffect().',
],
{withoutStack: true},
);
await waitForAll([]);
@@ -63,9 +69,12 @@ describe('ReactDOMRoot', () => {
}
const root = ReactDOMClient.createRoot(container);
expect(() => root.render(<App />, {})).toErrorDev(
'You passed a second argument to root.render(...) but it only accepts ' +
'one argument.',
root.render(<App />, {});
assertConsoleErrorDev(
[
'You passed a second argument to root.render(...) but it only accepts ' +
'one argument.',
],
{
withoutStack: true,
},
@@ -78,10 +87,13 @@ describe('ReactDOMRoot', () => {
}
const root = ReactDOMClient.createRoot(container);
expect(() => root.render(<App />, container)).toErrorDev(
'You passed a container to the second argument of root.render(...). ' +
"You don't need to pass it again since you already passed it to create " +
'the root.',
root.render(<App />, container);
assertConsoleErrorDev(
[
'You passed a container to the second argument of root.render(...). ' +
"You don't need to pass it again since you already passed it to create " +
'the root.',
],
{
withoutStack: true,
},
@@ -92,9 +104,12 @@ describe('ReactDOMRoot', () => {
const callback = jest.fn();
const root = ReactDOMClient.createRoot(container);
root.render(<div>Hi</div>);
expect(() => root.unmount(callback)).toErrorDev(
'does not support a callback argument. ' +
'To execute a side effect after rendering, declare it in a component body with useEffect().',
root.unmount(callback);
assertConsoleErrorDev(
[
'does not support a callback argument. ' +
'To execute a side effect after rendering, declare it in a component body with useEffect().',
],
{withoutStack: true},
);
await waitForAll([]);
@@ -148,8 +163,27 @@ describe('ReactDOMRoot', () => {
<span />
</div>,
);
await expect(async () => await waitForAll([])).toErrorDev(
"A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.",
await waitForAll([]);
assertConsoleErrorDev(
[
"A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. " +
"This won't be patched up. This can happen if a SSR-ed Client Component used:\n" +
'\n' +
"- A server/client branch `if (typeof window !== 'undefined')`.\n" +
"- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" +
"- Date formatting in a user's locale which doesn't match the server.\n" +
'- External changing data without sending a snapshot of it along with the HTML.\n' +
'- Invalid HTML tag nesting.\n' +
'\n' +
'It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.\n' +
'\n' +
'https://react.dev/link/hydration-mismatch\n' +
'\n' +
' <div>\n' +
' <span\n' +
'- className="extra"\n' +
' >\n',
],
{withoutStack: true},
);
});
@@ -183,12 +217,13 @@ describe('ReactDOMRoot', () => {
it('warns when creating two roots managing the same container', () => {
ReactDOMClient.createRoot(container);
expect(() => {
ReactDOMClient.createRoot(container);
}).toErrorDev(
'You are calling ReactDOMClient.createRoot() on a container that ' +
'has already been passed to createRoot() before. Instead, call ' +
'root.render() on the existing root instead if you want to update it.',
ReactDOMClient.createRoot(container);
assertConsoleErrorDev(
[
'You are calling ReactDOMClient.createRoot() on a container that ' +
'has already been passed to createRoot() before. Instead, call ' +
'root.render() on the existing root instead if you want to update it.',
],
{withoutStack: true},
);
});
@@ -352,14 +387,15 @@ describe('ReactDOMRoot', () => {
});
expect(container1.textContent).toEqual('Hi');
expect(() => {
ReactDOM.flushSync(() => {
root1.render(<App step={2} />);
});
}).toErrorDev(
'Attempted to synchronously unmount a root while React was ' +
'already rendering.',
);
ReactDOM.flushSync(() => {
root1.render(<App step={2} />);
});
assertConsoleErrorDev([
'Attempted to synchronously unmount a root while React was already rendering. ' +
'React cannot finish unmounting the root until the current render has completed, ' +
'which may lead to a race condition.\n' +
' in App (at **)',
]);
});
// @gate disableCommentsAsDOMContainers
@@ -378,8 +414,12 @@ describe('ReactDOMRoot', () => {
});
it('warn if no children passed to hydrateRoot', async () => {
expect(() => ReactDOMClient.hydrateRoot(container)).toErrorDev(
'Must provide initial children as second argument to hydrateRoot.',
ReactDOMClient.hydrateRoot(container);
assertConsoleErrorDev(
[
'Must provide initial children as second argument to hydrateRoot. ' +
'Example usage: hydrateRoot(domContainer, <App />)',
],
{withoutStack: true},
);
});
@@ -389,9 +429,15 @@ describe('ReactDOMRoot', () => {
return 'Child';
}
expect(() => ReactDOMClient.createRoot(container, <App />)).toErrorDev(
'You passed a JSX element to createRoot. You probably meant to call ' +
'root.render instead',
ReactDOMClient.createRoot(container, <App />);
assertConsoleErrorDev(
[
'You passed a JSX element to createRoot. You probably meant to call root.render instead. ' +
'Example usage:\n' +
'\n' +
' let root = createRoot(domContainer);\n' +
' root.render(<App />);',
],
{
withoutStack: true,
},
@@ -405,15 +451,16 @@ describe('ReactDOMRoot', () => {
const root = ReactDOMClient.createRoot(document.createElement('div'));
expect(() => {
ReactDOM.flushSync(() => {
root.render(Component);
});
}).toErrorDev(
'Functions are not valid as a React child. ' +
'This may happen if you return Component instead of <Component /> from render. ' +
'Or maybe you meant to call this function rather than return it.\n' +
' root.render(Component)',
ReactDOM.flushSync(() => {
root.render(Component);
});
assertConsoleErrorDev(
[
'Functions are not valid as a React child. ' +
'This may happen if you return Component instead of <Component /> from render. ' +
'Or maybe you meant to call this function rather than return it.\n' +
' root.render(Component)',
],
{withoutStack: true},
);
});
@@ -421,13 +468,14 @@ describe('ReactDOMRoot', () => {
it('warns when given a symbol', () => {
const root = ReactDOMClient.createRoot(document.createElement('div'));
expect(() => {
ReactDOM.flushSync(() => {
root.render(Symbol('foo'));
});
}).toErrorDev(
'Symbols are not valid as a React child.\n' +
' root.render(Symbol(foo))',
ReactDOM.flushSync(() => {
root.render(Symbol('foo'));
});
assertConsoleErrorDev(
[
'Symbols are not valid as a React child.\n' +
' root.render(Symbol(foo))',
],
{withoutStack: true},
);
});

File diff suppressed because it is too large Load Diff

View File

@@ -13,12 +13,16 @@ const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegratio
let React;
let ReactDOMServer;
let assertConsoleErrorDev;
let assertConsoleWarnDev;
function initModules() {
// Reset warning cache.
jest.resetModules();
React = require('react');
ReactDOMServer = require('react-dom/server');
assertConsoleErrorDev = require('internal-test-utils').assertConsoleErrorDev;
assertConsoleWarnDev = require('internal-test-utils').assertConsoleWarnDev;
// Make them available to the helpers.
return {
@@ -115,9 +119,17 @@ describe('ReactDOMServerLifecycles', () => {
}
}
expect(() => ReactDOMServer.renderToString(<Component />)).toErrorDev(
'Unsafe legacy lifecycles will not be called for components using new component APIs.',
);
ReactDOMServer.renderToString(<Component />);
assertConsoleErrorDev([
'Unsafe legacy lifecycles will not be called for components using new component APIs.\n' +
'\n' +
'Component uses getDerivedStateFromProps() but also contains the following legacy lifecycles:\n' +
' UNSAFE_componentWillMount\n' +
'\n' +
'The above lifecycles should be removed. Learn more about this warning here:\n' +
'https://react.dev/link/unsafe-component-lifecycles\n' +
' in Component (at **)',
]);
});
it('should update instance.state with value returned from getDerivedStateFromProps', () => {
@@ -182,10 +194,12 @@ describe('ReactDOMServerLifecycles', () => {
}
}
expect(() => ReactDOMServer.renderToString(<Component />)).toErrorDev(
ReactDOMServer.renderToString(<Component />);
assertConsoleErrorDev([
'Component.getDerivedStateFromProps(): A valid state object (or null) must ' +
'be returned. You have returned undefined.',
);
'be returned. You have returned undefined.\n' +
' in Component (at **)',
]);
// De-duped
ReactDOMServer.renderToString(<Component />);
@@ -201,12 +215,14 @@ describe('ReactDOMServerLifecycles', () => {
}
}
expect(() => ReactDOMServer.renderToString(<Component />)).toErrorDev(
ReactDOMServer.renderToString(<Component />);
assertConsoleErrorDev([
'`Component` uses `getDerivedStateFromProps` but its initial state is ' +
'undefined. This is not recommended. Instead, define the initial state by ' +
'assigning an object to `this.state` in the constructor of `Component`. ' +
'This ensures that `getDerivedStateFromProps` arguments have a consistent shape.',
);
'This ensures that `getDerivedStateFromProps` arguments have a consistent shape.\n' +
' in Component (at **)',
]);
// De-duped
ReactDOMServer.renderToString(<Component />);
@@ -227,9 +243,16 @@ describe('ReactDOMServerLifecycles', () => {
}
}
expect(() => ReactDOMServer.renderToString(<Component />)).toWarnDev(
'componentWillMount has been renamed',
);
ReactDOMServer.renderToString(<Component />);
assertConsoleWarnDev([
'componentWillMount has been renamed, and is not recommended for use. ' +
'See https://react.dev/link/unsafe-component-lifecycles for details.\n' +
'\n' +
'* Move code from componentWillMount to componentDidMount (preferred in most cases) or the constructor.\n' +
'\n' +
'Please update the following components: Component\n' +
' in Component (at **)',
]);
expect(log).toEqual(['componentWillMount', 'UNSAFE_componentWillMount']);
});
@@ -254,17 +277,18 @@ describe('ReactDOMServerLifecycles', () => {
return <div>{this.props.children + '-' + this.state.x}</div>;
}
}
expect(() => {
// Shouldn't be 1-3.
expect(ReactDOMServer.renderToStaticMarkup(<Outer />)).toBe(
'<div>1-2</div>',
);
}).toErrorDev(
// Shouldn't be 1-3.
expect(ReactDOMServer.renderToStaticMarkup(<Outer />)).toBe(
'<div>1-2</div>',
);
assertConsoleErrorDev([
'Can only update a mounting component. This ' +
'usually means you called setState() outside componentWillMount() on ' +
'the server. This is a no-op.\n\n' +
'Please check the code for the Outer component.',
);
'Please check the code for the Outer component.\n' +
(gate('enableOwnerStacks') ? '' : ' in Inner (at **)\n') +
' in Outer (at **)',
]);
});
it('should not invoke cWM if static gDSFP is present', () => {
@@ -281,9 +305,17 @@ describe('ReactDOMServerLifecycles', () => {
}
}
expect(() => ReactDOMServer.renderToString(<Component />)).toErrorDev(
'Unsafe legacy lifecycles will not be called for components using new component APIs.',
);
ReactDOMServer.renderToString(<Component />);
assertConsoleErrorDev([
'Unsafe legacy lifecycles will not be called for components using new component APIs.\n' +
'\n' +
'Component uses getDerivedStateFromProps() but also contains the following legacy lifecycles:\n' +
' componentWillMount\n' +
'\n' +
'The above lifecycles should be removed. Learn more about this warning here:\n' +
'https://react.dev/link/unsafe-component-lifecycles\n' +
' in Component (at **)',
]);
});
it('should warn about deprecated lifecycle hooks', () => {
@@ -294,11 +326,16 @@ describe('ReactDOMServerLifecycles', () => {
}
}
expect(() => ReactDOMServer.renderToString(<MyComponent />)).toWarnDev(
'componentWillMount has been renamed, and is not recommended for use. See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' +
'* Move code from componentWillMount to componentDidMount (preferred in most cases) or the constructor.\n\n' +
'Please update the following components: MyComponent',
);
ReactDOMServer.renderToString(<MyComponent />);
assertConsoleWarnDev([
'componentWillMount has been renamed, and is not recommended for use. ' +
'See https://react.dev/link/unsafe-component-lifecycles for details.\n' +
'\n' +
'* Move code from componentWillMount to componentDidMount (preferred in most cases) or the constructor.\n' +
'\n' +
'Please update the following components: MyComponent\n' +
' in MyComponent (at **)',
]);
// De-duped
ReactDOMServer.renderToString(<MyComponent />);

View File

@@ -26,6 +26,7 @@ let waitForAll;
let waitFor;
let waitForPaint;
let assertLog;
let assertConsoleErrorDev;
function normalizeError(msg) {
// Take the first sentence to make it easier to assert on.
@@ -124,6 +125,7 @@ describe('ReactDOMServerPartialHydration', () => {
assertLog = InternalTestUtils.assertLog;
waitForPaint = InternalTestUtils.waitForPaint;
waitFor = InternalTestUtils.waitFor;
assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev;
IdleEventPriority = require('react-reconciler/constants').IdleEventPriority;
});
@@ -1916,9 +1918,18 @@ describe('ReactDOMServerPartialHydration', () => {
// While we're part way through the hydration, we update the state.
// This will schedule an update on the children of the suspense boundary.
expect(() => updateText('Hi')).toErrorDev(
"Can't perform a React state update on a component that hasn't mounted yet.",
);
updateText('Hi');
assertConsoleErrorDev([
"Can't perform a React state update on a component that hasn't mounted yet. " +
'This indicates that you have a side-effect in your render function that ' +
'asynchronously later calls tries to update the component. Move this work to useEffect instead.\n' +
(gate('enableOwnerStacks')
? ''
: ' in Child (at **)\n' +
' in Suspense (at **)\n' +
' in div (at **)\n') +
' in App (at **)',
]);
// This will throw it away and rerender.
await waitForAll(['Child']);

View File

@@ -14,6 +14,7 @@ describe('ReactDOMShorthandCSSPropertyCollision', () => {
let React;
let ReactDOMClient;
let assertConsoleErrorDev;
beforeEach(() => {
jest.resetModules();
@@ -21,6 +22,8 @@ describe('ReactDOMShorthandCSSPropertyCollision', () => {
act = require('internal-test-utils').act;
React = require('react');
ReactDOMClient = require('react-dom/client');
assertConsoleErrorDev =
require('internal-test-utils').assertConsoleErrorDev;
});
it('should warn for conflicting CSS shorthand updates', async () => {
@@ -29,18 +32,17 @@ describe('ReactDOMShorthandCSSPropertyCollision', () => {
await act(() => {
root.render(<div style={{font: 'foo', fontStyle: 'bar'}} />);
});
await expect(async () => {
await act(() => {
root.render(<div style={{font: 'foo'}} />);
});
}).toErrorDev(
await act(() => {
root.render(<div style={{font: 'foo'}} />);
});
assertConsoleErrorDev([
'Removing a style property during rerender (fontStyle) ' +
'when a conflicting property is set (font) can lead to styling ' +
"bugs. To avoid this, don't mix shorthand and non-shorthand " +
'properties for the same value; instead, replace the shorthand ' +
'with separate values.' +
'\n in div (at **)',
);
]);
// These updates are OK and don't warn:
await act(() => {
@@ -50,30 +52,28 @@ describe('ReactDOMShorthandCSSPropertyCollision', () => {
root.render(<div style={{font: 'foo', fontStyle: 'baz'}} />);
});
await expect(async () => {
await act(() => {
root.render(<div style={{font: 'qux', fontStyle: 'baz'}} />);
});
}).toErrorDev(
await act(() => {
root.render(<div style={{font: 'qux', fontStyle: 'baz'}} />);
});
assertConsoleErrorDev([
'Updating a style property during rerender (font) when ' +
'a conflicting property is set (fontStyle) can lead to styling ' +
"bugs. To avoid this, don't mix shorthand and non-shorthand " +
'properties for the same value; instead, replace the shorthand ' +
'with separate values.' +
'\n in div (at **)',
);
await expect(async () => {
await act(() => {
root.render(<div style={{fontStyle: 'baz'}} />);
});
}).toErrorDev(
]);
await act(() => {
root.render(<div style={{fontStyle: 'baz'}} />);
});
assertConsoleErrorDev([
'Removing a style property during rerender (font) when ' +
'a conflicting property is set (fontStyle) can lead to styling ' +
"bugs. To avoid this, don't mix shorthand and non-shorthand " +
'properties for the same value; instead, replace the shorthand ' +
'with separate values.' +
'\n in div (at **)',
);
]);
// A bit of a special case: backgroundPosition isn't technically longhand
// (it expands to backgroundPosition{X,Y} but so does background)
@@ -82,18 +82,17 @@ describe('ReactDOMShorthandCSSPropertyCollision', () => {
<div style={{background: 'yellow', backgroundPosition: 'center'}} />,
);
});
await expect(async () => {
await act(() => {
root.render(<div style={{background: 'yellow'}} />);
});
}).toErrorDev(
await act(() => {
root.render(<div style={{background: 'yellow'}} />);
});
assertConsoleErrorDev([
'Removing a style property during rerender ' +
'(backgroundPosition) when a conflicting property is set ' +
"(background) can lead to styling bugs. To avoid this, don't mix " +
'shorthand and non-shorthand properties for the same value; ' +
'instead, replace the shorthand with separate values.' +
'\n in div (at **)',
);
]);
await act(() => {
root.render(
<div style={{background: 'yellow', backgroundPosition: 'center'}} />,
@@ -105,18 +104,17 @@ describe('ReactDOMShorthandCSSPropertyCollision', () => {
<div style={{background: 'green', backgroundPosition: 'top'}} />,
);
});
await expect(async () => {
await act(() => {
root.render(<div style={{backgroundPosition: 'top'}} />);
});
}).toErrorDev(
await act(() => {
root.render(<div style={{backgroundPosition: 'top'}} />);
});
assertConsoleErrorDev([
'Removing a style property during rerender (background) ' +
'when a conflicting property is set (backgroundPosition) can lead ' +
"to styling bugs. To avoid this, don't mix shorthand and " +
'non-shorthand properties for the same value; instead, replace the ' +
'shorthand with separate values.' +
'\n in div (at **)',
);
]);
// A bit of an even more special case: borderLeft and borderStyle overlap.
await act(() => {
@@ -124,49 +122,46 @@ describe('ReactDOMShorthandCSSPropertyCollision', () => {
<div style={{borderStyle: 'dotted', borderLeft: '1px solid red'}} />,
);
});
await expect(async () => {
await act(() => {
root.render(<div style={{borderLeft: '1px solid red'}} />);
});
}).toErrorDev(
await act(() => {
root.render(<div style={{borderLeft: '1px solid red'}} />);
});
assertConsoleErrorDev([
'Removing a style property during rerender (borderStyle) ' +
'when a conflicting property is set (borderLeft) can lead to ' +
"styling bugs. To avoid this, don't mix shorthand and " +
'non-shorthand properties for the same value; instead, replace the ' +
'shorthand with separate values.' +
'\n in div (at **)',
);
await expect(async () => {
await act(() => {
root.render(
<div style={{borderStyle: 'dashed', borderLeft: '1px solid red'}} />,
);
});
}).toErrorDev(
]);
await act(() => {
root.render(
<div style={{borderStyle: 'dashed', borderLeft: '1px solid red'}} />,
);
});
assertConsoleErrorDev([
'Updating a style property during rerender (borderStyle) ' +
'when a conflicting property is set (borderLeft) can lead to ' +
"styling bugs. To avoid this, don't mix shorthand and " +
'non-shorthand properties for the same value; instead, replace the ' +
'shorthand with separate values.' +
'\n in div (at **)',
);
]);
// But setting them at the same time is OK:
await act(() => {
root.render(
<div style={{borderStyle: 'dotted', borderLeft: '2px solid red'}} />,
);
});
await expect(async () => {
await act(() => {
root.render(<div style={{borderStyle: 'dotted'}} />);
});
}).toErrorDev(
await act(() => {
root.render(<div style={{borderStyle: 'dotted'}} />);
});
assertConsoleErrorDev([
'Removing a style property during rerender (borderLeft) ' +
'when a conflicting property is set (borderStyle) can lead to ' +
"styling bugs. To avoid this, don't mix shorthand and " +
'non-shorthand properties for the same value; instead, replace the ' +
'shorthand with separate values.' +
'\n in div (at **)',
);
]);
});
});

View File

@@ -23,6 +23,7 @@ let buffer = '';
let hasErrored = false;
let fatalError = undefined;
let waitForAll;
let assertConsoleErrorDev;
function normalizeError(msg) {
// Take the first sentence to make it easier to assert on.
@@ -45,6 +46,7 @@ describe('ReactDOM HostSingleton', () => {
const InternalTestUtils = require('internal-test-utils');
waitForAll = InternalTestUtils.waitForAll;
assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev;
// Test Environment
const jsdom = new JSDOM(
@@ -162,11 +164,16 @@ describe('ReactDOM HostSingleton', () => {
<body />
</html>,
);
await expect(async () => {
await waitForAll([]);
}).toErrorDev(
'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',
);
await waitForAll([]);
assertConsoleErrorDev([
'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.\n' +
' in head (at **)' +
(gate('enableOwnerStacks') ? '' : '\n in html (at **)'),
]);
expect(getVisibleChildren(document)).toEqual(
<html>
<head lang="es" data-foo="foo">
@@ -540,10 +547,31 @@ describe('ReactDOM HostSingleton', () => {
},
);
expect(hydrationErrors).toEqual([]);
await expect(async () => {
await waitForAll([]);
}).toErrorDev(
"A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.",
await waitForAll([]);
assertConsoleErrorDev(
[
"A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. " +
"This won't be patched up. This can happen if a SSR-ed Client Component used:\n" +
'\n' +
"- A server/client branch `if (typeof window !== 'undefined')`.\n" +
"- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" +
"- Date formatting in a user's locale which doesn't match the server.\n" +
'- External changing data without sending a snapshot of it along with the HTML.\n' +
'- Invalid HTML tag nesting.\n\nIt can also happen if the client has a browser extension installed ' +
'which messes with the HTML before React loaded.\n' +
'\n' +
'https://react.dev/link/hydration-mismatch\n' +
'\n' +
' <html\n' +
'+ data-client-foo="foo"\n' +
'- data-client-foo={null}\n' +
' >\n' +
' <head>\n' +
' <body\n' +
'+ data-client-baz="baz"\n' +
'- data-client-baz={null}\n' +
' >\n',
],
{withoutStack: true},
);
expect(persistentElements).toEqual([

View File

@@ -16,6 +16,7 @@ describe('ReactDOMTextarea', () => {
let ReactDOMClient;
let ReactDOMServer;
let act;
let assertConsoleErrorDev;
let renderTextarea;
@@ -28,6 +29,8 @@ describe('ReactDOMTextarea', () => {
ReactDOMClient = require('react-dom/client');
ReactDOMServer = require('react-dom/server');
act = require('internal-test-utils').act;
assertConsoleErrorDev =
require('internal-test-utils').assertConsoleErrorDev;
renderTextarea = async function (component, container, root) {
await act(() => {
@@ -330,12 +333,12 @@ describe('ReactDOMTextarea', () => {
);
});
};
await expect(() =>
expect(test).rejects.toThrowError(new TypeError('prod message')),
).toErrorDev(
await expect(test).rejects.toThrowError(new TypeError('prod message'));
assertConsoleErrorDev([
'Form field values (value, checked, defaultValue, or defaultChecked props) must be ' +
'strings, not TemporalLike. This value must be coerced to a string before using it here.',
);
'strings, not TemporalLike. This value must be coerced to a string before using it here.\n' +
' in textarea (at **',
]);
});
it('should take updates to `defaultValue` for uncontrolled textarea', async () => {
@@ -437,17 +440,15 @@ describe('ReactDOMTextarea', () => {
it('should ignore children content', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
let node;
await expect(async () => {
node = await renderTextarea(
<textarea>giraffe</textarea>,
container,
root,
);
}).toErrorDev(
'Use the `defaultValue` or `value` props instead of setting children on <textarea>.',
const node = await renderTextarea(
<textarea>giraffe</textarea>,
container,
root,
);
assertConsoleErrorDev([
'Use the `defaultValue` or `value` props instead of setting children on <textarea>.\n' +
' in textarea (at **)',
]);
expect(node.value).toBe('');
await act(() => {
@@ -462,17 +463,16 @@ describe('ReactDOMTextarea', () => {
it('should receive defaultValue and still ignore children content', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
let node;
await expect(async () => {
node = await renderTextarea(
<textarea defaultValue="dragon">monkey</textarea>,
container,
root,
);
}).toErrorDev(
'Use the `defaultValue` or `value` props instead of setting children on <textarea>.',
const node = await renderTextarea(
<textarea defaultValue="dragon">monkey</textarea>,
container,
root,
);
assertConsoleErrorDev([
'Use the `defaultValue` or `value` props instead of setting children on <textarea>.\n' +
' in textarea (at **)',
]);
expect(node.value).toBe('dragon');
});
}
@@ -481,17 +481,16 @@ describe('ReactDOMTextarea', () => {
it('should treat children like `defaultValue`', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
let node;
await expect(async () => {
node = await renderTextarea(
<textarea>giraffe</textarea>,
container,
root,
);
}).toErrorDev(
'Use the `defaultValue` or `value` props instead of setting children on <textarea>.',
const node = await renderTextarea(
<textarea>giraffe</textarea>,
container,
root,
);
assertConsoleErrorDev([
'Use the `defaultValue` or `value` props instead of setting children on <textarea>.\n' +
' in textarea (at **)',
]);
expect(node.value).toBe('giraffe');
@@ -549,12 +548,15 @@ describe('ReactDOMTextarea', () => {
it('should ignore numbers as children', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
let node;
await expect(async () => {
node = await renderTextarea(<textarea>{17}</textarea>, container, root);
}).toErrorDev(
'Use the `defaultValue` or `value` props instead of setting children on <textarea>.',
const node = await renderTextarea(
<textarea>{17}</textarea>,
container,
root,
);
assertConsoleErrorDev([
'Use the `defaultValue` or `value` props instead of setting children on <textarea>.\n' +
' in textarea (at **)',
]);
expect(node.value).toBe('');
});
}
@@ -563,12 +565,15 @@ describe('ReactDOMTextarea', () => {
it('should allow numbers as children', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
let node;
await expect(async () => {
node = await renderTextarea(<textarea>{17}</textarea>, container, root);
}).toErrorDev(
'Use the `defaultValue` or `value` props instead of setting children on <textarea>.',
const node = await renderTextarea(
<textarea>{17}</textarea>,
container,
root,
);
assertConsoleErrorDev([
'Use the `defaultValue` or `value` props instead of setting children on <textarea>.\n' +
' in textarea (at **)',
]);
expect(node.value).toBe('17');
});
}
@@ -577,16 +582,15 @@ describe('ReactDOMTextarea', () => {
it('should ignore booleans as children', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
let node;
await expect(async () => {
node = await renderTextarea(
<textarea>{false}</textarea>,
container,
root,
);
}).toErrorDev(
'Use the `defaultValue` or `value` props instead of setting children on <textarea>.',
const node = await renderTextarea(
<textarea>{false}</textarea>,
container,
root,
);
assertConsoleErrorDev([
'Use the `defaultValue` or `value` props instead of setting children on <textarea>.\n' +
' in textarea (at **)',
]);
expect(node.value).toBe('');
});
}
@@ -595,16 +599,15 @@ describe('ReactDOMTextarea', () => {
it('should allow booleans as children', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
let node;
await expect(async () => {
node = await renderTextarea(
<textarea>{false}</textarea>,
container,
root,
);
}).toErrorDev(
'Use the `defaultValue` or `value` props instead of setting children on <textarea>.',
const node = await renderTextarea(
<textarea>{false}</textarea>,
container,
root,
);
assertConsoleErrorDev([
'Use the `defaultValue` or `value` props instead of setting children on <textarea>.\n' +
' in textarea (at **)',
]);
expect(node.value).toBe('false');
});
}
@@ -618,16 +621,15 @@ describe('ReactDOMTextarea', () => {
return 'sharkswithlasers';
},
};
let node;
await expect(async () => {
node = await renderTextarea(
<textarea>{obj}</textarea>,
container,
root,
);
}).toErrorDev(
'Use the `defaultValue` or `value` props instead of setting children on <textarea>.',
const node = await renderTextarea(
<textarea>{obj}</textarea>,
container,
root,
);
assertConsoleErrorDev([
'Use the `defaultValue` or `value` props instead of setting children on <textarea>.\n' +
' in textarea (at **)',
]);
expect(node.value).toBe('');
});
}
@@ -641,16 +643,15 @@ describe('ReactDOMTextarea', () => {
return 'sharkswithlasers';
},
};
let node;
await expect(async () => {
node = await renderTextarea(
<textarea>{obj}</textarea>,
container,
root,
);
}).toErrorDev(
'Use the `defaultValue` or `value` props instead of setting children on <textarea>.',
const node = await renderTextarea(
<textarea>{obj}</textarea>,
container,
root,
);
assertConsoleErrorDev([
'Use the `defaultValue` or `value` props instead of setting children on <textarea>.\n' +
' in textarea (at **)',
]);
expect(node.value).toBe('sharkswithlasers');
});
}
@@ -660,35 +661,36 @@ describe('ReactDOMTextarea', () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await expect(async () => {
await expect(async () => {
await act(() => {
root.render(
<textarea>
{'hello'}
{'there'}
</textarea>,
);
});
}).rejects.toThrow('<textarea> can only have at most one child');
}).toErrorDev([
'Use the `defaultValue` or `value` props instead of setting children on <textarea>.',
'Use the `defaultValue` or `value` props instead of setting children on <textarea>.',
await act(() => {
root.render(
<textarea>
{'hello'}
{'there'}
</textarea>,
);
});
}).rejects.toThrow('<textarea> can only have at most one child');
assertConsoleErrorDev([
'Use the `defaultValue` or `value` props instead of setting children on <textarea>.\n' +
' in textarea (at **)',
'Use the `defaultValue` or `value` props instead of setting children on <textarea>.\n' +
' in textarea (at **)',
]);
let node;
await expect(async () => {
await expect(
(async () =>
(node = await renderTextarea(
<textarea>
<strong />
</textarea>,
container,
root,
)))(),
).resolves.not.toThrow();
}).toErrorDev([
'Use the `defaultValue` or `value` props instead of setting children on <textarea>.',
await expect(
(async () =>
(node = await renderTextarea(
<textarea>
<strong />
</textarea>,
container,
root,
)))(),
).resolves.not.toThrow();
assertConsoleErrorDev([
'Use the `defaultValue` or `value` props instead of setting children on <textarea>.\n' +
' in textarea (at **)',
]);
expect(node.value).toBe('[object Object]');
@@ -710,15 +712,15 @@ describe('ReactDOMTextarea', () => {
it('should warn if value is null', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await expect(async () => {
await act(() => {
root.render(<textarea value={null} />);
});
}).toErrorDev(
await act(() => {
root.render(<textarea value={null} />);
});
assertConsoleErrorDev([
'`value` prop on `textarea` should not be null. ' +
'Consider using an empty string to clear the component or `undefined` ' +
'for uncontrolled components.',
);
'for uncontrolled components.\n' +
' in textarea (at **)',
]);
await act(() => {
root.render(<textarea value={null} />);
@@ -731,18 +733,19 @@ describe('ReactDOMTextarea', () => {
);
let container = document.createElement('div');
let root = ReactDOMClient.createRoot(container);
await expect(async () => {
await act(() => {
root.render(<InvalidComponent />);
});
}).toErrorDev(
await act(() => {
root.render(<InvalidComponent />);
});
assertConsoleErrorDev([
'InvalidComponent contains a textarea with both value and defaultValue props. ' +
'Textarea elements must be either controlled or uncontrolled ' +
'(specify either the value prop, or the defaultValue prop, but not ' +
'both). Decide between using a controlled or uncontrolled textarea ' +
'and remove one of these props. More info: ' +
'https://react.dev/link/controlled-components',
);
'https://react.dev/link/controlled-components\n' +
' in textarea (at **)\n' +
' in InvalidComponent (at **)',
]);
container = document.createElement('div');
root = ReactDOMClient.createRoot(container);
@@ -819,13 +822,15 @@ describe('ReactDOMTextarea', () => {
it('treats initial Symbol value as an empty string', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await expect(async () => {
await act(() => {
root.render(
<textarea value={Symbol('foobar')} onChange={() => {}} />,
);
});
}).toErrorDev('Invalid value for prop `value`');
await act(() => {
root.render(<textarea value={Symbol('foobar')} onChange={() => {}} />);
});
assertConsoleErrorDev([
'Invalid value for prop `value` on <textarea> tag. ' +
'Either remove it from the element, or pass a string or number value to keep it in the DOM. ' +
'For details, see https://react.dev/link/attribute-behavior \n' +
' in textarea (at **)',
]);
const node = container.firstChild;
expect(node.value).toBe('');
@@ -834,11 +839,13 @@ describe('ReactDOMTextarea', () => {
it('treats initial Symbol children as an empty string', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await expect(async () => {
await act(() => {
root.render(<textarea onChange={() => {}}>{Symbol('foo')}</textarea>);
});
}).toErrorDev('Use the `defaultValue` or `value` props');
await act(() => {
root.render(<textarea onChange={() => {}}>{Symbol('foo')}</textarea>);
});
assertConsoleErrorDev([
'Use the `defaultValue` or `value` props instead of setting children on <textarea>.\n' +
' in textarea (at **)',
]);
const node = container.firstChild;
expect(node.value).toBe('');
@@ -852,11 +859,15 @@ describe('ReactDOMTextarea', () => {
root.render(<textarea value="foo" onChange={() => {}} />);
});
await expect(async () => {
await act(() => {
root.render(<textarea value={Symbol('foo')} onChange={() => {}} />);
});
}).toErrorDev('Invalid value for prop `value`');
await act(() => {
root.render(<textarea value={Symbol('foo')} onChange={() => {}} />);
});
assertConsoleErrorDev([
'Invalid value for prop `value` on <textarea> tag. ' +
'Either remove it from the element, or pass a string or number value to keep it in the DOM. ' +
'For details, see https://react.dev/link/attribute-behavior \n' +
' in textarea (at **)',
]);
const node = container.firstChild;
expect(node.value).toBe('');
@@ -897,13 +908,17 @@ describe('ReactDOMTextarea', () => {
describe('When given a function value', () => {
it('treats initial function value as an empty string', async () => {
const container = document.createElement('div');
await expect(async () => {
const root = ReactDOMClient.createRoot(container);
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<textarea value={() => {}} onChange={() => {}} />);
});
}).toErrorDev('Invalid value for prop `value`');
await act(() => {
root.render(<textarea value={() => {}} onChange={() => {}} />);
});
assertConsoleErrorDev([
'Invalid value for prop `value` on <textarea> tag. ' +
'Either remove it from the element, or pass a string or number value to keep it in the DOM. ' +
'For details, see https://react.dev/link/attribute-behavior \n' +
' in textarea (at **)',
]);
const node = container.firstChild;
expect(node.value).toBe('');
@@ -911,13 +926,15 @@ describe('ReactDOMTextarea', () => {
it('treats initial function children as an empty string', async () => {
const container = document.createElement('div');
await expect(async () => {
const root = ReactDOMClient.createRoot(container);
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<textarea onChange={() => {}}>{() => {}}</textarea>);
});
}).toErrorDev('Use the `defaultValue` or `value` props');
await act(() => {
root.render(<textarea onChange={() => {}}>{() => {}}</textarea>);
});
assertConsoleErrorDev([
'Use the `defaultValue` or `value` props instead of setting children on <textarea>.\n' +
' in textarea (at **)',
]);
const node = container.firstChild;
expect(node.value).toBe('');
@@ -931,11 +948,15 @@ describe('ReactDOMTextarea', () => {
root.render(<textarea value="foo" onChange={() => {}} />);
});
await expect(async () => {
await act(() => {
root.render(<textarea value={() => {}} onChange={() => {}} />);
});
}).toErrorDev('Invalid value for prop `value`');
await act(() => {
root.render(<textarea value={() => {}} onChange={() => {}} />);
});
assertConsoleErrorDev([
'Invalid value for prop `value` on <textarea> tag. ' +
'Either remove it from the element, or pass a string or number value to keep it in the DOM. ' +
'For details, see https://react.dev/link/attribute-behavior \n' +
' in textarea (at **)',
]);
const node = container.firstChild;
expect(node.value).toBe('');
@@ -1054,60 +1075,60 @@ describe('ReactDOMTextarea', () => {
it('should warn about missing onChange if value is false', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await expect(async () => {
await act(() => {
root.render(<textarea value={false} />);
});
}).toErrorDev(
await act(() => {
root.render(<textarea value={false} />);
});
assertConsoleErrorDev([
'You provided a `value` prop to a form ' +
'field without an `onChange` handler. This will render a read-only ' +
'field. If the field should be mutable use `defaultValue`. ' +
'Otherwise, set either `onChange` or `readOnly`.',
);
'Otherwise, set either `onChange` or `readOnly`.\n' +
' in textarea (at **)',
]);
});
it('should warn about missing onChange if value is 0', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await expect(async () => {
await act(() => {
root.render(<textarea value={0} />);
});
}).toErrorDev(
await act(() => {
root.render(<textarea value={0} />);
});
assertConsoleErrorDev([
'You provided a `value` prop to a form ' +
'field without an `onChange` handler. This will render a read-only ' +
'field. If the field should be mutable use `defaultValue`. ' +
'Otherwise, set either `onChange` or `readOnly`.',
);
'Otherwise, set either `onChange` or `readOnly`.\n' +
' in textarea (at **)',
]);
});
it('should warn about missing onChange if value is "0"', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await expect(async () => {
await act(() => {
root.render(<textarea value="0" />);
});
}).toErrorDev(
await act(() => {
root.render(<textarea value="0" />);
});
assertConsoleErrorDev([
'You provided a `value` prop to a form ' +
'field without an `onChange` handler. This will render a read-only ' +
'field. If the field should be mutable use `defaultValue`. ' +
'Otherwise, set either `onChange` or `readOnly`.',
);
'Otherwise, set either `onChange` or `readOnly`.\n' +
' in textarea (at **)',
]);
});
it('should warn about missing onChange if value is ""', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await expect(async () => {
await act(() => {
root.render(<textarea value="" />);
});
}).toErrorDev(
await act(() => {
root.render(<textarea value="" />);
});
assertConsoleErrorDev([
'You provided a `value` prop to a form ' +
'field without an `onChange` handler. This will render a read-only ' +
'field. If the field should be mutable use `defaultValue`. ' +
'Otherwise, set either `onChange` or `readOnly`.',
);
'Otherwise, set either `onChange` or `readOnly`.\n' +
' in textarea (at **)',
]);
});
});

View File

@@ -896,7 +896,9 @@ describe('ReactErrorBoundaries', () => {
);
});
assertConsoleErrorDev([
'BrokenComponentWillMountWithContext uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
'BrokenComponentWillMountWithContext uses the legacy childContextTypes API which will soon be removed. ' +
'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
' in BrokenComponentWillMountWithContext (at **)',
]);
expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
});
@@ -2366,44 +2368,53 @@ describe('ReactErrorBoundaries', () => {
it('discards a bad root if the root component fails', async () => {
const X = null;
const Y = undefined;
let err1;
let err2;
try {
await expect(async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await expect(
async () =>
await act(async () => {
root.render(<X />, container);
}),
).toErrorDev(
'React.createElement: type is invalid -- expected a string ' +
'(for built-in components) or a class/function ' +
'(for composite components) but got: null.',
await act(async () => {
root.render(<X />);
});
}).rejects.toThrow(
'Element type is invalid: ' +
'expected a string (for built-in components) or a ' +
'class/function (for composite components) but got: null.',
);
if (!gate('enableOwnerStacks')) {
assertConsoleErrorDev(
[
'React.jsx: type is invalid -- expected a string ' +
'(for built-in components) or a class/function ' +
'(for composite components) but got: null.',
],
{withoutStack: true},
);
} catch (err) {
err1 = err;
}
try {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await expect(
async () =>
await act(async () => {
root.render(<Y />, container);
}),
).toErrorDev(
'React.createElement: type is invalid -- expected a string ' +
'(for built-in components) or a class/function ' +
'(for composite components) but got: undefined.',
);
} catch (err) {
err2 = err;
}
expect(err1.message).toMatch(/got: null/);
expect(err2.message).toMatch(/got: undefined/);
await expect(async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(<Y />);
});
}).rejects.toThrow(
'Element type is invalid: ' +
'expected a string (for built-in components) or a ' +
'class/function (for composite components) but got: undefined.',
);
if (!gate('enableOwnerStacks')) {
assertConsoleErrorDev(
[
'React.jsx: type is invalid -- expected a string ' +
'(for built-in components) or a class/function ' +
'(for composite components) but got: undefined. ' +
"You likely forgot to export your component from the file it's defined in, " +
'or you might have mixed up default and named imports.',
],
{withoutStack: true},
);
}
});
it('renders empty output if error boundary does not handle the error', async () => {
@@ -2570,18 +2581,18 @@ describe('ReactErrorBoundaries', () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await expect(async () => {
await act(async () => {
root.render(
<InvalidErrorBoundary>
<Throws />
</InvalidErrorBoundary>,
);
});
}).toErrorDev(
await act(async () => {
root.render(
<InvalidErrorBoundary>
<Throws />
</InvalidErrorBoundary>,
);
});
assertConsoleErrorDev([
'InvalidErrorBoundary: Error boundaries should implement getDerivedStateFromError(). ' +
'In that method, return a state update to display an error message or fallback UI.',
);
'In that method, return a state update to display an error message or fallback UI.\n' +
' in InvalidErrorBoundary (at **)',
]);
expect(container.textContent).toBe('');
});

View File

@@ -111,8 +111,14 @@ describe('ReactFunctionComponent', () => {
});
assertConsoleErrorDev([
'GrandParent uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
'Child uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
'GrandParent uses the legacy childContextTypes API which will soon be removed. ' +
'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
' in GrandParent (at **)',
'Child uses the legacy contextTypes API which will soon be removed. ' +
'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
(gate('enableOwnerStacks') ? '' : ' in Child (at **)\n') +
' in Parent (at **)\n' +
' in GrandParent (at **)',
]);
expect(el.textContent).toBe('test');
@@ -132,15 +138,15 @@ describe('ReactFunctionComponent', () => {
const container = document.createElement('div');
await expect(async () => {
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<FunctionComponentWithChildContext />);
});
}).toErrorDev(
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<FunctionComponentWithChildContext />);
});
assertConsoleErrorDev([
'FunctionComponentWithChildContext: Function ' +
'components do not support getDerivedStateFromProps.',
);
'components do not support getDerivedStateFromProps.\n' +
' in FunctionComponentWithChildContext (at **)',
]);
});
it('should warn for childContextTypes on a function component', async () => {
@@ -154,14 +160,16 @@ describe('ReactFunctionComponent', () => {
const container = document.createElement('div');
await expect(async () => {
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<FunctionComponentWithChildContext name="A" />);
});
}).toErrorDev(
'childContextTypes cannot ' + 'be defined on a function component.',
);
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<FunctionComponentWithChildContext name="A" />);
});
assertConsoleErrorDev([
'childContextTypes cannot ' +
'be defined on a function component.\n' +
' FunctionComponentWithChildContext.childContextTypes = ...\n' +
' in FunctionComponentWithChildContext (at **)',
]);
});
it('should not throw when stateless component returns undefined', async () => {
@@ -184,16 +192,18 @@ describe('ReactFunctionComponent', () => {
return <div>{[<span />]}</div>;
}
await expect(async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<Child />);
});
}).toErrorDev(
'Each child in a list should have a unique "key" prop.\n\n' +
'Check the render method of `Child`.',
);
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<Child />);
});
assertConsoleErrorDev([
'Each child in a list should have a unique "key" prop.\n' +
'\n' +
'Check the render method of `Child`. See https://react.dev/link/warning-keys for more information.\n' +
' in span (at **)\n' +
' in Child (at **)',
]);
});
// @gate !disableDefaultPropsExceptForClasses
@@ -203,16 +213,17 @@ describe('ReactFunctionComponent', () => {
}
Child.defaultProps = {test: 2};
await expect(async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<Child />);
});
expect(container.textContent).toBe('2');
}).toErrorDev([
'Child: Support for defaultProps will be removed from function components in a future major release. Use JavaScript default parameters instead.',
await act(() => {
root.render(<Child />);
});
expect(container.textContent).toBe('2');
assertConsoleErrorDev([
'Child: Support for defaultProps will be removed from function components in a future major release. ' +
'Use JavaScript default parameters instead.\n' +
' in Child (at **)',
]);
});
@@ -244,8 +255,13 @@ describe('ReactFunctionComponent', () => {
root.render(<Parent />);
});
assertConsoleErrorDev([
'Parent uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
'Child uses the legacy contextTypes API which will be removed soon. Use React.createContext() with React.useContext() instead.',
'Parent uses the legacy childContextTypes API which will soon be removed. ' +
'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
' in Parent (at **)',
'Child uses the legacy contextTypes API which will be removed soon. ' +
'Use React.createContext() with React.useContext() instead. (https://react.dev/link/legacy-context)\n' +
' in Child (at **)\n' +
' in Parent (at **)',
]);
expect(el.textContent).toBe('en');
});

View File

@@ -12,6 +12,7 @@
let React;
let ReactDOMClient;
let act;
let assertConsoleErrorDev;
describe('ReactIdentity', () => {
beforeEach(() => {
@@ -19,6 +20,8 @@ describe('ReactIdentity', () => {
React = require('react');
ReactDOMClient = require('react-dom/client');
act = require('internal-test-utils').act;
assertConsoleErrorDev =
require('internal-test-utils').assertConsoleErrorDev;
});
it('should allow key property to express identity', async () => {
@@ -313,17 +316,18 @@ describe('ReactIdentity', () => {
const el = document.createElement('div');
const root = ReactDOMClient.createRoot(el);
await expect(() =>
expect(() => {
root.render(
<div>
<span key={new TemporalLike()} />
</div>,
);
}).toThrowError(new TypeError('prod message')),
).toErrorDev(
'The provided key is an unsupported type TemporalLike.' +
' This value must be coerced to a string before using it here.',
await expect(() => {
root.render(
<div>
<span key={new TemporalLike()} />
</div>,
);
}).toThrowError(new TypeError('prod message'));
assertConsoleErrorDev(
[
'The provided key is an unsupported type TemporalLike.' +
' This value must be coerced to a string before using it here.',
],
{withoutStack: true},
);
});

View File

@@ -50,15 +50,12 @@ describe('ReactLegacyCompositeComponent', () => {
return <div />;
}
}
let instance;
expect(() => {
instance = ReactDOM.render(<Component />, container);
}).toErrorDev(
const instance = ReactDOM.render(<Component />, container);
assertConsoleErrorDev([
'Cannot update during an existing state transition (such as within ' +
'`render`). Render methods should be a pure function of props and state.',
);
'`render`). Render methods should be a pure function of props and state.\n' +
' in Component (at **)',
]);
// The setState call is queued and then executed as a second pass. This
// behavior is undefined though so we're free to change it to suit the
@@ -122,8 +119,16 @@ describe('ReactLegacyCompositeComponent', () => {
root.render(<Parent ref={current => (component = current)} />);
});
assertConsoleErrorDev([
'Child uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
'Grandchild uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
'Child uses the legacy childContextTypes API which will soon be removed. ' +
'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
(gate('enableOwnerStacks') ? '' : ' in Child (at **)\n') +
' in Parent (at **)',
'Grandchild uses the legacy contextTypes API which will soon be removed. ' +
'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
(gate('enableOwnerStacks')
? ''
: ' in Grandchild (at **)\n' + ' in Child (at **)\n') +
' in Parent (at **)',
]);
expect(findDOMNode(component).innerHTML).toBe('bar');
});
@@ -190,8 +195,15 @@ describe('ReactLegacyCompositeComponent', () => {
expect(childInstance.context).toEqual({foo: 'bar', flag: false});
assertConsoleErrorDev([
'Parent uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
'Child uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
'Parent uses the legacy childContextTypes API which will soon be removed. ' +
'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
' in Parent (at **)',
'Child uses the legacy contextTypes API which will soon be removed. ' +
'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
' in Child (at **)' +
(gate('enableOwnerStacks')
? ''
: '\n in Middle (at **)' + '\n in Parent (at **)'),
]);
await act(() => {
@@ -254,8 +266,18 @@ describe('ReactLegacyCompositeComponent', () => {
});
assertConsoleErrorDev([
'Parent uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
'Child uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
'Parent uses the legacy childContextTypes API which will soon be removed. ' +
'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
(gate('enableOwnerStacks') ? '' : ' in Parent (at **)\n') +
' in Wrapper (at **)',
'Child uses the legacy contextTypes API which will soon be removed. ' +
'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
(gate('enableOwnerStacks')
? ''
: ' in Child (at **)\n' +
' in div (at **)\n' +
' in Parent (at **)\n') +
' in Wrapper (at **)',
]);
expect(wrapper.parentRef.current.state.flag).toEqual(true);
@@ -334,10 +356,22 @@ describe('ReactLegacyCompositeComponent', () => {
});
assertConsoleErrorDev([
'Parent uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
'Child uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
'Child uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
'Grandchild uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
'Parent uses the legacy childContextTypes API which will soon be removed. ' +
'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
' in Parent (at **)',
'Child uses the legacy childContextTypes API which will soon be removed. ' +
'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
(gate('enableOwnerStacks') ? '' : ' in Child (at **)\n') +
' in Parent (at **)',
'Child uses the legacy contextTypes API which will soon be removed. ' +
'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
(gate('enableOwnerStacks') ? '' : ' in Child (at **)\n') +
' in Parent (at **)',
'Grandchild uses the legacy contextTypes API which will soon be removed. ' +
'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
(gate('enableOwnerStacks') ? '' : ' in Grandchild (at **)\n') +
' in Child (at **)\n' +
' in Parent (at **)',
]);
expect(childInstance.context).toEqual({foo: 'bar', depth: 0});
@@ -393,7 +427,9 @@ describe('ReactLegacyCompositeComponent', () => {
root.render(<Parent ref={current => (parentInstance = current)} />);
});
assertConsoleErrorDev([
'Parent uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
'Parent uses the legacy childContextTypes API which will soon be removed. ' +
'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
' in Parent (at **)',
]);
expect(childInstance).toBeNull();
@@ -403,7 +439,10 @@ describe('ReactLegacyCompositeComponent', () => {
parentInstance.setState({flag: true});
});
assertConsoleErrorDev([
'Child uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
'Child uses the legacy contextTypes API which will soon be removed. ' +
'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
(gate('enableOwnerStacks') ? '' : ' in Child (at **)\n') +
' in Parent (at **)',
]);
expect(parentInstance.state.flag).toBe(true);
@@ -465,11 +504,15 @@ describe('ReactLegacyCompositeComponent', () => {
}
const div = document.createElement('div');
expect(() => {
ReactDOM.render(<Parent cntxt="noise" />, div);
}).toErrorDev([
'Parent uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
'Leaf uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
ReactDOM.render(<Parent cntxt="noise" />, div);
assertConsoleErrorDev([
'Parent uses the legacy childContextTypes API which will soon be removed. ' +
'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
' in Parent (at **)',
'Leaf uses the legacy contextTypes API which will soon be removed. ' +
'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
' in Intermediary (at **)\n' +
' in Parent (at **)',
]);
expect(div.children[0].innerHTML).toBe('noise');
div.children[0].innerHTML = 'aliens';
@@ -572,25 +615,30 @@ describe('ReactLegacyCompositeComponent', () => {
const div = document.createElement('div');
let parentInstance = null;
expect(() => {
ReactDOM.render(
<Parent ref={inst => (parentInstance = inst)}>
<ChildWithoutContext>
A1
<GrandChild>A2</GrandChild>
</ChildWithoutContext>
ReactDOM.render(
<Parent ref={inst => (parentInstance = inst)}>
<ChildWithoutContext>
A1
<GrandChild>A2</GrandChild>
</ChildWithoutContext>
<ChildWithContext>
B1
<GrandChild>B2</GrandChild>
</ChildWithContext>
</Parent>,
div,
);
}).toErrorDev([
'Parent uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
'GrandChild uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
'ChildWithContext uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
<ChildWithContext>
B1
<GrandChild>B2</GrandChild>
</ChildWithContext>
</Parent>,
div,
);
assertConsoleErrorDev([
'Parent uses the legacy childContextTypes API which will soon be removed. ' +
'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
' in Parent (at **)',
'GrandChild uses the legacy contextTypes API which will soon be removed. ' +
'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
' in GrandChild (at **)',
'ChildWithContext uses the legacy contextTypes API which will soon be removed. ' +
'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
' in ChildWithContext (at **)',
]);
parentInstance.setState({
@@ -774,16 +822,19 @@ describe('ReactLegacyCompositeComponent', () => {
}
const div = document.createElement('div');
expect(() => {
ReactDOM.render(
<Parent>
<Component />
</Parent>,
div,
);
}).toErrorDev([
'Parent uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
'Component uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
ReactDOM.render(
<Parent>
<Component />
</Parent>,
div,
);
assertConsoleErrorDev([
'Parent uses the legacy childContextTypes API which will soon be removed. ' +
'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
' in Parent (at **)',
'Component uses the legacy contextTypes API which will soon be removed. ' +
'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
' in Component (at **)',
]);
});

View File

@@ -14,6 +14,7 @@ let ReactDOMClient;
let ReactDOMServer;
let ReactFeatureFlags;
let act;
let assertConsoleErrorDev;
describe('ReactLegacyContextDisabled', () => {
beforeEach(() => {
@@ -25,6 +26,8 @@ describe('ReactLegacyContextDisabled', () => {
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.disableLegacyContext = true;
act = require('internal-test-utils').act;
assertConsoleErrorDev =
require('internal-test-utils').assertConsoleErrorDev;
});
function formatValue(val) {
@@ -84,25 +87,33 @@ describe('ReactLegacyContextDisabled', () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await expect(async () => {
await act(() => {
root.render(
<LegacyProvider>
<span>
<LegacyClsConsumer />
<LegacyFnConsumer />
<RegularFn />
</span>
</LegacyProvider>,
);
});
}).toErrorDev([
await act(() => {
root.render(
<LegacyProvider>
<span>
<LegacyClsConsumer />
<LegacyFnConsumer />
<RegularFn />
</span>
</LegacyProvider>,
);
});
assertConsoleErrorDev([
'LegacyProvider uses the legacy childContextTypes API which was removed in React 19. ' +
'Use React.createContext() instead.',
'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
' in LegacyProvider (at **)',
'LegacyClsConsumer uses the legacy contextTypes API which was removed in React 19. ' +
'Use React.createContext() with static contextType instead.',
'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
' in LegacyClsConsumer (at **)' +
(gate('enableOwnerStacks')
? ''
: '\n' + ' in span (at **)\n' + ' in LegacyProvider (at **)'),
'LegacyFnConsumer uses the legacy contextTypes API which was removed in React 19. ' +
'Use React.createContext() with React.useContext() instead.',
'Use React.createContext() with React.useContext() instead. (https://react.dev/link/legacy-context)\n' +
' in LegacyFnConsumer (at **)' +
(gate('enableOwnerStacks')
? ''
: '\n' + ' in span (at **)\n' + ' in LegacyProvider (at **)'),
]);
expect(container.textContent).toBe('{}undefinedundefined');
expect(lifecycleContextLog).toEqual([]);
@@ -124,25 +135,32 @@ describe('ReactLegacyContextDisabled', () => {
root.unmount();
// test server path.
let text;
expect(() => {
text = ReactDOMServer.renderToString(
<LegacyProvider>
<span>
<LegacyClsConsumer />
<LegacyFnConsumer />
<RegularFn />
</span>
</LegacyProvider>,
container,
);
}).toErrorDev([
const text = ReactDOMServer.renderToString(
<LegacyProvider>
<span>
<LegacyClsConsumer />
<LegacyFnConsumer />
<RegularFn />
</span>
</LegacyProvider>,
container,
);
assertConsoleErrorDev([
'LegacyProvider uses the legacy childContextTypes API which was removed in React 19. ' +
'Use React.createContext() instead.',
'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
' in LegacyProvider (at **)',
'LegacyClsConsumer uses the legacy contextTypes API which was removed in React 19. ' +
'Use React.createContext() with static contextType instead.',
'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
' in LegacyClsConsumer (at **)' +
(gate('enableOwnerStacks')
? ''
: '\n' + ' in span (at **)\n' + ' in LegacyProvider (at **)'),
'LegacyFnConsumer uses the legacy contextTypes API which was removed in React 19. ' +
'Use React.createContext() with React.useContext() instead.',
'Use React.createContext() with React.useContext() instead. (https://react.dev/link/legacy-context)\n' +
' in LegacyFnConsumer (at **)' +
(gate('enableOwnerStacks')
? ''
: '\n' + ' in span (at **)\n' + ' in LegacyProvider (at **)'),
]);
expect(text).toBe('<span>{}<!-- -->undefined<!-- -->undefined</span>');
expect(lifecycleContextLog).toEqual([{}, {}, {}]);

View File

@@ -850,7 +850,9 @@ describe('ReactLegacyErrorBoundaries', () => {
container,
);
assertConsoleErrorDev([
'BrokenComponentWillMountWithContext uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
'BrokenComponentWillMountWithContext uses the legacy childContextTypes API which will soon be removed. ' +
'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
' in BrokenComponentWillMountWithContext (at **)',
]);
expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
});
@@ -2142,19 +2144,20 @@ describe('ReactLegacyErrorBoundaries', () => {
// @gate !disableLegacyMode
it('renders empty output if error boundary does not handle the error', async () => {
const container = document.createElement('div');
expect(() => {
ReactDOM.render(
<div>
Sibling
<NoopErrorBoundary>
<BrokenRender />
</NoopErrorBoundary>
</div>,
container,
);
}).toErrorDev(
'ErrorBoundary: Error boundaries should implement getDerivedStateFromError()',
ReactDOM.render(
<div>
Sibling
<NoopErrorBoundary>
<BrokenRender />
</NoopErrorBoundary>
</div>,
container,
);
assertConsoleErrorDev([
'NoopErrorBoundary: Error boundaries should implement getDerivedStateFromError(). ' +
'In that method, return a state update to display an error message or fallback UI.\n' +
' in NoopErrorBoundary (at **)',
]);
expect(container.firstChild.textContent).toBe('Sibling');
expect(log).toEqual([
'NoopErrorBoundary constructor',

View File

@@ -15,6 +15,7 @@ let React;
let ReactDOM;
let ReactDOMClient;
let waitForAll;
let assertConsoleErrorDev;
describe('ReactMount', () => {
beforeEach(() => {
@@ -26,6 +27,7 @@ describe('ReactMount', () => {
const InternalTestUtils = require('internal-test-utils');
waitForAll = InternalTestUtils.waitForAll;
assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev;
});
describe('unmountComponentAtNode', () => {
@@ -63,14 +65,15 @@ describe('ReactMount', () => {
}
}
expect(() => {
const container = document.createElement('div');
ReactDOM.render(Component, container);
}).toErrorDev(
'Functions are not valid as a React child. ' +
'This may happen if you return Component instead of <Component /> from render. ' +
'Or maybe you meant to call this function rather than return it.\n' +
' root.render(Component)',
const container = document.createElement('div');
ReactDOM.render(Component, container);
assertConsoleErrorDev(
[
'Functions are not valid as a React child. ' +
'This may happen if you return Component instead of <Component /> from render. ' +
'Or maybe you meant to call this function rather than return it.\n' +
' root.render(Component)',
],
{withoutStack: true},
);
});
@@ -168,11 +171,14 @@ describe('ReactMount', () => {
// Test that blasting away children throws a warning
const rootNode = container.firstChild;
expect(() => ReactDOM.render(<span />, rootNode)).toErrorDev(
'Replacing React-rendered children with a new ' +
'root component. If you intended to update the children of this node, ' +
'you should instead have the existing children update their state and ' +
'render the new components instead of calling ReactDOM.render.',
ReactDOM.render(<span />, rootNode);
assertConsoleErrorDev(
[
'Replacing React-rendered children with a new ' +
'root component. If you intended to update the children of this node, ' +
'you should instead have the existing children update their state and ' +
'render the new components instead of calling ReactDOM.render.',
],
{withoutStack: true},
);
});
@@ -197,9 +203,12 @@ describe('ReactMount', () => {
// Make sure ReactDOM and ReactDOMOther are different copies
expect(ReactDOM).not.toEqual(ReactDOMOther);
expect(() => ReactDOMOther.unmountComponentAtNode(container)).toErrorDev(
"unmountComponentAtNode(): The node you're attempting to unmount " +
'was rendered by another copy of React.',
ReactDOMOther.unmountComponentAtNode(container);
assertConsoleErrorDev(
[
"unmountComponentAtNode(): The node you're attempting to unmount " +
'was rendered by another copy of React.',
],
{withoutStack: true},
);
@@ -351,16 +360,18 @@ describe('ReactMount', () => {
root.render(<div>Hi</div>);
await waitForAll([]);
expect(container.textContent).toEqual('Hi');
expect(() => {
ReactDOM.render(<div>Bye</div>, container);
}).toErrorDev(
ReactDOM.render(<div>Bye</div>, container);
assertConsoleErrorDev(
[
// We care about this warning:
'You are calling ReactDOM.render() on a container that was previously ' +
'passed to ReactDOMClient.createRoot(). This is not supported. ' +
'Did you mean to call root.render(element)?',
// This is more of a symptom but restructuring the code to avoid it isn't worth it:
'Replacing React-rendered children with a new root component.',
'Replacing React-rendered children with a new root component. ' +
'If you intended to update the children of this node, ' +
'you should instead have the existing children update their state ' +
'and render the new components instead of calling ReactDOM.render.',
],
{withoutStack: true},
);
@@ -376,16 +387,16 @@ describe('ReactMount', () => {
root.render(<div>Hi</div>);
await waitForAll([]);
expect(container.textContent).toEqual('Hi');
let unmounted = false;
expect(() => {
unmounted = ReactDOM.unmountComponentAtNode(container);
}).toErrorDev(
const unmounted = ReactDOM.unmountComponentAtNode(container);
assertConsoleErrorDev(
[
// We care about this warning:
'You are calling ReactDOM.unmountComponentAtNode() on a container that was previously ' +
'passed to ReactDOMClient.createRoot(). This is not supported. Did you mean to call root.unmount()?',
// This is more of a symptom but restructuring the code to avoid it isn't worth it:
"The node you're attempting to unmount was rendered by React and is not a top-level container.",
'unmountComponentAtNode(): ' +
"The node you're attempting to unmount was rendered by React and is not a top-level container. " +
'Instead, have the parent component update its state and rerender in order to remove this component.',
],
{withoutStack: true},
);
@@ -407,14 +418,16 @@ describe('ReactMount', () => {
root.render(<div>Hi</div>);
await waitForAll([]);
expect(container.textContent).toEqual('Hi');
let unmounted = false;
expect(() => {
unmounted = ReactDOM.unmountComponentAtNode(container);
}).toErrorDev(
const unmounted = ReactDOM.unmountComponentAtNode(container);
assertConsoleErrorDev(
[
'Did you mean to call root.unmount()?',
'You are calling ReactDOM.unmountComponentAtNode() on a container ' +
'that was previously passed to ReactDOMClient.createRoot(). ' +
'This is not supported. Did you mean to call root.unmount()?',
// This is more of a symptom but restructuring the code to avoid it isn't worth it:
"The node you're attempting to unmount was rendered by React and is not a top-level container.",
'unmountComponentAtNode(): ' +
"The node you're attempting to unmount was rendered by React and is not a top-level container. " +
'Instead, have the parent component update its state and rerender in order to remove this component.',
],
{withoutStack: true},
);
@@ -430,11 +443,12 @@ describe('ReactMount', () => {
it('warns when passing legacy container to createRoot()', () => {
const container = document.createElement('div');
ReactDOM.render(<div>Hi</div>, container);
expect(() => {
ReactDOMClient.createRoot(container);
}).toErrorDev(
'You are calling ReactDOMClient.createRoot() on a container that was previously ' +
'passed to ReactDOM.render(). This is not supported.',
ReactDOMClient.createRoot(container);
assertConsoleErrorDev(
[
'You are calling ReactDOMClient.createRoot() on a container that was previously ' +
'passed to ReactDOM.render(). This is not supported.',
],
{withoutStack: true},
);
});

View File

@@ -15,6 +15,7 @@ let findDOMNode;
let act;
let Scheduler;
let assertLog;
let assertConsoleErrorDev;
// Copy of ReactUpdates using ReactDOM.render and ReactDOM.unstable_batchedUpdates.
// Can be deleted when we remove both.
@@ -27,6 +28,8 @@ describe('ReactLegacyUpdates', () => {
ReactDOM.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE
.findDOMNode;
act = require('internal-test-utils').act;
assertConsoleErrorDev =
require('internal-test-utils').assertConsoleErrorDev;
Scheduler = require('scheduler');
const InternalTestUtils = require('internal-test-utils');
@@ -906,35 +909,37 @@ describe('ReactLegacyUpdates', () => {
let component = ReactDOM.render(<A />, container);
await expect(async () => {
await expect(async () => {
await act(() => {
component.setState({}, 'no');
});
}).rejects.toThrowError(
'Invalid argument passed as callback. Expected a function. Instead ' +
'received: no',
);
}).toErrorDev(
'Expected the last optional `callback` argument to be ' +
'a function. Instead received: no.',
{withoutStack: 1},
await act(() => {
component.setState({}, 'no');
});
}).rejects.toThrowError(
'Invalid argument passed as callback. Expected a function. ' +
'Instead received: no',
);
assertConsoleErrorDev(
[
'Expected the last optional `callback` argument to be ' +
'a function. Instead received: no.',
],
{withoutStack: true},
);
container = document.createElement('div');
component = ReactDOM.render(<A />, container);
await expect(async () => {
await expect(async () => {
await act(() => {
component.setState({}, {foo: 'bar'});
});
}).rejects.toThrowError(
'Invalid argument passed as callback. Expected a function. Instead ' +
'received: [object Object]',
);
}).toErrorDev(
'Expected the last optional `callback` argument to be ' +
"a function. Instead received: { foo: 'bar' }.",
{withoutStack: 1},
await act(() => {
component.setState({}, {foo: 'bar'});
});
}).rejects.toThrowError(
'Invalid argument passed as callback. Expected a function. Instead ' +
'received: [object Object]',
);
assertConsoleErrorDev(
[
'Expected the last optional `callback` argument to be a function. ' +
"Instead received: { foo: 'bar' }.",
],
{withoutStack: true},
);
// Make sure the warning is deduplicated and doesn't fire again
container = document.createElement('div');
@@ -968,34 +973,36 @@ describe('ReactLegacyUpdates', () => {
let component = ReactDOM.render(<A />, container);
await expect(async () => {
await expect(async () => {
await act(() => {
component.forceUpdate('no');
});
}).rejects.toThrowError(
'Invalid argument passed as callback. Expected a function. Instead ' +
'received: no',
);
}).toErrorDev(
'Expected the last optional `callback` argument to be ' +
'a function. Instead received: no.',
{withoutStack: 1},
await act(() => {
component.forceUpdate('no');
});
}).rejects.toThrowError(
'Invalid argument passed as callback. Expected a function. Instead ' +
'received: no',
);
assertConsoleErrorDev(
[
'Expected the last optional `callback` argument to be a function. ' +
'Instead received: no.',
],
{withoutStack: true},
);
container = document.createElement('div');
component = ReactDOM.render(<A />, container);
await expect(async () => {
await expect(async () => {
await act(() => {
component.forceUpdate({foo: 'bar'});
});
}).rejects.toThrowError(
'Invalid argument passed as callback. Expected a function. Instead ' +
'received: [object Object]',
);
}).toErrorDev(
'Expected the last optional `callback` argument to be ' +
"a function. Instead received: { foo: 'bar' }.",
{withoutStack: 1},
await act(() => {
component.forceUpdate({foo: 'bar'});
});
}).rejects.toThrowError(
'Invalid argument passed as callback. Expected a function. Instead ' +
'received: [object Object]',
);
assertConsoleErrorDev(
[
'Expected the last optional `callback` argument to be a function. ' +
"Instead received: { foo: 'bar' }.",
],
{withoutStack: true},
);
// Make sure the warning is deduplicated and doesn't fire again
container = document.createElement('div');
@@ -1318,9 +1325,12 @@ describe('ReactLegacyUpdates', () => {
}
const container = document.createElement('div');
expect(() => ReactDOM.render(<Foo />, container)).toErrorDev(
'Cannot update during an existing state transition',
);
ReactDOM.render(<Foo />, container);
assertConsoleErrorDev([
'Cannot update during an existing state transition (such as within `render`). ' +
'Render methods should be a pure function of props and state.\n' +
' in Foo (at **)',
]);
expect(ops).toEqual(['base: 0, memoized: 0', 'base: 1, memoized: 1']);
});

View File

@@ -13,6 +13,8 @@ const React = require('react');
const ReactDOM = require('react-dom');
const ReactDOMClient = require('react-dom/client');
const act = require('internal-test-utils').act;
const assertConsoleErrorDev =
require('internal-test-utils').assertConsoleErrorDev;
describe('ReactMount', () => {
it('should destroy a react root upon request', async () => {
@@ -63,11 +65,14 @@ describe('ReactMount', () => {
// Test that unmounting at a root node gives a helpful warning
const rootDiv = mainContainerDiv.firstChild;
expect(() => ReactDOM.unmountComponentAtNode(rootDiv)).toErrorDev(
"unmountComponentAtNode(): The node you're attempting to " +
'unmount was rendered by React and is not a top-level container. You ' +
'may have accidentally passed in a React root node instead of its ' +
'container.',
ReactDOM.unmountComponentAtNode(rootDiv);
assertConsoleErrorDev(
[
"unmountComponentAtNode(): The node you're attempting to " +
'unmount was rendered by React and is not a top-level container. You ' +
'may have accidentally passed in a React root node instead of its ' +
'container.',
],
{withoutStack: true},
);
});
@@ -88,11 +93,14 @@ describe('ReactMount', () => {
// Test that unmounting at a non-root node gives a different warning
const nonRootDiv = mainContainerDiv.firstChild.firstChild;
expect(() => ReactDOM.unmountComponentAtNode(nonRootDiv)).toErrorDev(
"unmountComponentAtNode(): The node you're attempting to " +
'unmount was rendered by React and is not a top-level container. ' +
'Instead, have the parent component update its state and rerender in ' +
'order to remove this component.',
ReactDOM.unmountComponentAtNode(nonRootDiv);
assertConsoleErrorDev(
[
"unmountComponentAtNode(): The node you're attempting to " +
'unmount was rendered by React and is not a top-level container. ' +
'Instead, have the parent component update its state and rerender in ' +
'order to remove this component.',
],
{withoutStack: true},
);
});

View File

@@ -13,12 +13,15 @@ describe('ReactMultiChild', () => {
let React;
let ReactDOMClient;
let act;
let assertConsoleErrorDev;
beforeEach(() => {
jest.resetModules();
React = require('react');
ReactDOMClient = require('react-dom/client');
act = require('internal-test-utils').act;
assertConsoleErrorDev =
require('internal-test-utils').assertConsoleErrorDev;
});
describe('reconciliation', () => {
@@ -216,12 +219,10 @@ describe('ReactMultiChild', () => {
root.render(<Parent>{[<div key="1" />]}</Parent>);
});
await expect(
async () =>
await act(async () => {
root.render(<Parent>{[<div key="1" />, <div key="1" />]}</Parent>);
}),
).toErrorDev(
await act(async () => {
root.render(<Parent>{[<div key="1" />, <div key="1" />]}</Parent>);
});
assertConsoleErrorDev([
'Encountered two children with the same key, `1`. ' +
'Keys should be unique so that components maintain their identity ' +
'across updates. Non-unique keys may cause children to be ' +
@@ -234,7 +235,7 @@ describe('ReactMultiChild', () => {
'\n in WrapperComponent (at **)' +
'\n in div (at **)' +
'\n in Parent (at **)'),
);
]);
});
it('should warn for duplicated iterable keys with component stack info', async () => {
@@ -278,16 +279,12 @@ describe('ReactMultiChild', () => {
root.render(<Parent>{createIterable([<div key="1" />])}</Parent>);
});
await expect(
async () =>
await act(async () => {
root.render(
<Parent>
{createIterable([<div key="1" />, <div key="1" />])}
</Parent>,
);
}),
).toErrorDev(
await act(async () => {
root.render(
<Parent>{createIterable([<div key="1" />, <div key="1" />])}</Parent>,
);
});
assertConsoleErrorDev([
'Encountered two children with the same key, `1`. ' +
'Keys should be unique so that components maintain their identity ' +
'across updates. Non-unique keys may cause children to be ' +
@@ -300,7 +297,7 @@ describe('ReactMultiChild', () => {
'\n in WrapperComponent (at **)' +
'\n in div (at **)' +
'\n in Parent (at **)'),
);
]);
});
});
@@ -321,17 +318,15 @@ describe('ReactMultiChild', () => {
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await expect(
async () =>
await act(async () => {
root.render(<Parent />);
}),
).toErrorDev(
await act(async () => {
root.render(<Parent />);
});
assertConsoleErrorDev([
'Using Maps as children is not supported. ' +
'Use an array of keyed ReactElements instead.\n' +
' in div (at **)\n' +
' in Parent (at **)',
);
]);
});
it('should NOT warn for using generator functions as components', async () => {
@@ -362,11 +357,10 @@ describe('ReactMultiChild', () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await expect(async () => {
await act(async () => {
root.render(<Foo />);
});
}).toErrorDev(
await act(async () => {
root.render(<Foo />);
});
assertConsoleErrorDev([
'Using Iterators as children is unsupported and will likely yield ' +
'unexpected results because enumerating a generator mutates it. ' +
'You may convert it to an array with `Array.from()` or the ' +
@@ -374,7 +368,7 @@ describe('ReactMultiChild', () => {
'Iterable that can iterate multiple times over the same items.\n' +
' in div (at **)\n' +
' in Foo (at **)',
);
]);
expect(container.textContent).toBe('HelloWorld');
@@ -407,18 +401,17 @@ describe('ReactMultiChild', () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await expect(async () => {
await act(async () => {
root.render(<Foo />);
});
}).toErrorDev(
await act(async () => {
root.render(<Foo />);
});
assertConsoleErrorDev([
'Using Iterators as children is unsupported and will likely yield ' +
'unexpected results because enumerating a generator mutates it. ' +
'You may convert it to an array with `Array.from()` or the ' +
'`[...spread]` operator before rendering. You can also use an ' +
'Iterable that can iterate multiple times over the same items.\n' +
' in Foo (at **)',
);
]);
expect(container.textContent).toBe('HelloWorld');

View File

@@ -80,98 +80,103 @@ describe('ReactMultiChildText', () => {
jest.setTimeout(30000);
it('should correctly handle all possible children for render and update', async () => {
await expect(async () => {
// prettier-ignore
await testAllPermutations([
// basic values
undefined, [],
null, [],
false, [],
true, [],
0, '0',
1.2, '1.2',
'', [],
'foo', 'foo',
[], [],
[undefined], [],
[null], [],
[false], [],
[true], [],
[0], ['0'],
[1.2], ['1.2'],
[''], [],
['foo'], ['foo'],
[<div />], [<div />],
// two adjacent values
[true, 0], ['0'],
[0, 0], ['0', '0'],
[1.2, 0], ['1.2', '0'],
[0, ''], ['0', ''],
['foo', 0], ['foo', '0'],
[0, <div />], ['0', <div />],
[true, 1.2], ['1.2'],
[1.2, 0], ['1.2', '0'],
[1.2, 1.2], ['1.2', '1.2'],
[1.2, ''], ['1.2', ''],
['foo', 1.2], ['foo', '1.2'],
[1.2, <div />], ['1.2', <div />],
[true, ''], [''],
['', 0], ['', '0'],
[1.2, ''], ['1.2', ''],
['', ''], ['', ''],
['foo', ''], ['foo', ''],
['', <div />], ['', <div />],
[true, 'foo'], ['foo'],
['foo', 0], ['foo', '0'],
[1.2, 'foo'], ['1.2', 'foo'],
['foo', ''], ['foo', ''],
['foo', 'foo'], ['foo', 'foo'],
['foo', <div />], ['foo', <div />],
// values separated by an element
[true, <div />, true], [<div />],
[1.2, <div />, 1.2], ['1.2', <div />, '1.2'],
['', <div />, ''], ['', <div />, ''],
['foo', <div />, 'foo'], ['foo', <div />, 'foo'],
[true, 1.2, <div />, '', 'foo'], ['1.2', <div />, '', 'foo'],
[1.2, '', <div />, 'foo', true], ['1.2', '', <div />, 'foo'],
['', 'foo', <div />, true, 1.2], ['', 'foo', <div />, '1.2'],
[true, 1.2, '', <div />, 'foo', true, 1.2], ['1.2', '', <div />, 'foo', '1.2'],
['', 'foo', true, <div />, 1.2, '', 'foo'], ['', 'foo', <div />, '1.2', '', 'foo'],
// values inside arrays
[[true], [true]], [],
[[1.2], [1.2]], ['1.2', '1.2'],
[[''], ['']], ['', ''],
[['foo'], ['foo']], ['foo', 'foo'],
[[<div />], [<div />]], [<div />, <div />],
[[true, 1.2, <div />], '', 'foo'], ['1.2', <div />, '', 'foo'],
[1.2, '', [<div />, 'foo', true]], ['1.2', '', <div />, 'foo'],
['', ['foo', <div />, true], 1.2], ['', 'foo', <div />, '1.2'],
[true, [1.2, '', <div />, 'foo'], true, 1.2], ['1.2', '', <div />, 'foo', '1.2'],
['', 'foo', [true, <div />, 1.2, ''], 'foo'], ['', 'foo', <div />, '1.2', '', 'foo'],
// values inside elements
[<div>{true}{1.2}{<div />}</div>, '', 'foo'], [<div />, '', 'foo'],
[1.2, '', <div>{<div />}{'foo'}{true}</div>], ['1.2', '', <div />],
['', <div>{'foo'}{<div />}{true}</div>, 1.2], ['', <div />, '1.2'],
[true, <div>{1.2}{''}{<div />}{'foo'}</div>, true, 1.2], [<div />, '1.2'],
['', 'foo', <div>{true}{<div />}{1.2}{''}</div>, 'foo'], ['', 'foo', <div />, 'foo'],
]);
}).toErrorDev([
'Each child in a list should have a unique "key" prop.',
'Each child in a list should have a unique "key" prop.',
spyOnDev(console, 'error').mockImplementation(() => {});
// prettier-ignore
await testAllPermutations([
// basic values
undefined, [],
null, [],
false, [],
true, [],
0, '0',
1.2, '1.2',
'', [],
'foo', 'foo',
[], [],
[undefined], [],
[null], [],
[false], [],
[true], [],
[0], ['0'],
[1.2], ['1.2'],
[''], [],
['foo'], ['foo'],
[<div />], [<div />],
// two adjacent values
[true, 0], ['0'],
[0, 0], ['0', '0'],
[1.2, 0], ['1.2', '0'],
[0, ''], ['0', ''],
['foo', 0], ['foo', '0'],
[0, <div />], ['0', <div />],
[true, 1.2], ['1.2'],
[1.2, 0], ['1.2', '0'],
[1.2, 1.2], ['1.2', '1.2'],
[1.2, ''], ['1.2', ''],
['foo', 1.2], ['foo', '1.2'],
[1.2, <div />], ['1.2', <div />],
[true, ''], [''],
['', 0], ['', '0'],
[1.2, ''], ['1.2', ''],
['', ''], ['', ''],
['foo', ''], ['foo', ''],
['', <div />], ['', <div />],
[true, 'foo'], ['foo'],
['foo', 0], ['foo', '0'],
[1.2, 'foo'], ['1.2', 'foo'],
['foo', ''], ['foo', ''],
['foo', 'foo'], ['foo', 'foo'],
['foo', <div />], ['foo', <div />],
// values separated by an element
[true, <div />, true], [<div />],
[1.2, <div />, 1.2], ['1.2', <div />, '1.2'],
['', <div />, ''], ['', <div />, ''],
['foo', <div />, 'foo'], ['foo', <div />, 'foo'],
[true, 1.2, <div />, '', 'foo'], ['1.2', <div />, '', 'foo'],
[1.2, '', <div />, 'foo', true], ['1.2', '', <div />, 'foo'],
['', 'foo', <div />, true, 1.2], ['', 'foo', <div />, '1.2'],
[true, 1.2, '', <div />, 'foo', true, 1.2], ['1.2', '', <div />, 'foo', '1.2'],
['', 'foo', true, <div />, 1.2, '', 'foo'], ['', 'foo', <div />, '1.2', '', 'foo'],
// values inside arrays
[[true], [true]], [],
[[1.2], [1.2]], ['1.2', '1.2'],
[[''], ['']], ['', ''],
[['foo'], ['foo']], ['foo', 'foo'],
[[<div />], [<div />]], [<div />, <div />],
[[true, 1.2, <div />], '', 'foo'], ['1.2', <div />, '', 'foo'],
[1.2, '', [<div />, 'foo', true]], ['1.2', '', <div />, 'foo'],
['', ['foo', <div />, true], 1.2], ['', 'foo', <div />, '1.2'],
[true, [1.2, '', <div />, 'foo'], true, 1.2], ['1.2', '', <div />, 'foo', '1.2'],
['', 'foo', [true, <div />, 1.2, ''], 'foo'], ['', 'foo', <div />, '1.2', '', 'foo'],
// values inside elements
[<div>{true}{1.2}{<div />}</div>, '', 'foo'], [<div />, '', 'foo'],
[1.2, '', <div>{<div />}{'foo'}{true}</div>], ['1.2', '', <div />],
['', <div>{'foo'}{<div />}{true}</div>, 1.2], ['', <div />, '1.2'],
[true, <div>{1.2}{''}{<div />}{'foo'}</div>, true, 1.2], [<div />, '1.2'],
['', 'foo', <div>{true}{<div />}{1.2}{''}</div>, 'foo'], ['', 'foo', <div />, 'foo'],
]);
if (__DEV__) {
expect(console.error).toHaveBeenCalledTimes(2);
expect(console.error.mock.calls[0][0]).toMatch(
'Each child in a list should have a unique "key" prop.',
);
expect(console.error.mock.calls[1][0]).toMatch(
'Each child in a list should have a unique "key" prop.',
);
}
});
it('should correctly handle bigint children for render and update', async () => {

View File

@@ -16,6 +16,7 @@ let ReactDOMServer;
let act;
let Scheduler;
let assertLog;
let assertConsoleErrorDev;
function getTestDocument(markup) {
const doc = document.implementation.createHTMLDocument('');
@@ -48,6 +49,8 @@ describe('rendering React components at document', () => {
act = require('internal-test-utils').act;
assertLog = require('internal-test-utils').assertLog;
Scheduler = require('scheduler');
assertConsoleErrorDev =
require('internal-test-utils').assertConsoleErrorDev;
});
describe('with new explicit hydration API', () => {
@@ -269,30 +272,46 @@ describe('rendering React components at document', () => {
const favorSafetyOverHydrationPerf = gate(
flags => flags.favorSafetyOverHydrationPerf,
);
expect(() => {
ReactDOM.flushSync(() => {
ReactDOMClient.hydrateRoot(
testDocument,
<Component text="Hello world" />,
{
onRecoverableError: error => {
Scheduler.log(
'onRecoverableError: ' + normalizeError(error.message),
);
if (error.cause) {
Scheduler.log(
'Cause: ' + normalizeError(error.cause.message),
);
}
},
ReactDOM.flushSync(() => {
ReactDOMClient.hydrateRoot(
testDocument,
<Component text="Hello world" />,
{
onRecoverableError: error => {
Scheduler.log(
'onRecoverableError: ' + normalizeError(error.message),
);
if (error.cause) {
Scheduler.log('Cause: ' + normalizeError(error.cause.message));
}
},
);
});
}).toErrorDev(
},
);
});
assertConsoleErrorDev(
favorSafetyOverHydrationPerf
? []
: [
"A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.",
"A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. " +
"This won't be patched up. This can happen if a SSR-ed Client Component used:\n" +
'\n' +
"- A server/client branch `if (typeof window !== 'undefined')`.\n" +
"- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" +
"- Date formatting in a user's locale which doesn't match the server.\n" +
'- External changing data without sending a snapshot of it along with the HTML.\n' +
'- Invalid HTML tag nesting.\n\nIt can also happen if the client has a browser extension ' +
'installed which messes with the HTML before React loaded.\n' +
'\n' +
'https://react.dev/link/hydration-mismatch\n' +
'\n' +
' <Component text="Hello world">\n' +
' <html>\n' +
' <head>\n' +
' <body>\n' +
'+ Hello world\n' +
'- Goodbye world\n' +
'+ Hello world\n' +
'- Goodbye world\n',
],
{withoutStack: true},
);

View File

@@ -14,6 +14,7 @@ let React;
let ReactDOMServer;
let PropTypes;
let ReactSharedInternals;
let assertConsoleErrorDev;
describe('ReactDOMServer', () => {
beforeEach(() => {
@@ -21,6 +22,8 @@ describe('ReactDOMServer', () => {
React = require('react');
PropTypes = require('prop-types');
ReactDOMServer = require('react-dom/server');
assertConsoleErrorDev =
require('internal-test-utils').assertConsoleErrorDev;
ReactSharedInternals =
React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
});
@@ -159,15 +162,18 @@ describe('ReactDOMServer', () => {
});
it('should not crash on poisoned hasOwnProperty', () => {
let html;
expect(
() =>
(html = ReactDOMServer.renderToString(
<div hasOwnProperty="poison">
<span unknown="test" />
</div>,
)),
).toErrorDev(['React does not recognize the `hasOwnProperty` prop']);
const html = ReactDOMServer.renderToString(
<div hasOwnProperty="poison">
<span unknown="test" />
</div>,
);
assertConsoleErrorDev([
'React does not recognize the `hasOwnProperty` prop on a DOM element. ' +
'If you intentionally want it to appear in the DOM as a custom attribute, ' +
'spell it as lowercase `hasownproperty` instead. ' +
'If you accidentally passed it from a parent component, remove it from the DOM element.\n' +
' in div (at **)',
]);
expect(html).toContain('<span unknown="test">');
});
});
@@ -371,16 +377,18 @@ describe('ReactDOMServer', () => {
text: PropTypes.string,
};
let markup;
expect(() => {
markup = ReactDOMServer.renderToStaticMarkup(
<ContextProvider>
<Component />
</ContextProvider>,
);
}).toErrorDev([
'ContextProvider uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
'Component uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
const markup = ReactDOMServer.renderToStaticMarkup(
<ContextProvider>
<Component />
</ContextProvider>,
);
assertConsoleErrorDev([
'ContextProvider uses the legacy childContextTypes API which will soon be removed. ' +
'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
' in ContextProvider (at **)',
'Component uses the legacy contextTypes API which will soon be removed. ' +
'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
' in Component (at **)',
]);
expect(markup).toContain('hello, world');
});
@@ -597,10 +605,15 @@ describe('ReactDOMServer', () => {
}
ReactDOMServer.renderToString(<Foo />);
expect(() => jest.runOnlyPendingTimers()).toErrorDev(
'Can only update a mounting component.' +
' This usually means you called setState() outside componentWillMount() on the server.' +
' This is a no-op.\n\nPlease check the code for the Foo component.',
jest.runOnlyPendingTimers();
assertConsoleErrorDev(
[
'Can only update a mounting component. ' +
'This usually means you called setState() outside componentWillMount() on the server. ' +
'This is a no-op.\n' +
'\n' +
'Please check the code for the Foo component.',
],
{withoutStack: true},
);
@@ -625,10 +638,15 @@ describe('ReactDOMServer', () => {
}
ReactDOMServer.renderToString(<Baz />);
expect(() => jest.runOnlyPendingTimers()).toErrorDev(
'Can only update a mounting component. ' +
'This usually means you called forceUpdate() outside componentWillMount() on the server. ' +
'This is a no-op.\n\nPlease check the code for the Baz component.',
jest.runOnlyPendingTimers();
assertConsoleErrorDev(
[
'Can only update a mounting component. ' +
'This usually means you called forceUpdate() outside componentWillMount() on the server. ' +
'This is a no-op.\n' +
'\n' +
'Please check the code for the Baz component.',
],
{withoutStack: true},
);
const markup = ReactDOMServer.renderToStaticMarkup(<Baz />);
@@ -722,41 +740,50 @@ describe('ReactDOMServer', () => {
// Make sure namespace passes through composites
return <g>{props.children}</g>;
}
expect(() =>
ReactDOMServer.renderToStaticMarkup(
<div>
<inPUT />
<svg>
<CompositeG>
<linearGradient />
<foreignObject>
{/* back to HTML */}
<iFrame />
</foreignObject>
</CompositeG>
</svg>
</div>,
),
).toErrorDev([
ReactDOMServer.renderToStaticMarkup(
<div>
<inPUT />
<svg>
<CompositeG>
<linearGradient />
<foreignObject>
{/* back to HTML */}
<iFrame />
</foreignObject>
</CompositeG>
</svg>
</div>,
);
assertConsoleErrorDev([
'<inPUT /> is using incorrect casing. ' +
'Use PascalCase for React components, ' +
'or lowercase for HTML elements.',
'or lowercase for HTML elements.\n' +
' in inPUT (at **)' +
(gate('enableOwnerStacks') ? '' : '\n in div (at **)'),
// linearGradient doesn't warn
'<iFrame /> is using incorrect casing. ' +
'Use PascalCase for React components, ' +
'or lowercase for HTML elements.',
'or lowercase for HTML elements.\n' +
' in iFrame (at **)' +
(gate('enableOwnerStacks')
? ''
: '\n in foreignObject (at **)' +
'\n in g (at **)' +
'\n in CompositeG (at **)' +
'\n in svg (at **)' +
'\n in div (at **)'),
]);
});
it('should warn about contentEditable and children', () => {
expect(() =>
ReactDOMServer.renderToString(<div contentEditable={true} children="" />),
).toErrorDev(
ReactDOMServer.renderToString(<div contentEditable={true} children="" />);
assertConsoleErrorDev([
'A component is `contentEditable` and contains `children` ' +
'managed by React. It is now your responsibility to guarantee that ' +
'none of those nodes are unexpectedly modified or duplicated. This ' +
'is probably not intentional.\n in div (at **)',
);
'is probably not intentional.\n' +
' in div (at **)',
]);
});
it('should warn when server rendering a class with a render method that does not extend React.Component', () => {
@@ -766,15 +793,15 @@ describe('ReactDOMServer', () => {
}
}
expect(() => {
expect(() =>
ReactDOMServer.renderToString(<ClassWithRenderNotExtended />),
).toThrow(TypeError);
}).toErrorDev(
expect(() =>
ReactDOMServer.renderToString(<ClassWithRenderNotExtended />),
).toThrow(TypeError);
assertConsoleErrorDev([
'The <ClassWithRenderNotExtended /> component appears to have a render method, ' +
"but doesn't extend React.Component. This is likely to cause errors. " +
'Change ClassWithRenderNotExtended to extend React.Component instead.',
);
'Change ClassWithRenderNotExtended to extend React.Component instead.\n' +
' in ClassWithRenderNotExtended (at **)',
]);
// Test deduplication
expect(() => {
@@ -839,7 +866,8 @@ describe('ReactDOMServer', () => {
);
}
expect(() => ReactDOMServer.renderToString(<App />)).toErrorDev([
ReactDOMServer.renderToString(<App />);
assertConsoleErrorDev([
'Invalid ARIA attribute `ariaTypo`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
(gate(flags => flags.enableOwnerStacks)
? ' in span (at **)\n' +
@@ -897,7 +925,8 @@ describe('ReactDOMServer', () => {
);
}
expect(() => ReactDOMServer.renderToString(<App />)).toErrorDev([
ReactDOMServer.renderToString(<App />);
assertConsoleErrorDev([
// ReactDOMServer(App > div > span)
'Invalid ARIA attribute `ariaTypo`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
(gate(flags => flags.enableOwnerStacks)
@@ -907,12 +936,23 @@ describe('ReactDOMServer', () => {
' in App (at **)'),
// ReactDOMServer(App > div > Child) >>> ReactDOMServer(App2) >>> ReactDOMServer(blink)
'Invalid ARIA attribute `ariaTypo2`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
' in blink (at **)',
(gate(flags => flags.enableOwnerStacks)
? ' in blink (at **)\n' +
' in App2 (at **)\n' +
' in Child (at **)\n' +
' in App (at **)'
: ' in blink (at **)'),
// ReactDOMServer(App > div > Child) >>> ReactDOMServer(App2 > Child2 > span)
'Invalid ARIA attribute `ariaTypo3`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
' in span (at **)\n' +
' in Child2 (at **)\n' +
' in App2 (at **)',
(gate(flags => flags.enableOwnerStacks)
? ' in span (at **)\n' +
' in Child2 (at **)\n' +
' in App2 (at **)\n' +
' in Child (at **)\n' +
' in App (at **)'
: ' in span (at **)\n' +
' in Child2 (at **)\n' +
' in App2 (at **)'),
// ReactDOMServer(App > div > Child > span)
'Invalid ARIA attribute `ariaTypo4`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
(gate(flags => flags.enableOwnerStacks)
@@ -943,13 +983,13 @@ describe('ReactDOMServer', () => {
}
}
expect(() => {
ReactDOMServer.renderToString(<ComponentA />);
}).toErrorDev(
ReactDOMServer.renderToString(<ComponentA />);
assertConsoleErrorDev([
'ComponentA defines an invalid contextType. ' +
'contextType should point to the Context object returned by React.createContext(). ' +
'Did you accidentally pass the Context.Consumer instead?',
);
'Did you accidentally pass the Context.Consumer instead?\n' +
' in ComponentA (at **)',
]);
// Warnings should be deduped by component type
ReactDOMServer.renderToString(<ComponentA />);
@@ -988,17 +1028,17 @@ describe('ReactDOMServer', () => {
}
expect(() => {
expect(() => {
ReactDOMServer.renderToString(<Foo />);
}).toThrow("Cannot read property 'world' of undefined");
}).toErrorDev(
ReactDOMServer.renderToString(<Foo />);
}).toThrow("Cannot read property 'world' of undefined");
assertConsoleErrorDev([
'Foo defines an invalid contextType. ' +
'contextType should point to the Context object returned by React.createContext(). ' +
'However, it is set to undefined. ' +
'This can be caused by a typo or by mixing up named and default imports. ' +
'This can also happen due to a circular dependency, ' +
'so try moving the createContext() call to a separate file.',
);
'so try moving the createContext() call to a separate file.\n' +
' in Foo (at **)',
]);
});
it('should warn when class contextType is an object', () => {
@@ -1014,14 +1054,14 @@ describe('ReactDOMServer', () => {
}
expect(() => {
expect(() => {
ReactDOMServer.renderToString(<Foo />);
}).toThrow("Cannot read property 'hello' of undefined");
}).toErrorDev(
ReactDOMServer.renderToString(<Foo />);
}).toThrow("Cannot read property 'hello' of undefined");
assertConsoleErrorDev([
'Foo defines an invalid contextType. ' +
'contextType should point to the Context object returned by React.createContext(). ' +
'However, it is set to an object with keys {x, y}.',
);
'However, it is set to an object with keys {x, y}.\n' +
' in Foo (at **)',
]);
});
it('should warn when class contextType is a primitive', () => {
@@ -1033,14 +1073,14 @@ describe('ReactDOMServer', () => {
}
expect(() => {
expect(() => {
ReactDOMServer.renderToString(<Foo />);
}).toThrow("Cannot read property 'world' of undefined");
}).toErrorDev(
ReactDOMServer.renderToString(<Foo />);
}).toThrow("Cannot read property 'world' of undefined");
assertConsoleErrorDev([
'Foo defines an invalid contextType. ' +
'contextType should point to the Context object returned by React.createContext(). ' +
'However, it is set to a string.',
);
'However, it is set to a string.\n' +
' in Foo (at **)',
]);
});
describe('custom element server rendering', () => {

View File

@@ -17,6 +17,8 @@ let ReactDOMServer;
let ReactDOMServerBrowser;
let waitForAll;
let act;
let assertConsoleErrorDev;
let assertConsoleWarnDev;
// These tests rely both on ReactDOMServer and ReactDOM.
// If a test only needs ReactDOMServer, put it in ReactServerRendering-test instead.
@@ -32,6 +34,8 @@ describe('ReactDOMServerHydration', () => {
const InternalTestUtils = require('internal-test-utils');
waitForAll = InternalTestUtils.waitForAll;
act = InternalTestUtils.act;
assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev;
assertConsoleWarnDev = InternalTestUtils.assertConsoleWarnDev;
});
it('should have the correct mounting behavior', async () => {
@@ -126,26 +130,40 @@ describe('ReactDOMServerHydration', () => {
const favorSafetyOverHydrationPerf = gate(
flags => flags.favorSafetyOverHydrationPerf,
);
await expect(async () => {
root = await act(() => {
return ReactDOMClient.hydrateRoot(
element,
<TestComponent
name="y"
ref={current => {
instance = current;
}}
/>,
{
onRecoverableError: error => {},
},
);
});
}).toErrorDev(
root = await act(() => {
return ReactDOMClient.hydrateRoot(
element,
<TestComponent
name="y"
ref={current => {
instance = current;
}}
/>,
{
onRecoverableError: error => {},
},
);
});
assertConsoleErrorDev(
favorSafetyOverHydrationPerf
? []
: [
"A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.",
"A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. " +
"This won't be patched up. This can happen if a SSR-ed Client Component used:\n" +
'\n' +
"- A server/client branch `if (typeof window !== 'undefined')`.\n" +
"- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" +
"- Date formatting in a user's locale which doesn't match the server.\n" +
'- External changing data without sending a snapshot of it along with the HTML.\n' +
'- Invalid HTML tag nesting.\n\nIt can also happen if the client has a browser extension ' +
'installed which messes with the HTML before React loaded.\n' +
'\n' +
'https://react.dev/link/hydration-mismatch\n' +
'\n' +
' <TestComponent name="y" ref={function ref}>\n' +
' <span ref={{current:null}} onClick={function}>\n' +
'+ y\n' +
'- x\n',
],
{withoutStack: true},
);
@@ -224,21 +242,34 @@ describe('ReactDOMServerHydration', () => {
const favorSafetyOverHydrationPerf = gate(
flags => flags.favorSafetyOverHydrationPerf,
);
await expect(async () => {
await act(() => {
ReactDOMClient.hydrateRoot(
element,
<button autoFocus={false} onFocus={onFocusAfterHydration}>
client
</button>,
{onRecoverableError: error => {}},
);
});
}).toErrorDev(
await act(() => {
ReactDOMClient.hydrateRoot(
element,
<button autoFocus={false} onFocus={onFocusAfterHydration}>
client
</button>,
{onRecoverableError: error => {}},
);
});
assertConsoleErrorDev(
favorSafetyOverHydrationPerf
? []
: [
"A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.",
"A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. " +
"This won't be patched up. This can happen if a SSR-ed Client Component used:\n" +
'\n' +
"- A server/client branch `if (typeof window !== 'undefined')`.\n" +
"- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" +
"- Date formatting in a user's locale which doesn't match the server.\n" +
'- External changing data without sending a snapshot of it along with the HTML.\n' +
'- Invalid HTML tag nesting.\n\nIt can also happen if the client has a browser extension ' +
'installed which messes with the HTML before React loaded.\n' +
'\n' +
'https://react.dev/link/hydration-mismatch\n' +
'\n' +
' <button autoFocus={false} onFocus={function mockConstructor}>\n' +
'+ client\n' +
'- server\n',
],
{withoutStack: true},
);
@@ -255,17 +286,37 @@ describe('ReactDOMServerHydration', () => {
expect(element.firstChild.style.textDecoration).toBe('none');
expect(element.firstChild.style.color).toBe('black');
await expect(async () => {
await act(() => {
ReactDOMClient.hydrateRoot(
element,
<div
style={{textDecoration: 'none', color: 'white', height: '10px'}}
/>,
);
});
}).toErrorDev(
"A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.",
await act(() => {
ReactDOMClient.hydrateRoot(
element,
<div
style={{textDecoration: 'none', color: 'white', height: '10px'}}
/>,
);
});
assertConsoleErrorDev(
[
"A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. " +
"This won't be patched up. This can happen if a SSR-ed Client Component used:\n" +
'\n' +
"- A server/client branch `if (typeof window !== 'undefined')`.\n" +
"- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" +
"- Date formatting in a user's locale which doesn't match the server.\n" +
'- External changing data without sending a snapshot of it along with the HTML.\n' +
'- Invalid HTML tag nesting.\n\nIt can also happen if the client has a browser extension ' +
'installed which messes with the HTML before React loaded.\n' +
'\n' +
'https://react.dev/link/hydration-mismatch\n' +
'\n' +
' <div\n style={{\n+ textDecoration: "none"\n' +
'+ color: "white"\n' +
'- color: "black"\n' +
'+ height: "10px"\n' +
'- height: "10px"\n' +
'- text-decoration: "none"\n' +
' }}\n' +
' >\n',
],
{withoutStack: true},
);
});
@@ -303,17 +354,39 @@ describe('ReactDOMServerHydration', () => {
element.innerHTML =
'<div style="text-decoration: none; color: black; height: 10px;"></div>';
await expect(async () => {
await act(() => {
ReactDOMClient.hydrateRoot(
element,
<div
style={{textDecoration: 'none', color: 'black', height: '10px'}}
/>,
);
});
}).toErrorDev(
"A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.",
await act(() => {
ReactDOMClient.hydrateRoot(
element,
<div
style={{textDecoration: 'none', color: 'black', height: '10px'}}
/>,
);
});
assertConsoleErrorDev(
[
"A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. " +
"This won't be patched up. This can happen if a SSR-ed Client Component used:\n" +
'\n' +
"- A server/client branch `if (typeof window !== 'undefined')`.\n" +
"- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" +
"- Date formatting in a user's locale which doesn't match the server.\n" +
'- External changing data without sending a snapshot of it along with the HTML.\n' +
'- Invalid HTML tag nesting.\n\nIt can also happen if the client has a browser extension ' +
'installed which messes with the HTML before React loaded.\n' +
'\n' +
'https://react.dev/link/hydration-mismatch\n' +
'\n' +
' <div\n' +
' style={{\n' +
'+ textDecoration: "none"\n' +
'+ color: "black"\n' +
'- color: "black"\n' +
'+ height: "10px"\n' +
'- height: "10px"\n' +
'- text-decoration: "none"\n' +
' }}\n' +
' >\n',
],
{withoutStack: true},
);
});
@@ -347,18 +420,38 @@ describe('ReactDOMServerHydration', () => {
);
const element = document.createElement('div');
expect(() => {
element.innerHTML = ReactDOMServer.renderToString(markup);
}).toWarnDev('componentWillMount has been renamed');
element.innerHTML = ReactDOMServer.renderToString(markup);
assertConsoleWarnDev([
'componentWillMount has been renamed, and is not recommended for use. ' +
'See https://react.dev/link/unsafe-component-lifecycles for details.\n' +
'\n' +
'* Move code from componentWillMount to componentDidMount (preferred in most cases) or the constructor.\n' +
'\n' +
'Please update the following components: ComponentWithWarning\n' +
' in ComponentWithWarning (at **)',
]);
expect(element.textContent).toBe('Hi');
await expect(async () => {
await act(() => {
ReactDOMClient.hydrateRoot(element, markup);
});
}).toWarnDev('componentWillMount has been renamed', {
withoutStack: true,
await act(() => {
ReactDOMClient.hydrateRoot(element, markup);
});
assertConsoleWarnDev(
[
'componentWillMount has been renamed, and is not recommended for use. ' +
'See https://react.dev/link/unsafe-component-lifecycles for details.\n' +
'\n' +
'* Move code with side effects to componentDidMount, and set initial state in the constructor.\n' +
'* Rename componentWillMount to UNSAFE_componentWillMount to suppress this warning in non-strict mode. ' +
'In React 18.x, only the UNSAFE_ name will work. ' +
'To rename all deprecated lifecycles to their new names, ' +
'you can run `npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n' +
'\n' +
'Please update the following components: ComponentWithWarning',
],
{
withoutStack: true,
},
);
expect(element.textContent).toBe('Hi');
});
@@ -531,21 +624,35 @@ describe('ReactDOMServerHydration', () => {
const favorSafetyOverHydrationPerf = gate(
flags => flags.favorSafetyOverHydrationPerf,
);
await expect(async () => {
await act(() => {
ReactDOMClient.hydrateRoot(
domElement,
<div dangerouslySetInnerHTML={undefined}>
<p>client</p>
</div>,
{onRecoverableError: error => {}},
);
});
}).toErrorDev(
await act(() => {
ReactDOMClient.hydrateRoot(
domElement,
<div dangerouslySetInnerHTML={undefined}>
<p>client</p>
</div>,
{onRecoverableError: error => {}},
);
});
assertConsoleErrorDev(
favorSafetyOverHydrationPerf
? []
: [
"A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.",
"A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. " +
"This won't be patched up. This can happen if a SSR-ed Client Component used:\n" +
'\n' +
"- A server/client branch `if (typeof window !== 'undefined')`.\n" +
"- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" +
"- Date formatting in a user's locale which doesn't match the server.\n" +
'- External changing data without sending a snapshot of it along with the HTML.\n' +
'- Invalid HTML tag nesting.\n\nIt can also happen if the client has a browser extension ' +
'installed which messes with the HTML before React loaded.\n' +
'\n' +
'https://react.dev/link/hydration-mismatch\n' +
'\n' +
' <div dangerouslySetInnerHTML={undefined}>\n' +
' <p>\n' +
'+ client\n' +
'- server\n',
],
{withoutStack: true},
);
@@ -593,13 +700,13 @@ describe('ReactDOMServerHydration', () => {
const jsx = React.createElement('my-custom-element', props);
const element = document.createElement('div');
element.innerHTML = ReactDOMServer.renderToString(jsx);
await expect(async () => {
await act(() => {
ReactDOMClient.hydrateRoot(element, jsx);
});
}).toErrorDev(
`Assignment to read-only property will result in a no-op: \`${readOnlyProperty}\``,
);
await act(() => {
ReactDOMClient.hydrateRoot(element, jsx);
});
assertConsoleErrorDev([
`Assignment to read-only property will result in a no-op: \`${readOnlyProperty}\`
in my-custom-element (at **)`,
]);
}
});

View File

@@ -13,6 +13,7 @@ let Scheduler;
let act;
let container;
let assertLog;
let assertConsoleErrorDev;
jest.useRealTimers();
@@ -88,6 +89,7 @@ function runActTests(render, unmount, rerender) {
const InternalTestUtils = require('internal-test-utils');
assertLog = InternalTestUtils.assertLog;
assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev;
container = document.createElement('div');
document.body.appendChild(container);
@@ -205,8 +207,20 @@ function runActTests(render, unmount, rerender) {
render(<App />, container);
});
expect(() => setValue(1)).toErrorDev([
'An update to App inside a test was not wrapped in act(...).',
setValue(1);
assertConsoleErrorDev([
'An update to App inside a test was not wrapped in act(...).\n' +
'\n' +
'When testing, code that causes React state updates should be wrapped into act(...):\n' +
'\n' +
'act(() => {\n' +
' /* fire events that update state */\n' +
'});\n' +
'/* assert on the output */\n' +
'\n' +
"This ensures that you're testing the behavior the user would see in the browser. " +
'Learn more at https://react.dev/link/wrap-tests-with-act\n' +
' in App (at **)',
]);
});
@@ -232,8 +246,20 @@ function runActTests(render, unmount, rerender) {
rerender(<App defaultValue={0} />, container);
});
expect(() => setValue(1)).toErrorDev([
'An update to App inside a test was not wrapped in act(...).',
setValue(1);
assertConsoleErrorDev([
'An update to App inside a test was not wrapped in act(...).\n' +
'\n' +
'When testing, code that causes React state updates should be wrapped into act(...):\n' +
'\n' +
'act(() => {\n' +
' /* fire events that update state */\n' +
'});\n' +
'/* assert on the output */\n' +
'\n' +
"This ensures that you're testing the behavior the user would see in the browser. " +
'Learn more at https://react.dev/link/wrap-tests-with-act\n' +
' in App (at **)',
]);
});
@@ -251,9 +277,21 @@ function runActTests(render, unmount, rerender) {
});
// First show that it does warn
expect(() => setState(1)).toErrorDev(
'An update to App inside a test was not wrapped in act(...)',
);
setState(1);
assertConsoleErrorDev([
'An update to App inside a test was not wrapped in act(...).\n' +
'\n' +
'When testing, code that causes React state updates should be wrapped into act(...):\n' +
'\n' +
'act(() => {\n' +
' /* fire events that update state */\n' +
'});\n' +
'/* assert on the output */\n' +
'\n' +
"This ensures that you're testing the behavior the user would see in the browser. " +
'Learn more at https://react.dev/link/wrap-tests-with-act\n' +
' in App (at **)',
]);
// Now do the same thing again, but disable with the environment flag
const prevIsActEnvironment = global.IS_REACT_ACT_ENVIRONMENT;
@@ -266,9 +304,21 @@ function runActTests(render, unmount, rerender) {
// When the flag is restored to its previous value, it should start
// warning again. This shows that React reads the flag each time.
expect(() => setState(3)).toErrorDev(
'An update to App inside a test was not wrapped in act(...)',
);
setState(3);
assertConsoleErrorDev([
'An update to App inside a test was not wrapped in act(...).\n' +
'\n' +
'When testing, code that causes React state updates should be wrapped into act(...):\n' +
'\n' +
'act(() => {\n' +
' /* fire events that update state */\n' +
'});\n' +
'/* assert on the output */\n' +
'\n' +
"This ensures that you're testing the behavior the user would see in the browser. " +
'Learn more at https://react.dev/link/wrap-tests-with-act\n' +
' in App (at **)',
]);
});
describe('fake timers', () => {

View File

@@ -18,6 +18,7 @@ let Scheduler;
let waitForAll;
let waitFor;
let assertLog;
let assertConsoleErrorDev;
describe('ReactUpdates', () => {
beforeEach(() => {
@@ -29,6 +30,8 @@ describe('ReactUpdates', () => {
ReactDOM.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE
.findDOMNode;
act = require('internal-test-utils').act;
assertConsoleErrorDev =
require('internal-test-utils').assertConsoleErrorDev;
Scheduler = require('scheduler');
const InternalTestUtils = require('internal-test-utils');
@@ -1039,38 +1042,42 @@ describe('ReactUpdates', () => {
root.render(<A ref={current => (component = current)} />);
});
await expect(
expect(async () => {
await act(() => {
component.setState({}, 'no');
});
}).toErrorDev(
'Expected the last optional `callback` argument to be ' +
'a function. Instead received: no.',
),
).rejects.toThrowError(
await expect(async () => {
await act(() => {
component.setState({}, 'no');
});
}).rejects.toThrowError(
'Invalid argument passed as callback. Expected a function. Instead ' +
'received: no',
);
assertConsoleErrorDev(
[
'Expected the last optional `callback` argument to be ' +
'a function. Instead received: no.',
],
{withoutStack: true},
);
container = document.createElement('div');
root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<A ref={current => (component = current)} />);
});
await expect(
expect(async () => {
await act(() => {
component.setState({}, {foo: 'bar'});
});
}).toErrorDev(
'Expected the last optional `callback` argument to be ' +
'a function. Instead received: [object Object].',
),
).rejects.toThrowError(
await expect(async () => {
await act(() => {
component.setState({}, {foo: 'bar'});
});
}).rejects.toThrowError(
'Invalid argument passed as callback. Expected a function. Instead ' +
'received: [object Object]',
);
assertConsoleErrorDev(
[
'Expected the last optional `callback` argument to be ' +
"a function. Instead received: { foo: 'bar' }.",
],
{withoutStack: true},
);
container = document.createElement('div');
root = ReactDOMClient.createRoot(container);
await act(() => {
@@ -1108,38 +1115,42 @@ describe('ReactUpdates', () => {
root.render(<A ref={current => (component = current)} />);
});
await expect(
expect(async () => {
await act(() => {
component.forceUpdate('no');
});
}).toErrorDev(
'Expected the last optional `callback` argument to be ' +
'a function. Instead received: no.',
),
).rejects.toThrowError(
await expect(async () => {
await act(() => {
component.forceUpdate('no');
});
}).rejects.toThrowError(
'Invalid argument passed as callback. Expected a function. Instead ' +
'received: no',
);
assertConsoleErrorDev(
[
'Expected the last optional `callback` argument to be ' +
'a function. Instead received: no.',
],
{withoutStack: true},
);
container = document.createElement('div');
root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<A ref={current => (component = current)} />);
});
await expect(
expect(async () => {
await act(() => {
component.forceUpdate({foo: 'bar'});
});
}).toErrorDev(
'Expected the last optional `callback` argument to be ' +
'a function. Instead received: [object Object].',
),
).rejects.toThrowError(
await expect(async () => {
await act(() => {
component.forceUpdate({foo: 'bar'});
});
}).rejects.toThrowError(
'Invalid argument passed as callback. Expected a function. Instead ' +
'received: [object Object]',
);
assertConsoleErrorDev(
[
'Expected the last optional `callback` argument to be ' +
"a function. Instead received: { foo: 'bar' }.",
],
{withoutStack: true},
);
// Make sure the warning is deduplicated and doesn't fire again
container = document.createElement('div');
root = ReactDOMClient.createRoot(container);
@@ -1351,11 +1362,14 @@ describe('ReactUpdates', () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await expect(async () => {
await act(() => {
root.render(<Foo />);
});
}).toErrorDev('Cannot update during an existing state transition');
await act(() => {
root.render(<Foo />);
});
assertConsoleErrorDev([
'Cannot update during an existing state transition (such as within `render`). ' +
'Render methods should be a pure function of props and state.\n' +
' in Foo (at **)',
]);
assertLog(['base: 0, memoized: 0', 'base: 1, memoized: 1']);
});
@@ -1798,12 +1812,14 @@ describe('ReactUpdates', () => {
const root = ReactDOMClient.createRoot(container);
await expect(async () => {
await expect(async () => {
await act(() => ReactDOM.flushSync(() => root.render(<App />)));
}).rejects.toThrow('Maximum update depth exceeded');
}).toErrorDev(
'Cannot update a component (`App`) while rendering a different component (`Child`)',
);
await act(() => ReactDOM.flushSync(() => root.render(<App />)));
}).rejects.toThrow('Maximum update depth exceeded');
assertConsoleErrorDev([
'Cannot update a component (`App`) while rendering a different component (`Child`). ' +
'To locate the bad setState() call inside `Child`, ' +
'follow the stack trace as described in https://react.dev/link/setstate-in-render\n' +
' in App (at **)',
]);
});
it("does not infinite loop if there's an async render phase update on another component", async () => {
@@ -1827,18 +1843,17 @@ describe('ReactUpdates', () => {
const root = ReactDOMClient.createRoot(container);
await expect(async () => {
let error;
try {
await act(() => {
React.startTransition(() => root.render(<App />));
});
} catch (e) {
error = e;
}
expect(error.message).toMatch('Maximum update depth exceeded');
}).toErrorDev(
'Cannot update a component (`App`) while rendering a different component (`Child`)',
);
await act(() => {
React.startTransition(() => root.render(<App />));
});
}).rejects.toThrow('Maximum update depth exceeded');
assertConsoleErrorDev([
'Cannot update a component (`App`) while rendering a different component (`Child`). ' +
'To locate the bad setState() call inside `Child`, ' +
'follow the stack trace as described in https://react.dev/link/setstate-in-render\n' +
' in App (at **)',
]);
});
// TODO: Replace this branch with @gate pragmas

View File

@@ -12,6 +12,8 @@
const React = require('react');
const ReactDOM = require('react-dom');
const ReactDOMClient = require('react-dom/client');
const assertConsoleErrorDev =
require('internal-test-utils').assertConsoleErrorDev;
function expectWarnings(tags, warnings = [], withoutStack = 0) {
tags = [...tags];
@@ -31,13 +33,13 @@ function expectWarnings(tags, warnings = [], withoutStack = 0) {
const root = ReactDOMClient.createRoot(container);
if (warnings.length) {
expect(() => {
ReactDOM.flushSync(() => {
root.render(element);
});
}).toErrorDev(warnings, {
withoutStack,
ReactDOM.flushSync(() => {
root.render(element);
});
assertConsoleErrorDev(
warnings,
withoutStack > 0 ? {withoutStack} : undefined,
);
}
}
@@ -164,7 +166,8 @@ describe('validateDOMNesting', () => {
' in body (at **)\n' +
' in foreignObject (at **)',
'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' +
' in body (at **)',
' in body (at **)\n' +
' in foreignObject (at **)',
],
);
});