mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 12:20:20 +01:00
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.
380 lines
11 KiB
JavaScript
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;
|
|
}
|
|
}
|