Codemod some expiration tests to waitForExpired (#26491)
Continuing my journey to migrate all the Scheduler flush* methods to async versions of the same helpers. `unstable_flushExpired` is a rarely used helper that is only meant to be used to test a very particular implementation detail (update starvation prevention, or what we sometimes refer to as "expiration"). I've prefixed the new helper with `unstable_`, too, to indicate that our tests should almost always prefer one of the other patterns instead.
Andrew Clark committed
Mar 27, 2023 at 15:52 UTC
8faf751937e9672fb69dac8143dbfb0929caf77e
2 files changed
+76
-37
packages/internal-test-utils/ReactInternalTestUtils.js
+38
-5
@@ -29,7 +29,7 @@ async function waitForMicrotasks() {
29
});
30
}
31
32
-export async function waitFor(expectedLog) {
32
+export async function waitFor(expectedLog, options) {
33
assertYieldsWereCleared(SchedulerMock);
34
35
// Create the error object before doing any async work, to get a better
@@ -37,16 +37,15 @@ export async function waitFor(expectedLog) {
37
const error = new Error();
38
Error.captureStackTrace(error, waitFor);
39
40
+ const stopAfter = expectedLog.length;
41
const actualLog = [];
42
do {
43
// Wait until end of current task/microtask.
44
await waitForMicrotasks();
45
if (SchedulerMock.unstable_hasPendingWork()) {
45
- SchedulerMock.unstable_flushNumberOfYields(
46
- expectedLog.length - actualLog.length,
47
- );
46
+ SchedulerMock.unstable_flushNumberOfYields(stopAfter - actualLog.length);
47
actualLog.push(...SchedulerMock.unstable_clearLog());
49
- if (expectedLog.length > actualLog.length) {
48
+ if (stopAfter > actualLog.length) {
49
// Continue flushing until we've logged the expected number of items.
50
} else {
51
// Once we've reached the expected sequence, wait one more microtask to
@@ -61,6 +60,12 @@ export async function waitFor(expectedLog) {
60
}
61
} while (true);
62
63
+ if (options && options.additionalLogsAfterAttemptingToYield) {
64
+ expectedLog = expectedLog.concat(
65
+ options.additionalLogsAfterAttemptingToYield,
66
+ );
67
+ }
68
+
69
if (equals(actualLog, expectedLog)) {
70
return;
71
}
@@ -151,6 +156,34 @@ ${diff(expectedError, x)}
156
} while (true);
157
}
158
159
+// This is prefixed with `unstable_` because you should almost always try to
160
+// avoid using it in tests. It's really only for testing a particular
161
+// implementation detail (update starvation prevention).
162
+export async function unstable_waitForExpired(expectedLog): mixed {
163
+ assertYieldsWereCleared(SchedulerMock);
164
+
165
+ // Create the error object before doing any async work, to get a better
166
+ // stack trace.
167
+ const error = new Error();
168
+ Error.captureStackTrace(error, unstable_waitForExpired);
169
+
170
+ // Wait until end of current task/microtask.
171
+ await waitForMicrotasks();
172
+ SchedulerMock.unstable_flushExpired();
173
+
174
+ const actualLog = SchedulerMock.unstable_clearLog();
175
+ if (equals(actualLog, expectedLog)) {
176
+ return;
177
+ }
178
+
179
+ error.message = `
180
+Expected sequence of events did not occur.
181
+
182
+${diff(expectedLog, actualLog)}
183
+`;
184
+ throw error;
185
+}
186
+
187
// TODO: This name is a bit misleading currently because it will stop as soon as
188
// React yields for any reason, not just for a paint. I've left it this way for
189
// now because that's how untable_flushUntilNextPaint already worked, but maybe
packages/react-reconciler/src/__tests__/ReactExpiration-test.js
+38
-32
@@ -21,6 +21,7 @@ let useEffect;
21
let assertLog;
22
let waitFor;
23
let waitForAll;
24
+let unstable_waitForExpired;
25
26
describe('ReactExpiration', () => {
27
beforeEach(() => {
@@ -38,6 +39,7 @@ describe('ReactExpiration', () => {
39
assertLog = InternalTestUtils.assertLog;
40
waitFor = InternalTestUtils.waitFor;
41
waitForAll = InternalTestUtils.waitForAll;
42
+ unstable_waitForExpired = InternalTestUtils.unstable_waitForExpired;
43
44
const textCache = new Map();
45
@@ -124,18 +126,17 @@ describe('ReactExpiration', () => {
126
expect(ReactNoop).toMatchRenderedOutput('Step 1');
127
128
// Nothing has expired yet because time hasn't advanced.
127
- Scheduler.unstable_flushExpired();
129
+ await unstable_waitForExpired([]);
130
expect(ReactNoop).toMatchRenderedOutput('Step 1');
131
132
// Advance time a bit, but not enough to expire the low pri update.
133
ReactNoop.expire(4500);
132
- Scheduler.unstable_flushExpired();
134
+ await unstable_waitForExpired([]);
135
expect(ReactNoop).toMatchRenderedOutput('Step 1');
136
137
// Advance by a little bit more. Now the update should expire and flush.
138
ReactNoop.expire(500);
137
- Scheduler.unstable_flushExpired();
138
- assertLog(['Step 2']);
139
+ await unstable_waitForExpired(['Step 2']);
140
expect(ReactNoop).toMatchRenderedOutput('Step 2');
141
});
142
@@ -339,8 +340,7 @@ describe('ReactExpiration', () => {
340
341
Scheduler.unstable_advanceTime(10000);
342
342
- Scheduler.unstable_flushExpired();
343
- assertLog(['D', 'E']);
343
+ await unstable_waitForExpired(['D', 'E']);
344
expect(root).toMatchRenderedOutput('ABCDE');
345
});
346
@@ -369,8 +369,7 @@ describe('ReactExpiration', () => {
369
370
Scheduler.unstable_advanceTime(10000);
371
372
- Scheduler.unstable_flushExpired();
373
- assertLog(['D', 'E']);
372
+ await unstable_waitForExpired(['D', 'E']);
373
expect(root).toMatchRenderedOutput('ABCDE');
374
});
375
@@ -383,6 +382,7 @@ describe('ReactExpiration', () => {
382
const InternalTestUtils = require('internal-test-utils');
383
waitFor = InternalTestUtils.waitFor;
384
assertLog = InternalTestUtils.assertLog;
385
+ unstable_waitForExpired = InternalTestUtils.unstable_waitForExpired;
386
387
// Before importing the renderer, advance the current time by a number
388
// larger than the maximum allowed for bitwise operations.
@@ -401,19 +401,17 @@ describe('ReactExpiration', () => {
401
await waitFor(['Step 1']);
402
403
// The update should not have expired yet.
404
- Scheduler.unstable_flushExpired();
405
- assertLog([]);
404
+ await unstable_waitForExpired([]);
405
406
expect(ReactNoop).toMatchRenderedOutput('Step 1');
407
408
// Advance the time some more to expire the update.
409
Scheduler.unstable_advanceTime(10000);
411
- Scheduler.unstable_flushExpired();
412
- assertLog(['Step 2']);
410
+ await unstable_waitForExpired(['Step 2']);
411
expect(ReactNoop).toMatchRenderedOutput('Step 2');
412
});
413
416
- it('should measure callback timeout relative to current time, not start-up time', () => {
414
+ it('should measure callback timeout relative to current time, not start-up time', async () => {
415
// Corresponds to a bugfix: https://github.com/facebook/react/pull/15479
416
// The bug wasn't caught by other tests because we use virtual times that
417
// default to 0, and most tests don't advance time.
@@ -424,15 +422,13 @@ describe('ReactExpiration', () => {
422
React.startTransition(() => {
423
ReactNoop.render('Hi');
424
});
427
- Scheduler.unstable_flushExpired();
428
- assertLog([]);
425
+ await unstable_waitForExpired([]);
426
expect(ReactNoop).toMatchRenderedOutput(null);
427
428
// Advancing by ~5 seconds should be sufficient to expire the update. (I
429
// used a slightly larger number to allow for possible rounding.)
430
Scheduler.unstable_advanceTime(6000);
434
- Scheduler.unstable_flushExpired();
435
- assertLog([]);
431
+ await unstable_waitForExpired([]);
432
expect(ReactNoop).toMatchRenderedOutput('Hi');
433
});
434
@@ -476,9 +472,9 @@ describe('ReactExpiration', () => {
472
// The remaining work hasn't expired, so the render phase is time sliced.
473
// In other words, we can flush just the first child without flushing
474
// the rest.
479
- Scheduler.unstable_flushNumberOfYields(1);
475
+ //
476
// Yield right after first child.
481
- assertLog(['Sync pri: 1']);
477
+ await waitFor(['Sync pri: 1']);
478
// Now do the rest.
479
await waitForAll(['Normal pri: 1']);
480
});
@@ -502,8 +498,9 @@ describe('ReactExpiration', () => {
498
499
// The remaining work _has_ expired, so the render phase is _not_ time
500
// sliced. Attempting to flush just the first child also flushes the rest.
505
- Scheduler.unstable_flushNumberOfYields(1);
506
- assertLog(['Sync pri: 2', 'Normal pri: 2']);
501
+ await waitFor(['Sync pri: 2'], {
502
+ additionalLogsAfterAttemptingToYield: ['Normal pri: 2'],
503
+ });
504
});
505
expect(root).toMatchRenderedOutput('Sync pri: 2, Normal pri: 2');
506
});
@@ -606,18 +603,22 @@ describe('ReactExpiration', () => {
603
startTransition(() => {
604
setB(1);
605
});
606
+ await waitFor(['B0']);
607
+
608
// Expire both the transitions
609
Scheduler.unstable_advanceTime(10000);
610
// Both transitions have expired, but since they aren't related
611
// (entangled), we should be able to finish the in-progress transition
612
// without also including the next one.
614
- Scheduler.unstable_flushNumberOfYields(1);
615
- assertLog(['B0', 'C']);
613
+ await waitFor([], {
614
+ additionalLogsAfterAttemptingToYield: ['C'],
615
+ });
616
expect(root).toMatchRenderedOutput('A1B0C');
617
618
// The next transition also finishes without yielding.
619
- Scheduler.unstable_flushNumberOfYields(1);
620
- assertLog(['A1', 'B1', 'C']);
619
+ await waitFor(['A1'], {
620
+ additionalLogsAfterAttemptingToYield: ['B1', 'C'],
621
+ });
622
expect(root).toMatchRenderedOutput('A1B1C');
623
});
624
});
@@ -662,8 +663,9 @@ describe('ReactExpiration', () => {
663
Scheduler.unstable_advanceTime(10000);
664
665
// The rest of the update finishes without yielding.
665
- Scheduler.unstable_flushNumberOfYields(1);
666
- assertLog(['B', 'C']);
666
+ await waitFor([], {
667
+ additionalLogsAfterAttemptingToYield: ['B', 'C'],
668
+ });
669
});
670
});
671
@@ -705,8 +707,9 @@ describe('ReactExpiration', () => {
707
708
// Now flush the original update. Because it expired, it should finish
709
// without yielding.
708
- Scheduler.unstable_flushNumberOfYields(1);
709
- assertLog(['A1', 'B1']);
710
+ await waitFor(['A1'], {
711
+ additionalLogsAfterAttemptingToYield: ['B1'],
712
+ });
713
});
714
});
715
@@ -731,16 +734,19 @@ describe('ReactExpiration', () => {
734
assertLog(['A0', 'B0', 'C0', 'Effect: 0']);
735
expect(root).toMatchRenderedOutput('A0B0C0');
736
734
- await act(() => {
737
+ await act(async () => {
738
startTransition(() => {
739
root.render(<App step={1} />);
740
});
741
+ await waitFor(['A1']);
742
+
743
// Expire the update
744
Scheduler.unstable_advanceTime(10000);
745
746
// The update finishes without yielding. But it does not flush the effect.
742
- Scheduler.unstable_flushNumberOfYields(1);
743
- assertLog(['A1', 'B1', 'C1']);
747
+ await waitFor(['B1'], {
748
+ additionalLogsAfterAttemptingToYield: ['C1'],
749
+ });
750
});
751
// The effect flushes after paint.
752
assertLog(['Effect: 1']);