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
+ // TODO: This should not require a discrete update ideally but work for default too.
113
+ ReactNoop.discreteUpdates(() => {
114
+ update('Loading...');
115
+ });
116
+ React.startTransition(() => {
117
+ update('');
118
+ root.render(<App>{promiseB}</App>);
119
+ });
120
+ });
121
+
122
+ assertLog([]);
123
+
124
+ expect(root).toMatchRenderedOutput(<div>Loading...Hi</div>);
125
+
126
+ await act(async () => {
127
+ await resolveB('Hello');
128
+ });
129
+
130
+ assertLog([]);
131
+
132
+ expect(root).toMatchRenderedOutput(<div>Hello</div>);
133
+ });
134
+
135
+ // @gate enableDefaultTransitionIndicator
136
+ it('does not trigger the default indicator if there is an optimistic update', async () => {
137
+ const promiseA = Promise.resolve('Hi');
138
+ let resolveB;
139
+ const promiseB = new Promise(r => (resolveB = r));
140
+ let update;
141
+ function App({children}) {
142
+ const [state, setOptimistic] = useOptimistic('');
143
+ update = setOptimistic;
144
+ return (
145
+ <div>
146
+ {state}
147
+ {children}
148
+ </div>
149
+ );
150
+ }
151
+
152
+ const root = ReactNoop.createRoot({
153
+ onDefaultTransitionIndicator() {
154
+ Scheduler.log('start');
155
+ return () => {
156
+ Scheduler.log('stop');
157
+ };
158
+ },
159
+ });
160
+ await act(() => {
161
+ React.startTransition(() => {
162
+ root.render(<App>{promiseA}</App>);
163
+ });
164
+ });
165
+
166
+ assertLog(['start', 'stop']);
167
+
168
+ expect(root).toMatchRenderedOutput(<div>Hi</div>);
169
+
170
+ await act(() => {
171
+ React.startTransition(() => {
172
+ update('Loading...');
173
+ root.render(<App>{promiseB}</App>);
174
+ });
175
+ });
176
+
177
+ assertLog([]);
178
+
179
+ expect(root).toMatchRenderedOutput(<div>Loading...Hi</div>);
180
+
181
+ await act(async () => {
182
+ await resolveB('Hello');
183
+ });
184
+
185
+ assertLog([]);
186
+
187
+ expect(root).toMatchRenderedOutput(<div>Hello</div>);
188
+ });
189
+
190
+ // @gate enableDefaultTransitionIndicator
191
+ it('does not trigger the default indicator if there is an isPending update', async () => {
192
+ const promiseA = Promise.resolve('Hi');
193
+ let resolveB;
194
+ const promiseB = new Promise(r => (resolveB = r));
195
+ let start;
196
+ function App({children}) {
197
+ const [isPending, startTransition] = useTransition();
198
+ start = startTransition;
199
+ return (
200
+ <div>
201
+ {isPending ? 'Loading...' : ''}
202
+ {children}
203
+ </div>
204
+ );
205
+ }
206
+
207
+ const root = ReactNoop.createRoot({
208
+ onDefaultTransitionIndicator() {
209
+ Scheduler.log('start');
210
+ return () => {
211
+ Scheduler.log('stop');
212
+ };
213
+ },
214
+ });
215
+ await act(() => {
216
+ React.startTransition(() => {
217
+ root.render(<App>{promiseA}</App>);
218
+ });
219
+ });
220
+
221
+ assertLog(['start', 'stop']);
222
+
223
+ expect(root).toMatchRenderedOutput(<div>Hi</div>);
224
+
225
+ await act(() => {
226
+ start(() => {
227
+ root.render(<App>{promiseB}</App>);
228
+ });
229
+ });
230
+
231
+ assertLog([]);
232
+
233
+ expect(root).toMatchRenderedOutput(<div>Loading...Hi</div>);
234
+
235
+ await act(async () => {
236
+ await resolveB('Hello');
237
+ });
238
+
239
+ assertLog([]);
240
+
241
+ expect(root).toMatchRenderedOutput(<div>Hello</div>);
242
+ });
243
+
244
+ // @gate enableDefaultTransitionIndicator
245
+ it('triggers the default indicator while an async transition is ongoing', async () => {
246
+ let resolve;
247
+ const promise = new Promise(r => (resolve = r));
248
+ let start;
249
+ function App() {
250
+ const [, startTransition] = useTransition();
251
+ start = startTransition;
252
+ return 'Hi';
253
+ }
254
+
255
+ const root = ReactNoop.createRoot({
256
+ onDefaultTransitionIndicator() {
257
+ Scheduler.log('start');
258
+ return () => {
259
+ Scheduler.log('stop');
260
+ };
261
+ },
262
+ });
263
+ await act(() => {
264
+ root.render(<App />);
265
+ });
266
+
267
+ assertLog([]);
268
+
269
+ await act(() => {
270
+ // Start an async action but we haven't called setState yet
271
+ // TODO: This should ideally work with React.startTransition too but we don't know the root.
272
+ start(() => promise);
273
+ });
274
+
275
+ assertLog(['start']);
276
+
277
+ await act(async () => {
278
+ await resolve('Hello');
279
+ });
280
+
281
+ assertLog(['stop']);
282
+
283
+ expect(root).toMatchRenderedOutput('Hi');
284
+ });
285
+
286
+ it('should not trigger for useDeferredValue (sync)', async () => {
287
+ function Text({text}) {
288
+ Scheduler.log(text);
289
+ return text;
290
+ }
291
+ function App({value}) {
292
+ const deferredValue = useDeferredValue(value, 'Hi');
293
+ return <Text text={deferredValue} />;
294
+ }
295
+
296
+ const root = ReactNoop.createRoot({
297
+ onDefaultTransitionIndicator() {
298
+ Scheduler.log('start');
299
+ return () => {
300
+ Scheduler.log('stop');
301
+ };
302
+ },
303
+ });
304
+ await act(async () => {
305
+ root.render(<App value="Hello" />);
306
+ await waitForPaint(['Hi']);
307
+ expect(root).toMatchRenderedOutput('Hi');
308
+ });
309
+
310
+ assertLog(['Hello']);
311
+
312
+ expect(root).toMatchRenderedOutput('Hello');
313
+
314
+ assertLog([]);
315
+
316
+ await act(async () => {
317
+ root.render(<App value="Bye" />);
318
+ await waitForPaint(['Hello']);
319
+ expect(root).toMatchRenderedOutput('Hello');
320
+ });
321
+
322
+ assertLog(['Bye']);
323
+
324
+ expect(root).toMatchRenderedOutput('Bye');
325
+ });
326
+
327
+ // @gate enableDefaultTransitionIndicator
328
+ it('should not trigger for useDeferredValue (transition)', async () => {
329
+ function Text({text}) {
330
+ Scheduler.log(text);
331
+ return text;
332
+ }
333
+ function App({value}) {
334
+ const deferredValue = useDeferredValue(value, 'Hi');
335
+ return <Text text={deferredValue} />;
336
+ }
337
+
338
+ const root = ReactNoop.createRoot({
339
+ onDefaultTransitionIndicator() {
340
+ Scheduler.log('start');
341
+ return () => {
342
+ Scheduler.log('stop');
343
+ };
344
+ },
345
+ });
346
+ await act(async () => {
347
+ React.startTransition(() => {
348
+ root.render(<App value="Hello" />);
349
+ });
350
+ await waitForPaint(['start', 'Hi', 'stop']);
351
+ expect(root).toMatchRenderedOutput('Hi');
352
+ });
353
+
354
+ assertLog(['Hello']);
355
+
356
+ expect(root).toMatchRenderedOutput('Hello');
357
+ });
358
+});