main
js 359 lines 8.3 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 let React;
13 let ReactDOMClient;
14 let ReactDOMServer;
15 let act;
16
17 describe('ReactDOMTextComponent', () => {
18 beforeEach(() => {
19 React = require('react');
20 ReactDOMClient = require('react-dom/client');
21 ReactDOMServer = require('react-dom/server');
22 act = require('internal-test-utils').act;
23 });
24
25 it('updates a mounted text component in place', async () => {
26 const container = document.createElement('div');
27 const root = ReactDOMClient.createRoot(container);
28 await act(() => {
29 root.render(
30 <div>
31 <span />
32 {'foo'}
33 {'bar'}
34 </div>,
35 );
36 });
37 let inst = container.firstChild;
38 let nodes = inst.childNodes;
39
40 const foo = nodes[1];
41 const bar = nodes[2];
42 expect(foo.data).toBe('foo');
43 expect(bar.data).toBe('bar');
44
45 await act(() => {
46 root.render(
47 <div>
48 <span />
49 {'baz'}
50 {'qux'}
51 </div>,
52 );
53 });
54 inst = container.firstChild;
55 // After the update, the text nodes should have stayed in place (as opposed
56 // to getting unmounted and remounted)
57 nodes = inst.childNodes;
58 expect(nodes[1]).toBe(foo);
59 expect(nodes[2]).toBe(bar);
60 expect(foo.data).toBe('baz');
61 expect(bar.data).toBe('qux');
62 });
63
64 it('can be toggled in and out of the markup', async () => {
65 const container = document.createElement('div');
66 const root = ReactDOMClient.createRoot(container);
67 await act(() => {
68 root.render(
69 <div>
70 {'foo'}
71 <div />
72 {'bar'}
73 </div>,
74 );
75 });
76 let inst = container.firstChild;
77
78 let childNodes = inst.childNodes;
79 const childDiv = childNodes[1];
80
81 await act(() => {
82 root.render(
83 <div>
84 {null}
85 <div />
86 {null}
87 </div>,
88 );
89 });
90 inst = container.firstChild;
91 childNodes = inst.childNodes;
92 expect(childNodes.length).toBe(1);
93 expect(childNodes[0]).toBe(childDiv);
94
95 await act(() => {
96 root.render(
97 <div>
98 {'foo'}
99 <div />
100 {'bar'}
101 </div>,
102 );
103 });
104 inst = container.firstChild;
105 childNodes = inst.childNodes;
106 expect(childNodes.length).toBe(3);
107 expect(childNodes[0].data).toBe('foo');
108 expect(childNodes[1]).toBe(childDiv);
109 expect(childNodes[2].data).toBe('bar');
110 });
111
112 /**
113 * The following Node.normalize() tests are intentionally failing.
114 * See https://github.com/facebook/react/issues/9836 tracking whether we'll need to fix this or if it's unnecessary.
115 */
116 // @gate TODO
117 it('can reconcile text merged by Node.normalize() alongside other elements', async () => {
118 const container = document.createElement('div');
119 const root = ReactDOMClient.createRoot(container);
120 await act(() => {
121 root.render(
122 <div>
123 {'foo'}
124 {'bar'}
125 {'baz'}
126 <span />
127 {'qux'}
128 </div>,
129 );
130 });
131
132 const inst = container.firstChild;
133
134 inst.normalize();
135
136 await act(() => {
137 root.render(
138 <div>
139 {'bar'}
140 {'baz'}
141 {'qux'}
142 <span />
143 {'foo'}
144 </div>,
145 container,
146 );
147 });
148 expect(inst.textContent).toBe('barbazquxfoo');
149 });
150
151 // @gate TODO
152 it('can reconcile text merged by Node.normalize()', async () => {
153 const container = document.createElement('div');
154 const root = ReactDOMClient.createRoot(container);
155 await act(() => {
156 root.render(
157 <div>
158 {'foo'}
159 {'bar'}
160 {'baz'}
161 </div>,
162 );
163 });
164 let inst = container.firstChild;
165
166 inst.normalize();
167
168 await act(() => {
169 root.render(
170 <div>
171 {'bar'}
172 {'baz'}
173 {'qux'}
174 </div>,
175 container,
176 );
177 });
178 inst = container.firstChild;
179 expect(inst.textContent).toBe('barbazqux');
180 });
181
182 it('can reconcile text from pre-rendered markup', async () => {
183 const container = document.createElement('div');
184 let children = (
185 <div>
186 {'foo'}
187 {'bar'}
188 {'baz'}
189 </div>
190 );
191 container.innerHTML = ReactDOMServer.renderToString(children);
192
193 const root = await act(() => {
194 return ReactDOMClient.hydrateRoot(container, children);
195 });
196 expect(container.textContent).toBe('foobarbaz');
197
198 await act(() => {
199 root.unmount();
200 });
201
202 children = (
203 <div>
204 {''}
205 {''}
206 {''}
207 </div>
208 );
209 container.innerHTML = ReactDOMServer.renderToString(children);
210
211 await act(() => {
212 ReactDOMClient.hydrateRoot(container, children);
213 });
214 expect(container.textContent).toBe('');
215 });
216
217 // @gate TODO
218 it('can reconcile text arbitrarily split into multiple nodes', async () => {
219 const container = document.createElement('div');
220 const root = ReactDOMClient.createRoot(container);
221
222 await act(() => {
223 root.render(
224 <div>
225 <span />
226 {'foobarbaz'}
227 </div>,
228 );
229 });
230 let inst = container.firstChild;
231
232 const childNodes = inst.childNodes;
233 const textNode = childNodes[1];
234 textNode.textContent = 'foo';
235 inst.insertBefore(
236 document.createTextNode('bar'),
237 childNodes[1].nextSibling,
238 );
239 inst.insertBefore(
240 document.createTextNode('baz'),
241 childNodes[1].nextSibling,
242 );
243
244 await act(() => {
245 root.render(
246 <div>
247 <span />
248 {'barbazqux'}
249 </div>,
250 container,
251 );
252 });
253 inst = container.firstChild;
254 expect(inst.textContent).toBe('barbazqux');
255 });
256
257 // @gate TODO
258 it('can reconcile text arbitrarily split into multiple nodes on some substitutions only', async () => {
259 const container = document.createElement('div');
260 const root = ReactDOMClient.createRoot(container);
261 await act(() => {
262 root.render(
263 <div>
264 <span />
265 {'bar'}
266 <span />
267 {'foobarbaz'}
268 {'foo'}
269 {'barfoo'}
270 <span />
271 </div>,
272 );
273 });
274
275 let inst = container.firstChild;
276
277 const childNodes = inst.childNodes;
278 const textNode = childNodes[3];
279 textNode.textContent = 'foo';
280 inst.insertBefore(
281 document.createTextNode('bar'),
282 childNodes[3].nextSibling,
283 );
284 inst.insertBefore(
285 document.createTextNode('baz'),
286 childNodes[3].nextSibling,
287 );
288 const secondTextNode = childNodes[5];
289 secondTextNode.textContent = 'bar';
290 inst.insertBefore(
291 document.createTextNode('foo'),
292 childNodes[5].nextSibling,
293 );
294
295 await act(() => {
296 root.render(
297 <div>
298 <span />
299 {'baz'}
300 <span />
301 {'barbazqux'}
302 {'bar'}
303 {'bazbar'}
304 <span />
305 </div>,
306 container,
307 );
308 });
309 inst = container.firstChild;
310 expect(inst.textContent).toBe('bazbarbazquxbarbazbar');
311 });
312
313 // @gate TODO
314 it('can unmount normalized text nodes', async () => {
315 const container = document.createElement('div');
316 const root = ReactDOMClient.createRoot(container);
317 await act(() => {
318 root.render(
319 <div>
320 {''}
321 {'foo'}
322 {'bar'}
323 </div>,
324 );
325 });
326
327 container.normalize();
328 await act(() => {
329 root.render(<div />);
330 });
331
332 expect(container.innerHTML).toBe('<div></div>');
333 });
334
335 it('throws for Temporal-like text nodes', async () => {
336 const container = document.createElement('div');
337 class TemporalLike {
338 valueOf() {
339 // Throwing here is the behavior of ECMAScript "Temporal" date/time API.
340 // See https://tc39.es/proposal-temporal/docs/plaindate.html#valueOf
341 throw new TypeError('prod message');
342 }
343 toString() {
344 return '2020-01-01';
345 }
346 }
347 const root = ReactDOMClient.createRoot(container);
348 await expect(
349 act(() => {
350 root.render(<div>{new TemporalLike()}</div>);
351 }),
352 ).rejects.toThrow(
353 new Error(
354 'Objects are not valid as a React child (found: object with keys {}).' +
355 ' If you meant to render a collection of children, use an array instead.',
356 ),
357 );
358 });
359 });