angular/packages/zone.js/test/browser/browser.spec.ts
Joey Perrott 2f82d811e5 build: migrate zone.js tests to use web test runner instead of karma web tests (#62826)
Migrate zone.js bazel tests to use web test runner instead of karma

PR Close #62826
2025-07-28 14:18:45 +02:00

4193 lines
141 KiB
TypeScript

/**
* @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 {isIEOrEdge, zoneSymbol} from '../../lib/common/utils';
import {
getEdgeVersion,
getIEVersion,
ifEnvSupports,
ifEnvSupportsWithDone,
isEdge,
} 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';
function ieOrEdge() {
return isIEOrEdge();
}
(ieOrEdge as any).message = 'IE/Edge Test';
class TestEventListener {
logs: any[] = [];
addEventListener(eventName: string, listener: any, options: any) {
this.logs.push(options);
}
removeEventListener(eventName: string, listener: any, options: any) {}
}
describe('Zone', 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 ieElementEventNames = [
'activate',
'afterupdate',
'ariarequest',
'beforeactivate',
'beforedeactivate',
'beforeeditfocus',
'beforeupdate',
'cellchange',
'controlselect',
'dataavailable',
'datasetchanged',
'datasetcomplete',
'errorupdate',
'filterchange',
'layoutcomplete',
'losecapture',
'move',
'moveend',
'movestart',
'propertychange',
'resizeend',
'resizestart',
'rowenter',
'rowexit',
'rowsdelete',
'rowsinserted',
'command',
'compassneedscalibration',
'deactivate',
'help',
'mscontentzoom',
'msmanipulationstatechanged',
'msgesturechange',
'msgesturedoubletap',
'msgestureend',
'msgesturehold',
'msgesturestart',
'msgesturetap',
'msgotpointercapture',
'msinertiastart',
'mslostpointercapture',
'mspointercancel',
'mspointerdown',
'mspointerenter',
'mspointerhover',
'mspointerleave',
'mspointermove',
'mspointerout',
'mspointerover',
'mspointerup',
'pointerout',
'mssitemodejumplistitemremoved',
'msthumbnailclick',
'stop',
'storagecommit',
];
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,
ieElementEventNames,
);
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');
if (getEdgeVersion() !== 14) {
// Edge 14, error will be undefined.
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 true
zone.run(function () {
(document as any).addEventListener('click', docListener, {capture: true});
button.addEventListener('click', btnListener);
});
button.dispatchEvent(clickEvent);
expect(hookSpy).toHaveBeenCalled();
expect(logs).toEqual(['document', 'button']);
logs = [];
(document as any).removeEventListener('click', docListener, {capture: true});
button.removeEventListener('click', btnListener);
expect(cancelSpy).toHaveBeenCalled();
button.dispatchEvent(clickEvent);
expect(logs).toEqual([]);
});
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']);
});
it(
'should bypass addEventListener of FunctionWrapper and __BROWSERTOOLS_CONSOLE_SAFEFUNC of IE/Edge',
ifEnvSupports(ieOrEdge, 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);
},
});
let logs: string[] = [];
const listener1 = function () {
logs.push(Zone.current.name);
};
(listener1 as any).toString = function () {
return '[object FunctionWrapper]';
};
const listener2 = function () {
logs.push(Zone.current.name);
};
(listener2 as any).toString = function () {
return 'function __BROWSERTOOLS_CONSOLE_SAFEFUNC() { [native code] }';
};
zone.run(() => {
button.addEventListener('click', listener1);
button.addEventListener('click', listener2);
});
button.dispatchEvent(clickEvent);
expect(hookSpy).not.toHaveBeenCalled();
expect(logs).toEqual(['ProxyZone', 'ProxyZone']);
logs = [];
button.removeEventListener('click', listener1);
button.removeEventListener('click', listener2);
button.dispatchEvent(clickEvent);
expect(hookSpy).not.toHaveBeenCalled();
expect(logs).toEqual([]);
}),
);
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 (
!isEdge() &&
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 !isEdge() && 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();
},
);
});
},
),
);
});
});
if (getIEVersion() === 11) {
describe('pointer event in IE', () => {
const pointerEventsMap: {[key: string]: string} = {
'MSPointerCancel': 'pointercancel',
'MSPointerDown': 'pointerdown',
'MSPointerEnter': 'pointerenter',
'MSPointerHover': 'pointerhover',
'MSPointerLeave': 'pointerleave',
'MSPointerMove': 'pointermove',
'MSPointerOut': 'pointerout',
'MSPointerOver': 'pointerover',
'MSPointerUp': 'pointerup',
};
let div: HTMLDivElement;
beforeEach(() => {
div = document.createElement('div');
document.body.appendChild(div);
});
afterEach(() => {
document.body.removeChild(div);
});
Object.keys(pointerEventsMap).forEach((key) => {
it(`${key} and ${pointerEventsMap[key]} should both be triggered`, (done: DoneFn) => {
const logs: string[] = [];
div.addEventListener(key, (event: any) => {
expect(event.type).toEqual(pointerEventsMap[key]);
logs.push(`${key} triggered`);
});
div.addEventListener(pointerEventsMap[key], (event: any) => {
expect(event.type).toEqual(pointerEventsMap[key]);
logs.push(`${pointerEventsMap[key]} triggered`);
});
const evt1 = document.createEvent('Event');
evt1.initEvent(key, true, true);
div.dispatchEvent(evt1);
setTimeout(() => {
expect(logs).toEqual([`${key} triggered`, `${pointerEventsMap[key]} triggered`]);
});
const evt2 = document.createEvent('Event');
evt2.initEvent(pointerEventsMap[key], true, true);
div.dispatchEvent(evt2);
setTimeout(() => {
expect(logs).toEqual([`${key} triggered`, `${pointerEventsMap[key]} triggered`]);
});
setTimeout(done);
});
it(`${key} and ${pointerEventsMap[key]} with same listener should not be triggered twice`, (done: DoneFn) => {
const logs: string[] = [];
const listener = function (event: any) {
expect(event.type).toEqual(pointerEventsMap[key]);
logs.push(`${key} triggered`);
};
div.addEventListener(key, listener);
div.addEventListener(pointerEventsMap[key], listener);
const evt1 = document.createEvent('Event');
evt1.initEvent(key, true, true);
div.dispatchEvent(evt1);
setTimeout(() => {
expect(logs).toEqual([`${key} triggered`]);
});
const evt2 = document.createEvent('Event');
evt2.initEvent(pointerEventsMap[key], true, true);
div.dispatchEvent(evt2);
setTimeout(() => {
expect(logs).toEqual([`${pointerEventsMap[key]} triggered`]);
});
setTimeout(done);
});
it(`${key} and ${pointerEventsMap[key]} should be able to be removed with removeEventListener`, (done: DoneFn) => {
const logs: string[] = [];
const listener1 = function (event: any) {
logs.push(`${key} triggered`);
};
const listener2 = function (event: any) {
logs.push(`${pointerEventsMap[key]} triggered`);
};
div.addEventListener(key, listener1);
div.addEventListener(pointerEventsMap[key], listener2);
div.removeEventListener(key, listener1);
div.removeEventListener(key, listener2);
const evt1 = document.createEvent('Event');
evt1.initEvent(key, true, true);
div.dispatchEvent(evt1);
setTimeout(() => {
expect(logs).toEqual([]);
});
const evt2 = document.createEvent('Event');
evt2.initEvent(pointerEventsMap[key], true, true);
div.dispatchEvent(evt2);
setTimeout(() => {
expect(logs).toEqual([]);
});
div.addEventListener(key, listener1);
div.addEventListener(pointerEventsMap[key], listener2);
div.removeEventListener(pointerEventsMap[key], listener1);
div.removeEventListener(pointerEventsMap[key], listener2);
div.dispatchEvent(evt1);
setTimeout(() => {
expect(logs).toEqual([]);
});
div.dispatchEvent(evt2);
setTimeout(() => {
expect(logs).toEqual([]);
});
setTimeout(done);
});
});
});
}
});