diff --git a/packages/react-dom/src/__tests__/ReactTestUtils-test.js b/packages/react-dom/src/__tests__/ReactTestUtils-test.js index 80871a12d8e52..842fb4c1ebc72 100644 --- a/packages/react-dom/src/__tests__/ReactTestUtils-test.js +++ b/packages/react-dom/src/__tests__/ReactTestUtils-test.js @@ -9,11 +9,11 @@ 'use strict'; -import {act} from 'internal-test-utils'; -import * as React from 'react'; -import * as ReactDOMClient from 'react-dom/client'; -import * as ReactDOMServer from 'react-dom/server'; -import * as ReactTestUtils from 'react-dom/test-utils'; +let React; +let ReactDOMClient; +let ReactDOMServer; +let ReactTestUtils; +let act; function getTestDocument(markup) { const doc = document.implementation.createHTMLDocument(''); @@ -27,8 +27,105 @@ function getTestDocument(markup) { } describe('ReactTestUtils', () => { + beforeEach(() => { + React = require('react'); + ReactDOMClient = require('react-dom/client'); + ReactDOMServer = require('react-dom/server'); + ReactTestUtils = require('react-dom/test-utils'); + act = require('internal-test-utils').act; + }); + + // @gate !disableDOMTestUtils it('Simulate should have locally attached media events', () => { - expect(Object.keys(ReactTestUtils.Simulate).sort()).toMatchSnapshot(); + expect(Object.keys(ReactTestUtils.Simulate).sort()).toMatchInlineSnapshot(` + [ + "abort", + "animationEnd", + "animationIteration", + "animationStart", + "auxClick", + "beforeInput", + "blur", + "canPlay", + "canPlayThrough", + "cancel", + "change", + "click", + "close", + "compositionEnd", + "compositionStart", + "compositionUpdate", + "contextMenu", + "copy", + "cut", + "doubleClick", + "drag", + "dragEnd", + "dragEnter", + "dragExit", + "dragLeave", + "dragOver", + "dragStart", + "drop", + "durationChange", + "emptied", + "encrypted", + "ended", + "error", + "focus", + "gotPointerCapture", + "input", + "invalid", + "keyDown", + "keyPress", + "keyUp", + "load", + "loadStart", + "loadedData", + "loadedMetadata", + "lostPointerCapture", + "mouseDown", + "mouseEnter", + "mouseLeave", + "mouseMove", + "mouseOut", + "mouseOver", + "mouseUp", + "paste", + "pause", + "play", + "playing", + "pointerCancel", + "pointerDown", + "pointerEnter", + "pointerLeave", + "pointerMove", + "pointerOut", + "pointerOver", + "pointerUp", + "progress", + "rateChange", + "reset", + "resize", + "scroll", + "seeked", + "seeking", + "select", + "stalled", + "submit", + "suspend", + "timeUpdate", + "toggle", + "touchCancel", + "touchEnd", + "touchMove", + "touchStart", + "transitionEnd", + "volumeChange", + "waiting", + "wheel", + ] + `); }); // @gate !disableDOMTestUtils @@ -575,14 +672,6 @@ describe('ReactTestUtils', () => { }); expect(idCallOrder).toEqual([CHILD]); }); - - // @gate disableDOMTestUtils - it('throws', async () => { - expect(ReactTestUtils.Simulate.click).toThrow( - '`Simulate` was removed from `react-dom/test-utils`. ' + - 'See https://react.dev/warnings/react-dom-test-utils for more info.', - ); - }); }); // @gate !disableDOMTestUtils @@ -626,14 +715,6 @@ describe('ReactTestUtils', () => { expect(renderedComponentType).toBe(instance); }); - // @gate disableDOMTestUtils - it('throws on every removed function', async () => { - expect(ReactTestUtils.isDOMComponent).toThrow( - '`isDOMComponent` was removed from `react-dom/test-utils`. ' + - 'See https://react.dev/warnings/react-dom-test-utils for more info.', - ); - }); - // @gate __DEV__ it('warns when using `act`', () => { expect(() => { diff --git a/packages/react-dom/src/__tests__/__snapshots__/ReactTestUtils-test.js.snap b/packages/react-dom/src/__tests__/__snapshots__/ReactTestUtils-test.js.snap deleted file mode 100644 index 8dda81dcae42b..0000000000000 --- a/packages/react-dom/src/__tests__/__snapshots__/ReactTestUtils-test.js.snap +++ /dev/null @@ -1,91 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ReactTestUtils Simulate should have locally attached media events 1`] = ` -[ - "abort", - "animationEnd", - "animationIteration", - "animationStart", - "auxClick", - "beforeInput", - "blur", - "canPlay", - "canPlayThrough", - "cancel", - "change", - "click", - "close", - "compositionEnd", - "compositionStart", - "compositionUpdate", - "contextMenu", - "copy", - "cut", - "doubleClick", - "drag", - "dragEnd", - "dragEnter", - "dragExit", - "dragLeave", - "dragOver", - "dragStart", - "drop", - "durationChange", - "emptied", - "encrypted", - "ended", - "error", - "focus", - "gotPointerCapture", - "input", - "invalid", - "keyDown", - "keyPress", - "keyUp", - "load", - "loadStart", - "loadedData", - "loadedMetadata", - "lostPointerCapture", - "mouseDown", - "mouseEnter", - "mouseLeave", - "mouseMove", - "mouseOut", - "mouseOver", - "mouseUp", - "paste", - "pause", - "play", - "playing", - "pointerCancel", - "pointerDown", - "pointerEnter", - "pointerLeave", - "pointerMove", - "pointerOut", - "pointerOver", - "pointerUp", - "progress", - "rateChange", - "reset", - "resize", - "scroll", - "seeked", - "seeking", - "select", - "stalled", - "submit", - "suspend", - "timeUpdate", - "toggle", - "touchCancel", - "touchEnd", - "touchMove", - "touchStart", - "transitionEnd", - "volumeChange", - "waiting", - "wheel", -] -`; diff --git a/packages/react-dom/src/test-utils/ReactTestUtils.js b/packages/react-dom/src/test-utils/ReactTestUtils.js index 1b4125c819e61..942600954d5d6 100644 --- a/packages/react-dom/src/test-utils/ReactTestUtils.js +++ b/packages/react-dom/src/test-utils/ReactTestUtils.js @@ -8,35 +8,9 @@ */ 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, - HostHoistable, - HostSingleton, - HostText, -} from 'react-reconciler/src/ReactWorkTags'; -import {SyntheticEvent} from 'react-dom-bindings/src/events/SyntheticEvent'; -import {ELEMENT_NODE} from 'react-dom-bindings/src/client/HTMLNodeType'; -import {disableDOMTestUtils} 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]; let didWarnAboutUsingAct = false; -function act(callback) { +export function act(callback) { if (didWarnAboutUsingAct === false) { didWarnAboutUsingAct = true; console.error( @@ -47,834 +21,3 @@ function act(callback) { } return React.act(callback); } - -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 || - node.tag === HostHoistable || - node.tag === HostSingleton - ) { - 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( - `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) { - if (disableDOMTestUtils) { - throw new Error( - '`renderIntoDocument` was removed from `react-dom/test-utils`. ' + - 'See https://react.dev/warnings/react-dom-test-utils for more info.', - ); - } - - 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) { - if (disableDOMTestUtils) { - throw new Error( - '`isElement` was removed from `react-dom/test-utils`. ' + - 'See https://react.dev/warnings/react-dom-test-utils for more info.', - ); - } - - return React.isValidElement(element); -} - -function isElementOfType(inst, convenienceConstructor) { - if (disableDOMTestUtils) { - throw new Error( - '`isElementOfType` was removed from `react-dom/test-utils`. ' + - 'See https://react.dev/warnings/react-dom-test-utils for more info.', - ); - } - - return React.isValidElement(inst) && inst.type === convenienceConstructor; -} - -function isDOMComponent(inst) { - if (disableDOMTestUtils) { - throw new Error( - '`isDOMComponent` was removed from `react-dom/test-utils`. ' + - 'See https://react.dev/warnings/react-dom-test-utils for more info.', - ); - } - - return !!(inst && inst.nodeType === ELEMENT_NODE && inst.tagName); -} - -function isDOMComponentElement(inst) { - if (disableDOMTestUtils) { - throw new Error( - '`isDOMComponentElement` was removed from `react-dom/test-utils`. ' + - 'See https://react.dev/warnings/react-dom-test-utils for more info.', - ); - } - - return !!(inst && React.isValidElement(inst) && !!inst.tagName); -} - -function isCompositeComponent(inst) { - if (disableDOMTestUtils) { - throw new Error( - '`isCompositeComponent` was removed from `react-dom/test-utils`. ' + - 'See https://react.dev/warnings/react-dom-test-utils for more info.', - ); - } - - 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 (disableDOMTestUtils) { - throw new Error( - '`isCompositeComponentWithType` was removed from `react-dom/test-utils`. ' + - 'See https://react.dev/warnings/react-dom-test-utils for more info.', - ); - } - - if (!isCompositeComponent(inst)) { - return false; - } - const internalInstance = getInstance(inst); - const constructor = internalInstance.type; - return constructor === type; -} - -function findAllInRenderedTree(inst, test) { - if (disableDOMTestUtils) { - throw new Error( - '`findAllInRenderedTree` was removed from `react-dom/test-utils`. ' + - 'See https://react.dev/warnings/react-dom-test-utils for more info.', - ); - } - - 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) { - if (disableDOMTestUtils) { - throw new Error( - '`scryRenderedDOMComponentsWithClass` was removed from `react-dom/test-utils`. ' + - 'See https://react.dev/warnings/react-dom-test-utils for more info.', - ); - } - - 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) { - if (disableDOMTestUtils) { - throw new Error( - '`findRenderedDOMComponentWithClass` was removed from `react-dom/test-utils`. ' + - 'See https://react.dev/warnings/react-dom-test-utils for more info.', - ); - } - - 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) { - if (disableDOMTestUtils) { - throw new Error( - '`scryRenderedDOMComponentsWithTag` was removed from `react-dom/test-utils`. ' + - 'See https://react.dev/warnings/react-dom-test-utils for more info.', - ); - } - - 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) { - if (disableDOMTestUtils) { - throw new Error( - '`findRenderedDOMComponentWithTag` was removed from `react-dom/test-utils`. ' + - 'See https://react.dev/warnings/react-dom-test-utils for more info.', - ); - } - - 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) { - if (disableDOMTestUtils) { - throw new Error( - '`scryRenderedComponentsWithType` was removed from `react-dom/test-utils`. ' + - 'See https://react.dev/warnings/react-dom-test-utils for more info.', - ); - } - - 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) { - if (disableDOMTestUtils) { - throw new Error( - '`findRenderedComponentWithType` was removed from `react-dom/test-utils`. ' + - 'See https://react.dev/warnings/react-dom-test-utils for more info.', - ); - } - - 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 - *
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 (disableDOMTestUtils) { - throw new Error( - '`mockComponent` was removed from `react-dom/test-utils`. ' + - 'See https://react.dev/warnings/react-dom-test-utils for more info.', - ); - } - - if (__DEV__) { - if (!hasWarnedAboutDeprecatedMockComponent) { - hasWarnedAboutDeprecatedMockComponent = true; - console.warn( - 'ReactTestUtils.mockComponent() is deprecated. ' + - 'Use shallow rendering or jest.mock() instead.\n\n' + - 'See https://react.dev/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) { - if (disableDOMTestUtils) { - throw new Error( - '`nativeTouchData` was removed from `react-dom/test-utils`. ' + - 'See https://react.dev/warnings/react-dom-test-utils for more info.', - ); - } - - 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. - -let hasError: boolean = false; -let caughtError: mixed = null; - -/** - * 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) { - event.currentTarget = getNodeFromInstance(inst); - try { - listener(event); - } catch (error) { - if (!hasError) { - hasError = true; - caughtError = error; - } - } - 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 - */ -function executeDispatchesAndRelease(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 && 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 (disableDOMTestUtils) { - throw new Error( - '`Simulate` was removed from `react-dom/test-utils`. ' + - 'See https://react.dev/warnings/react-dom-test-utils for more info.', - ); - } - - 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); - if (hasError) { - const error = caughtError; - hasError = false; - caughtError = null; - throw error; - } - }); - 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, -}; diff --git a/packages/react-dom/src/test-utils/ReactTestUtilsFB.js b/packages/react-dom/src/test-utils/ReactTestUtilsFB.js new file mode 100644 index 0000000000000..1b4125c819e61 --- /dev/null +++ b/packages/react-dom/src/test-utils/ReactTestUtilsFB.js @@ -0,0 +1,880 @@ +/** + * 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, + HostHoistable, + HostSingleton, + HostText, +} from 'react-reconciler/src/ReactWorkTags'; +import {SyntheticEvent} from 'react-dom-bindings/src/events/SyntheticEvent'; +import {ELEMENT_NODE} from 'react-dom-bindings/src/client/HTMLNodeType'; +import {disableDOMTestUtils} 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]; + +let didWarnAboutUsingAct = false; +function act(callback) { + if (didWarnAboutUsingAct === false) { + didWarnAboutUsingAct = true; + console.error( + '`ReactDOMTestUtils.act` is deprecated in favor of `React.act`. ' + + 'Import `act` from `react` instead of `react-dom/test-utils`. ' + + 'See https://react.dev/warnings/react-dom-test-utils for more info.', + ); + } + return React.act(callback); +} + +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 || + node.tag === HostHoistable || + node.tag === HostSingleton + ) { + 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( + `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) { + if (disableDOMTestUtils) { + throw new Error( + '`renderIntoDocument` was removed from `react-dom/test-utils`. ' + + 'See https://react.dev/warnings/react-dom-test-utils for more info.', + ); + } + + 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) { + if (disableDOMTestUtils) { + throw new Error( + '`isElement` was removed from `react-dom/test-utils`. ' + + 'See https://react.dev/warnings/react-dom-test-utils for more info.', + ); + } + + return React.isValidElement(element); +} + +function isElementOfType(inst, convenienceConstructor) { + if (disableDOMTestUtils) { + throw new Error( + '`isElementOfType` was removed from `react-dom/test-utils`. ' + + 'See https://react.dev/warnings/react-dom-test-utils for more info.', + ); + } + + return React.isValidElement(inst) && inst.type === convenienceConstructor; +} + +function isDOMComponent(inst) { + if (disableDOMTestUtils) { + throw new Error( + '`isDOMComponent` was removed from `react-dom/test-utils`. ' + + 'See https://react.dev/warnings/react-dom-test-utils for more info.', + ); + } + + return !!(inst && inst.nodeType === ELEMENT_NODE && inst.tagName); +} + +function isDOMComponentElement(inst) { + if (disableDOMTestUtils) { + throw new Error( + '`isDOMComponentElement` was removed from `react-dom/test-utils`. ' + + 'See https://react.dev/warnings/react-dom-test-utils for more info.', + ); + } + + return !!(inst && React.isValidElement(inst) && !!inst.tagName); +} + +function isCompositeComponent(inst) { + if (disableDOMTestUtils) { + throw new Error( + '`isCompositeComponent` was removed from `react-dom/test-utils`. ' + + 'See https://react.dev/warnings/react-dom-test-utils for more info.', + ); + } + + 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 (disableDOMTestUtils) { + throw new Error( + '`isCompositeComponentWithType` was removed from `react-dom/test-utils`. ' + + 'See https://react.dev/warnings/react-dom-test-utils for more info.', + ); + } + + if (!isCompositeComponent(inst)) { + return false; + } + const internalInstance = getInstance(inst); + const constructor = internalInstance.type; + return constructor === type; +} + +function findAllInRenderedTree(inst, test) { + if (disableDOMTestUtils) { + throw new Error( + '`findAllInRenderedTree` was removed from `react-dom/test-utils`. ' + + 'See https://react.dev/warnings/react-dom-test-utils for more info.', + ); + } + + 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) { + if (disableDOMTestUtils) { + throw new Error( + '`scryRenderedDOMComponentsWithClass` was removed from `react-dom/test-utils`. ' + + 'See https://react.dev/warnings/react-dom-test-utils for more info.', + ); + } + + 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) { + if (disableDOMTestUtils) { + throw new Error( + '`findRenderedDOMComponentWithClass` was removed from `react-dom/test-utils`. ' + + 'See https://react.dev/warnings/react-dom-test-utils for more info.', + ); + } + + 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) { + if (disableDOMTestUtils) { + throw new Error( + '`scryRenderedDOMComponentsWithTag` was removed from `react-dom/test-utils`. ' + + 'See https://react.dev/warnings/react-dom-test-utils for more info.', + ); + } + + 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) { + if (disableDOMTestUtils) { + throw new Error( + '`findRenderedDOMComponentWithTag` was removed from `react-dom/test-utils`. ' + + 'See https://react.dev/warnings/react-dom-test-utils for more info.', + ); + } + + 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) { + if (disableDOMTestUtils) { + throw new Error( + '`scryRenderedComponentsWithType` was removed from `react-dom/test-utils`. ' + + 'See https://react.dev/warnings/react-dom-test-utils for more info.', + ); + } + + 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) { + if (disableDOMTestUtils) { + throw new Error( + '`findRenderedComponentWithType` was removed from `react-dom/test-utils`. ' + + 'See https://react.dev/warnings/react-dom-test-utils for more info.', + ); + } + + 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 + *
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 (disableDOMTestUtils) { + throw new Error( + '`mockComponent` was removed from `react-dom/test-utils`. ' + + 'See https://react.dev/warnings/react-dom-test-utils for more info.', + ); + } + + if (__DEV__) { + if (!hasWarnedAboutDeprecatedMockComponent) { + hasWarnedAboutDeprecatedMockComponent = true; + console.warn( + 'ReactTestUtils.mockComponent() is deprecated. ' + + 'Use shallow rendering or jest.mock() instead.\n\n' + + 'See https://react.dev/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) { + if (disableDOMTestUtils) { + throw new Error( + '`nativeTouchData` was removed from `react-dom/test-utils`. ' + + 'See https://react.dev/warnings/react-dom-test-utils for more info.', + ); + } + + 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. + +let hasError: boolean = false; +let caughtError: mixed = null; + +/** + * 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) { + event.currentTarget = getNodeFromInstance(inst); + try { + listener(event); + } catch (error) { + if (!hasError) { + hasError = true; + caughtError = error; + } + } + 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 + */ +function executeDispatchesAndRelease(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 && 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 (disableDOMTestUtils) { + throw new Error( + '`Simulate` was removed from `react-dom/test-utils`. ' + + 'See https://react.dev/warnings/react-dom-test-utils for more info.', + ); + } + + 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); + if (hasError) { + const error = caughtError; + hasError = false; + caughtError = null; + throw error; + } + }); + 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, +}; diff --git a/packages/react-dom/test-utils.fb.js b/packages/react-dom/test-utils.fb.js new file mode 100644 index 0000000000000..dc43cbed3fcbd --- /dev/null +++ b/packages/react-dom/test-utils.fb.js @@ -0,0 +1,10 @@ +/** + * 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 + */ + +export * from './src/test-utils/ReactTestUtilsFB'; diff --git a/scripts/shared/inlinedHostConfigs.js b/scripts/shared/inlinedHostConfigs.js index fa6916cee621b..63ec4db4dbe39 100644 --- a/scripts/shared/inlinedHostConfigs.js +++ b/scripts/shared/inlinedHostConfigs.js @@ -15,6 +15,7 @@ module.exports = [ 'react-dom/unstable_testing', 'react-dom/src/server/react-dom-server.node.js', 'react-dom/static.node', + 'react-dom/test-utils', 'react-dom/server-rendering-stub', 'react-dom/unstable_server-external-runtime', 'react-server-dom-webpack/server.node.unbundled', @@ -29,6 +30,7 @@ module.exports = [ 'react-dom/server.node', 'react-dom/static', 'react-dom/static.node', + 'react-dom/test-utils', 'react-dom/src/server/react-dom-server.node', 'react-dom/src/server/ReactDOMFizzServerNode.js', // react-dom/server.node 'react-dom/src/server/ReactDOMFizzStaticNode.js',