fix(ivy): DebugElement.triggerEventHandler not picking up events registered via Renderer2 (#31845)

Fixes Ivy's `DebugElement.triggerEventHandler` to picking up events that have been registered through a `Renderer2`, unlike ViewEngine.

This PR resolves FW-1480.

PR Close #31845
This commit is contained in:
Kristiyan Kostadinov 2019-07-31 07:23:47 +03:00 committed by Alex Rickabaugh
parent a610d12266
commit 184d270725
5 changed files with 99 additions and 5 deletions

View File

@ -377,11 +377,28 @@ class DebugElement__POST_R3__ extends DebugNode__POST_R3__ implements DebugEleme
} }
triggerEventHandler(eventName: string, eventObj: any): void { triggerEventHandler(eventName: string, eventObj: any): void {
this.listeners.forEach((listener) => { const node = this.nativeNode as any;
const invokedListeners: Function[] = [];
this.listeners.forEach(listener => {
if (listener.name === eventName) { if (listener.name === eventName) {
listener.callback(eventObj); const callback = listener.callback;
callback(eventObj);
invokedListeners.push(callback);
} }
}); });
// We need to check whether `eventListeners` exists, because it's something
// that Zone.js only adds to `EventTarget` in browser environments.
if (typeof node.eventListeners === 'function') {
// Note that in Ivy we wrap event listeners with a call to `event.preventDefault` in some
// cases. We use `Function` as a special token that gives us access to the actual event
// listener.
node.eventListeners(eventName).forEach((listener: Function) => {
const unwrappedListener = listener(Function);
return invokedListeners.indexOf(unwrappedListener) === -1 && unwrappedListener(eventObj);
});
}
} }
} }

View File

