Skip to content

Commit 9a52cc8

Browse files
authored
Convert ReactLazy-test to waitFor pattern (#26304)
I'm in the process of codemodding our test suite to the waitFor pattern. See #26285 for full context. This module required a lot of manual changes so I'm doing it as its own PR. The reason is that most of the tests involved simulating an async import by wrapping them in `Promise.resolve()`, which means they would immediately resolve the next time the microtask queue was flushed. I rewrote the tests to resolve the simulated import explicitly. While converting these tests, I also realized that the `waitFor` helpers weren't properly waiting for the entire microtask queue to recursively finish — if a microtask schedules another microtask, the subsequent one wouldn't fire until after `waitFor` had resolved. To fix this, I used the same strategy as `act` — wait for a real task to finish before proceeding, such as a message event.
1 parent 03462cf commit 9a52cc8

File tree

6 files changed

+305
-262
lines changed

6 files changed

+305
-262
lines changed

packages/internal-test-utils/ReactInternalTestUtils.js

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import * as SchedulerMock from 'scheduler/unstable_mock';
1111
import {diff} from 'jest-diff';
1212
import {equals} from '@jest/expect-utils';
13+
import enqueueTask from './enqueueTask';
1314

1415
function assertYieldsWereCleared(Scheduler) {
1516
const actualYields = Scheduler.unstable_clearYields();
@@ -22,6 +23,12 @@ function assertYieldsWereCleared(Scheduler) {
2223
}
2324
}
2425

26+
async function waitForMicrotasks() {
27+
return new Promise(resolve => {
28+
enqueueTask(() => resolve());
29+
});
30+
}
31+
2532
export async function waitFor(expectedLog) {
2633
assertYieldsWereCleared(SchedulerMock);
2734

@@ -33,7 +40,7 @@ export async function waitFor(expectedLog) {
3340
const actualLog = [];
3441
do {
3542
// Wait until end of current task/microtask.
36-
await null;
43+
await waitForMicrotasks();
3744
if (SchedulerMock.unstable_hasPendingWork()) {
3845
SchedulerMock.unstable_flushNumberOfYields(
3946
expectedLog.length - actualLog.length,
@@ -44,7 +51,7 @@ export async function waitFor(expectedLog) {
4451
} else {
4552
// Once we've reached the expected sequence, wait one more microtask to
4653
// flush any remaining synchronous work.
47-
await null;
54+
await waitForMicrotasks();
4855
actualLog.push(...SchedulerMock.unstable_clearYields());
4956
break;
5057
}
@@ -72,11 +79,11 @@ export async function waitForAll(expectedLog) {
7279
// Create the error object before doing any async work, to get a better
7380
// stack trace.
7481
const error = new Error();
75-
Error.captureStackTrace(error, waitFor);
82+
Error.captureStackTrace(error, waitForAll);
7683

7784
do {
7885
// Wait until end of current task/microtask.
79-
await null;
86+
await waitForMicrotasks();
8087
if (!SchedulerMock.unstable_hasPendingWork()) {
8188
// There's no pending work, even after a microtask. Stop flushing.
8289
break;
@@ -103,11 +110,11 @@ export async function waitForThrow(expectedError: mixed) {
103110
// Create the error object before doing any async work, to get a better
104111
// stack trace.
105112
const error = new Error();
106-
Error.captureStackTrace(error, waitFor);
113+
Error.captureStackTrace(error, waitForThrow);
107114

108115
do {
109116
// Wait until end of current task/microtask.
110-
await null;
117+
await waitForMicrotasks();
111118
if (!SchedulerMock.unstable_hasPendingWork()) {
112119
// There's no pending work, even after a microtask. Stop flushing.
113120
error.message = 'Expected something to throw, but nothing did.';
@@ -119,7 +126,13 @@ export async function waitForThrow(expectedError: mixed) {
119126
if (equals(x, expectedError)) {
120127
return;
121128
}
122-
if (typeof x === 'object' && x !== null && x.message === expectedError) {
129+
if (
130+
typeof expectedError === 'string' &&
131+
typeof x === 'object' &&
132+
x !== null &&
133+
typeof x.message === 'string' &&
134+
x.message.includes(expectedError)
135+
) {
123136
return;
124137
}
125138
error.message = `
@@ -142,15 +155,15 @@ export async function waitForPaint(expectedLog) {
142155
// Create the error object before doing any async work, to get a better
143156
// stack trace.
144157
const error = new Error();
145-
Error.captureStackTrace(error, waitFor);
158+
Error.captureStackTrace(error, waitForPaint);
146159

147160
// Wait until end of current task/microtask.
148-
await null;
161+
await waitForMicrotasks();
149162
if (SchedulerMock.unstable_hasPendingWork()) {
150163
// Flush until React yields.
151164
SchedulerMock.unstable_flushUntilNextPaint();
152165
// Wait one more microtask to flush any remaining synchronous work.
153-
await null;
166+
await waitForMicrotasks();
154167
}
155168

156169
const actualLog = SchedulerMock.unstable_clearYields();
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
let didWarnAboutMessageChannel = false;
11+
let enqueueTaskImpl = null;
12+
13+
// Same as shared/enqeuueTask, but while that one used by the public
14+
// implementation of `act`, this is only used by our internal testing helpers.
15+
export default function enqueueTask(task: () => void): void {
16+
if (enqueueTaskImpl === null) {
17+
try {
18+
// read require off the module object to get around the bundlers.
19+
// we don't want them to detect a require and bundle a Node polyfill.
20+
const requireString = ('require' + Math.random()).slice(0, 7);
21+
const nodeRequire = module && module[requireString];
22+
// assuming we're in node, let's try to get node's
23+
// version of setImmediate, bypassing fake timers if any.
24+
enqueueTaskImpl = nodeRequire.call(module, 'timers').setImmediate;
25+
} catch (_err) {
26+
// we're in a browser
27+
// we can't use regular timers because they may still be faked
28+
// so we try MessageChannel+postMessage instead
29+
enqueueTaskImpl = function (callback: () => void) {
30+
if (__DEV__) {
31+
if (didWarnAboutMessageChannel === false) {
32+
didWarnAboutMessageChannel = true;
33+
if (typeof MessageChannel === 'undefined') {
34+
console['error'](
35+
'This browser does not have a MessageChannel implementation, ' +
36+
'so enqueuing tasks via await act(async () => ...) will fail. ' +
37+
'Please file an issue at https://github.com/facebook/react/issues ' +
38+
'if you encounter this warning.',
39+
);
40+
}
41+
}
42+
}
43+
const channel = new MessageChannel();
44+
channel.port1.onmessage = callback;
45+
channel.port2.postMessage(undefined);
46+
};
47+
}
48+
}
49+
return enqueueTaskImpl(task);
50+
}

packages/react-cache/src/__tests__/ReactCacheOld-test.internal.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ describe('ReactCache', () => {
183183
);
184184

185185
if (__DEV__) {
186-
expect(async () => {
186+
await expect(async () => {
187187
await waitForAll(['App', 'Loading...']);
188188
}).toErrorDev([
189189
'Invalid key type. Expected a string, number, symbol, or ' +

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ describe('ReactIncrementalScheduling', () => {
9393
expect(ReactNoop).toMatchRenderedOutput(<span prop={5} />);
9494
});
9595

96-
it('works on deferred roots in the order they were scheduled', () => {
96+
it('works on deferred roots in the order they were scheduled', async () => {
9797
const {useEffect} = React;
9898
function Text({text}) {
9999
useEffect(() => {
@@ -114,7 +114,7 @@ describe('ReactIncrementalScheduling', () => {
114114
expect(ReactNoop.getChildrenAsJSX('c')).toEqual('c:1');
115115

116116
// Schedule deferred work in the reverse order
117-
act(async () => {
117+
await act(async () => {
118118
React.startTransition(() => {
119119
ReactNoop.renderToRootWithID(<Text text="c:2" />, 'c');
120120
ReactNoop.renderToRootWithID(<Text text="b:2" />, 'b');

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -518,7 +518,7 @@ describe('ReactIncrementalUpdates', () => {
518518
expect(ReactNoop).toMatchRenderedOutput(<span prop="derived state" />);
519519
});
520520

521-
it('regression: does not expire soon due to layout effects in the last batch', () => {
521+
it('regression: does not expire soon due to layout effects in the last batch', async () => {
522522
const {useState, useLayoutEffect} = React;
523523

524524
let setCount;
@@ -533,7 +533,7 @@ describe('ReactIncrementalUpdates', () => {
533533
return null;
534534
}
535535

536-
act(async () => {
536+
await act(async () => {
537537
React.startTransition(() => {
538538
ReactNoop.render(<App />);
539539
});

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy