Kristiyan Kostadinov 3c66b100dd perf(common): remove unused methods from DomAdapter (#41102)
The `DomAdapter` is present in all Angular apps and its methods aren't tree shakeable.
These changes remove the methods that either aren't being used anymore or were only
used by our own tests. Note that these changes aren't breaking, because the adapter
is an internal API.

The following methods were removed:
* `getProperty` - only used within our own tests.
* `log` - Guaranteed to be defined on `console`.
* `logGroup` and `logGroupEnd` - Only used in one place. It was in the DomAdapter for built-in null checking.
* `logGroupEnd` - Only used in one place. It was placed in the DomAdapter for built in null checking.
* `performanceNow` - Only used in one place that has to be invoked through the browser console.
* `supportsCookies` - Unused.
* `getCookie` - Unused.
* `getLocation` and `getHistory` - Only used in one place which appears to have access to the DOM
already, because it had direct accesses to `window`. Furthermore, even if this was being used
in a non-browser context already, the `DominoAdapter` was set up to throw an error.

The following APIs were changed to be more compact:
* `supportsDOMEvents` - Changed to a readonly property.
* `remove` - No longer returns the removed node.

PR Close #41102
2021-03-10 11:48:24 -08:00

537 lines
20 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.io/license
*/
import {ɵgetDOM as getDOM} from '@angular/common';
import {NgZone} from '@angular/core/src/zone/ng_zone';
import {beforeEach, describe, expect, it} from '@angular/core/testing/src/testing_internal';
import {DomEventsPlugin} from '@angular/platform-browser/src/dom/events/dom_events';
import {EventManager, EventManagerPlugin} from '@angular/platform-browser/src/dom/events/event_manager';
import {createMouseEvent, el} from '../../../testing/src/browser_util';
(function() {
if (isNode) return;
let domEventPlugin: DomEventsPlugin;
let doc: any;
let zone: NgZone;
describe('EventManager', () => {
beforeEach(() => {
doc = getDOM().supportsDOMEvents ? document : getDOM().createHtmlDocument();
zone = new NgZone({});
domEventPlugin = new DomEventsPlugin(doc);
});
it('should delegate event bindings to plugins that are passed in from the most generic one to the most specific one',
() => {
const element = el('<div></div>');
const handler = (e: any /** TODO #9100 */) => e;
const plugin = new FakeEventManagerPlugin(doc, ['click']);
const manager = new EventManager([domEventPlugin, plugin], new FakeNgZone());
manager.addEventListener(element, 'click', handler);
expect(plugin.eventHandler['click']).toBe(handler);
});
it('should delegate event bindings to the first plugin supporting the event', () => {
const element = el('<div></div>');
const clickHandler = (e: any /** TODO #9100 */) => e;
const dblClickHandler = (e: any /** TODO #9100 */) => e;
const plugin1 = new FakeEventManagerPlugin(doc, ['dblclick']);
const plugin2 = new FakeEventManagerPlugin(doc, ['click', 'dblclick']);
const manager = new EventManager([plugin2, plugin1], new FakeNgZone());
manager.addEventListener(element, 'click', clickHandler);
manager.addEventListener(element, 'dblclick', dblClickHandler);
expect(plugin2.eventHandler['click']).toBe(clickHandler);
expect(plugin1.eventHandler['dblclick']).toBe(dblClickHandler);
});
it('should throw when no plugin can handle the event', () => {
const element = el('<div></div>');
const plugin = new FakeEventManagerPlugin(doc, ['dblclick']);
const manager = new EventManager([plugin], new FakeNgZone());
expect(() => manager.addEventListener(element, 'click', null!))
.toThrowError('No event manager plugin found for event click');
});
it('events are caught when fired from a child', () => {
const element = el('<div><div></div></div>');
// Workaround for https://bugs.webkit.org/show_bug.cgi?id=122755
doc.body.appendChild(element);
const child = element.firstChild as Element;
const dispatchedEvent = createMouseEvent('click');
let receivedEvent: any /** TODO #9100 */ = null;
const handler = (e: any /** TODO #9100 */) => {
receivedEvent = e;
};
const manager = new EventManager([domEventPlugin], new FakeNgZone());
manager.addEventListener(element, 'click', handler);
getDOM().dispatchEvent(child, dispatchedEvent);
expect(receivedEvent).toBe(dispatchedEvent);
});
it('should add and remove global event listeners', () => {
const element = el('<div><div></div></div>');
doc.body.appendChild(element);
const dispatchedEvent = createMouseEvent('click');
let receivedEvent: any /** TODO #9100 */ = null;
const handler = (e: any /** TODO #9100 */) => {
receivedEvent = e;
};
const manager = new EventManager([domEventPlugin], new FakeNgZone());
const remover = manager.addGlobalEventListener('document', 'click', handler);
getDOM().dispatchEvent(element, dispatchedEvent);
expect(receivedEvent).toBe(dispatchedEvent);
receivedEvent = null;
remover();
getDOM().dispatchEvent(element, dispatchedEvent);
expect(receivedEvent).toBe(null);
});
it('should keep zone when addEventListener', () => {
const Zone = (window as any)['Zone'];
const element = el('<div><div></div></div>');
doc.body.appendChild(element);
const dispatchedEvent = createMouseEvent('click');
let receivedEvent: any /** TODO #9100 */ = null;
let receivedZone: any = null;
const handler = (e: any /** TODO #9100 */) => {
receivedEvent = e;
receivedZone = Zone.current;
};
const manager = new EventManager([domEventPlugin], new FakeNgZone());
let remover: any = null;
Zone.root.run(() => {
remover = manager.addEventListener(element, 'click', handler);
});
getDOM().dispatchEvent(element, dispatchedEvent);
expect(receivedEvent).toBe(dispatchedEvent);
expect(receivedZone.name).toBe(Zone.root.name);
receivedEvent = null;
remover && remover();
getDOM().dispatchEvent(element, dispatchedEvent);
expect(receivedEvent).toBe(null);
});
it('should keep zone when addEventListener multiple times', () => {
const Zone = (window as any)['Zone'];
const element = el('<div><div></div></div>');
doc.body.appendChild(element);
const dispatchedEvent = createMouseEvent('click');
let receivedEvents: any[] /** TODO #9100 */ = [];
let receivedZones: any[] = [];
const handler1 = (e: any /** TODO #9100 */) => {
receivedEvents.push(e);
receivedZones.push(Zone.current.name);
};
const handler2 = (e: any /** TODO #9100 */) => {
receivedEvents.push(e);
receivedZones.push(Zone.current.name);
};
const manager = new EventManager([domEventPlugin], new FakeNgZone());
let remover1: any = null;
let remover2: any = null;
Zone.root.run(() => {
remover1 = manager.addEventListener(element, 'click', handler1);
});
Zone.root.fork({name: 'test'}).run(() => {
remover2 = manager.addEventListener(element, 'click', handler2);
});
getDOM().dispatchEvent(element, dispatchedEvent);
expect(receivedEvents).toEqual([dispatchedEvent, dispatchedEvent]);
expect(receivedZones).toEqual([Zone.root.name, 'test']);
receivedEvents = [];
remover1 && remover1();
remover2 && remover2();
getDOM().dispatchEvent(element, dispatchedEvent);
expect(receivedEvents).toEqual([]);
});
it('should support event.stopImmediatePropagation', () => {
const Zone = (window as any)['Zone'];
const element = el('<div><div></div></div>');
doc.body.appendChild(element);
const dispatchedEvent = createMouseEvent('click');
let receivedEvents: any[] /** TODO #9100 */ = [];
let receivedZones: any[] = [];
const handler1 = (e: any /** TODO #9100 */) => {
receivedEvents.push(e);
receivedZones.push(Zone.current.name);
e.stopImmediatePropagation();
};
const handler2 = (e: any /** TODO #9100 */) => {
receivedEvents.push(e);
receivedZones.push(Zone.current.name);
};
const manager = new EventManager([domEventPlugin], new FakeNgZone());
let remover1: any = null;
let remover2: any = null;
Zone.root.run(() => {
remover1 = manager.addEventListener(element, 'click', handler1);
});
Zone.root.fork({name: 'test'}).run(() => {
remover2 = manager.addEventListener(element, 'click', handler2);
});
getDOM().dispatchEvent(element, dispatchedEvent);
expect(receivedEvents).toEqual([dispatchedEvent]);
expect(receivedZones).toEqual([Zone.root.name]);
receivedEvents = [];
remover1 && remover1();
remover2 && remover2();
getDOM().dispatchEvent(element, dispatchedEvent);
expect(receivedEvents).toEqual([]);
});
it('should handle event correctly when one handler remove itself ', () => {
const Zone = (window as any)['Zone'];
const element = el('<div><div></div></div>');
doc.body.appendChild(element);
const dispatchedEvent = createMouseEvent('click');
let receivedEvents: any[] /** TODO #9100 */ = [];
let receivedZones: any[] = [];
let remover1: any = null;
let remover2: any = null;
const handler1 = (e: any /** TODO #9100 */) => {
receivedEvents.push(e);
receivedZones.push(Zone.current.name);
remover1 && remover1();
};
const handler2 = (e: any /** TODO #9100 */) => {
receivedEvents.push(e);
receivedZones.push(Zone.current.name);
};
const manager = new EventManager([domEventPlugin], new FakeNgZone());
Zone.root.run(() => {
remover1 = manager.addEventListener(element, 'click', handler1);
});
Zone.root.fork({name: 'test'}).run(() => {
remover2 = manager.addEventListener(element, 'click', handler2);
});
getDOM().dispatchEvent(element, dispatchedEvent);
expect(receivedEvents).toEqual([dispatchedEvent, dispatchedEvent]);
expect(receivedZones).toEqual([Zone.root.name, 'test']);
receivedEvents = [];
remover1 && remover1();
remover2 && remover2();
getDOM().dispatchEvent(element, dispatchedEvent);
expect(receivedEvents).toEqual([]);
});
it('should only add same callback once when addEventListener', () => {
const Zone = (window as any)['Zone'];
const element = el('<div><div></div></div>');
doc.body.appendChild(element);
const dispatchedEvent = createMouseEvent('click');
let receivedEvents: any[] /** TODO #9100 */ = [];
let receivedZones: any[] = [];
const handler = (e: any /** TODO #9100 */) => {
receivedEvents.push(e);
receivedZones.push(Zone.current.name);
};
const manager = new EventManager([domEventPlugin], new FakeNgZone());
let remover1: any = null;
let remover2: any = null;
Zone.root.run(() => {
remover1 = manager.addEventListener(element, 'click', handler);
});
Zone.root.fork({name: 'test'}).run(() => {
remover2 = manager.addEventListener(element, 'click', handler);
});
getDOM().dispatchEvent(element, dispatchedEvent);
expect(receivedEvents).toEqual([dispatchedEvent]);
expect(receivedZones).toEqual([Zone.root.name]);
receivedEvents = [];
remover1 && remover1();
remover2 && remover2();
getDOM().dispatchEvent(element, dispatchedEvent);
expect(receivedEvents).toEqual([]);
});
it('should be able to remove event listener which was added inside of ngZone', () => {
const Zone = (window as any)['Zone'];
const element = el('<div><div></div></div>');
doc.body.appendChild(element);
const dispatchedEvent = createMouseEvent('click');
let receivedEvents: any[] /** TODO #9100 */ = [];
let receivedZones: any[] = [];
const handler1 = (e: any /** TODO #9100 */) => {
receivedEvents.push(e);
receivedZones.push(Zone.current.name);
};
const handler2 = (e: any /** TODO #9100 */) => {
receivedEvents.push(e);
receivedZones.push(Zone.current.name);
};
const manager = new EventManager([domEventPlugin], new FakeNgZone());
let remover1: any = null;
let remover2: any = null;
// handler1 is added in root zone
Zone.root.run(() => {
remover1 = manager.addEventListener(element, 'click', handler1);
});
// handler2 is added in 'angular' zone
Zone.root.fork({name: 'fakeAngularZone', properties: {isAngularZone: true}}).run(() => {
remover2 = manager.addEventListener(element, 'click', handler2);
});
getDOM().dispatchEvent(element, dispatchedEvent);
expect(receivedEvents).toEqual([dispatchedEvent, dispatchedEvent]);
expect(receivedZones).toEqual([Zone.root.name, 'fakeAngularZone']);
receivedEvents = [];
remover1 && remover1();
remover2 && remover2();
getDOM().dispatchEvent(element, dispatchedEvent);
// handler1 and handler2 are added in different zone
// one is angular zone, the other is not
// should still be able to remove them correctly
expect(receivedEvents).toEqual([]);
});
it('should run unpatchedEvents handler outside of ngZone', () => {
const Zone = (window as any)['Zone'];
const element = el('<div><div></div></div>');
doc.body.appendChild(element);
const dispatchedEvent = createMouseEvent('scroll');
let receivedEvent: any /** TODO #9100 */ = null;
let receivedZone: any = null;
const handler = (e: any /** TODO #9100 */) => {
receivedEvent = e;
receivedZone = Zone.current;
};
const manager = new EventManager([domEventPlugin], new FakeNgZone());
let remover = manager.addEventListener(element, 'scroll', handler);
getDOM().dispatchEvent(element, dispatchedEvent);
expect(receivedEvent).toBe(dispatchedEvent);
expect(receivedZone.name).not.toEqual('angular');
receivedEvent = null;
remover && remover();
getDOM().dispatchEvent(element, dispatchedEvent);
expect(receivedEvent).toBe(null);
});
it('should only trigger one Change detection when bubbling with shouldCoalesceEventChangeDetection = true',
(done: DoneFn) => {
doc = getDOM().supportsDOMEvents ? document : getDOM().createHtmlDocument();
zone = new NgZone({shouldCoalesceEventChangeDetection: true});
domEventPlugin = new DomEventsPlugin(doc);
const element = el('<div></div>');
const child = el('<div></div>');
element.appendChild(child);
doc.body.appendChild(element);
const dispatchedEvent = createMouseEvent('click');
let receivedEvents: any = [];
let stables: any = [];
const handler = (e: any) => {
receivedEvents.push(e);
};
const manager = new EventManager([domEventPlugin], zone);
let removerChild: any;
let removerParent: any;
zone.run(() => {
removerChild = manager.addEventListener(child, 'click', handler);
removerParent = manager.addEventListener(element, 'click', handler);
});
zone.onStable.subscribe((isStable: any) => {
stables.push(isStable);
});
getDOM().dispatchEvent(child, dispatchedEvent);
requestAnimationFrame(() => {
expect(receivedEvents.length).toBe(2);
expect(stables.length).toBe(1);
removerChild && removerChild();
removerParent && removerParent();
done();
});
});
it('should only trigger one Change detection when bubbling with shouldCoalesceRunChangeDetection = true',
(done: DoneFn) => {
doc = getDOM().supportsDOMEvents ? document : getDOM().createHtmlDocument();
zone = new NgZone({shouldCoalesceRunChangeDetection: true});
domEventPlugin = new DomEventsPlugin(doc);
const element = el('<div></div>');
const child = el('<div></div>');
element.appendChild(child);
doc.body.appendChild(element);
const dispatchedEvent = createMouseEvent('click');
let receivedEvents: any = [];
let stables: any = [];
const handler = (e: any) => {
receivedEvents.push(e);
};
const manager = new EventManager([domEventPlugin], zone);
let removerChild: any;
let removerParent: any;
zone.run(() => {
removerChild = manager.addEventListener(child, 'click', handler);
removerParent = manager.addEventListener(element, 'click', handler);
});
zone.onStable.subscribe((isStable: any) => {
stables.push(isStable);
});
getDOM().dispatchEvent(child, dispatchedEvent);
requestAnimationFrame(() => {
expect(receivedEvents.length).toBe(2);
expect(stables.length).toBe(1);
removerChild && removerChild();
removerParent && removerParent();
done();
});
});
it('should not drain micro tasks queue too early with shouldCoalesceEventChangeDetection=true',
(done: DoneFn) => {
doc = getDOM().supportsDOMEvents ? document : getDOM().createHtmlDocument();
zone = new NgZone({shouldCoalesceEventChangeDetection: true});
domEventPlugin = new DomEventsPlugin(doc);
const element = el('<div></div>');
const child = el('<div></div>');
doc.body.appendChild(element);
const dispatchedClickEvent = createMouseEvent('click');
const dispatchedBlurEvent: FocusEvent =
getDOM().getDefaultDocument().createEvent('FocusEvent');
dispatchedBlurEvent.initEvent('blur', true, true);
let logs: any = [];
const handler = () => {};
const blurHandler = (e: any) => {
logs.push('blur');
};
const manager = new EventManager([domEventPlugin], zone);
let removerParent: any;
let removerChildFocus: any;
zone.run(() => {
removerParent = manager.addEventListener(element, 'click', handler);
removerChildFocus = manager.addEventListener(child, 'blur', blurHandler);
});
const sub = zone.onStable.subscribe(() => {
sub.unsubscribe();
logs.push('begin');
Promise.resolve().then(() => {
logs.push('promise resolved');
});
element.appendChild(child);
getDOM().dispatchEvent(child, dispatchedBlurEvent);
logs.push('end');
});
getDOM().dispatchEvent(element, dispatchedClickEvent);
requestAnimationFrame(() => {
expect(logs).toEqual(['begin', 'blur', 'end', 'promise resolved']);
removerParent && removerParent();
removerChildFocus && removerChildFocus();
done();
});
});
it('should not drain micro tasks queue too early with shouldCoalesceRunChangeDetection=true',
(done: DoneFn) => {
doc = getDOM().supportsDOMEvents ? document : getDOM().createHtmlDocument();
zone = new NgZone({shouldCoalesceRunChangeDetection: true});
domEventPlugin = new DomEventsPlugin(doc);
const element = el('<div></div>');
const child = el('<div></div>');
doc.body.appendChild(element);
const dispatchedClickEvent = createMouseEvent('click');
const dispatchedBlurEvent: FocusEvent =
getDOM().getDefaultDocument().createEvent('FocusEvent');
dispatchedBlurEvent.initEvent('blur', true, true);
let logs: any = [];
const handler = () => {};
const blurHandler = (e: any) => {
logs.push('blur');
};
const manager = new EventManager([domEventPlugin], zone);
let removerParent: any;
let removerChildFocus: any;
zone.run(() => {
removerParent = manager.addEventListener(element, 'click', handler);
removerChildFocus = manager.addEventListener(child, 'blur', blurHandler);
});
const sub = zone.onStable.subscribe(() => {
sub.unsubscribe();
logs.push('begin');
Promise.resolve().then(() => {
logs.push('promise resolved');
});
element.appendChild(child);
getDOM().dispatchEvent(child, dispatchedBlurEvent);
logs.push('end');
});
getDOM().dispatchEvent(element, dispatchedClickEvent);
requestAnimationFrame(() => {
expect(logs).toEqual(['begin', 'blur', 'end', 'promise resolved']);
removerParent && removerParent();
removerChildFocus && removerChildFocus();
done();
});
});
});
})();
/** @internal */
class FakeEventManagerPlugin extends EventManagerPlugin {
eventHandler: {[event: string]: Function} = {};
constructor(doc: any, public supportedEvents: string[]) {
super(doc);
}
supports(eventName: string): boolean {
return this.supportedEvents.indexOf(eventName) > -1;
}
addEventListener(element: any, eventName: string, handler: Function) {
this.eventHandler[eventName] = handler;
return () => {
delete this.eventHandler[eventName];
};
}
}
class FakeNgZone extends NgZone {
constructor() {
super({enableLongStackTrace: false, shouldCoalesceEventChangeDetection: true});
}
run<T>(fn: (...args: any[]) => T, applyThis?: any, applyArgs?: any[]): T {
return fn();
}
runOutsideAngular(fn: Function) {
return fn();
}
}