docs(testing): explain DebugElement.triggerEventHandler (#2438)

This commit is contained in:
Ward Bell 2016-09-21 20:01:44 -07:00 committed by GitHub
parent 8870f30a3d
commit 1c87bd67d8
7 changed files with 105 additions and 38 deletions

View File

@ -7,9 +7,7 @@ import { async, ComponentFixture, fakeAsync, TestBed, tick,
import { RouterTestingModule } from '@angular/router/testing'; import { RouterTestingModule } from '@angular/router/testing';
import { SpyLocation } from '@angular/common/testing'; import { SpyLocation } from '@angular/common/testing';
// tslint:disable:no-unused-variable import { click } from '../testing';
import { newEvent } from '../testing';
// tslint:enable:no-unused-variable
// r - for relatively obscure router symbols // r - for relatively obscure router symbols
import * as r from '@angular/router'; import * as r from '@angular/router';
@ -48,9 +46,8 @@ describe('AppComponent & RouterTestingModule', () => {
it('should navigate to "About" on click', fakeAsync(() => { it('should navigate to "About" on click', fakeAsync(() => {
createComponent(); createComponent();
// page.aboutLinkDe.triggerEventHandler('click', null); // fails click(page.aboutLinkDe);
// page.aboutLinkDe.nativeElement.dispatchEvent(newEvent('click')); // fails // page.aboutLinkDe.nativeElement.click(); // ok but fails in phantom
page.aboutLinkDe.nativeElement.click(); // fails in phantom
advance(); advance();
expectPathToBe('/about'); expectPathToBe('/about');

View File

@ -26,7 +26,7 @@ import { NgModel, NgControl } from '@angular/forms';
import { async, ComponentFixture, fakeAsync, inject, TestBed, tick import { async, ComponentFixture, fakeAsync, inject, TestBed, tick
} from '@angular/core/testing'; } from '@angular/core/testing';
import { addMatchers, newEvent } from '../../testing'; import { addMatchers, newEvent, click } from '../../testing';
beforeEach( addMatchers ); beforeEach( addMatchers );
@ -180,7 +180,7 @@ describe('TestBed Component Tests', () => {
const comp = fixture.componentInstance; const comp = fixture.componentInstance;
const hero = comp.heroes[0]; const hero = comp.heroes[0];
heroes[0].triggerEventHandler('click', null); click(heroes[0]);
fixture.detectChanges(); fixture.detectChanges();
const selected = fixture.debugElement.query(By.css('p')); const selected = fixture.debugElement.query(By.css('p'));
@ -213,7 +213,7 @@ describe('TestBed Component Tests', () => {
fixture.detectChanges(); fixture.detectChanges();
expect(span.textContent).toMatch(/is off/i, 'before click'); expect(span.textContent).toMatch(/is off/i, 'before click');
btn.triggerEventHandler('click', null); click(btn);
fixture.detectChanges(); fixture.detectChanges();
expect(span.textContent).toMatch(/is on/i, 'after click'); expect(span.textContent).toMatch(/is on/i, 'after click');
}); });
@ -610,7 +610,7 @@ describe('Lifecycle hooks w/ MyIfParentComp', () => {
getChild(); getChild();
const btn = fixture.debugElement.query(By.css('button')); const btn = fixture.debugElement.query(By.css('button'));
btn.triggerEventHandler('click', null); click(btn);
fixture.detectChanges(); fixture.detectChanges();
expect(child.ngOnDestroyCalled).toBe(true); expect(child.ngOnDestroyCalled).toBe(true);

View File

@ -4,7 +4,7 @@ import { async, ComponentFixture, TestBed
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core'; import { DebugElement } from '@angular/core';
import { addMatchers } from '../../testing'; import { addMatchers, click } from '../../testing';
import { Hero } from '../model/hero'; import { Hero } from '../model/hero';
import { DashboardHeroComponent } from './dashboard-hero.component'; import { DashboardHeroComponent } from './dashboard-hero.component';
@ -53,10 +53,22 @@ describe('DashboardHeroComponent when tested directly', () => {
let selectedHero: Hero; let selectedHero: Hero;
comp.selected.subscribe((hero: Hero) => selectedHero = hero); comp.selected.subscribe((hero: Hero) => selectedHero = hero);
// #docregion trigger-event-handler
heroEl.triggerEventHandler('click', null); heroEl.triggerEventHandler('click', null);
// #enddocregion trigger-event-handler
expect(selectedHero).toBe(expectedHero); expect(selectedHero).toBe(expectedHero);
}); });
// #enddocregion click-test // #enddocregion click-test
// #docregion click-test-2
it('should raise selected event when clicked', () => {
let selectedHero: Hero;
comp.selected.subscribe((hero: Hero) => selectedHero = hero);
click(heroEl); // triggerEventHandler helper
expect(selectedHero).toBe(expectedHero);
});
// #enddocregion click-test-2
}); });
////////////////// //////////////////
@ -89,7 +101,7 @@ describe('DashboardHeroComponent when inside a test host', () => {
}); });
it('should raise selected event when clicked', () => { it('should raise selected event when clicked', () => {
heroEl.triggerEventHandler('click', null); click(heroEl);
// selected hero should be the same data bound hero // selected hero should be the same data bound hero
expect(testHost.selectedHero).toBe(testHost.hero); expect(testHost.selectedHero).toBe(testHost.hero);
}); });
@ -102,8 +114,7 @@ import { Component } from '@angular/core';
// #docregion test-host // #docregion test-host
@Component({ @Component({
template: ` template: `
<dashboard-hero [hero]="hero" (selected)="onSelected($event)"> <dashboard-hero [hero]="hero" (selected)="onSelected($event)"></dashboard-hero>`
</dashboard-hero>`
}) })
class TestHostComponent { class TestHostComponent {
hero = new Hero(42, 'Test Name'); hero = new Hero(42, 'Test Name');

View File

@ -2,7 +2,7 @@
import { async, inject, ComponentFixture, TestBed import { async, inject, ComponentFixture, TestBed
} from '@angular/core/testing'; } from '@angular/core/testing';
import { addMatchers } from '../../testing'; import { addMatchers, click } from '../../testing';
import { HeroService } from '../model'; import { HeroService } from '../model';
import { FakeHeroService } from '../model/testing'; import { FakeHeroService } from '../model/testing';
@ -39,7 +39,7 @@ describe('DashboardComponent (deep)', () => {
function clickForDeep() { function clickForDeep() {
// get first <div class="hero"> DebugElement // get first <div class="hero"> DebugElement
const heroEl = fixture.debugElement.query(By.css('.hero')); const heroEl = fixture.debugElement.query(By.css('.hero'));
heroEl.triggerEventHandler('click', null); click(heroEl);
} }
}); });

View File

@ -7,7 +7,7 @@ import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core'; import { DebugElement } from '@angular/core';
import { import {
ActivatedRoute, ActivatedRouteStub, newEvent, Router, RouterStub ActivatedRoute, ActivatedRouteStub, click, newEvent, Router, RouterStub
} from '../../testing'; } from '../../testing';
import { Hero } from '../model'; import { Hero } from '../model';
@ -103,7 +103,7 @@ function overrideSetup() {
expect(comp.hero.name).toBe(newName, 'component hero has new name'); expect(comp.hero.name).toBe(newName, 'component hero has new name');
expect(hds.testHero.name).toBe(origName, 'service hero unchanged before save'); expect(hds.testHero.name).toBe(origName, 'service hero unchanged before save');
page.saveBtn.triggerEventHandler('click', null); click(page.saveBtn);
tick(); // wait for async save to complete tick(); // wait for async save to complete
expect(hds.testHero.name).toBe(newName, 'service hero has new name after save'); expect(hds.testHero.name).toBe(newName, 'service hero has new name after save');
expect(page.navSpy.calls.any()).toBe(true, 'router.navigate called'); expect(page.navSpy.calls.any()).toBe(true, 'router.navigate called');
@ -159,18 +159,18 @@ function heroModuleSetup() {
// #enddocregion route-good-id // #enddocregion route-good-id
it('should navigate when click cancel', () => { it('should navigate when click cancel', () => {
page.cancelBtn.triggerEventHandler('click', null); click(page.cancelBtn);
expect(page.navSpy.calls.any()).toBe(true, 'router.navigate called'); expect(page.navSpy.calls.any()).toBe(true, 'router.navigate called');
}); });
it('should save when click save but not navigate immediately', () => { it('should save when click save but not navigate immediately', () => {
page.saveBtn.triggerEventHandler('click', null); click(page.saveBtn);
expect(page.saveSpy.calls.any()).toBe(true, 'HeroDetailService.save called'); expect(page.saveSpy.calls.any()).toBe(true, 'HeroDetailService.save called');
expect(page.navSpy.calls.any()).toBe(false, 'router.navigate not called'); expect(page.navSpy.calls.any()).toBe(false, 'router.navigate not called');
}); });
it('should navigate when click save and save resolves', fakeAsync(() => { it('should navigate when click save and save resolves', fakeAsync(() => {
page.saveBtn.triggerEventHandler('click', null); click(page.saveBtn);
tick(); // wait for async save to complete tick(); // wait for async save to complete
expect(page.navSpy.calls.any()).toBe(true, 'router.navigate called'); expect(page.navSpy.calls.any()).toBe(true, 'router.navigate called');
})); }));

View File

@ -1,9 +1,17 @@
import { DebugElement } from '@angular/core';
import { tick, ComponentFixture } from '@angular/core/testing'; import { tick, ComponentFixture } from '@angular/core/testing';
export * from './jasmine-matchers'; export * from './jasmine-matchers';
export * from './router-stubs'; export * from './router-stubs';
// Short utilities ///// Short utilities /////
/** Wait a tick, then detect changes */
export function advance(f: ComponentFixture<any>): void {
tick();
f.detectChanges();
}
/** /**
* Create custom DOM event the old fashioned way * Create custom DOM event the old fashioned way
* *
@ -16,8 +24,20 @@ export function newEvent(eventName: string, bubbles = false, cancelable = false)
return evt; return evt;
} }
/** Wait a tick, then detect changes */ // See https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button
export function advance(f: ComponentFixture<any>): void { // #docregion click-event
tick(); /** Button events to pass to `DebugElement.triggerEventHandler` for RouterLink event handler */
f.detectChanges(); export const ButtonClickEvents = {
left: { button: 0 },
right: { button: 2 }
};
/** Simulate element click. Defaults to mouse left-button click event. */
export function click(el: DebugElement | HTMLElement, eventObj: any = ButtonClickEvents.left): void {
if (el instanceof HTMLElement) {
el.click();
} else {
el.triggerEventHandler('click', eventObj);
} }
}
// #enddocregion click-event

View File

@ -43,7 +43,7 @@ block includes
- [_async_](#async-in-before-each) in `beforeEach` - [_async_](#async-in-before-each) in `beforeEach`
- [_compileComponents_](#compile-components) - [_compileComponents_](#compile-components)
1. [Test a component with inputs and outputs](#component-with-inputs-output) 1. [Test a component with inputs and outputs](#component-with-inputs-output)
<br><br> - [_triggerEventHandler_](#trigger-event-handler)
1. [Test a component inside a test host component](#component-inside-test-host) 1. [Test a component inside a test host component](#component-inside-test-host)
<br><br> <br><br>
1. [Test a routed component](#routed-component) 1. [Test a routed component](#routed-component)
@ -965,14 +965,51 @@ a(href="#top").to-top Back to top
:marked :marked
The component exposes an `EventEmitter` property. The test subscribes to it just as the host component would do. The component exposes an `EventEmitter` property. The test subscribes to it just as the host component would do.
The Angular `DebugElement.triggerEventHandler` lets the test raise _any data-bound event_. The `heroEl` is a `DebugElement` that represents the hero `<div>`.
In this example, the component's template binds to the hero `<div>`. The test calls `triggerEventHandler` with the "click" event name.
The "click" event binding responds by calling `DashboardHeroComponent.click()`.
The test has a reference to that `<div>` in `heroEl` so triggering the `heroEl` click event should cause Angular If the component behaves as expected, `click()` tells the component's `selected` property to emit the `hero` object,
to call `DashboardHeroComponent.click`. the test detects that value through its subscription to `selected`, and the test should pass.
#trigger-event-handler
:marked
### _triggerEventHandler_
The Angular `DebugElement.triggerEventHandler` can raise _any data-bound event_ by its _event name_.
The second parameter is the event object passed to the handler.
In this example, the test triggers a "click" event with a null event object.
+makeExample('testing/ts/app/dashboard/dashboard-hero.component.spec.ts', 'trigger-event-handler')(format='.')
:marked
The test assumes (correctly in this case) that the runtime event handler &mdash; the component's `click()` method &mdash;
doesn't care about the event object.
Other handlers will be less forgiving.
For example, the `RouterLink` directive expects an object with a `button` property indicating the mouse button that was pressed.
The directive throws an error if the event object doesn't do this correctly.
#click-helper
:marked
Clicking a button, an anchor, or an arbitrary HTML element is a common test task.
Make that easy by encapsulating the _click-triggering_ process in a helper such as the `click` function below:
+makeExample('testing/ts/testing/index.ts', 'click-event', 'testing/index.ts (click helper)')(format='.')
:marked
The first parameter is the _element-to-click_. You can pass a custom event object as the second parameter if you wish. The default is a (partial)
<a href="https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button" target="_blank">left-button mouse event object</a>
accepted by many handlers including the `RouterLink` directive.
.callout.is-critical
header click() is not an ATP function
:marked
The `click()` helper function is **not** part of the _Angular Testing Platform_.
It's a function defined in _this chapter's sample code_ and used by all of the sample tests.
If you like it, add it to your own collection of helpers.
:marked
Here's the previous test, rewritten using this click helper.
+makeExample('testing/ts/app/dashboard/dashboard-hero.component.spec.ts', 'click-test-2', 'app/dashboard/dashboard-hero.component.spec.ts (click test revised)')(format='.')
If the component behaves as expected, its `selected` property should emit the `hero` object,
the test detects that emission through its subscription, and the test will pass.
.l-hr .l-hr
@ -2135,8 +2172,10 @@ table
:marked :marked
Triggers the event by its name if there is a corresponding listener Triggers the event by its name if there is a corresponding listener
in the element's `listeners` collection. in the element's `listeners` collection.
The second parameter is the _event object_ expected by the handler.
See [above](#trigger-event-handler).
If the event lacks a listner or there's some other problem, If the event lacks a listener or there's some other problem,
consider calling `nativeElement.dispatchEvent(eventObject)` consider calling `nativeElement.dispatchEvent(eventObject)`
tr tr