@@ -54,27 +54,6 @@ describe('dispatch hooks', () => {
5454 . withContext ( 'innerClickListener' )
5555 . toHaveBeenCalledTimes ( 1 ) ;
5656 } ) ;
57-
58- it ( 'should not trigger activation behavior for clicks coming from inner <a> elements' , ( ) => {
59- const shadowRoot = element . attachShadow ( { mode : 'open' } ) ;
60- const anchorElement = document . createElement ( 'a' ) ;
61- anchorElement . href = '#' ;
62- shadowRoot . appendChild ( anchorElement ) ;
63-
64- setupDispatchHooks ( element , 'click' ) ;
65-
66- const clickEvent = new MouseEvent ( 'click' , {
67- bubbles : true ,
68- cancelable : true ,
69- composed : true ,
70- } ) ;
71-
72- anchorElement . dispatchEvent ( clickEvent ) ;
73-
74- expect ( clickEvent . defaultPrevented )
75- . withContext ( 'clickEvent.defaultPrevented' )
76- . toBeTrue ( ) ;
77- } ) ;
7857 } ) ;
7958
8059 describe ( 'afterDispatch()' , ( ) => {
@@ -212,5 +191,224 @@ describe('dispatch hooks', () => {
212191 . withContext ( 'afterDispatch() callback' )
213192 . toHaveBeenCalledTimes ( 1 ) ;
214193 } ) ;
194+
195+ it ( 'is called after parent event listeners are called' , ( ) => {
196+ setupDispatchHooks ( element , 'click' ) ;
197+
198+ const callOrder : string [ ] = [ ] ;
199+ element . addEventListener ( 'click' , ( event ) => {
200+ callOrder . push ( 'element@click' ) ;
201+ afterDispatch ( event , ( ) => {
202+ callOrder . push ( 'afterDispatch' ) ;
203+ } ) ;
204+ } ) ;
205+ document . addEventListener ( 'click' , ( ) => {
206+ callOrder . push ( 'parent@click' ) ;
207+ } ) ;
208+
209+ element . click ( ) ;
210+
211+ const expectedCallOrder = [
212+ 'element@click' ,
213+ 'parent@click' ,
214+ 'afterDispatch' ,
215+ ] ;
216+ expect ( callOrder )
217+ . withContext ( 'call order of event listeners and afterDispatch()' )
218+ . toEqual ( expectedCallOrder ) ;
219+ } ) ;
220+
221+ it ( 'is called after other event listeners for non-bubbling events' , ( ) => {
222+ setupDispatchHooks ( element , 'change' ) ;
223+
224+ const callOrder : string [ ] = [ ] ;
225+ element . addEventListener ( 'change' , ( event ) => {
226+ callOrder . push ( 'element@change' ) ;
227+ afterDispatch ( event , ( ) => {
228+ callOrder . push ( 'afterDispatch' ) ;
229+ } ) ;
230+ } ) ;
231+ element . addEventListener ( 'change' , ( ) => {
232+ callOrder . push ( 'element@change2' ) ;
233+ } ) ;
234+
235+ element . dispatchEvent ( new Event ( 'change' ) ) ;
236+
237+ const expectedCallOrder = [
238+ 'element@change' ,
239+ 'element@change2' ,
240+ 'afterDispatch' ,
241+ ] ;
242+ expect ( callOrder )
243+ . withContext ( 'call order of event listeners and afterDispatch()' )
244+ . toEqual ( expectedCallOrder ) ;
245+ } ) ;
246+
247+ it ( 'is called after other event listeners for bubbling non-composed events in a shadow root' , ( ) => {
248+ const shadowRoot = element . attachShadow ( { mode : 'open' } ) ;
249+ const child = document . createElement ( 'div' ) ;
250+ shadowRoot . appendChild ( child ) ;
251+
252+ setupDispatchHooks ( child , 'custom-event' ) ;
253+
254+ const callOrder : string [ ] = [ ] ;
255+ child . addEventListener ( 'custom-event' , ( event ) => {
256+ callOrder . push ( 'child@custom-event' ) ;
257+ afterDispatch ( event , ( ) => {
258+ callOrder . push ( 'afterDispatch' ) ;
259+ } ) ;
260+ } ) ;
261+ shadowRoot . addEventListener ( 'custom-event' , ( ) => {
262+ callOrder . push ( 'shadowRoot@custom-event' ) ;
263+ } ) ;
264+ const elementListener = jasmine . createSpy ( 'elementListener' ) ;
265+ element . addEventListener ( 'custom-event' , elementListener ) ;
266+
267+ child . dispatchEvent (
268+ new Event ( 'custom-event' , { bubbles : true , composed : false } ) ,
269+ ) ;
270+
271+ const expectedCallOrder = [
272+ 'child@custom-event' ,
273+ 'shadowRoot@custom-event' ,
274+ 'afterDispatch' ,
275+ ] ;
276+
277+ expect ( callOrder )
278+ . withContext ( 'call order of event listeners and afterDispatch()' )
279+ . toEqual ( expectedCallOrder ) ;
280+ expect ( elementListener )
281+ . withContext (
282+ 'listener on element with shadow root should not be called for `composed: false` event' ,
283+ )
284+ . not . toHaveBeenCalled ( ) ;
285+ } ) ;
286+
287+ it ( 'is called when parent non-root event listeners stop propagation' , ( ) => {
288+ setupDispatchHooks ( element , 'click' ) ;
289+
290+ /*
291+ #document (root - should not be called)
292+ element (parent - stops propagation)
293+ child (child - calls afterDispatch)
294+ */
295+ const child = document . createElement ( 'div' ) ;
296+ element . appendChild ( child ) ;
297+ const childAfterDispatchCallback = jasmine . createSpy (
298+ 'childAfterDispatchCallback' ,
299+ ) ;
300+ child . addEventListener ( 'click' , ( event ) => {
301+ afterDispatch ( event , childAfterDispatchCallback ) ;
302+ } ) ;
303+ element . addEventListener ( 'click' , ( event ) => {
304+ event . stopPropagation ( ) ;
305+ } ) ;
306+ const rootClickListener = jasmine . createSpy ( 'rootClickListener' ) ;
307+ document . addEventListener ( 'click' , rootClickListener ) ;
308+
309+ child . click ( ) ;
310+
311+ expect ( rootClickListener )
312+ . withContext ( 'root click listener' )
313+ . not . toHaveBeenCalled ( ) ;
314+ expect ( childAfterDispatchCallback )
315+ . withContext ( 'child afterDispatch() callback' )
316+ . toHaveBeenCalledTimes ( 1 ) ;
317+ } ) ;
318+
319+ it ( 'is called when parent non-root event listeners immediately stops propagation' , ( ) => {
320+ setupDispatchHooks ( element , 'click' ) ;
321+
322+ /*
323+ element (parent - stops propagation immediately)
324+ child (child - calls afterDispatch)
325+ */
326+ const child = document . createElement ( 'div' ) ;
327+ element . appendChild ( child ) ;
328+ const childAfterDispatchCallback = jasmine . createSpy (
329+ 'childAfterDispatchCallback' ,
330+ ) ;
331+ child . addEventListener ( 'click' , ( event ) => {
332+ afterDispatch ( event , childAfterDispatchCallback ) ;
333+ } ) ;
334+ element . addEventListener ( 'click' , ( event ) => {
335+ event . stopImmediatePropagation ( ) ;
336+ } ) ;
337+ const additionalClickListener = jasmine . createSpy (
338+ 'notCalledClickListener' ,
339+ ) ;
340+ document . addEventListener ( 'click' , additionalClickListener ) ;
341+
342+ child . click ( ) ;
343+
344+ expect ( additionalClickListener )
345+ . withContext ( 'additional click listener after propagation is stopped' )
346+ . not . toHaveBeenCalled ( ) ;
347+ expect ( childAfterDispatchCallback )
348+ . withContext ( 'child afterDispatch() callback' )
349+ . toHaveBeenCalledTimes ( 1 ) ;
350+ } ) ;
351+
352+ it ( 'is DOES NOT support being called after the execution of the event listener that stopped propagation' , ( ) => {
353+ setupDispatchHooks ( element , 'click' ) ;
354+
355+ const child = document . createElement ( 'div' ) ;
356+ element . appendChild ( child ) ;
357+ const callOrder : string [ ] = [ ] ;
358+ child . addEventListener ( 'click' , ( event ) => {
359+ callOrder . push ( 'child@click' ) ;
360+ afterDispatch ( event , ( ) => {
361+ callOrder . push ( 'afterDispatch' ) ;
362+ } ) ;
363+ } ) ;
364+ element . addEventListener ( 'click' , ( event ) => {
365+ callOrder . push ( 'parent@click' ) ;
366+ event . stopPropagation ( ) ;
367+ callOrder . push ( 'parent done' ) ;
368+ } ) ;
369+
370+ child . click ( ) ;
371+
372+ // Ideally, when the event stops propagating, afterDispatch() is called
373+ // directly after the execution of the function that stops propagation.
374+ // However, this would mean introducing asynchronicity, which is not
375+ // allowed. The compromise is to synchronously call afterDispatch() hooks
376+ // when propagation is stopped.
377+ const desiredCallOrder = [
378+ 'child@click' ,
379+ 'parent@click' ,
380+ 'parent done' ,
381+ 'afterDispatch' ,
382+ ] ;
383+ const expectedCallOrder = [
384+ 'child@click' ,
385+ 'parent@click' ,
386+ 'afterDispatch' ,
387+ 'parent done' ,
388+ ] ;
389+ expect ( callOrder )
390+ . withContext ( 'call order of event listeners and afterDispatch()' )
391+ . toEqual ( expectedCallOrder ) ;
392+ expect ( callOrder ) . not . toEqual ( desiredCallOrder ) ;
393+ } ) ;
394+
395+ it ( 'is not called multiple times if stopPropagation() is called multiple times' , ( ) => {
396+ setupDispatchHooks ( element , 'click' ) ;
397+
398+ const afterDispatchCallback = jasmine . createSpy ( 'afterDispatchCallback' ) ;
399+ element . addEventListener ( 'click' , ( event ) => {
400+ afterDispatch ( event , afterDispatchCallback ) ;
401+ } ) ;
402+ element . addEventListener ( 'click' , ( event ) => {
403+ event . stopPropagation ( ) ;
404+ event . stopPropagation ( ) ;
405+ } ) ;
406+
407+ element . click ( ) ;
408+
409+ expect ( afterDispatchCallback )
410+ . withContext ( 'afterDispatch() callback' )
411+ . toHaveBeenCalledTimes ( 1 ) ;
412+ } ) ;
215413 } ) ;
216414} ) ;
0 commit comments