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 {
|
||||
this.listeners.forEach((listener) => {
|
||||
const node = this.nativeNode as any;
|
||||
const invokedListeners: Function[] = [];
|
||||
|
||||
this.listeners.forEach(listener => {
|
||||
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 {
|
||||
// Note: we are performing most of the work in the listener function itself
|
||||
// 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
|
||||
// must also mark the component view itself dirty (i.e. the view that it owns).
|
||||
const startView =
|
||||
|
|
|
@ -8,11 +8,12 @@
|
|||
|
||||
|
||||
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 {By} from '@angular/platform-browser/src/dom/debug/by';
|
||||
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
|
||||
import {expect} from '@angular/platform-browser/testing/src/matchers';
|
||||
import {ivyEnabled} from '@angular/private/testing';
|
||||
|
||||
@Injectable()
|
||||
class Logger {
|
||||
|
@ -653,7 +654,6 @@ class TestCmptWithPropBindings {
|
|||
|
||||
fixture.debugElement.children[1].triggerEventHandler('myevent', <Event>{});
|
||||
expect(fixture.componentInstance.customed).toBe(true);
|
||||
|
||||
});
|
||||
|
||||
it('should include classes in properties.className', () => {
|
||||
|
@ -683,6 +683,61 @@ class TestCmptWithPropBindings {
|
|||
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', () => {
|
||||
|
||||
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 {
|
||||
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);
|
||||
if (allowDefaultBehavior === false) {
|
||||
// TODO(tbosch): move preventDefault into event plugins...
|
||||
event.preventDefault();
|
||||
event.returnValue = false;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -182,6 +182,12 @@ class DefaultServerRenderer2 implements Renderer2 {
|
|||
|
||||
private decoratePreventDefault(eventHandler: Function): Function {
|
||||
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
|
||||
// by Zone on the server. This is required only for tests.
|
||||
const allowDefaultBehavior = this.ngZone.runGuarded(() => eventHandler(event));
|
||||
|
@ -189,6 +195,8 @@ class DefaultServerRenderer2 implements Renderer2 {
|
|||
event.preventDefault();
|
||||
event.returnValue = false;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue