Files
react/packages/internal-test-utils/internalAct.js
Sam Zhou e210d08180 [flow] Upgrade Flow to 0.245.2 (#30919)
## Summary

This PR bumps Flow all the way to the latest 0.245.2. 

Most of the suppressions comes from Flow v0.239.0's change to include
undefined in the return of `Array.pop`.

I also enabled `react.custom_jsx_typing=true` and added custom jsx
typing to match the old behavior that `React.createElement` is
effectively any typed. This is necessary since various builtin
components like `React.Fragment` is actually symbol in the React repo
instead of `React.AbstractComponent<...>`. It can be made more accurate
by customizing the `React$CustomJSXFactory` type, but I will leave it to
the React team to decide.

## How did you test this change?

`yarn flow` for all the renderers
2024-09-09 08:41:44 -07:00

284 lines
10 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';
import {assertConsoleLogsCleared} from './consoleMock';
import {diff} from 'jest-diff';
export let actingUpdatesScopeDepth: number = 0;
export const thrownErrors: Array<mixed> = [];
async function waitForMicrotasks() {
return new Promise(resolve => {
enqueueTask(() => resolve());
});
}
function aggregateErrors(errors: Array<mixed>): mixed {
if (errors.length > 1 && typeof AggregateError === 'function') {
// eslint-disable-next-line no-undef
return new AggregateError(errors);
}
return errors[0];
}
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.',
);
}
const actualYields = Scheduler.unstable_clearLog();
if (actualYields.length !== 0) {
const error = Error(
'Log of yielded values is not empty. Call assertLog first.\n\n' +
`Received:\n${diff('', actualYields.join('\n'), {
omitAnnotationLines: true,
})}`,
);
Error.captureStackTrace(error, act);
throw error;
}
// We require every `act` call to assert console logs
// with one of the assertion helpers. Fails if not empty.
assertConsoleLogsCleared();
// $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();
const errorHandlerDOM = function (event: ErrorEvent) {
// Prevent logs from reprinting this error.
event.preventDefault();
thrownErrors.push(event.error);
};
const errorHandlerNode = function (err: mixed) {
thrownErrors.push(err);
};
// We track errors that were logged globally as if they occurred in this scope and then rethrow them.
if (actingUpdatesScopeDepth === 1) {
if (
typeof window === 'object' &&
typeof window.addEventListener === 'function'
) {
// We're in a JS DOM environment.
window.addEventListener('error', errorHandlerDOM);
} else if (typeof process === 'object') {
// Node environment
process.on('uncaughtException', errorHandlerNode);
}
}
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);
if (thrownErrors.length > 0) {
// Rethrow any errors logged by the global error handling.
const thrownError = aggregateErrors(thrownErrors);
thrownErrors.length = 0;
throw thrownError;
}
// $FlowFixMe[incompatible-return]
return result;
} finally {
const depth = actingUpdatesScopeDepth;
if (depth === 1) {
if (
typeof window === 'object' &&
typeof window.addEventListener === 'function'
) {
// We're in a JS DOM environment.
window.removeEventListener('error', errorHandlerDOM);
} else if (typeof process === 'object') {
// Node environment
process.off('uncaughtException', errorHandlerNode);
}
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;
}
}
}
export async function serverAct<T>(scope: () => Thenable<T>): Thenable<T> {
// We require every `act` call to assert console logs
// with one of the assertion helpers. Fails if not empty.
assertConsoleLogsCleared();
// $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).',
);
}
// 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();
const errorHandlerNode = function (err: mixed) {
thrownErrors.push(err);
};
// We track errors that were logged globally as if they occurred in this scope and then rethrow them.
if (typeof process === 'object') {
// Node environment
process.on('uncaughtException', errorHandlerNode);
} else if (
typeof window === 'object' &&
typeof window.addEventListener === 'function'
) {
throw new Error('serverAct is not supported in JSDOM environments');
}
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;
}
// $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();
} else {
break;
}
} while (true);
if (thrownErrors.length > 0) {
// Rethrow any errors logged by the global error handling.
const thrownError = aggregateErrors(thrownErrors);
thrownErrors.length = 0;
throw thrownError;
}
// $FlowFixMe[incompatible-return]
return result;
} finally {
if (typeof process === 'object') {
// Node environment
process.off('uncaughtException', errorHandlerNode);
}
}
}