diff --git a/goldens/size-tracking/aio-payloads.json b/goldens/size-tracking/aio-payloads.json index 53a33f37e35..36d844487ae 100755 --- a/goldens/size-tracking/aio-payloads.json +++ b/goldens/size-tracking/aio-payloads.json @@ -16,7 +16,7 @@ "uncompressed": { "runtime": 4343, "main": 450179, - "polyfills": 37823, + "polyfills": 33349, "styles": 70416, "light-theme": 77582, "dark-theme": 77711 diff --git a/goldens/size-tracking/integration-payloads.json b/goldens/size-tracking/integration-payloads.json index c950f6df56f..80c689f523b 100644 --- a/goldens/size-tracking/integration-payloads.json +++ b/goldens/size-tracking/integration-payloads.json @@ -4,7 +4,7 @@ "uncompressed": { "runtime": 1083, "main": 126218, - "polyfills": 37778 + "polyfills": 33304 } } }, @@ -16,7 +16,7 @@ "main": "Likely there is a missing PURE annotation https://github.com/angular/angular/pull/43344", "main": "Tracking issue: https://github.com/angular/angular/issues/43568", "main": 20378, - "polyfills": 37802 + "polyfills": 33328 } } }, @@ -25,7 +25,7 @@ "uncompressed": { "runtime": 1105, "main": 131882, - "polyfills": 37800 + "polyfills": 33326 } } }, @@ -34,7 +34,7 @@ "uncompressed": { "runtime": 929, "main": 124544, - "polyfills": 38488 + "polyfills": 34010 } } }, @@ -43,7 +43,7 @@ "uncompressed": { "runtime": 2835, "main": 233348, - "polyfills": 37796, + "polyfills": 33322, "src_app_lazy_lazy_module_ts": 795 } } @@ -53,27 +53,27 @@ "uncompressed": { "runtime": 1063, "main": 158556, - "polyfills": 37758 + "polyfills": 33284 } - } - }, - "animations": { - "master": { - "uncompressed": { - "runtime": 1070, - "main": 158300, - "polyfills": 37768 + }, + "animations": { + "master": { + "uncompressed": { + "runtime": 1070, + "main": 158300, + "polyfills": 33294 + } } - } - }, - "hello_world__closure": { - "master": { - "uncompressed": { - "bundle": "TODO(i): temporarily increase the payload size limit from 105779 - this is due to a closure issue related to ESM reexports that still needs to be investigated", - "bundle": "TODO(i): we should define ngDevMode to false in Closure, but --define only works in the global scope.", - "bundle": "TODO(i): (FW-2164) TS 3.9 new class shape seems to have broken Closure in big ways. The size went from 169991 to 252338", - "bundle": "TODO(i): after removal of tsickle from ngc-wrapped / ng_package, we had to switch to SIMPLE optimizations which increased the size from 252338 to 1198917, see PR#37221 and PR#37317 for more info", - "bundle": 1214857 + }, + "hello_world__closure": { + "master": { + "uncompressed": { + "bundle": "TODO(i): temporarily increase the payload size limit from 105779 - this is due to a closure issue related to ESM reexports that still needs to be investigated", + "bundle": "TODO(i): we should define ngDevMode to false in Closure, but --define only works in the global scope.", + "bundle": "TODO(i): (FW-2164) TS 3.9 new class shape seems to have broken Closure in big ways. The size went from 169991 to 252338", + "bundle": "TODO(i): after removal of tsickle from ngc-wrapped / ng_package, we had to switch to SIMPLE optimizations which increased the size from 252338 to 1198917, see PR#37221 and PR#37317 for more info", + "bundle": 1214857 + } } } } diff --git a/packages/zone.js/lib/browser/api-util.ts b/packages/zone.js/lib/browser/api-util.ts index e9dff33932e..ed2259ef5fd 100644 --- a/packages/zone.js/lib/browser/api-util.ts +++ b/packages/zone.js/lib/browser/api-util.ts @@ -10,9 +10,12 @@ import {globalSources, patchEventPrototype, patchEventTarget, zoneSymbolEventNam import {ADD_EVENT_LISTENER_STR, ArraySlice, attachOriginToPatched, bindArguments, FALSE_STR, isBrowser, isIEOrEdge, isMix, isNode, ObjectCreate, ObjectDefineProperty, ObjectGetOwnPropertyDescriptor, patchClass, patchMacroTask, patchMethod, patchOnProperties, REMOVE_EVENT_LISTENER_STR, TRUE_STR, wrapWithCurrentZone, ZONE_SYMBOL_PREFIX} from '../common/utils'; import {patchCallbacks} from './browser-util'; -import {eventNames, filterProperties} from './property-descriptor'; +import {filterProperties, getOnEventNames} from './property-descriptor'; Zone.__load_patch('util', (global: any, Zone: ZoneType, api: _ZonePrivate) => { + // Collect native event names by looking at properties + // on the global namespace, e.g. 'onclick'. + const eventNames: string[] = getOnEventNames(global); api.patchOnProperties = patchOnProperties; api.patchMethod = patchMethod; api.bindArguments = bindArguments; diff --git a/packages/zone.js/lib/browser/property-descriptor.ts b/packages/zone.js/lib/browser/property-descriptor.ts index 2eaf64376c8..cc08e692df0 100644 --- a/packages/zone.js/lib/browser/property-descriptor.ts +++ b/packages/zone.js/lib/browser/property-descriptor.ts @@ -12,226 +12,6 @@ import {isBrowser, isIE, isMix, isNode, ObjectGetPrototypeOf, patchOnProperties} from '../common/utils'; -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']; - -export const eventNames = globalEventHandlersEventNames.concat( - webglEventNames, formEventNames, detailEventNames, documentEventNames, windowEventNames, - htmlElementEventNames, ieElementEventNames); - export interface IgnoreProperty { target: any; ignoreProperties: string[]; @@ -263,6 +43,16 @@ export function patchFilteredProperties( patchOnProperties(target, filteredProperties, prototype); } +/** + * Get all event name properties which the event name startsWith `on` + * from the target object itself, inherited properties are not considered. + */ +export function getOnEventNames(target: Object) { + return Object.getOwnPropertyNames(target) + .filter(name => name.startsWith('on') && name.length > 2) + .map(name => name.substring(2)); +} + export function propertyDescriptorPatch(api: _ZonePrivate, _global: any) { if (isNode && !isMix) { return; @@ -271,64 +61,32 @@ export function propertyDescriptorPatch(api: _ZonePrivate, _global: any) { // events are already been patched by legacy patch. return; } - const supportsWebSocket = typeof WebSocket !== 'undefined'; const ignoreProperties: IgnoreProperty[] = _global['__Zone_ignore_on_properties']; // for browsers that we can patch the descriptor: Chrome & Firefox + let patchTargets: string[] = []; if (isBrowser) { const internalWindow: any = window; + patchTargets = patchTargets.concat([ + 'Document', 'SVGElement', 'Element', 'HTMLElement', 'HTMLBodyElement', 'HTMLMediaElement', + 'HTMLFrameSetElement', 'HTMLFrameElement', 'HTMLIFrameElement', 'HTMLMarqueeElement', 'Worker' + ]); const ignoreErrorProperties = isIE() ? [{target: internalWindow, ignoreProperties: ['error']}] : []; // in IE/Edge, onProp not exist in window object, but in WindowPrototype // so we need to pass WindowPrototype to check onProp exist or not patchFilteredProperties( - internalWindow, eventNames.concat(['messageerror']), + internalWindow, getOnEventNames(internalWindow), ignoreProperties ? ignoreProperties.concat(ignoreErrorProperties) : ignoreProperties, ObjectGetPrototypeOf(internalWindow)); - patchFilteredProperties(Document.prototype, eventNames, ignoreProperties); - - if (typeof internalWindow['SVGElement'] !== 'undefined') { - patchFilteredProperties(internalWindow['SVGElement'].prototype, eventNames, ignoreProperties); - } - patchFilteredProperties(Element.prototype, eventNames, ignoreProperties); - patchFilteredProperties(HTMLElement.prototype, eventNames, ignoreProperties); - patchFilteredProperties(HTMLMediaElement.prototype, mediaElementEventNames, ignoreProperties); - patchFilteredProperties( - HTMLFrameSetElement.prototype, windowEventNames.concat(frameSetEventNames), - ignoreProperties); - patchFilteredProperties( - HTMLBodyElement.prototype, windowEventNames.concat(frameSetEventNames), ignoreProperties); - patchFilteredProperties(HTMLFrameElement.prototype, frameEventNames, ignoreProperties); - patchFilteredProperties(HTMLIFrameElement.prototype, frameEventNames, ignoreProperties); - - const HTMLMarqueeElement = internalWindow['HTMLMarqueeElement']; - if (HTMLMarqueeElement) { - patchFilteredProperties(HTMLMarqueeElement.prototype, marqueeEventNames, ignoreProperties); - } - const Worker = internalWindow['Worker']; - if (Worker) { - patchFilteredProperties(Worker.prototype, workerEventNames, ignoreProperties); - } } - const XMLHttpRequest = _global['XMLHttpRequest']; - if (XMLHttpRequest) { - // XMLHttpRequest is not available in ServiceWorker, so we need to check here - patchFilteredProperties(XMLHttpRequest.prototype, XMLHttpRequestEventNames, ignoreProperties); - } - const XMLHttpRequestEventTarget = _global['XMLHttpRequestEventTarget']; - if (XMLHttpRequestEventTarget) { - patchFilteredProperties( - XMLHttpRequestEventTarget && XMLHttpRequestEventTarget.prototype, XMLHttpRequestEventNames, - ignoreProperties); - } - if (typeof IDBIndex !== 'undefined') { - patchFilteredProperties(IDBIndex.prototype, IDBIndexEventNames, ignoreProperties); - patchFilteredProperties(IDBRequest.prototype, IDBIndexEventNames, ignoreProperties); - patchFilteredProperties(IDBOpenDBRequest.prototype, IDBIndexEventNames, ignoreProperties); - patchFilteredProperties(IDBDatabase.prototype, IDBIndexEventNames, ignoreProperties); - patchFilteredProperties(IDBTransaction.prototype, IDBIndexEventNames, ignoreProperties); - patchFilteredProperties(IDBCursor.prototype, IDBIndexEventNames, ignoreProperties); - } - if (supportsWebSocket) { - patchFilteredProperties(WebSocket.prototype, websocketEventNames, ignoreProperties); + patchTargets = patchTargets.concat([ + 'XMLHttpRequest', 'XMLHttpRequestEventTarget', 'IDBIndex', 'IDBRequest', 'IDBOpenDBRequest', + 'IDBDatabase', 'IDBTransaction', 'IDBCursor', 'WebSocket' + ]); + for (let i = 0; i < patchTargets.length; i++) { + const target = _global[patchTargets[i]]; + target && target.prototype && + patchFilteredProperties( + target.prototype, getOnEventNames(target.prototype), ignoreProperties); } } diff --git a/packages/zone.js/lib/common/utils.ts b/packages/zone.js/lib/common/utils.ts index 28a58f0ff16..ab0d79e9ba4 100644 --- a/packages/zone.js/lib/common/utils.ts +++ b/packages/zone.js/lib/common/utils.ts @@ -207,22 +207,20 @@ export function patchProperty(obj: any, prop: string, prototype?: any) { if (!target) { return; } - let previousValue = (target as any)[eventNameSymbol]; - if (previousValue) { + + const previousValue = (target as any)[eventNameSymbol]; + if (previousValue && typeof previousValue === 'function') { target.removeEventListener(eventName, wrapFn); } - // issue #978, when onload handler was added before loading zone.js - // we should remove it with originalDescSet - if (originalDescSet) { - originalDescSet.apply(target, NULL_ON_PROP_VALUE); - } - + (target as any)[eventNameSymbol] = newValue; if (typeof newValue === 'function') { - (target as any)[eventNameSymbol] = newValue; + // issue #978, when onload handler was added before loading zone.js + // we should remove it with originalDescSet + if (originalDescSet) { + originalDescSet.apply(target, NULL_ON_PROP_VALUE); + } target.addEventListener(eventName, wrapFn, false); - } else { - (target as any)[eventNameSymbol] = null; } }; diff --git a/packages/zone.js/test/assets/empty-worker.js b/packages/zone.js/test/assets/empty-worker.js new file mode 100644 index 00000000000..823e9bf40a2 --- /dev/null +++ b/packages/zone.js/test/assets/empty-worker.js @@ -0,0 +1,7 @@ +/** + * @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.io/license + */ diff --git a/packages/zone.js/test/browser/browser.spec.ts b/packages/zone.js/test/browser/browser.spec.ts index a828a25c37f..f8f7d87f4f6 100644 --- a/packages/zone.js/test/browser/browser.spec.ts +++ b/packages/zone.js/test/browser/browser.spec.ts @@ -138,17 +138,265 @@ describe('Zone', function() { 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']; - function checkIsOnPropertiesPatched(target: any, ignoredProperties?: string[]) { - for (let prop in target) { + 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.substr(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) { + continue; + } target[prop] = noop; if (!target[Zone.__symbol__('ON_PROPERTY' + prop.substr(2))]) { - console.log('onProp is null:', prop); + fail(`${prop} of ${target} is not patched`); } else { target[prop] = null; expect(!target[Zone.__symbol__('ON_PROPERTY' + prop.substr(2))]).toBeTruthy(); @@ -157,7 +405,90 @@ describe('Zone', function() { } } - it('should patch all possbile on properties on element', function() { + 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) { + return []; + } + 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', @@ -173,29 +504,60 @@ describe('Zone', function() { 'video' ]; htmlElementTagNames.forEach(tagName => { - checkIsOnPropertiesPatched(document.createElement(tagName), ['onorientationchange']); + checkIsOnPropertiesPatched( + document.createElement(tagName), eventNames, ['onorientationchange']); }); }); - it('should patch all possbile on properties on body', function() { - checkIsOnPropertiesPatched(document.body, ['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 possbile on properties on Document', function() { - checkIsOnPropertiesPatched(document, ['onorientationchange']); + it('should patch all possible on properties on media element', function() { + const audio = document.createElement('audio'); + checkIsOnPropertiesPatched(audio, mediaElementEventNames); }); - it('should patch all possbile on properties on Window', function() { - checkIsOnPropertiesPatched(window, [ - 'onvrdisplayactivate', 'onvrdisplayblur', 'onvrdisplayconnect', - 'onvrdisplaydeactivate', 'onvrdisplaydisconnect', 'onvrdisplayfocus', - 'onvrdisplaypointerrestricted', 'onvrdisplaypointerunrestricted', - 'onorientationchange', 'onerror' - ]); + 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 possbile on properties on xhr', function() { - checkIsOnPropertiesPatched(new XMLHttpRequest()); + 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('/base/angular/packages/zone.js/test/assets/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() { @@ -242,6 +604,16 @@ describe('Zone', function() { document.body.removeChild(div); }); + it('property startsWith 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('should be able to clear on handler added before load zone.js', function() { const TestTarget: any = (window as any)['TestTarget']; patchFilteredProperties(