Skip to content

Commit 8faf751

Browse files
authored
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.
1 parent 8342a09 commit 8faf751

File tree

2 files changed

+76
-37
lines changed

2 files changed

+76
-37
lines changed

packages/internal-test-utils/ReactInternalTestUtils.js

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,24 +29,23 @@ async function waitForMicrotasks() {
2929
});
3030
}
3131

32-
export async function waitFor(expectedLog) {
32+
export async function waitFor(expectedLog, options) {
3333
assertYieldsWereCleared(SchedulerMock);
3434

3535
// Create the error object before doing any async work, to get a better
3636
// stack trace.
3737
const error = new Error();
3838
Error.captureStackTrace(error, waitFor);
3939

40+
const stopAfter = expectedLog.length;
4041
const actualLog = [];
4142
do {
4243
// Wait until end of current task/microtask.
4344
await waitForMicrotasks();
4445
if (SchedulerMock.unstable_hasPendingWork()) {
45-
SchedulerMock.unstable_flushNumberOfYields(
46-
expectedLog.length - actualLog.length,
47-
);
46+
SchedulerMock.unstable_flushNumberOfYields(stopAfter - actualLog.length);
4847
actualLog.push(...SchedulerMock.unstable_clearLog());
49-
if (expectedLog.length > actualLog.length) {
48+
if (stopAfter > actualLog.length) {
5049
// Continue flushing until we've logged the expected number of items.
5150
} else {
5251
// Once we've reached the expected sequence, wait one more microtask to
@@ -61,6 +60,12 @@ export async function waitFor(expectedLog) {
6160
}
6261
} while (true);
6362

63+
if (options && options.additionalLogsAfterAttemptingToYield) {
64+
expectedLog = expectedLog.concat(
65+
options.additionalLogsAfterAttemptingToYield,
66+
);
67+
}
68+
6469
if (equals(actualLog, expectedLog)) {
6570
return;
6671
}
@@ -151,6 +156,34 @@ ${diff(expectedError, x)}
151156
} while (true);
152157
}
153158

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+
154187
// TODO: This name is a bit misleading currently because it will stop as soon as
155188
// React yields for any reason, not just for a paint. I've left it this way for
156189
// now because that's how untable_flushUntilNextPaint already worked, but maybe

packages/react-reconciler/src/__tests__/ReactExpiration-test.js

Lines changed: 38 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ let useEffect;
2121
let assertLog;
2222
let waitFor;
2323
let waitForAll;
24+
let unstable_waitForExpired;
2425

2526
describe('ReactExpiration', () => {
2627
beforeEach(() => {
@@ -38,6 +39,7 @@ describe('ReactExpiration', () => {
3839
assertLog = InternalTestUtils.assertLog;
3940
waitFor = InternalTestUtils.waitFor;
4041
waitForAll = InternalTestUtils.waitForAll;
42+
unstable_waitForExpired = InternalTestUtils.unstable_waitForExpired;
4143

4244
const textCache = new Map();
4345

@@ -124,18 +126,17 @@ describe('ReactExpiration', () => {
124126
expect(ReactNoop).toMatchRenderedOutput('Step 1');
125127

126128
// Nothing has expired yet because time hasn't advanced.
127-
Scheduler.unstable_flushExpired();
129+
await unstable_waitForExpired([]);
128130
expect(ReactNoop).toMatchRenderedOutput('Step 1');
129131

130132
// Advance time a bit, but not enough to expire the low pri update.
131133
ReactNoop.expire(4500);
132-
Scheduler.unstable_flushExpired();
134+
await unstable_waitForExpired([]);
133135
expect(ReactNoop).toMatchRenderedOutput('Step 1');
134136

135137
// Advance by a little bit more. Now the update should expire and flush.
136138
ReactNoop.expire(500);
137-
Scheduler.unstable_flushExpired();
138-
assertLog(['Step 2']);
139+
await unstable_waitForExpired(['Step 2']);
139140
expect(ReactNoop).toMatchRenderedOutput('Step 2');
140141
});
141142

@@ -339,8 +340,7 @@ describe('ReactExpiration', () => {
339340

340341
Scheduler.unstable_advanceTime(10000);
341342

342-
Scheduler.unstable_flushExpired();
343-
assertLog(['D', 'E']);
343+
await unstable_waitForExpired(['D', 'E']);
344344
expect(root).toMatchRenderedOutput('ABCDE');
345345
});
346346

@@ -369,8 +369,7 @@ describe('ReactExpiration', () => {
369369

370370
Scheduler.unstable_advanceTime(10000);
371371

372-
Scheduler.unstable_flushExpired();
373-
assertLog(['D', 'E']);
372+
await unstable_waitForExpired(['D', 'E']);
374373
expect(root).toMatchRenderedOutput('ABCDE');
375374
});
376375

@@ -383,6 +382,7 @@ describe('ReactExpiration', () => {
383382
const InternalTestUtils = require('internal-test-utils');
384383
waitFor = InternalTestUtils.waitFor;
385384
assertLog = InternalTestUtils.assertLog;
385+
unstable_waitForExpired = InternalTestUtils.unstable_waitForExpired;
386386

387387
// Before importing the renderer, advance the current time by a number
388388
// larger than the maximum allowed for bitwise operations.
@@ -401,19 +401,17 @@ describe('ReactExpiration', () => {
401401
await waitFor(['Step 1']);
402402

403403
// The update should not have expired yet.
404-
Scheduler.unstable_flushExpired();
405-
assertLog([]);
404+
await unstable_waitForExpired([]);
406405

407406
expect(ReactNoop).toMatchRenderedOutput('Step 1');
408407

409408
// Advance the time some more to expire the update.
410409
Scheduler.unstable_advanceTime(10000);
411-
Scheduler.unstable_flushExpired();
412-
assertLog(['Step 2']);
410+
await unstable_waitForExpired(['Step 2']);
413411
expect(ReactNoop).toMatchRenderedOutput('Step 2');
414412
});
415413

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 () => {
417415
// Corresponds to a bugfix: https://github.com/facebook/react/pull/15479
418416
// The bug wasn't caught by other tests because we use virtual times that
419417
// default to 0, and most tests don't advance time.
@@ -424,15 +422,13 @@ describe('ReactExpiration', () => {
424422
React.startTransition(() => {
425423
ReactNoop.render('Hi');
426424
});
427-
Scheduler.unstable_flushExpired();
428-
assertLog([]);
425+
await unstable_waitForExpired([]);
429426
expect(ReactNoop).toMatchRenderedOutput(null);
430427

431428
// Advancing by ~5 seconds should be sufficient to expire the update. (I
432429
// used a slightly larger number to allow for possible rounding.)
433430
Scheduler.unstable_advanceTime(6000);
434-
Scheduler.unstable_flushExpired();
435-
assertLog([]);
431+
await unstable_waitForExpired([]);
436432
expect(ReactNoop).toMatchRenderedOutput('Hi');
437433
});
438434

@@ -476,9 +472,9 @@ describe('ReactExpiration', () => {
476472
// The remaining work hasn't expired, so the render phase is time sliced.
477473
// In other words, we can flush just the first child without flushing
478474
// the rest.
479-
Scheduler.unstable_flushNumberOfYields(1);
475+
//
480476
// Yield right after first child.
481-
assertLog(['Sync pri: 1']);
477+
await waitFor(['Sync pri: 1']);
482478
// Now do the rest.
483479
await waitForAll(['Normal pri: 1']);
484480
});
@@ -502,8 +498,9 @@ describe('ReactExpiration', () => {
502498

503499
// The remaining work _has_ expired, so the render phase is _not_ time
504500
// 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+
});
507504
});
508505
expect(root).toMatchRenderedOutput('Sync pri: 2, Normal pri: 2');
509506
});
@@ -606,18 +603,22 @@ describe('ReactExpiration', () => {
606603
startTransition(() => {
607604
setB(1);
608605
});
606+
await waitFor(['B0']);
607+
609608
// Expire both the transitions
610609
Scheduler.unstable_advanceTime(10000);
611610
// Both transitions have expired, but since they aren't related
612611
// (entangled), we should be able to finish the in-progress transition
613612
// without also including the next one.
614-
Scheduler.unstable_flushNumberOfYields(1);
615-
assertLog(['B0', 'C']);
613+
await waitFor([], {
614+
additionalLogsAfterAttemptingToYield: ['C'],
615+
});
616616
expect(root).toMatchRenderedOutput('A1B0C');
617617

618618
// 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+
});
621622
expect(root).toMatchRenderedOutput('A1B1C');
622623
});
623624
});
@@ -662,8 +663,9 @@ describe('ReactExpiration', () => {
662663
Scheduler.unstable_advanceTime(10000);
663664

664665
// 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+
});
667669
});
668670
});
669671

@@ -705,8 +707,9 @@ describe('ReactExpiration', () => {
705707

706708
// Now flush the original update. Because it expired, it should finish
707709
// without yielding.
708-
Scheduler.unstable_flushNumberOfYields(1);
709-
assertLog(['A1', 'B1']);
710+
await waitFor(['A1'], {
711+
additionalLogsAfterAttemptingToYield: ['B1'],
712+
});
710713
});
711714
});
712715

@@ -731,16 +734,19 @@ describe('ReactExpiration', () => {
731734
assertLog(['A0', 'B0', 'C0', 'Effect: 0']);
732735
expect(root).toMatchRenderedOutput('A0B0C0');
733736

734-
await act(() => {
737+
await act(async () => {
735738
startTransition(() => {
736739
root.render(<App step={1} />);
737740
});
741+
await waitFor(['A1']);
742+
738743
// Expire the update
739744
Scheduler.unstable_advanceTime(10000);
740745

741746
// 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+
});
744750
});
745751
// The effect flushes after paint.
746752
assertLog(['Effect: 1']);

0 commit comments

Comments
 (0)