mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
This updates the Suspense fuzz tester to use `act` to recursively flush timers instead of doing it manually. This still isn't great because ideally the fuzz tester wouldn't fake timers at all. It should resolve promises using a custom queue instead of Jest's fake timer queue, like we've started doing in our other Suspense tests (i.e. the `resolveText` pattern). That's because our internal `act` API (not the public one, the one we use in our tests) uses Jest's fake timer queue as a way to force Suspense fallbacks to appear. However I'm not interested in upgrading this test suite to a better strategy right now because if I were writing a Suspense fuzzer today I would probably use an entirely different approach. So this is just an incremental improvement to make it slightly less decoupled to React implementation details.
128 lines
4.6 KiB
JavaScript
128 lines
4.6 KiB
JavaScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @flow strict
|
|
*/
|
|
|
|
// This version of `act` is only used by our tests. Unlike the public version
|
|
// of `act`, it's designed to work identically in both production and
|
|
// development. It may have slightly different behavior from the public
|
|
// version, too, since our constraints in our test suite are not the same as
|
|
// those of developers using React — we're testing React itself, as opposed to
|
|
// building an app with React.
|
|
|
|
import type {Thenable} from 'shared/ReactTypes';
|
|
|
|
import * as Scheduler from 'scheduler/unstable_mock';
|
|
|
|
import enqueueTask from './enqueueTask';
|
|
|
|
let actingUpdatesScopeDepth: number = 0;
|
|
|
|
async function waitForMicrotasks() {
|
|
return new Promise(resolve => {
|
|
enqueueTask(() => resolve());
|
|
});
|
|
}
|
|
|
|
export async function act<T>(scope: () => Thenable<T>): Thenable<T> {
|
|
if (Scheduler.unstable_flushUntilNextPaint === undefined) {
|
|
throw Error(
|
|
'This version of `act` requires a special mock build of Scheduler.',
|
|
);
|
|
}
|
|
|
|
// $FlowFixMe[cannot-resolve-name]: Flow doesn't know about global Jest object
|
|
if (!jest.isMockFunction(setTimeout)) {
|
|
throw Error(
|
|
"This version of `act` requires Jest's timer mocks " +
|
|
'(i.e. jest.useFakeTimers).',
|
|
);
|
|
}
|
|
|
|
const previousIsActEnvironment = global.IS_REACT_ACT_ENVIRONMENT;
|
|
const previousActingUpdatesScopeDepth = actingUpdatesScopeDepth;
|
|
actingUpdatesScopeDepth++;
|
|
if (actingUpdatesScopeDepth === 1) {
|
|
// Because this is not the "real" `act`, we set this to `false` so React
|
|
// knows not to fire `act` warnings.
|
|
global.IS_REACT_ACT_ENVIRONMENT = false;
|
|
}
|
|
|
|
// Create the error object before doing any async work, to get a better
|
|
// stack trace.
|
|
const error = new Error();
|
|
Error.captureStackTrace(error, act);
|
|
|
|
// Call the provided scope function after an async gap. This is an extra
|
|
// precaution to ensure that our tests do not accidentally rely on the act
|
|
// scope adding work to the queue synchronously. We don't do this in the
|
|
// public version of `act`, though we maybe should in the future.
|
|
await waitForMicrotasks();
|
|
|
|
try {
|
|
const result = await scope();
|
|
|
|
do {
|
|
// Wait until end of current task/microtask.
|
|
await waitForMicrotasks();
|
|
|
|
// $FlowFixMe[cannot-resolve-name]: Flow doesn't know about global Jest object
|
|
if (jest.isEnvironmentTornDown()) {
|
|
error.message =
|
|
'The Jest environment was torn down before `act` completed. This ' +
|
|
'probably means you forgot to `await` an `act` call.';
|
|
throw error;
|
|
}
|
|
|
|
if (!Scheduler.unstable_hasPendingWork()) {
|
|
// $FlowFixMe[cannot-resolve-name]: Flow doesn't know about global Jest object
|
|
const j = jest;
|
|
if (j.getTimerCount() > 0) {
|
|
// There's a pending timer. Flush it now. We only do this in order to
|
|
// force Suspense fallbacks to display; the fact that it's a timer
|
|
// is an implementation detail. If there are other timers scheduled,
|
|
// those will also fire now, too, which is not ideal. (The public
|
|
// version of `act` doesn't do this.) For this reason, we should try
|
|
// to avoid using timers in our internal tests.
|
|
j.runOnlyPendingTimers();
|
|
// If a committing a fallback triggers another update, it might not
|
|
// get scheduled until a microtask. So wait one more time.
|
|
await waitForMicrotasks();
|
|
}
|
|
if (Scheduler.unstable_hasPendingWork()) {
|
|
// Committing a fallback scheduled additional work. Continue flushing.
|
|
} else {
|
|
// There's no pending work, even after both the microtask queue
|
|
// and the timer queue are empty. Stop flushing.
|
|
break;
|
|
}
|
|
}
|
|
// flushUntilNextPaint stops when React yields execution. Allow microtasks
|
|
// queue to flush before continuing.
|
|
Scheduler.unstable_flushUntilNextPaint();
|
|
} while (true);
|
|
|
|
return result;
|
|
} finally {
|
|
const depth = actingUpdatesScopeDepth;
|
|
if (depth === 1) {
|
|
global.IS_REACT_ACT_ENVIRONMENT = previousIsActEnvironment;
|
|
}
|
|
actingUpdatesScopeDepth = depth - 1;
|
|
|
|
if (actingUpdatesScopeDepth !== previousActingUpdatesScopeDepth) {
|
|
// if it's _less than_ previousActingUpdatesScopeDepth, then we can
|
|
// assume the 'other' one has warned
|
|
Scheduler.unstable_clearLog();
|
|
error.message =
|
|
'You seem to have overlapping act() calls, this is not supported. ' +
|
|
'Be sure to await previous act() calls before making a new one. ';
|
|
throw error;
|
|
}
|
|
}
|
|
}
|