/**
* 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 ReactDOM;
let ReactDOMServer;
let ReactTestUtils;
beforeEach(() => {
jest.resetModules();
React = require('react');
ReactDOM = require('react-dom');
ReactDOMServer = require('react-dom/server');
ReactTestUtils = require('react-dom/test-utils');
});
it('should flatten children to a string', () => {
const stub = (
<option>
{1} {'foo'}
</option>
);
const node = ReactTestUtils.renderIntoDocument(stub);
expect(node.innerHTML).toBe('1 foo');
});
it('should warn for invalid child tags', () => {
const el = (
<option value="12">
{1} <div /> {2}
</option>
);
let node;
expect(() => {
node = ReactTestUtils.renderIntoDocument(el);
}).toErrorDev(
'validateDOMNesting(...): <div> cannot appear as a child of <option>.\n' +
' in div (at **)\n' +
' in option (at **)',
);
expect(node.innerHTML).toBe('1 <div></div> 2');
ReactTestUtils.renderIntoDocument(el);
});
it('should warn for component child if no value prop is provided', () => {
function Foo() {
return '2';
}
const el = (
<option>
{1} <Foo /> {3}
</option>
);
let node;
expect(() => {
node = ReactTestUtils.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(node.innerHTML).toBe('1 2 3');
ReactTestUtils.renderIntoDocument(el);
});
it('should not warn for component child if value prop is provided', () => {
function Foo() {
return '2';
}
const el = (
<option value="123">
{1} <Foo /> {3}
</option>
);
const node = ReactTestUtils.renderIntoDocument(el);
expect(node.innerHTML).toBe('1 2 3');
ReactTestUtils.renderIntoDocument(el);
});
it('should ignore null/undefined/false children without warning', () => {
const stub = (
<option>
{1} {false}
{true}
{null}
{undefined} {2}
</option>
);
const node = ReactTestUtils.renderIntoDocument(stub);
expect(node.innerHTML).toBe('1 2');
});
it('should throw on object children', () => {
expect(() => {
ReactTestUtils.renderIntoDocument(<option>{{}}</option>);
}).toThrow('Objects are not valid as a React child');
expect(() => {
ReactTestUtils.renderIntoDocument(<option>{[{}]}</option>);
}).toThrow('Objects are not valid as a React child');
expect(() => {
ReactTestUtils.renderIntoDocument(
<option>
{{}}
<span />
</option>,
);
}).toThrow('Objects are not valid as a React child');
expect(() => {
ReactTestUtils.renderIntoDocument(
<option>
{'1'}
{{}}
{2}
</option>,
);
}).toThrow('Objects are not valid as a React child');
});
it('should support element-ish child', () => {
// 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 node = ReactTestUtils.renderIntoDocument(
<option value="a">{obj}</option>,
);
expect(node.innerHTML).toBe('hello');
node = ReactTestUtils.renderIntoDocument(
<option value="b">{[obj]}</option>,
);
expect(node.innerHTML).toBe('hello');
node = ReactTestUtils.renderIntoDocument(
<option value={obj}>{obj}</option>,
);
expect(node.innerHTML).toBe('hello');
expect(node.value).toBe('hello');
node = ReactTestUtils.renderIntoDocument(
<option value={obj}>
{'1'}
{obj}
{2}
</option>,
);
expect(node.innerHTML).toBe('1hello2');
expect(node.value).toBe('hello');
});
it('should be able to use dangerouslySetInnerHTML on option', () => {
const stub = <option dangerouslySetInnerHTML={{__html: 'foobar'}} />;
let node;
expect(() => {
node = ReactTestUtils.renderIntoDocument(stub);
}).toErrorDev(
'Pass a `value` prop if you set dangerouslyInnerHTML so React knows which value should be selected.\n' +
' in option (at **)',
);
expect(node.innerHTML).toBe('foobar');
});
it('should set attribute for empty value', () => {
const container = document.createElement('div');
const option = ReactDOM.render(<option value="" />, container);
expect(option.hasAttribute('value')).toBe(true);
expect(option.getAttribute('value')).toBe('');
ReactDOM.render(<option value="lava" />, container);
expect(option.hasAttribute('value')).toBe(true);
expect(option.getAttribute('value')).toBe('lava');
});
it('should allow ignoring `value` on option', () => {
const a = 'a';
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 node = ReactDOM.render(stub, container);
expect(node.selectedIndex).toBe(1);
ReactDOM.render(<select value="gorilla">{options}</select>, container);
expect(node.selectedIndex).toEqual(2);
});
it('generates a warning and hydration error when an invalid nested tag is used as a child', () => {
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);
const option = container.firstChild.firstChild;
expect(option.nodeName).toBe('OPTION');
expect(option.textContent).toBe('BarFooBaz');
expect(option.selected).toBe(true);
expect(() => ReactDOM.hydrate(children, container)).toErrorDev([
'Text content did not match. Server: "FooBaz" Client: "Foo"',
'validateDOMNesting(...): <div> cannot appear as a child of <option>.',
]);
expect(option.textContent).toBe('BarFooBaz');
expect(option.selected).toBe(true);
expect(ref.current.nodeName).toBe('DIV');
expect(ref.current.parentNode).toBe(option);
});
});