main
js 391 lines 10 KB
Raw
1 const DOMException = require('jsdom/lib/jsdom/living/generated/DOMException');
2 const {nodeRoot} = require('jsdom/lib/jsdom/living/helpers/node');
3 const reportException = require('jsdom/lib/jsdom/living/helpers/runtime-script-errors');
4 const {
5 isNode,
6 isShadowRoot,
7 isSlotable,
8 getEventTargetParent,
9 isShadowInclusiveAncestor,
10 retarget,
11 } = require('jsdom/lib/jsdom/living/helpers/shadow-dom');
12
13 const {waitForMicrotasks} = require('./ReactInternalTestUtils');
14
15 const EVENT_PHASE = {
16 NONE: 0,
17 CAPTURING_PHASE: 1,
18 AT_TARGET: 2,
19 BUBBLING_PHASE: 3,
20 };
21
22 // Hack to get Symbol(wrapper) for target nodes.
23 let wrapperSymbol;
24 function wrapperForImpl(impl) {
25 if (impl == null) {
26 return null;
27 }
28
29 return impl[wrapperSymbol];
30 }
31
32 // This is a forked implementation of the jsdom dispatchEvent. The goal of
33 // this fork is to match the actual browser behavior of user events more closely.
34 // Real browser events yield to microtasks in-between event handlers, which is
35 // different from programmatically calling dispatchEvent (which does not yield).
36 // JSDOM correctly implements programmatic dispatchEvent, but sometimes we need
37 // to test the behavior of real user interactions, so we simulate it.
38 //
39 // It's async because we need to wait for microtasks between event handlers.
40 //
41 // Taken from:
42 // https://github.com/jsdom/jsdom/blob/2f8a7302a43fff92f244d5f3426367a8eb2b8896/lib/jsdom/living/events/EventTarget-impl.js#L88
43 async function simulateEventDispatch(eventImpl) {
44 if (eventImpl._dispatchFlag || !eventImpl._initializedFlag) {
45 throw DOMException.create(this._globalObject, [
46 'Tried to dispatch an uninitialized event',
47 'InvalidStateError',
48 ]);
49 }
50 if (eventImpl.eventPhase !== EVENT_PHASE.NONE) {
51 throw DOMException.create(this._globalObject, [
52 'Tried to dispatch a dispatching event',
53 'InvalidStateError',
54 ]);
55 }
56
57 eventImpl.isTrusted = false;
58
59 await _dispatch.call(this, eventImpl);
60 }
61
62 async function _dispatch(eventImpl, legacyTargetOverrideFlag) {
63 // Hack: save the wrapper Symbol.
64 wrapperSymbol = Object.getOwnPropertySymbols(eventImpl)[0];
65
66 let targetImpl = this;
67 let clearTargets = false;
68 let activationTarget = null;
69
70 eventImpl._dispatchFlag = true;
71
72 const targetOverride = legacyTargetOverrideFlag
73 ? wrapperForImpl(targetImpl._globalObject._document)
74 : targetImpl;
75 let relatedTarget = retarget(eventImpl.relatedTarget, targetImpl);
76
77 if (targetImpl !== relatedTarget || targetImpl === eventImpl.relatedTarget) {
78 const touchTargets = [];
79
80 appendToEventPath(
81 eventImpl,
82 targetImpl,
83 targetOverride,
84 relatedTarget,
85 touchTargets,
86 false,
87 );
88
89 const isActivationEvent = false; // TODO Not ported in fork.
90
91 if (isActivationEvent && targetImpl._hasActivationBehavior) {
92 activationTarget = targetImpl;
93 }
94
95 let slotInClosedTree = false;
96 let slotable =
97 isSlotable(targetImpl) && targetImpl._assignedSlot ? targetImpl : null;
98 let parent = getEventTargetParent(targetImpl, eventImpl);
99
100 // Populate event path
101 // https://dom.spec.whatwg.org/#event-path
102 while (parent !== null) {
103 if (slotable !== null) {
104 if (parent.localName !== 'slot') {
105 throw new Error(`JSDOM Internal Error: Expected parent to be a Slot`);
106 }
107
108 slotable = null;
109
110 const parentRoot = nodeRoot(parent);
111 if (isShadowRoot(parentRoot) && parentRoot.mode === 'closed') {
112 slotInClosedTree = true;
113 }
114 }
115
116 if (isSlotable(parent) && parent._assignedSlot) {
117 slotable = parent;
118 }
119
120 relatedTarget = retarget(eventImpl.relatedTarget, parent);
121
122 if (
123 (isNode(parent) &&
124 isShadowInclusiveAncestor(nodeRoot(targetImpl), parent)) ||
125 wrapperForImpl(parent).constructor.name === 'Window'
126 ) {
127 if (
128 isActivationEvent &&
129 eventImpl.bubbles &&
130 activationTarget === null &&
131 parent._hasActivationBehavior
132 ) {
133 activationTarget = parent;
134 }
135
136 appendToEventPath(
137 eventImpl,
138 parent,
139 null,
140 relatedTarget,
141 touchTargets,
142 slotInClosedTree,
143 );
144 } else if (parent === relatedTarget) {
145 parent = null;
146 } else {
147 targetImpl = parent;
148
149 if (
150 isActivationEvent &&
151 activationTarget === null &&
152 targetImpl._hasActivationBehavior
153 ) {
154 activationTarget = targetImpl;
155 }
156
157 appendToEventPath(
158 eventImpl,
159 parent,
160 targetImpl,
161 relatedTarget,
162 touchTargets,
163 slotInClosedTree,
164 );
165 }
166
167 if (parent !== null) {
168 parent = getEventTargetParent(parent, eventImpl);
169 }
170
171 slotInClosedTree = false;
172 }
173
174 let clearTargetsStructIndex = -1;
175 for (
176 let i = eventImpl._path.length - 1;
177 i >= 0 && clearTargetsStructIndex === -1;
178 i--
179 ) {
180 if (eventImpl._path[i].target !== null) {
181 clearTargetsStructIndex = i;
182 }
183 }
184 const clearTargetsStruct = eventImpl._path[clearTargetsStructIndex];
185
186 clearTargets =
187 (isNode(clearTargetsStruct.target) &&
188 isShadowRoot(nodeRoot(clearTargetsStruct.target))) ||
189 (isNode(clearTargetsStruct.relatedTarget) &&
190 isShadowRoot(nodeRoot(clearTargetsStruct.relatedTarget)));
191
192 if (
193 activationTarget !== null &&
194 activationTarget._legacyPreActivationBehavior
195 ) {
196 activationTarget._legacyPreActivationBehavior();
197 }
198
199 for (let i = eventImpl._path.length - 1; i >= 0; --i) {
200 const struct = eventImpl._path[i];
201
202 if (struct.target !== null) {
203 eventImpl.eventPhase = EVENT_PHASE.AT_TARGET;
204 } else {
205 eventImpl.eventPhase = EVENT_PHASE.CAPTURING_PHASE;
206 }
207
208 await invokeEventListeners(struct, eventImpl, 'capturing');
209 }
210
211 for (let i = 0; i < eventImpl._path.length; i++) {
212 const struct = eventImpl._path[i];
213
214 if (struct.target !== null) {
215 eventImpl.eventPhase = EVENT_PHASE.AT_TARGET;
216 } else {
217 if (!eventImpl.bubbles) {
218 continue;
219 }
220
221 eventImpl.eventPhase = EVENT_PHASE.BUBBLING_PHASE;
222 }
223
224 await invokeEventListeners(struct, eventImpl, 'bubbling');
225 }
226 }
227
228 eventImpl.eventPhase = EVENT_PHASE.NONE;
229
230 eventImpl.currentTarget = null;
231 eventImpl._path = [];
232 eventImpl._dispatchFlag = false;
233 eventImpl._stopPropagationFlag = false;
234 eventImpl._stopImmediatePropagationFlag = false;
235
236 if (clearTargets) {
237 eventImpl.target = null;
238 eventImpl.relatedTarget = null;
239 }
240
241 if (activationTarget !== null) {
242 if (!eventImpl._canceledFlag) {
243 activationTarget._activationBehavior(eventImpl);
244 } else if (activationTarget._legacyCanceledActivationBehavior) {
245 activationTarget._legacyCanceledActivationBehavior();
246 }
247 }
248
249 return !eventImpl._canceledFlag;
250 }
251
252 async function invokeEventListeners(struct, eventImpl, phase) {
253 const structIndex = eventImpl._path.indexOf(struct);
254 for (let i = structIndex; i >= 0; i--) {
255 const t = eventImpl._path[i];
256 if (t.target) {
257 eventImpl.target = t.target;
258 break;
259 }
260 }
261
262 eventImpl.relatedTarget = wrapperForImpl(struct.relatedTarget);
263
264 if (eventImpl._stopPropagationFlag) {
265 return;
266 }
267
268 eventImpl.currentTarget = wrapperForImpl(struct.item);
269
270 const listeners = struct.item._eventListeners;
271 await innerInvokeEventListeners(
272 eventImpl,
273 listeners,
274 phase,
275 struct.itemInShadowTree,
276 );
277 }
278
279 async function innerInvokeEventListeners(
280 eventImpl,
281 listeners,
282 phase,
283 itemInShadowTree,
284 ) {
285 let found = false;
286
287 const {type, target} = eventImpl;
288 const wrapper = wrapperForImpl(target);
289
290 if (!listeners || !listeners[type]) {
291 return found;
292 }
293
294 // Copy event listeners before iterating since the list can be modified during the iteration.
295 const handlers = listeners[type].slice();
296
297 for (let i = 0; i < handlers.length; i++) {
298 const listener = handlers[i];
299 const {capture, once, passive} = listener.options;
300
301 // Check if the event listener has been removed since the listeners has been cloned.
302 if (!listeners[type].includes(listener)) {
303 continue;
304 }
305
306 found = true;
307
308 if (
309 (phase === 'capturing' && !capture) ||
310 (phase === 'bubbling' && capture)
311 ) {
312 continue;
313 }
314
315 if (once) {
316 listeners[type].splice(listeners[type].indexOf(listener), 1);
317 }
318
319 let window = null;
320 if (wrapper && wrapper._document) {
321 // Triggered by Window
322 window = wrapper;
323 } else if (target._ownerDocument) {
324 // Triggered by most webidl2js'ed instances
325 window = target._ownerDocument._defaultView;
326 } else if (wrapper._ownerDocument) {
327 // Currently triggered by some non-webidl2js things
328 window = wrapper._ownerDocument._defaultView;
329 }
330
331 let currentEvent;
332 if (window) {
333 currentEvent = window._currentEvent;
334 if (!itemInShadowTree) {
335 window._currentEvent = eventImpl;
336 }
337 }
338
339 if (passive) {
340 eventImpl._inPassiveListenerFlag = true;
341 }
342
343 try {
344 listener.callback.call(eventImpl.currentTarget, eventImpl);
345 } catch (e) {
346 if (window) {
347 reportException(window, e);
348 }
349 // Errors in window-less documents just get swallowed... can you think of anything better?
350 }
351
352 eventImpl._inPassiveListenerFlag = false;
353
354 if (window) {
355 window._currentEvent = currentEvent;
356 }
357
358 if (eventImpl._stopImmediatePropagationFlag) {
359 return found;
360 }
361
362 // IMPORTANT: Flush microtasks
363 await waitForMicrotasks();
364 }
365
366 return found;
367 }
368
369 function appendToEventPath(
370 eventImpl,
371 target,
372 targetOverride,
373 relatedTarget,
374 touchTargets,
375 slotInClosedTree,
376 ) {
377 const itemInShadowTree = isNode(target) && isShadowRoot(nodeRoot(target));
378 const rootOfClosedTree = isShadowRoot(target) && target.mode === 'closed';
379
380 eventImpl._path.push({
381 item: target,
382 itemInShadowTree,
383 target: targetOverride,
384 relatedTarget,
385 touchTargets,
386 rootOfClosedTree,
387 slotInClosedTree,
388 });
389 }
390
391 export default simulateEventDispatch;