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:
parent
a610d12266
commit
184d270725
|
@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 =
|
||||||
|
|
|
@ -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', () => {
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue