mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Currently we're printing parent stacks at the end of DOM nesting even with owner stacks enabled. That's because the context of parent tree is relevant for determining why two things are nested. It might not be sufficient to see the owner stack alone. I'm trying to get rid of parent stacks and rely on more of the plain owner stacks or ideally console.createTask. These are generally better anyway since the exact line for creating the JSX is available. It also lets you find a parent stack frame that is most relevant e.g. if it's hidden inside internals. For DOM nesting there's really only two stacks that are relevant. The creation of the parent and the creation of the child. Sometimes they're close enough to be the same thing. Such as for parents that can't have text children or when the ancestor is the direct parent created at the same place (same owner). Sometimes they're far apart. In this case I add a second console.error within the context of the ancestor. That way the second stack trace can be used to read the stack trace for where it was created. To preserve some parent context I now print the parent stack in a diff view format using the logic from hydration diffs. This includes some siblings and props for context. <img width="756" alt="Screenshot 2024-07-10 at 12 21 38 AM" src="https://github.com/facebook/react/assets/63648/0843133d-cc7a-4ecc-91c0-f46ae8e99f20"> Text Nodes: <img width="749" alt="Screenshot 2024-07-10 at 12 37 40 AM" src="https://github.com/facebook/react/assets/63648/ee377d82-54ee-450a-99d1-fcc3ef290d59"> --------- Co-authored-by: tjallingt <tjallingt@gmail.com>
299 lines
8.6 KiB
JavaScript
299 lines
8.6 KiB
JavaScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @emails react-core
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
describe('ReactDOMOption', () => {
|
|
let React;
|
|
let ReactDOMClient;
|
|
let ReactDOMServer;
|
|
let act;
|
|
|
|
beforeEach(() => {
|
|
jest.resetModules();
|
|
React = require('react');
|
|
ReactDOMClient = require('react-dom/client');
|
|
ReactDOMServer = require('react-dom/server');
|
|
act = require('internal-test-utils').act;
|
|
});
|
|
|
|
async function renderIntoDocument(children) {
|
|
const container = document.createElement('div');
|
|
const root = ReactDOMClient.createRoot(container);
|
|
await act(async () => root.render(children));
|
|
return container;
|
|
}
|
|
|
|
it('should flatten children to a string', async () => {
|
|
const stub = (
|
|
<option>
|
|
{1} {'foo'}
|
|
</option>
|
|
);
|
|
const container = await renderIntoDocument(stub);
|
|
|
|
expect(container.firstChild.innerHTML).toBe('1 foo');
|
|
});
|
|
|
|
it('should warn for invalid child tags', async () => {
|
|
const el = (
|
|
<option value="12">
|
|
{1} <div /> {2}
|
|
</option>
|
|
);
|
|
let container;
|
|
await expect(async () => {
|
|
container = await renderIntoDocument(el);
|
|
}).toErrorDev(
|
|
'In HTML, <div> cannot be a child of <option>.\n' +
|
|
'This will cause a hydration error.\n' +
|
|
'\n' +
|
|
'> <option value="12">\n' +
|
|
'> <div>\n' +
|
|
' ...\n' +
|
|
'\n' +
|
|
' in div (at **)' +
|
|
(gate(flags => flags.enableOwnerStacks)
|
|
? ''
|
|
: '\n in option (at **)'),
|
|
);
|
|
expect(container.firstChild.innerHTML).toBe('1 <div></div> 2');
|
|
await renderIntoDocument(el);
|
|
});
|
|
|
|
it('should warn for component child if no value prop is provided', async () => {
|
|
function Foo() {
|
|
return '2';
|
|
}
|
|
const el = (
|
|
<option>
|
|
{1} <Foo /> {3}
|
|
</option>
|
|
);
|
|
let container;
|
|
await expect(async () => {
|
|
container = await renderIntoDocument(el);
|
|
}).toErrorDev(
|
|
'Cannot infer the option value of complex children. ' +
|
|
'Pass a `value` prop or use a plain string as children to <option>.',
|
|
);
|
|
expect(container.firstChild.innerHTML).toBe('1 2 3');
|
|
await renderIntoDocument(el);
|
|
});
|
|
|
|
it('should not warn for component child if value prop is provided', async () => {
|
|
function Foo() {
|
|
return '2';
|
|
}
|
|
const el = (
|
|
<option value="123">
|
|
{1} <Foo /> {3}
|
|
</option>
|
|
);
|
|
const container = await renderIntoDocument(el);
|
|
expect(container.firstChild.innerHTML).toBe('1 2 3');
|
|
await renderIntoDocument(el);
|
|
});
|
|
|
|
it('should ignore null/undefined/false children without warning', async () => {
|
|
const stub = (
|
|
<option>
|
|
{1} {false}
|
|
{true}
|
|
{null}
|
|
{undefined} {2}
|
|
</option>
|
|
);
|
|
const container = await renderIntoDocument(stub);
|
|
|
|
expect(container.firstChild.innerHTML).toBe('1 2');
|
|
});
|
|
|
|
it('should throw on object children', async () => {
|
|
await expect(async () =>
|
|
renderIntoDocument(<option>{{}}</option>),
|
|
).rejects.toThrow('Objects are not valid as a React child');
|
|
await expect(async () => {
|
|
await renderIntoDocument(<option>{[{}]}</option>);
|
|
}).rejects.toThrow('Objects are not valid as a React child');
|
|
await expect(async () => {
|
|
await renderIntoDocument(
|
|
<option>
|
|
{{}}
|
|
<span />
|
|
</option>,
|
|
);
|
|
}).rejects.toThrow('Objects are not valid as a React child');
|
|
await expect(async () => {
|
|
await renderIntoDocument(
|
|
<option>
|
|
{'1'}
|
|
{{}}
|
|
{2}
|
|
</option>,
|
|
);
|
|
}).rejects.toThrow('Objects are not valid as a React child');
|
|
});
|
|
|
|
// @gate www && !renameElementSymbol
|
|
it('should support element-ish child', async () => {
|
|
// This is similar to <fbt>.
|
|
// We don't toString it because you must instead provide a value prop.
|
|
const obj = {
|
|
$$typeof: Symbol.for('react.element'),
|
|
type: props => props.content,
|
|
ref: null,
|
|
key: null,
|
|
props: {
|
|
content: 'hello',
|
|
},
|
|
toString() {
|
|
return this.props.content;
|
|
},
|
|
};
|
|
|
|
let container = await renderIntoDocument(<option value="a">{obj}</option>);
|
|
expect(container.firstChild.innerHTML).toBe('hello');
|
|
|
|
container = await renderIntoDocument(<option value="b">{[obj]}</option>);
|
|
expect(container.firstChild.innerHTML).toBe('hello');
|
|
|
|
container = await renderIntoDocument(<option value={obj}>{obj}</option>);
|
|
expect(container.firstChild.innerHTML).toBe('hello');
|
|
expect(container.firstChild.value).toBe('hello');
|
|
|
|
container = await renderIntoDocument(
|
|
<option value={obj}>
|
|
{'1'}
|
|
{obj}
|
|
{2}
|
|
</option>,
|
|
);
|
|
expect(container.firstChild.innerHTML).toBe('1hello2');
|
|
expect(container.firstChild.value).toBe('hello');
|
|
});
|
|
|
|
it('should support bigint values', async () => {
|
|
const container = await renderIntoDocument(<option>{5n}</option>);
|
|
expect(container.firstChild.innerHTML).toBe('5');
|
|
expect(container.firstChild.value).toBe('5');
|
|
});
|
|
|
|
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(
|
|
'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');
|
|
});
|
|
|
|
it('should set attribute for empty value', async () => {
|
|
const container = document.createElement('div');
|
|
const root = ReactDOMClient.createRoot(container);
|
|
let option;
|
|
await act(() => {
|
|
root.render(<option value="" />);
|
|
});
|
|
option = container.firstChild;
|
|
expect(option.hasAttribute('value')).toBe(true);
|
|
expect(option.getAttribute('value')).toBe('');
|
|
|
|
await act(() => {
|
|
root.render(<option value="lava" />);
|
|
});
|
|
option = container.firstChild;
|
|
expect(option.hasAttribute('value')).toBe(true);
|
|
expect(option.getAttribute('value')).toBe('lava');
|
|
});
|
|
|
|
it('should allow ignoring `value` on option', async () => {
|
|
const a = 'a';
|
|
let node;
|
|
const stub = (
|
|
<select value="giraffe" onChange={() => {}}>
|
|
<option>monkey</option>
|
|
<option>gir{a}ffe</option>
|
|
<option>gorill{a}</option>
|
|
</select>
|
|
);
|
|
const options = stub.props.children;
|
|
const container = document.createElement('div');
|
|
const root = ReactDOMClient.createRoot(container);
|
|
await act(() => {
|
|
root.render(stub);
|
|
});
|
|
node = container.firstChild;
|
|
|
|
expect(node.selectedIndex).toBe(1);
|
|
|
|
await act(() => {
|
|
root.render(<select value="gorilla">{options}</select>);
|
|
});
|
|
node = container.firstChild;
|
|
expect(node.selectedIndex).toEqual(2);
|
|
});
|
|
|
|
it('generates a hydration error when an invalid nested tag is used as a child', async () => {
|
|
const ref = React.createRef();
|
|
const children = (
|
|
<select readOnly={true} value="bar">
|
|
<option value="bar">
|
|
{['Bar', false, 'Foo', <div key="1" ref={ref} />, 'Baz']}
|
|
</option>
|
|
</select>
|
|
);
|
|
|
|
const container = document.createElement('div');
|
|
|
|
container.innerHTML = ReactDOMServer.renderToString(children);
|
|
|
|
expect(container.firstChild.getAttribute('value')).toBe(null);
|
|
expect(container.firstChild.getAttribute('defaultValue')).toBe(null);
|
|
|
|
let option = container.firstChild.firstChild;
|
|
expect(option.nodeName).toBe('OPTION');
|
|
|
|
expect(option.textContent).toBe('BarFooBaz');
|
|
expect(option.selected).toBe(true);
|
|
|
|
await expect(async () => {
|
|
await act(async () => {
|
|
ReactDOMClient.hydrateRoot(container, children, {
|
|
onRecoverableError: () => {},
|
|
});
|
|
});
|
|
}).toErrorDev(
|
|
'In HTML, <div> cannot be a child of <option>.\n' +
|
|
'This will cause a hydration error.\n' +
|
|
'\n' +
|
|
' <select readOnly={true} value="bar">\n' +
|
|
'> <option value="bar">\n' +
|
|
'> <div ref={{current:null}}>\n' +
|
|
' ...\n' +
|
|
'\n' +
|
|
' in div (at **)' +
|
|
(gate(flags => flags.enableOwnerStacks)
|
|
? ''
|
|
: '\n in option (at **)'),
|
|
);
|
|
option = container.firstChild.firstChild;
|
|
|
|
expect(option.textContent).toBe('BarFooBaz');
|
|
expect(option.selected).toBe(true);
|
|
|
|
expect(ref.current.nodeName).toBe('DIV');
|
|
expect(ref.current.parentNode).toBe(option);
|
|
});
|
|
});
|