main
js 400 lines 10.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 let React;
13 let ReactDOM;
14 let findDOMNode;
15 let ReactDOMClient;
16 let TogglingComponent;
17 let act;
18 let Scheduler;
19 let assertLog;
20 let assertConsoleErrorDev;
21
22 let container;
23
24 describe('ReactEmptyComponent', () => {
25 beforeEach(() => {
26 jest.resetModules();
27
28 React = require('react');
29 ReactDOM = require('react-dom');
30 ReactDOMClient = require('react-dom/client');
31 findDOMNode =
32 ReactDOM.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE
33 .findDOMNode;
34 Scheduler = require('scheduler');
35 const InternalTestUtils = require('internal-test-utils');
36 act = InternalTestUtils.act;
37 assertLog = InternalTestUtils.assertLog;
38 assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev;
39
40 container = document.createElement('div');
41
42 TogglingComponent = class extends React.Component {
43 state = {component: this.props.firstComponent};
44
45 componentDidMount() {
46 Scheduler.log('mount ' + findDOMNode(this)?.nodeName);
47 this.setState({component: this.props.secondComponent});
48 }
49
50 componentDidUpdate() {
51 Scheduler.log('update ' + findDOMNode(this)?.nodeName);
52 }
53
54 render() {
55 const Component = this.state.component;
56 return Component ? <Component /> : null;
57 }
58 };
59 });
60
61 describe.each([null, undefined])('when %s', nullORUndefined => {
62 it('should not throw when rendering', () => {
63 function EmptyComponent() {
64 return nullORUndefined;
65 }
66
67 const root = ReactDOMClient.createRoot(container);
68
69 expect(() => {
70 ReactDOM.flushSync(() => {
71 root.render(<EmptyComponent />);
72 });
73 }).not.toThrow();
74 });
75
76 it('should not produce child DOM nodes for nullish and false', async () => {
77 function Component1() {
78 return nullORUndefined;
79 }
80
81 function Component2() {
82 return false;
83 }
84
85 const container1 = document.createElement('div');
86 const root1 = ReactDOMClient.createRoot(container1);
87 await act(() => {
88 root1.render(<Component1 />);
89 });
90 expect(container1.children.length).toBe(0);
91
92 const container2 = document.createElement('div');
93 const root2 = ReactDOMClient.createRoot(container2);
94 await act(() => {
95 root2.render(<Component2 />);
96 });
97 expect(container2.children.length).toBe(0);
98 });
99
100 it('should be able to switch between rendering nullish and a normal tag', async () => {
101 const instance1 = (
102 <TogglingComponent
103 firstComponent={nullORUndefined}
104 secondComponent={'div'}
105 />
106 );
107 const instance2 = (
108 <TogglingComponent
109 firstComponent={'div'}
110 secondComponent={nullORUndefined}
111 />
112 );
113
114 const container2 = document.createElement('div');
115 const root1 = ReactDOMClient.createRoot(container);
116 await act(() => {
117 root1.render(instance1);
118 });
119
120 assertLog(['mount undefined', 'update DIV']);
121
122 const root2 = ReactDOMClient.createRoot(container2);
123 await act(() => {
124 root2.render(instance2);
125 });
126
127 assertLog(['mount DIV', 'update undefined']);
128 });
129
130 it('should be able to switch in a list of children', async () => {
131 const instance1 = (
132 <TogglingComponent
133 firstComponent={nullORUndefined}
134 secondComponent={'div'}
135 />
136 );
137
138 const root = ReactDOMClient.createRoot(container);
139 await act(() => {
140 root.render(
141 <div>
142 {instance1}
143 {instance1}
144 {instance1}
145 </div>,
146 );
147 });
148
149 assertLog([
150 'mount undefined',
151 'mount undefined',
152 'mount undefined',
153 'update DIV',
154 'update DIV',
155 'update DIV',
156 ]);
157 });
158
159 it('should distinguish between a script placeholder and an actual script tag', () => {
160 const instance1 = (
161 <TogglingComponent
162 firstComponent={nullORUndefined}
163 secondComponent={'script'}
164 />
165 );
166 const instance2 = (
167 <TogglingComponent
168 firstComponent={'script'}
169 secondComponent={nullORUndefined}
170 />
171 );
172
173 const root1 = ReactDOMClient.createRoot(container);
174 expect(() => {
175 ReactDOM.flushSync(() => {
176 root1.render(instance1);
177 });
178 }).not.toThrow();
179
180 expect(container.innerHTML).toBe('<script></script>');
181 if (gate('enableTrustedTypesIntegration')) {
182 assertConsoleErrorDev([
183 'Encountered a script tag while rendering React component. ' +
184 'Scripts inside React components are never executed when rendering on the client. ' +
185 'Consider using template tag instead (https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template).\n' +
186 ' in script (at **)\n' +
187 ' in TogglingComponent (at **)',
188 ]);
189 }
190
191 const container2 = document.createElement('div');
192 const root2 = ReactDOMClient.createRoot(container2);
193 expect(() => {
194 ReactDOM.flushSync(() => {
195 root2.render(instance2);
196 });
197 }).not.toThrow();
198
199 assertLog([
200 'mount undefined',
201 'update SCRIPT',
202 'mount SCRIPT',
203 'update undefined',
204 ]);
205 expect(container2.innerHTML).toBe('');
206 });
207
208 it(
209 'should have findDOMNode return null when multiple layers of composite ' +
210 'components render to the same nullish placeholder',
211 () => {
212 function GrandChild() {
213 return nullORUndefined;
214 }
215
216 function Child() {
217 return <GrandChild />;
218 }
219
220 const instance1 = (
221 <TogglingComponent firstComponent={'div'} secondComponent={Child} />
222 );
223 const instance2 = (
224 <TogglingComponent firstComponent={Child} secondComponent={'div'} />
225 );
226
227 const root1 = ReactDOMClient.createRoot(container);
228 expect(() => {
229 ReactDOM.flushSync(() => {
230 root1.render(instance1);
231 });
232 }).not.toThrow();
233
234 const container2 = document.createElement('div');
235 const root2 = ReactDOMClient.createRoot(container2);
236 expect(() => {
237 ReactDOM.flushSync(() => {
238 root2.render(instance2);
239 });
240 }).not.toThrow();
241
242 assertLog([
243 'mount DIV',
244 'update undefined',
245 'mount undefined',
246 'update DIV',
247 ]);
248 },
249 );
250
251 it('works when switching components', async () => {
252 let innerRef;
253
254 class Inner extends React.Component {
255 render() {
256 return <span />;
257 }
258
259 componentDidMount() {
260 // Make sure the DOM node resolves properly even if we're replacing a
261 // `null` component
262 expect(findDOMNode(this)).not.toBe(null);
263 }
264
265 componentWillUnmount() {
266 // Even though we're getting replaced by `null`, we haven't been
267 // replaced yet!
268 expect(findDOMNode(this)).not.toBe(null);
269 }
270 }
271
272 function Wrapper({showInner}) {
273 innerRef = React.createRef(null);
274 return showInner ? <Inner ref={innerRef} /> : nullORUndefined;
275 }
276
277 const el = document.createElement('div');
278
279 // Render the <Inner /> component...
280 const root = ReactDOMClient.createRoot(el);
281 await act(() => {
282 root.render(<Wrapper showInner={true} />);
283 });
284 expect(innerRef.current).not.toBe(null);
285
286 // Switch to null...
287 await act(() => {
288 root.render(<Wrapper showInner={false} />);
289 });
290 expect(innerRef.current).toBe(null);
291
292 // ...then switch back.
293 await act(() => {
294 root.render(<Wrapper showInner={true} />);
295 });
296 expect(innerRef.current).not.toBe(null);
297
298 expect.assertions(6);
299 });
300
301 it('can render nullish at the top level', async () => {
302 const div = document.createElement('div');
303 const root = ReactDOMClient.createRoot(div);
304
305 await act(() => {
306 root.render(nullORUndefined);
307 });
308 expect(div.innerHTML).toBe('');
309 });
310
311 it('does not break when updating during mount', () => {
312 class Child extends React.Component {
313 componentDidMount() {
314 if (this.props.onMount) {
315 this.props.onMount();
316 }
317 }
318
319 render() {
320 if (!this.props.visible) {
321 return nullORUndefined;
322 }
323
324 return <div>hello world</div>;
325 }
326 }
327
328 class Parent extends React.Component {
329 update = () => {
330 this.forceUpdate();
331 };
332
333 render() {
334 return (
335 <div>
336 <Child key="1" visible={false} />
337 <Child key="0" visible={true} onMount={this.update} />
338 <Child key="2" visible={false} />
339 </div>
340 );
341 }
342 }
343
344 const root = ReactDOMClient.createRoot(container);
345 expect(() => {
346 ReactDOM.flushSync(() => {
347 root.render(<Parent />);
348 });
349 }).not.toThrow();
350 });
351
352 it('preserves the dom node during updates', async () => {
353 function Empty() {
354 return nullORUndefined;
355 }
356
357 const root = ReactDOMClient.createRoot(container);
358 await act(() => {
359 root.render(<Empty />);
360 });
361 const noscript1 = container.firstChild;
362 expect(noscript1).toBe(null);
363
364 // This update shouldn't create a DOM node
365 await act(() => {
366 root.render(<Empty />);
367 });
368 const noscript2 = container.firstChild;
369 expect(noscript2).toBe(null);
370 });
371
372 it('should not warn about React.forwardRef that returns nullish', () => {
373 const Empty = () => {
374 return nullORUndefined;
375 };
376 const EmptyForwardRef = React.forwardRef(Empty);
377
378 const root = ReactDOMClient.createRoot(container);
379 expect(() => {
380 ReactDOM.flushSync(() => {
381 root.render(<EmptyForwardRef />);
382 });
383 }).not.toThrow();
384 });
385
386 it('should not warn about React.memo that returns nullish', () => {
387 const Empty = () => {
388 return nullORUndefined;
389 };
390 const EmptyMemo = React.memo(Empty);
391
392 const root = ReactDOMClient.createRoot(container);
393 expect(() => {
394 ReactDOM.flushSync(() => {
395 root.render(<EmptyMemo />);
396 });
397 }).not.toThrow();
398 });
399 });
400 });