@ -235,7 +235,13 @@ function wrapListener(
wrapWithPreventDefault: boolean): EventListener { wrapWithPreventDefault: boolean): EventListener {
// Note: we are performing most of the work in the listener function itself // Note: we are performing most of the work in the listener function itself
// to optimize listener registration. // to optimize listener registration.
return function wrapListenerIn_markDirtyAndPreventDefault(e: Event) { return function wrapListenerIn_markDirtyAndPreventDefault(e: any) {
// Ivy uses `Function` as a special token that allows us to unwrap the function
// so that it can be invoked programmatically by `DebugNode.triggerEventHandler`.
if (e === Function) {
return listenerFn;
}
// In order to be backwards compatible with View Engine, events on component host nodes // In order to be backwards compatible with View Engine, events on component host nodes
// must also mark the component view itself dirty (i.e. the view that it owns). // must also mark the component view itself dirty (i.e. the view that it owns).
const startView = const startView =

View File

@ -8,11 +8,12 @@
import {CommonModule, NgIfContext} from '@angular/common'; import {CommonModule, NgIfContext} from '@angular/common';
import {Component, DebugNode, Directive, ElementRef, EmbeddedViewRef, EventEmitter, HostBinding, Injectable, Input, NO_ERRORS_SCHEMA, Output, Renderer2, TemplateRef, ViewChild, ViewContainerRef} from '@angular/core'; import {Component, DebugNode, Directive, ElementRef, EmbeddedViewRef, EventEmitter, HostBinding, Injectable, Input, NO_ERRORS_SCHEMA, OnInit, Output, Renderer2, TemplateRef, ViewChild, ViewContainerRef} from '@angular/core';
import {ComponentFixture, TestBed, async} from '@angular/core/testing'; import {ComponentFixture, TestBed, async} from '@angular/core/testing';
import {By} from '@angular/platform-browser/src/dom/debug/by'; import {By} from '@angular/platform-browser/src/dom/debug/by';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {expect} from '@angular/platform-browser/testing/src/matchers'; import {expect} from '@angular/platform-browser/testing/src/matchers';
import {ivyEnabled} from '@angular/private/testing';
@Injectable() @Injectable()
class Logger { class Logger {
@ -653,7 +654,6 @@ class TestCmptWithPropBindings {
fixture.debugElement.children[1].triggerEventHandler('myevent', <Event>{}); fixture.debugElement.children[1].triggerEventHandler('myevent', <Event>{});
expect(fixture.componentInstance.customed).toBe(true); expect(fixture.componentInstance.customed).toBe(true);
}); });
it('should include classes in properties.className', () => { it('should include classes in properties.className', () => {
@ -683,6 +683,61 @@ class TestCmptWithPropBindings {
expect(button.properties).toEqual({disabled: true, tabIndex: 1337, title: 'hello'}); expect(button.properties).toEqual({disabled: true, tabIndex: 1337, title: 'hello'});
}); });
it('should trigger events registered via Renderer2', () => {
@Component({template: ''})
class TestComponent implements OnInit {
count = 0;
eventObj: any;
constructor(private renderer: Renderer2, private elementRef: ElementRef) {}
ngOnInit() {
this.renderer.listen(this.elementRef.nativeElement, 'click', (event: any) => {
this.count++;
this.eventObj = event;
});
}
}
TestBed.configureTestingModule({declarations: [TestComponent]});
const fixture = TestBed.createComponent(TestComponent);
// Ivy depends on `eventListeners` to pick up events that haven't been registered through
// Angular templates. At the time of writing Zone.js doesn't add `eventListeners` in Node
// environments so we have to skip the test.
if (!ivyEnabled || typeof fixture.debugElement.nativeElement.eventListeners === 'function') {
const event = {value: true};
fixture.detectChanges();
fixture.debugElement.triggerEventHandler('click', event);
expect(fixture.componentInstance.count).toBe(1);
expect(fixture.componentInstance.eventObj).toBe(event);
}
});
it('should be able to trigger an event with a null value', () => {
let value = undefined;
@Component({template: '<button (click)="handleClick($event)"></button>'})
class TestComponent {
handleClick(_event: any) {
value = _event;
// Returning `false` is what causes the renderer to call `event.preventDefault`.
return false;
}
}
TestBed.configureTestingModule({declarations: [TestComponent]});
const fixture = TestBed.createComponent(TestComponent);
const button = fixture.debugElement.query(By.css('button'));
expect(() => {
button.triggerEventHandler('click', null);
fixture.detectChanges();
}).not.toThrow();
expect(value).toBeNull();
});
describe('componentInstance on DebugNode', () => { describe('componentInstance on DebugNode', () => {
it('should return component associated with a node if a node is a component host', () => { it('should return component associated with a node if a node is a component host', () => {

View File

@ -49,12 +49,20 @@ export function flattenStyles(
function decoratePreventDefault(eventHandler: Function): Function { function decoratePreventDefault(eventHandler: Function): Function {
return (event: any) => { return (event: any) => {
// Ivy uses `Function` as a special token that allows us to unwrap the function
// so that it can be invoked programmatically by `DebugNode.triggerEventHandler`.
if (event === Function) {
return eventHandler;
}
const allowDefaultBehavior = eventHandler(event); const allowDefaultBehavior = eventHandler(event);
if (allowDefaultBehavior === false) { if (allowDefaultBehavior === false) {
// TODO(tbosch): move preventDefault into event plugins... // TODO(tbosch): move preventDefault into event plugins...
event.preventDefault(); event.preventDefault();
event.returnValue = false; event.returnValue = false;
} }
return undefined;
}; };
} }

View File

@ -182,6 +182,12 @@ class DefaultServerRenderer2 implements Renderer2 {
private decoratePreventDefault(eventHandler: Function): Function { private decoratePreventDefault(eventHandler: Function): Function {
return (event: any) => { return (event: any) => {
// Ivy uses `Function` as a special token that allows us to unwrap the function
// so that it can be invoked programmatically by `DebugNode.triggerEventHandler`.
if (event === Function) {
return eventHandler;
}
// Run the event handler inside the ngZone because event handlers are not patched // Run the event handler inside the ngZone because event handlers are not patched
// by Zone on the server. This is required only for tests. // by Zone on the server. This is required only for tests.
const allowDefaultBehavior = this.ngZone.runGuarded(() => eventHandler(event)); const allowDefaultBehavior = this.ngZone.runGuarded(() => eventHandler(event));
@ -189,6 +195,8 @@ class DefaultServerRenderer2 implements Renderer2 {
event.preventDefault(); event.preventDefault();
event.returnValue = false; event.returnValue = false;
} }
return undefined;
}; };
} }
} }