main
js 253 lines 7.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 const React = require('react');
13 const ReactDOMClient = require('react-dom/client');
14 const act = require('internal-test-utils').act;
15
16 // Helpers
17 const testAllPermutations = async function (testCases) {
18 for (let i = 0; i < testCases.length; i += 2) {
19 const renderWithChildren = testCases[i];
20 const expectedResultAfterRender = testCases[i + 1];
21
22 for (let j = 0; j < testCases.length; j += 2) {
23 const updateWithChildren = testCases[j];
24 const expectedResultAfterUpdate = testCases[j + 1];
25
26 const container = document.createElement('div');
27 const root = ReactDOMClient.createRoot(container);
28 await act(() => root.render(<div>{renderWithChildren}</div>));
29 expectChildren(container, expectedResultAfterRender);
30
31 await act(() => root.render(<div>{updateWithChildren}</div>));
32 expectChildren(container, expectedResultAfterUpdate);
33 }
34 }
35 };
36
37 const expectChildren = function (container, children) {
38 const outerNode = container.firstChild;
39 let textNode;
40 if (typeof children === 'string') {
41 textNode = outerNode.firstChild;
42
43 if (children === '') {
44 expect(textNode != null).toBe(false);
45 } else {
46 expect(textNode != null).toBe(true);
47 expect(textNode.nodeType).toBe(3);
48 expect(textNode.data).toBe(String(children));
49 }
50 } else {
51 let mountIndex = 0;
52
53 for (let i = 0; i < children.length; i++) {
54 const child = children[i];
55
56 if (typeof child === 'string') {
57 if (child === '') {
58 continue;
59 }
60 textNode = outerNode.childNodes[mountIndex];
61 expect(textNode != null).toBe(true);
62 expect(textNode.nodeType).toBe(3);
63 expect(textNode.data).toBe(child);
64 mountIndex++;
65 } else {
66 const elementDOMNode = outerNode.childNodes[mountIndex];
67 expect(elementDOMNode.tagName).toBe('DIV');
68 mountIndex++;
69 }
70 }
71 }
72 };
73
74 /**
75 * ReactMultiChild DOM integration test. In ReactDOM components, we make sure
76 * that single children that are strings are treated as "content" which is much
77 * faster to render and update.
78 */
79 describe('ReactMultiChildText', () => {
80 jest.setTimeout(30000);
81
82 it('should correctly handle all possible children for render and update', async () => {
83 spyOnDev(console, 'error').mockImplementation(() => {});
84 // prettier-ignore
85 await testAllPermutations([
86 // basic values
87 undefined, [],
88 null, [],
89 false, [],
90 true, [],
91 0, '0',
92 1.2, '1.2',
93 '', [],
94 'foo', 'foo',
95
96 [], [],
97 [undefined], [],
98 [null], [],
99 [false], [],
100 [true], [],
101 [0], ['0'],
102 [1.2], ['1.2'],
103 [''], [],
104 ['foo'], ['foo'],
105 [<div />], [<div />],
106
107 // two adjacent values
108 [true, 0], ['0'],
109 [0, 0], ['0', '0'],
110 [1.2, 0], ['1.2', '0'],
111 [0, ''], ['0', ''],
112 ['foo', 0], ['foo', '0'],
113 [0, <div />], ['0', <div />],
114
115 [true, 1.2], ['1.2'],
116 [1.2, 0], ['1.2', '0'],
117 [1.2, 1.2], ['1.2', '1.2'],
118 [1.2, ''], ['1.2', ''],
119 ['foo', 1.2], ['foo', '1.2'],
120 [1.2, <div />], ['1.2', <div />],
121
122 [true, ''], [''],
123 ['', 0], ['', '0'],
124 [1.2, ''], ['1.2', ''],
125 ['', ''], ['', ''],
126 ['foo', ''], ['foo', ''],
127 ['', <div />], ['', <div />],
128
129 [true, 'foo'], ['foo'],
130 ['foo', 0], ['foo', '0'],
131 [1.2, 'foo'], ['1.2', 'foo'],
132 ['foo', ''], ['foo', ''],
133 ['foo', 'foo'], ['foo', 'foo'],
134 ['foo', <div />], ['foo', <div />],
135
136 // values separated by an element
137 [true, <div />, true], [<div />],
138 [1.2, <div />, 1.2], ['1.2', <div />, '1.2'],
139 ['', <div />, ''], ['', <div />, ''],
140 ['foo', <div />, 'foo'], ['foo', <div />, 'foo'],
141
142 [true, 1.2, <div />, '', 'foo'], ['1.2', <div />, '', 'foo'],
143 [1.2, '', <div />, 'foo', true], ['1.2', '', <div />, 'foo'],
144 ['', 'foo', <div />, true, 1.2], ['', 'foo', <div />, '1.2'],
145
146 [true, 1.2, '', <div />, 'foo', true, 1.2], ['1.2', '', <div />, 'foo', '1.2'],
147 ['', 'foo', true, <div />, 1.2, '', 'foo'], ['', 'foo', <div />, '1.2', '', 'foo'],
148
149 // values inside arrays
150 [[true], [true]], [],
151 [[1.2], [1.2]], ['1.2', '1.2'],
152 [[''], ['']], ['', ''],
153 [['foo'], ['foo']], ['foo', 'foo'],
154 [[<div />], [<div />]], [<div />, <div />],
155
156 [[true, 1.2, <div />], '', 'foo'], ['1.2', <div />, '', 'foo'],
157 [1.2, '', [<div />, 'foo', true]], ['1.2', '', <div />, 'foo'],
158 ['', ['foo', <div />, true], 1.2], ['', 'foo', <div />, '1.2'],
159
160 [true, [1.2, '', <div />, 'foo'], true, 1.2], ['1.2', '', <div />, 'foo', '1.2'],
161 ['', 'foo', [true, <div />, 1.2, ''], 'foo'], ['', 'foo', <div />, '1.2', '', 'foo'],
162
163 // values inside elements
164 [<div>{true}{1.2}{<div />}</div>, '', 'foo'], [<div />, '', 'foo'],
165 [1.2, '', <div>{<div />}{'foo'}{true}</div>], ['1.2', '', <div />],
166 ['', <div>{'foo'}{<div />}{true}</div>, 1.2], ['', <div />, '1.2'],
167
168 [true, <div>{1.2}{''}{<div />}{'foo'}</div>, true, 1.2], [<div />, '1.2'],
169 ['', 'foo', <div>{true}{<div />}{1.2}{''}</div>, 'foo'], ['', 'foo', <div />, 'foo'],
170 ]);
171 if (__DEV__) {
172 expect(console.error).toHaveBeenCalledTimes(2);
173 expect(console.error.mock.calls[0][0]).toMatch(
174 'Each child in a list should have a unique "key" prop.',
175 );
176 expect(console.error.mock.calls[1][0]).toMatch(
177 'Each child in a list should have a unique "key" prop.',
178 );
179 }
180 });
181
182 it('should correctly handle bigint children for render and update', async () => {
183 // prettier-ignore
184 await testAllPermutations([
185 10n, '10',
186 [10n], ['10']
187 ]);
188 });
189
190 it('should throw if rendering both HTML and children', async () => {
191 const container = document.createElement('div');
192 const root = ReactDOMClient.createRoot(container);
193 await expect(
194 act(() => {
195 root.render(
196 <div dangerouslySetInnerHTML={{__html: 'abcdef'}}>ghjkl</div>,
197 );
198 }),
199 ).rejects.toThrow();
200 });
201
202 it('should render between nested components and inline children', async () => {
203 let container = document.createElement('div');
204 let root = ReactDOMClient.createRoot(container);
205
206 await act(() => {
207 root.render(
208 <div>
209 <h1>
210 <span />
211 <span />
212 </h1>
213 </div>,
214 );
215 });
216
217 container = document.createElement('div');
218 root = ReactDOMClient.createRoot(container);
219 await expect(
220 act(() => {
221 root.render(
222 <div>
223 <h1>A</h1>
224 </div>,
225 );
226 }),
227 ).resolves.not.toThrow();
228
229 container = document.createElement('div');
230 root = ReactDOMClient.createRoot(container);
231 await expect(
232 act(() => {
233 root.render(
234 <div>
235 <h1>{['A']}</h1>
236 </div>,
237 );
238 }),
239 ).resolves.not.toThrow();
240
241 container = document.createElement('div');
242 root = ReactDOMClient.createRoot(container);
243 await expect(
244 act(() => {
245 root.render(
246 <div>
247 <h1>{['A', 'B']}</h1>
248 </div>,
249 );
250 }),
251 ).resolves.not.toThrow();
252 });
253 });