/**
 * @license
 * Copyright Google LLC All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.dev/license
 */

import {patchFilteredProperties} from '../../lib/browser/property-descriptor';
import {patchEventTarget} from '../../lib/common/events';
import {zoneSymbol} from '../../lib/common/utils';
import {ifEnvSupports, ifEnvSupportsWithDone} from '../test-util';

import Spy = jasmine.Spy;
declare const global: any;

const noop = function () {};

function windowPrototype() {
  return !!(global['Window'] && global['Window'].prototype);
}

function promiseUnhandleRejectionSupport() {
  return !!global['PromiseRejectionEvent'];
}

function canPatchOnProperty(obj: any, prop: string) {
  const func = function () {
    if (!obj) {
      return false;
    }
    const desc = Object.getOwnPropertyDescriptor(obj, prop);
    if (!desc || !desc.configurable) {
      return false;
    }
    return true;
  };

  (func as any).message = 'patchOnProperties';
  return func;
}

let supportsPassive = false;
try {
  const opts = Object.defineProperty({}, 'passive', {
    get: function () {
      supportsPassive = true;
    },
  });
  window.addEventListener('test', opts as any, opts);
  window.removeEventListener('test', opts as any, opts);
} catch (e) {}

function supportEventListenerOptions() {
  return supportsPassive;
}

(supportEventListenerOptions as any).message = 'supportsEventListenerOptions';

function supportCanvasTest() {
  const HTMLCanvasElement = (window as any)['HTMLCanvasElement'];
  const supportCanvas =
    typeof HTMLCanvasElement !== 'undefined' &&
    HTMLCanvasElement.prototype &&
    HTMLCanvasElement.prototype.toBlob;
  const FileReader = (window as any)['FileReader'];
  const supportFileReader = typeof FileReader !== 'undefined';
  return supportCanvas && supportFileReader;
}

(supportCanvasTest as any).message = 'supportCanvasTest';

class TestEventListener {
  logs: any[] = [];
  addEventListener(eventName: string, listener: any, options: any) {
    this.logs.push(options);
  }
  removeEventListener(eventName: string, listener: any, options: any) {}
}

