main
js 288 lines 8.35 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @emails react-core
8 */
9
10 'use strict';
11
12 describe('ReactDOMOption', () => {
13 let React;
14 let ReactDOMClient;
15 let ReactDOMServer;
16 let act;
17 let assertConsoleErrorDev;
18
19 beforeEach(() => {
20 jest.resetModules();
21 React = require('react');
22 ReactDOMClient = require('react-dom/client');
23 ReactDOMServer = require('react-dom/server');
24 act = require('internal-test-utils').act;
25 assertConsoleErrorDev =
26 require('internal-test-utils').assertConsoleErrorDev;
27 });
28
29 async function renderIntoDocument(children) {
30 const container = document.createElement('div');
31 const root = ReactDOMClient.createRoot(container);
32 await act(async () => root.render(children));
33 return container;
34 }
35
36 it('should flatten children to a string', async () => {
37 const stub = (
38 <option>
39 {1} {'foo'}
40 </option>
41 );
42 const container = await renderIntoDocument(stub);
43
44 expect(container.firstChild.innerHTML).toBe('1 foo');
45 });
46
47 it('should warn for invalid child tags', async () => {
48 const el = (
49 <option value="12">
50 {1} <div /> {2}
51 </option>
52 );
53 const container = await renderIntoDocument(el);
54 assertConsoleErrorDev([
55 'In HTML, <div> cannot be a child of <option>.\n' +
56 'This will cause a hydration error.\n' +
57 '\n' +
58 '> <option value="12">\n' +
59 '> <div>\n' +
60 ' ...\n' +
61 '\n' +
62 ' in div (at **)',
63 ]);
64 expect(container.firstChild.innerHTML).toBe('1 <div></div> 2');
65 await renderIntoDocument(el);
66 });
67
68 it('should warn for component child if no value prop is provided', async () => {
69 function Foo() {
70 return '2';
71 }
72 const el = (
73 <option>
74 {1} <Foo /> {3}
75 </option>
76 );
77 const container = await renderIntoDocument(el);
78 assertConsoleErrorDev([
79 'Cannot infer the option value of complex children. ' +
80 'Pass a `value` prop or use a plain string as children to <option>.\n' +
81 ' in option (at **)',
82 ]);
83 expect(container.firstChild.innerHTML).toBe('1 2 3');
84 await renderIntoDocument(el);
85 });
86
87 it('should not warn for component child if value prop is provided', async () => {
88 function Foo() {
89 return '2';
90 }
91 const el = (
92 <option value="123">
93 {1} <Foo /> {3}
94 </option>
95 );
96 const container = await renderIntoDocument(el);
97 expect(container.firstChild.innerHTML).toBe('1 2 3');
98 await renderIntoDocument(el);
99 });
100
101 it('should ignore null/undefined/false children without warning', async () => {
102 const stub = (
103 <option>
104 {1} {false}
105 {true}
106 {null}
107 {undefined} {2}
108 </option>
109 );
110 const container = await renderIntoDocument(stub);
111
112 expect(container.firstChild.innerHTML).toBe('1 2');
113 });
114
115 it('should throw on object children', async () => {
116 await expect(async () =>
117 renderIntoDocument(<option>{{}}</option>),
118 ).rejects.toThrow('Objects are not valid as a React child');
119 await expect(async () => {
120 await renderIntoDocument(<option>{[{}]}</option>);
121 }).rejects.toThrow('Objects are not valid as a React child');
122 await expect(async () => {
123 await renderIntoDocument(
124 <option>
125 {{}}
126 <span />
127 </option>,
128 );
129 }).rejects.toThrow('Objects are not valid as a React child');
130 await expect(async () => {
131 await renderIntoDocument(
132 <option>
133 {'1'}
134 {{}}
135 {2}
136 </option>,
137 );
138 }).rejects.toThrow('Objects are not valid as a React child');
139 });
140
141 it('should support element-ish child', async () => {
142 // This is similar to <fbt>.
143 // We don't toString it because you must instead provide a value prop.
144 const obj = {
145 $$typeof: Symbol.for('react.transitional.element'),
146 type: props => props.content,
147 ref: null,
148 key: null,
149 props: {
150 content: 'hello',
151 },
152 toString() {
153 return this.props.content;
154 },
155 };
156
157 let container = await renderIntoDocument(<option value="a">{obj}</option>);
158 expect(container.firstChild.innerHTML).toBe('hello');
159
160 container = await renderIntoDocument(<option value="b">{[obj]}</option>);
161 expect(container.firstChild.innerHTML).toBe('hello');
162
163 container = await renderIntoDocument(<option value={obj}>{obj}</option>);
164 expect(container.firstChild.innerHTML).toBe('hello');
165 expect(container.firstChild.value).toBe('hello');
166
167 container = await renderIntoDocument(
168 <option value={obj}>
169 {'1'}
170 {obj}
171 {2}
172 </option>,
173 );
174 expect(container.firstChild.innerHTML).toBe('1hello2');
175 expect(container.firstChild.value).toBe('hello');
176 });
177
178 it('should support bigint values', async () => {
179 const container = await renderIntoDocument(<option>{5n}</option>);
180 expect(container.firstChild.innerHTML).toBe('5');
181 expect(container.firstChild.value).toBe('5');
182 });
183
184 it('should be able to use dangerouslySetInnerHTML on option', async () => {
185 const stub = <option dangerouslySetInnerHTML={{__html: 'foobar'}} />;
186 const container = await renderIntoDocument(stub);
187 assertConsoleErrorDev([
188 'Pass a `value` prop if you set dangerouslyInnerHTML so React knows which value should be selected.\n' +
189 ' in option (at **)',
190 ]);
191
192 expect(container.firstChild.innerHTML).toBe('foobar');
193 });
194
195 it('should set attribute for empty value', async () => {
196 const container = document.createElement('div');
197 const root = ReactDOMClient.createRoot(container);
198 let option;
199 await act(() => {
200 root.render(<option value="" />);
201 });
202 option = container.firstChild;
203 expect(option.hasAttribute('value')).toBe(true);
204 expect(option.getAttribute('value')).toBe('');
205
206 await act(() => {
207 root.render(<option value="lava" />);
208 });
209 option = container.firstChild;
210 expect(option.hasAttribute('value')).toBe(true);
211 expect(option.getAttribute('value')).toBe('lava');
212 });
213
214 it('should allow ignoring `value` on option', async () => {
215 const a = 'a';
216 let node;
217 const stub = (
218 <select value="giraffe" onChange={() => {}}>
219 <option>monkey</option>
220 <option>gir{a}ffe</option>
221 <option>gorill{a}</option>
222 </select>
223 );
224 const options = stub.props.children;
225 const container = document.createElement('div');
226 const root = ReactDOMClient.createRoot(container);
227 await act(() => {
228 root.render(stub);
229 });
230 node = container.firstChild;
231
232 expect(node.selectedIndex).toBe(1);
233
234 await act(() => {
235 root.render(<select value="gorilla">{options}</select>);
236 });
237 node = container.firstChild;
238 expect(node.selectedIndex).toEqual(2);
239 });
240
241 it('generates a hydration error when an invalid nested tag is used as a child', async () => {
242 const ref = React.createRef();
243 const children = (
244 <select readOnly={true} value="bar">
245 <option value="bar">
246 {['Bar', false, 'Foo', <div key="1" ref={ref} />, 'Baz']}
247 </option>
248 </select>
249 );
250
251 const container = document.createElement('div');
252
253 container.innerHTML = ReactDOMServer.renderToString(children);
254
255 expect(container.firstChild.getAttribute('value')).toBe(null);
256 expect(container.firstChild.getAttribute('defaultValue')).toBe(null);
257
258 let option = container.firstChild.firstChild;
259 expect(option.nodeName).toBe('OPTION');
260
261 expect(option.textContent).toBe('BarFooBaz');
262 expect(option.selected).toBe(true);
263
264 await act(async () => {
265 ReactDOMClient.hydrateRoot(container, children, {
266 onRecoverableError: () => {},
267 });
268 });
269 assertConsoleErrorDev([
270 'In HTML, <div> cannot be a child of <option>.\n' +
271 'This will cause a hydration error.\n' +
272 '\n' +
273 ' <select readOnly={true} value="bar">\n' +
274 '> <option value="bar">\n' +
275 '> <div ref={{current:null}}>\n' +
276 ' ...\n' +
277 '\n' +
278 ' in div (at **)',
279 ]);
280 option = container.firstChild.firstChild;
281
282 expect(option.textContent).toBe('BarFooBaz');
283 expect(option.selected).toBe(true);
284
285 expect(ref.current.nodeName).toBe('DIV');
286 expect(ref.current.parentNode).toBe(option);
287 });
288 });