mirror of
https://github.com/facebook/react.git
synced 2026-02-27 03:07:57 +00:00
## Overview **Internal React repo tests only** Depends on https://github.com/facebook/react/pull/28710 Adds three new assertions: - `assertConsoleLogDev` - `assertConsoleWarnDev` - `assertConsoleErrorDev` These will replace this pattern: ```js await expect(async () => { await expect(async () => { await act(() => { root.render(<Fail />) }); }).toThrow(); }).toWarnDev('Warning'); ``` With this: ```js await expect(async () => { await act(() => { root.render(<Fail />) }); }).toThrow(); assertConsoleWarnDev('Warning'); ``` It works similar to our other `assertLog` matchers which clear the log and assert on it, failing the tests if the log is not asserted before the test ends. ## Diffs There are a few improvements I also added including better log diffs and more logging. When there's a failure, the output will look something like: <img width="655" alt="Screenshot 2024-04-03 at 11 50 08 AM" src="https://github.com/facebook/react/assets/2440089/0c4bf1b2-5f63-4204-8af3-09e0c2d752ad"> Check out the test suite for snapshots of all the failures we may log.
195 lines
6.8 KiB
JavaScript
195 lines
6.8 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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|