describe('Zone Browser', function () {
  const rootZone = Zone.current;
  (Zone as any)[zoneSymbol('ignoreConsoleErrorUncaughtError')] = true;

  describe('hooks', function () {
    it('should allow you to override alert/prompt/confirm', function () {
      const alertSpy = jasmine.createSpy('alert');
      const promptSpy = jasmine.createSpy('prompt');
      const confirmSpy = jasmine.createSpy('confirm');
      const spies: {[k: string]: Function} = {
        'alert': alertSpy,
        'prompt': promptSpy,
        'confirm': confirmSpy,
      };
      const myZone = Zone.current.fork({
        name: 'spy',
        onInvoke: (
          parentZoneDelegate: ZoneDelegate,
          currentZone: Zone,
          targetZone: Zone,
          callback: Function,
          applyThis?: any,
          applyArgs?: any[],
          source?: string,
        ): any => {
          if (source) {
            spies[source].apply(null, applyArgs);
          } else {
            return parentZoneDelegate.invoke(targetZone, callback, applyThis, applyArgs, source);
          }
        },
      });

      myZone.run(function () {
        alert('alertMsg');
        prompt('promptMsg', 'default');
        confirm('confirmMsg');
      });

      expect(alertSpy).toHaveBeenCalledWith('alertMsg');
      expect(promptSpy).toHaveBeenCalledWith('promptMsg', 'default');
      expect(confirmSpy).toHaveBeenCalledWith('confirmMsg');
    });

    describe(
      'DOM onProperty hooks',
      ifEnvSupports(canPatchOnProperty(HTMLElement.prototype, 'onclick'), function () {
        let mouseEvent = document.createEvent('Event');
        let hookSpy: Spy, eventListenerSpy: Spy;
        const zone = rootZone.fork({
          name: 'spy',
          onScheduleTask: (
            parentZoneDelegate: ZoneDelegate,
            currentZone: Zone,
            targetZone: Zone,
            task: Task,
          ): any => {
            hookSpy();
            return parentZoneDelegate.scheduleTask(targetZone, task);
          },
        });

        beforeEach(function () {
          mouseEvent.initEvent('mousedown', true, true);
          hookSpy = jasmine.createSpy('hook');
          eventListenerSpy = jasmine.createSpy('eventListener');
        });
        const globalEventHandlersEventNames = [
          'abort',
          'animationcancel',
          'animationend',
          'animationiteration',
          'auxclick',
          'beforeinput',
          'blur',
          'cancel',
          'canplay',
          'canplaythrough',
          'change',
          'compositionstart',
          'compositionupdate',
          'compositionend',
          'cuechange',
          'click',
          'close',
          'contextmenu',
          'curechange',
          'dblclick',
          'drag',
          'dragend',
          'dragenter',
          'dragexit',
          'dragleave',
          'dragover',
          'drop',
          'durationchange',
          'emptied',
          'ended',
          'error',
          'focus',
          'focusin',
          'focusout',
          'gotpointercapture',
          'input',
          'invalid',
          'keydown',
          'keypress',
          'keyup',
          'load',
          'loadstart',
          'loadeddata',
          'loadedmetadata',
          'lostpointercapture',
          'mousedown',
          'mouseenter',
          'mouseleave',
          'mousemove',
          'mouseout',
          'mouseover',
          'mouseup',
          'mousewheel',
          'orientationchange',
          'pause',
          'play',
          'playing',
          'pointercancel',
          'pointerdown',
          'pointerenter',
          'pointerleave',
          'pointerlockchange',
          'mozpointerlockchange',
          'webkitpointerlockerchange',
          'pointerlockerror',
          'mozpointerlockerror',
          'webkitpointerlockerror',
          'pointermove',
          'pointout',
          'pointerover',
          'pointerup',
          'progress',
          'ratechange',
          'reset',
          'resize',
          'scroll',
          'seeked',
          'seeking',
          'select',
          'selectionchange',
          'selectstart',
          'show',
          'sort',
          'stalled',
          'submit',
          'suspend',
          'timeupdate',
          'volumechange',
          'touchcancel',
          'touchmove',
          'touchstart',
          'touchend',
          'transitioncancel',
          'transitionend',
          'waiting',
          'wheel',
        ];
        const documentEventNames = [
          'afterscriptexecute',
          'beforescriptexecute',
          'DOMContentLoaded',
          'freeze',
          'fullscreenchange',
          'mozfullscreenchange',
          'webkitfullscreenchange',
          'msfullscreenchange',
          'fullscreenerror',
          'mozfullscreenerror',
          'webkitfullscreenerror',
          'msfullscreenerror',
          'readystatechange',
          'visibilitychange',
          'resume',
        ];
        const windowEventNames = [
          'absolutedeviceorientation',
          'afterinput',
          'afterprint',
          'appinstalled',
          'beforeinstallprompt',
          'beforeprint',
          'beforeunload',
          'devicelight',
          'devicemotion',
          'deviceorientation',
          'deviceorientationabsolute',
          'deviceproximity',
          'hashchange',
          'languagechange',
          'message',
          'mozbeforepaint',
          'offline',
          'online',
          'paint',
          'pageshow',
          'pagehide',
          'popstate',
          'rejectionhandled',
          'storage',
          'unhandledrejection',
          'unload',
          'userproximity',
          'vrdisplayconnected',
          'vrdisplaydisconnected',
          'vrdisplaypresentchange',
        ];
        const htmlElementEventNames = [
          'beforecopy',
          'beforecut',
          'beforepaste',
          'copy',
          'cut',
          'paste',
          'dragstart',
          'loadend',
          'animationstart',
          'search',
          'transitionrun',
          'transitionstart',
          'webkitanimationend',
          'webkitanimationiteration',
          'webkitanimationstart',
          'webkittransitionend',
        ];
        const mediaElementEventNames = [
          'encrypted',
          'waitingforkey',
          'msneedkey',
          'mozinterruptbegin',
          'mozinterruptend',
        ];
        const webglEventNames = [
          'webglcontextrestored',
          'webglcontextlost',
          'webglcontextcreationerror',
        ];
        const formEventNames = ['autocomplete', 'autocompleteerror'];
        const detailEventNames = ['toggle'];
        const frameEventNames = ['load'];
        const frameSetEventNames = [
          'blur',
          'error',
          'focus',
          'load',
          'resize',
          'scroll',
          'messageerror',
        ];
        const marqueeEventNames = ['bounce', 'finish', 'start'];

        const XMLHttpRequestEventNames = [
          'loadstart',
          'progress',
          'abort',
          'error',
          'load',
          'progress',
          'timeout',
          'loadend',
          'readystatechange',
        ];
        const IDBIndexEventNames = [
          'upgradeneeded',
          'complete',
          'abort',
          'success',
          'error',
          'blocked',
          'versionchange',
          'close',
        ];
        const websocketEventNames = ['close', 'error', 'open', 'message'];
        const workerEventNames = ['error', 'message'];

        const eventNames = globalEventHandlersEventNames.concat(
          webglEventNames,
          formEventNames,
          detailEventNames,
          documentEventNames,
          windowEventNames,
          htmlElementEventNames,
        );

        function checkIsOnPropertiesPatched(
          target: any,
          shouldPatchedProperties?: string[],
          ignoredProperties?: string[],
        ) {
          let checkTargetProps =
            shouldPatchedProperties && shouldPatchedProperties.map((p) => `on${p}`);
          if (!checkTargetProps) {
            checkTargetProps = [];
            for (let prop in target) {
              checkTargetProps.push(prop);
            }
          }
          for (let i = 0; i < checkTargetProps.length; i++) {
            const prop = checkTargetProps[i];
            if (
              ignoredProperties &&
              ignoredProperties.filter((ignoreProp) => ignoreProp === prop).length > 0
            ) {
              continue;
            }
            if (prop.slice(0, 2) === 'on' && prop.length > 2) {
              let propExistsOnTarget = false;
              let checkTarget = target;
              while (checkTarget && checkTarget !== Object) {
                const desc = Object.getOwnPropertyDescriptor(checkTarget, prop);
                if (desc && desc.configurable) {
                  propExistsOnTarget = true;
                  break;
                }
                checkTarget = Object.getPrototypeOf(checkTarget);
              }
              if (!propExistsOnTarget) {
                //console.warn(`${prop} not exists on target ${target}`);
                continue;
              }
              target[prop] = noop;
              if (!target[Zone.__symbol__('ON_PROPERTY' + prop.slice(2))]) {
                fail(`${prop} of ${target} is not patched`);
              } else {
                expect(target[prop]).toBe(noop);
                target[prop] = null;
                expect(target[Zone.__symbol__('ON_PROPERTY' + prop.slice(2))]).toBeNull();
              }
            }
          }
        }

        it('should patch all possible on properties on native prototype', function () {
          function isPropertyPatched(obj: any, prop: string, prototype?: any) {
            let desc = Object.getOwnPropertyDescriptor(obj, prop);
            if (!desc && prototype) {
              // when patch window object, use prototype to check prop exist or not
              const prototypeDesc = Object.getOwnPropertyDescriptor(prototype, prop);
              if (prototypeDesc) {
                desc = {enumerable: true, configurable: true};
              }
            }
            // if the descriptor not exists or is not configurable
            // just return
            if (!desc || !desc.configurable) {
              return true;
            }

            const onPropPatchedSymbol = zoneSymbol('on' + prop + 'patched');
            if (obj.hasOwnProperty(onPropPatchedSymbol) && obj[onPropPatchedSymbol]) {
              return true;
            }
            return false;
          }

          function isPropertiesPatched(obj: any, properties: string[] | null, prototype?: any) {
            if (properties) {
              for (let i = 0; i < properties.length; i++) {
                if (!isPropertyPatched(obj, 'on' + properties[i], prototype)) {
                  fail(`${properties[i]} is not patched on ${obj}`);
                }
              }
            }
          }

          isPropertiesPatched(
            window,
            eventNames.concat(['messageerror']),
            Object.getPrototypeOf(window),
          );
          isPropertiesPatched(Document.prototype, eventNames);

          if (typeof window['SVGElement'] !== 'undefined') {
            isPropertiesPatched(window['SVGElement'].prototype, eventNames);
          }
          isPropertiesPatched(Element.prototype, eventNames);
          isPropertiesPatched(HTMLElement.prototype, eventNames);
          isPropertiesPatched(HTMLMediaElement.prototype, mediaElementEventNames);
          isPropertiesPatched(
            HTMLFrameSetElement.prototype,
            windowEventNames.concat(frameSetEventNames),
          );
          isPropertiesPatched(
            HTMLBodyElement.prototype,
            windowEventNames.concat(frameSetEventNames),
          );
          isPropertiesPatched(HTMLFrameElement.prototype, frameEventNames);
          isPropertiesPatched(HTMLIFrameElement.prototype, frameEventNames);

          const HTMLMarqueeElement = window['HTMLMarqueeElement'];
          if (HTMLMarqueeElement) {
            isPropertiesPatched(HTMLMarqueeElement.prototype, marqueeEventNames);
          }
          const Worker = window['Worker'];
          if (Worker) {
            isPropertiesPatched(Worker.prototype, workerEventNames);
          }
          const XMLHttpRequest = window['XMLHttpRequest'];
          if (XMLHttpRequest) {
            // XMLHttpRequest is not available in ServiceWorker, so we need to check here
            isPropertiesPatched(XMLHttpRequest.prototype, XMLHttpRequestEventNames);
          }
          const XMLHttpRequestEventTarget = window['XMLHttpRequestEventTarget'];
          if (XMLHttpRequestEventTarget) {
            isPropertiesPatched(
              XMLHttpRequestEventTarget && XMLHttpRequestEventTarget.prototype,
              XMLHttpRequestEventNames,
            );
          }
          if (typeof IDBIndex !== 'undefined') {
            isPropertiesPatched(IDBIndex.prototype, IDBIndexEventNames);
            isPropertiesPatched(IDBRequest.prototype, IDBIndexEventNames);
            isPropertiesPatched(IDBOpenDBRequest.prototype, IDBIndexEventNames);
            isPropertiesPatched(IDBDatabase.prototype, IDBIndexEventNames);
            isPropertiesPatched(IDBTransaction.prototype, IDBIndexEventNames);
            isPropertiesPatched(IDBCursor.prototype, IDBIndexEventNames);
          }
          const WebSocket = window['WebSocket'];
          if (WebSocket) {
            isPropertiesPatched(WebSocket.prototype, websocketEventNames);
          }
        });

        it('should patch all possible on properties on element', function () {
          const htmlElementTagNames: string[] = [
            'a',
            'area',
            'audio',
            'base',
            'basefont',
            'blockquote',
            'br',
            'button',
            'canvas',
            'caption',
            'col',
            'colgroup',
            'data',
            'datalist',
            'del',
            'dir',
            'div',
            'dl',
            'embed',
            'fieldset',
            'font',
            'form',
            'frame',
            'frameset',
            'h1',
            'h2',
            'h3',
            'h4',
            'h5',
            'h6',
            'head',
            'hr',
            'html',
            'iframe',
            'img',
            'input',
            'ins',
            'isindex',
            'label',
            'legend',
            'li',
            'link',
            'listing',
            'map',
            'marquee',
            'menu',
            'meta',
            'meter',
            'nextid',
            'ol',
            'optgroup',
            'option',
            'output',
            'p',
            'param',
            'picture',
            'pre',
            'progress',
            'q',
            'script',
            'select',
            'source',
            'span',
            'style',
            'table',
            'tbody',
            'td',
            'template',
            'textarea',
            'tfoot',
            'th',
            'thead',
            'time',
            'title',
            'tr',
            'track',
            'ul',
            'video',
          ];
          htmlElementTagNames.forEach((tagName) => {
            checkIsOnPropertiesPatched(document.createElement(tagName), eventNames, [
              'onorientationchange',
            ]);
          });
        });

        it('should patch all possible on properties on svg element', function () {
          const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
          checkIsOnPropertiesPatched(svg, eventNames);
        });

        it('should patch all possible on properties on media element', function () {
          const audio = document.createElement('audio');
          checkIsOnPropertiesPatched(audio, mediaElementEventNames);
        });

        it('should patch all possible on properties on frameset element', function () {
          const frameset = document.createElement('frameset');
          checkIsOnPropertiesPatched(frameset, windowEventNames.concat(frameSetEventNames));
        });

        it('should patch all possible on properties on body', function () {
          checkIsOnPropertiesPatched(document.body, windowEventNames.concat(frameSetEventNames));
        });

        it('should patch all possible on properties on Frame', function () {
          const frame = document.createElement('frame');
          checkIsOnPropertiesPatched(frame, frameEventNames);
        });

        it('should patch all possible on properties on marquee element', function () {
          const marquee = document.createElement('marquee');
          checkIsOnPropertiesPatched(marquee, marqueeEventNames);
        });

        it('should patch all possible on properties on Window', function () {
          checkIsOnPropertiesPatched(window, eventNames.concat(['messageerror']));
        });

        it('should patch all possible on properties on xhr', function () {
          checkIsOnPropertiesPatched(new XMLHttpRequest(), XMLHttpRequestEventNames);
        });

        it('should patch all possible on properties on worker', function () {
          checkIsOnPropertiesPatched(
            new Worker('/packages/zone.js/test/assets/empty-worker.js'),
            workerEventNames,
          );
        });

        it('should patch all possible on properties on websocket', function () {
          try {
            checkIsOnPropertiesPatched(new WebSocket('ws://localhost:8001'), websocketEventNames);
          } catch (e) {
            console.log('error when creating websocket', e);
          }
        });

        it('should not patch ignored on properties', function () {
          const TestTarget: any = (window as any)['TestTarget'];
          patchFilteredProperties(
            TestTarget.prototype,
            ['prop1', 'prop2'],
            global['__Zone_ignore_on_properties'],
          );
          const testTarget = new TestTarget();
          Zone.current.fork({name: 'test'}).run(() => {
            testTarget.onprop1 = function () {
              // onprop1 should not be patched
              expect(Zone.current.name).toEqual('test1');
            };
            testTarget.onprop2 = function () {
              // onprop2 should be patched
              expect(Zone.current.name).toEqual('test');
            };
          });

          Zone.current.fork({name: 'test1'}).run(() => {
            testTarget.dispatchEvent('prop1');
            testTarget.dispatchEvent('prop2');
          });
        });

        it('should not patch ignored eventListener', function () {
          let scrollEvent = document.createEvent('Event');
          scrollEvent.initEvent('scroll', true, true);

          const zone = Zone.current.fork({name: 'run'});
          const div = document.createElement('div');
          document.body.appendChild(div);

          Zone.current.fork({name: 'scroll'}).run(() => {
            const listener = () => {
              expect(Zone.current.name).toEqual(zone.name);
              div.removeEventListener('scroll', listener);
            };
            div.addEventListener('scroll', listener);
          });

          zone.run(() => {
            div.dispatchEvent(scrollEvent);
          });
          document.body.removeChild(div);
        });

        it('non function property starts with on but not event listener should still work as expected', () => {
          (window as any).one_two_three = {foo: 'bar'};

          expect((window as any).one_two_three).toEqual({foo: 'bar'});
          (window as any).one_two_three = {bar: 'foo'};
          expect((window as any).one_two_three).toEqual({bar: 'foo'});
          (window as any).one_two_three = null;
          expect((window as any).one_two_three).toBeNull();
        });

        it('function property starts with on but not event listener should still work as expected', () => {
          let called = false;
          const func = function () {
            called = true;
          };
          (window as any).one_two_three = func;

          expect((window as any).one_two_three).toEqual(func);
          expect(called).toBeFalse();
          (window as any).one_two_three();
          expect(called).toBeTrue();
          (window as any).one_two_three = null;
          expect((window as any).one_two_three).toBeNull();
        });

        it('should be able to clear on handler added before load zone.js', function () {
          const TestTarget: any = (window as any)['TestTarget'];
          patchFilteredProperties(
            TestTarget.prototype,
            ['prop3'],
            global['__Zone_ignore_on_properties'],
          );
          const testTarget = new TestTarget();
          Zone.current.fork({name: 'test'}).run(() => {
            expect(testTarget.onprop3).toBeTruthy();
            const newProp3Handler = function () {};
            testTarget.onprop3 = newProp3Handler;
            expect(testTarget.onprop3).toBe(newProp3Handler);
            testTarget.onprop3 = null;
            expect(!testTarget.onprop3).toBeTruthy();
            testTarget.onprop3 = function () {
              // onprop1 should not be patched
              expect(Zone.current.name).toEqual('test');
            };
          });

          Zone.current.fork({name: 'test1'}).run(() => {
            testTarget.dispatchEvent('prop3');
          });
        });

        it(
          'window onmousedown should be in zone',
          ifEnvSupports(canPatchOnProperty(window, 'onmousedown'), function () {
            zone.run(function () {
              window.onmousedown = eventListenerSpy;
            });

            window.dispatchEvent(mouseEvent);

            expect(hookSpy).toHaveBeenCalled();
            expect(eventListenerSpy).toHaveBeenCalled();
            window.removeEventListener('mousedown', eventListenerSpy);
            expect((window as any)[zoneSymbol('ON_PROPERTYmousedown')]).toEqual(eventListenerSpy);
            window.onmousedown = null;
            expect(!!(window as any)[zoneSymbol('ON_PROPERTYmousedown')]).toBeFalsy();
          }),
        );

        it(
          'window onresize should be patched',
          ifEnvSupports(canPatchOnProperty(window, 'onmousedown'), function () {
            window.onresize = eventListenerSpy;
            const innerResizeProp: any = (window as any)[zoneSymbol('ON_PROPERTYresize')];
            expect(innerResizeProp).toBeTruthy();
            innerResizeProp();
            expect(eventListenerSpy).toHaveBeenCalled();
            window.removeEventListener('resize', eventListenerSpy);
            expect((window as any)[zoneSymbol('ON_PROPERTYresize')]).toEqual(eventListenerSpy);
            window.onresize = null;
            expect(!!(window as any)[zoneSymbol('ON_PROPERTYresize')]).toBeFalsy();
          }),
        );

        it(
          'document onmousedown should be in zone',
          ifEnvSupports(canPatchOnProperty(Document.prototype, 'onmousedown'), function () {
            zone.run(function () {
              document.onmousedown = eventListenerSpy;
            });

            document.dispatchEvent(mouseEvent);

            expect(hookSpy).toHaveBeenCalled();
            expect(eventListenerSpy).toHaveBeenCalled();
            document.removeEventListener('mousedown', eventListenerSpy);
            expect((document as any)[zoneSymbol('ON_PROPERTYmousedown')]).toEqual(eventListenerSpy);
            document.onmousedown = null;
            expect(!!(document as any)[zoneSymbol('ON_PROPERTYmousedown')]).toBeFalsy();
          }),
        );

        // TODO: JiaLiPassion, need to find out why the test bundle is not `use strict`.
        xit(
          'event handler with null context should use event.target',
          ifEnvSupports(canPatchOnProperty(Document.prototype, 'onmousedown'), function () {
            const logs: string[] = [];
            const EventTarget = (window as any)['EventTarget'];
            let oriAddEventListener =
              EventTarget && EventTarget.prototype
                ? (EventTarget.prototype as any)[zoneSymbol('addEventListener')]
                : (HTMLSpanElement.prototype as any)[zoneSymbol('addEventListener')];

            if (!oriAddEventListener) {
              // no patched addEventListener found
              return;
            }
            let handler1: Function;
            let handler2: Function;

            const listener = function () {
              logs.push('listener1');
            };

            const listener1 = function () {
              logs.push('listener2');
            };

            HTMLSpanElement.prototype.addEventListener = function (
              eventName: string,
              callback: any,
            ) {
              if (eventName === 'click') {
                handler1 = callback;
              } else if (eventName === 'mousedown') {
                handler2 = callback;
              }
              return oriAddEventListener.apply(this, arguments);
            };

            (HTMLSpanElement.prototype as any)[zoneSymbol('addEventListener')] = null;

            patchEventTarget(window, null as any, [HTMLSpanElement.prototype]);

            const span = document.createElement('span');
            document.body.appendChild(span);

            zone.run(function () {
              span.addEventListener('click', listener);
              span.onmousedown = listener1;
            });

            expect(handler1!).toBe(handler2!);

            handler1!.apply(null, [{type: 'click', target: span}]);

            handler2!.apply(null, [{type: 'mousedown', target: span}]);

            expect(hookSpy).toHaveBeenCalled();
            expect(logs).toEqual(['listener1', 'listener2']);
            document.body.removeChild(span);
            if (EventTarget) {
              (EventTarget.prototype as any)[zoneSymbol('addEventListener')] = oriAddEventListener;
            } else {
              (HTMLSpanElement.prototype as any)[zoneSymbol('addEventListener')] =
                oriAddEventListener;
            }
          }),
        );

        it(
          'SVGElement onmousedown should be in zone',
          ifEnvSupports(
            canPatchOnProperty(SVGElement && SVGElement.prototype, 'onmousedown'),
            function () {
              const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
              document.body.appendChild(svg);
              zone.run(function () {
                svg.onmousedown = eventListenerSpy;
              });

              svg.dispatchEvent(mouseEvent);

              expect(hookSpy).toHaveBeenCalled();
              expect(eventListenerSpy).toHaveBeenCalled();
              svg.removeEventListener('mouse', eventListenerSpy);
              document.body.removeChild(svg);
            },
          ),
        );

        it(
          'get window onerror should not throw error',
          ifEnvSupports(canPatchOnProperty(window, 'onerror'), function () {
            const oriOnError = window.onerror;
            const testFn = function () {
              try {
                let onerror = window.onerror;
                window.onerror = function () {};
                onerror = window.onerror;
              } finally {
                window.onerror = oriOnError;
              }
            };
            expect(testFn).not.toThrow();
          }),
        );

        // TODO: Sort out why the thrown error bubbles up before the window.onerror callback is
        // executed, though confirmed it does get executed and passes the expectations.
        xit(
          'window.onerror callback signature should be (message, source, lineno, colno, error)',
          ifEnvSupportsWithDone(canPatchOnProperty(window, 'onerror'), function (done: DoneFn) {
            const oriOnError = window.onerror;
            let testError = new Error('testError');
            window.onerror = function (
              message: any,
              source?: string,
              lineno?: number,
              colno?: number,
              error?: any,
            ) {
              expect(message).toContain('testError');
              expect(error).toBe(testError);
              (window as any).onerror = oriOnError;
              setTimeout(done);
              return true;
            };
            setTimeout(() => {
              throw testError;
            }, 100);
          }),
        );
      }),
    );

    describe('eventListener hooks', function () {
      let button: HTMLButtonElement;
      let clickEvent: Event;

      beforeEach(function () {
        button = document.createElement('button');
        clickEvent = document.createEvent('Event');
        clickEvent.initEvent('click', true, true);
        document.body.appendChild(button);
      });

      afterEach(function () {
        document.body.removeChild(button);
      });

      it('should support addEventListener', function () {
        const hookSpy = jasmine.createSpy('hook');
        const eventListenerSpy = jasmine.createSpy('eventListener');
        const zone = rootZone.fork({
          name: 'spy',
          onScheduleTask: (
            parentZoneDelegate: ZoneDelegate,
            currentZone: Zone,
            targetZone: Zone,
            task: Task,
          ): any => {
            hookSpy();
            return parentZoneDelegate.scheduleTask(targetZone, task);
          },
        });

        zone.run(function () {
          button.addEventListener('click', eventListenerSpy);
        });

        button.dispatchEvent(clickEvent);

        expect(hookSpy).toHaveBeenCalled();
        expect(eventListenerSpy).toHaveBeenCalled();
      });

      it('should be able to access addEventListener information in onScheduleTask', function () {
        const hookSpy = jasmine.createSpy('hook');
        const eventListenerSpy = jasmine.createSpy('eventListener');
        let scheduleButton;
        let scheduleEventName: string | undefined;
        let scheduleCapture: boolean | undefined;
        let scheduleTask;
        const zone = rootZone.fork({
          name: 'spy',
          onScheduleTask: (
            parentZoneDelegate: ZoneDelegate,
            currentZone: Zone,
            targetZone: Zone,
            task: Task,
          ): any => {
            hookSpy();
            scheduleButton = (task.data as any).taskData.target;
            scheduleEventName = (task.data as any).taskData.eventName;
            scheduleCapture = (task.data as any).taskData.capture;
            scheduleTask = task;
            return parentZoneDelegate.scheduleTask(targetZone, task);
          },
        });

        zone.run(function () {
          button.addEventListener('click', eventListenerSpy);
        });

        button.dispatchEvent(clickEvent);

        expect(hookSpy).toHaveBeenCalled();
        expect(eventListenerSpy).toHaveBeenCalled();
        expect(scheduleButton).toBe(button as any);
        expect(scheduleEventName).toBe('click');
        expect(scheduleCapture).toBe(false);
        expect(scheduleTask && (scheduleTask as any).data.taskData).toBe(null as any);
      });

      it(
        'should support addEventListener on window',
        ifEnvSupports(windowPrototype, function () {
          const hookSpy = jasmine.createSpy('hook');
          const eventListenerSpy = jasmine.createSpy('eventListener');
          const zone = rootZone.fork({
            name: 'spy',
            onScheduleTask: (
              parentZoneDelegate: ZoneDelegate,
              currentZone: Zone,
              targetZone: Zone,
              task: Task,
            ): any => {
              hookSpy();
              return parentZoneDelegate.scheduleTask(targetZone, task);
            },
          });

          zone.run(function () {
            window.addEventListener('click', eventListenerSpy);
          });

          window.dispatchEvent(clickEvent);

          expect(hookSpy).toHaveBeenCalled();
          expect(eventListenerSpy).toHaveBeenCalled();
        }),
      );

      it('should support removeEventListener', function () {
        const hookSpy = jasmine.createSpy('hook');
        const eventListenerSpy = jasmine.createSpy('eventListener');
        const zone = rootZone.fork({
          name: 'spy',
          onCancelTask: (
            parentZoneDelegate: ZoneDelegate,
            currentZone: Zone,
            targetZone: Zone,
            task: Task,
          ): any => {
            hookSpy();
            return parentZoneDelegate.cancelTask(targetZone, task);
          },
        });

        zone.run(function () {
          button.addEventListener('click', eventListenerSpy);
          button.removeEventListener('click', eventListenerSpy);
        });

        button.dispatchEvent(clickEvent);

        expect(hookSpy).toHaveBeenCalled();
        expect(eventListenerSpy).not.toHaveBeenCalled();
      });

      describe(
        'should support addEventListener/removeEventListener with AddEventListenerOptions with capture setting',
        ifEnvSupports(supportEventListenerOptions, function () {
          let hookSpy: Spy;
          let cancelSpy: Spy;
          let logs: string[];
          const zone = rootZone.fork({
            name: 'spy',
            onScheduleTask: (
              parentZoneDelegate: ZoneDelegate,
              currentZone: Zone,
              targetZone: Zone,
              task: Task,
            ): any => {
              hookSpy();
              return parentZoneDelegate.scheduleTask(targetZone, task);
            },
            onCancelTask: (
              parentZoneDelegate: ZoneDelegate,
              currentZone: Zone,
              targetZone: Zone,
              task: Task,
            ): any => {
              cancelSpy();
              return parentZoneDelegate.cancelTask(targetZone, task);
            },
          });

          const docListener = () => {
            logs.push('document');
          };
          const btnListener = () => {
            logs.push('button');
          };

          beforeEach(() => {
            logs = [];
            hookSpy = jasmine.createSpy('hook');
            cancelSpy = jasmine.createSpy('cancel');
          });

          it('should handle child event when addEventListener with capture true', () => {
            // test capture false
            zone.run(function () {
              (document as any).addEventListener('click', docListener, {capture: false});
              button.addEventListener('click', btnListener);
            });

            button.dispatchEvent(clickEvent);
            expect(hookSpy).toHaveBeenCalled();
            expect(logs).toEqual(['button', 'document']);
            logs = [];

            (document as any).removeEventListener('click', docListener, {capture: false});
            button.removeEventListener('click', btnListener);
            expect(cancelSpy).toHaveBeenCalled();

            button.dispatchEvent(clickEvent);
            expect(logs).toEqual([]);
          });
        }),
      );

      describe(
        'should ignore duplicate event handler',
        ifEnvSupports(supportEventListenerOptions, function () {
          let hookSpy: Spy;
          let cancelSpy: Spy;
          let logs: string[];
          const zone = rootZone.fork({
            name: 'spy',
            onScheduleTask: (
              parentZoneDelegate: ZoneDelegate,
              currentZone: Zone,
              targetZone: Zone,
              task: Task,
            ): any => {
              hookSpy();
              return parentZoneDelegate.scheduleTask(targetZone, task);
            },
            onCancelTask: (
              parentZoneDelegate: ZoneDelegate,
              currentZone: Zone,
              targetZone: Zone,
              task: Task,
            ): any => {
              cancelSpy();
              return parentZoneDelegate.cancelTask(targetZone, task);
            },
          });

          const docListener = () => {
            logs.push('document options');
          };

          beforeEach(() => {
            logs = [];
            hookSpy = jasmine.createSpy('hook');
            cancelSpy = jasmine.createSpy('cancel');
          });

          const testDuplicate = function (args1?: any, args2?: any) {
            zone.run(function () {
              if (args1) {
                (document as any).addEventListener('click', docListener, args1);
              } else {
                (document as any).addEventListener('click', docListener);
              }
              if (args2) {
                (document as any).addEventListener('click', docListener, args2);
              } else {
                (document as any).addEventListener('click', docListener);
              }
            });

            button.dispatchEvent(clickEvent);
            expect(hookSpy).toHaveBeenCalled();
            expect(logs).toEqual(['document options']);
            logs = [];

            (document as any).removeEventListener('click', docListener, args1);
            expect(cancelSpy).toHaveBeenCalled();
            button.dispatchEvent(clickEvent);
            expect(logs).toEqual([]);
          };

          it('should ignore duplicate handler', () => {
            let captureFalse = [
              undefined,
              false,
              {capture: false},
              {capture: false, passive: false},
              {passive: false},
              {},
            ];
            let captureTrue = [true, {capture: true}, {capture: true, passive: false}];
            for (let i = 0; i < captureFalse.length; i++) {
              for (let j = 0; j < captureFalse.length; j++) {
                testDuplicate(captureFalse[i], captureFalse[j]);
              }
            }
            for (let i = 0; i < captureTrue.length; i++) {
              for (let j = 0; j < captureTrue.length; j++) {
                testDuplicate(captureTrue[i], captureTrue[j]);
              }
            }
          });
        }),
      );

      describe(
        'should support mix useCapture with AddEventListenerOptions capture',
        ifEnvSupports(supportEventListenerOptions, function () {
          let hookSpy: Spy;
          let cancelSpy: Spy;
          let logs: string[];
          const zone = rootZone.fork({
            name: 'spy',
            onScheduleTask: (
              parentZoneDelegate: ZoneDelegate,
              currentZone: Zone,
              targetZone: Zone,
              task: Task,
            ): any => {
              hookSpy();
              return parentZoneDelegate.scheduleTask(targetZone, task);
            },
            onCancelTask: (
              parentZoneDelegate: ZoneDelegate,
              currentZone: Zone,
              targetZone: Zone,
              task: Task,
            ): any => {
              cancelSpy();
              return parentZoneDelegate.cancelTask(targetZone, task);
            },
          });

          const docListener = () => {
            logs.push('document options');
          };
          const docListener1 = () => {
            logs.push('document useCapture');
          };
          const btnListener = () => {
            logs.push('button');
          };

          beforeEach(() => {
            logs = [];
            hookSpy = jasmine.createSpy('hook');
            cancelSpy = jasmine.createSpy('cancel');
          });

          const testAddRemove = function (args1?: any, args2?: any) {
            zone.run(function () {
              if (args1) {
                (document as any).addEventListener('click', docListener, args1);
              } else {
                (document as any).addEventListener('click', docListener);
              }
              if (args2) {
                (document as any).removeEventListener('click', docListener, args2);
              } else {
                (document as any).removeEventListener('click', docListener);
              }
            });

            button.dispatchEvent(clickEvent);
            expect(cancelSpy).toHaveBeenCalled();
            expect(logs).toEqual([]);
          };

          it('should be able to add/remove same handler with mix options and capture', function () {
            let captureFalse = [
              undefined,
              false,
              {capture: false},
              {capture: false, passive: false},
              {passive: false},
              {},
            ];
            let captureTrue = [true, {capture: true}, {capture: true, passive: false}];
            for (let i = 0; i < captureFalse.length; i++) {
              for (let j = 0; j < captureFalse.length; j++) {
                testAddRemove(captureFalse[i], captureFalse[j]);
              }
            }
            for (let i = 0; i < captureTrue.length; i++) {
              for (let j = 0; j < captureTrue.length; j++) {
                testAddRemove(captureTrue[i], captureTrue[j]);
              }
            }
          });

          const testDifferent = function (args1?: any, args2?: any) {
            zone.run(function () {
              if (args1) {
                (document as any).addEventListener('click', docListener, args1);
              } else {
                (document as any).addEventListener('click', docListener);
              }
              if (args2) {
                (document as any).addEventListener('click', docListener1, args2);
              } else {
                (document as any).addEventListener('click', docListener1);
              }
            });

            button.dispatchEvent(clickEvent);
            expect(hookSpy).toHaveBeenCalled();
            expect(logs.sort()).toEqual(['document options', 'document useCapture']);
            logs = [];

            if (args1) {
              (document as any).removeEventListener('click', docListener, args1);
            } else {
              (document as any).removeEventListener('click', docListener);
            }

            button.dispatchEvent(clickEvent);
            expect(logs).toEqual(['document useCapture']);
            logs = [];

            if (args2) {
              (document as any).removeEventListener('click', docListener1, args2);
            } else {
              (document as any).removeEventListener('click', docListener1);
            }

            button.dispatchEvent(clickEvent);
            expect(logs).toEqual([]);
          };

          it('should be able to add different handlers for same event', function () {
            let captureFalse = [
              undefined,
              false,
              {capture: false},
              {capture: false, passive: false},
              {passive: false},
              {},
            ];
            let captureTrue = [true, {capture: true}, {capture: true, passive: false}];
            for (let i = 0; i < captureFalse.length; i++) {
              for (let j = 0; j < captureTrue.length; j++) {
                testDifferent(captureFalse[i], captureTrue[j]);
              }
            }
            for (let i = 0; i < captureTrue.length; i++) {
              for (let j = 0; j < captureFalse.length; j++) {
                testDifferent(captureTrue[i], captureFalse[j]);
              }
            }
          });

          it('should handle options.capture true with capture true correctly', function () {
            zone.run(function () {
              (document as any).addEventListener('click', docListener, {capture: true});
              document.addEventListener('click', docListener1, true);
              button.addEventListener('click', btnListener);
            });

            button.dispatchEvent(clickEvent);
            expect(hookSpy).toHaveBeenCalled();
            expect(logs).toEqual(['document options', 'document useCapture', 'button']);
            logs = [];

            (document as any).removeEventListener('click', docListener, {capture: true});
            button.dispatchEvent(clickEvent);
            expect(logs).toEqual(['document useCapture', 'button']);
            logs = [];

            document.removeEventListener('click', docListener1, true);
            button.dispatchEvent(clickEvent);
            expect(logs).toEqual(['button']);
            logs = [];

            button.removeEventListener('click', btnListener);
            expect(cancelSpy).toHaveBeenCalled();

            button.dispatchEvent(clickEvent);
            expect(logs).toEqual([]);
            (document as any).removeAllListeners('click');
          });
        }),
      );

      it(
        'should support addEventListener with AddEventListenerOptions once setting',
        ifEnvSupports(supportEventListenerOptions, function () {
          let hookSpy = jasmine.createSpy('hook');
          let logs: string[] = [];
          const zone = rootZone.fork({
            name: 'spy',
            onScheduleTask: (
              parentZoneDelegate: ZoneDelegate,
              currentZone: Zone,
              targetZone: Zone,
              task: Task,
            ): any => {
              hookSpy();
              return parentZoneDelegate.scheduleTask(targetZone, task);
            },
          });

          zone.run(function () {
            (button as any).addEventListener(
              'click',
              function () {
                logs.push('click');
              },
              {once: true},
            );
          });

          button.dispatchEvent(clickEvent);

          expect(hookSpy).toHaveBeenCalled();
          expect(logs.length).toBe(1);
          expect(logs).toEqual(['click']);
          logs = [];

          button.dispatchEvent(clickEvent);
          expect(logs.length).toBe(0);
        }),
      );

      it(
        'should support addEventListener with AddEventListenerOptions once setting and capture',
        ifEnvSupports(supportEventListenerOptions, function () {
          let hookSpy = jasmine.createSpy('hook');
          let logs: string[] = [];
          const zone = rootZone.fork({
            name: 'spy',
            onScheduleTask: (
              parentZoneDelegate: ZoneDelegate,
              currentZone: Zone,
              targetZone: Zone,
              task: Task,
            ): any => {
              hookSpy();
              return parentZoneDelegate.scheduleTask(targetZone, task);
            },
          });

          zone.run(function () {
            (button as any).addEventListener(
              'click',
              function () {
                logs.push('click');
              },
              {once: true, capture: true},
            );
          });

          button.dispatchEvent(clickEvent);

          expect(hookSpy).toHaveBeenCalled();
          expect(logs.length).toBe(1);
          expect(logs).toEqual(['click']);
          logs = [];

          button.dispatchEvent(clickEvent);
          expect(logs.length).toBe(0);
        }),
      );

      it(
        'should support add multiple listeners with AddEventListenerOptions once setting and same capture after normal listener',
        ifEnvSupports(supportEventListenerOptions, function () {
          let logs: string[] = [];

          button.addEventListener(
            'click',
            function () {
              logs.push('click');
            },
            true,
          );
          (button as any).addEventListener(
            'click',
            function () {
              logs.push('once click');
            },
            {once: true, capture: true},
          );

          button.dispatchEvent(clickEvent);

          expect(logs.length).toBe(2);
          expect(logs).toEqual(['click', 'once click']);
          logs = [];

          button.dispatchEvent(clickEvent);
          expect(logs.length).toBe(1);
          expect(logs).toEqual(['click']);
        }),
      );

      it(
        'should support add multiple listeners with AddEventListenerOptions once setting and mixed capture after normal listener',
        ifEnvSupports(supportEventListenerOptions, function () {
          let logs: string[] = [];

          button.addEventListener('click', function () {
            logs.push('click');
          });
          (button as any).addEventListener(
            'click',
            function () {
              logs.push('once click');
            },
            {once: true, capture: true},
          );

          button.dispatchEvent(clickEvent);

          expect(logs.length).toBe(2);
          expect(logs).toEqual(['once click', 'click']);
          logs = [];

          button.dispatchEvent(clickEvent);
          expect(logs.length).toBe(1);
          expect(logs).toEqual(['click']);
        }),
      );

      it(
        'should support add multiple listeners with AddEventListenerOptions once setting before normal listener',
        ifEnvSupports(supportEventListenerOptions, function () {
          let logs: string[] = [];

          (button as any).addEventListener(
            'click',
            function () {
              logs.push('once click');
            },
            {once: true},
          );

          button.addEventListener('click', function () {
            logs.push('click');
          });

          button.dispatchEvent(clickEvent);

          expect(logs.length).toBe(2);
          expect(logs).toEqual(['once click', 'click']);
          logs = [];

          button.dispatchEvent(clickEvent);
          expect(logs.length).toBe(1);
          expect(logs).toEqual(['click']);
        }),
      );

      it(
        'should support add multiple listeners with AddEventListenerOptions once setting with same capture before normal listener',
        ifEnvSupports(supportEventListenerOptions, function () {
          let logs: string[] = [];

          (button as any).addEventListener(
            'click',
            function () {
              logs.push('once click');
            },
            {once: true, capture: true},
          );

          button.addEventListener(
            'click',
            function () {
              logs.push('click');
            },
            true,
          );

          button.dispatchEvent(clickEvent);

          expect(logs.length).toBe(2);
          expect(logs).toEqual(['once click', 'click']);
          logs = [];

          button.dispatchEvent(clickEvent);
          expect(logs.length).toBe(1);
          expect(logs).toEqual(['click']);
        }),
      );

      it(
        'should support add multiple listeners with AddEventListenerOptions once setting with mixed capture before normal listener',
        ifEnvSupports(supportEventListenerOptions, function () {
          let logs: string[] = [];

          (button as any).addEventListener(
            'click',
            function () {
              logs.push('once click');
            },
            {once: true, capture: true},
          );

          button.addEventListener('click', function () {
            logs.push('click');
          });

          button.dispatchEvent(clickEvent);

          expect(logs.length).toBe(2);
          expect(logs).toEqual(['once click', 'click']);
          logs = [];

          button.dispatchEvent(clickEvent);
          expect(logs.length).toBe(1);
          expect(logs).toEqual(['click']);
        }),
      );

      it('should change options to boolean if not support passive', () => {
        patchEventTarget(window, null as any, [TestEventListener.prototype]);
        const testEventListener = new TestEventListener();

        const listener = function () {};
        testEventListener.addEventListener('test', listener, {passive: true});
        testEventListener.addEventListener('test1', listener, {once: true});
        testEventListener.addEventListener('test2', listener, {capture: true});
        testEventListener.addEventListener('test3', listener, {passive: false});
        testEventListener.addEventListener('test4', listener, {once: false});
        testEventListener.addEventListener('test5', listener, {capture: false});
        if (!supportsPassive) {
          expect(testEventListener.logs).toEqual([false, false, true, false, false, false]);
        } else {
          expect(testEventListener.logs).toEqual([
            {passive: true},
            {once: true},
            {capture: true},
            {passive: false},
            {once: false},
            {capture: false},
          ]);
        }
      });

      it('should change options to boolean if not support passive on HTMLElement', () => {
        const logs: string[] = [];
        const listener = (e: Event) => {
          logs.push('clicked');
        };

        (button as any).addEventListener('click', listener, {once: true});
        button.dispatchEvent(clickEvent);
        expect(logs).toEqual(['clicked']);
        button.dispatchEvent(clickEvent);
        if (supportsPassive) {
          expect(logs).toEqual(['clicked']);
        } else {
          expect(logs).toEqual(['clicked', 'clicked']);
        }

        button.removeEventListener('click', listener);
      });

      it(
        'should support addEventListener with AddEventListenerOptions passive setting',
        ifEnvSupports(supportEventListenerOptions, function () {
          const hookSpy = jasmine.createSpy('hook');
          const logs: string[] = [];
          const zone = rootZone.fork({
            name: 'spy',
            onScheduleTask: (
              parentZoneDelegate: ZoneDelegate,
              currentZone: Zone,
              targetZone: Zone,
              task: Task,
            ): any => {
              hookSpy();
              return parentZoneDelegate.scheduleTask(targetZone, task);
            },
          });

          const listener = (e: Event) => {
            logs.push(e.defaultPrevented.toString());
            e.preventDefault();
            logs.push(e.defaultPrevented.toString());
          };

          zone.run(function () {
            (button as any).addEventListener('click', listener, {passive: true});
          });

          button.dispatchEvent(clickEvent);

          expect(hookSpy).toHaveBeenCalled();
          expect(logs).toEqual(['false', 'false']);

          button.removeEventListener('click', listener);
        }),
      );

      describe('passiveEvents by global settings', () => {
        let logs: string[] = [];
        const listener = (e: Event) => {
          logs.push(e.defaultPrevented ? 'defaultPrevented' : 'default will run');
          e.preventDefault();
          logs.push(e.defaultPrevented ? 'defaultPrevented' : 'default will run');
        };
        const testPassive = function (eventName: string, expectedPassiveLog: string, options: any) {
          (button as any).addEventListener(eventName, listener, options);
          const evt = document.createEvent('Event');
          evt.initEvent(eventName, false, true);
          button.dispatchEvent(evt);
          expect(logs).toEqual(['default will run', expectedPassiveLog]);
          (button as any).removeAllListeners(eventName);
        };
        beforeEach(() => {
          logs = [];
          (button as any).removeAllListeners();
        });
        afterEach(() => {
          (button as any).removeAllListeners();
        });
        it('should be passive with global variable defined', () => {
          testPassive('touchstart', 'default will run', {passive: true});
        });
        it('should not be passive without global variable defined', () => {
          testPassive('touchend', 'defaultPrevented', undefined);
        });
        it('should be passive with global variable defined even without passive options', () => {
          testPassive('touchstart', 'default will run', undefined);
        });
        it('should be passive with global variable defined even without passive options and with capture', () => {
          testPassive('touchstart', 'default will run', {capture: true});
        });
        it('should be passive with global variable defined with capture option', () => {
          testPassive('touchstart', 'default will run', true);
        });
        it('should not be passive with global variable defined with passive false option', () => {
          testPassive('touchstart', 'defaultPrevented', {passive: false});
        });
        it('should be passive with global variable defined and also unpatched', () => {
          testPassive('scroll', 'default will run', undefined);
        });
        it('should not be passive without global variable defined and also unpatched', () => {
          testPassive('wheel', 'defaultPrevented', undefined);
        });
      });

      it(
        'should support Event.stopImmediatePropagation',
        ifEnvSupports(supportEventListenerOptions, function () {
          const hookSpy = jasmine.createSpy('hook');
          const logs: any[] = [];
          const zone = rootZone.fork({
            name: 'spy',
            onScheduleTask: (
              parentZoneDelegate: ZoneDelegate,
              currentZone: Zone,
              targetZone: Zone,
              task: Task,
            ): any => {
              hookSpy();
              return parentZoneDelegate.scheduleTask(targetZone, task);
            },
          });

          const listener1 = (e: Event) => {
            logs.push('listener1');
            e.stopImmediatePropagation();
          };

          const listener2 = (e: Event) => {
            logs.push('listener2');
          };

          zone.run(function () {
            (button as any).addEventListener('click', listener1);
            (button as any).addEventListener('click', listener2);
          });

          button.dispatchEvent(clickEvent);

          expect(hookSpy).toHaveBeenCalled();
          expect(logs).toEqual(['listener1']);

          button.removeEventListener('click', listener1);
          button.removeEventListener('click', listener2);
        }),
      );

      it('should support remove event listener by call zone.cancelTask directly', function () {
        let logs: string[] = [];
        let eventTask: Task;
        const zone = rootZone.fork({
          name: 'spy',
          onScheduleTask: (
            parentZoneDelegate: ZoneDelegate,
            currentZone: Zone,
            targetZone: Zone,
            task: Task,
          ): any => {
            eventTask = task;
            return parentZoneDelegate.scheduleTask(targetZone, task);
          },
        });

        zone.run(() => {
          button.addEventListener('click', function () {
            logs.push('click');
          });
        });
        let listeners = button.eventListeners!('click');
        expect(listeners.length).toBe(1);
        eventTask!.zone.cancelTask(eventTask!);

        listeners = button.eventListeners!('click');
        button.dispatchEvent(clickEvent);
        expect(logs.length).toBe(0);
        expect(listeners.length).toBe(0);
      });

      it('should support remove event listener by call zone.cancelTask directly with capture=true', function () {
        let logs: string[] = [];
        let eventTask: Task;
        const zone = rootZone.fork({
          name: 'spy',
          onScheduleTask: (
            parentZoneDelegate: ZoneDelegate,
            currentZone: Zone,
            targetZone: Zone,
            task: Task,
          ): any => {
            eventTask = task;
            return parentZoneDelegate.scheduleTask(targetZone, task);
          },
        });

        zone.run(() => {
          button.addEventListener(
            'click',
            function () {
              logs.push('click');
            },
            true,
          );
        });
        let listeners = button.eventListeners!('click');
        expect(listeners.length).toBe(1);
        eventTask!.zone.cancelTask(eventTask!);

        listeners = button.eventListeners!('click');
        button.dispatchEvent(clickEvent);
        expect(logs.length).toBe(0);
        expect(listeners.length).toBe(0);
      });

      it('should support remove event listeners by call zone.cancelTask directly with multiple listeners', function () {
        let logs: string[] = [];
        let eventTask: Task;
        const zone = rootZone.fork({
          name: 'spy',
          onScheduleTask: (
            parentZoneDelegate: ZoneDelegate,
            currentZone: Zone,
            targetZone: Zone,
            task: Task,
          ): any => {
            eventTask = task;
            return parentZoneDelegate.scheduleTask(targetZone, task);
          },
        });

        zone.run(() => {
          button.addEventListener('click', function () {
            logs.push('click1');
          });
        });
        button.addEventListener('click', function () {
          logs.push('click2');
        });
        let listeners = button.eventListeners!('click');
        expect(listeners.length).toBe(2);

        button.dispatchEvent(clickEvent);
        expect(logs.length).toBe(2);
        expect(logs).toEqual(['click1', 'click2']);
        eventTask!.zone.cancelTask(eventTask!);
        logs = [];

        listeners = button.eventListeners!('click');
        button.dispatchEvent(clickEvent);
        expect(logs.length).toBe(1);
        expect(listeners.length).toBe(1);
        expect(logs).toEqual(['click2']);
      });

      it('should support remove event listeners by call zone.cancelTask directly with multiple listeners with same capture=true', function () {
        let logs: string[] = [];
        let eventTask: Task;
        const zone = rootZone.fork({
          name: 'spy',
          onScheduleTask: (
            parentZoneDelegate: ZoneDelegate,
            currentZone: Zone,
            targetZone: Zone,
            task: Task,
          ): any => {
            eventTask = task;
            return parentZoneDelegate.scheduleTask(targetZone, task);
          },
        });

        zone.run(() => {
          button.addEventListener(
            'click',
            function () {
              logs.push('click1');
            },
            true,
          );
        });
        button.addEventListener(
          'click',
          function () {
            logs.push('click2');
          },
          true,
        );
        let listeners = button.eventListeners!('click');
        expect(listeners.length).toBe(2);

        button.dispatchEvent(clickEvent);
        expect(logs.length).toBe(2);
        expect(logs).toEqual(['click1', 'click2']);
        eventTask!.zone.cancelTask(eventTask!);
        logs = [];

        listeners = button.eventListeners!('click');
        button.dispatchEvent(clickEvent);
        expect(logs.length).toBe(1);
        expect(listeners.length).toBe(1);
        expect(logs).toEqual(['click2']);
      });

      it('should support remove event listeners by call zone.cancelTask directly with multiple listeners with mixed capture', function () {
        let logs: string[] = [];
        let eventTask: Task;
        const zone = rootZone.fork({
          name: 'spy',
          onScheduleTask: (
            parentZoneDelegate: ZoneDelegate,
            currentZone: Zone,
            targetZone: Zone,
            task: Task,
          ): any => {
            eventTask = task;
            return parentZoneDelegate.scheduleTask(targetZone, task);
          },
        });

        zone.run(() => {
          button.addEventListener(
            'click',
            function () {
              logs.push('click1');
            },
            true,
          );
        });
        button.addEventListener('click', function () {
          logs.push('click2');
        });
        let listeners = button.eventListeners!('click');
        expect(listeners.length).toBe(2);

        button.dispatchEvent(clickEvent);
        expect(logs.length).toBe(2);
        expect(logs).toEqual(['click1', 'click2']);
        eventTask!.zone.cancelTask(eventTask!);
        logs = [];

        listeners = button.eventListeners!('click');
        button.dispatchEvent(clickEvent);
        expect(logs.length).toBe(1);
        expect(listeners.length).toBe(1);
        expect(logs).toEqual(['click2']);
      });

      it('should support remove event listeners via AbortController', function () {
        let logs: string[] = [];
        const ac = new AbortController();

        button.addEventListener(
          'click',
          function () {
            logs.push('click1');
          },
          {signal: ac.signal},
        );
        button.addEventListener('click', function () {
          logs.push('click2');
        });
        button.addEventListener(
          'click',
          function () {
            logs.push('click3');
          },
          {signal: ac.signal},
        );
        let listeners = button.eventListeners!('click');
        expect(listeners.length).toBe(3);

        button.dispatchEvent(clickEvent);
        expect(logs.length).toBe(3);
        expect(logs).toEqual(['click1', 'click2', 'click3']);
        ac.abort();
        logs = [];

        listeners = button.eventListeners!('click');
        button.dispatchEvent(clickEvent);
        expect(logs.length).toBe(1);
        expect(listeners.length).toBe(1);
        expect(logs).toEqual(['click2']);
      });

      it('should support remove event listeners with AbortController', function () {
        let logs: string[] = [];
        const ac = new AbortController();

        const listener1 = function () {
          logs.push('click1');
        };
        button.addEventListener('click', listener1, {signal: ac.signal});
        button.addEventListener('click', function () {
          logs.push('click2');
        });
        let listeners = button.eventListeners!('click');
        expect(listeners.length).toBe(2);

        button.dispatchEvent(clickEvent);
        expect(logs.length).toBe(2);
        expect(logs).toEqual(['click1', 'click2']);

        ac.abort();
        listeners = button.eventListeners!('click');
        expect(listeners.length).toBe(1);

        logs = [];

        listeners = button.eventListeners!('click');
        button.dispatchEvent(clickEvent);
        expect(logs.length).toBe(1);
        expect(listeners.length).toBe(1);
        expect(logs).toEqual(['click2']);
      });

      it('should not break remove event listeners with AbortController', function () {
        let logs: string[] = [];
        const ac = new AbortController();

        const listener1 = function () {
          logs.push('click1');
        };
        button.addEventListener('click', listener1, {signal: ac.signal});
        button.addEventListener('click', function () {
          logs.push('click2');
        });
        let listeners = button.eventListeners!('click');
        expect(listeners.length).toBe(2);

        button.dispatchEvent(clickEvent);
        expect(logs.length).toBe(2);
        expect(logs).toEqual(['click1', 'click2']);

        button.removeEventListener('click', listener1);
        listeners = button.eventListeners!('click');
        expect(listeners.length).toBe(1);

        logs = [];

        listeners = button.eventListeners!('click');
        button.dispatchEvent(clickEvent);
        expect(logs.length).toBe(1);
        expect(listeners.length).toBe(1);
        expect(logs).toEqual(['click2']);
      });

      it('should support remove multiple event listeners with the same AbortController', function () {
        let logs: string[] = [];
        const ac = new AbortController();
        const button1 = document.createElement('button');
        const keyEvent: any = document.createEvent('KeyboardEvent');
        keyEvent.initKeyboardEvent(
          'keypress', // typeArg,
          true, // canBubbleArg,
          true, // cancelableArg,
          null, // viewArg, Specifies UIEvent.view. This value may be null.
          '',
          0,
          false, // ctrlKeyArg,
          false, // altKeyArg,
          false, // shiftKeyArg,
          false, // metaKeyArg,
        );
        document.body.appendChild(button1);

        const listener1 = function () {
          logs.push('click1');
        };
        button.addEventListener('click', listener1, {signal: ac.signal});
        button.addEventListener('click', function () {
          logs.push('click2');
        });
        button1.addEventListener('keypress', () => logs.push('click3'), {signal: ac.signal});
        button1.addEventListener('keypress', function () {
          logs.push('click4');
        });
        let listeners = button.eventListeners!('click');
        expect(listeners.length).toBe(2);

        button.dispatchEvent(clickEvent);
        expect(logs.length).toBe(2);
        expect(logs).toEqual(['click1', 'click2']);
        button1.dispatchEvent(keyEvent);
        expect(logs.length).toBe(4);
        expect(logs).toEqual(['click1', 'click2', 'click3', 'click4']);

        logs = [];
        ac.abort();
        listeners = button.eventListeners!('click');
        button.dispatchEvent(clickEvent);
        expect(logs.length).toBe(1);
        expect(listeners.length).toBe(1);
        expect(logs).toEqual(['click2']);
        button1.dispatchEvent(keyEvent);
        expect(logs.length).toBe(2);
        expect(logs).toEqual(['click2', 'click4']);
      });

      // https://github.com/angular/angular/issues/54831
      // https://github.com/angular/angular/issues/54142
      it('should support passing `AbortController` directly to `addEventListener`', function () {
        let logs: string[] = [];
        const ac = new AbortController();

        button.addEventListener(
          'click',
          function () {
            logs.push('click1');
          },
          ac,
        );
        button.addEventListener('click', function () {
          logs.push('click2');
        });
        button.addEventListener(
          'click',
          function () {
            logs.push('click3');
          },
          ac,
        );
        let listeners = button.eventListeners!('click');
        expect(listeners.length).toBe(3);

        button.dispatchEvent(clickEvent);
        expect(logs.length).toBe(3);
        expect(logs).toEqual(['click1', 'click2', 'click3']);
        ac.abort();
        logs = [];

        listeners = button.eventListeners!('click');
        button.dispatchEvent(clickEvent);
        expect(logs.length).toBe(1);
        expect(listeners.length).toBe(1);
        expect(logs).toEqual(['click2']);
      });

      // https://github.com/angular/angular/issues/56148
      it('should store the remove abort listener on the task itself and not the task data', function () {
        const logs: string[] = [];
        const ac = new AbortController();
        const onLoad = jasmine.createSpy();

        // Note that we have to manually spy on the `removeEventListener`
        // because zone.js calls the native `addEventListener` for an
        // `AbortSignal`, which isn't patched by zone.js. Thus, calling
        // `signal.eventListeners('abort')` would always return an empty list.
        const removeEventListenerSpy = spyOn(
          AbortSignal.prototype,
          'removeEventListener',
        ).and.callThrough();

        window.addEventListener('load', onLoad, {once: true});

        button.addEventListener(
          'click',
          function () {
            logs.push('click');
          },
          {signal: ac.signal},
        );

        // In this scenario, we must dispatch the load event because
        // `{once:true}` options are provided. This action will remove
        // the event listener on the `window` after the task is executed,
        // effectively cancelling it. Previously, we encountered a regression
        // where `removeAbortListener` was stored on the task data instead of
        // the task itself. Consequently, it would remove the `abort` listener
        // from the signal. As a result, calling `abort()` on the controller wouldn't
        // cancel any events because there was no `abort` listener present.
        window.dispatchEvent(new Event('load'));
        expect(onLoad).toHaveBeenCalled();

        // Assert that the `abort` event listener has NOT been removed.
        expect(removeEventListenerSpy).toHaveBeenCalledTimes(0);

        // When calling abort, it should now remove the event listener
        // from the button, thus dispatching events should not trigger the listener.
        ac.abort();

        button.dispatchEvent(clickEvent);

        const listeners = button.eventListeners!('click');
        expect(listeners.length).toBe(0);
        expect(logs).toEqual([]);
      });

      it('should not add event listeners with aborted signal', function () {
        let logs: string[] = [];

        button.addEventListener(
          'click',
          function () {
            logs.push('click1');
          },
          {signal: AbortSignal.abort()},
        );
        button.addEventListener('click', function () {
          logs.push('click2');
        });
        let listeners = button.eventListeners!('click');
        expect(listeners.length).toBe(1);

        button.dispatchEvent(clickEvent);
        expect(logs.length).toBe(1);
        expect(logs).toEqual(['click2']);
      });

      it(
        'should remove event listeners with timeout signal',
        ifEnvSupportsWithDone(
          () => typeof AbortSignal.timeout === 'function',
          function (done: DoneFn) {
            let logs: string[] = [];

            button.addEventListener(
              'click',
              function () {
                logs.push('click1');
              },
              {signal: AbortSignal.timeout(1)},
            );
            button.addEventListener('click', function () {
              logs.push('click2');
            });
            let listeners = button.eventListeners!('click');
            expect(listeners.length).toBe(2);

            button.dispatchEvent(clickEvent);
            expect(logs.length).toBe(2);
            expect(logs).toEqual(['click1', 'click2']);

            setTimeout(() => {
              logs = [];
              button.dispatchEvent(clickEvent);
              expect(logs.length).toBe(1);
              expect(logs).toEqual(['click2']);
              done();
            }, 10);
          },
        ),
      );

      it(
        'should support reschedule eventTask',
        ifEnvSupports(supportEventListenerOptions, function () {
          let hookSpy1 = jasmine.createSpy('spy1');
          let hookSpy2 = jasmine.createSpy('spy2');
          let hookSpy3 = jasmine.createSpy('spy3');
          let logs: string[] = [];
          const isUnpatchedEvent = function (source: string) {
            return source.lastIndexOf('click') !== -1;
          };
          const zone1 = Zone.current.fork({
            name: 'zone1',
            onScheduleTask: (
              parentZoneDelegate: ZoneDelegate,
              currentZone: Zone,
              targetZone: Zone,
              task: Task,
            ): any => {
              if (
                (task.type === 'eventTask' || task.type === 'macroTask') &&
                isUnpatchedEvent(task.source)
              ) {
                task.cancelScheduleRequest();

                return zone2.scheduleTask(task);
              } else {
                return parentZoneDelegate.scheduleTask(targetZone, task);
              }
            },
            onInvokeTask(
              parentZoneDelegate: ZoneDelegate,
              currentZone: Zone,
              targetZone: Zone,
              task: Task,
              applyThis: any,
              applyArgs: any,
            ) {
              hookSpy1();
              return parentZoneDelegate.invokeTask(targetZone, task, applyThis, applyArgs);
            },
          });
          const zone2 = Zone.current.fork({
            name: 'zone2',
            onScheduleTask: (
              parentZoneDelegate: ZoneDelegate,
              currentZone: Zone,
              targetZone: Zone,
              task: Task,
            ): any => {
              hookSpy2();
              return parentZoneDelegate.scheduleTask(targetZone, task);
            },
            onInvokeTask(
              parentZoneDelegate: ZoneDelegate,
              currentZone: Zone,
              targetZone: Zone,
              task: Task,
              applyThis: any,
              applyArgs: any,
            ) {
              hookSpy3();
              return parentZoneDelegate.invokeTask(targetZone, task, applyThis, applyArgs);
            },
          });

          const listener = function () {
            logs.push(Zone.current.name);
          };
          zone1.run(() => {
            button.addEventListener('click', listener);
            button.addEventListener('mouseover', listener);
          });

          const clickEvent = document.createEvent('Event');
          clickEvent.initEvent('click', true, true);
          const mouseEvent = document.createEvent('Event');
          mouseEvent.initEvent('mouseover', true, true);

          button.dispatchEvent(clickEvent);
          button.removeEventListener('click', listener);

          expect(logs).toEqual(['zone2']);
          expect(hookSpy1).not.toHaveBeenCalled();
          expect(hookSpy2).toHaveBeenCalled();
          expect(hookSpy3).toHaveBeenCalled();
          logs = [];
          hookSpy2 = jasmine.createSpy('hookSpy2');
          hookSpy3 = jasmine.createSpy('hookSpy3');

          button.dispatchEvent(mouseEvent);
          button.removeEventListener('mouseover', listener);
          expect(logs).toEqual(['zone1']);
          expect(hookSpy1).toHaveBeenCalled();
          expect(hookSpy2).not.toHaveBeenCalled();
          expect(hookSpy3).not.toHaveBeenCalled();
        }),
      );

      it('should support inline event handler attributes', function () {
        const hookSpy = jasmine.createSpy('hook');
        const zone = rootZone.fork({
          name: 'spy',
          onScheduleTask: (
            parentZoneDelegate: ZoneDelegate,
            currentZone: Zone,
            targetZone: Zone,
            task: Task,
          ): any => {
            hookSpy();
            return parentZoneDelegate.scheduleTask(targetZone, task);
          },
        });

        zone.run(function () {
          button.setAttribute('onclick', 'return');
          expect(button.onclick).not.toBe(null);
        });
      });

      it('should not remove onEventListener when removing capture listener', function () {
        const button = document.createElement('button');
        document.body.append(button);
        const createEvt = () => {
          const evt = document.createEvent('Event');
          evt.initEvent('click', true, true);
          return evt;
        };
        let logs: string[] = [];
        const onClickHandler = () => logs.push('onclick');
        button.onclick = onClickHandler;
        let evt = createEvt();
        button.dispatchEvent(evt);
        expect(logs).toEqual(['onclick']);
        logs = [];
        const listener = () => logs.push('click listener');
        button.addEventListener('click', listener, {capture: true});
        evt = createEvt();
        button.dispatchEvent(evt);
        expect(logs.sort()).toEqual(['onclick', 'click listener'].sort());
        logs = [];
        button.removeEventListener('click', listener, true);
        evt = createEvt();
        button.dispatchEvent(evt);
        expect(logs).toEqual(['onclick']);
        expect(button.onclick).toBe(onClickHandler);
        button.onclick = null;
        logs = [];
        evt = createEvt();
        button.dispatchEvent(evt);
        expect(logs).toEqual([]);
        expect(button.onclick).toBe(null);
        document.body.removeChild(button);
      });

      describe('should be able to remove eventListener during eventListener callback', function () {
        it('should be able to remove eventListener during eventListener callback', function () {
          let logs: string[] = [];
          const listener1 = function () {
            button.removeEventListener('click', listener1);
            logs.push('listener1');
          };
          const listener2 = function () {
            logs.push('listener2');
          };
          const listener3 = {
            handleEvent: function (event: Event) {
              logs.push('listener3');
            },
          };

          button.addEventListener('click', listener1);
          button.addEventListener('click', listener2);
          button.addEventListener('click', listener3);

          button.dispatchEvent(clickEvent);
          expect(logs.length).toBe(3);
          expect(logs).toEqual(['listener1', 'listener2', 'listener3']);

          logs = [];
          button.dispatchEvent(clickEvent);
          expect(logs.length).toBe(2);
          expect(logs).toEqual(['listener2', 'listener3']);

          button.removeEventListener('click', listener2);
          button.removeEventListener('click', listener3);
        });

        it('should be able to remove eventListener during eventListener callback with capture=true', function () {
          let logs: string[] = [];
          const listener1 = function () {
            button.removeEventListener('click', listener1, true);
            logs.push('listener1');
          };
          const listener2 = function () {
            logs.push('listener2');
          };
          const listener3 = {
            handleEvent: function (event: Event) {
              logs.push('listener3');
            },
          };

          button.addEventListener('click', listener1, true);
          button.addEventListener('click', listener2, true);
          button.addEventListener('click', listener3, true);

          button.dispatchEvent(clickEvent);
          expect(logs.length).toBe(3);
          expect(logs).toEqual(['listener1', 'listener2', 'listener3']);

          logs = [];
          button.dispatchEvent(clickEvent);
          expect(logs.length).toBe(2);
          expect(logs).toEqual(['listener2', 'listener3']);

          button.removeEventListener('click', listener2, true);
          button.removeEventListener('click', listener3, true);
        });

        it('should be able to remove handleEvent eventListener during eventListener callback', function () {
          let logs: string[] = [];
          const listener1 = function () {
            logs.push('listener1');
          };
          const listener2 = function () {
            logs.push('listener2');
          };
          const listener3 = {
            handleEvent: function (event: Event) {
              logs.push('listener3');
              button.removeEventListener('click', listener3);
            },
          };

          button.addEventListener('click', listener1);
          button.addEventListener('click', listener2);
          button.addEventListener('click', listener3);

          button.dispatchEvent(clickEvent);
          expect(logs.length).toBe(3);
          expect(logs).toEqual(['listener1', 'listener2', 'listener3']);

          logs = [];
          button.dispatchEvent(clickEvent);
          expect(logs.length).toBe(2);
          expect(logs).toEqual(['listener1', 'listener2']);

          button.removeEventListener('click', listener1);
          button.removeEventListener('click', listener2);
        });

        it('should be able to remove handleEvent eventListener during eventListener callback with capture=true', function () {
          let logs: string[] = [];
          const listener1 = function () {
            logs.push('listener1');
          };
          const listener2 = function () {
            logs.push('listener2');
          };
          const listener3 = {
            handleEvent: function (event: Event) {
              logs.push('listener3');
              button.removeEventListener('click', listener3, true);
            },
          };

          button.addEventListener('click', listener1, true);
          button.addEventListener('click', listener2, true);
          button.addEventListener('click', listener3, true);

          button.dispatchEvent(clickEvent);
          expect(logs.length).toBe(3);
          expect(logs).toEqual(['listener1', 'listener2', 'listener3']);

          logs = [];
          button.dispatchEvent(clickEvent);
          expect(logs.length).toBe(2);
          expect(logs).toEqual(['listener1', 'listener2']);

          button.removeEventListener('click', listener1, true);
          button.removeEventListener('click', listener2, true);
        });

        it('should be able to remove multiple eventListeners during eventListener callback', function () {
          let logs: string[] = [];
          const listener1 = function () {
            logs.push('listener1');
            button.removeEventListener('click', listener2);
            button.removeEventListener('click', listener3);
          };
          const listener2 = function () {
            logs.push('listener2');
          };
          const listener3 = {
            handleEvent: function (event: Event) {
              logs.push('listener3');
            },
          };

          button.addEventListener('click', listener1);
          button.addEventListener('click', listener2);
          button.addEventListener('click', listener3);

          button.dispatchEvent(clickEvent);
          expect(logs.length).toBe(1);
          expect(logs).toEqual(['listener1']);

          button.removeEventListener('click', listener1);
        });

        it('should be able to remove multiple eventListeners during eventListener callback with capture=true', function () {
          let logs: string[] = [];
          const listener1 = function () {
            logs.push('listener1');
            button.removeEventListener('click', listener2, true);
            button.removeEventListener('click', listener3, true);
          };
          const listener2 = function () {
            logs.push('listener2');
          };
          const listener3 = {
            handleEvent: function (event: Event) {
              logs.push('listener3');
            },
          };

          button.addEventListener('click', listener1, true);
          button.addEventListener('click', listener2, true);
          button.addEventListener('click', listener3, true);

          button.dispatchEvent(clickEvent);
          expect(logs.length).toBe(1);
          expect(logs).toEqual(['listener1']);

          button.removeEventListener('click', listener1, true);
        });

        it('should be able to remove part of other eventListener during eventListener callback', function () {
          let logs: string[] = [];
          const listener1 = function () {
            logs.push('listener1');
            button.removeEventListener('click', listener2);
          };
          const listener2 = function () {
            logs.push('listener2');
          };
          const listener3 = {
            handleEvent: function (event: Event) {
              logs.push('listener3');
            },
          };

          button.addEventListener('click', listener1);
          button.addEventListener('click', listener2);
          button.addEventListener('click', listener3);

          button.dispatchEvent(clickEvent);
          expect(logs.length).toBe(2);
          expect(logs).toEqual(['listener1', 'listener3']);

          button.removeEventListener('click', listener1);
          button.removeEventListener('click', listener3);
        });

        it('should be able to remove part of other eventListener during eventListener callback with capture=true', function () {
          let logs: string[] = [];
          const listener1 = function () {
            logs.push('listener1');
            button.removeEventListener('click', listener2, true);
          };
          const listener2 = function () {
            logs.push('listener2');
          };
          const listener3 = {
            handleEvent: function (event: Event) {
              logs.push('listener3');
            },
          };

          button.addEventListener('click', listener1, true);
          button.addEventListener('click', listener2, true);
          button.addEventListener('click', listener3, true);

          button.dispatchEvent(clickEvent);
          expect(logs.length).toBe(2);
          expect(logs).toEqual(['listener1', 'listener3']);

          button.removeEventListener('click', listener1, true);
          button.removeEventListener('click', listener3, true);
        });

        it('should be able to remove all beforeward and afterward eventListener during eventListener callback', function () {
          let logs: string[] = [];
          const listener1 = function () {
            logs.push('listener1');
          };
          const listener2 = function () {
            logs.push('listener2');
            button.removeEventListener('click', listener1);
            button.removeEventListener('click', listener3);
          };
          const listener3 = {
            handleEvent: function (event: Event) {
              logs.push('listener3');
            },
          };

          button.addEventListener('click', listener1);
          button.addEventListener('click', listener2);
          button.addEventListener('click', listener3);

          button.dispatchEvent(clickEvent);
          expect(logs.length).toBe(2);
          expect(logs).toEqual(['listener1', 'listener2']);

          logs = [];
          button.dispatchEvent(clickEvent);
          expect(logs.length).toBe(1);
          expect(logs).toEqual(['listener2']);

          button.removeEventListener('click', listener2);
        });

        it('should be able to remove all beforeward and afterward eventListener during eventListener callback with capture=true', function () {
          let logs: string[] = [];
          const listener1 = function () {
            logs.push('listener1');
          };
          const listener2 = function () {
            logs.push('listener2');
            button.removeEventListener('click', listener1, true);
            button.removeEventListener('click', listener3, true);
          };
          const listener3 = {
            handleEvent: function (event: Event) {
              logs.push('listener3');
            },
          };

          button.addEventListener('click', listener1, true);
          button.addEventListener('click', listener2, true);
          button.addEventListener('click', listener3, true);

          button.dispatchEvent(clickEvent);
          expect(logs.length).toBe(2);
          expect(logs).toEqual(['listener1', 'listener2']);

          logs = [];
          button.dispatchEvent(clickEvent);
          expect(logs.length).toBe(1);
          expect(logs).toEqual(['listener2']);

          button.removeEventListener('click', listener2, true);
        });

        it('should be able to remove part of beforeward and afterward eventListener during eventListener callback', function () {
          let logs: string[] = [];
          const listener1 = function () {
            logs.push('listener1');
          };
          const listener2 = function () {
            logs.push('listener2');
          };
          const listener3 = {
            handleEvent: function (event: Event) {
              logs.push('listener3');
              button.removeEventListener('click', listener2);
              button.removeEventListener('click', listener4);
            },
          };
          const listener4 = function () {
            logs.push('listener4');
          };
          const listener5 = function () {
            logs.push('listener5');
          };

          button.addEventListener('click', listener1);
          button.addEventListener('click', listener2);
          button.addEventListener('click', listener3);
          button.addEventListener('click', listener4);
          button.addEventListener('click', listener5);

          button.dispatchEvent(clickEvent);
          expect(logs.length).toBe(4);
          expect(logs).toEqual(['listener1', 'listener2', 'listener3', 'listener5']);

          logs = [];
          button.dispatchEvent(clickEvent);
          expect(logs.length).toBe(3);
          expect(logs).toEqual(['listener1', 'listener3', 'listener5']);

          button.removeEventListener('click', listener1);
          button.removeEventListener('click', listener3);
          button.removeEventListener('click', listener5);
        });

        it('should be able to remove part of beforeward and afterward eventListener during eventListener callback with capture=true', function () {
          let logs: string[] = [];
          const listener1 = function () {
            logs.push('listener1');
          };
          const listener2 = function () {
            logs.push('listener2');
          };
          const listener3 = {
            handleEvent: function (event: Event) {
              logs.push('listener3');
              button.removeEventListener('click', listener2, true);
              button.removeEventListener('click', listener4, true);
            },
          };
          const listener4 = function () {
            logs.push('listener4');
          };
          const listener5 = function () {
            logs.push('listener5');
          };

          button.addEventListener('click', listener1, true);
          button.addEventListener('click', listener2, true);
          button.addEventListener('click', listener3, true);
          button.addEventListener('click', listener4, true);
          button.addEventListener('click', listener5, true);

          button.dispatchEvent(clickEvent);
          expect(logs.length).toBe(4);
          expect(logs).toEqual(['listener1', 'listener2', 'listener3', 'listener5']);

          logs = [];
          button.dispatchEvent(clickEvent);
          expect(logs.length).toBe(3);
          expect(logs).toEqual(['listener1', 'listener3', 'listener5']);

          button.removeEventListener('click', listener1, true);
          button.removeEventListener('click', listener3, true);
          button.removeEventListener('click', listener5, true);
        });

        it('should be able to remove all beforeward eventListener during eventListener callback', function () {
          let logs: string[] = [];
          const listener1 = function () {
            logs.push('listener1');
          };
          const listener2 = function () {
            logs.push('listener2');
          };
          const listener3 = {
            handleEvent: function (event: Event) {
              logs.push('listener3');
              button.removeEventListener('click', listener1);
              button.removeEventListener('click', listener2);
            },
          };

          button.addEventListener('click', listener1);
          button.addEventListener('click', listener2);
          button.addEventListener('click', listener3);

          button.dispatchEvent(clickEvent);
          expect(logs.length).toBe(3);
          expect(logs).toEqual(['listener1', 'listener2', 'listener3']);

          logs = [];
          button.dispatchEvent(clickEvent);
          expect(logs.length).toBe(1);
          expect(logs).toEqual(['listener3']);

          button.removeEventListener('click', listener3);
        });

        it('should be able to remove all beforeward eventListener during eventListener callback with capture=true', function () {
          let logs: string[] = [];
          const listener1 = function () {
            logs.push('listener1');
          };
          const listener2 = function () {
            logs.push('listener2');
          };
          const listener3 = {
            handleEvent: function (event: Event) {
              logs.push('listener3');
              button.removeEventListener('click', listener1, true);
              button.removeEventListener('click', listener2, true);
            },
          };

          button.addEventListener('click', listener1, true);
          button.addEventListener('click', listener2, true);
          button.addEventListener('click', listener3, true);

          button.dispatchEvent(clickEvent);
          expect(logs.length).toBe(3);
          expect(logs).toEqual(['listener1', 'listener2', 'listener3']);

          logs = [];
          button.dispatchEvent(clickEvent);
          expect(logs.length).toBe(1);
          expect(logs).toEqual(['listener3']);

          button.removeEventListener('click', listener3, true);
        });

        it('should be able to remove part of beforeward eventListener during eventListener callback', function () {
          let logs: string[] = [];
          const listener1 = function () {
            logs.push('listener1');
          };
          const listener2 = function () {
            logs.push('listener2');
          };
          const listener3 = {
            handleEvent: function (event: Event) {
              logs.push('listener3');
              button.removeEventListener('click', listener1);
            },
          };

          button.addEventListener('click', listener1);
          button.addEventListener('click', listener2);
          button.addEventListener('click', listener3);

          button.dispatchEvent(clickEvent);
          expect(logs.length).toBe(3);
          expect(logs).toEqual(['listener1', 'listener2', 'listener3']);

          logs = [];
          button.dispatchEvent(clickEvent);
          expect(logs.length).toBe(2);
          expect(logs).toEqual(['listener2', 'listener3']);

          button.removeEventListener('click', listener2);
          button.removeEventListener('click', listener3);
        });

        it('should be able to remove part of beforeward eventListener during eventListener callback with capture=true', function () {
          let logs: string[] = [];
          const listener1 = function () {
            logs.push('listener1');
          };
          const listener2 = function () {
            logs.push('listener2');
          };
          const listener3 = {
            handleEvent: function (event: Event) {
              logs.push('listener3');
              button.removeEventListener('click', listener1, true);
            },
          };

          button.addEventListener('click', listener1, true);
          button.addEventListener('click', listener2, true);
          button.addEventListener('click', listener3, true);

          button.dispatchEvent(clickEvent);
          expect(logs.length).toBe(3);
          expect(logs).toEqual(['listener1', 'listener2', 'listener3']);

          logs = [];
          button.dispatchEvent(clickEvent);
          expect(logs.length).toBe(2);
          expect(logs).toEqual(['listener2', 'listener3']);

          button.removeEventListener('click', listener2, true);
          button.removeEventListener('click', listener3, true);
        });

        it('should be able to remove all eventListeners during first eventListener callback', function () {
          let logs: string[] = [];
          const listener1 = function () {
            button.removeAllListeners!('click');
            logs.push('listener1');
          };
          const listener2 = function () {
            logs.push('listener2');
          };
          const listener3 = {
            handleEvent: function (event: Event) {
              logs.push('listener3');
            },
          };

          button.addEventListener('click', listener1);
          button.addEventListener('click', listener2);
          button.addEventListener('click', listener3);

          button.dispatchEvent(clickEvent);
          expect(logs.length).toBe(1);
          expect(logs).toEqual(['listener1']);

          logs = [];
          button.dispatchEvent(clickEvent);
          expect(logs.length).toBe(0);
        });

        it('should be able to remove all eventListeners during first eventListener callback with capture=true', function () {
          let logs: string[] = [];
          const listener1 = function () {
            button.removeAllListeners!('click');
            logs.push('listener1');
          };
          const listener2 = function () {
            logs.push('listener2');
          };
          const listener3 = {
            handleEvent: function (event: Event) {
              logs.push('listener3');
            },
          };

          button.addEventListener('click', listener1, true);
          button.addEventListener('click', listener2, true);
          button.addEventListener('click', listener3, true);

          button.dispatchEvent(clickEvent);
          expect(logs.length).toBe(1);
          expect(logs).toEqual(['listener1']);

          logs = [];
          button.dispatchEvent(clickEvent);
          expect(logs.length).toBe(0);
        });

        it('should be able to remove all eventListeners during middle eventListener callback', function () {
          let logs: string[] = [];
          const listener1 = function () {
            logs.push('listener1');
          };
          const listener2 = function () {
            button.removeAllListeners!('click');
            logs.push('listener2');
          };
          const listener3 = {
            handleEvent: function (event: Event) {
              logs.push('listener3');
            },
          };

          button.addEventListener('click', listener1);
          button.addEventListener('click', listener2);
          button.addEventListener('click', listener3);

          button.dispatchEvent(clickEvent);
          expect(logs.length).toBe(2);
          expect(logs).toEqual(['listener1', 'listener2']);

          logs = [];
          button.dispatchEvent(clickEvent);
          expect(logs.length).toBe(0);
        });

        it('should be able to remove all eventListeners during middle eventListener callback with capture=true', function () {
          let logs: string[] = [];
          const listener1 = function () {
            logs.push('listener1');
          };
          const listener2 = function () {
            button.removeAllListeners!('click');
            logs.push('listener2');
          };
          const listener3 = {
            handleEvent: function (event: Event) {
              logs.push('listener3');
            },
          };

          button.addEventListener('click', listener1, true);
          button.addEventListener('click', listener2, true);
          button.addEventListener('click', listener3, true);

          button.dispatchEvent(clickEvent);
          expect(logs.length).toBe(2);
          expect(logs).toEqual(['listener1', 'listener2']);

          logs = [];
          button.dispatchEvent(clickEvent);
          expect(logs.length).toBe(0);
        });

        it('should be able to remove all eventListeners during last eventListener callback', function () {
          let logs: string[] = [];
          const listener1 = function () {
            logs.push('listener1');
          };
          const listener2 = function () {
            logs.push('listener2');
          };
          const listener3 = {
            handleEvent: function (event: Event) {
              logs.push('listener3');
              button.removeAllListeners!('click');
            },
          };

          button.addEventListener('click', listener1);
          button.addEventListener('click', listener2);
          button.addEventListener('click', listener3);

          button.dispatchEvent(clickEvent);
          expect(logs.length).toBe(3);
          expect(logs).toEqual(['listener1', 'listener2', 'listener3']);

          logs = [];
          button.dispatchEvent(clickEvent);
          expect(logs.length).toBe(0);
        });

        it('should be able to remove all eventListeners during last eventListener callback with capture=true', function () {
          let logs: string[] = [];
          const listener1 = function () {
            logs.push('listener1');
          };
          const listener2 = function () {
            logs.push('listener2');
          };
          const listener3 = {
            handleEvent: function (event: Event) {
              logs.push('listener3');
              button.removeAllListeners!('click');
            },
          };

          button.addEventListener('click', listener1, true);
          button.addEventListener('click', listener2, true);
          button.addEventListener('click', listener3, true);

          button.dispatchEvent(clickEvent);
          expect(logs.length).toBe(3);
          expect(logs).toEqual(['listener1', 'listener2', 'listener3']);

          logs = [];
          button.dispatchEvent(clickEvent);
          expect(logs.length).toBe(0);
        });
      });

      it('should be able to get eventListeners of specified event form EventTarget', function () {
        const listener1 = function () {};
        const listener2 = function () {};
        const listener3 = {handleEvent: function (event: Event) {}};
        const listener4 = function () {};

        button.addEventListener('click', listener1);
        button.addEventListener('click', listener2);
        button.addEventListener('click', listener3);
        button.addEventListener('mouseover', listener4);

        const listeners = button.eventListeners!('click');
        expect(listeners.length).toBe(3);
        expect(listeners).toEqual([listener1, listener2, listener3]);
        button.removeEventListener('click', listener1);
        button.removeEventListener('click', listener2);
        button.removeEventListener('click', listener3);
      });

      it('should be able to get all eventListeners form EventTarget without eventName', function () {
        const listener1 = function () {};
        const listener2 = function () {};
        const listener3 = {handleEvent: function (event: Event) {}};

        button.addEventListener('click', listener1);
        button.addEventListener('mouseover', listener2);
        button.addEventListener('mousehover', listener3);

        const listeners = button.eventListeners!();
        expect(listeners.length).toBe(3);
        expect(listeners).toEqual([listener1, listener2, listener3]);
        button.removeEventListener('click', listener1);
        button.removeEventListener('mouseover', listener2);
        button.removeEventListener('mousehover', listener3);
      });

      it('should be able to remove all listeners of specified event form EventTarget', function () {
        let logs: string[] = [];
        const listener1 = function () {
          logs.push('listener1');
        };
        const listener2 = function () {
          logs.push('listener2');
        };
        const listener3 = {
          handleEvent: function (event: Event) {
            logs.push('listener3');
          },
        };
        const listener4 = function () {
          logs.push('listener4');
        };
        const listener5 = function () {
          logs.push('listener5');
        };

        button.addEventListener('mouseover', listener1);
        button.addEventListener('mouseover', listener2);
        button.addEventListener('mouseover', listener3);
        button.addEventListener('click', listener4);
        button.onmouseover = listener5;
        expect((button as any)[Zone.__symbol__('ON_PROPERTYmouseover')]).toEqual(listener5);

        button.removeAllListeners!('mouseover');
        const listeners = button.eventListeners!('mouseover');
        expect(listeners.length).toBe(0);
        expect((button as any)[Zone.__symbol__('ON_PROPERTYmouseover')]).toBeNull();
        expect(!!button.onmouseover).toBeFalsy();

        const mouseEvent = document.createEvent('Event');
        mouseEvent.initEvent('mouseover', true, true);

        button.dispatchEvent(mouseEvent);
        expect(logs).toEqual([]);

        button.dispatchEvent(clickEvent);
        expect(logs).toEqual(['listener4']);

        button.removeEventListener('click', listener4);
      });

      it('should be able to remove all listeners of specified event form EventTarget with capture=true', function () {
        let logs: string[] = [];
        const listener1 = function () {
          logs.push('listener1');
        };
        const listener2 = function () {
          logs.push('listener2');
        };
        const listener3 = {
          handleEvent: function (event: Event) {
            logs.push('listener3');
          },
        };
        const listener4 = function () {
          logs.push('listener4');
        };

        button.addEventListener('mouseover', listener1, true);
        button.addEventListener('mouseover', listener2, true);
        button.addEventListener('mouseover', listener3, true);
        button.addEventListener('click', listener4, true);

        button.removeAllListeners!('mouseover');
        const listeners = button.eventListeners!('mouseover');
        expect(listeners.length).toBe(0);

        const mouseEvent = document.createEvent('Event');
        mouseEvent.initEvent('mouseover', true, true);

        button.dispatchEvent(mouseEvent);
        expect(logs).toEqual([]);

        button.dispatchEvent(clickEvent);
        expect(logs).toEqual(['listener4']);

        button.removeEventListener('click', listener4);
      });

      it('should be able to remove all listeners of specified event form EventTarget with mixed capture', function () {
        let logs: string[] = [];
        const listener1 = function () {
          logs.push('listener1');
        };
        const listener2 = function () {
          logs.push('listener2');
        };
        const listener3 = {
          handleEvent: function (event: Event) {
            logs.push('listener3');
          },
        };
        const listener4 = function () {
          logs.push('listener4');
        };

        button.addEventListener('mouseover', listener1, true);
        button.addEventListener('mouseover', listener2, false);
        button.addEventListener('mouseover', listener3, true);
        button.addEventListener('click', listener4, true);

        button.removeAllListeners!('mouseover');
        const listeners = button.eventListeners!('mouseover');
        expect(listeners.length).toBe(0);

        const mouseEvent = document.createEvent('Event');
        mouseEvent.initEvent('mouseover', true, true);

        button.dispatchEvent(mouseEvent);
        expect(logs).toEqual([]);

        button.dispatchEvent(clickEvent);
        expect(logs).toEqual(['listener4']);

        button.removeEventListener('click', listener4);
      });

      it('should be able to remove all listeners of all events form EventTarget', function () {
        let logs: string[] = [];
        const listener1 = function () {
          logs.push('listener1');
        };
        const listener2 = function () {
          logs.push('listener2');
        };
        const listener3 = {
          handleEvent: function (event: Event) {
            logs.push('listener3');
          },
        };
        const listener4 = function () {
          logs.push('listener4');
        };
        const listener5 = function () {
          logs.push('listener5');
        };

        button.addEventListener('mouseover', listener1);
        button.addEventListener('mouseover', listener2);
        button.addEventListener('mouseover', listener3);
        button.addEventListener('click', listener4);
        button.onmouseover = listener5;
        expect((button as any)[Zone.__symbol__('ON_PROPERTYmouseover')]).toEqual(listener5);

        button.removeAllListeners!();
        const listeners = button.eventListeners!('mouseover');
        expect(listeners.length).toBe(0);
        expect((button as any)[Zone.__symbol__('ON_PROPERTYmouseover')]).toBeNull();
        expect(!!button.onmouseover).toBeFalsy();

        const mouseEvent = document.createEvent('Event');
        mouseEvent.initEvent('mouseover', true, true);

        button.dispatchEvent(mouseEvent);
        expect(logs).toEqual([]);

        button.dispatchEvent(clickEvent);
        expect(logs).toEqual([]);
      });

      it('should be able to remove listener which was added outside of zone ', function () {
        let logs: string[] = [];
        const listener1 = function () {
          logs.push('listener1');
        };
        const listener2 = function () {
          logs.push('listener2');
        };
        const listener3 = {
          handleEvent: function (event: Event) {
            logs.push('listener3');
          },
        };
        const listener4 = function () {
          logs.push('listener4');
        };

        button.addEventListener('mouseover', listener1);
        (button as any)[Zone.__symbol__('addEventListener')]('mouseover', listener2);
        button.addEventListener('click', listener3);
        (button as any)[Zone.__symbol__('addEventListener')]('click', listener4);

        button.removeEventListener('mouseover', listener1);
        button.removeEventListener('mouseover', listener2);
        button.removeEventListener('click', listener3);
        button.removeEventListener('click', listener4);
        const listeners = button.eventListeners!('mouseover');
        expect(listeners.length).toBe(0);

        const mouseEvent = document.createEvent('Event');
        mouseEvent.initEvent('mouseover', true, true);

        button.dispatchEvent(mouseEvent);
        expect(logs).toEqual([]);

        button.dispatchEvent(clickEvent);
        expect(logs).toEqual([]);
      });

      it('should be able to remove all listeners which were added inside of zone ', function () {
        let logs: string[] = [];
        const listener1 = function () {
          logs.push('listener1');
        };
        const listener2 = function () {
          logs.push('listener2');
        };
        const listener3 = {
          handleEvent: function (event: Event) {
            logs.push('listener3');
          },
        };
        const listener4 = function () {
          logs.push('listener4');
        };

        button.addEventListener('mouseover', listener1);
        (button as any)[Zone.__symbol__('addEventListener')]('mouseover', listener2);
        button.addEventListener('click', listener3);
        (button as any)[Zone.__symbol__('addEventListener')]('click', listener4);

        button.removeAllListeners!();
        const listeners = button.eventListeners!('mouseover');
        expect(listeners.length).toBe(0);

        const mouseEvent = document.createEvent('Event');
        mouseEvent.initEvent('mouseover', true, true);

        button.dispatchEvent(mouseEvent);
        expect(logs).toEqual(['listener2']);

        button.dispatchEvent(clickEvent);
        expect(logs).toEqual(['listener2', 'listener4']);
      });

      xit('should re-throw the error when the only listener throw error', function (done: DoneFn) {
        // override global.onerror to prevent jasmine report error
        let oriWindowOnError = window.onerror;
        let logs: string[] = [];
        window.onerror = function (err: any) {
          logs.push(err);
        };
        try {
          const listener1 = function () {
            throw new Error('test1');
          };
          button.addEventListener('click', listener1);

          const mouseEvent = document.createEvent('MouseEvent');
          mouseEvent.initEvent('click', true, true);

          const unhandledRejection = (e: PromiseRejectionEvent) => {
            fail('should not be here');
          };
          window.addEventListener('unhandledrejection', unhandledRejection);

          button.dispatchEvent(mouseEvent);
          expect(logs).toEqual(['Uncaught Error: test1']);

          setTimeout(() => {
            expect(logs).toEqual(['Uncaught Error: test1']);
            window.removeEventListener('unhandledrejection', unhandledRejection);
            window.onerror = oriWindowOnError;
            done();
          });
        } catch (e: any) {
          window.onerror = oriWindowOnError;
        }
      });

      xit('should not re-throw the error when zone onHandleError handled the error and the only listener throw error', function (done: DoneFn) {
        // override global.onerror to prevent jasmine report error
        let oriWindowOnError = window.onerror;
        window.onerror = function () {};
        try {
          let logs: string[] = [];
          const listener1 = function () {
            throw new Error('test1');
          };
          const zone = Zone.current.fork({
            name: 'error',
            onHandleError: (delegate, curr, target, error) => {
              logs.push('zone handled ' + target.name + ' ' + error.message);
              return false;
            },
          });

          zone.runGuarded(() => {
            button.addEventListener('click', listener1);
          });

          const mouseEvent = document.createEvent('MouseEvent');
          mouseEvent.initEvent('click', true, true);

          const unhandledRejection = (e: PromiseRejectionEvent) => {
            logs.push(e.reason.message);
          };
          window.addEventListener('unhandledrejection', unhandledRejection);

          button.dispatchEvent(mouseEvent);
          expect(logs).toEqual(['zone handled error test1']);

          setTimeout(() => {
            expect(logs).toEqual(['zone handled error test1']);
            window.removeEventListener('unhandledrejection', unhandledRejection);
            window.onerror = oriWindowOnError;
            done();
          });
        } catch (e: any) {
          window.onerror = oriWindowOnError;
        }
      });

      xit('should be able to continue to invoke remaining listeners even some listener throw error', async function () {
        await jasmine.spyOnGlobalErrorsAsync(async (globalErrorSpy) => {
          let logs: string[] = [];
          const listener1 = function () {
            logs.push('listener1');
          };
          const listener2 = function () {
            throw new Error('test1');
          };
          const listener3 = function () {
            throw new Error('test2');
          };
          const listener4 = {
            handleEvent: function () {
              logs.push('listener2');
            },
          };

          button.addEventListener('click', listener1);
          button.addEventListener('click', listener2);
          button.addEventListener('click', listener3);
          button.addEventListener('click', listener4);

          const mouseEvent = document.createEvent('MouseEvent');
          mouseEvent.initEvent('click', true, true);

          button.dispatchEvent(mouseEvent);
          expect(logs).toEqual(['listener1', 'listener2']);
        });
      });

      xit('should be able to continue to invoke remaining listeners even some listener throw error with onHandleError zone', function (done: DoneFn) {
        // override global.onerror to prevent jasmine report error
        try {
          const zone = Zone.current.fork({
            name: 'error',
            onHandleError: (delegate, curr, target, error) => {
              logs.push('zone handled ' + target.name + ' ' + error.message);
              return false;
            },
          });
          let logs: string[] = [];
          const listener1 = function () {
            logs.push('listener1');
          };
          const listener2 = function () {
            throw new Error('test1');
          };
          const listener3 = function () {
            throw new Error('test2');
          };
          const listener4 = {
            handleEvent: function () {
              logs.push('listener2');
            },
          };

          zone.runGuarded(() => {
            button.addEventListener('click', listener1);
            button.addEventListener('click', listener2);
            button.addEventListener('click', listener3);
            button.addEventListener('click', listener4);
          });

          const mouseEvent = document.createEvent('MouseEvent');
          mouseEvent.initEvent('click', true, true);

          const unhandledRejection = (e: PromiseRejectionEvent) => {
            fail('should not be here');
          };
          window.addEventListener('unhandledrejection', unhandledRejection);

          button.dispatchEvent(mouseEvent);
          expect(logs).toEqual([
            'listener1',
            'zone handled error test1',
            'zone handled error test2',
            'listener2',
          ]);

          setTimeout(() => {
            expect(logs).toEqual([
              'listener1',
              'zone handled error test1',
              'zone handled error test2',
              'listener2',
            ]);
            window.removeEventListener('unhandledrejection', unhandledRejection);
            done();
          });
        } catch (e: any) {}
      });

      xit('should be able to continue to invoke remaining listeners even some listener throw error in the different zones', function (done: DoneFn) {
        let logs: string[] = [];
        try {
          const zone1 = Zone.current.fork({
            name: 'zone1',
            onHandleError: (delegate, curr, target, error) => {
              logs.push(error.message);
              return false;
            },
          });
          const listener1 = function () {
            logs.push('listener1');
          };
          const listener2 = function () {
            throw new Error('test1');
          };
          const listener3 = function () {
            throw new Error('test2');
          };
          const listener4 = {
            handleEvent: function () {
              logs.push('listener2');
            },
          };

          button.addEventListener('click', listener1);
          zone1.run(() => {
            button.addEventListener('click', listener2);
          });
          button.addEventListener('click', listener3);
          button.addEventListener('click', listener4);

          const mouseEvent = document.createEvent('MouseEvent');
          mouseEvent.initEvent('click', true, true);

          const unhandledRejection = (e: PromiseRejectionEvent) => {
            fail('should not be here');
          };
          window.addEventListener('unhandledrejection', unhandledRejection);

          button.dispatchEvent(mouseEvent);
          expect(logs).toEqual(['listener1', 'listener2']);
        } catch (e: any) {}
      });
    });

    // TODO: Re-enable via https://github.com/angular/angular/pull/41526
    xdescribe('unhandled promise rejection', () => {
      const AsyncTestZoneSpec = (Zone as any)['AsyncTestZoneSpec'];
      const asyncTest = function (testFn: Function) {
        return (done: Function) => {
          let asyncTestZone: Zone = Zone.current.fork(
            new AsyncTestZoneSpec(
              done,
              (error: Error) => {
                fail(error);
              },
              'asyncTest',
            ),
          );
          asyncTestZone.run(testFn);
        };
      };

      it(
        'should support window.addEventListener(unhandledrejection)',
        asyncTest(() => {
          if (!promiseUnhandleRejectionSupport()) {
            return;
          }
          (Zone as any)[zoneSymbol('ignoreConsoleErrorUncaughtError')] = true;
          Zone.root.fork({name: 'promise'}).run(function () {
            const listener = (evt: any) => {
              window.removeEventListener('unhandledrejection', listener);
              expect(evt.type).toEqual('unhandledrejection');
              expect(evt.promise.constructor.name).toEqual('Promise');
              expect(evt.reason.message).toBe('promise error');
            };
            window.addEventListener('unhandledrejection', listener);
            new Promise((resolve, reject) => {
              throw new Error('promise error');
            });
          });
        }),
      );

      it(
        'should support window.addEventListener(rejectionhandled)',
        asyncTest(() => {
          if (!promiseUnhandleRejectionSupport()) {
            return;
          }
          (Zone as any)[zoneSymbol('ignoreConsoleErrorUncaughtError')] = true;
          Zone.root.fork({name: 'promise'}).run(function () {
            const listener = (evt: any) => {
              window.removeEventListener('unhandledrejection', listener);
              p.catch((reason) => {});
            };
            window.addEventListener('unhandledrejection', listener);

            const handledListener = (evt: any) => {
              window.removeEventListener('rejectionhandled', handledListener);
              expect(evt.type).toEqual('rejectionhandled');
              expect(evt.promise.constructor.name).toEqual('Promise');
              expect(evt.reason.message).toBe('promise error');
            };

            window.addEventListener('rejectionhandled', handledListener);
            const p = new Promise((resolve, reject) => {
              throw new Error('promise error');
            });
          });
        }),
      );

      it(
        'should support multiple window.addEventListener(unhandledrejection)',
        asyncTest(() => {
          if (!promiseUnhandleRejectionSupport()) {
            return;
          }
          (Zone as any)[zoneSymbol('ignoreConsoleErrorUncaughtError')] = true;
          Zone.root.fork({name: 'promise'}).run(function () {
            const listener1 = (evt: any) => {
              window.removeEventListener('unhandledrejection', listener1);
              expect(evt.type).toEqual('unhandledrejection');
              expect(evt.promise.constructor.name).toEqual('Promise');
              expect(evt.reason.message).toBe('promise error');
            };
            const listener2 = (evt: any) => {
              window.removeEventListener('unhandledrejection', listener2);
              expect(evt.type).toEqual('unhandledrejection');
              expect(evt.promise.constructor.name).toEqual('Promise');
              expect(evt.reason.message).toBe('promise error');
              evt.preventDefault();
            };
            window.addEventListener('unhandledrejection', listener1);
            window.addEventListener('unhandledrejection', listener2);
            new Promise((resolve, reject) => {
              throw new Error('promise error');
            });
          });
        }),
      );
    });

    // @JiaLiPassion, Edge 15, the behavior is not the same with Chrome
    // wait for fix.
    xit(
      'IntersectionObserver should run callback in zone',
      ifEnvSupportsWithDone('IntersectionObserver', (done: Function) => {
        const div = document.createElement('div');
        document.body.appendChild(div);
        const options: any = {threshold: 0.5};

        const zone = Zone.current.fork({name: 'intersectionObserverZone'});

        zone.run(() => {
          const observer = new IntersectionObserver(() => {
            expect(Zone.current.name).toEqual(zone.name);
            observer.unobserve(div);
            done();
          }, options);
          observer.observe(div);
        });
        div.style.display = 'none';
        div.style.visibility = 'block';
      }),
    );

    it(
      'HTMLCanvasElement.toBlob should be a ZoneAware MacroTask',
      ifEnvSupportsWithDone(supportCanvasTest, (done: Function) => {
        const canvas = document.createElement('canvas');
        const d = canvas.width;
        const ctx = canvas.getContext('2d')!;
        ctx.beginPath();
        ctx.moveTo(d / 2, 0);
        ctx.lineTo(d, d);
        ctx.lineTo(0, d);
        ctx.closePath();
        ctx.fillStyle = 'yellow';
        ctx.fill();

        const scheduleSpy = jasmine.createSpy('scheduleSpy');
        const zone: Zone = Zone.current.fork({
          name: 'canvas',
          onScheduleTask: (
            delegate: ZoneDelegate,
            currentZone: Zone,
            targetZone: Zone,
            task: Task,
          ) => {
            scheduleSpy();
            return delegate.scheduleTask(targetZone, task);
          },
        });

        zone.run(() => {
          const canvasData = canvas.toDataURL();
          canvas.toBlob(function (blob) {
            expect(Zone.current.name).toEqual('canvas');
            expect(scheduleSpy).toHaveBeenCalled();

            const reader = new FileReader();
            reader.readAsDataURL(blob!);
            reader.onloadend = function () {
              const base64data = reader.result;
              expect(base64data).toEqual(canvasData);
              done();
            };
          });
        });
      }),
    );

    describe(
      'ResizeObserver',
      ifEnvSupports('ResizeObserver', () => {
        it('ResizeObserver callback should be in zone', (done) => {
          const ResizeObserver = (window as any)['ResizeObserver'];
          const div = document.createElement('div');
          const zone = Zone.current.fork({name: 'observer'});
          const observer = new ResizeObserver((entries: any, ob: any) => {
            expect(Zone.current.name).toEqual(zone.name);

            expect(entries.length).toBe(1);
            expect(entries[0].target).toBe(div);
            observer.disconnect();
            done();
          });

          zone.run(() => {
            observer.observe(div);
          });

          document.body.appendChild(div);
        });

        it('ResizeObserver callback should be able to in different zones which when they were observed', (done) => {
          const ResizeObserver = (window as any)['ResizeObserver'];
          const div1 = document.createElement('div');
          const div2 = document.createElement('div');
          const zone = Zone.current.fork({name: 'observer'});
          let count = 0;
          const observer = new ResizeObserver((entries: any, ob: any) => {
            entries.forEach((entry: any) => {
              if (entry.target === div1) {
                expect(Zone.current.name).toEqual(zone.name);
              } else {
                expect(Zone.current.name).toEqual('<root>');
              }
            });
            count++;
            if (count === 2) {
              done();
            }
          });

          zone.run(() => {
            observer.observe(div1);
          });
          Zone.root.run(() => {
            observer.observe(div2);
          });

          document.body.appendChild(div1);
          document.body.appendChild(div2);
        });
      }),
    );

    xdescribe('getUserMedia', () => {
      it(
        'navigator.mediaDevices.getUserMedia should in zone',
        ifEnvSupportsWithDone(
          () => {
            return (
              navigator &&
              navigator.mediaDevices &&
              typeof navigator.mediaDevices.getUserMedia === 'function'
            );
          },
          (done: Function) => {
            const zone = Zone.current.fork({name: 'media'});
            zone.run(() => {
              const constraints = {audio: true, video: {width: 1280, height: 720}};

              navigator.mediaDevices
                .getUserMedia(constraints)
                .then(function (mediaStream) {
                  expect(Zone.current.name).toEqual(zone.name);
                  done();
                })
                .catch(function (err) {
                  console.log(err.name + ': ' + err.message);
                  expect(Zone.current.name).toEqual(zone.name);
                  done();
                });
            });
          },
        ),
      );

      // Note: `navigator` is cast to `any` in this test, because the preferred way of accessing
      // `getUserMedia` is through `navigator.mediaDevices`, however some older browsers still
      // expose it directly on `navigator`.
      it(
        'navigator.getUserMedia should in zone',
        ifEnvSupportsWithDone(
          () => {
            return navigator && typeof (navigator as any).getUserMedia === 'function';
          },
          (done: Function) => {
            const zone = Zone.current.fork({name: 'media'});
            zone.run(() => {
              const constraints = {audio: true, video: {width: 1280, height: 720}};
              (navigator as any).getUserMedia(
                constraints,
                () => {
                  expect(Zone.current.name).toEqual(zone.name);
                  done();
                },
                () => {
                  expect(Zone.current.name).toEqual(zone.name);
                  done();
                },
              );
            });
          },
        ),
      );
    });
  });
});
