react/packages/internal-test-utils/ReactInternalTestUtils.js
Josh Story dc32c7f35e
[Flight] use microtask for scheduling during prerenders (#30768)
In https://github.com/facebook/react/pull/29491 I updated the work
scheduler for Flight to use microtasks to perform work when something
pings. This is useful but it does have some downsides in terms of our
ability to do task prioritization. Additionally the initial work is not
instantiated using a microtask which is inconsistent with how pings
work.

In this change I update the scheduling logic to use microtasks
consistently for prerenders and use regular tasks for renders both for
the initial work and pings.
2024-08-20 21:43:21 -07:00

379 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 {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;
}
}