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 InteractionTracking;
14
+let InteractionTrackingSubscriptions;
15
let React;
16
let ReactFeatureFlags;
17
let ReactNoop;
18
let ReactTestRenderer;
19
+let advanceTimeBy;
20
+let mockNow;
21
+let AdvanceTime;
22
23
function loadModules({
24
enableProfilerTimer = true,
25
+ enableSuspense = false,
26
+ enableInteractionTracking = true,
27
replayFailedUnitOfWorkWithInvokeGuardedCallback = false,
28
useNoopRenderer = false,
29
} = {}) {
30
+ let currentTime = 0;
31
+
32
+ mockNow = jest.fn().mockImplementation(() => currentTime);
33
+
34
+ global.Date.now = mockNow;
35
+
36
ReactFeatureFlags = require('shared/ReactFeatureFlags');
37
ReactFeatureFlags.debugRenderPhaseSideEffects = false;
38
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
39
ReactFeatureFlags.enableProfilerTimer = enableProfilerTimer;
40
ReactFeatureFlags.enableGetDerivedStateFromCatch = true;
41
+ ReactFeatureFlags.enableInteractionTracking = enableInteractionTracking;
42
+ ReactFeatureFlags.enableSuspense = enableSuspense;
43
ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = replayFailedUnitOfWorkWithInvokeGuardedCallback;
44
+
45
+ InteractionTracking = require('interaction-tracking');
46
+ InteractionTrackingSubscriptions = require('interaction-tracking/subscriptions');
47
React = require('react');
48
49
if (useNoopRenderer) {
50
ReactNoop = require('react-noop-renderer');
51
} else {
52
ReactTestRenderer = require('react-test-renderer');
53
+ ReactTestRenderer.unstable_setNowImplementation(mockNow);
54
}
55
+
56
+ advanceTimeBy = amount => {
57
+ currentTime += amount;
58
+ };
59
+
60
+ AdvanceTime = class extends React.Component {
61
+ static defaultProps = {
62
+ byAmount: 10,
63
+ shouldComponentUpdate: true,
64
+ };
65
+ shouldComponentUpdate(nextProps) {
66
+ return nextProps.shouldComponentUpdate;
67
+ }
68
+ render() {
69
+ // Simulate time passing when this component is rendered
70
+ advanceTimeBy(this.props.byAmount);
71
+ return this.props.children || null;
72
+ }
73
+ };
74
}
75
76
const mockDevToolsForTest = () => {
84
85
describe('Profiler', () => {
86
describe('works in profiling and non-profiling bundles', () => {
48
- [true, false].forEach(flagEnabled => {
49
- describe(`enableProfilerTimer ${
50
- flagEnabled ? 'enabled' : 'disabled'
51
- }`, () => {
52
- beforeEach(() => {
53
- jest.resetModules();
54
-
55
- loadModules({enableProfilerTimer: flagEnabled});
56
- });
87
+ [true, false].forEach(enableInteractionTracking => {
88
+ [true, false].forEach(enableProfilerTimer => {
89
+ describe(`enableInteractionTracking:${
90
+ enableInteractionTracking ? 'enabled' : 'disabled'
91
+ } enableProfilerTimer:${
92
+ enableProfilerTimer ? 'enabled' : 'disabled'
93
+ }`, () => {
94
+ beforeEach(() => {
95
+ jest.resetModules();
96
58
- // This will throw in production too,
59
- // But the test is only interested in verifying the DEV error message.
60
- if (__DEV__ && flagEnabled) {
61
- it('should warn if required params are missing', () => {
62
- expect(() => {
63
- ReactTestRenderer.create(<React.unstable_Profiler />);
64
- }).toThrow(
65
- 'Profiler must specify an "id" string and "onRender" function as props',
66
- );
97
+ loadModules({enableInteractionTracking, enableProfilerTimer});
98
+ });
99
+
100
+ // This will throw in production too,
101
+ // But the test is only interested in verifying the DEV error message.
102
+ if (__DEV__ && enableProfilerTimer) {
103
+ it('should warn if required params are missing', () => {
104
+ expect(() => {
105
+ expect(() => {
106
+ ReactTestRenderer.create(<React.unstable_Profiler />);
107
+ }).toThrow('onRender is not a function');
108
+ }).toWarnDev(
109
+ 'Profiler must specify an "id" string and "onRender" function as props',
110
+ {withoutStack: true},
111
+ );
112
+ });
113
+ }
114
+
115
+ it('should support an empty Profiler (with no children)', () => {
116
+ // As root
117
+ expect(
118
+ ReactTestRenderer.create(
119
+ <React.unstable_Profiler id="label" onRender={jest.fn()} />,
120
+ ).toJSON(),
121
+ ).toMatchSnapshot();
122
+
123
+ // As non-root
124
+ expect(
125
+ ReactTestRenderer.create(
126
+ <div>
127
+ <React.unstable_Profiler id="label" onRender={jest.fn()} />
128
+ </div>,
129
+ ).toJSON(),
130
+ ).toMatchSnapshot();
131
});
68
- }
132
70
- it('should support an empty Profiler (with no children)', () => {
71
- // As root
72
- expect(
73
- ReactTestRenderer.create(
74
- <React.unstable_Profiler id="label" onRender={jest.fn()} />,
75
- ).toJSON(),
76
- ).toMatchSnapshot();
77
-
78
- // As non-root
79
- expect(
80
- ReactTestRenderer.create(
133
+ it('should render children', () => {
134
+ const FunctionalComponent = ({label}) => <span>{label}</span>;
135
+ const renderer = ReactTestRenderer.create(
136
<div>
82
- <React.unstable_Profiler id="label" onRender={jest.fn()} />
137
+ <span>outside span</span>
138
+ <React.unstable_Profiler id="label" onRender={jest.fn()}>
139
+ <span>inside span</span>
140
+ <FunctionalComponent label="functional component" />
141
+ </React.unstable_Profiler>
142
</div>,
84
- ).toJSON(),
85
- ).toMatchSnapshot();
86
- });
87
-
88
- it('should render children', () => {
89
- const FunctionalComponent = ({label}) => <span>{label}</span>;
90
- const renderer = ReactTestRenderer.create(
91
- <div>
92
- <span>outside span</span>
93
- <React.unstable_Profiler id="label" onRender={jest.fn()}>
94
- <span>inside span</span>
95
- <FunctionalComponent label="functional component" />
96
- </React.unstable_Profiler>
97
- </div>,
98
- );
99
- expect(renderer.toJSON()).toMatchSnapshot();
100
- });
143
+ );
144
+ expect(renderer.toJSON()).toMatchSnapshot();
145
+ });
146
102
- it('should support nested Profilers', () => {
103
- const FunctionalComponent = ({label}) => <div>{label}</div>;
104
- class ClassComponent extends React.Component {
105
- render() {
106
- return <block>{this.props.label}</block>;
147
+ it('should support nested Profilers', () => {
148
+ const FunctionalComponent = ({label}) => <div>{label}</div>;
149
+ class ClassComponent extends React.Component {
150
+ render() {
151
+ return <block>{this.props.label}</block>;
152
+ }
153
}
108
- }
109
- const renderer = ReactTestRenderer.create(
110
- <React.unstable_Profiler id="outer" onRender={jest.fn()}>
111
- <FunctionalComponent label="outer functional component" />
112
- <React.unstable_Profiler id="inner" onRender={jest.fn()}>
113
- <ClassComponent label="inner class component" />
114
- <span>inner span</span>
115
- </React.unstable_Profiler>
116
- </React.unstable_Profiler>,
117
- );
118
- expect(renderer.toJSON()).toMatchSnapshot();
154
+ const renderer = ReactTestRenderer.create(
155
+ <React.unstable_Profiler id="outer" onRender={jest.fn()}>
156
+ <FunctionalComponent label="outer functional component" />
157
+ <React.unstable_Profiler id="inner" onRender={jest.fn()}>
158
+ <ClassComponent label="inner class component" />
159
+ <span>inner span</span>
160
+ </React.unstable_Profiler>
161
+ </React.unstable_Profiler>,
162
+ );
163
+ expect(renderer.toJSON()).toMatchSnapshot();
164
+ });
165
});
166
});
167
});
168
});
169
124
- describe('onRender callback', () => {
125
- let AdvanceTime;
126
- let advanceTimeBy;
127
- let mockNow;
128
-
129
- const mockNowForTests = () => {
130
- let currentTime = 0;
131
-
132
- mockNow = jest.fn().mockImplementation(() => currentTime);
133
-
134
- ReactTestRenderer.unstable_setNowImplementation(mockNow);
135
- advanceTimeBy = amount => {
136
- currentTime += amount;
137
- };
138
- };
170
+ [true, false].forEach(enableInteractionTracking => {
171
+ describe('onRender callback', () => {
172
+ beforeEach(() => {
173
+ jest.resetModules();
174
140
- beforeEach(() => {
141
- jest.resetModules();
175
+ loadModules({enableInteractionTracking});
176
+ });
177
143
- loadModules();
144
- mockNowForTests();
178
+ it('is not invoked until the commit phase', () => {
179
+ const callback = jest.fn();
180
146
- AdvanceTime = class extends React.Component {
147
- static defaultProps = {
148
- byAmount: 10,
149
- shouldComponentUpdate: true,
181
+ const Yield = ({value}) => {
182
+ ReactTestRenderer.unstable_yield(value);
183
+ return null;
184
};
151
- shouldComponentUpdate(nextProps) {
152
- return nextProps.shouldComponentUpdate;
153
- }
154
- render() {
155
- // Simulate time passing when this component is rendered
156
- advanceTimeBy(this.props.byAmount);
157
- return this.props.children || null;
158
- }
159
- };
160
- });
185
162
- it('is not invoked until the commit phase', () => {
163
- const callback = jest.fn();
164
-
165
- const Yield = ({value}) => {
166
- ReactTestRenderer.unstable_yield(value);
167
- return null;
168
- };
186
+ const renderer = ReactTestRenderer.create(
187
+ <React.unstable_Profiler id="test" onRender={callback}>
188
+ <Yield value="first" />
189
+ <Yield value="last" />
190
+ </React.unstable_Profiler>,
191
+ {
192
+ unstable_isAsync: true,
193
+ },
194
+ );
195
170
- const renderer = ReactTestRenderer.create(
171
- <React.unstable_Profiler id="test" onRender={callback}>
172
- <Yield value="first" />
173
- <Yield value="last" />
174
- </React.unstable_Profiler>,
175
- {
176
- unstable_isAsync: true,
177
- },
178
- );
196
+ // Times are logged until a render is committed.
197
+ expect(renderer).toFlushThrough(['first']);
198
+ expect(callback).toHaveBeenCalledTimes(0);
199
+ expect(renderer).toFlushAll(['last']);
200
+ expect(callback).toHaveBeenCalledTimes(1);
201
+ });
202
180
- // Times are logged until a render is committed.
181
- expect(renderer).toFlushThrough(['first']);
182
- expect(callback).toHaveBeenCalledTimes(0);
183
- expect(renderer).toFlushAll(['last']);
184
- expect(callback).toHaveBeenCalledTimes(1);
185
- });
203
+ it('does not record times for components outside of Profiler tree', () => {
204
+ ReactTestRenderer.create(
205
+ <div>
206
+ <AdvanceTime />
207
+ <AdvanceTime />
208
+ <AdvanceTime />
209
+ <AdvanceTime />
210
+ <AdvanceTime />
211
+ </div>,
212
+ );
213
187
- it('does not record times for components outside of Profiler tree', () => {
188
- ReactTestRenderer.create(
189
- <div>
190
- <AdvanceTime />
191
- <AdvanceTime />
192
- <AdvanceTime />
193
- <AdvanceTime />
194
- <AdvanceTime />
195
- </div>,
196
- );
214
+ // Should be called two times:
215
+ // 2. To compute the update expiration time
216
+ // 3. To record the commit time
217
+ // No additional calls from ProfilerTimer are expected.
218
+ expect(mockNow).toHaveBeenCalledTimes(2);
219
+ });
220
198
- // Should be called two times:
199
- // 2. To compute the update expiration time
200
- // 3. To record the commit time
201
- // No additional calls from ProfilerTimer are expected.
202
- expect(mockNow).toHaveBeenCalledTimes(2);
203
- });
221
+ it('logs render times for both mount and update', () => {
222
+ const callback = jest.fn();
223
205
- it('logs render times for both mount and update', () => {
206
- const callback = jest.fn();
224
+ advanceTimeBy(5); // 0 -> 5
225
208
- advanceTimeBy(5); // 0 -> 5
226
+ const renderer = ReactTestRenderer.create(
227
+ <React.unstable_Profiler id="test" onRender={callback}>
228
+ <AdvanceTime />
229
+ </React.unstable_Profiler>,
230
+ );
231
210
- const renderer = ReactTestRenderer.create(
211
- <React.unstable_Profiler id="test" onRender={callback}>
212
- <AdvanceTime />
213
- </React.unstable_Profiler>,
214
- );
232
+ expect(callback).toHaveBeenCalledTimes(1);
233
216
- expect(callback).toHaveBeenCalledTimes(1);
234
+ let [call] = callback.mock.calls;
235
218
- let [call] = callback.mock.calls;
236
+ expect(call).toHaveLength(enableInteractionTracking ? 7 : 6);
237
+ expect(call[0]).toBe('test');
238
+ expect(call[1]).toBe('mount');
239
+ expect(call[2]).toBe(10); // actual time
240
+ expect(call[3]).toBe(10); // base time
241
+ expect(call[4]).toBe(5); // start time
242
+ expect(call[5]).toBe(15); // commit time
243
+ expect(call[6]).toEqual(
244
+ enableInteractionTracking ? new Set() : undefined,
245
+ ); // interaction events
246
220
- expect(call).toHaveLength(6);
221
- expect(call[0]).toBe('test');
222
- expect(call[1]).toBe('mount');
223
- expect(call[2]).toBe(10); // actual time
224
- expect(call[3]).toBe(10); // base time
225
- expect(call[4]).toBe(5); // start time
226
- expect(call[5]).toBe(15); // commit time
247
+ callback.mockReset();
248
228
- callback.mockReset();
249
+ advanceTimeBy(20); // 15 -> 35
250
230
- advanceTimeBy(20); // 15 -> 35
251
+ renderer.update(
252
+ <React.unstable_Profiler id="test" onRender={callback}>
253
+ <AdvanceTime />
254
+ </React.unstable_Profiler>,
255
+ );
256
232
- renderer.update(
233
- <React.unstable_Profiler id="test" onRender={callback}>
234
- <AdvanceTime />
235
- </React.unstable_Profiler>,
236
- );
257
+ expect(callback).toHaveBeenCalledTimes(1);
258
238
- expect(callback).toHaveBeenCalledTimes(1);
259
+ [call] = callback.mock.calls;
260
240
- [call] = callback.mock.calls;
261
+ expect(call).toHaveLength(enableInteractionTracking ? 7 : 6);
262
+ expect(call[0]).toBe('test');
263
+ expect(call[1]).toBe('update');
264
+ expect(call[2]).toBe(10); // actual time
265
+ expect(call[3]).toBe(10); // base time
266
+ expect(call[4]).toBe(35); // start time
267
+ expect(call[5]).toBe(45); // commit time
268
+ expect(call[6]).toEqual(
269
+ enableInteractionTracking ? new Set() : undefined,
270
+ ); // interaction events
271
242
- expect(call).toHaveLength(6);
243
- expect(call[0]).toBe('test');
244
- expect(call[1]).toBe('update');
245
- expect(call[2]).toBe(10); // actual time
246
- expect(call[3]).toBe(10); // base time
247
- expect(call[4]).toBe(35); // start time
248
- expect(call[5]).toBe(45); // commit time
272
+ callback.mockReset();
273
250
- callback.mockReset();
274
+ advanceTimeBy(20); // 45 -> 65
275
252
- advanceTimeBy(20); // 45 -> 65
276
+ renderer.update(
277
+ <React.unstable_Profiler id="test" onRender={callback}>
278
+ <AdvanceTime byAmount={4} />
279
+ </React.unstable_Profiler>,
280
+ );
281
254
- renderer.update(
255
- <React.unstable_Profiler id="test" onRender={callback}>
256
- <AdvanceTime byAmount={4} />
257
- </React.unstable_Profiler>,
258
- );
282
+ expect(callback).toHaveBeenCalledTimes(1);
283
260
- expect(callback).toHaveBeenCalledTimes(1);
284
+ [call] = callback.mock.calls;
285
+
286
+ expect(call).toHaveLength(enableInteractionTracking ? 7 : 6);
287
+ expect(call[0]).toBe('test');
288
+ expect(call[1]).toBe('update');
289
+ expect(call[2]).toBe(4); // actual time
290
+ expect(call[3]).toBe(4); // base time
291
+ expect(call[4]).toBe(65); // start time
292
+ expect(call[5]).toBe(69); // commit time
293
+ expect(call[6]).toEqual(
294
+ enableInteractionTracking ? new Set() : undefined,
295
+ ); // interaction events
296
+ });
297
262
- [call] = callback.mock.calls;
298
+ it('includes render times of nested Profilers in their parent times', () => {
299
+ const callback = jest.fn();
300
264
- expect(call).toHaveLength(6);
265
- expect(call[0]).toBe('test');
266
- expect(call[1]).toBe('update');
267
- expect(call[2]).toBe(4); // actual time
268
- expect(call[3]).toBe(4); // base time
269
- expect(call[4]).toBe(65); // start time
270
- expect(call[5]).toBe(69); // commit time
271
- });
301
+ advanceTimeBy(5); // 0 -> 5
302
273
- it('includes render times of nested Profilers in their parent times', () => {
274
- const callback = jest.fn();
303
+ ReactTestRenderer.create(
304
+ <React.Fragment>
305
+ <React.unstable_Profiler id="parent" onRender={callback}>
306
+ <AdvanceTime byAmount={10}>
307
+ <React.unstable_Profiler id="child" onRender={callback}>
308
+ <AdvanceTime byAmount={20} />
309
+ </React.unstable_Profiler>
310
+ </AdvanceTime>
311
+ </React.unstable_Profiler>
312
+ </React.Fragment>,
313
+ );
314
276
- advanceTimeBy(5); // 0 -> 5
315
+ expect(callback).toHaveBeenCalledTimes(2);
316
278
- ReactTestRenderer.create(
279
- <React.Fragment>
280
- <React.unstable_Profiler id="parent" onRender={callback}>
281
- <AdvanceTime byAmount={10}>
282
- <React.unstable_Profiler id="child" onRender={callback}>
283
- <AdvanceTime byAmount={20} />
284
- </React.unstable_Profiler>
285
- </AdvanceTime>
286
- </React.unstable_Profiler>
287
- </React.Fragment>,
288
- );
317
+ // Callbacks bubble (reverse order).
318
+ const [childCall, parentCall] = callback.mock.calls;
319
+ expect(childCall[0]).toBe('child');
320
+ expect(parentCall[0]).toBe('parent');
321
+
322
+ // Parent times should include child times
323
+ expect(childCall[2]).toBe(20); // actual time
324
+ expect(childCall[3]).toBe(20); // base time
325
+ expect(childCall[4]).toBe(15); // start time
326
+ expect(childCall[5]).toBe(35); // commit time
327
+ expect(parentCall[2]).toBe(30); // actual time
328
+ expect(parentCall[3]).toBe(30); // base time
329
+ expect(parentCall[4]).toBe(5); // start time
330
+ expect(parentCall[5]).toBe(35); // commit time
331
+ });
332
290
- expect(callback).toHaveBeenCalledTimes(2);
291
-
292
- // Callbacks bubble (reverse order).
293
- const [childCall, parentCall] = callback.mock.calls;
294
- expect(childCall[0]).toBe('child');
295
- expect(parentCall[0]).toBe('parent');
296
-
297
- // Parent times should include child times
298
- expect(childCall[2]).toBe(20); // actual time
299
- expect(childCall[3]).toBe(20); // base time
300
- expect(childCall[4]).toBe(15); // start time
301
- expect(childCall[5]).toBe(35); // commit time
302
- expect(parentCall[2]).toBe(30); // actual time
303
- expect(parentCall[3]).toBe(30); // base time
304
- expect(parentCall[4]).toBe(5); // start time
305
- expect(parentCall[5]).toBe(35); // commit time
306
- });
333
+ it('tracks sibling Profilers separately', () => {
334
+ const callback = jest.fn();
335
308
- it('tracks sibling Profilers separately', () => {
309
- const callback = jest.fn();
336
+ advanceTimeBy(5); // 0 -> 5
337
311
- advanceTimeBy(5); // 0 -> 5
338
+ ReactTestRenderer.create(
339
+ <React.Fragment>
340
+ <React.unstable_Profiler id="first" onRender={callback}>
341
+ <AdvanceTime byAmount={20} />
342
+ </React.unstable_Profiler>
343
+ <React.unstable_Profiler id="second" onRender={callback}>
344
+ <AdvanceTime byAmount={5} />
345
+ </React.unstable_Profiler>
346
+ </React.Fragment>,
347
+ );
348
313
- ReactTestRenderer.create(
314
- <React.Fragment>
315
- <React.unstable_Profiler id="first" onRender={callback}>
316
- <AdvanceTime byAmount={20} />
317
- </React.unstable_Profiler>
318
- <React.unstable_Profiler id="second" onRender={callback}>
319
- <AdvanceTime byAmount={5} />
320
- </React.unstable_Profiler>
321
- </React.Fragment>,
322
- );
349
+ expect(callback).toHaveBeenCalledTimes(2);
350
324
- expect(callback).toHaveBeenCalledTimes(2);
325
-
326
- const [firstCall, secondCall] = callback.mock.calls;
327
- expect(firstCall[0]).toBe('first');
328
- expect(secondCall[0]).toBe('second');
329
-
330
- // Parent times should include child times
331
- expect(firstCall[2]).toBe(20); // actual time
332
- expect(firstCall[3]).toBe(20); // base time
333
- expect(firstCall[4]).toBe(5); // start time
334
- expect(firstCall[5]).toBe(30); // commit time
335
- expect(secondCall[2]).toBe(5); // actual time
336
- expect(secondCall[3]).toBe(5); // base time
337
- expect(secondCall[4]).toBe(25); // start time
338
- expect(secondCall[5]).toBe(30); // commit time
339
- });
351
+ const [firstCall, secondCall] = callback.mock.calls;
352
+ expect(firstCall[0]).toBe('first');
353
+ expect(secondCall[0]).toBe('second');
354
+
355
+ // Parent times should include child times
356
+ expect(firstCall[2]).toBe(20); // actual time
357
+ expect(firstCall[3]).toBe(20); // base time
358
+ expect(firstCall[4]).toBe(5); // start time
359
+ expect(firstCall[5]).toBe(30); // commit time
360
+ expect(secondCall[2]).toBe(5); // actual time
361
+ expect(secondCall[3]).toBe(5); // base time
362
+ expect(secondCall[4]).toBe(25); // start time
363
+ expect(secondCall[5]).toBe(30); // commit time
364
+ });
365
341
- it('does not include time spent outside of profile root', () => {
342
- const callback = jest.fn();
366
+ it('does not include time spent outside of profile root', () => {
367
+ const callback = jest.fn();
368
344
- advanceTimeBy(5); // 0 -> 5
369
+ advanceTimeBy(5); // 0 -> 5
370
346
- ReactTestRenderer.create(
347
- <React.Fragment>
348
- <AdvanceTime byAmount={20} />
349
- <React.unstable_Profiler id="test" onRender={callback}>
350
- <AdvanceTime byAmount={5} />
351
- </React.unstable_Profiler>
352
- <AdvanceTime byAmount={20} />
353
- </React.Fragment>,
354
- );
371
+ ReactTestRenderer.create(
372
+ <React.Fragment>
373
+ <AdvanceTime byAmount={20} />
374
+ <React.unstable_Profiler id="test" onRender={callback}>
375
+ <AdvanceTime byAmount={5} />
376
+ </React.unstable_Profiler>
377
+ <AdvanceTime byAmount={20} />
378
+ </React.Fragment>,
379
+ );
380
356
- expect(callback).toHaveBeenCalledTimes(1);
381
+ expect(callback).toHaveBeenCalledTimes(1);
382
358
- const [call] = callback.mock.calls;
359
- expect(call[0]).toBe('test');
360
- expect(call[2]).toBe(5); // actual time
361
- expect(call[3]).toBe(5); // base time
362
- expect(call[4]).toBe(25); // start time
363
- expect(call[5]).toBe(50); // commit time
364
- });
383
+ const [call] = callback.mock.calls;
384
+ expect(call[0]).toBe('test');
385
+ expect(call[2]).toBe(5); // actual time
386
+ expect(call[3]).toBe(5); // base time
387
+ expect(call[4]).toBe(25); // start time
388
+ expect(call[5]).toBe(50); // commit time
389
+ });
390
366
- it('is not called when blocked by sCU false', () => {
367
- const callback = jest.fn();
391
+ it('is not called when blocked by sCU false', () => {
392
+ const callback = jest.fn();
393
369
- let instance;
370
- class Updater extends React.Component {
371
- state = {};
372
- render() {
373
- instance = this;
374
- return this.props.children;
394
+ let instance;
395
+ class Updater extends React.Component {
396
+ state = {};
397
+ render() {
398
+ instance = this;
399
+ return this.props.children;
400
+ }
401
}
376
- }
402
378
- class Pure extends React.PureComponent {
379
- render() {
380
- return this.props.children;
403
+ class Pure extends React.PureComponent {
404
+ render() {
405
+ return this.props.children;
406
+ }
407
}
382
- }
408
384
- const renderer = ReactTestRenderer.create(
385
- <React.unstable_Profiler id="outer" onRender={callback}>
386
- <Updater>
387
- <React.unstable_Profiler id="middle" onRender={callback}>
388
- <Pure>
389
- <React.unstable_Profiler id="inner" onRender={callback}>
390
- <div />
391
- </React.unstable_Profiler>
392
- </Pure>
393
- </React.unstable_Profiler>
394
- </Updater>
395
- </React.unstable_Profiler>,
396
- );
409
+ const renderer = ReactTestRenderer.create(
410
+ <React.unstable_Profiler id="outer" onRender={callback}>
411
+ <Updater>
412
+ <React.unstable_Profiler id="middle" onRender={callback}>
413
+ <Pure>
414
+ <React.unstable_Profiler id="inner" onRender={callback}>
415
+ <div />
416
+ </React.unstable_Profiler>
417
+ </Pure>
418
+ </React.unstable_Profiler>
419
+ </Updater>
420
+ </React.unstable_Profiler>,
421
+ );
422
398
- // All profile callbacks are called for initial render
399
- expect(callback).toHaveBeenCalledTimes(3);
423
+ // All profile callbacks are called for initial render
424
+ expect(callback).toHaveBeenCalledTimes(3);
425
401
- callback.mockReset();
426
+ callback.mockReset();
427
403
- renderer.unstable_flushSync(() => {
404
- instance.setState({
405
- count: 1,
428
+ renderer.unstable_flushSync(() => {
429
+ instance.setState({
430
+ count: 1,
431
+ });
432
});
407
- });
433
409
- // Only call profile updates for paths that have re-rendered
410
- // Since "inner" is beneath a pure compoent, it isn't called
411
- expect(callback).toHaveBeenCalledTimes(2);
412
- expect(callback.mock.calls[0][0]).toBe('middle');
413
- expect(callback.mock.calls[1][0]).toBe('outer');
414
- });
434
+ // Only call profile updates for paths that have re-rendered
435
+ // Since "inner" is beneath a pure compoent, it isn't called
436
+ expect(callback).toHaveBeenCalledTimes(2);
437
+ expect(callback.mock.calls[0][0]).toBe('middle');
438
+ expect(callback.mock.calls[1][0]).toBe('outer');
439
+ });
440
416
- it('decreases actual time but not base time when sCU prevents an update', () => {
417
- const callback = jest.fn();
441
+ it('decreases actual time but not base time when sCU prevents an update', () => {
442
+ const callback = jest.fn();
443
419
- advanceTimeBy(5); // 0 -> 5
444
+ advanceTimeBy(5); // 0 -> 5
445
421
- const renderer = ReactTestRenderer.create(
422
- <React.unstable_Profiler id="test" onRender={callback}>
423
- <AdvanceTime byAmount={10}>
424
- <AdvanceTime byAmount={13} shouldComponentUpdate={false} />
425
- </AdvanceTime>
426
- </React.unstable_Profiler>,
427
- );
446
+ const renderer = ReactTestRenderer.create(
447
+ <React.unstable_Profiler id="test" onRender={callback}>
448
+ <AdvanceTime byAmount={10}>
449
+ <AdvanceTime byAmount={13} shouldComponentUpdate={false} />
450
+ </AdvanceTime>
451
+ </React.unstable_Profiler>,
452
+ );
453
429
- expect(callback).toHaveBeenCalledTimes(1);
454
+ expect(callback).toHaveBeenCalledTimes(1);
455
431
- advanceTimeBy(30); // 28 -> 58
456
+ advanceTimeBy(30); // 28 -> 58
457
433
- renderer.update(
434
- <React.unstable_Profiler id="test" onRender={callback}>
435
- <AdvanceTime byAmount={4}>
436
- <AdvanceTime byAmount={7} shouldComponentUpdate={false} />
437
- </AdvanceTime>
438
- </React.unstable_Profiler>,
439
- );
458
+ renderer.update(
459
+ <React.unstable_Profiler id="test" onRender={callback}>
460
+ <AdvanceTime byAmount={4}>
461
+ <AdvanceTime byAmount={7} shouldComponentUpdate={false} />
462
+ </AdvanceTime>
463
+ </React.unstable_Profiler>,
464
+ );
465
441
- expect(callback).toHaveBeenCalledTimes(2);
466
+ expect(callback).toHaveBeenCalledTimes(2);
467
443
- const [mountCall, updateCall] = callback.mock.calls;
468
+ const [mountCall, updateCall] = callback.mock.calls;
469
445
- expect(mountCall[1]).toBe('mount');
446
- expect(mountCall[2]).toBe(23); // actual time
447
- expect(mountCall[3]).toBe(23); // base time
448
- expect(mountCall[4]).toBe(5); // start time
449
- expect(mountCall[5]).toBe(28); // commit time
470
+ expect(mountCall[1]).toBe('mount');
471
+ expect(mountCall[2]).toBe(23); // actual time
472
+ expect(mountCall[3]).toBe(23); // base time
473
+ expect(mountCall[4]).toBe(5); // start time
474
+ expect(mountCall[5]).toBe(28); // commit time
475
451
- expect(updateCall[1]).toBe('update');
452
- expect(updateCall[2]).toBe(4); // actual time
453
- expect(updateCall[3]).toBe(17); // base time
454
- expect(updateCall[4]).toBe(58); // start time
455
- expect(updateCall[5]).toBe(62); // commit time
456
- });
476
+ expect(updateCall[1]).toBe('update');
477
+ expect(updateCall[2]).toBe(4); // actual time
478
+ expect(updateCall[3]).toBe(17); // base time
479
+ expect(updateCall[4]).toBe(58); // start time
480
+ expect(updateCall[5]).toBe(62); // commit time
481
+ });
482
458
- it('includes time spent in render phase lifecycles', () => {
459
- class WithLifecycles extends React.Component {
460
- state = {};
461
- static getDerivedStateFromProps() {
462
- advanceTimeBy(3);
463
- return null;
464
- }
465
- shouldComponentUpdate() {
466
- advanceTimeBy(7);
467
- return true;
468
- }
469
- render() {
470
- advanceTimeBy(5);
471
- return null;
483
+ it('includes time spent in render phase lifecycles', () => {
484
+ class WithLifecycles extends React.Component {
485
+ state = {};
486
+ static getDerivedStateFromProps() {
487
+ advanceTimeBy(3);
488
+ return null;
489
+ }
490
+ shouldComponentUpdate() {
491
+ advanceTimeBy(7);
492
+ return true;
493
+ }
494
+ render() {
495
+ advanceTimeBy(5);
496
+ return null;
497
+ }
498
}
473
- }
474
-
475
- const callback = jest.fn();
499
477
- advanceTimeBy(5); // 0 -> 5
478
-
479
- const renderer = ReactTestRenderer.create(
480
- <React.unstable_Profiler id="test" onRender={callback}>
481
- <WithLifecycles />
482
- </React.unstable_Profiler>,
483
- );
484
-
485
- advanceTimeBy(15); // 13 -> 28
486
-
487
- renderer.update(
488
- <React.unstable_Profiler id="test" onRender={callback}>
489
- <WithLifecycles />
490
- </React.unstable_Profiler>,
491
- );
492
-
493
- expect(callback).toHaveBeenCalledTimes(2);
494
-
495
- const [mountCall, updateCall] = callback.mock.calls;
496
-
497
- expect(mountCall[1]).toBe('mount');
498
- expect(mountCall[2]).toBe(8); // actual time
499
- expect(mountCall[3]).toBe(8); // base time
500
- expect(mountCall[4]).toBe(5); // start time
501
- expect(mountCall[5]).toBe(13); // commit time
502
-
503
- expect(updateCall[1]).toBe('update');
504
- expect(updateCall[2]).toBe(15); // actual time
505
- expect(updateCall[3]).toBe(15); // base time
506
- expect(updateCall[4]).toBe(28); // start time
507
- expect(updateCall[5]).toBe(43); // commit time
508
- });
509
-
510
- describe('with regard to interruptions', () => {
511
- it('should accumulate actual time after a scheduling interruptions', () => {
500
const callback = jest.fn();
501
514
- const Yield = ({renderTime}) => {
515
- advanceTimeBy(renderTime);
516
- ReactTestRenderer.unstable_yield('Yield:' + renderTime);
517
- return null;
518
- };
519
-
502
advanceTimeBy(5); // 0 -> 5
503
522
- // Render partially, but run out of time before completing.
504
const renderer = ReactTestRenderer.create(
505
<React.unstable_Profiler id="test" onRender={callback}>
525
- <Yield renderTime={2} />
526
- <Yield renderTime={3} />
506
+ <WithLifecycles />
507
</React.unstable_Profiler>,
528
- {unstable_isAsync: true},
508
);
530
- expect(renderer).toFlushThrough(['Yield:2']);
531
- expect(callback).toHaveBeenCalledTimes(0);
532
-
533
- // Resume render for remaining children.
534
- expect(renderer).toFlushAll(['Yield:3']);
535
-
536
- // Verify that logged times include both durations above.
537
- expect(callback).toHaveBeenCalledTimes(1);
538
- const [call] = callback.mock.calls;
539
- expect(call[2]).toBe(5); // actual time
540
- expect(call[3]).toBe(5); // base time
541
- expect(call[4]).toBe(5); // start time
542
- expect(call[5]).toBe(10); // commit time
543
- });
544
-
545
- it('should not include time between frames', () => {
546
- const callback = jest.fn();
547
-
548
- const Yield = ({renderTime}) => {
549
- advanceTimeBy(renderTime);
550
- ReactTestRenderer.unstable_yield('Yield:' + renderTime);
551
- return null;
552
- };
509
554
- advanceTimeBy(5); // 0 -> 5
510
+ advanceTimeBy(15); // 13 -> 28
511
556
- // Render partially, but don't finish.
557
- // This partial render should take 5ms of simulated time.
558
- const renderer = ReactTestRenderer.create(
559
- <React.unstable_Profiler id="outer" onRender={callback}>
560
- <Yield renderTime={5} />
561
- <Yield renderTime={10} />
562
- <React.unstable_Profiler id="inner" onRender={callback}>
563
- <Yield renderTime={17} />
564
- </React.unstable_Profiler>
512
+ renderer.update(
513
+ <React.unstable_Profiler id="test" onRender={callback}>
514
+ <WithLifecycles />
515
</React.unstable_Profiler>,
566
- {unstable_isAsync: true},
516
);
568
- expect(renderer).toFlushThrough(['Yield:5']);
569
- expect(callback).toHaveBeenCalledTimes(0);
517
571
- // Simulate time moving forward while frame is paused.
572
- advanceTimeBy(50); // 10 -> 60
573
-
574
- // Flush the remaninig work,
575
- // Which should take an additional 10ms of simulated time.
576
- expect(renderer).toFlushAll(['Yield:10', 'Yield:17']);
518
expect(callback).toHaveBeenCalledTimes(2);
519
579
- const [innerCall, outerCall] = callback.mock.calls;
580
-
581
- // Verify that the actual time includes all work times,
582
- // But not the time that elapsed between frames.
583
- expect(innerCall[0]).toBe('inner');
584
- expect(innerCall[2]).toBe(17); // actual time
585
- expect(innerCall[3]).toBe(17); // base time
586
- expect(innerCall[4]).toBe(70); // start time
587
- expect(innerCall[5]).toBe(87); // commit time
588
- expect(outerCall[0]).toBe('outer');
589
- expect(outerCall[2]).toBe(32); // actual time
590
- expect(outerCall[3]).toBe(32); // base time
591
- expect(outerCall[4]).toBe(5); // start time
592
- expect(outerCall[5]).toBe(87); // commit time
520
+ const [mountCall, updateCall] = callback.mock.calls;
521
+
522
+ expect(mountCall[1]).toBe('mount');
523
+ expect(mountCall[2]).toBe(8); // actual time
524
+ expect(mountCall[3]).toBe(8); // base time
525
+ expect(mountCall[4]).toBe(5); // start time
526
+ expect(mountCall[5]).toBe(13); // commit time
527
+
528
+ expect(updateCall[1]).toBe('update');
529
+ expect(updateCall[2]).toBe(15); // actual time
530
+ expect(updateCall[3]).toBe(15); // base time
531
+ expect(updateCall[4]).toBe(28); // start time
532
+ expect(updateCall[5]).toBe(43); // commit time
533
});
534
595
- it('should report the expected times when a high-priority update replaces an in-progress initial render', () => {
596
- const callback = jest.fn();
535
+ describe('with regard to interruptions', () => {
536
+ it('should accumulate actual time after a scheduling interruptions', () => {
537
+ const callback = jest.fn();
538
598
- const Yield = ({renderTime}) => {
599
- advanceTimeBy(renderTime);
600
- ReactTestRenderer.unstable_yield('Yield:' + renderTime);
601
- return null;
602
- };
539
+ const Yield = ({renderTime}) => {
540
+ advanceTimeBy(renderTime);
541
+ ReactTestRenderer.unstable_yield('Yield:' + renderTime);
542
+ return null;
543
+ };
544
604
- advanceTimeBy(5); // 0 -> 5
545
+ advanceTimeBy(5); // 0 -> 5
546
606
- // Render a partially update, but don't finish.
607
- // This partial render should take 10ms of simulated time.
608
- const renderer = ReactTestRenderer.create(
609
- <React.unstable_Profiler id="test" onRender={callback}>
610
- <Yield renderTime={10} />
611
- <Yield renderTime={20} />
612
- </React.unstable_Profiler>,
613
- {unstable_isAsync: true},
614
- );
615
- expect(renderer).toFlushThrough(['Yield:10']);
616
- expect(callback).toHaveBeenCalledTimes(0);
617
-
618
- // Simulate time moving forward while frame is paused.
619
- advanceTimeBy(100); // 15 -> 115
620
-
621
- // Interrupt with higher priority work.
622
- // The interrupted work simulates an additional 5ms of time.
623
- renderer.unstable_flushSync(() => {
624
- renderer.update(
547
+ // Render partially, but run out of time before completing.
548
+ const renderer = ReactTestRenderer.create(
549
<React.unstable_Profiler id="test" onRender={callback}>
626
- <Yield renderTime={5} />
550
+ <Yield renderTime={2} />
551
+ <Yield renderTime={3} />
552
</React.unstable_Profiler>,
553
+ {unstable_isAsync: true},
554
);
555
+ expect(renderer).toFlushThrough(['Yield:2']);
556
+ expect(callback).toHaveBeenCalledTimes(0);
557
+
558
+ // Resume render for remaining children.
559
+ expect(renderer).toFlushAll(['Yield:3']);
560
+
561
+ // Verify that logged times include both durations above.
562
+ expect(callback).toHaveBeenCalledTimes(1);
563
+ const [call] = callback.mock.calls;
564
+ expect(call[2]).toBe(5); // actual time
565
+ expect(call[3]).toBe(5); // base time
566
+ expect(call[4]).toBe(5); // start time
567
+ expect(call[5]).toBe(10); // commit time
568
});
630
- expect(ReactTestRenderer).toClearYields(['Yield:5']);
569
632
- // The initial work was thrown away in this case,
633
- // So the actual and base times should only include the final rendered tree times.
634
- expect(callback).toHaveBeenCalledTimes(1);
635
- let call = callback.mock.calls[0];
636
- expect(call[2]).toBe(5); // actual time
637
- expect(call[3]).toBe(5); // base time
638
- expect(call[4]).toBe(115); // start time
639
- expect(call[5]).toBe(120); // commit time
640
-
641
- callback.mockReset();
642
-
643
- // Verify no more unexpected callbacks from low priority work
644
- expect(renderer).toFlushAll([]);
645
- expect(callback).toHaveBeenCalledTimes(0);
646
- });
570
+ it('should not include time between frames', () => {
571
+ const callback = jest.fn();
572
648
- it('should report the expected times when a high-priority update replaces a low-priority update', () => {
649
- const callback = jest.fn();
573
+ const Yield = ({renderTime}) => {
574
+ advanceTimeBy(renderTime);
575
+ ReactTestRenderer.unstable_yield('Yield:' + renderTime);
576
+ return null;
577
+ };
578
651
- const Yield = ({renderTime}) => {
652
- advanceTimeBy(renderTime);
653
- ReactTestRenderer.unstable_yield('Yield:' + renderTime);
654
- return null;
655
- };
579
+ advanceTimeBy(5); // 0 -> 5
580
657
- advanceTimeBy(5); // 0 -> 5
658
-
659
- const renderer = ReactTestRenderer.create(
660
- <React.unstable_Profiler id="test" onRender={callback}>
661
- <Yield renderTime={6} />
662
- <Yield renderTime={15} />
663
- </React.unstable_Profiler>,
664
- {unstable_isAsync: true},
665
- );
666
-
667
- // Render everything initially.
668
- // This should take 21 seconds of actual and base time.
669
- expect(renderer).toFlushAll(['Yield:6', 'Yield:15']);
670
- expect(callback).toHaveBeenCalledTimes(1);
671
- let call = callback.mock.calls[0];
672
- expect(call[2]).toBe(21); // actual time
673
- expect(call[3]).toBe(21); // base time
674
- expect(call[4]).toBe(5); // start time
675
- expect(call[5]).toBe(26); // commit time
676
-
677
- callback.mockReset();
678
-
679
- advanceTimeBy(30); // 26 -> 56
680
-
681
- // Render a partially update, but don't finish.
682
- // This partial render should take 3ms of simulated time.
683
- renderer.update(
684
- <React.unstable_Profiler id="test" onRender={callback}>
685
- <Yield renderTime={3} />
686
- <Yield renderTime={5} />
687
- <Yield renderTime={9} />
688
- </React.unstable_Profiler>,
689
- );
690
- expect(renderer).toFlushThrough(['Yield:3']);
691
- expect(callback).toHaveBeenCalledTimes(0);
581
+ // Render partially, but don't finish.
582
+ // This partial render should take 5ms of simulated time.
583
+ const renderer = ReactTestRenderer.create(
584
+ <React.unstable_Profiler id="outer" onRender={callback}>
585
+ <Yield renderTime={5} />
586
+ <Yield renderTime={10} />
587
+ <React.unstable_Profiler id="inner" onRender={callback}>
588
+ <Yield renderTime={17} />
589
+ </React.unstable_Profiler>
590
+ </React.unstable_Profiler>,
591
+ {unstable_isAsync: true},
592
+ );
593
+ expect(renderer).toFlushThrough(['Yield:5']);
594
+ expect(callback).toHaveBeenCalledTimes(0);
595
+
596
+ // Simulate time moving forward while frame is paused.
597
+ advanceTimeBy(50); // 10 -> 60
598
+
599
+ // Flush the remaninig work,
600
+ // Which should take an additional 10ms of simulated time.
601
+ expect(renderer).toFlushAll(['Yield:10', 'Yield:17']);
602
+ expect(callback).toHaveBeenCalledTimes(2);
603
+
604
+ const [innerCall, outerCall] = callback.mock.calls;
605
+
606
+ // Verify that the actual time includes all work times,
607
+ // But not the time that elapsed between frames.
608
+ expect(innerCall[0]).toBe('inner');
609
+ expect(innerCall[2]).toBe(17); // actual time
610
+ expect(innerCall[3]).toBe(17); // base time
611
+ expect(innerCall[4]).toBe(70); // start time
612
+ expect(innerCall[5]).toBe(87); // commit time
613
+ expect(outerCall[0]).toBe('outer');
614
+ expect(outerCall[2]).toBe(32); // actual time
615
+ expect(outerCall[3]).toBe(32); // base time
616
+ expect(outerCall[4]).toBe(5); // start time
617
+ expect(outerCall[5]).toBe(87); // commit time
618
+ });
619
693
- // Simulate time moving forward while frame is paused.
694
- advanceTimeBy(100); // 59 -> 159
620
+ it('should report the expected times when a high-pri update replaces a mount in-progress', () => {
621
+ const callback = jest.fn();
622
696
- // Render another 5ms of simulated time.
697
- expect(renderer).toFlushThrough(['Yield:5']);
698
- expect(callback).toHaveBeenCalledTimes(0);
623
+ const Yield = ({renderTime}) => {
624
+ advanceTimeBy(renderTime);
625
+ ReactTestRenderer.unstable_yield('Yield:' + renderTime);
626
+ return null;
627
+ };
628
700
- // Simulate time moving forward while frame is paused.
701
- advanceTimeBy(100); // 164 -> 264
629
+ advanceTimeBy(5); // 0 -> 5
630
703
- // Interrupt with higher priority work.
704
- // The interrupted work simulates an additional 11ms of time.
705
- renderer.unstable_flushSync(() => {
706
- renderer.update(
631
+ // Render a partially update, but don't finish.
632
+ // This partial render should take 10ms of simulated time.
633
+ const renderer = ReactTestRenderer.create(
634
<React.unstable_Profiler id="test" onRender={callback}>
708
- <Yield renderTime={11} />
635
+ <Yield renderTime={10} />
636
+ <Yield renderTime={20} />
637
</React.unstable_Profiler>,
638
+ {unstable_isAsync: true},
639
);
711
- });
712
- expect(ReactTestRenderer).toClearYields(['Yield:11']);
640
+ expect(renderer).toFlushThrough(['Yield:10']);
641
+ expect(callback).toHaveBeenCalledTimes(0);
642
714
- // The actual time should include only the most recent render,
715
- // Because this lets us avoid a lot of commit phase reset complexity.
716
- // The base time includes only the final rendered tree times.
717
- expect(callback).toHaveBeenCalledTimes(1);
718
- call = callback.mock.calls[0];
719
- expect(call[2]).toBe(11); // actual time
720
- expect(call[3]).toBe(11); // base time
721
- expect(call[4]).toBe(264); // start time
722
- expect(call[5]).toBe(275); // commit time
723
-
724
- // Verify no more unexpected callbacks from low priority work
725
- expect(renderer).toFlushAll([]);
726
- expect(callback).toHaveBeenCalledTimes(1);
727
- });
728
-
729
- it('should report the expected times when a high-priority update interrupts a low-priority update', () => {
730
- const callback = jest.fn();
643
+ // Simulate time moving forward while frame is paused.
644
+ advanceTimeBy(100); // 15 -> 115
645
732
- const Yield = ({renderTime}) => {
733
- advanceTimeBy(renderTime);
734
- ReactTestRenderer.unstable_yield('Yield:' + renderTime);
735
- return null;
736
- };
737
-
738
- let first;
739
- class FirstComponent extends React.Component {
740
- state = {renderTime: 1};
741
- render() {
742
- first = this;
743
- advanceTimeBy(this.state.renderTime);
744
- ReactTestRenderer.unstable_yield(
745
- 'FirstComponent:' + this.state.renderTime,
746
- );
747
- return <Yield renderTime={4} />;
748
- }
749
- }
750
- let second;
751
- class SecondComponent extends React.Component {
752
- state = {renderTime: 2};
753
- render() {
754
- second = this;
755
- advanceTimeBy(this.state.renderTime);
756
- ReactTestRenderer.unstable_yield(
757
- 'SecondComponent:' + this.state.renderTime,
646
+ // Interrupt with higher priority work.
647
+ // The interrupted work simulates an additional 5ms of time.
648
+ renderer.unstable_flushSync(() => {
649
+ renderer.update(
650
+ <React.unstable_Profiler id="test" onRender={callback}>
651
+ <Yield renderTime={5} />
652
+ </React.unstable_Profiler>,
653
);
759
- return <Yield renderTime={7} />;
760
- }
761
- }
762
-
763
- advanceTimeBy(5); // 0 -> 5
764
-
765
- const renderer = ReactTestRenderer.create(
766
- <React.unstable_Profiler id="test" onRender={callback} foo="1">
767
- <FirstComponent />
768
- <SecondComponent />
769
- </React.unstable_Profiler>,
770
- {unstable_isAsync: true},
771
- );
772
-
773
- // Render everything initially.
774
- // This simulates a total of 14ms of actual render time.
775
- // The base render time is also 14ms for the initial render.
776
- expect(renderer).toFlushAll([
777
- 'FirstComponent:1',
778
- 'Yield:4',
779
- 'SecondComponent:2',
780
- 'Yield:7',
781
- ]);
782
- expect(callback).toHaveBeenCalledTimes(1);
783
- let call = callback.mock.calls[0];
784
- expect(call[2]).toBe(14); // actual time
785
- expect(call[3]).toBe(14); // base time
786
- expect(call[4]).toBe(5); // start time
787
- expect(call[5]).toBe(19); // commit time
788
-
789
- callback.mockClear();
790
-
791
- advanceTimeBy(100); // 19 -> 119
792
-
793
- // Render a partially update, but don't finish.
794
- // This partial render will take 10ms of actual render time.
795
- first.setState({renderTime: 10});
796
- expect(renderer).toFlushThrough(['FirstComponent:10']);
797
- expect(callback).toHaveBeenCalledTimes(0);
798
-
799
- // Simulate time moving forward while frame is paused.
800
- advanceTimeBy(100); // 129 -> 229
654
+ });
655
+ expect(ReactTestRenderer).toClearYields(['Yield:5']);
656
+
657
+ // The initial work was thrown away in this case,
658
+ // So the actual and base times should only include the final rendered tree times.
659
+ expect(callback).toHaveBeenCalledTimes(1);
660
+ let call = callback.mock.calls[0];
661
+ expect(call[2]).toBe(5); // actual time
662
+ expect(call[3]).toBe(5); // base time
663
+ expect(call[4]).toBe(115); // start time
664
+ expect(call[5]).toBe(120); // commit time
665
+
666
+ callback.mockReset();
667
+
668
+ // Verify no more unexpected callbacks from low priority work
669
+ expect(renderer).toFlushAll([]);
670
+ expect(callback).toHaveBeenCalledTimes(0);
671
+ });
672
802
- // Interrupt with higher priority work.
803
- // This simulates a total of 37ms of actual render time.
804
- renderer.unstable_flushSync(() => second.setState({renderTime: 30}));
805
- expect(ReactTestRenderer).toClearYields([
806
- 'SecondComponent:30',
807
- 'Yield:7',
808
- ]);
673
+ it('should report the expected times when a high-priority update replaces a low-priority update', () => {
674
+ const callback = jest.fn();
675
810
- // The actual time should include only the most recent render (37ms),
811
- // Because this greatly simplifies the commit phase logic.
812
- // The base time should include the more recent times for the SecondComponent subtree,
813
- // As well as the original times for the FirstComponent subtree.
814
- expect(callback).toHaveBeenCalledTimes(1);
815
- call = callback.mock.calls[0];
816
- expect(call[2]).toBe(37); // actual time
817
- expect(call[3]).toBe(42); // base time
818
- expect(call[4]).toBe(229); // start time
819
- expect(call[5]).toBe(266); // commit time
676
+ const Yield = ({renderTime}) => {
677
+ advanceTimeBy(renderTime);
678
+ ReactTestRenderer.unstable_yield('Yield:' + renderTime);
679
+ return null;
680
+ };
681
821
- callback.mockClear();
682
+ advanceTimeBy(5); // 0 -> 5
683
823
- // Simulate time moving forward while frame is paused.
824
- advanceTimeBy(100); // 266 -> 366
684
+ const renderer = ReactTestRenderer.create(
685
+ <React.unstable_Profiler id="test" onRender={callback}>
686
+ <Yield renderTime={6} />
687
+ <Yield renderTime={15} />
688
+ </React.unstable_Profiler>,
689
+ {unstable_isAsync: true},
690
+ );
691
826
- // Resume the original low priority update, with rebased state.
827
- // This simulates a total of 14ms of actual render time,
828
- // And does not include the original (interrupted) 10ms.
829
- // The tree contains 42ms of base render time at this point,
830
- // Reflecting the most recent (longer) render durations.
831
- // TODO: This actual time should decrease by 10ms once the scheduler supports resuming.
832
- expect(renderer).toFlushAll(['FirstComponent:10', 'Yield:4']);
833
- expect(callback).toHaveBeenCalledTimes(1);
834
- call = callback.mock.calls[0];
835
- expect(call[2]).toBe(14); // actual time
836
- expect(call[3]).toBe(51); // base time
837
- expect(call[4]).toBe(366); // start time
838
- expect(call[5]).toBe(380); // commit time
839
- });
692
+ // Render everything initially.
693
+ // This should take 21 seconds of actual and base time.
694
+ expect(renderer).toFlushAll(['Yield:6', 'Yield:15']);
695
+ expect(callback).toHaveBeenCalledTimes(1);
696
+ let call = callback.mock.calls[0];
697
+ expect(call[2]).toBe(21); // actual time
698
+ expect(call[3]).toBe(21); // base time
699
+ expect(call[4]).toBe(5); // start time
700
+ expect(call[5]).toBe(26); // commit time
701
841
- [true, false].forEach(flagEnabled => {
842
- describe(`replayFailedUnitOfWorkWithInvokeGuardedCallback ${
843
- flagEnabled ? 'enabled' : 'disabled'
844
- }`, () => {
845
- beforeEach(() => {
846
- jest.resetModules();
702
+ callback.mockReset();
703
848
- loadModules({
849
- replayFailedUnitOfWorkWithInvokeGuardedCallback: flagEnabled,
850
- });
851
- mockNowForTests();
852
- });
704
+ advanceTimeBy(30); // 26 -> 56
705
854
- it('should accumulate actual time after an error handled by componentDidCatch()', () => {
855
- const callback = jest.fn();
706
+ // Render a partially update, but don't finish.
707
+ // This partial render should take 3ms of simulated time.
708
+ renderer.update(
709
+ <React.unstable_Profiler id="test" onRender={callback}>
710
+ <Yield renderTime={3} />
711
+ <Yield renderTime={5} />
712
+ <Yield renderTime={9} />
713
+ </React.unstable_Profiler>,
714
+ );
715
+ expect(renderer).toFlushThrough(['Yield:3']);
716
+ expect(callback).toHaveBeenCalledTimes(0);
717
857
- const ThrowsError = () => {
858
- advanceTimeBy(3);
859
- throw Error('expected error');
860
- };
718
+ // Simulate time moving forward while frame is paused.
719
+ advanceTimeBy(100); // 59 -> 159
720
862
- class ErrorBoundary extends React.Component {
863
- state = {error: null};
864
- componentDidCatch(error) {
865
- this.setState({error});
866
- }
867
- render() {
868
- advanceTimeBy(2);
869
- return this.state.error === null ? (
870
- this.props.children
871
- ) : (
872
- <AdvanceTime byAmount={20} />
873
- );
874
- }
875
- }
721
+ // Render another 5ms of simulated time.
722
+ expect(renderer).toFlushThrough(['Yield:5']);
723
+ expect(callback).toHaveBeenCalledTimes(0);
724
877
- advanceTimeBy(5); // 0 -> 5
725
+ // Simulate time moving forward while frame is paused.
726
+ advanceTimeBy(100); // 164 -> 264
727
879
- ReactTestRenderer.create(
728
+ // Interrupt with higher priority work.
729
+ // The interrupted work simulates an additional 11ms of time.
730
+ renderer.unstable_flushSync(() => {
731
+ renderer.update(
732
<React.unstable_Profiler id="test" onRender={callback}>
881
- <ErrorBoundary>
882
- <AdvanceTime byAmount={9} />
883
- <ThrowsError />
884
- </ErrorBoundary>
733
+ <Yield renderTime={11} />
734
</React.unstable_Profiler>,
735
);
887
-
888
- expect(callback).toHaveBeenCalledTimes(2);
889
-
890
- // Callbacks bubble (reverse order).
891
- let [mountCall, updateCall] = callback.mock.calls;
892
-
893
- // The initial mount only includes the ErrorBoundary (which takes 2)
894
- // But it spends time rendering all of the failed subtree also.
895
- expect(mountCall[1]).toBe('mount');
896
- // actual time includes: 2 (ErrorBoundary) + 9 (AdvanceTime) + 3 (ThrowsError)
897
- // We don't count the time spent in replaying the failed unit of work (ThrowsError)
898
- expect(mountCall[2]).toBe(14);
899
- // base time includes: 2 (ErrorBoundary)
900
- expect(mountCall[3]).toBe(2);
901
- // start time
902
- expect(mountCall[4]).toBe(5);
903
- // commit time: 5 initially + 14 of work
904
- // Add an additional 3 (ThrowsError) if we replaced the failed work
905
- expect(mountCall[5]).toBe(__DEV__ && flagEnabled ? 22 : 19);
906
-
907
- // The update includes the ErrorBoundary and its fallback child
908
- expect(updateCall[1]).toBe('update');
909
- // actual time includes: 2 (ErrorBoundary) + 20 (AdvanceTime)
910
- expect(updateCall[2]).toBe(22);
911
- // base time includes: 2 (ErrorBoundary) + 20 (AdvanceTime)
912
- expect(updateCall[3]).toBe(22);
913
- // start time
914
- expect(updateCall[4]).toBe(__DEV__ && flagEnabled ? 22 : 19);
915
- // commit time: 19 (startTime) + 2 (ErrorBoundary) + 20 (AdvanceTime)
916
- // Add an additional 3 (ThrowsError) if we replaced the failed work
917
- expect(updateCall[5]).toBe(__DEV__ && flagEnabled ? 44 : 41);
736
});
737
+ expect(ReactTestRenderer).toClearYields(['Yield:11']);
738
+
739
+ // The actual time should include only the most recent render,
740
+ // Because this lets us avoid a lot of commit phase reset complexity.
741
+ // The base time includes only the final rendered tree times.
742
+ expect(callback).toHaveBeenCalledTimes(1);
743
+ call = callback.mock.calls[0];
744
+ expect(call[2]).toBe(11); // actual time
745
+ expect(call[3]).toBe(11); // base time
746
+ expect(call[4]).toBe(264); // start time
747
+ expect(call[5]).toBe(275); // commit time
748
+
749
+ // Verify no more unexpected callbacks from low priority work
750
+ expect(renderer).toFlushAll([]);
751
+ expect(callback).toHaveBeenCalledTimes(1);
752
+ });
753
920
- it('should accumulate actual time after an error handled by getDerivedStateFromCatch()', () => {
921
- const callback = jest.fn();
754
+ it('should report the expected times when a high-priority update interrupts a low-priority update', () => {
755
+ const callback = jest.fn();
756
923
- const ThrowsError = () => {
924
- advanceTimeBy(10);
925
- throw Error('expected error');
926
- };
757
+ const Yield = ({renderTime}) => {
758
+ advanceTimeBy(renderTime);
759
+ ReactTestRenderer.unstable_yield('Yield:' + renderTime);
760
+ return null;
761
+ };
762
928
- class ErrorBoundary extends React.Component {
929
- state = {error: null};
930
- static getDerivedStateFromCatch(error) {
931
- return {error};
932
- }
933
- render() {
934
- advanceTimeBy(2);
935
- return this.state.error === null ? (
936
- this.props.children
937
- ) : (
938
- <AdvanceTime byAmount={20} />
939
- );
940
- }
763
+ let first;
764
+ class FirstComponent extends React.Component {
765
+ state = {renderTime: 1};
766
+ render() {
767
+ first = this;
768
+ advanceTimeBy(this.state.renderTime);
769
+ ReactTestRenderer.unstable_yield(
770
+ 'FirstComponent:' + this.state.renderTime,
771
+ );
772
+ return <Yield renderTime={4} />;
773
+ }
774
+ }
775
+ let second;
776
+ class SecondComponent extends React.Component {
777
+ state = {renderTime: 2};
778
+ render() {
779
+ second = this;
780
+ advanceTimeBy(this.state.renderTime);
781
+ ReactTestRenderer.unstable_yield(
782
+ 'SecondComponent:' + this.state.renderTime,
783
+ );
784
+ return <Yield renderTime={7} />;
785
}
786
+ }
787
943
- advanceTimeBy(5); // 0 -> 5
788
+ advanceTimeBy(5); // 0 -> 5
789
945
- ReactTestRenderer.create(
946
- <React.unstable_Profiler id="test" onRender={callback}>
947
- <ErrorBoundary>
948
- <AdvanceTime byAmount={5} />
949
- <ThrowsError />
950
- </ErrorBoundary>
951
- </React.unstable_Profiler>,
952
- );
790
+ const renderer = ReactTestRenderer.create(
791
+ <React.unstable_Profiler id="test" onRender={callback}>
792
+ <FirstComponent />
793
+ <SecondComponent />
794
+ </React.unstable_Profiler>,
795
+ {unstable_isAsync: true},
796
+ );
797
954
- expect(callback).toHaveBeenCalledTimes(1);
955
-
956
- // Callbacks bubble (reverse order).
957
- let [mountCall] = callback.mock.calls;
958
-
959
- // The initial mount includes the ErrorBoundary's error state,
960
- // But i also spends actual time rendering UI that fails and isn't included.
961
- expect(mountCall[1]).toBe('mount');
962
- // actual time includes: 2 (ErrorBoundary) + 5 (AdvanceTime) + 10 (ThrowsError)
963
- // Then the re-render: 2 (ErrorBoundary) + 20 (AdvanceTime)
964
- // We don't count the time spent in replaying the failed unit of work (ThrowsError)
965
- expect(mountCall[2]).toBe(39);
966
- // base time includes: 2 (ErrorBoundary) + 20 (AdvanceTime)
967
- expect(mountCall[3]).toBe(22);
968
- // start time
969
- expect(mountCall[4]).toBe(5);
970
- // commit time
971
- expect(mountCall[5]).toBe(__DEV__ && flagEnabled ? 54 : 44);
972
- });
798
+ // Render everything initially.
799
+ // This simulates a total of 14ms of actual render time.
800
+ // The base render time is also 14ms for the initial render.
801
+ expect(renderer).toFlushAll([
802
+ 'FirstComponent:1',
803
+ 'Yield:4',
804
+ 'SecondComponent:2',
805
+ 'Yield:7',
806
+ ]);
807
+ expect(callback).toHaveBeenCalledTimes(1);
808
+ let call = callback.mock.calls[0];
809
+ expect(call[2]).toBe(14); // actual time
810
+ expect(call[3]).toBe(14); // base time
811
+ expect(call[4]).toBe(5); // start time
812
+ expect(call[5]).toBe(19); // commit time
813
+
814
+ callback.mockClear();
815
+
816
+ advanceTimeBy(100); // 19 -> 119
817
+
818
+ // Render a partially update, but don't finish.
819
+ // This partial render will take 10ms of actual render time.
820
+ first.setState({renderTime: 10});
821
+ expect(renderer).toFlushThrough(['FirstComponent:10']);
822
+ expect(callback).toHaveBeenCalledTimes(0);
823
+
824
+ // Simulate time moving forward while frame is paused.
825
+ advanceTimeBy(100); // 129 -> 229
826
+
827
+ // Interrupt with higher priority work.
828
+ // This simulates a total of 37ms of actual render time.
829
+ renderer.unstable_flushSync(() => second.setState({renderTime: 30}));
830
+ expect(ReactTestRenderer).toClearYields([
831
+ 'SecondComponent:30',
832
+ 'Yield:7',
833
+ ]);
834
+
835
+ // The actual time should include only the most recent render (37ms),
836
+ // Because this greatly simplifies the commit phase logic.
837
+ // The base time should include the more recent times for the SecondComponent subtree,
838
+ // As well as the original times for the FirstComponent subtree.
839
+ expect(callback).toHaveBeenCalledTimes(1);
840
+ call = callback.mock.calls[0];
841
+ expect(call[2]).toBe(37); // actual time
842
+ expect(call[3]).toBe(42); // base time
843
+ expect(call[4]).toBe(229); // start time
844
+ expect(call[5]).toBe(266); // commit time
845
+
846
+ callback.mockClear();
847
+
848
+ // Simulate time moving forward while frame is paused.
849
+ advanceTimeBy(100); // 266 -> 366
850
+
851
+ // Resume the original low priority update, with rebased state.
852
+ // This simulates a total of 14ms of actual render time,
853
+ // And does not include the original (interrupted) 10ms.
854
+ // The tree contains 42ms of base render time at this point,
855
+ // Reflecting the most recent (longer) render durations.
856
+ // TODO: This actual time should decrease by 10ms once the scheduler supports resuming.
857
+ expect(renderer).toFlushAll(['FirstComponent:10', 'Yield:4']);
858
+ expect(callback).toHaveBeenCalledTimes(1);
859
+ call = callback.mock.calls[0];
860
+ expect(call[2]).toBe(14); // actual time
861
+ expect(call[3]).toBe(51); // base time
862
+ expect(call[4]).toBe(366); // start time
863
+ expect(call[5]).toBe(380); // commit time
864
+ });
865
974
- it('should reset the fiber stack correct after a "complete" phase error', () => {
975
- jest.resetModules();
866
+ [true, false].forEach(
867
+ replayFailedUnitOfWorkWithInvokeGuardedCallback => {
868
+ describe(`replayFailedUnitOfWorkWithInvokeGuardedCallback ${
869
+ replayFailedUnitOfWorkWithInvokeGuardedCallback
870
+ ? 'enabled'
871
+ : 'disabled'
872
+ }`, () => {
873
+ beforeEach(() => {
874
+ jest.resetModules();
875
+
876
+ loadModules({
877
+ replayFailedUnitOfWorkWithInvokeGuardedCallback,
878
+ });
879
+ });
880
+
881
+ it('should accumulate actual time after an error handled by componentDidCatch()', () => {
882
+ const callback = jest.fn();
883
+
884
+ const ThrowsError = () => {
885
+ advanceTimeBy(3);
886
+ throw Error('expected error');
887
+ };
888
+
889
+ class ErrorBoundary extends React.Component {
890
+ state = {error: null};
891
+ componentDidCatch(error) {
892
+ this.setState({error});
893
+ }
894
+ render() {
895
+ advanceTimeBy(2);
896
+ return this.state.error === null ? (
897
+ this.props.children
898
+ ) : (
899
+ <AdvanceTime byAmount={20} />
900
+ );
901
+ }
902
+ }
903
+
904
+ advanceTimeBy(5); // 0 -> 5
905
+
906
+ ReactTestRenderer.create(
907
+ <React.unstable_Profiler id="test" onRender={callback}>
908
+ <ErrorBoundary>
909
+ <AdvanceTime byAmount={9} />
910
+ <ThrowsError />
911
+ </ErrorBoundary>
912
+ </React.unstable_Profiler>,
913
+ );
914
977
- loadModules({
978
- useNoopRenderer: true,
979
- replayFailedUnitOfWorkWithInvokeGuardedCallback: flagEnabled,
980
- });
915
+ expect(callback).toHaveBeenCalledTimes(2);
916
+
917
+ // Callbacks bubble (reverse order).
918
+ let [mountCall, updateCall] = callback.mock.calls;
919
+
920
+ // The initial mount only includes the ErrorBoundary (which takes 2)
921
+ // But it spends time rendering all of the failed subtree also.
922
+ expect(mountCall[1]).toBe('mount');
923
+ // actual time includes: 2 (ErrorBoundary) + 9 (AdvanceTime) + 3 (ThrowsError)
924
+ // We don't count the time spent in replaying the failed unit of work (ThrowsError)
925
+ expect(mountCall[2]).toBe(14);
926
+ // base time includes: 2 (ErrorBoundary)
927
+ // Since the tree is empty for the initial commit
928
+ expect(mountCall[3]).toBe(2);
929
+ // start time
930
+ expect(mountCall[4]).toBe(5);
931
+ // commit time: 5 initially + 14 of work
932
+ // Add an additional 3 (ThrowsError) if we replayed the failed work
933
+ expect(mountCall[5]).toBe(
934
+ __DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback
935
+ ? 22
936
+ : 19,
937
+ );
938
982
- // Simulate a renderer error during the "complete" phase.
983
- // This mimics behavior like React Native's View/Text nesting validation.
984
- ReactNoop.render(
985
- <React.unstable_Profiler id="profiler" onRender={jest.fn()}>
986
- <errorInCompletePhase>hi</errorInCompletePhase>
987
- </React.unstable_Profiler>,
988
- );
989
- expect(ReactNoop.flush).toThrow('Error in host config.');
990
-
991
- // A similar case we've seen caused by an invariant in ReactDOM.
992
- // It didn't reproduce without a host component inside.
993
- ReactNoop.render(
994
- <React.unstable_Profiler id="profiler" onRender={jest.fn()}>
995
- <errorInCompletePhase>
996
- <span>hi</span>
997
- </errorInCompletePhase>
998
- </React.unstable_Profiler>,
999
- );
1000
- expect(ReactNoop.flush).toThrow('Error in host config.');
939
+ // The update includes the ErrorBoundary and its fallback child
940
+ expect(updateCall[1]).toBe('update');
941
+ // actual time includes: 2 (ErrorBoundary) + 20 (AdvanceTime)
942
+ expect(updateCall[2]).toBe(22);
943
+ // base time includes: 2 (ErrorBoundary) + 20 (AdvanceTime)
944
+ expect(updateCall[3]).toBe(22);
945
+ // start time
946
+ expect(updateCall[4]).toBe(
947
+ __DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback
948
+ ? 22
949
+ : 19,
950
+ );
951
+ // commit time: 19 (startTime) + 2 (ErrorBoundary) + 20 (AdvanceTime)
952
+ // Add an additional 3 (ThrowsError) if we replayed the failed work
953
+ expect(updateCall[5]).toBe(
954
+ __DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback
955
+ ? 44
956
+ : 41,
957
+ );
958
+ });
959
+
960
+ it('should accumulate actual time after an error handled by getDerivedStateFromCatch()', () => {
961
+ const callback = jest.fn();
962
+
963
+ const ThrowsError = () => {
964
+ advanceTimeBy(10);
965
+ throw Error('expected error');
966
+ };
967
+
968
+ class ErrorBoundary extends React.Component {
969
+ state = {error: null};
970
+ static getDerivedStateFromCatch(error) {
971
+ return {error};
972
+ }
973
+ render() {
974
+ advanceTimeBy(2);
975
+ return this.state.error === null ? (
976
+ this.props.children
977
+ ) : (
978
+ <AdvanceTime byAmount={20} />
979
+ );
980
+ }
981
+ }
982
+
983
+ advanceTimeBy(5); // 0 -> 5
984
+
985
+ ReactTestRenderer.create(
986
+ <React.unstable_Profiler id="test" onRender={callback}>
987
+ <ErrorBoundary>
988
+ <AdvanceTime byAmount={5} />
989
+ <ThrowsError />
990
+ </ErrorBoundary>
991
+ </React.unstable_Profiler>,
992
+ );
993
1002
- // So long as the profiler timer's fiber stack is reset correctly,
1003
- // Subsequent renders should not error.
1004
- ReactNoop.render(
1005
- <React.unstable_Profiler id="profiler" onRender={jest.fn()}>
1006
- <span>hi</span>
1007
- </React.unstable_Profiler>,
1008
- );
1009
- ReactNoop.flush();
1010
- });
1011
- });
994
+ expect(callback).toHaveBeenCalledTimes(1);
995
+
996
+ // Callbacks bubble (reverse order).
997
+ let [mountCall] = callback.mock.calls;
998
+
999
+ // The initial mount includes the ErrorBoundary's error state,
1000
+ // But it also spends actual time rendering UI that fails and isn't included.
1001
+ expect(mountCall[1]).toBe('mount');
1002
+ // actual time includes: 2 (ErrorBoundary) + 5 (AdvanceTime) + 10 (ThrowsError)
1003
+ // Then the re-render: 2 (ErrorBoundary) + 20 (AdvanceTime)
1004
+ // We don't count the time spent in replaying the failed unit of work (ThrowsError)
1005
+ expect(mountCall[2]).toBe(39);
1006
+ // base time includes: 2 (ErrorBoundary) + 20 (AdvanceTime)
1007
+ expect(mountCall[3]).toBe(22);
1008
+ // start time
1009
+ expect(mountCall[4]).toBe(5);
1010
+ // commit time
1011
+ expect(mountCall[5]).toBe(
1012
+ __DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback
1013
+ ? 54
1014
+ : 44,
1015
+ );
1016
+ });
1017
+
1018
+ it('should reset the fiber stack correct after a "complete" phase error', () => {
1019
+ jest.resetModules();
1020
+
1021
+ loadModules({
1022
+ useNoopRenderer: true,
1023
+ replayFailedUnitOfWorkWithInvokeGuardedCallback,
1024
+ });
1025
+
1026
+ // Simulate a renderer error during the "complete" phase.
1027
+ // This mimics behavior like React Native's View/Text nesting validation.
1028
+ ReactNoop.render(
1029
+ <React.unstable_Profiler id="profiler" onRender={jest.fn()}>
1030
+ <errorInCompletePhase>hi</errorInCompletePhase>
1031
+ </React.unstable_Profiler>,
1032
+ );
1033
+ expect(ReactNoop.flush).toThrow('Error in host config.');
1034
+
1035
+ // A similar case we've seen caused by an invariant in ReactDOM.
1036
+ // It didn't reproduce without a host component inside.
1037
+ ReactNoop.render(
1038
+ <React.unstable_Profiler id="profiler" onRender={jest.fn()}>
1039
+ <errorInCompletePhase>
1040
+ <span>hi</span>
1041
+ </errorInCompletePhase>
1042
+ </React.unstable_Profiler>,
1043
+ );
1044
+ expect(ReactNoop.flush).toThrow('Error in host config.');
1045
+
1046
+ // So long as the profiler timer's fiber stack is reset correctly,
1047
+ // Subsequent renders should not error.
1048
+ ReactNoop.render(
1049
+ <React.unstable_Profiler id="profiler" onRender={jest.fn()}>
1050
+ <span>hi</span>
1051
+ </React.unstable_Profiler>,
1052
+ );
1053
+ ReactNoop.flush();
1054
+ });
1055
+ });
1056
+ },
1057
+ );
1058
});
1013
- });
1059
1015
- it('reflects the most recently rendered id value', () => {
1016
- const callback = jest.fn();
1060
+ it('reflects the most recently rendered id value', () => {
1061
+ const callback = jest.fn();
1062
1018
- advanceTimeBy(5); // 0 -> 5
1063
+ advanceTimeBy(5); // 0 -> 5
1064
1020
- const renderer = ReactTestRenderer.create(
1021
- <React.unstable_Profiler id="one" onRender={callback}>
1022
- <AdvanceTime byAmount={2} />
1023
- </React.unstable_Profiler>,
1024
- );
1065
+ const renderer = ReactTestRenderer.create(
1066
+ <React.unstable_Profiler id="one" onRender={callback}>
1067
+ <AdvanceTime byAmount={2} />
1068
+ </React.unstable_Profiler>,
1069
+ );
1070
1026
- expect(callback).toHaveBeenCalledTimes(1);
1071
+ expect(callback).toHaveBeenCalledTimes(1);
1072
1028
- advanceTimeBy(20); // 7 -> 27
1073
+ advanceTimeBy(20); // 7 -> 27
1074
1030
- renderer.update(
1031
- <React.unstable_Profiler id="two" onRender={callback}>
1032
- <AdvanceTime byAmount={1} />
1033
- </React.unstable_Profiler>,
1034
- );
1075
+ renderer.update(
1076
+ <React.unstable_Profiler id="two" onRender={callback}>
1077
+ <AdvanceTime byAmount={1} />
1078
+ </React.unstable_Profiler>,
1079
+ );
1080
1036
- expect(callback).toHaveBeenCalledTimes(2);
1081
+ expect(callback).toHaveBeenCalledTimes(2);
1082
1038
- const [mountCall, updateCall] = callback.mock.calls;
1083
+ const [mountCall, updateCall] = callback.mock.calls;
1084
1040
- expect(mountCall[0]).toBe('one');
1041
- expect(mountCall[1]).toBe('mount');
1042
- expect(mountCall[2]).toBe(2); // actual time
1043
- expect(mountCall[3]).toBe(2); // base time
1044
- expect(mountCall[4]).toBe(5); // start time
1085
+ expect(mountCall[0]).toBe('one');
1086
+ expect(mountCall[1]).toBe('mount');
1087
+ expect(mountCall[2]).toBe(2); // actual time
1088
+ expect(mountCall[3]).toBe(2); // base time
1089
+ expect(mountCall[4]).toBe(5); // start time
1090
1046
- expect(updateCall[0]).toBe('two');
1047
- expect(updateCall[1]).toBe('update');
1048
- expect(updateCall[2]).toBe(1); // actual time
1049
- expect(updateCall[3]).toBe(1); // base time
1050
- expect(updateCall[4]).toBe(27); // start time
1091
+ expect(updateCall[0]).toBe('two');
1092
+ expect(updateCall[1]).toBe('update');
1093
+ expect(updateCall[2]).toBe(1); // actual time
1094
+ expect(updateCall[3]).toBe(1); // base time
1095
+ expect(updateCall[4]).toBe(27); // start time
1096
+ });
1097
});
1098
});
1099
1156
1157
expect(ReactNoop.getRoot('two').current.actualDuration).toBe(14);
1158
});
1159
+
1160
+ describe('interaction tracking', () => {
1161
+ let onInteractionScheduledWorkCompleted;
1162
+ let onInteractionTracked;
1163
+ let onWorkCanceled;
1164
+ let onWorkScheduled;
1165
+ let onWorkStarted;
1166
+ let onWorkStopped;
1167
+ let throwInOnInteractionScheduledWorkCompleted;
1168
+ let throwInOnWorkScheduled;
1169
+ let throwInOnWorkStarted;
1170
+ let throwInOnWorkStopped;
1171
+
1172
+ const getWorkForReactThreads = mockFn =>
1173
+ mockFn.mock.calls.filter(([interactions, threadID]) => threadID > 0);
1174
+
1175
+ beforeEach(() => {
1176
+ jest.resetModules();
1177
+
1178
+ loadModules({
1179
+ enableInteractionTracking: true,
1180
+ });
1181
+
1182
+ throwInOnInteractionScheduledWorkCompleted = false;
1183
+ throwInOnWorkScheduled = false;
1184
+ throwInOnWorkStarted = false;
1185
+ throwInOnWorkStopped = false;
1186
+
1187
+ onInteractionScheduledWorkCompleted = jest.fn(() => {
1188
+ if (throwInOnInteractionScheduledWorkCompleted) {
1189
+ throw Error('Expected error onInteractionScheduledWorkCompleted');
1190
+ }
1191
+ });
1192
+ onInteractionTracked = jest.fn();
1193
+ onWorkCanceled = jest.fn();
1194
+ onWorkScheduled = jest.fn(() => {
1195
+ if (throwInOnWorkScheduled) {
1196
+ throw Error('Expected error onWorkScheduled');
1197
+ }
1198
+ });
1199
+ onWorkStarted = jest.fn(() => {
1200
+ if (throwInOnWorkStarted) {
1201
+ throw Error('Expected error onWorkStarted');
1202
+ }
1203
+ });
1204
+ onWorkStopped = jest.fn(() => {
1205
+ if (throwInOnWorkStopped) {
1206
+ throw Error('Expected error onWorkStopped');
1207
+ }
1208
+ });
1209
+
1210
+ // Verify interaction subscriber methods are called as expected.
1211
+ InteractionTrackingSubscriptions.subscribe({
1212
+ onInteractionScheduledWorkCompleted,
1213
+ onInteractionTracked,
1214
+ onWorkCanceled,
1215
+ onWorkScheduled,
1216
+ onWorkStarted,
1217
+ onWorkStopped,
1218
+ });
1219
+ });
1220
+
1221
+ describe('error handling', () => {
1222
+ it('should cover errors thrown in onWorkScheduled', () => {
1223
+ function Component({children}) {
1224
+ ReactTestRenderer.unstable_yield('Component:' + children);
1225
+ return children;
1226
+ }
1227
+
1228
+ let renderer;
1229
+
1230
+ // Errors that happen inside of a subscriber should throw,
1231
+ throwInOnWorkScheduled = true;
1232
+ expect(() => {
1233
+ InteractionTracking.track('event', mockNow(), () => {
1234
+ renderer = ReactTestRenderer.create(<Component>fail</Component>, {
1235
+ unstable_isAsync: true,
1236
+ });
1237
+ });
1238
+ }).toThrow('Expected error onWorkScheduled');
1239
+ throwInOnWorkScheduled = false;
1240
+ expect(onWorkScheduled).toHaveBeenCalled();
1241
+
1242
+ // But should not leave React in a broken state for subsequent renders.
1243
+ renderer = ReactTestRenderer.create(<Component>succeed</Component>, {
1244
+ unstable_isAsync: true,
1245
+ });
1246
+ expect(renderer).toFlushAll(['Component:succeed']);
1247
+ const tree = renderer.toTree();
1248
+ expect(tree.type).toBe(Component);
1249
+ expect(tree.props.children).toBe('succeed');
1250
+ });
1251
+
1252
+ it('should cover errors thrown in onWorkStarted', () => {
1253
+ function Component({children}) {
1254
+ ReactTestRenderer.unstable_yield('Component:' + children);
1255
+ return children;
1256
+ }
1257
+
1258
+ let renderer;
1259
+ InteractionTracking.track('event', mockNow(), () => {
1260
+ renderer = ReactTestRenderer.create(<Component>text</Component>, {
1261
+ unstable_isAsync: true,
1262
+ });
1263
+ });
1264
+ onWorkStarted.mockClear();
1265
+
1266
+ // Errors that happen inside of a subscriber should throw,
1267
+ throwInOnWorkStarted = true;
1268
+ expect(() => {
1269
+ expect(renderer).toFlushAll(['Component:text']);
1270
+ }).toThrow('Expected error onWorkStarted');
1271
+ throwInOnWorkStarted = false;
1272
+ expect(onWorkStarted).toHaveBeenCalled();
1273
+
1274
+ // But the React work should have still been processed.
1275
+ expect(renderer).toFlushAll([]);
1276
+ const tree = renderer.toTree();
1277
+ expect(tree.type).toBe(Component);
1278
+ expect(tree.props.children).toBe('text');
1279
+ });
1280
+
1281
+ it('should cover errors thrown in onWorkStopped', () => {
1282
+ function Component({children}) {
1283
+ ReactTestRenderer.unstable_yield('Component:' + children);
1284
+ return children;
1285
+ }
1286
+
1287
+ let renderer;
1288
+ InteractionTracking.track('event', mockNow(), () => {
1289
+ renderer = ReactTestRenderer.create(<Component>text</Component>, {
1290
+ unstable_isAsync: true,
1291
+ });
1292
+ });
1293
+ expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
1294
+
1295
+ // Errors that happen in an on-stopped callback,
1296
+ throwInOnWorkStopped = true;
1297
+ expect(() => {
1298
+ renderer.unstable_flushAll(['Component:text']);
1299
+ }).toThrow('Expected error onWorkStopped');
1300
+ throwInOnWorkStopped = false;
1301
+ expect(onWorkStopped).toHaveBeenCalledTimes(2);
1302
+
1303
+ // Should still commit the update,
1304
+ const tree = renderer.toTree();
1305
+ expect(tree.type).toBe(Component);
1306
+ expect(tree.props.children).toBe('text');
1307
+
1308
+ // And still call onInteractionScheduledWorkCompleted if the interaction is finished.
1309
+ expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
1310
+ });
1311
+
1312
+ it('should cover errors thrown in onInteractionScheduledWorkCompleted', () => {
1313
+ function Component({children}) {
1314
+ ReactTestRenderer.unstable_yield('Component:' + children);
1315
+ return children;
1316
+ }
1317
+
1318
+ const eventOne = {
1319
+ id: 0,
1320
+ name: 'event one',
1321
+ timestamp: mockNow(),
1322
+ };
1323
+ const eventTwo = {
1324
+ id: 1,
1325
+ name: 'event two',
1326
+ timestamp: mockNow(),
1327
+ };
1328
+
1329
+ let renderer;
1330
+ InteractionTracking.track(eventOne.name, mockNow(), () => {
1331
+ InteractionTracking.track(eventTwo.name, mockNow(), () => {
1332
+ renderer = ReactTestRenderer.create(<Component>text</Component>, {
1333
+ unstable_isAsync: true,
1334
+ });
1335
+ });
1336
+ });
1337
+ expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
1338
+
1339
+ throwInOnInteractionScheduledWorkCompleted = true;
1340
+ expect(() => {
1341
+ renderer.unstable_flushAll(['Component:text']);
1342
+ }).toThrow('Expected error onInteractionScheduledWorkCompleted');
1343
+
1344
+ // Even though an error is thrown for one completed interaction,
1345
+ // The completed callback should be called for all completed interactions.
1346
+ expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(2);
1347
+ });
1348
+ });
1349
+
1350
+ it('should associate tracked events with their subsequent commits', () => {
1351
+ let instance = null;
1352
+
1353
+ const Yield = ({duration = 10, value}) => {
1354
+ advanceTimeBy(duration);
1355
+ ReactTestRenderer.unstable_yield(value);
1356
+ return null;
1357
+ };
1358
+
1359
+ class Example extends React.Component {
1360
+ state = {
1361
+ count: 0,
1362
+ };
1363
+ render() {
1364
+ instance = this;
1365
+ return (
1366
+ <React.Fragment>
1367
+ <Yield value="first" />
1368
+ {this.state.count}
1369
+ <Yield value="last" />
1370
+ </React.Fragment>
1371
+ );
1372
+ }
1373
+ }
1374
+
1375
+ advanceTimeBy(1);
1376
+
1377
+ const interactionCreation = {
1378
+ id: 0,
1379
+ name: 'creation event',
1380
+ timestamp: mockNow(),
1381
+ };
1382
+
1383
+ const onRender = jest.fn();
1384
+ let renderer;
1385
+ InteractionTracking.track(interactionCreation.name, mockNow(), () => {
1386
+ renderer = ReactTestRenderer.create(
1387
+ <React.unstable_Profiler id="test-profiler" onRender={onRender}>
1388
+ <Example />
1389
+ </React.unstable_Profiler>,
1390
+ {
1391
+ unstable_isAsync: true,
1392
+ },
1393
+ );
1394
+ });
1395
+
1396
+ expect(onInteractionTracked).toHaveBeenCalledTimes(1);
1397
+ expect(onInteractionTracked).toHaveBeenLastNotifiedOfInteraction(
1398
+ interactionCreation,
1399
+ );
1400
+ expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
1401
+
1402
+ // The interaction-tracking package will notify of work started for the default thread,
1403
+ // But React shouldn't notify until it's been flushed.
1404
+ expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(0);
1405
+ expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(0);
1406
+
1407
+ // Work may have been scheduled multiple times.
1408
+ // We only care that the subscriber was notified at least once.
1409
+ // As for the thread ID- the actual value isn't important, only that there was one.
1410
+ expect(onWorkScheduled).toHaveBeenCalled();
1411
+ expect(onWorkScheduled.mock.calls[0][0]).toMatchInteractions([
1412
+ interactionCreation,
1413
+ ]);
1414
+ expect(onWorkScheduled.mock.calls[0][1] > 0).toBe(true);
1415
+
1416
+ // Mount
1417
+ renderer.unstable_flushAll(['first', 'last']);
1418
+ expect(onRender).toHaveBeenCalledTimes(1);
1419
+ let call = onRender.mock.calls[0];
1420
+ expect(call[0]).toEqual('test-profiler');
1421
+ expect(call[5]).toEqual(mockNow());
1422
+ if (ReactFeatureFlags.enableInteractionTracking) {
1423
+ expect(call[6]).toMatchInteractions([interactionCreation]);
1424
+ }
1425
+
1426
+ expect(onInteractionTracked).toHaveBeenCalledTimes(1);
1427
+ expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
1428
+ expect(
1429
+ onInteractionScheduledWorkCompleted,
1430
+ ).toHaveBeenLastNotifiedOfInteraction(interactionCreation);
1431
+ expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(1);
1432
+ expect(getWorkForReactThreads(onWorkStarted)[0][0]).toMatchInteractions([
1433
+ interactionCreation,
1434
+ ]);
1435
+ expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(1);
1436
+ expect(getWorkForReactThreads(onWorkStopped)[0][0]).toMatchInteractions([
1437
+ interactionCreation,
1438
+ ]);
1439
+
1440
+ onRender.mockClear();
1441
+ onWorkScheduled.mockClear();
1442
+ onWorkStarted.mockClear();
1443
+ onWorkStopped.mockClear();
1444
+
1445
+ advanceTimeBy(3);
1446
+
1447
+ let didRunCallback = false;
1448
+
1449
+ const interactionOne = {
1450
+ id: 1,
1451
+ name: 'initial event',
1452
+ timestamp: mockNow(),
1453
+ };
1454
+ InteractionTracking.track(interactionOne.name, mockNow(), () => {
1455
+ instance.setState({count: 1});
1456
+
1457
+ // Update state again to verify our tracked interaction isn't registered twice
1458
+ instance.setState({count: 2});
1459
+
1460
+ // The interaction-tracking package will notify of work started for the default thread,
1461
+ // But React shouldn't notify until it's been flushed.
1462
+ expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(0);
1463
+ expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(0);
1464
+
1465
+ // Work may have been scheduled multiple times.
1466
+ // We only care that the subscriber was notified at least once.
1467
+ // As for the thread ID- the actual value isn't important, only that there was one.
1468
+ expect(onWorkScheduled).toHaveBeenCalled();
1469
+ expect(onWorkScheduled.mock.calls[0][0]).toMatchInteractions([
1470
+ interactionOne,
1471
+ ]);
1472
+ expect(onWorkScheduled.mock.calls[0][1] > 0).toBe(true);
1473
+
1474
+ expect(renderer).toFlushThrough(['first']);
1475
+ expect(onRender).not.toHaveBeenCalled();
1476
+
1477
+ expect(onInteractionTracked).toHaveBeenCalledTimes(2);
1478
+ expect(onInteractionTracked).toHaveBeenLastNotifiedOfInteraction(
1479
+ interactionOne,
1480
+ );
1481
+ expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
1482
+ expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(1);
1483
+ expect(getWorkForReactThreads(onWorkStarted)[0][0]).toMatchInteractions(
1484
+ [interactionOne],
1485
+ );
1486
+ expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(0);
1487
+
1488
+ renderer.unstable_flushAll(['last']);
1489
+ expect(onRender).toHaveBeenCalledTimes(1);
1490
+
1491
+ call = onRender.mock.calls[0];
1492
+ expect(call[0]).toEqual('test-profiler');
1493
+ expect(call[5]).toEqual(mockNow());
1494
+ if (ReactFeatureFlags.enableInteractionTracking) {
1495
+ expect(call[6]).toMatchInteractions([interactionOne]);
1496
+ }
1497
+
1498
+ didRunCallback = true;
1499
+
1500
+ expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(1);
1501
+ expect(getWorkForReactThreads(onWorkStarted)[0][0]).toMatchInteractions(
1502
+ [interactionOne],
1503
+ );
1504
+ expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(1);
1505
+ expect(getWorkForReactThreads(onWorkStopped)[0][0]).toMatchInteractions(
1506
+ [interactionOne],
1507
+ );
1508
+ });
1509
+
1510
+ expect(didRunCallback).toBe(true);
1511
+
1512
+ onRender.mockClear();
1513
+ onWorkScheduled.mockClear();
1514
+ onWorkStarted.mockClear();
1515
+ onWorkStopped.mockClear();
1516
+
1517
+ advanceTimeBy(17);
1518
+
1519
+ // Verify that updating state again does not re-log our interaction.
1520
+ instance.setState({count: 3});
1521
+ renderer.unstable_flushAll(['first', 'last']);
1522
+
1523
+ expect(onRender).toHaveBeenCalledTimes(1);
1524
+ call = onRender.mock.calls[0];
1525
+ expect(call[0]).toEqual('test-profiler');
1526
+ expect(call[5]).toEqual(mockNow());
1527
+ if (ReactFeatureFlags.enableInteractionTracking) {
1528
+ expect(call[6]).toMatchInteractions([]);
1529
+ }
1530
+
1531
+ expect(onInteractionTracked).toHaveBeenCalledTimes(2);
1532
+ expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(2);
1533
+ expect(
1534
+ onInteractionScheduledWorkCompleted,
1535
+ ).toHaveBeenLastNotifiedOfInteraction(interactionOne);
1536
+ expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(0);
1537
+ expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(0);
1538
+
1539
+ onRender.mockClear();
1540
+
1541
+ advanceTimeBy(3);
1542
+
1543
+ // Verify that root updates are also associated with tracked events.
1544
+ const interactionTwo = {
1545
+ id: 2,
1546
+ name: 'root update event',
1547
+ timestamp: mockNow(),
1548
+ };
1549
+ InteractionTracking.track(interactionTwo.name, mockNow(), () => {
1550
+ renderer.update(
1551
+ <React.unstable_Profiler id="test-profiler" onRender={onRender}>
1552
+ <Example />
1553
+ </React.unstable_Profiler>,
1554
+ );
1555
+ });
1556
+
1557
+ expect(onInteractionTracked).toHaveBeenCalledTimes(3);
1558
+ expect(onInteractionTracked).toHaveBeenLastNotifiedOfInteraction(
1559
+ interactionTwo,
1560
+ );
1561
+ expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(2);
1562
+
1563
+ // The interaction-tracking package will notify of work started for the default thread,
1564
+ // But React shouldn't notify until it's been flushed.
1565
+ expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(0);
1566
+ expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(0);
1567
+
1568
+ // Work may have been scheduled multiple times.
1569
+ // We only care that the subscriber was notified at least once.
1570
+ // As for the thread ID- the actual value isn't important, only that there was one.
1571
+ expect(onWorkScheduled).toHaveBeenCalled();
1572
+ expect(onWorkScheduled.mock.calls[0][0]).toMatchInteractions([
1573
+ interactionTwo,
1574
+ ]);
1575
+ expect(onWorkScheduled.mock.calls[0][1] > 0).toBe(true);
1576
+
1577
+ renderer.unstable_flushAll(['first', 'last']);
1578
+
1579
+ expect(onRender).toHaveBeenCalledTimes(1);
1580
+ call = onRender.mock.calls[0];
1581
+ expect(call[0]).toEqual('test-profiler');
1582
+ expect(call[5]).toEqual(mockNow());
1583
+ if (ReactFeatureFlags.enableInteractionTracking) {
1584
+ expect(call[6]).toMatchInteractions([interactionTwo]);
1585
+ }
1586
+
1587
+ expect(onInteractionTracked).toHaveBeenCalledTimes(3);
1588
+ expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(3);
1589
+ expect(
1590
+ onInteractionScheduledWorkCompleted,
1591
+ ).toHaveBeenLastNotifiedOfInteraction(interactionTwo);
1592
+ expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(1);
1593
+ expect(getWorkForReactThreads(onWorkStarted)[0][0]).toMatchInteractions([
1594
+ interactionTwo,
1595
+ ]);
1596
+ expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(1);
1597
+ expect(getWorkForReactThreads(onWorkStopped)[0][0]).toMatchInteractions([
1598
+ interactionTwo,
1599
+ ]);
1600
+ });
1601
+
1602
+ it('should report the expected times when a high-priority update interrupts a low-priority update', () => {
1603
+ const onRender = jest.fn();
1604
+
1605
+ let first;
1606
+ class FirstComponent extends React.Component {
1607
+ state = {count: 0};
1608
+ render() {
1609
+ first = this;
1610
+ ReactTestRenderer.unstable_yield('FirstComponent');
1611
+ return null;
1612
+ }
1613
+ }
1614
+ let second;
1615
+ class SecondComponent extends React.Component {
1616
+ state = {count: 0};
1617
+ render() {
1618
+ second = this;
1619
+ ReactTestRenderer.unstable_yield('SecondComponent');
1620
+ return null;
1621
+ }
1622
+ }
1623
+
1624
+ advanceTimeBy(5);
1625
+
1626
+ const renderer = ReactTestRenderer.create(
1627
+ <React.unstable_Profiler id="test" onRender={onRender}>
1628
+ <FirstComponent />
1629
+ <SecondComponent />
1630
+ </React.unstable_Profiler>,
1631
+ {unstable_isAsync: true},
1632
+ );
1633
+
1634
+ // Initial mount.
1635
+ renderer.unstable_flushAll(['FirstComponent', 'SecondComponent']);
1636
+
1637
+ expect(onInteractionTracked).not.toHaveBeenCalled();
1638
+ expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
1639
+
1640
+ onRender.mockClear();
1641
+
1642
+ advanceTimeBy(100);
1643
+
1644
+ const interactionLowPri = {
1645
+ id: 0,
1646
+ name: 'lowPri',
1647
+ timestamp: mockNow(),
1648
+ };
1649
+
1650
+ InteractionTracking.track(interactionLowPri.name, mockNow(), () => {
1651
+ // Render a partially update, but don't finish.
1652
+ first.setState({count: 1});
1653
+
1654
+ expect(onWorkScheduled).toHaveBeenCalled();
1655
+ expect(onWorkScheduled.mock.calls[0][0]).toMatchInteractions([
1656
+ interactionLowPri,
1657
+ ]);
1658
+
1659
+ expect(renderer).toFlushThrough(['FirstComponent']);
1660
+ expect(onRender).not.toHaveBeenCalled();
1661
+
1662
+ expect(onInteractionTracked).toHaveBeenCalledTimes(1);
1663
+ expect(onInteractionTracked).toHaveBeenLastNotifiedOfInteraction(
1664
+ interactionLowPri,
1665
+ );
1666
+ expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
1667
+ expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(1);
1668
+ expect(getWorkForReactThreads(onWorkStarted)[0][0]).toMatchInteractions(
1669
+ [interactionLowPri],
1670
+ );
1671
+ expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(0);
1672
+
1673
+ advanceTimeBy(100);
1674
+
1675
+ const interactionHighPri = {
1676
+ id: 1,
1677
+ name: 'highPri',
1678
+ timestamp: mockNow(),
1679
+ };
1680
+
1681
+ // Interrupt with higher priority work.
1682
+ // This simulates a total of 37ms of actual render time.
1683
+ renderer.unstable_flushSync(() => {
1684
+ InteractionTracking.track(interactionHighPri.name, mockNow(), () => {
1685
+ second.setState({count: 1});
1686
+
1687
+ expect(onInteractionTracked).toHaveBeenCalledTimes(2);
1688
+ expect(onInteractionTracked).toHaveBeenLastNotifiedOfInteraction(
1689
+ interactionHighPri,
1690
+ );
1691
+ expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
1692
+
1693
+ expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(1);
1694
+ expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(0);
1695
+ });
1696
+ });
1697
+ expect(ReactTestRenderer).toClearYields(['SecondComponent']);
1698
+
1699
+ expect(onInteractionTracked).toHaveBeenCalledTimes(2);
1700
+ expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
1701
+ expect(
1702
+ onInteractionScheduledWorkCompleted,
1703
+ ).toHaveBeenLastNotifiedOfInteraction(interactionHighPri);
1704
+
1705
+ // Verify the high priority update was associated with the high priority event.
1706
+ expect(onRender).toHaveBeenCalledTimes(1);
1707
+ let call = onRender.mock.calls[0];
1708
+ expect(call[0]).toEqual('test');
1709
+ expect(call[5]).toEqual(mockNow());
1710
+ expect(call[6]).toMatchInteractions(
1711
+ ReactFeatureFlags.enableInteractionTracking
1712
+ ? [interactionLowPri, interactionHighPri]
1713
+ : [],
1714
+ );
1715
+
1716
+ onRender.mockClear();
1717
+
1718
+ advanceTimeBy(100);
1719
+
1720
+ // Resume the original low priority update, with rebased state.
1721
+ // Verify the low priority update was retained.
1722
+ renderer.unstable_flushAll(['FirstComponent']);
1723
+ expect(onRender).toHaveBeenCalledTimes(1);
1724
+ call = onRender.mock.calls[0];
1725
+ expect(call[0]).toEqual('test');
1726
+ expect(call[5]).toEqual(mockNow());
1727
+ expect(call[6]).toMatchInteractions(
1728
+ ReactFeatureFlags.enableInteractionTracking
1729
+ ? [interactionLowPri]
1730
+ : [],
1731
+ );
1732
+
1733
+ expect(onInteractionTracked).toHaveBeenCalledTimes(2);
1734
+ expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
1735
+
1736
+ // Work might be started multiple times before being completed.
1737
+ // This is okay; it's part of the interaction-tracking subscriber contract.
1738
+ expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(3);
1739
+ expect(getWorkForReactThreads(onWorkStarted)[1][0]).toMatchInteractions(
1740
+ [interactionLowPri, interactionHighPri],
1741
+ );
1742
+ expect(getWorkForReactThreads(onWorkStarted)[2][0]).toMatchInteractions(
1743
+ [interactionLowPri],
1744
+ );
1745
+ expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(2);
1746
+ expect(getWorkForReactThreads(onWorkStopped)[0][0]).toMatchInteractions(
1747
+ [interactionLowPri, interactionHighPri],
1748
+ );
1749
+ expect(getWorkForReactThreads(onWorkStopped)[1][0]).toMatchInteractions(
1750
+ [interactionLowPri],
1751
+ );
1752
+ });
1753
+
1754
+ expect(onInteractionTracked).toHaveBeenCalledTimes(2);
1755
+ expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(2);
1756
+ expect(
1757
+ onInteractionScheduledWorkCompleted,
1758
+ ).toHaveBeenLastNotifiedOfInteraction(interactionLowPri);
1759
+ expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(3);
1760
+ expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(2);
1761
+ });
1762
+
1763
+ it('should track work spawned by a commit phase lifecycle and setState callback', () => {
1764
+ let instance;
1765
+ class Example extends React.Component {
1766
+ state = {
1767
+ count: 0,
1768
+ };
1769
+ componentDidMount() {
1770
+ advanceTimeBy(10); // Advance timer to keep commits separate
1771
+ this.setState({count: 1}); // Intentional cascading update
1772
+ }
1773
+ componentDidUpdate(prevProps, prevState) {
1774
+ if (this.state.count === 2 && prevState.count === 1) {
1775
+ advanceTimeBy(10); // Advance timer to keep commits separate
1776
+ this.setState({count: 3}); // Intentional cascading update
1777
+ }
1778
+ }
1779
+ render() {
1780
+ instance = this;
1781
+ ReactTestRenderer.unstable_yield('Example:' + this.state.count);
1782
+ return null;
1783
+ }
1784
+ }
1785
+
1786
+ const interactionOne = {
1787
+ id: 0,
1788
+ name: 'componentDidMount test',
1789
+ timestamp: mockNow(),
1790
+ };
1791
+
1792
+ // Initial mount.
1793
+ const onRender = jest.fn();
1794
+ let firstCommitTime = mockNow();
1795
+ let renderer;
1796
+ InteractionTracking.track(interactionOne.name, mockNow(), () => {
1797
+ renderer = ReactTestRenderer.create(
1798
+ <React.unstable_Profiler id="test" onRender={onRender}>
1799
+ <Example />
1800
+ </React.unstable_Profiler>,
1801
+ {unstable_isAsync: true},
1802
+ );
1803
+ });
1804
+
1805
+ expect(onInteractionTracked).toHaveBeenCalledTimes(1);
1806
+ expect(onInteractionTracked).toHaveBeenLastNotifiedOfInteraction(
1807
+ interactionOne,
1808
+ );
1809
+ expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
1810
+ expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(0);
1811
+ expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(0);
1812
+
1813
+ renderer.unstable_flushAll(['Example:0', 'Example:1']);
1814
+
1815
+ expect(onInteractionTracked).toHaveBeenCalledTimes(1);
1816
+ expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
1817
+ expect(
1818
+ onInteractionScheduledWorkCompleted,
1819
+ ).toHaveBeenLastNotifiedOfInteraction(interactionOne);
1820
+ expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(2);
1821
+ expect(getWorkForReactThreads(onWorkStarted)[0][0]).toMatchInteractions([
1822
+ interactionOne,
1823
+ ]);
1824
+ expect(getWorkForReactThreads(onWorkStarted)[1][0]).toMatchInteractions([
1825
+ interactionOne,
1826
+ ]);
1827
+ expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(2);
1828
+ expect(getWorkForReactThreads(onWorkStopped)[0][0]).toMatchInteractions([
1829
+ interactionOne,
1830
+ ]);
1831
+ expect(getWorkForReactThreads(onWorkStopped)[1][0]).toMatchInteractions([
1832
+ interactionOne,
1833
+ ]);
1834
+
1835
+ expect(onRender).toHaveBeenCalledTimes(2);
1836
+ let call = onRender.mock.calls[0];
1837
+ expect(call[0]).toEqual('test');
1838
+ expect(call[5]).toEqual(firstCommitTime);
1839
+ expect(call[6]).toMatchInteractions(
1840
+ ReactFeatureFlags.enableInteractionTracking ? [interactionOne] : [],
1841
+ );
1842
+ call = onRender.mock.calls[1];
1843
+ expect(call[0]).toEqual('test');
1844
+ expect(call[5]).toEqual(mockNow());
1845
+ expect(call[6]).toMatchInteractions(
1846
+ ReactFeatureFlags.enableInteractionTracking ? [interactionOne] : [],
1847
+ );
1848
+
1849
+ onRender.mockClear();
1850
+
1851
+ const interactionTwo = {
1852
+ id: 1,
1853
+ name: 'componentDidUpdate test',
1854
+ timestamp: mockNow(),
1855
+ };
1856
+
1857
+ // Cause an tracked, async update
1858
+ InteractionTracking.track(interactionTwo.name, mockNow(), () => {
1859
+ instance.setState({count: 2});
1860
+ });
1861
+ expect(onRender).not.toHaveBeenCalled();
1862
+ expect(onInteractionTracked).toHaveBeenCalledTimes(2);
1863
+ expect(onInteractionTracked).toHaveBeenLastNotifiedOfInteraction(
1864
+ interactionTwo,
1865
+ );
1866
+ expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
1867
+ expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(2);
1868
+ expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(2);
1869
+
1870
+ advanceTimeBy(5);
1871
+
1872
+ // Flush async work (outside of tracked scope)
1873
+ // This will cause an intentional cascading update from did-update
1874
+ firstCommitTime = mockNow();
1875
+ renderer.unstable_flushAll(['Example:2', 'Example:3']);
1876
+
1877
+ expect(onInteractionTracked).toHaveBeenCalledTimes(2);
1878
+ expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(2);
1879
+ expect(
1880
+ onInteractionScheduledWorkCompleted,
1881
+ ).toHaveBeenLastNotifiedOfInteraction(interactionTwo);
1882
+ expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(4);
1883
+ expect(getWorkForReactThreads(onWorkStarted)[2][0]).toMatchInteractions([
1884
+ interactionTwo,
1885
+ ]);
1886
+ expect(getWorkForReactThreads(onWorkStarted)[3][0]).toMatchInteractions([
1887
+ interactionTwo,
1888
+ ]);
1889
+ expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(4);
1890
+ expect(getWorkForReactThreads(onWorkStopped)[2][0]).toMatchInteractions([
1891
+ interactionTwo,
1892
+ ]);
1893
+ expect(getWorkForReactThreads(onWorkStopped)[3][0]).toMatchInteractions([
1894
+ interactionTwo,
1895
+ ]);
1896
+
1897
+ // Verify the cascading commit is associated with the origin event
1898
+ expect(onRender).toHaveBeenCalledTimes(2);
1899
+ call = onRender.mock.calls[0];
1900
+ expect(call[0]).toEqual('test');
1901
+ expect(call[5]).toEqual(firstCommitTime);
1902
+ expect(call[6]).toMatchInteractions(
1903
+ ReactFeatureFlags.enableInteractionTracking ? [interactionTwo] : [],
1904
+ );
1905
+ call = onRender.mock.calls[1];
1906
+ expect(call[0]).toEqual('test');
1907
+ expect(call[5]).toEqual(mockNow());
1908
+ expect(call[6]).toMatchInteractions(
1909
+ ReactFeatureFlags.enableInteractionTracking ? [interactionTwo] : [],
1910
+ );
1911
+
1912
+ onRender.mockClear();
1913
+
1914
+ const interactionThree = {
1915
+ id: 2,
1916
+ name: 'setState callback test',
1917
+ timestamp: mockNow(),
1918
+ };
1919
+
1920
+ // Cause a cascading update from the setState callback
1921
+ function callback() {
1922
+ instance.setState({count: 6});
1923
+ }
1924
+ InteractionTracking.track(interactionThree.name, mockNow(), () => {
1925
+ instance.setState({count: 5}, callback);
1926
+ });
1927
+ expect(onRender).not.toHaveBeenCalled();
1928
+
1929
+ expect(onInteractionTracked).toHaveBeenCalledTimes(3);
1930
+ expect(onInteractionTracked).toHaveBeenLastNotifiedOfInteraction(
1931
+ interactionThree,
1932
+ );
1933
+ expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(2);
1934
+ expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(4);
1935
+ expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(4);
1936
+
1937
+ // Flush async work (outside of tracked scope)
1938
+ // This will cause an intentional cascading update from the setState callback
1939
+ firstCommitTime = mockNow();
1940
+ renderer.unstable_flushAll(['Example:5', 'Example:6']);
1941
+
1942
+ expect(onInteractionTracked).toHaveBeenCalledTimes(3);
1943
+ expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(3);
1944
+ expect(
1945
+ onInteractionScheduledWorkCompleted,
1946
+ ).toHaveBeenLastNotifiedOfInteraction(interactionThree);
1947
+ expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(6);
1948
+ expect(getWorkForReactThreads(onWorkStarted)[4][0]).toMatchInteractions([
1949
+ interactionThree,
1950
+ ]);
1951
+ expect(getWorkForReactThreads(onWorkStarted)[5][0]).toMatchInteractions([
1952
+ interactionThree,
1953
+ ]);
1954
+ expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(6);
1955
+ expect(getWorkForReactThreads(onWorkStopped)[4][0]).toMatchInteractions([
1956
+ interactionThree,
1957
+ ]);
1958
+ expect(getWorkForReactThreads(onWorkStopped)[5][0]).toMatchInteractions([
1959
+ interactionThree,
1960
+ ]);
1961
+
1962
+ // Verify the cascading commit is associated with the origin event
1963
+ expect(onRender).toHaveBeenCalledTimes(2);
1964
+ call = onRender.mock.calls[0];
1965
+ expect(call[0]).toEqual('test');
1966
+ expect(call[5]).toEqual(firstCommitTime);
1967
+ expect(call[6]).toMatchInteractions(
1968
+ ReactFeatureFlags.enableInteractionTracking ? [interactionThree] : [],
1969
+ );
1970
+ call = onRender.mock.calls[1];
1971
+ expect(call[0]).toEqual('test');
1972
+ expect(call[5]).toEqual(mockNow());
1973
+ expect(call[6]).toMatchInteractions(
1974
+ ReactFeatureFlags.enableInteractionTracking ? [interactionThree] : [],
1975
+ );
1976
+ });
1977
+
1978
+ it('should track interactions associated with a parent component state update', () => {
1979
+ const onRender = jest.fn();
1980
+ let parentInstance = null;
1981
+
1982
+ class Child extends React.Component {
1983
+ render() {
1984
+ ReactTestRenderer.unstable_yield('Child:', this.props.count);
1985
+ return null;
1986
+ }
1987
+ }
1988
+
1989
+ class Parent extends React.Component {
1990
+ state = {
1991
+ count: 0,
1992
+ };
1993
+ render() {
1994
+ parentInstance = this;
1995
+ return (
1996
+ <React.unstable_Profiler id="test-profiler" onRender={onRender}>
1997
+ <Child count={this.state.count} />
1998
+ </React.unstable_Profiler>
1999
+ );
2000
+ }
2001
+ }
2002
+
2003
+ advanceTimeBy(1);
2004
+
2005
+ const renderer = ReactTestRenderer.create(<Parent />, {
2006
+ unstable_isAsync: true,
2007
+ });
2008
+ renderer.unstable_flushAll(['Child:0']);
2009
+ onRender.mockClear();
2010
+
2011
+ const interaction = {
2012
+ id: 0,
2013
+ name: 'parent interaction',
2014
+ timestamp: mockNow(),
2015
+ };
2016
+
2017
+ InteractionTracking.track(interaction.name, mockNow(), () => {
2018
+ parentInstance.setState({count: 1});
2019
+ });
2020
+
2021
+ expect(onInteractionTracked).toHaveBeenCalledTimes(1);
2022
+ expect(onInteractionTracked).toHaveBeenLastNotifiedOfInteraction(
2023
+ interaction,
2024
+ );
2025
+ expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
2026
+ expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(0);
2027
+ expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(0);
2028
+
2029
+ expect(onRender).not.toHaveBeenCalled();
2030
+ renderer.unstable_flushAll(['Child:1']);
2031
+ expect(onRender).toHaveBeenCalledTimes(1);
2032
+ let call = onRender.mock.calls[0];
2033
+ expect(call[0]).toEqual('test-profiler');
2034
+ expect(call[6]).toMatchInteractions(
2035
+ ReactFeatureFlags.enableInteractionTracking ? [interaction] : [],
2036
+ );
2037
+
2038
+ expect(onInteractionTracked).toHaveBeenCalledTimes(1);
2039
+ expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
2040
+ expect(
2041
+ onInteractionScheduledWorkCompleted,
2042
+ ).toHaveBeenLastNotifiedOfInteraction(interaction);
2043
+ expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(1);
2044
+ expect(getWorkForReactThreads(onWorkStarted)[0][0]).toMatchInteractions([
2045
+ interaction,
2046
+ ]);
2047
+ expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(1);
2048
+ expect(getWorkForReactThreads(onWorkStopped)[0][0]).toMatchInteractions([
2049
+ interaction,
2050
+ ]);
2051
+ });
2052
+
2053
+ it('tracks both the temporary placeholder and the finished render for an interaction', async () => {
2054
+ jest.resetModules();
2055
+
2056
+ loadModules({
2057
+ useNoopRenderer: true,
2058
+ enableSuspense: true,
2059
+ enableInteractionTracking: true,
2060
+ });
2061
+
2062
+ // Re-register since we've reloaded modules
2063
+ InteractionTrackingSubscriptions.subscribe({
2064
+ onInteractionScheduledWorkCompleted,
2065
+ onInteractionTracked,
2066
+ onWorkCanceled,
2067
+ onWorkScheduled,
2068
+ onWorkStarted,
2069
+ onWorkStopped,
2070
+ });
2071
+
2072
+ function awaitableAdvanceTimers(ms) {
2073
+ jest.advanceTimersByTime(ms);
2074
+ // Wait until the end of the current tick
2075
+ return new Promise(resolve => {
2076
+ setImmediate(resolve);
2077
+ });
2078
+ }
2079
+
2080
+ const SimpleCacheProvider = require('simple-cache-provider');
2081
+ let cache;
2082
+ function invalidateCache() {
2083
+ cache = SimpleCacheProvider.createCache(invalidateCache);
2084
+ }
2085
+ invalidateCache();
2086
+ const TextResource = SimpleCacheProvider.createResource(
2087
+ ([text, ms = 0]) => {
2088
+ return new Promise((resolve, reject) => {
2089
+ setTimeout(() => {
2090
+ ReactNoop.yield(`Promise resolved [${text}]`);
2091
+ resolve(text);
2092
+ }, ms);
2093
+ });
2094
+ },
2095
+ ([text, ms]) => text,
2096
+ );
2097
+
2098
+ function Text(props) {
2099
+ ReactNoop.yield(props.text);
2100
+ return <span prop={props.text} />;
2101
+ }
2102
+
2103
+ function span(prop) {
2104
+ return {type: 'span', children: [], prop};
2105
+ }
2106
+
2107
+ function AsyncText(props) {
2108
+ const text = props.text;
2109
+ try {
2110
+ TextResource.read(cache, [props.text, props.ms]);
2111
+ ReactNoop.yield(text);
2112
+ return <span prop={text} />;
2113
+ } catch (promise) {
2114
+ if (typeof promise.then === 'function') {
2115
+ ReactNoop.yield(`Suspend! [${text}]`);
2116
+ } else {
2117
+ ReactNoop.yield(`Error! [${text}]`);
2118
+ }
2119
+ throw promise;
2120
+ }
2121
+ }
2122
+
2123
+ const interaction = {
2124
+ id: 0,
2125
+ name: 'initial render',
2126
+ timestamp: mockNow(),
2127
+ };
2128
+
2129
+ const onRender = jest.fn();
2130
+ InteractionTracking.track(interaction.name, mockNow(), () => {
2131
+ ReactNoop.render(
2132
+ <React.unstable_Profiler id="test-profiler" onRender={onRender}>
2133
+ <React.Placeholder fallback={<Text text="Loading..." />}>
2134
+ <AsyncText text="Async" ms={20000} />
2135
+ </React.Placeholder>
2136
+ <Text text="Sync" />
2137
+ </React.unstable_Profiler>,
2138
+ );
2139
+ });
2140
+
2141
+ expect(onInteractionTracked).toHaveBeenCalledTimes(1);
2142
+ expect(onInteractionTracked).toHaveBeenLastNotifiedOfInteraction(
2143
+ interaction,
2144
+ );
2145
+ expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
2146
+ expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(0);
2147
+ expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(0);
2148
+
2149
+ expect(ReactNoop.flush()).toEqual([
2150
+ 'Suspend! [Async]',
2151
+ 'Loading...',
2152
+ 'Sync',
2153
+ ]);
2154
+ // The update hasn't expired yet, so we commit nothing.
2155
+ expect(ReactNoop.getChildren()).toEqual([]);
2156
+ expect(onRender).not.toHaveBeenCalled();
2157
+
2158
+ // Advance both React's virtual time and Jest's timers by enough to expire
2159
+ // the update, but not by enough to flush the suspending promise.
2160
+ ReactNoop.expire(10000);
2161
+ await awaitableAdvanceTimers(10000);
2162
+ // No additional rendering work is required, since we already prepared
2163
+ // the placeholder.
2164
+ expect(ReactNoop.flushExpired()).toEqual([]);
2165
+ // Should have committed the placeholder.
2166
+ expect(ReactNoop.getChildren()).toEqual([
2167
+ span('Loading...'),
2168
+ span('Sync'),
2169
+ ]);
2170
+ expect(onRender).toHaveBeenCalledTimes(1);
2171
+
2172
+ let call = onRender.mock.calls[0];
2173
+ expect(call[0]).toEqual('test-profiler');
2174
+ expect(call[6]).toMatchInteractions(
2175
+ ReactFeatureFlags.enableInteractionTracking ? [interaction] : [],
2176
+ );
2177
+
2178
+ expect(onInteractionTracked).toHaveBeenCalledTimes(1);
2179
+ expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
2180
+
2181
+ // Once the promise resolves, we render the suspended view
2182
+ await awaitableAdvanceTimers(10000);
2183
+ expect(ReactNoop.flush()).toEqual(['Promise resolved [Async]', 'Async']);
2184
+ expect(ReactNoop.getChildren()).toEqual([span('Async'), span('Sync')]);
2185
+ expect(onRender).toHaveBeenCalledTimes(2);
2186
+
2187
+ call = onRender.mock.calls[1];
2188
+ expect(call[0]).toEqual('test-profiler');
2189
+ expect(call[6]).toMatchInteractions(
2190
+ ReactFeatureFlags.enableInteractionTracking ? [interaction] : [],
2191
+ );
2192
+
2193
+ expect(onInteractionTracked).toHaveBeenCalledTimes(1);
2194
+ expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
2195
+ expect(
2196
+ onInteractionScheduledWorkCompleted,
2197
+ ).toHaveBeenLastNotifiedOfInteraction(interaction);
2198
+ });
2199
+ });
2200
});