main
js 434 lines 14.5 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 {COMMENT_NODE} = require('react-dom-bindings/src/client/HTMLNodeType');
13
14 let React;
15 let ReactDOM;
16 let ReactDOMClient;
17 let waitForAll;
18 let assertConsoleErrorDev;
19
20 describe('ReactMount', () => {
21 beforeEach(() => {
22 jest.resetModules();
23
24 React = require('react');
25 ReactDOM = require('react-dom');
26 ReactDOMClient = require('react-dom/client');
27
28 const InternalTestUtils = require('internal-test-utils');
29 waitForAll = InternalTestUtils.waitForAll;
30 assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev;
31 });
32
33 describe('unmountComponentAtNode', () => {
34 // @gate !disableLegacyMode
35 it('throws when given a non-node', () => {
36 const nodeArray = document.getElementsByTagName('div');
37 expect(() => {
38 ReactDOM.unmountComponentAtNode(nodeArray);
39 }).toThrow('Target container is not a DOM element.');
40 });
41
42 // @gate !disableLegacyMode
43 it('returns false on non-React containers', () => {
44 const d = document.createElement('div');
45 d.innerHTML = '<b>hellooo</b>';
46 expect(ReactDOM.unmountComponentAtNode(d)).toBe(false);
47 expect(d.textContent).toBe('hellooo');
48 });
49
50 // @gate !disableLegacyMode
51 it('returns true on React containers', () => {
52 const d = document.createElement('div');
53 ReactDOM.render(<b>hellooo</b>, d);
54 expect(d.textContent).toBe('hellooo');
55 expect(ReactDOM.unmountComponentAtNode(d)).toBe(true);
56 expect(d.textContent).toBe('');
57 });
58 });
59
60 // @gate !disableLegacyMode
61 it('warns when given a factory', () => {
62 class Component extends React.Component {
63 render() {
64 return <div />;
65 }
66 }
67
68 const container = document.createElement('div');
69 ReactDOM.render(Component, container);
70 assertConsoleErrorDev([
71 'Functions are not valid as a React child. ' +
72 'This may happen if you return Component instead of <Component /> from render. ' +
73 'Or maybe you meant to call this function rather than return it.\n' +
74 ' root.render(Component)',
75 ]);
76 });
77
78 // @gate !disableLegacyMode
79 it('should render different components in same root', () => {
80 const container = document.createElement('container');
81 document.body.appendChild(container);
82
83 ReactDOM.render(<div />, container);
84 expect(container.firstChild.nodeName).toBe('DIV');
85
86 ReactDOM.render(<span />, container);
87 expect(container.firstChild.nodeName).toBe('SPAN');
88 });
89
90 // @gate !disableLegacyMode
91 it('should unmount and remount if the key changes', () => {
92 const container = document.createElement('container');
93
94 const mockMount = jest.fn();
95 const mockUnmount = jest.fn();
96
97 class Component extends React.Component {
98 componentDidMount = mockMount;
99 componentWillUnmount = mockUnmount;
100 render() {
101 return <span>{this.props.text}</span>;
102 }
103 }
104
105 expect(mockMount).toHaveBeenCalledTimes(0);
106 expect(mockUnmount).toHaveBeenCalledTimes(0);
107
108 ReactDOM.render(<Component text="orange" key="A" />, container);
109 expect(container.firstChild.innerHTML).toBe('orange');
110 expect(mockMount).toHaveBeenCalledTimes(1);
111 expect(mockUnmount).toHaveBeenCalledTimes(0);
112
113 // If we change the key, the component is unmounted and remounted
114 ReactDOM.render(<Component text="green" key="B" />, container);
115 expect(container.firstChild.innerHTML).toBe('green');
116 expect(mockMount).toHaveBeenCalledTimes(2);
117 expect(mockUnmount).toHaveBeenCalledTimes(1);
118
119 // But if we don't change the key, the component instance is reused
120 ReactDOM.render(<Component text="blue" key="B" />, container);
121 expect(container.firstChild.innerHTML).toBe('blue');
122 expect(mockMount).toHaveBeenCalledTimes(2);
123 expect(mockUnmount).toHaveBeenCalledTimes(1);
124 });
125
126 // @gate !disableLegacyMode
127 it('should reuse markup if rendering to the same target twice', () => {
128 const container = document.createElement('container');
129 const instance1 = ReactDOM.render(<div />, container);
130 const instance2 = ReactDOM.render(<div />, container);
131
132 expect(instance1 === instance2).toBe(true);
133 });
134
135 // @gate !disableLegacyMode
136 it('should not warn if mounting into non-empty node', () => {
137 const container = document.createElement('container');
138 container.innerHTML = '<div></div>';
139
140 ReactDOM.render(<div />, container);
141 });
142
143 // @gate !disableLegacyMode
144 it('should warn when mounting into document.body', () => {
145 const iFrame = document.createElement('iframe');
146 document.body.appendChild(iFrame);
147
148 // HostSingletons make the warning for document.body unecessary
149 ReactDOM.render(<div />, iFrame.contentDocument.body);
150 });
151
152 // @gate !disableLegacyMode
153 it('should warn if render removes React-rendered children', () => {
154 const container = document.createElement('container');
155
156 class Component extends React.Component {
157 render() {
158 return (
159 <div>
160 <div />
161 </div>
162 );
163 }
164 }
165
166 ReactDOM.render(<Component />, container);
167
168 // Test that blasting away children throws a warning
169 const rootNode = container.firstChild;
170
171 ReactDOM.render(<span />, rootNode);
172 assertConsoleErrorDev([
173 'Replacing React-rendered children with a new ' +
174 'root component. If you intended to update the children of this node, ' +
175 'you should instead have the existing children update their state and ' +
176 'render the new components instead of calling ReactDOM.render.',
177 ]);
178 });
179
180 // @gate !disableLegacyMode
181 it('should warn if the unmounted node was rendered by another copy of React', () => {
182 jest.resetModules();
183 const ReactDOMOther = require('react-dom');
184 const container = document.createElement('div');
185
186 class Component extends React.Component {
187 render() {
188 return (
189 <div>
190 <div />
191 </div>
192 );
193 }
194 }
195
196 ReactDOM.render(<Component />, container);
197 // Make sure ReactDOM and ReactDOMOther are different copies
198 expect(ReactDOM).not.toEqual(ReactDOMOther);
199
200 ReactDOMOther.unmountComponentAtNode(container);
201 assertConsoleErrorDev([
202 "unmountComponentAtNode(): The node you're attempting to unmount " +
203 'was rendered by another copy of React.',
204 ]);
205
206 // Don't throw a warning if the correct React copy unmounts the node
207 ReactDOM.unmountComponentAtNode(container);
208 });
209
210 // @gate !disableLegacyMode
211 it('passes the correct callback context', () => {
212 const container = document.createElement('div');
213 let calls = 0;
214
215 ReactDOM.render(<div />, container, function () {
216 expect(this.nodeName).toBe('DIV');
217 calls++;
218 });
219
220 // Update, no type change
221 ReactDOM.render(<div />, container, function () {
222 expect(this.nodeName).toBe('DIV');
223 calls++;
224 });
225
226 // Update, type change
227 ReactDOM.render(<span />, container, function () {
228 expect(this.nodeName).toBe('SPAN');
229 calls++;
230 });
231
232 // Batched update, no type change
233 ReactDOM.unstable_batchedUpdates(function () {
234 ReactDOM.render(<span />, container, function () {
235 expect(this.nodeName).toBe('SPAN');
236 calls++;
237 });
238 });
239
240 // Batched update, type change
241 ReactDOM.unstable_batchedUpdates(function () {
242 ReactDOM.render(<article />, container, function () {
243 expect(this.nodeName).toBe('ARTICLE');
244 calls++;
245 });
246 });
247
248 expect(calls).toBe(5);
249 });
250
251 // @gate !disableLegacyMode && classic
252 it('initial mount of legacy root is sync inside batchedUpdates, as if it were wrapped in flushSync', () => {
253 const container1 = document.createElement('div');
254 const container2 = document.createElement('div');
255
256 class Foo extends React.Component {
257 state = {active: false};
258 componentDidMount() {
259 this.setState({active: true});
260 }
261 render() {
262 return (
263 <div>{this.props.children + (this.state.active ? '!' : '')}</div>
264 );
265 }
266 }
267
268 ReactDOM.render(<div>1</div>, container1);
269
270 ReactDOM.unstable_batchedUpdates(() => {
271 // Update. Does not flush yet.
272 ReactDOM.render(<div>2</div>, container1);
273 expect(container1.textContent).toEqual('1');
274
275 // Initial mount on another root. Should flush immediately.
276 ReactDOM.render(<Foo>a</Foo>, container2);
277 // The earlier update also flushed, since flushSync flushes all pending
278 // sync work across all roots.
279 expect(container1.textContent).toEqual('2');
280 // Layout updates are also flushed synchronously
281 expect(container2.textContent).toEqual('a!');
282 });
283 expect(container1.textContent).toEqual('2');
284 expect(container2.textContent).toEqual('a!');
285 });
286
287 describe('mount point is a comment node', () => {
288 let containerDiv;
289 let mountPoint;
290
291 beforeEach(() => {
292 containerDiv = document.createElement('div');
293 containerDiv.innerHTML = 'A<!-- react-mount-point-unstable -->B';
294 mountPoint = containerDiv.childNodes[1];
295 expect(mountPoint.nodeType).toBe(COMMENT_NODE);
296 });
297
298 // @gate !disableLegacyMode
299 it('renders at a comment node', () => {
300 function Char(props) {
301 return props.children;
302 }
303 function list(chars) {
304 return chars.split('').map(c => <Char key={c}>{c}</Char>);
305 }
306
307 ReactDOM.render(list('aeiou'), mountPoint);
308 expect(containerDiv.innerHTML).toBe(
309 'Aaeiou<!-- react-mount-point-unstable -->B',
310 );
311
312 ReactDOM.render(list('yea'), mountPoint);
313 expect(containerDiv.innerHTML).toBe(
314 'Ayea<!-- react-mount-point-unstable -->B',
315 );
316
317 ReactDOM.render(list(''), mountPoint);
318 expect(containerDiv.innerHTML).toBe(
319 'A<!-- react-mount-point-unstable -->B',
320 );
321 });
322 });
323
324 // @gate !disableLegacyMode
325 it('clears existing children with legacy API', async () => {
326 const container = document.createElement('div');
327 container.innerHTML = '<div>a</div><div>b</div>';
328 ReactDOM.render(
329 <div>
330 <span>c</span>
331 <span>d</span>
332 </div>,
333 container,
334 );
335 expect(container.textContent).toEqual('cd');
336 ReactDOM.render(
337 <div>
338 <span>d</span>
339 <span>c</span>
340 </div>,
341 container,
342 );
343 await waitForAll([]);
344 expect(container.textContent).toEqual('dc');
345 });
346
347 // @gate !disableLegacyMode
348 it('warns when rendering with legacy API into createRoot() container', async () => {
349 const container = document.createElement('div');
350 const root = ReactDOMClient.createRoot(container);
351 root.render(<div>Hi</div>);
352 await waitForAll([]);
353 expect(container.textContent).toEqual('Hi');
354 ReactDOM.render(<div>Bye</div>, container);
355 assertConsoleErrorDev([
356 // We care about this warning:
357 'You are calling ReactDOM.render() on a container that was previously ' +
358 'passed to ReactDOMClient.createRoot(). This is not supported. ' +
359 'Did you mean to call root.render(element)?',
360 // This is more of a symptom but restructuring the code to avoid it isn't worth it:
361 'Replacing React-rendered children with a new root component. ' +
362 'If you intended to update the children of this node, ' +
363 'you should instead have the existing children update their state ' +
364 'and render the new components instead of calling ReactDOM.render.',
365 ]);
366 await waitForAll([]);
367 // This works now but we could disallow it:
368 expect(container.textContent).toEqual('Bye');
369 });
370
371 // @gate !disableLegacyMode
372 it('warns when unmounting with legacy API (no previous content)', async () => {
373 const container = document.createElement('div');
374 const root = ReactDOMClient.createRoot(container);
375 root.render(<div>Hi</div>);
376 await waitForAll([]);
377 expect(container.textContent).toEqual('Hi');
378 const unmounted = ReactDOM.unmountComponentAtNode(container);
379 assertConsoleErrorDev([
380 // We care about this warning:
381 'You are calling ReactDOM.unmountComponentAtNode() on a container that was previously ' +
382 'passed to ReactDOMClient.createRoot(). This is not supported. Did you mean to call root.unmount()?',
383 // This is more of a symptom but restructuring the code to avoid it isn't worth it:
384 'unmountComponentAtNode(): ' +
385 "The node you're attempting to unmount was rendered by React and is not a top-level container. " +
386 'Instead, have the parent component update its state and rerender in order to remove this component.',
387 ]);
388 expect(unmounted).toBe(false);
389 await waitForAll([]);
390 expect(container.textContent).toEqual('Hi');
391 root.unmount();
392 await waitForAll([]);
393 expect(container.textContent).toEqual('');
394 });
395
396 // @gate !disableLegacyMode
397 it('warns when unmounting with legacy API (has previous content)', async () => {
398 const container = document.createElement('div');
399 // Currently createRoot().render() doesn't clear this.
400 container.appendChild(document.createElement('div'));
401 // The rest is the same as test above.
402 const root = ReactDOMClient.createRoot(container);
403 root.render(<div>Hi</div>);
404 await waitForAll([]);
405 expect(container.textContent).toEqual('Hi');
406 const unmounted = ReactDOM.unmountComponentAtNode(container);
407 assertConsoleErrorDev([
408 'You are calling ReactDOM.unmountComponentAtNode() on a container ' +
409 'that was previously passed to ReactDOMClient.createRoot(). ' +
410 'This is not supported. Did you mean to call root.unmount()?',
411 // This is more of a symptom but restructuring the code to avoid it isn't worth it:
412 'unmountComponentAtNode(): ' +
413 "The node you're attempting to unmount was rendered by React and is not a top-level container. " +
414 'Instead, have the parent component update its state and rerender in order to remove this component.',
415 ]);
416 expect(unmounted).toBe(false);
417 await waitForAll([]);
418 expect(container.textContent).toEqual('Hi');
419 root.unmount();
420 await waitForAll([]);
421 expect(container.textContent).toEqual('');
422 });
423
424 // @gate !disableLegacyMode
425 it('warns when passing legacy container to createRoot()', () => {
426 const container = document.createElement('div');
427 ReactDOM.render(<div>Hi</div>, container);
428 ReactDOMClient.createRoot(container);
429 assertConsoleErrorDev([
430 'You are calling ReactDOMClient.createRoot() on a container that was previously ' +
431 'passed to ReactDOM.render(). This is not supported.',
432 ]);
433 });
434 });