react/packages/react-dom/src/test-utils/ReactTestUtils.js

745 lines
20 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.
*
* @noflow
*/
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import {findCurrentFiberUsingSlowPath} from 'react-reconciler/src/ReactFiberTreeReflection';
import {get as getInstance} from 'shared/ReactInstanceMap';
import {
ClassComponent,
FunctionComponent,
HostComponent,
HostResource,
HostSingleton,
HostText,
} from 'react-reconciler/src/ReactWorkTags';
import {SyntheticEvent} from 'react-dom-bindings/src/events/SyntheticEvent';
import {ELEMENT_NODE} from 'react-dom-bindings/src/shared/HTMLNodeType';
import {
rethrowCaughtError,
invokeGuardedCallbackAndCatchFirstError,
} from 'shared/ReactErrorUtils';
import {enableFloat, enableHostSingletons} from 'shared/ReactFeatureFlags';
import assign from 'shared/assign';
import isArray from 'shared/isArray';
// Keep in sync with ReactDOM.js:
const SecretInternals =
ReactDOM.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;
const EventInternals = SecretInternals.Events;
const getInstanceFromNode = EventInternals[0];
const getNodeFromInstance = EventInternals[1];
const getFiberCurrentPropsFromNode = EventInternals[2];
const enqueueStateRestore = EventInternals[3];
const restoreStateIfNeeded = EventInternals[4];
const act = React.unstable_act;
function Event(suffix) {}
let hasWarnedAboutDeprecatedMockComponent = false;
/**
* @class ReactTestUtils
*/
function findAllInRenderedFiberTreeInternal(fiber, test) {
if (!fiber) {
return [];
}
const currentParent = findCurrentFiberUsingSlowPath(fiber);
if (!currentParent) {
return [];
}
let node = currentParent;
const ret = [];
while (true) {
if (
node.tag === HostComponent ||
node.tag === HostText ||
node.tag === ClassComponent ||
node.tag === FunctionComponent ||
(enableFloat ? node.tag === HostResource : false) ||
(enableHostSingletons ? node.tag === HostSingleton : false)
) {
const publicInst = node.stateNode;
if (test(publicInst)) {
ret.push(publicInst);
}
}
if (node.child) {
node.child.return = node;
node = node.child;
continue;
}
if (node === currentParent) {
return ret;
}
while (!node.sibling) {
if (!node.return || node.return === currentParent) {
return ret;
}
node = node.return;
}
node.sibling.return = node.return;
node = node.sibling;
}
}
function validateClassInstance(inst, methodName) {
if (!inst) {
// This is probably too relaxed but it's existing behavior.
return;
}
if (getInstance(inst)) {
// This is a public instance indeed.
return;
}
let received;
const stringified = String(inst);
if (isArray(inst)) {
received = 'an array';
} else if (inst && inst.nodeType === ELEMENT_NODE && inst.tagName) {
received = 'a DOM node';
} else if (stringified === '[object Object]') {
received = 'object with keys {' + Object.keys(inst).join(', ') + '}';
} else {
received = stringified;
}
throw new Error(
`${methodName}(...): the first argument must be a React class instance. ` +
`Instead received: ${received}.`,
);
}
/**
* Utilities for making it easy to test React components.
*
* See https://reactjs.org/docs/test-utils.html
*
* Todo: Support the entire DOM.scry query syntax. For now, these simple
* utilities will suffice for testing purposes.
* @lends ReactTestUtils
*/
function renderIntoDocument(element) {
const div = document.createElement('div');
// None of our tests actually require attaching the container to the
// DOM, and doing so creates a mess that we rely on test isolation to
// clean up, so we're going to stop honoring the name of this method
// (and probably rename it eventually) if no problems arise.
// document.documentElement.appendChild(div);
return ReactDOM.render(element, div);
}
function isElement(element) {
return React.isValidElement(element);
}
function isElementOfType(inst, convenienceConstructor) {
return React.isValidElement(inst) && inst.type === convenienceConstructor;
}
function isDOMComponent(inst) {
return !!(inst && inst.nodeType === ELEMENT_NODE && inst.tagName);
}
function isDOMComponentElement(inst) {
return !!(inst && React.isValidElement(inst) && !!inst.tagName);
}
function isCompositeComponent(inst) {
if (isDOMComponent(inst)) {
// Accessing inst.setState warns; just return false as that'll be what
// this returns when we have DOM nodes as refs directly
return false;
}
return (
inst != null &&
typeof inst.render === 'function' &&
typeof inst.setState === 'function'
);
}
function isCompositeComponentWithType(inst, type) {
if (!isCompositeComponent(inst)) {
return false;
}
const internalInstance = getInstance(inst);
const constructor = internalInstance.type;
return constructor === type;
}
function findAllInRenderedTree(inst, test) {
validateClassInstance(inst, 'findAllInRenderedTree');
if (!inst) {
return [];
}
const internalInstance = getInstance(inst);
return findAllInRenderedFiberTreeInternal(internalInstance, test);
}
/**
* Finds all instances of components in the rendered tree that are DOM
* components with the class name matching `className`.
* @return {array} an array of all the matches.
*/
function scryRenderedDOMComponentsWithClass(root, classNames) {
validateClassInstance(root, 'scryRenderedDOMComponentsWithClass');
return findAllInRenderedTree(root, function(inst) {
if (isDOMComponent(inst)) {
let className = inst.className;
if (typeof className !== 'string') {
// SVG, probably.
className = inst.getAttribute('class') || '';
}
const classList = className.split(/\s+/);
if (!isArray(classNames)) {
if (classNames === undefined) {
throw new Error(
'TestUtils.scryRenderedDOMComponentsWithClass expects a ' +
'className as a second argument.',
);
}
classNames = classNames.split(/\s+/);
}
return classNames.every(function(name) {
return classList.indexOf(name) !== -1;
});
}
return false;
});
}
/**
* Like scryRenderedDOMComponentsWithClass but expects there to be one result,
* and returns that one result, or throws exception if there is any other
* number of matches besides one.
* @return {!ReactDOMComponent} The one match.
*/
function findRenderedDOMComponentWithClass(root, className) {
validateClassInstance(root, 'findRenderedDOMComponentWithClass');
const all = scryRenderedDOMComponentsWithClass(root, className);
if (all.length !== 1) {
throw new Error(
'Did not find exactly one match (found: ' +
all.length +
') ' +
'for class:' +
className,
);
}
return all[0];
}
/**
* Finds all instances of components in the rendered tree that are DOM
* components with the tag name matching `tagName`.
* @return {array} an array of all the matches.
*/
function scryRenderedDOMComponentsWithTag(root, tagName) {
validateClassInstance(root, 'scryRenderedDOMComponentsWithTag');
return findAllInRenderedTree(root, function(inst) {
return (
isDOMComponent(inst) &&
inst.tagName.toUpperCase() === tagName.toUpperCase()
);
});
}
/**
* Like scryRenderedDOMComponentsWithTag but expects there to be one result,
* and returns that one result, or throws exception if there is any other
* number of matches besides one.
* @return {!ReactDOMComponent} The one match.
*/
function findRenderedDOMComponentWithTag(root, tagName) {
validateClassInstance(root, 'findRenderedDOMComponentWithTag');
const all = scryRenderedDOMComponentsWithTag(root, tagName);
if (all.length !== 1) {
throw new Error(
'Did not find exactly one match (found: ' +
all.length +
') ' +
'for tag:' +
tagName,
);
}
return all[0];
}
/**
* Finds all instances of components with type equal to `componentType`.
* @return {array} an array of all the matches.
*/
function scryRenderedComponentsWithType(root, componentType) {
validateClassInstance(root, 'scryRenderedComponentsWithType');
return findAllInRenderedTree(root, function(inst) {
return isCompositeComponentWithType(inst, componentType);
});
}
/**
* Same as `scryRenderedComponentsWithType` but expects there to be one result
* and returns that one result, or throws exception if there is any other
* number of matches besides one.
* @return {!ReactComponent} The one match.
*/
function findRenderedComponentWithType(root, componentType) {
validateClassInstance(root, 'findRenderedComponentWithType');
const all = scryRenderedComponentsWithType(root, componentType);
if (all.length !== 1) {
throw new Error(
'Did not find exactly one match (found: ' +
all.length +
') ' +
'for componentType:' +
componentType,
);
}
return all[0];
}
/**
* Pass a mocked component module to this method to augment it with
* useful methods that allow it to be used as a dummy React component.
* Instead of rendering as usual, the component will become a simple
* <div> containing any provided children.
*
* @param {object} module the mock function object exported from a
* module that defines the component to be mocked
* @param {?string} mockTagName optional dummy root tag name to return
* from render method (overrides
* module.mockTagName if provided)
* @return {object} the ReactTestUtils object (for chaining)
*/
function mockComponent(module, mockTagName) {
if (__DEV__) {
if (!hasWarnedAboutDeprecatedMockComponent) {
hasWarnedAboutDeprecatedMockComponent = true;
console.warn(
'ReactTestUtils.mockComponent() is deprecated. ' +
'Use shallow rendering or jest.mock() instead.\n\n' +
'See https://reactjs.org/link/test-utils-mock-component for more information.',
);
}
}
mockTagName = mockTagName || module.mockTagName || 'div';
module.prototype.render.mockImplementation(function() {
return React.createElement(mockTagName, null, this.props.children);
});
return this;
}
function nativeTouchData(x, y) {
return {
touches: [{pageX: x, pageY: y}],
};
}
// Start of inline: the below functions were inlined from
// EventPropagator.js, as they deviated from ReactDOM's newer
// implementations.
/**
* Dispatch the event to the listener.
* @param {SyntheticEvent} event SyntheticEvent to handle
* @param {function} listener Application-level callback
* @param {*} inst Internal component instance
*/
function executeDispatch(event, listener, inst) {
const type = event.type || 'unknown-event';
event.currentTarget = getNodeFromInstance(inst);
invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event);
event.currentTarget = null;
}
/**
* Standard/simple iteration through an event's collected dispatches.
*/
function executeDispatchesInOrder(event) {
const dispatchListeners = event._dispatchListeners;
const dispatchInstances = event._dispatchInstances;
if (isArray(dispatchListeners)) {
for (let i = 0; i < dispatchListeners.length; i++) {
if (event.isPropagationStopped()) {
break;
}
// Listeners and Instances are two parallel arrays that are always in sync.
executeDispatch(event, dispatchListeners[i], dispatchInstances[i]);
}
} else if (dispatchListeners) {
executeDispatch(event, dispatchListeners, dispatchInstances);
}
event._dispatchListeners = null;
event._dispatchInstances = null;
}
/**
* Dispatches an event and releases it back into the pool, unless persistent.
*
* @param {?object} event Synthetic event to be dispatched.
* @private
*/
const executeDispatchesAndRelease = function(event /* ReactSyntheticEvent */) {
if (event) {
executeDispatchesInOrder(event);
if (!event.isPersistent()) {
event.constructor.release(event);
}
}
};
function isInteractive(tag) {
return (
tag === 'button' ||
tag === 'input' ||
tag === 'select' ||
tag === 'textarea'
);
}
function getParent(inst) {
do {
inst = inst.return;
// TODO: If this is a HostRoot we might want to bail out.
// That is depending on if we want nested subtrees (layers) to bubble
// events to their parent. We could also go through parentNode on the
// host node but that wouldn't work for React Native and doesn't let us
// do the portal feature.
} while (
inst &&
inst.tag !== HostComponent &&
(!enableHostSingletons ? true : inst.tag !== HostSingleton)
);
if (inst) {
return inst;
}
return null;
}
/**
* Simulates the traversal of a two-phase, capture/bubble event dispatch.
*/
export function traverseTwoPhase(inst, fn, arg) {
const path = [];
while (inst) {
path.push(inst);
inst = getParent(inst);
}
let i;
for (i = path.length; i-- > 0; ) {
fn(path[i], 'captured', arg);
}
for (i = 0; i < path.length; i++) {
fn(path[i], 'bubbled', arg);
}
}
function shouldPreventMouseEvent(name, type, props) {
switch (name) {
case 'onClick':
case 'onClickCapture':
case 'onDoubleClick':
case 'onDoubleClickCapture':
case 'onMouseDown':
case 'onMouseDownCapture':
case 'onMouseMove':
case 'onMouseMoveCapture':
case 'onMouseUp':
case 'onMouseUpCapture':
case 'onMouseEnter':
return !!(props.disabled && isInteractive(type));
default:
return false;
}
}
/**
* @param {object} inst The instance, which is the source of events.
* @param {string} registrationName Name of listener (e.g. `onClick`).
* @return {?function} The stored callback.
*/
function getListener(inst /* Fiber */, registrationName: string) {
// TODO: shouldPreventMouseEvent is DOM-specific and definitely should not
// live here; needs to be moved to a better place soon
const stateNode = inst.stateNode;
if (!stateNode) {
// Work in progress (ex: onload events in incremental mode).
return null;
}
const props = getFiberCurrentPropsFromNode(stateNode);
if (!props) {
// Work in progress.
return null;
}
const listener = props[registrationName];
if (shouldPreventMouseEvent(registrationName, inst.type, props)) {
return null;
}
if (listener && typeof listener !== 'function') {
throw new Error(
`Expected \`${registrationName}\` listener to be a function, instead got a value of \`${typeof listener}\` type.`,
);
}
return listener;
}
function listenerAtPhase(inst, event, propagationPhase: PropagationPhases) {
let registrationName = event._reactName;
if (propagationPhase === 'captured') {
registrationName += 'Capture';
}
return getListener(inst, registrationName);
}
function accumulateDispatches(inst, ignoredDirection, event) {
if (inst && event && event._reactName) {
const registrationName = event._reactName;
const listener = getListener(inst, registrationName);
if (listener) {
if (event._dispatchListeners == null) {
event._dispatchListeners = [];
}
if (event._dispatchInstances == null) {
event._dispatchInstances = [];
}
event._dispatchListeners.push(listener);
event._dispatchInstances.push(inst);
}
}
}
function accumulateDirectionalDispatches(inst, phase, event) {
if (__DEV__) {
if (!inst) {
console.error('Dispatching inst must not be null');
}
}
const listener = listenerAtPhase(inst, event, phase);
if (listener) {
if (event._dispatchListeners == null) {
event._dispatchListeners = [];
}
if (event._dispatchInstances == null) {
event._dispatchInstances = [];
}
event._dispatchListeners.push(listener);
event._dispatchInstances.push(inst);
}
}
function accumulateDirectDispatchesSingle(event) {
if (event && event._reactName) {
accumulateDispatches(event._targetInst, null, event);
}
}
function accumulateTwoPhaseDispatchesSingle(event) {
if (event && event._reactName) {
traverseTwoPhase(event._targetInst, accumulateDirectionalDispatches, event);
}
}
// End of inline
const Simulate = {};
const directDispatchEventTypes = new Set([
'mouseEnter',
'mouseLeave',
'pointerEnter',
'pointerLeave',
]);
/**
* Exports:
*
* - `Simulate.click(Element)`
* - `Simulate.mouseMove(Element)`
* - `Simulate.change(Element)`
* - ... (All keys from event plugin `eventTypes` objects)
*/
function makeSimulator(eventType) {
return function(domNode, eventData) {
if (React.isValidElement(domNode)) {
throw new Error(
'TestUtils.Simulate expected a DOM node as the first argument but received ' +
'a React element. Pass the DOM node you wish to simulate the event on instead. ' +
'Note that TestUtils.Simulate will not work if you are using shallow rendering.',
);
}
if (isCompositeComponent(domNode)) {
throw new Error(
'TestUtils.Simulate expected a DOM node as the first argument but received ' +
'a component instance. Pass the DOM node you wish to simulate the event on instead.',
);
}
const reactName = 'on' + eventType[0].toUpperCase() + eventType.slice(1);
const fakeNativeEvent = new Event();
fakeNativeEvent.target = domNode;
fakeNativeEvent.type = eventType.toLowerCase();
const targetInst = getInstanceFromNode(domNode);
const event = new SyntheticEvent(
reactName,
fakeNativeEvent.type,
targetInst,
fakeNativeEvent,
domNode,
);
// Since we aren't using pooling, always persist the event. This will make
// sure it's marked and won't warn when setting additional properties.
event.persist();
assign(event, eventData);
if (directDispatchEventTypes.has(eventType)) {
accumulateDirectDispatchesSingle(event);
} else {
accumulateTwoPhaseDispatchesSingle(event);
}
ReactDOM.unstable_batchedUpdates(function() {
// Normally extractEvent enqueues a state restore, but we'll just always
// do that since we're by-passing it here.
enqueueStateRestore(domNode);
executeDispatchesAndRelease(event);
rethrowCaughtError();
});
restoreStateIfNeeded();
};
}
// A one-time snapshot with no plans to update. We'll probably want to deprecate Simulate API.
const simulatedEventTypes = [
'blur',
'cancel',
'click',
'close',
'contextMenu',
'copy',
'cut',
'auxClick',
'doubleClick',
'dragEnd',
'dragStart',
'drop',
'focus',
'input',
'invalid',
'keyDown',
'keyPress',
'keyUp',
'mouseDown',
'mouseUp',
'paste',
'pause',
'play',
'pointerCancel',
'pointerDown',
'pointerUp',
'rateChange',
'reset',
'resize',
'seeked',
'submit',
'touchCancel',
'touchEnd',
'touchStart',
'volumeChange',
'drag',
'dragEnter',
'dragExit',
'dragLeave',
'dragOver',
'mouseMove',
'mouseOut',
'mouseOver',
'pointerMove',
'pointerOut',
'pointerOver',
'scroll',
'toggle',
'touchMove',
'wheel',
'abort',
'animationEnd',
'animationIteration',
'animationStart',
'canPlay',
'canPlayThrough',
'durationChange',
'emptied',
'encrypted',
'ended',
'error',
'gotPointerCapture',
'load',
'loadedData',
'loadedMetadata',
'loadStart',
'lostPointerCapture',
'playing',
'progress',
'seeking',
'stalled',
'suspend',
'timeUpdate',
'transitionEnd',
'waiting',
'mouseEnter',
'mouseLeave',
'pointerEnter',
'pointerLeave',
'change',
'select',
'beforeInput',
'compositionEnd',
'compositionStart',
'compositionUpdate',
];
function buildSimulators() {
simulatedEventTypes.forEach(eventType => {
Simulate[eventType] = makeSimulator(eventType);
});
}
buildSimulators();
export {
renderIntoDocument,
isElement,
isElementOfType,
isDOMComponent,
isDOMComponentElement,
isCompositeComponent,
isCompositeComponentWithType,
findAllInRenderedTree,
scryRenderedDOMComponentsWithClass,
findRenderedDOMComponentWithClass,
scryRenderedDOMComponentsWithTag,
findRenderedDOMComponentWithTag,
scryRenderedComponentsWithType,
findRenderedComponentWithType,
mockComponent,
nativeTouchData,
Simulate,
act,
};