main
js 480 lines 10.9 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 * @jest-environment node
9 */
10
11 'use strict';
12
13 let React;
14 let ReactNoop;
15 let Scheduler;
16 let act;
17 let use;
18 let useOptimistic;
19 let useState;
20 let useTransition;
21 let useDeferredValue;
22 let assertLog;
23 let waitForPaint;
24
25 describe('ReactDefaultTransitionIndicator', () => {
26 beforeEach(() => {
27 jest.resetModules();
28
29 React = require('react');
30 ReactNoop = require('react-noop-renderer');
31 Scheduler = require('scheduler');
32 const InternalTestUtils = require('internal-test-utils');
33 act = InternalTestUtils.act;
34 assertLog = InternalTestUtils.assertLog;
35 waitForPaint = InternalTestUtils.waitForPaint;
36 use = React.use;
37 useOptimistic = React.useOptimistic;
38 useState = React.useState;
39 useTransition = React.useTransition;
40 useDeferredValue = React.useDeferredValue;
41 });
42
43 // @gate enableDefaultTransitionIndicator
44 it('triggers the default indicator while a transition is on-going', async () => {
45 let resolve;
46 const promise = new Promise(r => (resolve = r));
47 function App() {
48 return use(promise);
49 }
50
51 const root = ReactNoop.createRoot({
52 onDefaultTransitionIndicator() {
53 Scheduler.log('start');
54 return () => {
55 Scheduler.log('stop');
56 };
57 },
58 });
59 await act(() => {
60 React.startTransition(() => {
61 root.render(<App />);
62 });
63 });
64
65 assertLog(['start']);
66
67 await act(async () => {
68 await resolve('Hello');
69 });
70
71 assertLog(['stop']);
72
73 expect(root).toMatchRenderedOutput('Hello');
74 });
75
76 // @gate enableDefaultTransitionIndicator
77 it('does not trigger the default indicator if there is a sync mutation', async () => {
78 const promiseA = Promise.resolve('Hi');
79 let resolveB;
80 const promiseB = new Promise(r => (resolveB = r));
81 let update;
82 function App({children}) {
83 const [state, setState] = useState('');
84 update = setState;
85 return (
86 <div>
87 {state}
88 {children}
89 </div>
90 );
91 }
92
93 const root = ReactNoop.createRoot({
94 onDefaultTransitionIndicator() {
95 Scheduler.log('start');
96 return () => {
97 Scheduler.log('stop');
98 };
99 },
100 });
101 await act(() => {
102 React.startTransition(() => {
103 root.render(<App>{promiseA}</App>);
104 });
105 });
106
107 assertLog(['start', 'stop']);
108
109 expect(root).toMatchRenderedOutput(<div>Hi</div>);
110
111 await act(() => {
112 update('Loading...');
113 React.startTransition(() => {
114 update('');
115 root.render(<App>{promiseB}</App>);
116 });
117 });
118
119 assertLog([]);
120
121 expect(root).toMatchRenderedOutput(<div>Loading...Hi</div>);
122
123 await act(async () => {
124 await resolveB('Hello');
125 });
126
127 assertLog([]);
128
129 expect(root).toMatchRenderedOutput(<div>Hello</div>);
130 });
131
132 // @gate enableDefaultTransitionIndicator
133 it('does not trigger the default indicator if there is an optimistic update', async () => {
134 const promiseA = Promise.resolve('Hi');
135 let resolveB;
136 const promiseB = new Promise(r => (resolveB = r));
137 let update;
138 function App({children}) {
139 const [state, setOptimistic] = useOptimistic('');
140 update = setOptimistic;
141 return (
142 <div>
143 {state}
144 {children}
145 </div>
146 );
147 }
148
149 const root = ReactNoop.createRoot({
150 onDefaultTransitionIndicator() {
151 Scheduler.log('start');
152 return () => {
153 Scheduler.log('stop');
154 };
155 },
156 });
157 await act(() => {
158 React.startTransition(() => {
159 root.render(<App>{promiseA}</App>);
160 });
161 });
162
163 assertLog(['start', 'stop']);
164
165 expect(root).toMatchRenderedOutput(<div>Hi</div>);
166
167 await act(() => {
168 React.startTransition(() => {
169 update('Loading...');
170 root.render(<App>{promiseB}</App>);
171 });
172 });
173
174 assertLog([]);
175
176 expect(root).toMatchRenderedOutput(<div>Loading...Hi</div>);
177
178 await act(async () => {
179 await resolveB('Hello');
180 });
181
182 assertLog([]);
183
184 expect(root).toMatchRenderedOutput(<div>Hello</div>);
185 });
186
187 // @gate enableDefaultTransitionIndicator
188 it('does not trigger the default indicator if there is an isPending update', async () => {
189 const promiseA = Promise.resolve('Hi');
190 let resolveB;
191 const promiseB = new Promise(r => (resolveB = r));
192 let start;
193 function App({children}) {
194 const [isPending, startTransition] = useTransition();
195 start = startTransition;
196 return (
197 <div>
198 {isPending ? 'Loading...' : ''}
199 {children}
200 </div>
201 );
202 }
203
204 const root = ReactNoop.createRoot({
205 onDefaultTransitionIndicator() {
206 Scheduler.log('start');
207 return () => {
208 Scheduler.log('stop');
209 };
210 },
211 });
212 await act(() => {
213 React.startTransition(() => {
214 root.render(<App>{promiseA}</App>);
215 });
216 });
217
218 assertLog(['start', 'stop']);
219
220 expect(root).toMatchRenderedOutput(<div>Hi</div>);
221
222 await act(() => {
223 start(() => {
224 root.render(<App>{promiseB}</App>);
225 });
226 });
227
228 assertLog([]);
229
230 expect(root).toMatchRenderedOutput(<div>Loading...Hi</div>);
231
232 await act(async () => {
233 await resolveB('Hello');
234 });
235
236 assertLog([]);
237
238 expect(root).toMatchRenderedOutput(<div>Hello</div>);
239 });
240
241 // @gate enableDefaultTransitionIndicator
242 it('triggers the default indicator while an async transition is ongoing', async () => {
243 let resolve;
244 const promise = new Promise(r => (resolve = r));
245 let start;
246 function App() {
247 const [, startTransition] = useTransition();
248 start = startTransition;
249 return 'Hi';
250 }
251
252 const root = ReactNoop.createRoot({
253 onDefaultTransitionIndicator() {
254 Scheduler.log('start');
255 return () => {
256 Scheduler.log('stop');
257 };
258 },
259 });
260 await act(() => {
261 root.render(<App />);
262 });
263
264 assertLog([]);
265
266 await act(() => {
267 // Start an async action but we haven't called setState yet
268 start(() => promise);
269 });
270
271 assertLog(['start']);
272
273 await act(async () => {
274 await resolve('Hello');
275 });
276
277 assertLog(['stop']);
278
279 expect(root).toMatchRenderedOutput('Hi');
280 });
281
282 // @gate enableDefaultTransitionIndicator
283 it('triggers the default indicator while an async transition is ongoing (isomorphic)', async () => {
284 let resolve;
285 const promise = new Promise(r => (resolve = r));
286 function App() {
287 return 'Hi';
288 }
289
290 const root = ReactNoop.createRoot({
291 onDefaultTransitionIndicator() {
292 Scheduler.log('start');
293 return () => {
294 Scheduler.log('stop');
295 };
296 },
297 });
298 await act(() => {
299 root.render(<App />);
300 });
301
302 assertLog([]);
303
304 await act(() => {
305 // Start an async action but we haven't called setState yet
306 React.startTransition(() => promise);
307 });
308
309 assertLog(['start']);
310
311 await act(async () => {
312 await resolve('Hello');
313 });
314
315 assertLog(['stop']);
316
317 expect(root).toMatchRenderedOutput('Hi');
318 });
319
320 it('does not triggers isomorphic async action default indicator if there are two different ones', async () => {
321 let resolve;
322 const promise = new Promise(r => (resolve = r));
323 function App() {
324 return 'Hi';
325 }
326
327 const root = ReactNoop.createRoot({
328 onDefaultTransitionIndicator() {
329 Scheduler.log('start');
330 return () => {
331 Scheduler.log('stop');
332 };
333 },
334 });
335 // Initialize second root. This is now ambiguous which indicator to use.
336 ReactNoop.createRoot({
337 onDefaultTransitionIndicator() {
338 Scheduler.log('start2');
339 return () => {
340 Scheduler.log('stop2');
341 };
342 },
343 });
344 await act(() => {
345 root.render(<App />);
346 });
347
348 assertLog([]);
349
350 await act(() => {
351 // Start an async action but we haven't called setState yet
352 React.startTransition(() => promise);
353 });
354
355 assertLog([]);
356
357 await act(async () => {
358 await resolve('Hello');
359 });
360
361 assertLog([]);
362
363 expect(root).toMatchRenderedOutput('Hi');
364 });
365
366 it('does not triggers isomorphic async action default indicator if there is a loading state', async () => {
367 let resolve;
368 const promise = new Promise(r => (resolve = r));
369 let update;
370 function App() {
371 const [state, setState] = useState(false);
372 update = setState;
373 return state ? 'Loading' : 'Hi';
374 }
375
376 const root = ReactNoop.createRoot({
377 onDefaultTransitionIndicator() {
378 Scheduler.log('start');
379 return () => {
380 Scheduler.log('stop');
381 };
382 },
383 });
384 await act(() => {
385 root.render(<App />);
386 });
387
388 assertLog([]);
389
390 await act(() => {
391 update(true);
392 React.startTransition(() => promise.then(() => update(false)));
393 });
394
395 assertLog([]);
396
397 expect(root).toMatchRenderedOutput('Loading');
398
399 await act(async () => {
400 await resolve('Hello');
401 });
402
403 assertLog([]);
404
405 expect(root).toMatchRenderedOutput('Hi');
406 });
407
408 it('should not trigger for useDeferredValue (sync)', async () => {
409 function Text({text}) {
410 Scheduler.log(text);
411 return text;
412 }
413 function App({value}) {
414 const deferredValue = useDeferredValue(value, 'Hi');
415 return <Text text={deferredValue} />;
416 }
417
418 const root = ReactNoop.createRoot({
419 onDefaultTransitionIndicator() {
420 Scheduler.log('start');
421 return () => {
422 Scheduler.log('stop');
423 };
424 },
425 });
426 await act(async () => {
427 root.render(<App value="Hello" />);
428 await waitForPaint(['Hi']);
429 expect(root).toMatchRenderedOutput('Hi');
430 });
431
432 assertLog(['Hello']);
433
434 expect(root).toMatchRenderedOutput('Hello');
435
436 assertLog([]);
437
438 await act(async () => {
439 root.render(<App value="Bye" />);
440 await waitForPaint(['Hello']);
441 expect(root).toMatchRenderedOutput('Hello');
442 });
443
444 assertLog(['Bye']);
445
446 expect(root).toMatchRenderedOutput('Bye');
447 });
448
449 // @gate enableDefaultTransitionIndicator
450 it('should not trigger for useDeferredValue (transition)', async () => {
451 function Text({text}) {
452 Scheduler.log(text);
453 return text;
454 }
455 function App({value}) {
456 const deferredValue = useDeferredValue(value, 'Hi');
457 return <Text text={deferredValue} />;
458 }
459
460 const root = ReactNoop.createRoot({
461 onDefaultTransitionIndicator() {
462 Scheduler.log('start');
463 return () => {
464 Scheduler.log('stop');
465 };
466 },
467 });
468 await act(async () => {
469 React.startTransition(() => {
470 root.render(<App value="Hello" />);
471 });
472 await waitForPaint(['start', 'Hi', 'stop']);
473 expect(root).toMatchRenderedOutput('Hi');
474 });
475
476 assertLog(['Hello']);
477
478 expect(root).toMatchRenderedOutput('Hello');
479 });
480 });