main
js 345 lines 8.7 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 Suspense;
17 let Scheduler;
18 let act;
19 let textCache;
20 let assertLog;
21
22 describe('ReactDOMSuspensePlaceholder', () => {
23 let container;
24
25 beforeEach(() => {
26 jest.resetModules();
27 React = require('react');
28 ReactDOM = require('react-dom');
29 ReactDOMClient = require('react-dom/client');
30 findDOMNode =
31 ReactDOM.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE
32 .findDOMNode;
33 Scheduler = require('scheduler');
34 act = require('internal-test-utils').act;
35 assertLog = require('internal-test-utils').assertLog;
36 Suspense = React.Suspense;
37 container = document.createElement('div');
38 document.body.appendChild(container);
39
40 textCache = new Map();
41 });
42
43 afterEach(() => {
44 document.body.removeChild(container);
45 });
46
47 function resolveText(text) {
48 const record = textCache.get(text);
49 if (record === undefined) {
50 const newRecord = {
51 status: 'resolved',
52 value: text,
53 };
54 textCache.set(text, newRecord);
55 } else if (record.status === 'pending') {
56 const thenable = record.value;
57 record.status = 'resolved';
58 record.value = text;
59 thenable.pings.forEach(t => t());
60 }
61 }
62
63 function readText(text) {
64 const record = textCache.get(text);
65 if (record !== undefined) {
66 switch (record.status) {
67 case 'pending':
68 Scheduler.log(`Suspend! [${text}]`);
69 throw record.value;
70 case 'rejected':
71 throw record.value;
72 case 'resolved':
73 return record.value;
74 }
75 } else {
76 Scheduler.log(`Suspend! [${text}]`);
77 const thenable = {
78 pings: [],
79 then(resolve) {
80 if (newRecord.status === 'pending') {
81 thenable.pings.push(resolve);
82 } else {
83 Promise.resolve().then(() => resolve(newRecord.value));
84 }
85 },
86 };
87
88 const newRecord = {
89 status: 'pending',
90 value: thenable,
91 };
92 textCache.set(text, newRecord);
93
94 throw thenable;
95 }
96 }
97
98 function Text({text}) {
99 Scheduler.log(text);
100 return text;
101 }
102
103 function AsyncText({text}) {
104 readText(text);
105 Scheduler.log(text);
106 return text;
107 }
108
109 // @gate !disableLegacyMode
110 it('hides and unhides timed out DOM elements in legacy roots', async () => {
111 const divs = [
112 React.createRef(null),
113 React.createRef(null),
114 React.createRef(null),
115 ];
116 function App() {
117 return (
118 <Suspense fallback={<Text text="Loading..." />}>
119 <div ref={divs[0]}>
120 <Text text="A" />
121 </div>
122 <div ref={divs[1]}>
123 <AsyncText text="B" />
124 </div>
125 <div style={{display: 'inline'}} ref={divs[2]}>
126 <Text text="C" />
127 </div>
128 </Suspense>
129 );
130 }
131 ReactDOM.render(<App />, container);
132 expect(window.getComputedStyle(divs[0].current).display).toEqual('none');
133 expect(window.getComputedStyle(divs[1].current).display).toEqual('none');
134 expect(window.getComputedStyle(divs[2].current).display).toEqual('none');
135 assertLog(['A', 'Suspend! [B]', 'C', 'Loading...']);
136 await act(async () => {
137 await resolveText('B');
138 });
139
140 expect(window.getComputedStyle(divs[0].current).display).toEqual('block');
141 expect(window.getComputedStyle(divs[1].current).display).toEqual('block');
142 // This div's display was set with a prop.
143 expect(window.getComputedStyle(divs[2].current).display).toEqual('inline');
144 assertLog(['B']);
145 });
146
147 it('hides and unhides timed out text nodes', async () => {
148 function App() {
149 return (
150 <Suspense fallback={<Text text="Loading..." />}>
151 <Text text="A" />
152 <AsyncText text="B" />
153 <Text text="C" />
154 </Suspense>
155 );
156 }
157 const root = ReactDOMClient.createRoot(container);
158 await act(() => {
159 root.render(<App />);
160 });
161
162 expect(container.textContent).toEqual('Loading...');
163 assertLog([
164 'A',
165 'Suspend! [B]',
166 'Loading...',
167 // pre-warming
168 'A',
169 'Suspend! [B]',
170 'C',
171 ]);
172 await act(() => {
173 resolveText('B');
174 });
175 assertLog(['A', 'B', 'C']);
176 expect(container.textContent).toEqual('ABC');
177 });
178
179 // @gate !disableLegacyMode
180 it(
181 'in legacy roots, re-hides children if their display is updated ' +
182 'but the boundary is still showing the fallback',
183 async () => {
184 const {useState} = React;
185
186 let setIsVisible;
187 function Sibling({children}) {
188 const [isVisible, _setIsVisible] = useState(false);
189 setIsVisible = _setIsVisible;
190 return (
191 <span style={{display: isVisible ? 'inline' : 'none'}}>
192 {children}
193 </span>
194 );
195 }
196
197 function App() {
198 return (
199 <Suspense fallback={<Text text="Loading..." />}>
200 <Sibling>Sibling</Sibling>
201 <span>
202 <AsyncText text="Async" />
203 </span>
204 </Suspense>
205 );
206 }
207
208 await act(() => {
209 ReactDOM.render(<App />, container);
210 });
211 expect(container.innerHTML).toEqual(
212 '<span style="display: none;">Sibling</span><span style=' +
213 '"display: none;"></span>Loading...',
214 );
215 assertLog(['Suspend! [Async]', 'Loading...']);
216
217 // Update the inline display style. It will be overridden because it's
218 // inside a hidden fallback.
219 await act(() => setIsVisible(true));
220 expect(container.innerHTML).toEqual(
221 '<span style="display: none;">Sibling</span><span style=' +
222 '"display: none;"></span>Loading...',
223 );
224 assertLog(['Suspend! [Async]']);
225
226 // Unsuspend. The style should now match the inline prop.
227 await act(() => resolveText('Async'));
228 expect(container.innerHTML).toEqual(
229 '<span style="display: inline;">Sibling</span><span style="">Async</span>',
230 );
231 },
232 );
233
234 // Regression test for https://github.com/facebook/react/issues/14188
235 // @gate !disableLegacyMode
236 it('can call findDOMNode() in a suspended component commit phase in legacy roots', async () => {
237 const log = [];
238 const Lazy = React.lazy(
239 () =>
240 new Promise(resolve =>
241 resolve({
242 default() {
243 return 'lazy';
244 },
245 }),
246 ),
247 );
248
249 class Child extends React.Component {
250 componentDidMount() {
251 log.push('cDM ' + this.props.id);
252 findDOMNode(this);
253 }
254 componentDidUpdate() {
255 log.push('cDU ' + this.props.id);
256 findDOMNode(this);
257 }
258 render() {
259 return 'child';
260 }
261 }
262
263 const buttonRef = React.createRef();
264 class App extends React.Component {
265 state = {
266 suspend: false,
267 };
268 handleClick = () => {
269 this.setState({suspend: true});
270 };
271 render() {
272 return (
273 <React.Suspense fallback="Loading">
274 <Child id="first" />
275 <button ref={buttonRef} onClick={this.handleClick}>
276 Suspend
277 </button>
278 <Child id="second" />
279 {this.state.suspend && <Lazy />}
280 </React.Suspense>
281 );
282 }
283 }
284
285 ReactDOM.render(<App />, container);
286
287 expect(log).toEqual(['cDM first', 'cDM second']);
288 log.length = 0;
289
290 buttonRef.current.dispatchEvent(new MouseEvent('click', {bubbles: true}));
291 await Lazy;
292 expect(log).toEqual(['cDU first', 'cDU second']);
293 });
294
295 // Regression test for https://github.com/facebook/react/issues/14188
296 it('can call legacy findDOMNode() in a suspended component commit phase (#2)', async () => {
297 let suspendOnce = Promise.resolve();
298 function Suspend() {
299 if (suspendOnce) {
300 const promise = suspendOnce;
301 suspendOnce = null;
302 throw promise;
303 }
304 return null;
305 }
306
307 const log = [];
308 class Child extends React.Component {
309 componentDidMount() {
310 log.push('cDM');
311 findDOMNode(this);
312 }
313
314 componentDidUpdate() {
315 log.push('cDU');
316 findDOMNode(this);
317 }
318
319 render() {
320 return null;
321 }
322 }
323
324 function App() {
325 return (
326 <Suspense fallback="Loading">
327 <Suspend />
328 <Child />
329 </Suspense>
330 );
331 }
332
333 const root = ReactDOMClient.createRoot(container);
334 await act(() => {
335 root.render(<App />);
336 });
337
338 expect(log).toEqual(['cDM']);
339 await act(() => {
340 root.render(<App />);
341 });
342
343 expect(log).toEqual(['cDM', 'cDU']);
344 });
345 });