Last active
April 6, 2018 21:56
-
-
Save BryanWilhite/f7885f10e8b39233a32d196c2c56bb78 to your computer and use it in GitHub Desktop.
Angular Tour of Heroes Tests [https://angular.io/guide/testing]
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { async, ComponentFixture, TestBed | |
} from '@angular/core/testing'; | |
import { By } from '@angular/platform-browser'; | |
import { DebugElement } from '@angular/core'; | |
import { addMatchers, click } from '../../testing'; | |
import { Hero } from '../model/hero'; | |
import { DashboardHeroComponent } from './dashboard-hero.component'; | |
beforeEach( addMatchers ); | |
describe('DashboardHeroComponent class only', () => { | |
it('raises the selected event when clicked', () => { | |
const comp = new DashboardHeroComponent(); | |
const hero: Hero = { id: 42, name: 'Test' }; | |
comp.hero = hero; | |
comp.selected.subscribe(selectedHero => expect(selectedHero).toBe(hero)); | |
comp.click(); | |
}); | |
}); | |
describe('DashboardHeroComponent when tested directly', () => { | |
let comp: DashboardHeroComponent; | |
let expectedHero: Hero; | |
let fixture: ComponentFixture<DashboardHeroComponent>; | |
let heroDe: DebugElement; | |
let heroEl: HTMLElement; | |
beforeEach(async(() => { | |
TestBed.configureTestingModule({ | |
declarations: [ DashboardHeroComponent ] | |
}) | |
.compileComponents(); | |
})); | |
beforeEach(() => { | |
fixture = TestBed.createComponent(DashboardHeroComponent); | |
comp = fixture.componentInstance; | |
// find the hero's DebugElement and element | |
heroDe = fixture.debugElement.query(By.css('.hero')); | |
heroEl = heroDe.nativeElement; | |
// mock the hero supplied by the parent component | |
expectedHero = { id: 42, name: 'Test Name' }; | |
// simulate the parent setting the input property with that hero | |
comp.hero = expectedHero; | |
// trigger initial data binding | |
fixture.detectChanges(); | |
}); | |
it('should display hero name in uppercase', () => { | |
const expectedPipedName = expectedHero.name.toUpperCase(); | |
expect(heroEl.textContent).toContain(expectedPipedName); | |
}); | |
it('should raise selected event when clicked (triggerEventHandler)', () => { | |
let selectedHero: Hero; | |
comp.selected.subscribe((hero: Hero) => selectedHero = hero); | |
heroDe.triggerEventHandler('click', null); | |
expect(selectedHero).toBe(expectedHero); | |
}); | |
it('should raise selected event when clicked (element.click)', () => { | |
let selectedHero: Hero; | |
comp.selected.subscribe((hero: Hero) => selectedHero = hero); | |
heroEl.click(); | |
expect(selectedHero).toBe(expectedHero); | |
}); | |
it('should raise selected event when clicked (click helper)', () => { | |
let selectedHero: Hero; | |
comp.selected.subscribe(hero => selectedHero = hero); | |
click(heroDe); // click helper with DebugElement | |
click(heroEl); // click helper with native element | |
expect(selectedHero).toBe(expectedHero); | |
}); | |
}); | |
////////////////// | |
describe('DashboardHeroComponent when inside a test host', () => { | |
let testHost: TestHostComponent; | |
let fixture: ComponentFixture<TestHostComponent>; | |
let heroEl: HTMLElement; | |
beforeEach(async(() => { | |
TestBed.configureTestingModule({ | |
declarations: [ DashboardHeroComponent, TestHostComponent ] | |
}) | |
.compileComponents(); | |
})); | |
beforeEach(() => { | |
// create TestHostComponent instead of DashboardHeroComponent | |
fixture = TestBed.createComponent(TestHostComponent); | |
testHost = fixture.componentInstance; | |
heroEl = fixture.nativeElement.querySelector('.hero'); | |
fixture.detectChanges(); // trigger initial data binding | |
}); | |
it('should display hero name', () => { | |
const expectedPipedName = testHost.hero.name.toUpperCase(); | |
expect(heroEl.textContent).toContain(expectedPipedName); | |
}); | |
it('should raise selected event when clicked', () => { | |
click(heroEl); | |
// selected hero should be the same data bound hero | |
expect(testHost.selectedHero).toBe(testHost.hero); | |
}); | |
}); | |
////// Test Host Component ////// | |
import { Component } from '@angular/core'; | |
@Component({ | |
template: ` | |
<dashboard-hero | |
[hero]="hero" (selected)="onSelected($event)"> | |
</dashboard-hero>` | |
}) | |
class TestHostComponent { | |
hero: Hero = {id: 42, name: 'Test Name' }; | |
selectedHero: Hero; | |
onSelected(hero: Hero) { this.selectedHero = hero; } | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Router } from '@angular/router'; | |
import { DashboardComponent } from './dashboard.component'; | |
import { Hero } from '../model/hero'; | |
import { addMatchers } from '../../testing'; | |
import { TestHeroService, HeroService } from '../model/testing/test-hero.service'; | |
class FakeRouter { | |
navigateByUrl(url: string) { return url; } | |
} | |
describe('DashboardComponent class only', () => { | |
let comp: DashboardComponent; | |
let heroService: TestHeroService; | |
let router: Router; | |
beforeEach(() => { | |
addMatchers(); | |
router = new FakeRouter() as any as Router; | |
heroService = new TestHeroService(); | |
comp = new DashboardComponent(router, heroService); | |
}); | |
it('should NOT have heroes before calling OnInit', () => { | |
expect(comp.heroes.length).toBe(0, | |
'should not have heroes before OnInit'); | |
}); | |
it('should NOT have heroes immediately after OnInit', () => { | |
comp.ngOnInit(); // ngOnInit -> getHeroes | |
expect(comp.heroes.length).toBe(0, | |
'should not have heroes until service promise resolves'); | |
}); | |
it('should HAVE heroes after HeroService gets them', (done: DoneFn) => { | |
comp.ngOnInit(); // ngOnInit -> getHeroes | |
heroService.lastResult // the one from getHeroes | |
.subscribe( | |
() => { | |
// throw new Error('deliberate error'); // see it fail gracefully | |
expect(comp.heroes.length).toBeGreaterThan(0, | |
'should have heroes after service promise resolves'); | |
done(); | |
}, | |
done.fail); | |
}); | |
it('should tell ROUTER to navigate by hero id', () => { | |
const hero: Hero = {id: 42, name: 'Abbracadabra' }; | |
const spy = spyOn(router, 'navigateByUrl'); | |
comp.gotoDetail(hero); | |
const navArgs = spy.calls.mostRecent().args[0]; | |
expect(navArgs).toBe('/heroes/42', 'should nav to HeroDetail for Hero 42'); | |
}); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { async, inject, ComponentFixture, TestBed | |
} from '@angular/core/testing'; | |
import { addMatchers, asyncData, click } from '../../testing'; | |
import { HeroService } from '../model/hero.service'; | |
import { getTestHeroes } from '../model/testing/test-heroes'; | |
import { By } from '@angular/platform-browser'; | |
import { Router } from '@angular/router'; | |
import { DashboardComponent } from './dashboard.component'; | |
import { DashboardModule } from './dashboard.module'; | |
beforeEach ( addMatchers ); | |
let comp: DashboardComponent; | |
let fixture: ComponentFixture<DashboardComponent>; | |
//////// Deep //////////////// | |
describe('DashboardComponent (deep)', () => { | |
beforeEach(() => { | |
TestBed.configureTestingModule({ | |
imports: [ DashboardModule ] | |
}); | |
}); | |
compileAndCreate(); | |
tests(clickForDeep); | |
function clickForDeep() { | |
// get first <div class="hero"> | |
const heroEl: HTMLElement = fixture.nativeElement.querySelector('.hero'); | |
click(heroEl); | |
} | |
}); | |
//////// Shallow //////////////// | |
import { NO_ERRORS_SCHEMA } from '@angular/core'; | |
describe('DashboardComponent (shallow)', () => { | |
beforeEach(() => { | |
TestBed.configureTestingModule({ | |
declarations: [ DashboardComponent ], | |
schemas: [NO_ERRORS_SCHEMA] | |
}); | |
}); | |
compileAndCreate(); | |
tests(clickForShallow); | |
function clickForShallow() { | |
// get first <dashboard-hero> DebugElement | |
const heroDe = fixture.debugElement.query(By.css('dashboard-hero')); | |
heroDe.triggerEventHandler('selected', comp.heroes[0]); | |
} | |
}); | |
/** Add TestBed providers, compile, and create DashboardComponent */ | |
function compileAndCreate() { | |
beforeEach(async(() => { | |
const routerSpy = jasmine.createSpyObj('Router', ['navigateByUrl']); | |
const heroServiceSpy = jasmine.createSpyObj('HeroService', ['getHeroes']); | |
TestBed.configureTestingModule({ | |
providers: [ | |
{ provide: HeroService, useValue: heroServiceSpy }, | |
{ provide: Router, useValue: routerSpy } | |
] | |
}) | |
.compileComponents().then(() => { | |
fixture = TestBed.createComponent(DashboardComponent); | |
comp = fixture.componentInstance; | |
// getHeroes spy returns observable of test heroes | |
heroServiceSpy.getHeroes.and.returnValue(asyncData(getTestHeroes())); | |
}); | |
})); | |
} | |
/** | |
* The (almost) same tests for both. | |
* Only change: the way that the first hero is clicked | |
*/ | |
function tests(heroClick: Function) { | |
it('should NOT have heroes before ngOnInit', () => { | |
expect(comp.heroes.length).toBe(0, | |
'should not have heroes before ngOnInit'); | |
}); | |
it('should NOT have heroes immediately after ngOnInit', () => { | |
fixture.detectChanges(); // runs initial lifecycle hooks | |
expect(comp.heroes.length).toBe(0, | |
'should not have heroes until service promise resolves'); | |
}); | |
describe('after get dashboard heroes', () => { | |
let router: Router; | |
// Trigger component so it gets heroes and binds to them | |
beforeEach(async(() => { | |
router = fixture.debugElement.injector.get(Router); | |
fixture.detectChanges(); // runs ngOnInit -> getHeroes | |
fixture.whenStable() // No need for the `lastPromise` hack! | |
.then(() => fixture.detectChanges()); // bind to heroes | |
})); | |
it('should HAVE heroes', () => { | |
expect(comp.heroes.length).toBeGreaterThan(0, | |
'should have heroes after service promise resolves'); | |
}); | |
it('should DISPLAY heroes', () => { | |
// Find and examine the displayed heroes | |
// Look for them in the DOM by css class | |
const heroes = fixture.nativeElement.querySelectorAll('dashboard-hero'); | |
expect(heroes.length).toBe(4, 'should display 4 heroes'); | |
}); | |
it('should tell ROUTER to navigate when hero clicked', () => { | |
heroClick(); // trigger click on first inner <div class="hero"> | |
// args passed to router.navigateByUrl() spy | |
const spy = router.navigateByUrl as jasmine.Spy; | |
const navArgs = spy.calls.first().args[0]; | |
// expecting to navigate to id of the component's first hero | |
const id = comp.heroes[0].id; | |
expect(navArgs).toBe('/heroes/' + id, | |
'should nav to HeroDetail for first hero'); | |
}); | |
}); | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { asyncData, ActivatedRouteStub } from '../../testing'; | |
import { HeroDetailComponent } from './hero-detail.component'; | |
import { Hero } from '../model/hero'; | |
////////// Tests //////////////////// | |
describe('HeroDetailComponent - no TestBed', () => { | |
let activatedRoute: ActivatedRouteStub; | |
let comp: HeroDetailComponent; | |
let expectedHero: Hero; | |
let hds: any; | |
let router: any; | |
beforeEach((done: DoneFn) => { | |
expectedHero = {id: 42, name: 'Bubba' }; | |
const activatedRoute = new ActivatedRouteStub({ id: expectedHero.id }); | |
router = jasmine.createSpyObj('router', ['navigate']); | |
hds = jasmine.createSpyObj('HeroDetailService', ['getHero', 'saveHero']); | |
hds.getHero.and.returnValue(asyncData(expectedHero)); | |
hds.saveHero.and.returnValue(asyncData(expectedHero)); | |
comp = new HeroDetailComponent(hds, <any> activatedRoute, router); | |
comp.ngOnInit(); | |
// OnInit calls HDS.getHero; wait for it to get the fake hero | |
hds.getHero.calls.first().returnValue.subscribe(done); | |
}); | |
it('should expose the hero retrieved from the service', () => { | |
expect(comp.hero).toBe(expectedHero); | |
}); | |
it('should navigate when click cancel', () => { | |
comp.cancel(); | |
expect(router.navigate.calls.any()).toBe(true, 'router.navigate called'); | |
}); | |
it('should save when click save', () => { | |
comp.save(); | |
expect(hds.saveHero.calls.any()).toBe(true, 'HeroDetailService.save called'); | |
expect(router.navigate.calls.any()).toBe(false, 'router.navigate not called yet'); | |
}); | |
it('should navigate when click save resolves', (done: DoneFn) => { | |
comp.save(); | |
// waits for async save to complete before navigating | |
hds.saveHero.calls.first().returnValue | |
.subscribe(() => { | |
expect(router.navigate.calls.any()).toBe(true, 'router.navigate called'); | |
done(); | |
}); | |
}); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
async, ComponentFixture, fakeAsync, inject, TestBed, tick | |
} from '@angular/core/testing'; | |
import { Router } from '@angular/router'; | |
import { | |
ActivatedRoute, ActivatedRouteStub, asyncData, click, newEvent | |
} from '../../testing'; | |
import { Hero } from '../model/hero'; | |
import { HeroDetailComponent } from './hero-detail.component'; | |
import { HeroDetailService } from './hero-detail.service'; | |
import { HeroModule } from './hero.module'; | |
////// Testing Vars ////// | |
let activatedRoute: ActivatedRouteStub; | |
let component: HeroDetailComponent; | |
let fixture: ComponentFixture<HeroDetailComponent>; | |
let page: Page; | |
////// Tests ////// | |
describe('HeroDetailComponent', () => { | |
beforeEach(() => { | |
activatedRoute = new ActivatedRouteStub(); | |
}); | |
describe('with HeroModule setup', heroModuleSetup); | |
describe('when override its provided HeroDetailService', overrideSetup); | |
describe('with FormsModule setup', formsModuleSetup); | |
describe('with SharedModule setup', sharedModuleSetup); | |
}); | |
/////////////////// | |
function overrideSetup() { | |
class HeroDetailServiceSpy { | |
testHero: Hero = {id: 42, name: 'Test Hero' }; | |
/* emit cloned test hero */ | |
getHero = jasmine.createSpy('getHero').and.callFake( | |
() => asyncData(Object.assign({}, this.testHero)) | |
); | |
/* emit clone of test hero, with changes merged in */ | |
saveHero = jasmine.createSpy('saveHero').and.callFake( | |
(hero: Hero) => asyncData(Object.assign(this.testHero, hero)) | |
); | |
} | |
// the `id` value is irrelevant because ignored by service stub | |
beforeEach(() => activatedRoute.setParamMap({ id: 99999 })); | |
beforeEach(async(() => { | |
const routerSpy = createRouterSpy(); | |
TestBed.configureTestingModule({ | |
imports: [ HeroModule ], | |
providers: [ | |
{ provide: ActivatedRoute, useValue: activatedRoute }, | |
{ provide: Router, useValue: routerSpy}, | |
// HeroDetailService at this level is IRRELEVANT! | |
{ provide: HeroDetailService, useValue: {} } | |
] | |
}) | |
// Override component's own provider | |
.overrideComponent(HeroDetailComponent, { | |
set: { | |
providers: [ | |
{ provide: HeroDetailService, useClass: HeroDetailServiceSpy } | |
] | |
} | |
}) | |
.compileComponents(); | |
})); | |
let hdsSpy: HeroDetailServiceSpy; | |
beforeEach(async(() => { | |
createComponent(); | |
// get the component's injected HeroDetailServiceSpy | |
hdsSpy = fixture.debugElement.injector.get(HeroDetailService) as any; | |
})); | |
it('should have called `getHero`', () => { | |
expect(hdsSpy.getHero.calls.count()).toBe(1, 'getHero called once'); | |
}); | |
it('should display stub hero\'s name', () => { | |
expect(page.nameDisplay.textContent).toBe(hdsSpy.testHero.name); | |
}); | |
it('should save stub hero change', fakeAsync(() => { | |
const origName = hdsSpy.testHero.name; | |
const newName = 'New Name'; | |
page.nameInput.value = newName; | |
page.nameInput.dispatchEvent(newEvent('input')); // tell Angular | |
expect(component.hero.name).toBe(newName, 'component hero has new name'); | |
expect(hdsSpy.testHero.name).toBe(origName, 'service hero unchanged before save'); | |
click(page.saveBtn); | |
expect(hdsSpy.saveHero.calls.count()).toBe(1, 'saveHero called once'); | |
tick(); // wait for async save to complete | |
expect(hdsSpy.testHero.name).toBe(newName, 'service hero has new name after save'); | |
expect(page.navigateSpy.calls.any()).toBe(true, 'router.navigate called'); | |
})); | |
it('fixture injected service is not the component injected service', | |
// inject gets the service from the fixture | |
inject([HeroDetailService], (fixtureService: HeroDetailService) => { | |
// use `fixture.debugElement.injector` to get service from component | |
const componentService = fixture.debugElement.injector.get(HeroDetailService); | |
expect(fixtureService).not.toBe(componentService, 'service injected from fixture'); | |
})); | |
} | |
//////////////////// | |
import { getTestHeroes, TestHeroService, HeroService } from '../model/testing/test-hero.service'; | |
const firstHero = getTestHeroes()[0]; | |
function heroModuleSetup() { | |
beforeEach(async(() => { | |
const routerSpy = createRouterSpy(); | |
TestBed.configureTestingModule({ | |
imports: [ HeroModule ], | |
// declarations: [ HeroDetailComponent ], // NO! DOUBLE DECLARATION | |
providers: [ | |
{ provide: ActivatedRoute, useValue: activatedRoute }, | |
{ provide: HeroService, useClass: TestHeroService }, | |
{ provide: Router, useValue: routerSpy}, | |
] | |
}) | |
.compileComponents(); | |
})); | |
describe('when navigate to existing hero', () => { | |
let expectedHero: Hero; | |
beforeEach(async(() => { | |
expectedHero = firstHero; | |
activatedRoute.setParamMap({ id: expectedHero.id }); | |
createComponent(); | |
})); | |
it('should display that hero\'s name', () => { | |
expect(page.nameDisplay.textContent).toBe(expectedHero.name); | |
}); | |
it('should navigate when click cancel', () => { | |
click(page.cancelBtn); | |
expect(page.navigateSpy.calls.any()).toBe(true, 'router.navigate called'); | |
}); | |
it('should save when click save but not navigate immediately', () => { | |
// Get service injected into component and spy on its`saveHero` method. | |
// It delegates to fake `HeroService.updateHero` which delivers a safe test result. | |
const hds = fixture.debugElement.injector.get(HeroDetailService); | |
const saveSpy = spyOn(hds, 'saveHero').and.callThrough(); | |
click(page.saveBtn); | |
expect(saveSpy.calls.any()).toBe(true, 'HeroDetailService.save called'); | |
expect(page.navigateSpy.calls.any()).toBe(false, 'router.navigate not called'); | |
}); | |
it('should navigate when click save and save resolves', fakeAsync(() => { | |
click(page.saveBtn); | |
tick(); // wait for async save to complete | |
expect(page.navigateSpy.calls.any()).toBe(true, 'router.navigate called'); | |
})); | |
it('should convert hero name to Title Case', () => { | |
// get the name's input and display elements from the DOM | |
const hostElement = fixture.nativeElement; | |
const nameInput: HTMLInputElement = hostElement.querySelector('input'); | |
const nameDisplay: HTMLElement = hostElement.querySelector('span'); | |
// simulate user entering a new name into the input box | |
nameInput.value = 'quick BROWN fOx'; | |
// dispatch a DOM event so that Angular learns of input value change. | |
nameInput.dispatchEvent(newEvent('input')); | |
// Tell Angular to update the display binding through the title pipe | |
fixture.detectChanges(); | |
expect(nameDisplay.textContent).toBe('Quick Brown Fox'); | |
}); | |
}); | |
describe('when navigate with no hero id', () => { | |
beforeEach(async( createComponent )); | |
it('should have hero.id === 0', () => { | |
expect(component.hero.id).toBe(0); | |
}); | |
it('should display empty hero name', () => { | |
expect(page.nameDisplay.textContent).toBe(''); | |
}); | |
}); | |
describe('when navigate to non-existent hero id', () => { | |
beforeEach(async(() => { | |
activatedRoute.setParamMap({ id: 99999 }); | |
createComponent(); | |
})); | |
it('should try to navigate back to hero list', () => { | |
expect(page.gotoListSpy.calls.any()).toBe(true, 'comp.gotoList called'); | |
expect(page.navigateSpy.calls.any()).toBe(true, 'router.navigate called'); | |
}); | |
}); | |
// Why we must use `fixture.debugElement.injector` in `Page()` | |
it('cannot use `inject` to get component\'s provided HeroDetailService', () => { | |
let service: HeroDetailService; | |
fixture = TestBed.createComponent(HeroDetailComponent); | |
expect( | |
// Throws because `inject` only has access to TestBed's injector | |
// which is an ancestor of the component's injector | |
inject([HeroDetailService], (hds: HeroDetailService) => service = hds ) | |
) | |
.toThrowError(/No provider for HeroDetailService/); | |
// get `HeroDetailService` with component's own injector | |
service = fixture.debugElement.injector.get(HeroDetailService); | |
expect(service).toBeDefined('debugElement.injector'); | |
}); | |
} | |
///////////////////// | |
import { FormsModule } from '@angular/forms'; | |
import { TitleCasePipe } from '../shared/title-case.pipe'; | |
function formsModuleSetup() { | |
beforeEach(async(() => { | |
const routerSpy = createRouterSpy(); | |
TestBed.configureTestingModule({ | |
imports: [ FormsModule ], | |
declarations: [ HeroDetailComponent, TitleCasePipe ], | |
providers: [ | |
{ provide: ActivatedRoute, useValue: activatedRoute }, | |
{ provide: HeroService, useClass: TestHeroService }, | |
{ provide: Router, useValue: routerSpy}, | |
] | |
}) | |
.compileComponents(); | |
})); | |
it('should display 1st hero\'s name', async(() => { | |
const expectedHero = firstHero; | |
activatedRoute.setParamMap({ id: expectedHero.id }); | |
createComponent().then(() => { | |
expect(page.nameDisplay.textContent).toBe(expectedHero.name); | |
}); | |
})); | |
} | |
/////////////////////// | |
import { SharedModule } from '../shared/shared.module'; | |
function sharedModuleSetup() { | |
beforeEach(async(() => { | |
const routerSpy = createRouterSpy(); | |
TestBed.configureTestingModule({ | |
imports: [ SharedModule ], | |
declarations: [ HeroDetailComponent ], | |
providers: [ | |
{ provide: ActivatedRoute, useValue: activatedRoute }, | |
{ provide: HeroService, useClass: TestHeroService }, | |
{ provide: Router, useValue: routerSpy}, | |
] | |
}) | |
.compileComponents(); | |
})); | |
it('should display 1st hero\'s name', async(() => { | |
const expectedHero = firstHero; | |
activatedRoute.setParamMap({ id: expectedHero.id }); | |
createComponent().then(() => { | |
expect(page.nameDisplay.textContent).toBe(expectedHero.name); | |
}); | |
})); | |
} | |
/////////// Helpers ///// | |
/** Create the HeroDetailComponent, initialize it, set test variables */ | |
function createComponent() { | |
fixture = TestBed.createComponent(HeroDetailComponent); | |
component = fixture.componentInstance; | |
page = new Page(fixture); | |
// 1st change detection triggers ngOnInit which gets a hero | |
fixture.detectChanges(); | |
return fixture.whenStable().then(() => { | |
// 2nd change detection displays the async-fetched hero | |
fixture.detectChanges(); | |
}); | |
} | |
class Page { | |
// getter properties wait to query the DOM until called. | |
get buttons() { return this.queryAll<HTMLButtonElement>('button'); } | |
get saveBtn() { return this.buttons[0]; } | |
get cancelBtn() { return this.buttons[1]; } | |
get nameDisplay() { return this.query<HTMLElement>('span'); } | |
get nameInput() { return this.query<HTMLInputElement>('input'); } | |
gotoListSpy: jasmine.Spy; | |
navigateSpy: jasmine.Spy; | |
constructor(fixture: ComponentFixture<HeroDetailComponent>) { | |
// get the navigate spy from the injected router spy object | |
const routerSpy = <any> fixture.debugElement.injector.get(Router); | |
this.navigateSpy = routerSpy.navigate; | |
// spy on component's `gotoList()` method | |
const component = fixture.componentInstance; | |
this.gotoListSpy = spyOn(component, 'gotoList').and.callThrough(); | |
} | |
//// query helpers //// | |
private query<T>(selector: string): T { | |
return fixture.nativeElement.querySelector(selector); | |
} | |
private queryAll<T>(selector: string): T[] { | |
return fixture.nativeElement.querySelectorAll(selector); | |
} | |
} | |
function createRouterSpy() { | |
return jasmine.createSpyObj('Router', ['navigate']); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { async, ComponentFixture, fakeAsync, TestBed, tick | |
} from '@angular/core/testing'; | |
import { By } from '@angular/platform-browser'; | |
import { DebugElement } from '@angular/core'; | |
import { Router } from '@angular/router'; | |
import { addMatchers, newEvent } from '../../testing'; | |
import { getTestHeroes, TestHeroService } from '../model/testing/test-hero.service'; | |
import { HeroModule } from './hero.module'; | |
import { HeroListComponent } from './hero-list.component'; | |
import { HighlightDirective } from '../shared/highlight.directive'; | |
import { HeroService } from '../model/hero.service'; | |
const HEROES = getTestHeroes(); | |
let comp: HeroListComponent; | |
let fixture: ComponentFixture<HeroListComponent>; | |
let page: Page; | |
/////// Tests ////// | |
describe('HeroListComponent', () => { | |
beforeEach(async(() => { | |
addMatchers(); | |
const routerSpy = jasmine.createSpyObj('Router', ['navigate']); | |
TestBed.configureTestingModule({ | |
imports: [HeroModule], | |
providers: [ | |
{ provide: HeroService, useClass: TestHeroService }, | |
{ provide: Router, useValue: routerSpy} | |
] | |
}) | |
.compileComponents() | |
.then(createComponent); | |
})); | |
it('should display heroes', () => { | |
expect(page.heroRows.length).toBeGreaterThan(0); | |
}); | |
it('1st hero should match 1st test hero', () => { | |
const expectedHero = HEROES[0]; | |
const actualHero = page.heroRows[0].textContent; | |
expect(actualHero).toContain(expectedHero.id.toString(), 'hero.id'); | |
expect(actualHero).toContain(expectedHero.name, 'hero.name'); | |
}); | |
it('should select hero on click', fakeAsync(() => { | |
const expectedHero = HEROES[1]; | |
const li = page.heroRows[1]; | |
li.dispatchEvent(newEvent('click')); | |
tick(); | |
// `.toEqual` because selectedHero is clone of expectedHero; see FakeHeroService | |
expect(comp.selectedHero).toEqual(expectedHero); | |
})); | |
it('should navigate to selected hero detail on click', fakeAsync(() => { | |
const expectedHero = HEROES[1]; | |
const li = page.heroRows[1]; | |
li.dispatchEvent(newEvent('click')); | |
tick(); | |
// should have navigated | |
expect(page.navSpy.calls.any()).toBe(true, 'navigate called'); | |
// composed hero detail will be URL like 'heroes/42' | |
// expect link array with the route path and hero id | |
// first argument to router.navigate is link array | |
const navArgs = page.navSpy.calls.first().args[0]; | |
expect(navArgs[0]).toContain('heroes', 'nav to heroes detail URL'); | |
expect(navArgs[1]).toBe(expectedHero.id, 'expected hero.id'); | |
})); | |
it('should find `HighlightDirective` with `By.directive', () => { | |
// Can find DebugElement either by css selector or by directive | |
const h2 = fixture.debugElement.query(By.css('h2')); | |
const directive = fixture.debugElement.query(By.directive(HighlightDirective)); | |
expect(h2).toBe(directive); | |
}); | |
it('should color header with `HighlightDirective`', () => { | |
const h2 = page.highlightDe.nativeElement as HTMLElement; | |
const bgColor = h2.style.backgroundColor; | |
// different browsers report color values differently | |
const isExpectedColor = bgColor === 'gold' || bgColor === 'rgb(255, 215, 0)'; | |
expect(isExpectedColor).toBe(true, 'backgroundColor'); | |
}); | |
it('the `HighlightDirective` is among the element\'s providers', () => { | |
expect(page.highlightDe.providerTokens).toContain(HighlightDirective, 'HighlightDirective'); | |
}); | |
}); | |
/////////// Helpers ///// | |
/** Create the component and set the `page` test variables */ | |
function createComponent() { | |
fixture = TestBed.createComponent(HeroListComponent); | |
comp = fixture.componentInstance; | |
// change detection triggers ngOnInit which gets a hero | |
fixture.detectChanges(); | |
return fixture.whenStable().then(() => { | |
// got the heroes and updated component | |
// change detection updates the view | |
fixture.detectChanges(); | |
page = new Page(); | |
}); | |
} | |
class Page { | |
/** Hero line elements */ | |
heroRows: HTMLLIElement[]; | |
/** Highlighted DebugElement */ | |
highlightDe: DebugElement; | |
/** Spy on router navigate method */ | |
navSpy: jasmine.Spy; | |
constructor() { | |
const heroRowNodes = fixture.nativeElement.querySelectorAll('li'); | |
this.heroRows = Array.from(heroRowNodes); | |
// Find the first element with an attached HighlightDirective | |
this.highlightDe = fixture.debugElement.query(By.directive(HighlightDirective)); | |
// Get the component's injected router navigation spy | |
const routerSpy = fixture.debugElement.injector.get(Router); | |
this.navSpy = routerSpy.navigate as jasmine.Spy; | |
}; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment