Skip to content
This repository was archived by the owner on Dec 4, 2017. It is now read-only.

Commit 4c71a32

Browse files
authored
docs(testing): import test module and override component providers (#2428)
1 parent 36a3ea2 commit 4c71a32

File tree

6 files changed

+346
-40
lines changed

6 files changed

+346
-40
lines changed

public/docs/_examples/testing/ts/app/hero/hero-detail.component.spec.ts

Lines changed: 174 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,37 +7,131 @@ import { By } from '@angular/platform-browser';
77
import { DebugElement } from '@angular/core';
88

99
import {
10-
addMatchers, newEvent,
11-
ActivatedRoute, ActivatedRouteStub, Router, RouterStub
10+
ActivatedRoute, ActivatedRouteStub, newEvent, Router, RouterStub
1211
} from '../../testing';
1312

14-
import { HEROES, FakeHeroService } from '../model/testing';
15-
16-
import { HeroModule } from './hero.module';
13+
import { Hero } from '../model';
1714
import { HeroDetailComponent } from './hero-detail.component';
1815
import { HeroDetailService } from './hero-detail.service';
19-
import { Hero, HeroService } from '../model';
16+
import { HeroModule } from './hero.module';
2017

2118
////// Testing Vars //////
2219
let activatedRoute: ActivatedRouteStub;
2320
let comp: HeroDetailComponent;
2421
let fixture: ComponentFixture<HeroDetailComponent>;
2522
let page: Page;
2623

27-
////////// Tests ////////////////////
28-
24+
////// Tests //////
2925
describe('HeroDetailComponent', () => {
30-
31-
beforeEach( async(() => {
32-
addMatchers();
26+
beforeEach(() => {
3327
activatedRoute = new ActivatedRouteStub();
28+
});
29+
describe('with HeroModule setup', heroModuleSetup);
30+
describe('when override its provided HeroDetailService', overrideSetup);
31+
describe('with FormsModule setup', formsModuleSetup);
32+
describe('with SharedModule setup', sharedModuleSetup);
33+
});
3434

35+
////////////////////
36+
function overrideSetup() {
37+
// #docregion stub-hds
38+
class StubHeroDetailService {
39+
testHero = new Hero(42, 'Test Hero');
40+
41+
getHero(id: number | string): Promise<Hero> {
42+
return Promise.resolve(true).then(() => Object.assign({}, this.testHero) );
43+
}
44+
45+
saveHero(hero: Hero): Promise<Hero> {
46+
return Promise.resolve(true).then(() => Object.assign(this.testHero, hero) );
47+
}
48+
}
49+
// #enddocregion stub-hds
50+
51+
// the `id` value is irrelevant because ignored by service stub
52+
beforeEach(() => activatedRoute.testParams = { id: 99999 } );
53+
54+
// #docregion setup-override
55+
beforeEach( async(() => {
3556
TestBed.configureTestingModule({
36-
imports: [ HeroModule ],
57+
imports: [ HeroModule ],
58+
providers: [
59+
{ provide: ActivatedRoute, useValue: activatedRoute },
60+
{ provide: Router, useClass: RouterStub},
61+
// #enddocregion setup-override
62+
// HeroDetailService at this level is IRRELEVANT!
63+
{ provide: HeroDetailService, useValue: {} }
64+
// #docregion setup-override
65+
]
66+
})
67+
68+
// Override component's own provider
69+
// #docregion override-component-method
70+
.overrideComponent(HeroDetailComponent, {
71+
set: {
72+
providers: [
73+
{ provide: HeroDetailService, useClass: StubHeroDetailService }
74+
]
75+
}
76+
})
77+
// #enddocregion override-component-method
78+
79+
.compileComponents();
80+
}));
81+
// #enddocregion setup-override
82+
83+
// #docregion override-tests
84+
let hds: StubHeroDetailService;
85+
86+
beforeEach( async(() => {
87+
createComponent();
88+
// get the component's injected StubHeroDetailService
89+
hds = fixture.debugElement.injector.get(HeroDetailService);
90+
}));
91+
92+
it('should display stub hero\'s name', () => {
93+
expect(page.nameDisplay.textContent).toBe(hds.testHero.name);
94+
});
95+
96+
it('should save stub hero change', fakeAsync(() => {
97+
const origName = hds.testHero.name;
98+
const newName = 'New Name';
99+
100+
page.nameInput.value = newName;
101+
page.nameInput.dispatchEvent(newEvent('input')); // tell Angular
102+
103+
expect(comp.hero.name).toBe(newName, 'component hero has new name');
104+
expect(hds.testHero.name).toBe(origName, 'service hero unchanged before save');
105+
106+
page.saveBtn.triggerEventHandler('click', null);
107+
tick(); // wait for async save to complete
108+
expect(hds.testHero.name).toBe(newName, 'service hero has new name after save');
109+
expect(page.navSpy.calls.any()).toBe(true, 'router.navigate called');
110+
}));
111+
// #enddocregion override-tests
112+
113+
it('fixture injected service is not the component injected service',
114+
inject([HeroDetailService], (service: HeroDetailService) => {
115+
116+
expect(service).toEqual({}, 'service injected from fixture');
117+
expect(hds).toBeTruthy('service injected into component');
118+
}));
119+
}
37120

38-
// DON'T RE-DECLARE because already declared in HeroModule
39-
// declarations: [HeroDetailComponent, TitleCasePipe], // No!
121+
////////////////////
122+
import { HEROES, FakeHeroService } from '../model/testing';
123+
import { HeroService } from '../model';
124+
125+
const firstHero = HEROES[0];
40126

127+
function heroModuleSetup() {
128+
// #docregion setup-hero-module
129+
beforeEach( async(() => {
130+
TestBed.configureTestingModule({
131+
imports: [ HeroModule ],
132+
// #enddocregion setup-hero-module
133+
// declarations: [ HeroDetailComponent ], // NO! DOUBLE DECLARATION
134+
// #docregion setup-hero-module
41135
providers: [
42136
{ provide: ActivatedRoute, useValue: activatedRoute },
43137
{ provide: HeroService, useClass: FakeHeroService },
@@ -46,13 +140,14 @@ describe('HeroDetailComponent', () => {
46140
})
47141
.compileComponents();
48142
}));
143+
// #enddocregion setup-hero-module
49144

50145
// #docregion route-good-id
51-
describe('when navigate to hero id=' + HEROES[0].id, () => {
146+
describe('when navigate to existing hero', () => {
52147
let expectedHero: Hero;
53148

54149
beforeEach( async(() => {
55-
expectedHero = HEROES[0];
150+
expectedHero = firstHero;
56151
activatedRoute.testParams = { id: expectedHero.id };
57152
createComponent();
58153
}));
@@ -76,7 +171,7 @@ describe('HeroDetailComponent', () => {
76171

77172
it('should navigate when click save and save resolves', fakeAsync(() => {
78173
page.saveBtn.triggerEventHandler('click', null);
79-
tick(); // wait for async save to "complete" before navigating
174+
tick(); // wait for async save to complete
80175
expect(page.navSpy.calls.any()).toBe(true, 'router.navigate called');
81176
}));
82177

@@ -91,8 +186,7 @@ describe('HeroDetailComponent', () => {
91186
// dispatch a DOM event so that Angular learns of input value change.
92187
page.nameInput.dispatchEvent(newEvent('input'));
93188

94-
// detectChanges() makes [(ngModel)] push input value to component property
95-
// and Angular updates the output span through the title pipe
189+
// Tell Angular to update the output span through the title pipe
96190
fixture.detectChanges();
97191

98192
expect(page.nameDisplay.textContent).toBe(titleCaseName);
@@ -131,10 +225,8 @@ describe('HeroDetailComponent', () => {
131225
});
132226
// #enddocregion route-bad-id
133227

134-
///////////////////////////
135-
136228
// Why we must use `fixture.debugElement.injector` in `Page()`
137-
it('cannot use `inject` to get component\'s provided service', () => {
229+
it('cannot use `inject` to get component\'s provided HeroDetailService', () => {
138230
let service: HeroDetailService;
139231
fixture = TestBed.createComponent(HeroDetailComponent);
140232
expect(
@@ -148,7 +240,64 @@ describe('HeroDetailComponent', () => {
148240
service = fixture.debugElement.injector.get(HeroDetailService);
149241
expect(service).toBeDefined('debugElement.injector');
150242
});
151-
});
243+
}
244+
245+
/////////////////////
246+
import { FormsModule } from '@angular/forms';
247+
import { TitleCasePipe } from '../shared/title-case.pipe';
248+
249+
function formsModuleSetup() {
250+
// #docregion setup-forms-module
251+
beforeEach( async(() => {
252+
TestBed.configureTestingModule({
253+
imports: [ FormsModule ],
254+
declarations: [ HeroDetailComponent, TitleCasePipe ],
255+
providers: [
256+
{ provide: ActivatedRoute, useValue: activatedRoute },
257+
{ provide: HeroService, useClass: FakeHeroService },
258+
{ provide: Router, useClass: RouterStub},
259+
]
260+
})
261+
.compileComponents();
262+
}));
263+
// #enddocregion setup-forms-module
264+
265+
it('should display 1st hero\'s name', fakeAsync(() => {
266+
const expectedHero = firstHero;
267+
activatedRoute.testParams = { id: expectedHero.id };
268+
createComponent().then(() => {
269+
expect(page.nameDisplay.textContent).toBe(expectedHero.name);
270+
});
271+
}));
272+
}
273+
274+
///////////////////////
275+
import { SharedModule } from '../shared/shared.module';
276+
277+
function sharedModuleSetup() {
278+
// #docregion setup-shared-module
279+
beforeEach( async(() => {
280+
TestBed.configureTestingModule({
281+
imports: [ SharedModule ],
282+
declarations: [ HeroDetailComponent ],
283+
providers: [
284+
{ provide: ActivatedRoute, useValue: activatedRoute },
285+
{ provide: HeroService, useClass: FakeHeroService },
286+
{ provide: Router, useClass: RouterStub},
287+
]
288+
})
289+
.compileComponents();
290+
}));
291+
// #enddocregion setup-shared-module
292+
293+
it('should display 1st hero\'s name', fakeAsync(() => {
294+
const expectedHero = firstHero;
295+
activatedRoute.testParams = { id: expectedHero.id };
296+
createComponent().then(() => {
297+
expect(page.nameDisplay.textContent).toBe(expectedHero.name);
298+
});
299+
}));
300+
}
152301

153302
/////////// Helpers /////
154303

@@ -185,9 +334,10 @@ class Page {
185334
const compInjector = fixture.debugElement.injector;
186335
const hds = compInjector.get(HeroDetailService);
187336
const router = compInjector.get(Router);
337+
188338
this.gotoSpy = spyOn(comp, 'gotoList').and.callThrough();
189-
this.saveSpy = spyOn(hds, 'saveHero').and.callThrough();
190339
this.navSpy = spyOn(router, 'navigate');
340+
this.saveSpy = spyOn(hds, 'saveHero').and.callThrough();
191341
}
192342

193343
/** Add page elements after hero arrives */

public/docs/_examples/testing/ts/app/hero/hero-detail.component.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,30 @@
1+
/* tslint:disable:member-ordering */
2+
// #docplaster
13
import { Component, Input, OnInit } from '@angular/core';
24
import { ActivatedRoute, Router } from '@angular/router';
35
import 'rxjs/add/operator/pluck';
46

57
import { Hero } from '../model';
68
import { HeroDetailService } from './hero-detail.service';
79

10+
// #docregion prototype
811
@Component({
9-
selector: 'app-hero-detail',
12+
selector: 'app-hero-detail',
1013
templateUrl: 'app/hero/hero-detail.component.html',
1114
styleUrls: ['app/hero/hero-detail.component.css'],
1215
providers: [ HeroDetailService ]
1316
})
1417
export class HeroDetailComponent implements OnInit {
15-
@Input() hero: Hero;
16-
1718
// #docregion ctor
1819
constructor(
1920
private heroDetailService: HeroDetailService,
20-
private route: ActivatedRoute,
21+
private route: ActivatedRoute,
2122
private router: Router) {
2223
}
2324
// #enddocregion ctor
25+
// #enddocregion prototype
26+
27+
@Input() hero: Hero;
2428

2529
// #docregion ng-on-init
2630
ngOnInit(): void {
@@ -50,4 +54,6 @@ export class HeroDetailComponent implements OnInit {
5054
gotoList() {
5155
this.router.navigate(['../'], {relativeTo: this.route});
5256
}
57+
// #docregion prototype
5358
}
59+
// #enddocregion prototype

public/docs/_examples/testing/ts/app/hero/hero-detail.service.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@ import { Injectable } from '@angular/core';
22

33
import { Hero, HeroService } from '../model';
44

5+
// #docregion prototype
56
@Injectable()
67
export class HeroDetailService {
78
constructor(private heroService: HeroService) { }
9+
// #enddocregion prototype
810

11+
// Returns a clone which caller may modify safely
912
getHero(id: number | string): Promise<Hero> {
1013
if (typeof id === 'string') {
1114
id = parseInt(id as string, 10);
@@ -18,4 +21,6 @@ export class HeroDetailService {
1821
saveHero(hero: Hero) {
1922
return this.heroService.updateHero(hero);
2023
}
24+
// #docregion prototype
2125
}
26+
// #enddocregion prototype

public/docs/_examples/testing/ts/app/model/hero.service.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Hero } from './hero';
44
import { HEROES } from './test-heroes';
55

66
@Injectable()
7-
/** Dummy HeroService that pretends to be real */
7+
/** Dummy HeroService. Pretend it makes real http requests */
88
export class HeroService {
99
getHeroes() {
1010
return Promise.resolve(HEROES);
@@ -21,9 +21,10 @@ export class HeroService {
2121

2222
updateHero(hero: Hero): Promise<Hero> {
2323
return this.getHero(hero.id).then(h => {
24-
return h ?
25-
Object.assign(h, hero) :
26-
Promise.reject(`Hero ${hero.id} not found`) as any as Promise<Hero>;
24+
if (!h) {
25+
throw new Error(`Hero ${hero.id} not found`);
26+
}
27+
return Object.assign(h, hero);
2728
});
2829
}
2930
}

public/docs/_examples/testing/ts/browser-test-shim.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Error.stackTraceLimit = 0; // "No stacktrace"" is usually best for app testing.
99
// Uncomment to get full stacktrace output. Sometimes helpful, usually not.
1010
// Error.stackTraceLimit = Infinity; //
1111

12-
jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000;
12+
jasmine.DEFAULT_TIMEOUT_INTERVAL = 3000;
1313

1414
var baseURL = document.baseURI;
1515
baseURL = baseURL + baseURL[baseURL.length-1] ? '' : '/';

0 commit comments

Comments
 (0)