const DOMException = require('domexception/webidl2js-wrapper'); const {nodeRoot} = require('jsdom/lib/jsdom/living/helpers/node'); const reportException = require('jsdom/lib/jsdom/living/helpers/runtime-script-errors'); const { isNode, isShadowRoot, isSlotable, getEventTargetParent, isShadowInclusiveAncestor, retarget, } = require('jsdom/lib/jsdom/living/helpers/shadow-dom'); const {waitForMicrotasks} = require('./ReactInternalTestUtils'); const EVENT_PHASE = { NONE: 0, CAPTURING_PHASE: 1, AT_TARGET: 2, BUBBLING_PHASE: 3, }; // Hack to get Symbol(wrapper) for target nodes. let wrapperSymbol; function wrapperForImpl(impl) { if (impl == null) { return null; } return impl[wrapperSymbol]; } // This is a forked implementation of the jsdom dispatchEvent. The goal of // this fork is to match the actual browser behavior of user events more closely. // Real browser events yield to microtasks in-between event handlers, which is // different from programmatically calling dispatchEvent (which does not yield). // JSDOM correctly implements programmatic dispatchEvent, but sometimes we need // to test the behavior of real user interactions, so we simulate it. // // It's async because we need to wait for microtasks between event handlers. // // Taken from: // https://github.com/jsdom/jsdom/blob/2f8a7302a43fff92f244d5f3426367a8eb2b8896/lib/jsdom/living/events/EventTarget-impl.js#L88 async function simulateEventDispatch(eventImpl) { if (eventImpl._dispatchFlag || !eventImpl._initializedFlag) { throw DOMException.create(this._globalObject, [ 'Tried to dispatch an uninitialized event', 'InvalidStateError', ]); } if (eventImpl.eventPhase !== EVENT_PHASE.NONE) { throw DOMException.create(this._globalObject, [ 'Tried to dispatch a dispatching event', 'InvalidStateError', ]); } eventImpl.isTrusted = false; await _dispatch.call(this, eventImpl); } async function _dispatch(eventImpl, legacyTargetOverrideFlag) { // Hack: save the wrapper Symbol. wrapperSymbol = Object.getOwnPropertySymbols(eventImpl)[0]; let targetImpl = this; let clearTargets = false; let activationTarget = null; eventImpl._dispatchFlag = true; const targetOverride = legacyTargetOverrideFlag ? wrapperForImpl(targetImpl._globalObject._document) : targetImpl; let relatedTarget = retarget(eventImpl.relatedTarget, targetImpl); if (targetImpl !== relatedTarget || targetImpl === eventImpl.relatedTarget) { const touchTargets = []; appendToEventPath( eventImpl, targetImpl, targetOverride, relatedTarget, touchTargets, false, ); const isActivationEvent = false; // TODO Not ported in fork. if (isActivationEvent && targetImpl._hasActivationBehavior) { activationTarget = targetImpl; } let slotInClosedTree = false; let slotable = isSlotable(targetImpl) && targetImpl._assignedSlot ? targetImpl : null; let parent = getEventTargetParent(targetImpl, eventImpl); // Populate event path // https://dom.spec.whatwg.org/#event-path while (parent !== null) { if (slotable !== null) { if (parent.localName !== 'slot') { throw new Error(`JSDOM Internal Error: Expected parent to be a Slot`); } slotable = null; const parentRoot = nodeRoot(parent); if (isShadowRoot(parentRoot) && parentRoot.mode === 'closed') { slotInClosedTree = true; } } if (isSlotable(parent) && parent._assignedSlot) { slotable = parent; } relatedTarget = retarget(eventImpl.relatedTarget, parent); if ( (isNode(parent) && isShadowInclusiveAncestor(nodeRoot(targetImpl), parent)) || wrapperForImpl(parent).constructor.name === 'Window' ) { if ( isActivationEvent && eventImpl.bubbles && activationTarget === null && parent._hasActivationBehavior ) { activationTarget = parent; } appendToEventPath( eventImpl, parent, null, relatedTarget, touchTargets, slotInClosedTree, ); } else if (parent === relatedTarget) { parent = null; } else { targetImpl = parent; if ( isActivationEvent && activationTarget === null && targetImpl._hasActivationBehavior ) { activationTarget = targetImpl; } appendToEventPath( eventImpl, parent, targetImpl, relatedTarget, touchTargets, slotInClosedTree, ); } if (parent !== null) { parent = getEventTargetParent(parent, eventImpl); } slotInClosedTree = false; } let clearTargetsStructIndex = -1; for ( let i = eventImpl._path.length - 1; i >= 0 && clearTargetsStructIndex === -1; i-- ) { if (eventImpl._path[i].target !== null) { clearTargetsStructIndex = i; } } const clearTargetsStruct = eventImpl._path[clearTargetsStructIndex]; clearTargets = (isNode(clearTargetsStruct.target) && isShadowRoot(nodeRoot(clearTargetsStruct.target))) || (isNode(clearTargetsStruct.relatedTarget) && isShadowRoot(nodeRoot(clearTargetsStruct.relatedTarget))); if ( activationTarget !== null && activationTarget._legacyPreActivationBehavior ) { activationTarget._legacyPreActivationBehavior(); } for (let i = eventImpl._path.length - 1; i >= 0; --i) { const struct = eventImpl._path[i]; if (struct.target !== null) { eventImpl.eventPhase = EVENT_PHASE.AT_TARGET; } else { eventImpl.eventPhase = EVENT_PHASE.CAPTURING_PHASE; } await invokeEventListeners(struct, eventImpl, 'capturing'); } for (let i = 0; i < eventImpl._path.length; i++) { const struct = eventImpl._path[i]; if (struct.target !== null) { eventImpl.eventPhase = EVENT_PHASE.AT_TARGET; } else { if (!eventImpl.bubbles) { continue; } eventImpl.eventPhase = EVENT_PHASE.BUBBLING_PHASE; } await invokeEventListeners(struct, eventImpl, 'bubbling'); } } eventImpl.eventPhase = EVENT_PHASE.NONE; eventImpl.currentTarget = null; eventImpl._path = []; eventImpl._dispatchFlag = false; eventImpl._stopPropagationFlag = false; eventImpl._stopImmediatePropagationFlag = false; if (clearTargets) { eventImpl.target = null; eventImpl.relatedTarget = null; } if (activationTarget !== null) { if (!eventImpl._canceledFlag) { activationTarget._activationBehavior(eventImpl); } else if (activationTarget._legacyCanceledActivationBehavior) { activationTarget._legacyCanceledActivationBehavior(); } } return !eventImpl._canceledFlag; } async function invokeEventListeners(struct, eventImpl, phase) { const structIndex = eventImpl._path.indexOf(struct); for (let i = structIndex; i >= 0; i--) { const t = eventImpl._path[i]; if (t.target) { eventImpl.target = t.target; break; } } eventImpl.relatedTarget = wrapperForImpl(struct.relatedTarget); if (eventImpl._stopPropagationFlag) { return; } eventImpl.currentTarget = wrapperForImpl(struct.item); const listeners = struct.item._eventListeners; await innerInvokeEventListeners( eventImpl, listeners, phase, struct.itemInShadowTree, ); } async function innerInvokeEventListeners( eventImpl, listeners, phase, itemInShadowTree, ) { let found = false; const {type, target} = eventImpl; const wrapper = wrapperForImpl(target); if (!listeners || !listeners[type]) { return found; } // Copy event listeners before iterating since the list can be modified during the iteration. const handlers = listeners[type].slice(); for (let i = 0; i < handlers.length; i++) { const listener = handlers[i]; const {capture, once, passive} = listener.options; // Check if the event listener has been removed since the listeners has been cloned. if (!listeners[type].includes(listener)) { continue; } found = true; if ( (phase === 'capturing' && !capture) || (phase === 'bubbling' && capture) ) { continue; } if (once) { listeners[type].splice(listeners[type].indexOf(listener), 1); } let window = null; if (wrapper && wrapper._document) { // Triggered by Window window = wrapper; } else if (target._ownerDocument) { // Triggered by most webidl2js'ed instances window = target._ownerDocument._defaultView; } else if (wrapper._ownerDocument) { // Currently triggered by some non-webidl2js things window = wrapper._ownerDocument._defaultView; } let currentEvent; if (window) { currentEvent = window._currentEvent; if (!itemInShadowTree) { window._currentEvent = eventImpl; } } if (passive) { eventImpl._inPassiveListenerFlag = true; } try { listener.callback.call(eventImpl.currentTarget, eventImpl); } catch (e) { if (window) { reportException(window, e); } // Errors in window-less documents just get swallowed... can you think of anything better? } eventImpl._inPassiveListenerFlag = false; if (window) { window._currentEvent = currentEvent; } if (eventImpl._stopImmediatePropagationFlag) { return found; } // IMPORTANT: Flush microtasks await waitForMicrotasks(); } return found; } function appendToEventPath( eventImpl, target, targetOverride, relatedTarget, touchTargets, slotInClosedTree, ) { const itemInShadowTree = isNode(target) && isShadowRoot(nodeRoot(target)); const rootOfClosedTree = isShadowRoot(target) && target.mode === 'closed'; eventImpl._path.push({ item: target, itemInShadowTree, target: targetOverride, relatedTarget, touchTargets, rootOfClosedTree, slotInClosedTree, }); } export default simulateEventDispatch;