8
* @jest-environment node
9
*/
10
11
-runPlaceholderTests('ReactSuspensePlaceholder (mutation)', () =>
12
- require('react-noop-renderer'),
13
-);
14
-runPlaceholderTests('ReactSuspensePlaceholder (persistence)', () =>
15
- require('react-noop-renderer/persistent'),
16
-);
17
-
18
-function runPlaceholderTests(suiteLabel, loadReactNoop) {
19
- let Profiler;
20
- let React;
21
- let ReactNoop;
22
- let Scheduler;
23
- let ReactFeatureFlags;
24
- let ReactCache;
25
- let Suspense;
26
- let TextResource;
27
- let textResourceShouldFail;
28
-
29
- describe(suiteLabel, () => {
30
- beforeEach(() => {
31
- jest.resetModules();
32
-
33
- ReactFeatureFlags = require('shared/ReactFeatureFlags');
34
- ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
35
- ReactFeatureFlags.enableProfilerTimer = true;
36
- ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
37
- React = require('react');
38
- ReactNoop = loadReactNoop();
39
- Scheduler = require('scheduler');
40
- ReactCache = require('react-cache');
41
-
42
- Profiler = React.unstable_Profiler;
43
- Suspense = React.Suspense;
44
-
45
- TextResource = ReactCache.unstable_createResource(([text, ms = 0]) => {
46
- let listeners = null;
47
- let status = 'pending';
48
- let value = null;
49
- return {
50
- then(resolve, reject) {
51
- switch (status) {
52
- case 'pending': {
53
- if (listeners === null) {
54
- listeners = [{resolve, reject}];
55
- setTimeout(() => {
56
- if (textResourceShouldFail) {
57
- Scheduler.yieldValue(`Promise rejected [${text}]`);
58
- status = 'rejected';
59
- value = new Error('Failed to load: ' + text);
60
- listeners.forEach(listener => listener.reject(value));
61
- } else {
62
- Scheduler.yieldValue(`Promise resolved [${text}]`);
63
- status = 'resolved';
64
- value = text;
65
- listeners.forEach(listener => listener.resolve(value));
66
- }
67
- }, ms);
68
- } else {
69
- listeners.push({resolve, reject});
70
- }
71
- break;
72
- }
73
- case 'resolved': {
74
- resolve(value);
75
- break;
76
- }
77
- case 'rejected': {
78
- reject(value);
79
- break;
11
+let Profiler;
12
+let React;
13
+let ReactNoop;
14
+let Scheduler;
15
+let ReactFeatureFlags;
16
+let ReactCache;
17
+let Suspense;
18
+let TextResource;
19
+let textResourceShouldFail;
20
+
21
+describe('ReactSuspensePlaceholder', () => {
22
+ beforeEach(() => {
23
+ jest.resetModules();
24
+
25
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
26
+ ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
27
+ ReactFeatureFlags.enableProfilerTimer = true;
28
+ ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
29
+ React = require('react');
30
+ ReactNoop = require('react-noop-renderer');
31
+ Scheduler = require('scheduler');
32
+ ReactCache = require('react-cache');
33
+
34
+ Profiler = React.unstable_Profiler;
35
+ Suspense = React.Suspense;
36
+
37
+ TextResource = ReactCache.unstable_createResource(([text, ms = 0]) => {
38
+ let listeners = null;
39
+ let status = 'pending';
40
+ let value = null;
41
+ return {
42
+ then(resolve, reject) {
43
+ switch (status) {
44
+ case 'pending': {
45
+ if (listeners === null) {
46
+ listeners = [{resolve, reject}];
47
+ setTimeout(() => {
48
+ if (textResourceShouldFail) {
49
+ Scheduler.yieldValue(`Promise rejected [${text}]`);
50
+ status = 'rejected';
51
+ value = new Error('Failed to load: ' + text);
52
+ listeners.forEach(listener => listener.reject(value));
53
+ } else {
54
+ Scheduler.yieldValue(`Promise resolved [${text}]`);
55
+ status = 'resolved';
56
+ value = text;
57
+ listeners.forEach(listener => listener.resolve(value));
58
+ }
59
+ }, ms);
60
+ } else {
61
+ listeners.push({resolve, reject});
62
}
63
+ break;
64
}
82
- },
83
- };
84
- }, ([text, ms]) => text);
85
- textResourceShouldFail = false;
86
- });
65
+ case 'resolved': {
66
+ resolve(value);
67
+ break;
68
+ }
69
+ case 'rejected': {
70
+ reject(value);
71
+ break;
72
+ }
73
+ }
74
+ },
75
+ };
76
+ }, ([text, ms]) => text);
77
+ textResourceShouldFail = false;
78
+ });
79
88
- function Text({fakeRenderDuration = 0, text = 'Text'}) {
89
- Scheduler.advanceTime(fakeRenderDuration);
80
+ function Text({fakeRenderDuration = 0, text = 'Text'}) {
81
+ Scheduler.advanceTime(fakeRenderDuration);
82
+ Scheduler.yieldValue(text);
83
+ return text;
84
+ }
85
+
86
+ function AsyncText({fakeRenderDuration = 0, ms, text}) {
87
+ Scheduler.advanceTime(fakeRenderDuration);
88
+ try {
89
+ TextResource.read([text, ms]);
90
Scheduler.yieldValue(text);
91
return text;
92
+ } catch (promise) {
93
+ if (typeof promise.then === 'function') {
94
+ Scheduler.yieldValue(`Suspend! [${text}]`);
95
+ } else {
96
+ Scheduler.yieldValue(`Error! [${text}]`);
97
+ }
98
+ throw promise;
99
}
100
+ }
101
94
- function AsyncText({fakeRenderDuration = 0, ms, text}) {
95
- Scheduler.advanceTime(fakeRenderDuration);
96
- try {
97
- TextResource.read([text, ms]);
102
+ it('times out children that are already hidden', () => {
103
+ class HiddenText extends React.PureComponent {
104
+ render() {
105
+ const text = this.props.text;
106
Scheduler.yieldValue(text);
99
- return text;
100
- } catch (promise) {
101
- if (typeof promise.then === 'function') {
102
- Scheduler.yieldValue(`Suspend! [${text}]`);
103
- } else {
104
- Scheduler.yieldValue(`Error! [${text}]`);
105
- }
106
- throw promise;
107
+ return <span hidden={true}>{text}</span>;
108
}
109
}
110
110
- it('times out children that are already hidden', () => {
111
- class HiddenText extends React.PureComponent {
112
- render() {
113
- const text = this.props.text;
114
- Scheduler.yieldValue(text);
115
- return <span hidden={true}>{text}</span>;
116
- }
117
- }
118
-
119
- function App(props) {
120
- return (
121
- <Suspense maxDuration={500} fallback={<Text text="Loading..." />}>
122
- <HiddenText text="A" />
123
- <span>
124
- <AsyncText ms={1000} text={props.middleText} />
125
- </span>
126
- <span>
127
- <Text text="C" />
128
- </span>
129
- </Suspense>
130
- );
131
- }
132
-
133
- // Initial mount
134
- ReactNoop.render(<App middleText="B" />);
135
-
136
- expect(Scheduler).toFlushAndYield([
137
- 'A',
138
- 'Suspend! [B]',
139
- 'C',
140
- 'Loading...',
141
- ]);
142
- expect(ReactNoop).toMatchRenderedOutput(null);
143
-
144
- jest.advanceTimersByTime(1000);
145
- expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
146
-
147
- expect(Scheduler).toFlushAndYield(['A', 'B', 'C']);
148
-
149
- expect(ReactNoop).toMatchRenderedOutput(
150
- <React.Fragment>
151
- <span hidden={true}>A</span>
152
- <span>B</span>
153
- <span>C</span>
154
- </React.Fragment>,
111
+ function App(props) {
112
+ return (
113
+ <Suspense maxDuration={500} fallback={<Text text="Loading..." />}>
114
+ <HiddenText text="A" />
115
+ <span>
116
+ <AsyncText ms={1000} text={props.middleText} />
117
+ </span>
118
+ <span>
119
+ <Text text="C" />
120
+ </span>
121
+ </Suspense>
122
);
123
+ }
124
157
- // Update
158
- ReactNoop.render(<App middleText="B2" />);
159
- expect(Scheduler).toFlushAndYield(['Suspend! [B2]', 'C', 'Loading...']);
160
-
161
- // Time out the update
162
- jest.advanceTimersByTime(750);
163
- expect(Scheduler).toFlushAndYield([]);
164
- expect(ReactNoop).toMatchRenderedOutput(
165
- <React.Fragment>
166
- <span hidden={true}>A</span>
167
- <span hidden={true}>B</span>
168
- <span hidden={true}>C</span>
169
- Loading...
170
- </React.Fragment>,
171
- );
125
+ // Initial mount
126
+ ReactNoop.render(<App middleText="B" />);
127
+
128
+ expect(Scheduler).toFlushAndYield(['A', 'Suspend! [B]', 'C', 'Loading...']);
129
+ expect(ReactNoop).toMatchRenderedOutput(null);
130
+
131
+ jest.advanceTimersByTime(1000);
132
+ expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
133
+
134
+ expect(Scheduler).toFlushAndYield(['A', 'B', 'C']);
135
+
136
+ expect(ReactNoop).toMatchRenderedOutput(
137
+ <React.Fragment>
138
+ <span hidden={true}>A</span>
139
+ <span>B</span>
140
+ <span>C</span>
141
+ </React.Fragment>,
142
+ );
143
+
144
+ // Update
145
+ ReactNoop.render(<App middleText="B2" />);
146
+ expect(Scheduler).toFlushAndYield(['Suspend! [B2]', 'C', 'Loading...']);
147
+
148
+ // Time out the update
149
+ jest.advanceTimersByTime(750);
150
+ expect(Scheduler).toFlushAndYield([]);
151
+ expect(ReactNoop).toMatchRenderedOutput(
152
+ <React.Fragment>
153
+ <span hidden={true}>A</span>
154
+ <span hidden={true}>B</span>
155
+ <span hidden={true}>C</span>
156
+ Loading...
157
+ </React.Fragment>,
158
+ );
159
+
160
+ // Resolve the promise
161
+ jest.advanceTimersByTime(1000);
162
+ expect(Scheduler).toHaveYielded(['Promise resolved [B2]']);
163
+ expect(Scheduler).toFlushAndYield(['B2', 'C']);
164
+
165
+ // Render the final update. A should still be hidden, because it was
166
+ // given a `hidden` prop.
167
+ expect(ReactNoop).toMatchRenderedOutput(
168
+ <React.Fragment>
169
+ <span hidden={true}>A</span>
170
+ <span>B2</span>
171
+ <span>C</span>
172
+ </React.Fragment>,
173
+ );
174
+ });
175
173
- // Resolve the promise
174
- jest.advanceTimersByTime(1000);
175
- expect(Scheduler).toHaveYielded(['Promise resolved [B2]']);
176
- expect(Scheduler).toFlushAndYield(['B2', 'C']);
177
-
178
- // Render the final update. A should still be hidden, because it was
179
- // given a `hidden` prop.
180
- expect(ReactNoop).toMatchRenderedOutput(
181
- <React.Fragment>
182
- <span hidden={true}>A</span>
183
- <span>B2</span>
184
- <span>C</span>
185
- </React.Fragment>,
176
+ it('times out text nodes', async () => {
177
+ function App(props) {
178
+ return (
179
+ <Suspense maxDuration={500} fallback={<Text text="Loading..." />}>
180
+ <Text text="A" />
181
+ <AsyncText ms={1000} text={props.middleText} />
182
+ <Text text="C" />
183
+ </Suspense>
184
);
187
- });
185
+ }
186
189
- it('times out text nodes', async () => {
190
- function App(props) {
191
- return (
187
+ // Initial mount
188
+ ReactNoop.render(<App middleText="B" />);
189
+
190
+ expect(Scheduler).toFlushAndYield(['A', 'Suspend! [B]', 'C', 'Loading...']);
191
+
192
+ expect(ReactNoop).toMatchRenderedOutput(null);
193
+
194
+ jest.advanceTimersByTime(1000);
195
+ expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
196
+ expect(Scheduler).toFlushAndYield(['A', 'B', 'C']);
197
+ expect(ReactNoop).toMatchRenderedOutput('ABC');
198
+
199
+ // Update
200
+ ReactNoop.render(<App middleText="B2" />);
201
+ expect(Scheduler).toFlushAndYield([
202
+ 'A',
203
+ 'Suspend! [B2]',
204
+ 'C',
205
+ 'Loading...',
206
+ ]);
207
+ // Time out the update
208
+ jest.advanceTimersByTime(750);
209
+ expect(Scheduler).toFlushAndYield([]);
210
+ expect(ReactNoop).toMatchRenderedOutput('Loading...');
211
+
212
+ // Resolve the promise
213
+ jest.advanceTimersByTime(1000);
214
+ expect(Scheduler).toHaveYielded(['Promise resolved [B2]']);
215
+ expect(Scheduler).toFlushAndYield(['A', 'B2', 'C']);
216
+
217
+ // Render the final update. A should still be hidden, because it was
218
+ // given a `hidden` prop.
219
+ expect(ReactNoop).toMatchRenderedOutput('AB2C');
220
+ });
221
+
222
+ it('preserves host context for text nodes', () => {
223
+ function App(props) {
224
+ return (
225
+ // uppercase is a special type that causes React Noop to render child
226
+ // text nodes as uppercase.
227
+ <uppercase>
228
<Suspense maxDuration={500} fallback={<Text text="Loading..." />}>
193
- <Text text="A" />
229
+ <Text text="a" />
230
<AsyncText ms={1000} text={props.middleText} />
195
- <Text text="C" />
231
+ <Text text="c" />
232
</Suspense>
197
- );
198
- }
233
+ </uppercase>
234
+ );
235
+ }
236
200
- // Initial mount
201
- ReactNoop.render(<App middleText="B" />);
202
-
203
- expect(Scheduler).toFlushAndYield([
204
- 'A',
205
- 'Suspend! [B]',
206
- 'C',
207
- 'Loading...',
208
- ]);
209
-
210
- expect(ReactNoop).toMatchRenderedOutput(null);
211
-
212
- jest.advanceTimersByTime(1000);
213
- expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
214
- expect(Scheduler).toFlushAndYield(['A', 'B', 'C']);
215
- expect(ReactNoop).toMatchRenderedOutput('ABC');
216
-
217
- // Update
218
- ReactNoop.render(<App middleText="B2" />);
219
- expect(Scheduler).toFlushAndYield([
220
- 'A',
221
- 'Suspend! [B2]',
222
- 'C',
223
- 'Loading...',
224
- ]);
225
- // Time out the update
226
- jest.advanceTimersByTime(750);
227
- expect(Scheduler).toFlushAndYield([]);
228
- expect(ReactNoop).toMatchRenderedOutput('Loading...');
229
-
230
- // Resolve the promise
231
- jest.advanceTimersByTime(1000);
232
- expect(Scheduler).toHaveYielded(['Promise resolved [B2]']);
233
- expect(Scheduler).toFlushAndYield(['A', 'B2', 'C']);
234
-
235
- // Render the final update. A should still be hidden, because it was
236
- // given a `hidden` prop.
237
- expect(ReactNoop).toMatchRenderedOutput('AB2C');
238
- });
237
+ // Initial mount
238
+ ReactNoop.render(<App middleText="b" />);
239
+
240
+ expect(Scheduler).toFlushAndYield(['a', 'Suspend! [b]', 'c', 'Loading...']);
241
+
242
+ expect(ReactNoop).toMatchRenderedOutput(null);
243
+
244
+ jest.advanceTimersByTime(1000);
245
+ expect(Scheduler).toHaveYielded(['Promise resolved [b]']);
246
+ expect(Scheduler).toFlushAndYield(['a', 'b', 'c']);
247
+ expect(ReactNoop).toMatchRenderedOutput(<uppercase>ABC</uppercase>);
248
+
249
+ // Update
250
+ ReactNoop.render(<App middleText="b2" />);
251
+ expect(Scheduler).toFlushAndYield([
252
+ 'a',
253
+ 'Suspend! [b2]',
254
+ 'c',
255
+ 'Loading...',
256
+ ]);
257
+ // Time out the update
258
+ jest.advanceTimersByTime(750);
259
+ expect(Scheduler).toFlushAndYield([]);
260
+ expect(ReactNoop).toMatchRenderedOutput(<uppercase>LOADING...</uppercase>);
261
+
262
+ // Resolve the promise
263
+ jest.advanceTimersByTime(1000);
264
+ expect(Scheduler).toHaveYielded(['Promise resolved [b2]']);
265
+ expect(Scheduler).toFlushAndYield(['a', 'b2', 'c']);
266
+
267
+ // Render the final update. A should still be hidden, because it was
268
+ // given a `hidden` prop.
269
+ expect(ReactNoop).toMatchRenderedOutput(<uppercase>AB2C</uppercase>);
270
+ });
271
+
272
+ describe('profiler durations', () => {
273
+ let App;
274
+ let onRender;
275
240
- it('preserves host context for text nodes', () => {
241
- function App(props) {
276
+ beforeEach(() => {
277
+ // Order of parameters: id, phase, actualDuration, treeBaseDuration
278
+ onRender = jest.fn();
279
+
280
+ const Fallback = () => {
281
+ Scheduler.yieldValue('Fallback');
282
+ Scheduler.advanceTime(10);
283
+ return 'Loading...';
284
+ };
285
+
286
+ const Suspending = () => {
287
+ Scheduler.yieldValue('Suspending');
288
+ Scheduler.advanceTime(2);
289
+ return <AsyncText ms={1000} text="Loaded" fakeRenderDuration={1} />;
290
+ };
291
+
292
+ App = ({shouldSuspend, text = 'Text', textRenderDuration = 5}) => {
293
+ Scheduler.yieldValue('App');
294
return (
243
- // uppercase is a special type that causes React Noop to render child
244
- // text nodes as uppercase.
245
- <uppercase>
246
- <Suspense maxDuration={500} fallback={<Text text="Loading..." />}>
247
- <Text text="a" />
248
- <AsyncText ms={1000} text={props.middleText} />
249
- <Text text="c" />
295
+ <Profiler id="root" onRender={onRender}>
296
+ <Suspense maxDuration={500} fallback={<Fallback />}>
297
+ {shouldSuspend && <Suspending />}
298
+ <Text fakeRenderDuration={textRenderDuration} text={text} />
299
</Suspense>
251
- </uppercase>
300
+ </Profiler>
301
);
253
- }
254
-
255
- // Initial mount
256
- ReactNoop.render(<App middleText="b" />);
257
-
258
- expect(Scheduler).toFlushAndYield([
259
- 'a',
260
- 'Suspend! [b]',
261
- 'c',
262
- 'Loading...',
263
- ]);
264
-
265
- expect(ReactNoop).toMatchRenderedOutput(null);
266
-
267
- jest.advanceTimersByTime(1000);
268
- expect(Scheduler).toHaveYielded(['Promise resolved [b]']);
269
- expect(Scheduler).toFlushAndYield(['a', 'b', 'c']);
270
- expect(ReactNoop).toMatchRenderedOutput(<uppercase>ABC</uppercase>);
271
-
272
- // Update
273
- ReactNoop.render(<App middleText="b2" />);
274
- expect(Scheduler).toFlushAndYield([
275
- 'a',
276
- 'Suspend! [b2]',
277
- 'c',
278
- 'Loading...',
279
- ]);
280
- // Time out the update
281
- jest.advanceTimersByTime(750);
282
- expect(Scheduler).toFlushAndYield([]);
283
- expect(ReactNoop).toMatchRenderedOutput(
284
- <uppercase>LOADING...</uppercase>,
285
- );
286
-
287
- // Resolve the promise
288
- jest.advanceTimersByTime(1000);
289
- expect(Scheduler).toHaveYielded(['Promise resolved [b2]']);
290
- expect(Scheduler).toFlushAndYield(['a', 'b2', 'c']);
291
-
292
- // Render the final update. A should still be hidden, because it was
293
- // given a `hidden` prop.
294
- expect(ReactNoop).toMatchRenderedOutput(<uppercase>AB2C</uppercase>);
302
+ };
303
});
304
297
- describe('profiler durations', () => {
298
- let App;
299
- let onRender;
300
-
301
- beforeEach(() => {
302
- // Order of parameters: id, phase, actualDuration, treeBaseDuration
303
- onRender = jest.fn();
304
-
305
- const Fallback = () => {
306
- Scheduler.yieldValue('Fallback');
307
- Scheduler.advanceTime(10);
308
- return 'Loading...';
309
- };
310
-
311
- const Suspending = () => {
312
- Scheduler.yieldValue('Suspending');
313
- Scheduler.advanceTime(2);
314
- return <AsyncText ms={1000} text="Loaded" fakeRenderDuration={1} />;
315
- };
316
-
317
- App = ({shouldSuspend, text = 'Text', textRenderDuration = 5}) => {
318
- Scheduler.yieldValue('App');
319
- return (
320
- <Profiler id="root" onRender={onRender}>
321
- <Suspense maxDuration={500} fallback={<Fallback />}>
322
- {shouldSuspend && <Suspending />}
323
- <Text fakeRenderDuration={textRenderDuration} text={text} />
324
- </Suspense>
325
- </Profiler>
326
- );
327
- };
305
+ describe('when suspending during mount', () => {
306
+ it('properly accounts for base durations when a suspended times out in a sync tree', () => {
307
+ ReactNoop.renderLegacySyncRoot(<App shouldSuspend={true} />);
308
+ expect(Scheduler).toHaveYielded([
309
+ 'App',
310
+ 'Suspending',
311
+ 'Suspend! [Loaded]',
312
+ 'Text',
313
+ 'Fallback',
314
+ ]);
315
+ expect(ReactNoop).toMatchRenderedOutput('Loading...');
316
+ expect(onRender).toHaveBeenCalledTimes(1);
317
+
318
+ // Initial mount only shows the "Loading..." Fallback.
319
+ // The treeBaseDuration then should be 10ms spent rendering Fallback,
320
+ // but the actualDuration should also include the 8ms spent rendering the hidden tree.
321
+ expect(onRender.mock.calls[0][2]).toBe(18);
322
+ expect(onRender.mock.calls[0][3]).toBe(10);
323
+
324
+ jest.advanceTimersByTime(1000);
325
+
326
+ expect(Scheduler).toHaveYielded([
327
+ 'Promise resolved [Loaded]',
328
+ 'Loaded',
329
+ ]);
330
+ expect(ReactNoop).toMatchRenderedOutput('LoadedText');
331
+ expect(onRender).toHaveBeenCalledTimes(2);
332
+
333
+ // When the suspending data is resolved and our final UI is rendered,
334
+ // the baseDuration should only include the 1ms re-rendering AsyncText,
335
+ // but the treeBaseDuration should include the full 8ms spent in the tree.
336
+ expect(onRender.mock.calls[1][2]).toBe(1);
337
+ expect(onRender.mock.calls[1][3]).toBe(8);
338
});
339
330
- describe('when suspending during mount', () => {
331
- it('properly accounts for base durations when a suspended times out in a sync tree', () => {
332
- ReactNoop.renderLegacySyncRoot(<App shouldSuspend={true} />);
333
- expect(Scheduler).toHaveYielded([
334
- 'App',
335
- 'Suspending',
336
- 'Suspend! [Loaded]',
337
- 'Text',
338
- 'Fallback',
339
- ]);
340
- expect(ReactNoop).toMatchRenderedOutput('Loading...');
341
- expect(onRender).toHaveBeenCalledTimes(1);
342
-
343
- // Initial mount only shows the "Loading..." Fallback.
344
- // The treeBaseDuration then should be 10ms spent rendering Fallback,
345
- // but the actualDuration should also include the 8ms spent rendering the hidden tree.
346
- expect(onRender.mock.calls[0][2]).toBe(18);
347
- expect(onRender.mock.calls[0][3]).toBe(10);
348
-
349
- jest.advanceTimersByTime(1000);
350
-
351
- expect(Scheduler).toHaveYielded([
352
- 'Promise resolved [Loaded]',
353
- 'Loaded',
354
- ]);
355
- expect(ReactNoop).toMatchRenderedOutput('LoadedText');
356
- expect(onRender).toHaveBeenCalledTimes(2);
357
-
358
- // When the suspending data is resolved and our final UI is rendered,
359
- // the baseDuration should only include the 1ms re-rendering AsyncText,
360
- // but the treeBaseDuration should include the full 8ms spent in the tree.
361
- expect(onRender.mock.calls[1][2]).toBe(1);
362
- expect(onRender.mock.calls[1][3]).toBe(8);
363
- });
364
-
365
- it('properly accounts for base durations when a suspended times out in a concurrent tree', () => {
366
- ReactNoop.render(<App shouldSuspend={true} />);
367
-
368
- expect(Scheduler).toFlushAndYield([
369
- 'App',
370
- 'Suspending',
371
- 'Suspend! [Loaded]',
372
- 'Text',
373
- 'Fallback',
374
- ]);
375
- expect(ReactNoop).toMatchRenderedOutput(null);
376
-
377
- // Show the fallback UI.
378
- jest.advanceTimersByTime(750);
379
- expect(ReactNoop).toMatchRenderedOutput('Loading...');
380
- expect(onRender).toHaveBeenCalledTimes(1);
381
-
382
- // Initial mount only shows the "Loading..." Fallback.
383
- // The treeBaseDuration then should be 10ms spent rendering Fallback,
384
- // but the actualDuration should also include the 8ms spent rendering the hidden tree.
385
- expect(onRender.mock.calls[0][2]).toBe(18);
386
- expect(onRender.mock.calls[0][3]).toBe(10);
387
-
388
- // Resolve the pending promise.
389
- jest.advanceTimersByTime(250);
390
- expect(Scheduler).toHaveYielded(['Promise resolved [Loaded]']);
391
- expect(Scheduler).toFlushAndYield(['Suspending', 'Loaded', 'Text']);
392
- expect(ReactNoop).toMatchRenderedOutput('LoadedText');
393
- expect(onRender).toHaveBeenCalledTimes(2);
394
-
395
- // When the suspending data is resolved and our final UI is rendered,
396
- // both times should include the 8ms re-rendering Suspending and AsyncText.
397
- expect(onRender.mock.calls[1][2]).toBe(8);
398
- expect(onRender.mock.calls[1][3]).toBe(8);
399
- });
340
+ it('properly accounts for base durations when a suspended times out in a concurrent tree', () => {
341
+ ReactNoop.render(<App shouldSuspend={true} />);
342
+
343
+ expect(Scheduler).toFlushAndYield([
344
+ 'App',
345
+ 'Suspending',
346
+ 'Suspend! [Loaded]',
347
+ 'Text',
348
+ 'Fallback',
349
+ ]);
350
+ expect(ReactNoop).toMatchRenderedOutput(null);
351
+
352
+ // Show the fallback UI.
353
+ jest.advanceTimersByTime(750);
354
+ expect(ReactNoop).toMatchRenderedOutput('Loading...');
355
+ expect(onRender).toHaveBeenCalledTimes(1);
356
+
357
+ // Initial mount only shows the "Loading..." Fallback.
358
+ // The treeBaseDuration then should be 10ms spent rendering Fallback,
359
+ // but the actualDuration should also include the 8ms spent rendering the hidden tree.
360
+ expect(onRender.mock.calls[0][2]).toBe(18);
361
+ expect(onRender.mock.calls[0][3]).toBe(10);
362
+
363
+ // Resolve the pending promise.
364
+ jest.advanceTimersByTime(250);
365
+ expect(Scheduler).toHaveYielded(['Promise resolved [Loaded]']);
366
+ expect(Scheduler).toFlushAndYield(['Suspending', 'Loaded', 'Text']);
367
+ expect(ReactNoop).toMatchRenderedOutput('LoadedText');
368
+ expect(onRender).toHaveBeenCalledTimes(2);
369
+
370
+ // When the suspending data is resolved and our final UI is rendered,
371
+ // both times should include the 8ms re-rendering Suspending and AsyncText.
372
+ expect(onRender.mock.calls[1][2]).toBe(8);
373
+ expect(onRender.mock.calls[1][3]).toBe(8);
374
+ });
375
+ });
376
+
377
+ describe('when suspending during update', () => {
378
+ it('properly accounts for base durations when a suspended times out in a sync tree', () => {
379
+ ReactNoop.renderLegacySyncRoot(
380
+ <App shouldSuspend={false} textRenderDuration={5} />,
381
+ );
382
+ expect(Scheduler).toHaveYielded(['App', 'Text']);
383
+ expect(ReactNoop).toMatchRenderedOutput('Text');
384
+ expect(onRender).toHaveBeenCalledTimes(1);
385
+
386
+ // Initial mount only shows the "Text" text.
387
+ // It should take 5ms to render.
388
+ expect(onRender.mock.calls[0][2]).toBe(5);
389
+ expect(onRender.mock.calls[0][3]).toBe(5);
390
+
391
+ ReactNoop.render(<App shouldSuspend={true} textRenderDuration={5} />);
392
+ expect(Scheduler).toHaveYielded([
393
+ 'App',
394
+ 'Suspending',
395
+ 'Suspend! [Loaded]',
396
+ 'Text',
397
+ 'Fallback',
398
+ ]);
399
+ expect(ReactNoop).toMatchRenderedOutput('Loading...');
400
+ expect(onRender).toHaveBeenCalledTimes(2);
401
+
402
+ // The suspense update should only show the "Loading..." Fallback.
403
+ // Both durations should include 10ms spent rendering Fallback
404
+ // plus the 8ms rendering the (hidden) components.
405
+ expect(onRender.mock.calls[1][2]).toBe(18);
406
+ expect(onRender.mock.calls[1][3]).toBe(18);
407
+
408
+ ReactNoop.renderLegacySyncRoot(
409
+ <App shouldSuspend={true} text="New" textRenderDuration={6} />,
410
+ );
411
+ expect(Scheduler).toHaveYielded([
412
+ 'App',
413
+ 'Suspending',
414
+ 'Suspend! [Loaded]',
415
+ 'New',
416
+ 'Fallback',
417
+ ]);
418
+ expect(ReactNoop).toMatchRenderedOutput('Loading...');
419
+ expect(onRender).toHaveBeenCalledTimes(3);
420
+
421
+ // If we force another update while still timed out,
422
+ // but this time the Text component took 1ms longer to render.
423
+ // This should impact both actualDuration and treeBaseDuration.
424
+ expect(onRender.mock.calls[2][2]).toBe(19);
425
+ expect(onRender.mock.calls[2][3]).toBe(19);
426
+
427
+ jest.advanceTimersByTime(1000);
428
+
429
+ expect(Scheduler).toHaveYielded([
430
+ 'Promise resolved [Loaded]',
431
+ 'Loaded',
432
+ ]);
433
+ expect(ReactNoop).toMatchRenderedOutput('LoadedNew');
434
+ expect(onRender).toHaveBeenCalledTimes(4);
435
+
436
+ // When the suspending data is resolved and our final UI is rendered,
437
+ // the baseDuration should only include the 1ms re-rendering AsyncText,
438
+ // but the treeBaseDuration should include the full 9ms spent in the tree.
439
+ expect(onRender.mock.calls[3][2]).toBe(1);
440
+ expect(onRender.mock.calls[3][3]).toBe(9);
441
});
442
402
- describe('when suspending during update', () => {
403
- it('properly accounts for base durations when a suspended times out in a sync tree', () => {
404
- ReactNoop.renderLegacySyncRoot(
405
- <App shouldSuspend={false} textRenderDuration={5} />,
406
- );
407
- expect(Scheduler).toHaveYielded(['App', 'Text']);
408
- expect(ReactNoop).toMatchRenderedOutput('Text');
409
- expect(onRender).toHaveBeenCalledTimes(1);
410
-
411
- // Initial mount only shows the "Text" text.
412
- // It should take 5ms to render.
413
- expect(onRender.mock.calls[0][2]).toBe(5);
414
- expect(onRender.mock.calls[0][3]).toBe(5);
415
-
416
- ReactNoop.render(<App shouldSuspend={true} textRenderDuration={5} />);
417
- expect(Scheduler).toHaveYielded([
418
- 'App',
419
- 'Suspending',
420
- 'Suspend! [Loaded]',
421
- 'Text',
422
- 'Fallback',
423
- ]);
424
- expect(ReactNoop).toMatchRenderedOutput('Loading...');
425
- expect(onRender).toHaveBeenCalledTimes(2);
426
-
427
- // The suspense update should only show the "Loading..." Fallback.
428
- // Both durations should include 10ms spent rendering Fallback
429
- // plus the 8ms rendering the (hidden) components.
430
- expect(onRender.mock.calls[1][2]).toBe(18);
431
- expect(onRender.mock.calls[1][3]).toBe(18);
432
-
433
- ReactNoop.renderLegacySyncRoot(
434
- <App shouldSuspend={true} text="New" textRenderDuration={6} />,
435
- );
436
- expect(Scheduler).toHaveYielded([
437
- 'App',
438
- 'Suspending',
439
- 'Suspend! [Loaded]',
440
- 'New',
441
- 'Fallback',
442
- ]);
443
- expect(ReactNoop).toMatchRenderedOutput('Loading...');
444
- expect(onRender).toHaveBeenCalledTimes(3);
445
-
446
- // If we force another update while still timed out,
447
- // but this time the Text component took 1ms longer to render.
448
- // This should impact both actualDuration and treeBaseDuration.
449
- expect(onRender.mock.calls[2][2]).toBe(19);
450
- expect(onRender.mock.calls[2][3]).toBe(19);
451
-
452
- jest.advanceTimersByTime(1000);
453
-
454
- expect(Scheduler).toHaveYielded([
455
- 'Promise resolved [Loaded]',
456
- 'Loaded',
457
- ]);
458
- expect(ReactNoop).toMatchRenderedOutput('LoadedNew');
459
- expect(onRender).toHaveBeenCalledTimes(4);
460
-
461
- // When the suspending data is resolved and our final UI is rendered,
462
- // the baseDuration should only include the 1ms re-rendering AsyncText,
463
- // but the treeBaseDuration should include the full 9ms spent in the tree.
464
- expect(onRender.mock.calls[3][2]).toBe(1);
465
- expect(onRender.mock.calls[3][3]).toBe(9);
466
- });
467
-
468
- it('properly accounts for base durations when a suspended times out in a concurrent tree', () => {
469
- ReactNoop.render(
470
- <App shouldSuspend={false} textRenderDuration={5} />,
471
- );
472
-
473
- expect(Scheduler).toFlushAndYield(['App', 'Text']);
474
- expect(ReactNoop).toMatchRenderedOutput('Text');
475
- expect(onRender).toHaveBeenCalledTimes(1);
476
-
477
- // Initial mount only shows the "Text" text.
478
- // It should take 5ms to render.
479
- expect(onRender.mock.calls[0][2]).toBe(5);
480
- expect(onRender.mock.calls[0][3]).toBe(5);
481
-
482
- ReactNoop.render(<App shouldSuspend={true} textRenderDuration={5} />);
483
- expect(Scheduler).toFlushAndYield([
484
- 'App',
485
- 'Suspending',
486
- 'Suspend! [Loaded]',
487
- 'Text',
488
- 'Fallback',
489
- ]);
490
- expect(ReactNoop).toMatchRenderedOutput('Text');
491
-
492
- // Show the fallback UI.
493
- jest.advanceTimersByTime(750);
494
- expect(ReactNoop).toMatchRenderedOutput('Loading...');
495
- expect(onRender).toHaveBeenCalledTimes(2);
496
-
497
- // The suspense update should only show the "Loading..." Fallback.
498
- // The actual duration should include 10ms spent rendering Fallback,
499
- // plus the 8ms render all of the hidden, suspended subtree.
500
- // But the tree base duration should only include 10ms spent rendering Fallback,
501
- // plus the 5ms rendering the previously committed version of the hidden tree.
502
- expect(onRender.mock.calls[1][2]).toBe(18);
503
- expect(onRender.mock.calls[1][3]).toBe(15);
504
-
505
- // Update again while timed out.
506
- ReactNoop.render(
507
- <App shouldSuspend={true} text="New" textRenderDuration={6} />,
508
- );
509
- expect(Scheduler).toFlushAndYield([
510
- 'App',
511
- 'Suspending',
512
- 'Suspend! [Loaded]',
513
- 'New',
514
- 'Fallback',
515
- ]);
516
- expect(ReactNoop).toMatchRenderedOutput('Loading...');
517
- expect(onRender).toHaveBeenCalledTimes(2);
518
-
519
- // Resolve the pending promise.
520
- jest.advanceTimersByTime(250);
521
- expect(Scheduler).toHaveYielded(['Promise resolved [Loaded]']);
522
- expect(Scheduler).toFlushAndYield([
523
- 'App',
524
- 'Suspending',
525
- 'Loaded',
526
- 'New',
527
- ]);
528
- expect(onRender).toHaveBeenCalledTimes(3);
529
-
530
- // When the suspending data is resolved and our final UI is rendered,
531
- // both times should include the 6ms rendering Text,
532
- // the 2ms rendering Suspending, and the 1ms rendering AsyncText.
533
- expect(onRender.mock.calls[2][2]).toBe(9);
534
- expect(onRender.mock.calls[2][3]).toBe(9);
535
- });
443
+ it('properly accounts for base durations when a suspended times out in a concurrent tree', () => {
444
+ ReactNoop.render(<App shouldSuspend={false} textRenderDuration={5} />);
445
+
446
+ expect(Scheduler).toFlushAndYield(['App', 'Text']);
447
+ expect(ReactNoop).toMatchRenderedOutput('Text');
448
+ expect(onRender).toHaveBeenCalledTimes(1);
449
+
450
+ // Initial mount only shows the "Text" text.
451
+ // It should take 5ms to render.
452
+ expect(onRender.mock.calls[0][2]).toBe(5);
453
+ expect(onRender.mock.calls[0][3]).toBe(5);
454
+
455
+ ReactNoop.render(<App shouldSuspend={true} textRenderDuration={5} />);
456
+ expect(Scheduler).toFlushAndYield([
457
+ 'App',
458
+ 'Suspending',
459
+ 'Suspend! [Loaded]',
460
+ 'Text',
461
+ 'Fallback',
462
+ ]);
463
+ expect(ReactNoop).toMatchRenderedOutput('Text');
464
+
465
+ // Show the fallback UI.
466
+ jest.advanceTimersByTime(750);
467
+ expect(ReactNoop).toMatchRenderedOutput('Loading...');
468
+ expect(onRender).toHaveBeenCalledTimes(2);
469
+
470
+ // The suspense update should only show the "Loading..." Fallback.
471
+ // The actual duration should include 10ms spent rendering Fallback,
472
+ // plus the 8ms render all of the hidden, suspended subtree.
473
+ // But the tree base duration should only include 10ms spent rendering Fallback,
474
+ // plus the 5ms rendering the previously committed version of the hidden tree.
475
+ expect(onRender.mock.calls[1][2]).toBe(18);
476
+ expect(onRender.mock.calls[1][3]).toBe(15);
477
+
478
+ // Update again while timed out.
479
+ ReactNoop.render(
480
+ <App shouldSuspend={true} text="New" textRenderDuration={6} />,
481
+ );
482
+ expect(Scheduler).toFlushAndYield([
483
+ 'App',
484
+ 'Suspending',
485
+ 'Suspend! [Loaded]',
486
+ 'New',
487
+ 'Fallback',
488
+ ]);
489
+ expect(ReactNoop).toMatchRenderedOutput('Loading...');
490
+ expect(onRender).toHaveBeenCalledTimes(2);
491
+
492
+ // Resolve the pending promise.
493
+ jest.advanceTimersByTime(250);
494
+ expect(Scheduler).toHaveYielded(['Promise resolved [Loaded]']);
495
+ expect(Scheduler).toFlushAndYield([
496
+ 'App',
497
+ 'Suspending',
498
+ 'Loaded',
499
+ 'New',
500
+ ]);
501
+ expect(onRender).toHaveBeenCalledTimes(3);
502
+
503
+ // When the suspending data is resolved and our final UI is rendered,
504
+ // both times should include the 6ms rendering Text,
505
+ // the 2ms rendering Suspending, and the 1ms rendering AsyncText.
506
+ expect(onRender.mock.calls[2][2]).toBe(9);
507
+ expect(onRender.mock.calls[2][3]).toBe(9);
508
});
509
});
510
});
539
-}
511
+});