react/packages/internal-test-utils/ReactInternalTestUtils.js
Hendrik Liebau c03a51d836
Move getDebugInfo test util function to internal-test-utils (#34523)
In an upstack PR, I need `getDebugInfo` in another test file, so I'm
moving it to `internal-test-utils` so it can be shared.
2025-09-18 21:32:36 +02:00

380 lines
11 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.
*/
import * as SchedulerMock from 'scheduler/unstable_mock';
import {diff} from 'jest-diff';
import {equals} from '@jest/expect-utils';
import enqueueTask from './enqueueTask';
import simulateBrowserEventDispatch from './simulateBrowserEventDispatch';
import {
clearLogs,
clearWarnings,
clearErrors,
createLogAssertion,
} from './consoleMock';
export {getDebugInfo} from './debugInfo';
export {act, serverAct} from './internalAct';
const {assertConsoleLogsCleared} = require('internal-test-utils/consoleMock');
import {thrownErrors, actingUpdatesScopeDepth} from './internalAct';
function assertYieldsWereCleared(caller) {
const actualYields = SchedulerMock.unstable_clearLog();
if (actualYields.length !== 0) {
const error = Error(
'The event log is not empty. Call assertLog(...) first.',
);
Error.captureStackTrace(error, caller);
throw error;
}
assertConsoleLogsCleared();
}
export async function waitForMicrotasks() {
return new Promise(resolve => {
enqueueTask(() => resolve());
});
}
export async function waitFor(expectedLog, options) {
assertYieldsWereCleared(waitFor);
// Create the error object before doing any async work, to get a better
// stack trace.
const error = new Error();
Error.captureStackTrace(error, waitFor);
const stopAfter = expectedLog.length;
const actualLog = [];
do {
// Wait until end of current task/microtask.
await waitForMicrotasks();
if (SchedulerMock.unstable_hasPendingWork()) {
SchedulerMock.unstable_flushNumberOfYields(stopAfter - actualLog.length);
actualLog.push(...SchedulerMock.unstable_clearLog());
if (stopAfter > actualLog.length) {
// Continue flushing until we've logged the expected number of items.
} else {
// Once we've reached the expected sequence, wait one more microtask to
// flush any remaining synchronous work.
await waitForMicrotasks();
actualLog.push(...SchedulerMock.unstable_clearLog());
break;
}
} else {
// There's no pending work, even after a microtask.
break;
}
} while (true);
if (options && options.additionalLogsAfterAttemptingToYield) {
expectedLog = expectedLog.concat(
options.additionalLogsAfterAttemptingToYield,
);
}
if (equals(actualLog, expectedLog)) {
return;
}
error.message = `
Expected sequence of events did not occur.
${diff(expectedLog, actualLog)}
`;
throw error;
}
export async function waitForAll(expectedLog) {
assertYieldsWereCleared(waitForAll);
// Create the error object before doing any async work, to get a better
// stack trace.
const error = new Error();
Error.captureStackTrace(error, waitForAll);
do {
// Wait until end of current task/microtask.
await waitForMicrotasks();
if (!SchedulerMock.unstable_hasPendingWork()) {
// There's no pending work, even after a microtask. Stop flushing.
break;
}
SchedulerMock.unstable_flushAllWithoutAsserting();
} while (true);
const actualLog = SchedulerMock.unstable_clearLog();
if (equals(actualLog, expectedLog)) {
return;
}
error.message = `
Expected sequence of events did not occur.
${diff(expectedLog, actualLog)}
`;
throw error;
}
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 waitForThrow(expectedError: mixed): mixed {
assertYieldsWereCleared(waitForThrow);
// Create the error object before doing any async work, to get a better
// stack trace.
const error = new Error();
Error.captureStackTrace(error, waitForThrow);
do {
// Wait until end of current task/microtask.
await waitForMicrotasks();
if (!SchedulerMock.unstable_hasPendingWork()) {
// There's no pending work, even after a microtask. Stop flushing.
error.message = 'Expected something to throw, but nothing did.';
throw error;
}
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 === 0) {
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 {
SchedulerMock.unstable_flushAllWithoutAsserting();
} catch (x) {
thrownErrors.push(x);
} finally {
if (actingUpdatesScopeDepth === 0) {
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);
}
}
}
if (thrownErrors.length > 0) {
const thrownError = aggregateErrors(thrownErrors);
thrownErrors.length = 0;
if (expectedError === undefined) {
// If no expected error was provided, then assume the caller is OK with
// any error being thrown. We're returning the error so they can do
// their own checks, if they wish.
return thrownError;
}
if (equals(thrownError, expectedError)) {
return thrownError;
}
if (
typeof expectedError === 'string' &&
typeof thrownError === 'object' &&
thrownError !== null &&
typeof thrownError.message === 'string'
) {
if (thrownError.message.includes(expectedError)) {
return thrownError;
} else {
error.message = `
Expected error was not thrown.
${diff(expectedError, thrownError.message)}
`;
throw error;
}
}
error.message = `
Expected error was not thrown.
${diff(expectedError, thrownError)}
`;
throw error;
}
} while (true);
}
// This is prefixed with `unstable_` because you should almost always try to
// avoid using it in tests. It's really only for testing a particular
// implementation detail (update starvation prevention).
export async function unstable_waitForExpired(expectedLog): mixed {
assertYieldsWereCleared(unstable_waitForExpired);
// Create the error object before doing any async work, to get a better
// stack trace.
const error = new Error();
Error.captureStackTrace(error, unstable_waitForExpired);
// Wait until end of current task/microtask.
await waitForMicrotasks();
SchedulerMock.unstable_flushExpired();
const actualLog = SchedulerMock.unstable_clearLog();
if (equals(actualLog, expectedLog)) {
return;
}
error.message = `
Expected sequence of events did not occur.
${diff(expectedLog, actualLog)}
`;
throw error;
}
// TODO: This name is a bit misleading currently because it will stop as soon as
// React yields for any reason, not just for a paint. I've left it this way for
// now because that's how untable_flushUntilNextPaint already worked, but maybe
// we should split these use cases into separate APIs.
export async function waitForPaint(expectedLog) {
assertYieldsWereCleared(waitForPaint);
// Create the error object before doing any async work, to get a better
// stack trace.
const error = new Error();
Error.captureStackTrace(error, waitForPaint);
// Wait until end of current task/microtask.
await waitForMicrotasks();
if (SchedulerMock.unstable_hasPendingWork()) {
// Flush until React yields.
SchedulerMock.unstable_flushUntilNextPaint();
// Wait one more microtask to flush any remaining synchronous work.
await waitForMicrotasks();
}
const actualLog = SchedulerMock.unstable_clearLog();
if (equals(actualLog, expectedLog)) {
return;
}
error.message = `
Expected sequence of events did not occur.
${diff(expectedLog, actualLog)}
`;
throw error;
}
export async function waitForDiscrete(expectedLog) {
assertYieldsWereCleared(waitForDiscrete);
// Create the error object before doing any async work, to get a better
// stack trace.
const error = new Error();
Error.captureStackTrace(error, waitForDiscrete);
// Wait until end of current task/microtask.
await waitForMicrotasks();
const actualLog = SchedulerMock.unstable_clearLog();
if (equals(actualLog, expectedLog)) {
return;
}
error.message = `
Expected sequence of events did not occur.
${diff(expectedLog, actualLog)}
`;
throw error;
}
export function assertLog(expectedLog) {
const actualLog = SchedulerMock.unstable_clearLog();
if (equals(actualLog, expectedLog)) {
return;
}
const error = new Error(`
Expected sequence of events did not occur.
${diff(expectedLog, actualLog)}
`);
Error.captureStackTrace(error, assertLog);
throw error;
}
export const assertConsoleLogDev = createLogAssertion(
'log',
'assertConsoleLogDev',
clearLogs,
);
export const assertConsoleWarnDev = createLogAssertion(
'warn',
'assertConsoleWarnDev',
clearWarnings,
);
export const assertConsoleErrorDev = createLogAssertion(
'error',
'assertConsoleErrorDev',
clearErrors,
);
// Simulates dispatching events, waiting for microtasks in between.
// This matches the browser behavior, which will flush microtasks
// between each event handler. This will allow discrete events to
// flush between events across different event handlers.
export async function simulateEventDispatch(
node: Node,
eventType: string,
): Promise<void> {
// Ensure the node is in the document.
for (let current = node; current; current = current.parentNode) {
if (current === document) {
break;
} else if (current.parentNode == null) {
return;
}
}
const customEvent = new Event(eventType, {
bubbles: true,
});
Object.defineProperty(customEvent, 'target', {
// Override the target to the node on which we dispatched the event.
value: node,
});
const impl = Object.getOwnPropertySymbols(node)[0];
const oldDispatch = node[impl].dispatchEvent;
try {
node[impl].dispatchEvent = simulateBrowserEventDispatch;
await node.dispatchEvent(customEvent);
} finally {
node[impl].dispatchEvent = oldDispatch;
}
}