docs: testing guide for CLI (#20697)

- updates tests
- heavy prose revisions
- uses HttpClient (with angular-in-memory-web-api)
- test HeroService using `HttpClientTestingModule`
- scrub away most By.CSS
- fake async observable with `asyncData()`
- extensive Twain work
- different take on retryWhen
- remove app barrels (& systemjs.extras) which troubled plunker/systemjs
- add dummy export const to hero.ts (plunkr/systemjs fails w/o it)
- shrink and re-organize TOC
- add marble testing package and tests
- demonstrate the "no beforeEach()" test coding style
- add section on Http service testing
- prepare for stackblitz
- confirm works in plunker except excluded marble test
- add tests for avoidFile class feature of CodeExampleComponent

PR Close #20697
This commit is contained in:
Ward Bell 2017-10-27 15:48:50 -07:00 committed by Alex Eagle
parent 1f599818bd
commit a7e1f236ff
88 changed files with 4831 additions and 3974 deletions

View File

@ -1,5 +1,5 @@
{
"description": "Testing - app.specs",
"description": "Testing - specs",
"files":[
"src/styles.css",

View File

@ -1,5 +0,0 @@
// #docplaster
// #docregion
describe('1st tests', () => {
it('true is true', () => expect(true).toBe(true));
});

View File

@ -1,9 +1,8 @@
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { AboutComponent } from './about.component';
import { HighlightDirective } from './shared/highlight.directive';
import { HighlightDirective } from '../shared/highlight.directive';
let fixture: ComponentFixture<AboutComponent>;
@ -19,8 +18,8 @@ describe('AboutComponent (highlightDirective)', () => {
});
it('should have skyblue <h2>', () => {
const de = fixture.debugElement.query(By.css('h2'));
const bgColor = de.nativeElement.style.backgroundColor;
const h2: HTMLElement = fixture.nativeElement.querySelector('h2');
const bgColor = h2.style.backgroundColor;
expect(bgColor).toBe('skyblue');
});
// #enddocregion tests

View File

@ -3,7 +3,8 @@ import { Component } from '@angular/core';
@Component({
template: `
<h2 highlight="skyblue">About</h2>
<h3>Quote of the day:</h3>
<twain-quote></twain-quote>
<p>All about this sample</p>`
`
})
export class AboutComponent { }

View File

@ -0,0 +1,76 @@
// #docplaster
// #docregion
import { TestBed, async } from '@angular/core/testing';
// #enddocregion
import { AppComponent } from './app-initial.component';
/*
// #docregion
import { AppComponent } from './app.component';
describe('AppComponent', () => {
// #enddocregion
*/
describe('AppComponent (initial CLI version)', () => {
// #docregion
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent
],
}).compileComponents();
}));
it('should create the app', async(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
}));
it(`should have as title 'app'`, async(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app.title).toEqual('app');
}));
it('should render title in a h1 tag', async(() => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement;
expect(compiled.querySelector('h1').textContent).toContain('Welcome to app!');
}));
});
// #enddocregion
/// As it should be
import { DebugElement } from '@angular/core';
import { ComponentFixture } from '@angular/core/testing';
describe('AppComponent (initial CLI version - as it should be)', () => {
let app: AppComponent;
let de: DebugElement;
let fixture: ComponentFixture<AppComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent
],
});
fixture = TestBed.createComponent(AppComponent);
app = fixture.componentInstance;
de = fixture.debugElement;
});
it('should create the app', () => {
expect(app).toBeDefined();
});
it(`should have as title 'app'`, () => {
expect(app.title).toEqual('app');
});
it('should render title in an h1 tag', () => {
fixture.detectChanges();
expect(de.nativeElement.querySelector('h1').textContent)
.toContain('Welcome to app!');
});
});

View File

@ -0,0 +1,11 @@
// #docregion
// Reduced version of the initial AppComponent generated by CLI
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: '<h1>Welcome to {{title}}!</h1>'
})
export class AppComponent {
title = 'app';
}

View File

@ -1,7 +1,7 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { AboutComponent } from './about.component';
import { AboutComponent } from './about/about.component';
@NgModule({
imports: [

View File

@ -1,11 +1,11 @@
<!-- #docregion -->
<app-banner></app-banner>
<app-welcome></app-welcome>
<!-- #docregion links -->
<nav>
<a routerLink="/dashboard">Dashboard</a>
<a routerLink="/heroes">Heroes</a>
<a routerLink="/about">About</a>
</nav>
<!-- #enddocregion links -->
<router-outlet></router-outlet>

View File

@ -4,11 +4,11 @@
import { async, ComponentFixture, fakeAsync, TestBed, tick,
} from '@angular/core/testing';
import { asyncData } from '../testing';
import { RouterTestingModule } from '@angular/router/testing';
import { SpyLocation } from '@angular/common/testing';
import { click } from '../testing';
// r - for relatively obscure router symbols
import * as r from '@angular/router';
import { Router, RouterLinkWithHref } from '@angular/router';
@ -17,11 +17,15 @@ import { By } from '@angular/platform-browser';
import { DebugElement, Type } from '@angular/core';
import { Location } from '@angular/common';
import { click } from '../testing';
import { AppModule } from './app.module';
import { AppComponent } from './app.component';
import { AboutComponent } from './about.component';
import { AboutComponent } from './about/about.component';
import { DashboardComponent } from './dashboard/dashboard.component';
import { TwainService } from './shared/twain.service';
import { TwainService } from './twain/twain.service';
import { HeroService, TestHeroService } from './model/testing/test-hero.service';
let comp: AppComponent;
let fixture: ComponentFixture<AppComponent>;
@ -31,15 +35,19 @@ let location: SpyLocation;
describe('AppComponent & RouterTestingModule', () => {
beforeEach( async(() => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [ AppModule, RouterTestingModule ]
imports: [ AppModule, RouterTestingModule ],
providers: [
{ provide: HeroService, useClass: TestHeroService }
]
})
.compileComponents();
}));
it('should navigate to "Dashboard" immediately', fakeAsync(() => {
createComponent();
tick(); // wait for async data to arrive
expect(location.path()).toEqual('/dashboard', 'after initialNavigation()');
expectElementOf(DashboardComponent);
}));
@ -64,7 +72,7 @@ describe('AppComponent & RouterTestingModule', () => {
}));
// Can't navigate to lazy loaded modules with this technique
xit('should navigate to "Heroes" on click', fakeAsync(() => {
xit('should navigate to "Heroes" on click (not working yet)', fakeAsync(() => {
createComponent();
page.heroesLinkDe.nativeElement.click();
advance();
@ -84,9 +92,9 @@ import { HeroListComponent } from './hero/hero-list.component';
let loader: SpyNgModuleFactoryLoader;
///////// Can't get lazy loaded Heroes to work yet
xdescribe('AppComponent & Lazy Loading', () => {
xdescribe('AppComponent & Lazy Loading (not working yet)', () => {
beforeEach( async(() => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [ AppModule, RouterTestingModule ]
})
@ -95,14 +103,11 @@ xdescribe('AppComponent & Lazy Loading', () => {
beforeEach(fakeAsync(() => {
createComponent();
loader = TestBed.get(NgModuleFactoryLoader);
loader.stubbedModules = {expected: HeroModule};
loader = TestBed.get(NgModuleFactoryLoader);
loader.stubbedModules = { expected: HeroModule };
router.resetConfig([{path: 'heroes', loadChildren: 'expected'}]);
}));
it('dummy', () => expect(true).toBe(true) );
it('should navigate to "Heroes" on click', async(() => {
page.heroesLinkDe.nativeElement.click();
advance();
@ -110,25 +115,24 @@ xdescribe('AppComponent & Lazy Loading', () => {
expectElementOf(HeroListComponent);
}));
xit('can navigate to "Heroes" w/ browser location URL change', fakeAsync(() => {
it('can navigate to "Heroes" w/ browser location URL change', fakeAsync(() => {
location.go('/heroes');
advance();
expectPathToBe('/heroes');
expectElementOf(HeroListComponent);
page.expectEvents([
[r.NavigationStart, '/heroes'], [r.RoutesRecognized, '/heroes'],
[r.NavigationEnd, '/heroes']
]);
}));
});
////// Helpers /////////
/** Wait a tick, then detect changes */
/**
* Advance to the routed page
* Wait a tick, then detect changes, and tick again
*/
function advance(): void {
tick();
fixture.detectChanges();
tick(); // wait while navigating
fixture.detectChanges(); // update view
tick(); // wait for async data to arrive
}
function createComponent() {
@ -140,8 +144,8 @@ function createComponent() {
router = injector.get(Router);
router.initialNavigation();
spyOn(injector.get(TwainService), 'getQuote')
.and.returnValue(Promise.resolve('Test Quote')); // fakes it
// fake fast async observable
.and.returnValue(asyncData('Test Quote'));
advance();
page = new Page();
@ -151,7 +155,6 @@ class Page {
aboutLinkDe: DebugElement;
dashboardLinkDe: DebugElement;
heroesLinkDe: DebugElement;
recordedEvents: any[] = [];
// for debugging
comp: AppComponent;
@ -159,17 +162,7 @@ class Page {
router: Router;
fixture: ComponentFixture<AppComponent>;
expectEvents(pairs: any[]) {
const events = this.recordedEvents;
expect(events.length).toEqual(pairs.length, 'actual/expected events length mismatch');
for (let i = 0; i < events.length; ++i) {
expect((<any>events[i].constructor).name).toBe(pairs[i][0].name, 'unexpected event name');
expect((<any>events[i]).url).toBe(pairs[i][1], 'unexpected event url');
}
}
constructor() {
router.events.subscribe(e => this.recordedEvents.push(e));
const links = fixture.debugElement.queryAll(By.directive(RouterLinkWithHref));
this.aboutLinkDe = links[2];
this.dashboardLinkDe = links[0];

View File

@ -1,69 +1,67 @@
// #docplaster
import { async, ComponentFixture, TestBed
} from '@angular/core/testing';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { DebugElement } from '@angular/core';
import { Component, DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
import { By } from '@angular/platform-browser';
// #docregion setup-schemas
import { NO_ERRORS_SCHEMA } from '@angular/core';
// #enddocregion setup-schemas
// #docregion setup-stubs-w-imports
import { Component } from '@angular/core';
// #docregion setup-schemas
import { AppComponent } from './app.component';
// #enddocregion setup-schemas
import { BannerComponent } from './banner.component';
import { RouterLinkStubDirective } from '../testing';
// #docregion setup-schemas
import { RouterOutletStubComponent } from '../testing';
import { AppComponent } from './app.component';
import { RouterLinkDirectiveStub } from '../testing';
// #enddocregion setup-schemas
@Component({selector: 'app-welcome', template: ''})
class WelcomeStubComponent {}
// #docregion component-stubs
@Component({selector: 'app-banner', template: ''})
class BannerStubComponent {}
// #enddocregion setup-stubs-w-imports
@Component({selector: 'router-outlet', template: ''})
class RouterOutletStubComponent { }
@Component({selector: 'app-welcome', template: ''})
class WelcomeStubComponent {}
// #enddocregion component-stubs
let comp: AppComponent;
let fixture: ComponentFixture<AppComponent>;
describe('AppComponent & TestModule', () => {
// #docregion setup-stubs, setup-stubs-w-imports
beforeEach( async(() => {
beforeEach(async(() => {
// #docregion testbed-stubs
TestBed.configureTestingModule({
declarations: [
AppComponent,
BannerComponent, WelcomeStubComponent,
RouterLinkStubDirective, RouterOutletStubComponent
RouterLinkDirectiveStub,
BannerStubComponent,
RouterOutletStubComponent,
WelcomeStubComponent
]
})
.compileComponents()
.then(() => {
// #enddocregion testbed-stubs
.compileComponents().then(() => {
fixture = TestBed.createComponent(AppComponent);
comp = fixture.componentInstance;
});
}));
// #enddocregion setup-stubs, setup-stubs-w-imports
tests();
});
//////// Testing w/ NO_ERRORS_SCHEMA //////
describe('AppComponent & NO_ERRORS_SCHEMA', () => {
// #docregion setup-schemas
beforeEach( async(() => {
beforeEach(async(() => {
// #docregion no-errors-schema, mixed-setup
TestBed.configureTestingModule({
declarations: [ AppComponent, RouterLinkStubDirective ],
schemas: [ NO_ERRORS_SCHEMA ]
declarations: [
AppComponent,
// #enddocregion no-errors-schema
BannerStubComponent,
// #docregion no-errors-schema
RouterLinkDirectiveStub
],
schemas: [ NO_ERRORS_SCHEMA ]
})
.compileComponents()
.then(() => {
// #enddocregion no-errors-schema, mixed-setup
.compileComponents().then(() => {
fixture = TestBed.createComponent(AppComponent);
comp = fixture.componentInstance;
});
}));
// #enddocregion setup-schemas
tests();
});
@ -75,7 +73,7 @@ import { AppRoutingModule } from './app-routing.module';
describe('AppComponent & AppModule', () => {
beforeEach( async(() => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [ AppModule ]
@ -88,7 +86,7 @@ describe('AppComponent & AppModule', () => {
imports: [ AppRoutingModule ]
},
add: {
declarations: [ RouterLinkStubDirective, RouterOutletStubComponent ]
declarations: [ RouterLinkDirectiveStub, RouterOutletStubComponent ]
}
})
@ -104,40 +102,40 @@ describe('AppComponent & AppModule', () => {
});
function tests() {
let links: RouterLinkStubDirective[];
let routerLinks: RouterLinkDirectiveStub[];
let linkDes: DebugElement[];
// #docregion test-setup
beforeEach(() => {
// trigger initial data binding
fixture.detectChanges();
fixture.detectChanges(); // trigger initial data binding
// find DebugElements with an attached RouterLinkStubDirective
linkDes = fixture.debugElement
.queryAll(By.directive(RouterLinkStubDirective));
.queryAll(By.directive(RouterLinkDirectiveStub));
// get the attached link directive instances using the DebugElement injectors
links = linkDes
.map(de => de.injector.get(RouterLinkStubDirective) as RouterLinkStubDirective);
// get attached link directive instances
// using each DebugElement's injector
routerLinks = linkDes.map(de => de.injector.get(RouterLinkDirectiveStub));
});
// #enddocregion test-setup
it('can instantiate it', () => {
it('can instantiate the component', () => {
expect(comp).not.toBeNull();
});
// #docregion tests
it('can get RouterLinks from template', () => {
expect(links.length).toBe(3, 'should have 3 links');
expect(links[0].linkParams).toBe('/dashboard', '1st link should go to Dashboard');
expect(links[1].linkParams).toBe('/heroes', '2nd link should go to Heroes');
expect(routerLinks.length).toBe(3, 'should have 3 routerLinks');
expect(routerLinks[0].linkParams).toBe('/dashboard');
expect(routerLinks[1].linkParams).toBe('/heroes');
expect(routerLinks[2].linkParams).toBe('/about');
});
it('can click Heroes link in template', () => {
const heroesLinkDe = linkDes[1];
const heroesLink = links[1];
const heroesLinkDe = linkDes[1]; // heroes link DebugElement
const heroesLink = routerLinks[1]; // heroes link directive
expect(heroesLink.navigatedTo).toBeNull('link should not have navigated yet');
expect(heroesLink.navigatedTo).toBeNull('should not have navigated yet');
heroesLinkDe.triggerEventHandler('click', null);
fixture.detectChanges();

View File

@ -1,29 +1,50 @@
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { AboutComponent } from './about.component';
import { BannerComponent } from './banner.component';
import { AboutComponent } from './about/about.component';
import { BannerComponent } from './banner/banner.component';
import { HeroService } from './model/hero.service';
import { UserService } from './model/user.service';
import { HeroService } from './model/hero.service';
import { TwainService } from './shared/twain.service';
import { WelcomeComponent } from './welcome.component';
import { TwainComponent } from './twain/twain.component';
import { TwainService } from './twain/twain.service';
import { WelcomeComponent } from './welcome/welcome.component';
import { DashboardModule } from './dashboard/dashboard.module';
import { SharedModule } from './shared/shared.module';
import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
import { InMemoryDataService } from './in-memory-data.service';
@NgModule({
imports: [
BrowserModule,
DashboardModule,
AppRoutingModule,
SharedModule
SharedModule,
HttpClientModule,
// The HttpClientInMemoryWebApiModule module intercepts HTTP requests
// and returns simulated server responses.
// Remove it when a real server is ready to receive requests.
HttpClientInMemoryWebApiModule.forRoot(
InMemoryDataService, { dataEncapsulation: false }
)
],
providers: [ HeroService, TwainService, UserService ],
declarations: [ AppComponent, AboutComponent, BannerComponent, WelcomeComponent ],
bootstrap: [ AppComponent ]
providers: [
HeroService,
TwainService,
UserService
],
declarations: [
AppComponent,
AboutComponent,
BannerComponent,
TwainComponent,
WelcomeComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule { }

View File

@ -1,130 +0,0 @@
// #docplaster
import { DependentService, FancyService } from './bag';
///////// Fakes /////////
export class FakeFancyService extends FancyService {
value = 'faked value';
}
////////////////////////
// #docregion FancyService
// Straight Jasmine - no imports from Angular test libraries
describe('FancyService without the TestBed', () => {
let service: FancyService;
beforeEach(() => { service = new FancyService(); });
it('#getValue should return real value', () => {
expect(service.getValue()).toBe('real value');
});
it('#getAsyncValue should return async value', (done: DoneFn) => {
service.getAsyncValue().then(value => {
expect(value).toBe('async value');
done();
});
});
// #docregion getTimeoutValue
it('#getTimeoutValue should return timeout value', (done: DoneFn) => {
service = new FancyService();
service.getTimeoutValue().then(value => {
expect(value).toBe('timeout value');
done();
});
});
// #enddocregion getTimeoutValue
it('#getObservableValue should return observable value', (done: DoneFn) => {
service.getObservableValue().subscribe(value => {
expect(value).toBe('observable value');
done();
});
});
});
// #enddocregion FancyService
// DependentService requires injection of a FancyService
// #docregion DependentService
describe('DependentService without the TestBed', () => {
let service: DependentService;
it('#getValue should return real value by way of the real FancyService', () => {
service = new DependentService(new FancyService());
expect(service.getValue()).toBe('real value');
});
it('#getValue should return faked value by way of a fakeService', () => {
service = new DependentService(new FakeFancyService());
expect(service.getValue()).toBe('faked value');
});
it('#getValue should return faked value from a fake object', () => {
const fake = { getValue: () => 'fake value' };
service = new DependentService(fake as FancyService);
expect(service.getValue()).toBe('fake value');
});
it('#getValue should return stubbed value from a FancyService spy', () => {
const fancy = new FancyService();
const stubValue = 'stub value';
const spy = spyOn(fancy, 'getValue').and.returnValue(stubValue);
service = new DependentService(fancy);
expect(service.getValue()).toBe(stubValue, 'service returned stub value');
expect(spy.calls.count()).toBe(1, 'stubbed method was called once');
expect(spy.calls.mostRecent().returnValue).toBe(stubValue);
});
});
// #enddocregion DependentService
// #docregion ReversePipe
import { ReversePipe } from './bag';
describe('ReversePipe', () => {
let pipe: ReversePipe;
beforeEach(() => { pipe = new ReversePipe(); });
it('transforms "abc" to "cba"', () => {
expect(pipe.transform('abc')).toBe('cba');
});
it('no change to palindrome: "able was I ere I saw elba"', () => {
const palindrome = 'able was I ere I saw elba';
expect(pipe.transform(palindrome)).toBe(palindrome);
});
});
// #enddocregion ReversePipe
import { ButtonComponent } from './bag';
// #docregion ButtonComp
describe('ButtonComp', () => {
let comp: ButtonComponent;
beforeEach(() => comp = new ButtonComponent());
it('#isOn should be false initially', () => {
expect(comp.isOn).toBe(false);
});
it('#clicked() should set #isOn to true', () => {
comp.clicked();
expect(comp.isOn).toBe(true);
});
it('#clicked() should set #message to "is on"', () => {
comp.clicked();
expect(comp.message).toMatch(/is on/i);
});
it('#clicked() should toggle #isOn', () => {
comp.clicked();
expect(comp.isOn).toBe(true);
comp.clicked();
expect(comp.isOn).toBe(false);
});
});
// #enddocregion ButtonComp

View File

@ -1,681 +0,0 @@
// #docplaster
import {
BagModule,
BankAccountComponent, BankAccountParentComponent,
ButtonComponent,
Child1Component, Child2Component, Child3Component,
FancyService,
ExternalTemplateComponent,
InputComponent,
IoComponent, IoParentComponent,
MyIfComponent, MyIfChildComponent, MyIfParentComponent,
NeedsContentComponent, ParentComponent,
TestProvidersComponent, TestViewProvidersComponent,
ReversePipeComponent, ShellComponent
} from './bag';
import { By } from '@angular/platform-browser';
import { Component,
DebugElement,
Injectable } from '@angular/core';
import { FormsModule } from '@angular/forms';
// Forms symbols imported only for a specific test below
import { NgModel, NgControl } from '@angular/forms';
import { async, ComponentFixture, fakeAsync, inject, TestBed, tick
} from '@angular/core/testing';
import { addMatchers, newEvent, click } from '../../testing';
beforeEach( addMatchers );
//////// Service Tests /////////////
// #docregion FancyService
describe('use inject helper in beforeEach', () => {
let service: FancyService;
beforeEach(() => {
TestBed.configureTestingModule({ providers: [FancyService] });
// `TestBed.get` returns the injectable or an
// alternative object (including null) if the service provider is not found.
// Of course it will be found in this case because we're providing it.
// #docregion testbed-get
service = TestBed.get(FancyService, null);
// #enddocregion testbed-get
});
it('should use FancyService', () => {
expect(service.getValue()).toBe('real value');
});
it('should use FancyService', () => {
expect(service.getValue()).toBe('real value');
});
it('test should wait for FancyService.getAsyncValue', async(() => {
service.getAsyncValue().then(
value => expect(value).toBe('async value')
);
}));
it('test should wait for FancyService.getTimeoutValue', async(() => {
service.getTimeoutValue().then(
value => expect(value).toBe('timeout value')
);
}));
it('test should wait for FancyService.getObservableValue', async(() => {
service.getObservableValue().subscribe(
value => expect(value).toBe('observable value')
);
}));
// Must use done. See https://github.com/angular/angular/issues/10127
it('test should wait for FancyService.getObservableDelayValue', (done: DoneFn) => {
service.getObservableDelayValue().subscribe(value => {
expect(value).toBe('observable delay value');
done();
});
});
it('should allow the use of fakeAsync', fakeAsync(() => {
let value: any;
service.getAsyncValue().then((val: any) => value = val);
tick(); // Trigger JS engine cycle until all promises resolve.
expect(value).toBe('async value');
}));
});
// #enddocregion FancyService
describe('use inject within `it`', () => {
// #docregion getTimeoutValue
beforeEach(() => {
TestBed.configureTestingModule({ providers: [FancyService] });
});
// #enddocregion getTimeoutValue
it('should use modified providers',
inject([FancyService], (service: FancyService) => {
service.setValue('value modified in beforeEach');
expect(service.getValue()).toBe('value modified in beforeEach');
})
);
// #docregion getTimeoutValue
it('test should wait for FancyService.getTimeoutValue',
async(inject([FancyService], (service: FancyService) => {
service.getTimeoutValue().then(
value => expect(value).toBe('timeout value')
);
})));
// #enddocregion getTimeoutValue
});
describe('using async(inject) within beforeEach', () => {
let serviceValue: string;
beforeEach(() => {
TestBed.configureTestingModule({ providers: [FancyService] });
});
beforeEach( async(inject([FancyService], (service: FancyService) => {
service.getAsyncValue().then(value => serviceValue = value);
})));
it('should use asynchronously modified value ... in synchronous test', () => {
expect(serviceValue).toBe('async value');
});
});
/////////// Component Tests //////////////////
describe('TestBed Component Tests', () => {
beforeEach( async(() => {
TestBed
.configureTestingModule({
imports: [BagModule],
})
// Compile everything in BagModule
.compileComponents();
}));
it('should create a component with inline template', () => {
const fixture = TestBed.createComponent(Child1Component);
fixture.detectChanges();
expect(fixture).toHaveText('Child');
});
it('should create a component with external template', () => {
const fixture = TestBed.createComponent(ExternalTemplateComponent);
fixture.detectChanges();
expect(fixture).toHaveText('from external template');
});
it('should allow changing members of the component', () => {
const fixture = TestBed.createComponent(MyIfComponent);
fixture.detectChanges();
expect(fixture).toHaveText('MyIf()');
fixture.componentInstance.showMore = true;
fixture.detectChanges();
expect(fixture).toHaveText('MyIf(More)');
});
it('should create a nested component bound to inputs/outputs', () => {
const fixture = TestBed.createComponent(IoParentComponent);
fixture.detectChanges();
const heroes = fixture.debugElement.queryAll(By.css('.hero'));
expect(heroes.length).toBeGreaterThan(0, 'has heroes');
const comp = fixture.componentInstance;
const hero = comp.heroes[0];
click(heroes[0]);
fixture.detectChanges();
const selected = fixture.debugElement.query(By.css('p'));
expect(selected).toHaveText(hero.name);
});
it('can access the instance variable of an `*ngFor` row component', () => {
const fixture = TestBed.createComponent(IoParentComponent);
const comp = fixture.componentInstance;
const heroName = comp.heroes[0].name; // first hero's name
fixture.detectChanges();
const ngForRow = fixture.debugElement.query(By.directive(IoComponent)); // first hero ngForRow
const hero = ngForRow.context['hero']; // the hero object passed into the row
expect(hero.name).toBe(heroName, 'ngRow.context.hero');
const rowComp = ngForRow.componentInstance;
// jasmine.any is an "instance-of-type" test.
expect(rowComp).toEqual(jasmine.any(IoComponent), 'component is IoComp');
expect(rowComp.hero.name).toBe(heroName, 'component.hero');
});
// #docregion ButtonComp
it('should support clicking a button', () => {
const fixture = TestBed.createComponent(ButtonComponent);
const btn = fixture.debugElement.query(By.css('button'));
const span = fixture.debugElement.query(By.css('span')).nativeElement;
fixture.detectChanges();
expect(span.textContent).toMatch(/is off/i, 'before click');
click(btn);
fixture.detectChanges();
expect(span.textContent).toMatch(/is on/i, 'after click');
});
// #enddocregion ButtonComp
// ngModel is async so we must wait for it with promise-based `whenStable`
it('should support entering text in input box (ngModel)', async(() => {
const expectedOrigName = 'John';
const expectedNewName = 'Sally';
const fixture = TestBed.createComponent(InputComponent);
fixture.detectChanges();
const comp = fixture.componentInstance;
const input = <HTMLInputElement> fixture.debugElement.query(By.css('input')).nativeElement;
expect(comp.name).toBe(expectedOrigName,
`At start name should be ${expectedOrigName} `);
// wait until ngModel binds comp.name to input box
fixture.whenStable().then(() => {
expect(input.value).toBe(expectedOrigName,
`After ngModel updates input box, input.value should be ${expectedOrigName} `);
// simulate user entering new name in input
input.value = expectedNewName;
// that change doesn't flow to the component immediately
expect(comp.name).toBe(expectedOrigName,
`comp.name should still be ${expectedOrigName} after value change, before binding happens`);
// dispatch a DOM event so that Angular learns of input value change.
// then wait while ngModel pushes input.box value to comp.name
input.dispatchEvent(newEvent('input'));
return fixture.whenStable();
})
.then(() => {
expect(comp.name).toBe(expectedNewName,
`After ngModel updates the model, comp.name should be ${expectedNewName} `);
});
}));
// fakeAsync version of ngModel input test enables sync test style
// synchronous `tick` replaces asynchronous promise-base `whenStable`
it('should support entering text in input box (ngModel) - fakeAsync', fakeAsync(() => {
const expectedOrigName = 'John';
const expectedNewName = 'Sally';
const fixture = TestBed.createComponent(InputComponent);
fixture.detectChanges();
const comp = fixture.componentInstance;
const input = <HTMLInputElement> fixture.debugElement.query(By.css('input')).nativeElement;
expect(comp.name).toBe(expectedOrigName,
`At start name should be ${expectedOrigName} `);
// wait until ngModel binds comp.name to input box
tick();
expect(input.value).toBe(expectedOrigName,
`After ngModel updates input box, input.value should be ${expectedOrigName} `);
// simulate user entering new name in input
input.value = expectedNewName;
// that change doesn't flow to the component immediately
expect(comp.name).toBe(expectedOrigName,
`comp.name should still be ${expectedOrigName} after value change, before binding happens`);
// dispatch a DOM event so that Angular learns of input value change.
// then wait a tick while ngModel pushes input.box value to comp.name
input.dispatchEvent(newEvent('input'));
tick();
expect(comp.name).toBe(expectedNewName,
`After ngModel updates the model, comp.name should be ${expectedNewName} `);
}));
// #docregion ReversePipeComp
it('ReversePipeComp should reverse the input text', fakeAsync(() => {
const inputText = 'the quick brown fox.';
const expectedText = '.xof nworb kciuq eht';
const fixture = TestBed.createComponent(ReversePipeComponent);
fixture.detectChanges();
const comp = fixture.componentInstance;
const input = fixture.debugElement.query(By.css('input')).nativeElement as HTMLInputElement;
const span = fixture.debugElement.query(By.css('span')).nativeElement as HTMLElement;
// simulate user entering new name in input
input.value = inputText;
// dispatch a DOM event so that Angular learns of input value change.
// then wait a tick while ngModel pushes input.box value to comp.text
// and Angular updates the output span
input.dispatchEvent(newEvent('input'));
tick();
fixture.detectChanges();
expect(span.textContent).toBe(expectedText, 'output span');
expect(comp.text).toBe(inputText, 'component.text');
}));
// #enddocregion ReversePipeComp
// Use this technique to find attached directives of any kind
it('can examine attached directives and listeners', () => {
const fixture = TestBed.createComponent(InputComponent);
fixture.detectChanges();
const inputEl = fixture.debugElement.query(By.css('input'));
expect(inputEl.providerTokens).toContain(NgModel, 'NgModel directive');
const ngControl = inputEl.injector.get(NgControl);
expect(ngControl).toEqual(jasmine.any(NgControl), 'NgControl directive');
expect(inputEl.listeners.length).toBeGreaterThan(2, 'several listeners attached');
});
// #docregion dom-attributes
it('BankAccountComponent should set attributes, styles, classes, and properties', () => {
const fixture = TestBed.createComponent(BankAccountParentComponent);
fixture.detectChanges();
const comp = fixture.componentInstance;
// the only child is debugElement of the BankAccount component
const el = fixture.debugElement.children[0];
const childComp = el.componentInstance as BankAccountComponent;
expect(childComp).toEqual(jasmine.any(BankAccountComponent));
expect(el.context).toBe(childComp, 'context is the child component');
expect(el.attributes['account']).toBe(childComp.id, 'account attribute');
expect(el.attributes['bank']).toBe(childComp.bank, 'bank attribute');
expect(el.classes['closed']).toBe(true, 'closed class');
expect(el.classes['open']).toBe(false, 'open class');
expect(el.styles['color']).toBe(comp.color, 'color style');
expect(el.styles['width']).toBe(comp.width + 'px', 'width style');
// #enddocregion dom-attributes
// Removed on 12/02/2016 when ceased public discussion of the `Renderer`. Revive in future?
// expect(el.properties['customProperty']).toBe(true, 'customProperty');
// #docregion dom-attributes
});
// #enddocregion dom-attributes
});
describe('TestBed Component Overrides:', () => {
it('should override ChildComp\'s template', () => {
const fixture = TestBed.configureTestingModule({
declarations: [Child1Component],
})
.overrideComponent(Child1Component, {
set: { template: '<span>Fake</span>' }
})
.createComponent(Child1Component);
fixture.detectChanges();
expect(fixture).toHaveText('Fake');
});
it('should override TestProvidersComp\'s FancyService provider', () => {
const fixture = TestBed.configureTestingModule({
declarations: [TestProvidersComponent],
})
.overrideComponent(TestProvidersComponent, {
remove: { providers: [FancyService]},
add: { providers: [{ provide: FancyService, useClass: FakeFancyService }] },
// Or replace them all (this component has only one provider)
// set: { providers: [{ provide: FancyService, useClass: FakeFancyService }] },
})
.createComponent(TestProvidersComponent);
fixture.detectChanges();
expect(fixture).toHaveText('injected value: faked value', 'text');
// Explore the providerTokens
const tokens = fixture.debugElement.providerTokens;
expect(tokens).toContain(fixture.componentInstance.constructor, 'component ctor');
expect(tokens).toContain(TestProvidersComponent, 'TestProvidersComp');
expect(tokens).toContain(FancyService, 'FancyService');
});
it('should override TestViewProvidersComp\'s FancyService viewProvider', () => {
const fixture = TestBed.configureTestingModule({
declarations: [TestViewProvidersComponent],
})
.overrideComponent(TestViewProvidersComponent, {
// remove: { viewProviders: [FancyService]},
// add: { viewProviders: [{ provide: FancyService, useClass: FakeFancyService }] },
// Or replace them all (this component has only one viewProvider)
set: { viewProviders: [{ provide: FancyService, useClass: FakeFancyService }] },
})
.createComponent(TestViewProvidersComponent);
fixture.detectChanges();
expect(fixture).toHaveText('injected value: faked value');
});
it('injected provider should not be same as component\'s provider', () => {
// TestComponent is parent of TestProvidersComponent
@Component({ template: '<my-service-comp></my-service-comp>' })
class TestComponent {}
// 3 levels of FancyService provider: module, TestCompomponent, TestProvidersComponent
const fixture = TestBed.configureTestingModule({
declarations: [TestComponent, TestProvidersComponent],
providers: [FancyService]
})
.overrideComponent(TestComponent, {
set: { providers: [{ provide: FancyService, useValue: {} }] }
})
.overrideComponent(TestProvidersComponent, {
set: { providers: [{ provide: FancyService, useClass: FakeFancyService }] }
})
.createComponent(TestComponent);
let testBedProvider: FancyService;
let tcProvider: {};
let tpcProvider: FakeFancyService;
// `inject` uses TestBed's injector
inject([FancyService], (s: FancyService) => testBedProvider = s)();
tcProvider = fixture.debugElement.injector.get(FancyService);
tpcProvider = fixture.debugElement.children[0].injector.get(FancyService) as FakeFancyService;
expect(testBedProvider).not.toBe(<any> tcProvider, 'testBed/tc not same providers');
expect(testBedProvider).not.toBe(tpcProvider, 'testBed/tpc not same providers');
expect(testBedProvider instanceof FancyService).toBe(true, 'testBedProvider is FancyService');
expect(tcProvider).toEqual({}, 'tcProvider is {}');
expect(tpcProvider instanceof FakeFancyService).toBe(true, 'tpcProvider is FakeFancyService');
});
it('can access template local variables as references', () => {
const fixture = TestBed.configureTestingModule({
declarations: [ShellComponent, NeedsContentComponent, Child1Component, Child2Component, Child3Component],
})
.overrideComponent(ShellComponent, {
set: {
selector: 'test-shell',
template: `
<needs-content #nc>
<child-1 #content text="My"></child-1>
<child-2 #content text="dog"></child-2>
<child-2 text="has"></child-2>
<child-3 #content text="fleas"></child-3>
<div #content>!</div>
</needs-content>
`
}
})
.createComponent(ShellComponent);
fixture.detectChanges();
// NeedsContentComp is the child of ShellComp
const el = fixture.debugElement.children[0];
const comp = el.componentInstance;
expect(comp.children.toArray().length).toBe(4,
'three different child components and an ElementRef with #content');
expect(el.references['nc']).toBe(comp, '#nc reference to component');
// #docregion custom-predicate
// Filter for DebugElements with a #content reference
const contentRefs = el.queryAll( de => de.references['content']);
// #enddocregion custom-predicate
expect(contentRefs.length).toBe(4, 'elements w/ a #content reference');
});
});
describe('Nested (one-deep) component override', () => {
beforeEach( async(() => {
TestBed.configureTestingModule({
declarations: [ParentComponent, FakeChildComponent]
})
.compileComponents();
}));
it('ParentComp should use Fake Child component', () => {
const fixture = TestBed.createComponent(ParentComponent);
fixture.detectChanges();
expect(fixture).toHaveText('Parent(Fake Child)');
});
});
describe('Nested (two-deep) component override', () => {
beforeEach( async(() => {
TestBed.configureTestingModule({
declarations: [ParentComponent, FakeChildWithGrandchildComponent, FakeGrandchildComponent]
})
.compileComponents();
}));
it('should use Fake Grandchild component', () => {
const fixture = TestBed.createComponent(ParentComponent);
fixture.detectChanges();
expect(fixture).toHaveText('Parent(Fake Child(Fake Grandchild))');
});
});
describe('Lifecycle hooks w/ MyIfParentComp', () => {
let fixture: ComponentFixture<MyIfParentComponent>;
let parent: MyIfParentComponent;
let child: MyIfChildComponent;
beforeEach( async(() => {
TestBed.configureTestingModule({
imports: [FormsModule],
declarations: [MyIfChildComponent, MyIfParentComponent]
})
.compileComponents().then(() => {
fixture = TestBed.createComponent(MyIfParentComponent);
parent = fixture.componentInstance;
});
}));
it('should instantiate parent component', () => {
expect(parent).not.toBeNull('parent component should exist');
});
it('parent component OnInit should NOT be called before first detectChanges()', () => {
expect(parent.ngOnInitCalled).toBe(false);
});
it('parent component OnInit should be called after first detectChanges()', () => {
fixture.detectChanges();
expect(parent.ngOnInitCalled).toBe(true);
});
it('child component should exist after OnInit', () => {
fixture.detectChanges();
getChild();
expect(child instanceof MyIfChildComponent).toBe(true, 'should create child');
});
it('should have called child component\'s OnInit ', () => {
fixture.detectChanges();
getChild();
expect(child.ngOnInitCalled).toBe(true);
});
it('child component called OnChanges once', () => {
fixture.detectChanges();
getChild();
expect(child.ngOnChangesCounter).toBe(1);
});
it('changed parent value flows to child', () => {
fixture.detectChanges();
getChild();
parent.parentValue = 'foo';
fixture.detectChanges();
expect(child.ngOnChangesCounter).toBe(2,
'expected 2 changes: initial value and changed value');
expect(child.childValue).toBe('foo',
'childValue should eq changed parent value');
});
// must be async test to see child flow to parent
it('changed child value flows to parent', async(() => {
fixture.detectChanges();
getChild();
child.childValue = 'bar';
return new Promise(resolve => {
// Wait one JS engine turn!
setTimeout(() => resolve(), 0);
})
.then(() => {
fixture.detectChanges();
expect(child.ngOnChangesCounter).toBe(2,
'expected 2 changes: initial value and changed value');
expect(parent.parentValue).toBe('bar',
'parentValue should eq changed parent value');
});
}));
it('clicking "Close Child" triggers child OnDestroy', () => {
fixture.detectChanges();
getChild();
const btn = fixture.debugElement.query(By.css('button'));
click(btn);
fixture.detectChanges();
expect(child.ngOnDestroyCalled).toBe(true);
});
////// helpers ///
/**
* Get the MyIfChildComp from parent; fail w/ good message if cannot.
*/
function getChild() {
let childDe: DebugElement; // DebugElement that should hold the MyIfChildComp
// The Hard Way: requires detailed knowledge of the parent template
try {
childDe = fixture.debugElement.children[4].children[0];
} catch (err) { /* we'll report the error */ }
// DebugElement.queryAll: if we wanted all of many instances:
childDe = fixture.debugElement
.queryAll(function (de) { return de.componentInstance instanceof MyIfChildComponent; })[0];
// WE'LL USE THIS APPROACH !
// DebugElement.query: find first instance (if any)
childDe = fixture.debugElement
.query(function (de) { return de.componentInstance instanceof MyIfChildComponent; });
if (childDe && childDe.componentInstance) {
child = childDe.componentInstance;
} else {
fail('Unable to find MyIfChildComp within MyIfParentComp');
}
return child;
}
});
////////// Fakes ///////////
@Component({
selector: 'child-1',
template: `Fake Child`
})
class FakeChildComponent { }
@Component({
selector: 'child-1',
template: `Fake Child(<grandchild-1></grandchild-1>)`
})
class FakeChildWithGrandchildComponent { }
@Component({
selector: 'grandchild-1',
template: `Fake Grandchild`
})
class FakeGrandchildComponent { }
@Injectable()
class FakeFancyService extends FancyService {
value = 'faked value';
}

View File

@ -1,55 +0,0 @@
// #docplaster
// #docregion imports
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { BannerComponent } from './banner-inline.component';
// #enddocregion imports
// #docregion setup
describe('BannerComponent (inline template)', () => {
let comp: BannerComponent;
let fixture: ComponentFixture<BannerComponent>;
let de: DebugElement;
let el: HTMLElement;
// #docregion before-each
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ BannerComponent ], // declare the test component
});
fixture = TestBed.createComponent(BannerComponent);
comp = fixture.componentInstance; // BannerComponent test instance
// query for the title <h1> by CSS element selector
de = fixture.debugElement.query(By.css('h1'));
el = de.nativeElement;
});
// #enddocregion before-each
// #enddocregion setup
// #docregion test-w-o-detect-changes
it('no title in the DOM until manually call `detectChanges`', () => {
expect(el.textContent).toEqual('');
});
// #enddocregion test-w-o-detect-changes
// #docregion tests
it('should display original title', () => {
fixture.detectChanges();
expect(el.textContent).toContain(comp.title);
});
it('should display a different test title', () => {
comp.title = 'Test Title';
fixture.detectChanges();
expect(el.textContent).toContain('Test Title');
});
// #enddocregion tests
// #docregion setup
});
// #enddocregion setup

View File

@ -1,53 +0,0 @@
// #docplaster
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { BannerComponent } from './banner.component';
describe('BannerComponent (templateUrl)', () => {
let comp: BannerComponent;
let fixture: ComponentFixture<BannerComponent>;
let de: DebugElement;
let el: HTMLElement;
// #docregion async-before-each
// async beforeEach
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ BannerComponent ], // declare the test component
})
.compileComponents(); // compile template and css
}));
// #enddocregion async-before-each
// #docregion sync-before-each
// synchronous beforeEach
beforeEach(() => {
fixture = TestBed.createComponent(BannerComponent);
comp = fixture.componentInstance; // BannerComponent test instance
// query for the title <h1> by CSS element selector
de = fixture.debugElement.query(By.css('h1'));
el = de.nativeElement;
});
// #enddocregion sync-before-each
it('no title in the DOM until manually call `detectChanges`', () => {
expect(el.textContent).toEqual('');
});
it('should display original title', () => {
fixture.detectChanges();
expect(el.textContent).toContain(comp.title);
});
it('should display a different test title', () => {
comp.title = 'Test Title';
fixture.detectChanges();
expect(el.textContent).toContain('Test Title');
});
});

View File

@ -0,0 +1,72 @@
// #docplaster
// #docregion import-async
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
// #enddocregion import-async
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { BannerComponent } from './banner-external.component';
describe('BannerComponent (external files)', () => {
let component: BannerComponent;
let fixture: ComponentFixture<BannerComponent>;
let h1: HTMLElement;
describe('Two beforeEach', () => {
// #docregion async-before-each
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ BannerComponent ],
})
.compileComponents(); // compile template and css
}));
// #enddocregion async-before-each
// synchronous beforeEach
// #docregion sync-before-each
beforeEach(() => {
fixture = TestBed.createComponent(BannerComponent);
component = fixture.componentInstance; // BannerComponent test instance
h1 = fixture.nativeElement.querySelector('h1');
});
// #enddocregion sync-before-each
tests();
});
describe('One beforeEach', () => {
// #docregion one-before-each
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ BannerComponent ],
})
.compileComponents()
.then(() => {
fixture = TestBed.createComponent(BannerComponent);
component = fixture.componentInstance;
h1 = fixture.nativeElement.querySelector('h1');
});
}));
// #enddocregion one-before-each
tests();
});
function tests() {
it('no title in the DOM until manually call `detectChanges`', () => {
expect(h1.textContent).toEqual('');
});
it('should display original title', () => {
fixture.detectChanges();
expect(h1.textContent).toContain(component.title);
});
it('should display a different test title', () => {
component.title = 'Test Title';
fixture.detectChanges();
expect(h1.textContent).toContain('Test Title');
});
}
});

View File

@ -1,11 +1,14 @@
// #docplaster
// #docregion
import { Component } from '@angular/core';
// #docregion metadata
@Component({
selector: 'app-banner',
template: '<h1>{{title}}</h1>'
templateUrl: './banner-external.component.html',
styleUrls: ['./banner-external.component.css']
})
// #enddocregion metadata
export class BannerComponent {
title = 'Test Tour of Heroes';
}

View File

@ -0,0 +1,119 @@
// #docplaster
// #docregion import-by
import { By } from '@angular/platform-browser';
// #enddocregion import-by
// #docregion import-debug-element
import { DebugElement } from '@angular/core';
// #enddocregion import-debug-element
// #docregion v1
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
// #enddocregion v1
import { BannerComponent } from './banner-initial.component';
/*
// #docregion v1
import { BannerComponent } from './banner.component';
describe('BannerComponent', () => {
// #enddocregion v1
*/
describe('BannerComponent (initial CLI generated)', () => {
// #docregion v1
let component: BannerComponent;
let fixture: ComponentFixture<BannerComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ BannerComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(BannerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeDefined();
});
});
// #enddocregion v1
// #docregion v2
describe('BannerComponent (minimal)', () => {
it('should create', () => {
// #docregion configureTestingModule
TestBed.configureTestingModule({
declarations: [ BannerComponent ]
});
// #enddocregion configureTestingModule
// #docregion createComponent
const fixture = TestBed.createComponent(BannerComponent);
// #enddocregion createComponent
// #docregion componentInstance
const component = fixture.componentInstance;
expect(component).toBeDefined();
// #enddocregion componentInstance
});
});
// #enddocregion v2
// #docregion v3, v4
describe('BannerComponent (with beforeEach)', () => {
let component: BannerComponent;
let fixture: ComponentFixture<BannerComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ BannerComponent ]
});
fixture = TestBed.createComponent(BannerComponent);
component = fixture.componentInstance;
});
it('should create', () => {
expect(component).toBeDefined();
});
// #enddocregion v3
// #docregion v4-test-2
it('should contain "banner works!"', () => {
const bannerElement: HTMLElement = fixture.nativeElement;
expect(bannerElement.textContent).toContain('banner works!');
});
// #enddocregion v4-test-2
// #docregion v4-test-3
it('should have <p> with "banner works!"', () => {
// #docregion nativeElement
const bannerElement: HTMLElement = fixture.nativeElement;
// #enddocregion nativeElement
const p = bannerElement.querySelector('p');
expect(p.textContent).toEqual('banner works!');
});
// #enddocregion v4-test-3
// #docregion v4-test-4
it('should find the <p> with fixture.debugElement.nativeElement)', () => {
// #docregion debugElement-nativeElement
const bannerDe: DebugElement = fixture.debugElement;
const bannerEl: HTMLElement = bannerDe.nativeElement;
// #enddocregion debugElement-nativeElement
const p = bannerEl.querySelector('p');
expect(p.textContent).toEqual('banner works!');
});
// #enddocregion v4-test-4
// #docregion v4-test-5
it('should find the <p> with fixture.debugElement.query(By.css)', () => {
const bannerDe: DebugElement = fixture.debugElement;
const paragraphDe = bannerDe.query(By.css('p'));
const p: HTMLElement = paragraphDe.nativeElement;
expect(p.textContent).toEqual('banner works!');
});
// #enddocregion v4-test-5
// #docregion v3
});
// #enddocregion v3, v4

View File

@ -0,0 +1,10 @@
// BannerComponent as initially generated by the CLI
// #docregion
import { Component } from '@angular/core';
@Component({
selector: 'app-banner',
template: `<p>banner works!</p>`,
styles: []
})
export class BannerComponent { }

View File

@ -7,53 +7,45 @@ import { async } from '@angular/core/testing';
import { ComponentFixtureAutoDetect } from '@angular/core/testing';
// #enddocregion import-ComponentFixtureAutoDetect
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { BannerComponent } from './banner.component';
describe('BannerComponent (AutoChangeDetect)', () => {
let comp: BannerComponent;
let fixture: ComponentFixture<BannerComponent>;
let de: DebugElement;
let el: HTMLElement;
let h1: HTMLElement;
beforeEach(async(() => {
beforeEach(() => {
// #docregion auto-detect
TestBed.configureTestingModule({
declarations: [ BannerComponent ],
providers: [
{ provide: ComponentFixtureAutoDetect, useValue: true }
]
})
});
// #enddocregion auto-detect
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(BannerComponent);
comp = fixture.componentInstance;
de = fixture.debugElement.query(By.css('h1'));
el = de.nativeElement;
h1 = fixture.nativeElement.querySelector('h1');
});
// #docregion auto-detect-tests
it('should display original title', () => {
// Hooray! No `fixture.detectChanges()` needed
expect(el.textContent).toContain(comp.title);
expect(h1.textContent).toContain(comp.title);
});
it('should still see original title after comp.title change', () => {
const oldTitle = comp.title;
comp.title = 'Test Title';
// Displayed title is old because Angular didn't hear the change :(
expect(el.textContent).toContain(oldTitle);
expect(h1.textContent).toContain(oldTitle);
});
it('should display updated title after detectChanges', () => {
comp.title = 'Test Title';
fixture.detectChanges(); // detect changes explicitly
expect(el.textContent).toContain(comp.title);
expect(h1.textContent).toContain(comp.title);
});
// #enddocregion auto-detect-tests
});

View File

@ -0,0 +1,56 @@
// #docplaster
// #docregion
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DebugElement } from '@angular/core';
import { BannerComponent } from './banner.component';
describe('BannerComponent (inline template)', () => {
// #docregion setup
let component: BannerComponent;
let fixture: ComponentFixture<BannerComponent>;
let h1: HTMLElement;
// #docregion configure-and-create
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ BannerComponent ],
});
fixture = TestBed.createComponent(BannerComponent);
// #enddocregion configure-and-create
component = fixture.componentInstance; // BannerComponent test instance
h1 = fixture.nativeElement.querySelector('h1');
// #docregion configure-and-create
});
// #enddocregion setup, configure-and-create
// #docregion test-w-o-detect-changes
it('no title in the DOM after createComponent()', () => {
expect(h1.textContent).toEqual('');
});
// #enddocregion test-w-o-detect-changes
// #docregion expect-h1-default-v1
it('should display original title', () => {
// #enddocregion expect-h1-default-v1
fixture.detectChanges();
// #docregion expect-h1-default-v1
expect(h1.textContent).toContain(component.title);
});
// #enddocregion expect-h1-default-v1
// #docregion expect-h1-default
it('should display original title after detectChanges()', () => {
fixture.detectChanges();
expect(h1.textContent).toContain(component.title);
});
// #enddocregion expect-h1-default
// #docregion after-change
it('should display a different test title', () => {
component.title = 'Test Title';
fixture.detectChanges();
expect(h1.textContent).toContain('Test Title');
});
// #enddocregion after-change
});

View File

@ -1,12 +1,12 @@
// #docregion
import { Component } from '@angular/core';
// #docregion component
@Component({
selector: 'app-banner',
templateUrl: './banner.component.html',
styleUrls: ['./banner.component.css']
template: '<h1>{{title}}</h1>',
styles: ['h1 { color: green; font-size: 350%}']
})
export class BannerComponent {
title = 'Test Tour of Heroes';
}
// #enddocregion component

View File

@ -1,4 +0,0 @@
<!-- #docregion -->
<div (click)="click()" class="hero">
{{hero.name | uppercase}}
</div>

View File

@ -1,7 +1,9 @@
// #docplaster
import { async, ComponentFixture, TestBed
} from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { addMatchers, click } from '../../testing';
@ -11,64 +13,96 @@ import { DashboardHeroComponent } from './dashboard-hero.component';
beforeEach( addMatchers );
describe('DashboardHeroComponent class only', () => {
// #docregion 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();
});
// #enddocregion class-only
});
describe('DashboardHeroComponent when tested directly', () => {
let comp: DashboardHeroComponent;
let expectedHero: Hero;
let fixture: ComponentFixture<DashboardHeroComponent>;
let heroEl: DebugElement;
let heroDe: DebugElement;
let heroEl: HTMLElement;
// #docregion setup, compile-components
// async beforeEach
beforeEach( async(() => {
beforeEach(async(() => {
// #docregion setup, config-testbed
TestBed.configureTestingModule({
declarations: [ DashboardHeroComponent ],
declarations: [ DashboardHeroComponent ]
})
.compileComponents(); // compile template and css
// #enddocregion setup, config-testbed
.compileComponents();
}));
// #enddocregion compile-components
// synchronous beforeEach
beforeEach(() => {
// #docregion setup
fixture = TestBed.createComponent(DashboardHeroComponent);
comp = fixture.componentInstance;
heroEl = fixture.debugElement.query(By.css('.hero')); // find hero element
// pretend that it was wired to something that supplied a hero
expectedHero = new Hero(42, 'Test Name');
// 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;
fixture.detectChanges(); // trigger initial data binding
// trigger initial data binding
fixture.detectChanges();
// #enddocregion setup
});
// #enddocregion setup
// #docregion name-test
it('should display hero name', () => {
it('should display hero name in uppercase', () => {
const expectedPipedName = expectedHero.name.toUpperCase();
expect(heroEl.nativeElement.textContent).toContain(expectedPipedName);
expect(heroEl.textContent).toContain(expectedPipedName);
});
// #enddocregion name-test
// #docregion click-test
it('should raise selected event when clicked', () => {
it('should raise selected event when clicked (triggerEventHandler)', () => {
let selectedHero: Hero;
comp.selected.subscribe((hero: Hero) => selectedHero = hero);
// #docregion trigger-event-handler
heroEl.triggerEventHandler('click', null);
heroDe.triggerEventHandler('click', null);
// #enddocregion trigger-event-handler
expect(selectedHero).toBe(expectedHero);
});
// #enddocregion click-test
// #docregion click-test-2
it('should raise selected event when clicked', () => {
let selectedHero: Hero;
comp.selected.subscribe((hero: Hero) => selectedHero = hero);
// #docregion click-test-2
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);
});
// #enddocregion click-test-2
// #docregion click-test-3
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
click(heroEl); // triggerEventHandler helper
expect(selectedHero).toBe(expectedHero);
});
// #enddocregion click-test-2
// #enddocregion click-test-3
});
//////////////////
@ -76,28 +110,31 @@ describe('DashboardHeroComponent when tested directly', () => {
describe('DashboardHeroComponent when inside a test host', () => {
let testHost: TestHostComponent;
let fixture: ComponentFixture<TestHostComponent>;
let heroEl: DebugElement;
let heroEl: HTMLElement;
// #docregion test-host-setup
beforeEach( async(() => {
beforeEach(async(() => {
// #docregion test-host-setup
TestBed.configureTestingModule({
declarations: [ DashboardHeroComponent, TestHostComponent ], // declare both
}).compileComponents();
declarations: [ DashboardHeroComponent, TestHostComponent ]
})
// #enddocregion test-host-setup
.compileComponents();
}));
beforeEach(() => {
// #docregion test-host-setup
// create TestHostComponent instead of DashboardHeroComponent
fixture = TestBed.createComponent(TestHostComponent);
testHost = fixture.componentInstance;
heroEl = fixture.debugElement.query(By.css('.hero')); // find hero
heroEl = fixture.nativeElement.querySelector('.hero');
fixture.detectChanges(); // trigger initial data binding
// #enddocregion test-host-setup
});
// #enddocregion test-host-setup
// #docregion test-host-tests
it('should display hero name', () => {
const expectedPipedName = testHost.hero.name.toUpperCase();
expect(heroEl.nativeElement.textContent).toContain(expectedPipedName);
expect(heroEl.textContent).toContain(expectedPipedName);
});
it('should raise selected event when clicked', () => {
@ -114,10 +151,12 @@ import { Component } from '@angular/core';
// #docregion test-host
@Component({
template: `
<dashboard-hero [hero]="hero" (selected)="onSelected($event)"></dashboard-hero>`
<dashboard-hero
[hero]="hero" (selected)="onSelected($event)">
</dashboard-hero>`
})
class TestHostComponent {
hero = new Hero(42, 'Test Name');
hero: Hero = {id: 42, name: 'Test Name' };
selectedHero: Hero;
onSelected(hero: Hero) { this.selectedHero = hero; }
}

View File

@ -5,13 +5,17 @@ import { Hero } from '../model/hero';
// #docregion component
@Component({
selector: 'dashboard-hero',
templateUrl: './dashboard-hero.component.html',
selector: 'dashboard-hero',
template: `
<div (click)="click()" class="hero">
{{hero.name | uppercase}}
</div>`,
styleUrls: [ './dashboard-hero.component.css' ]
})
// #docregion class
export class DashboardHeroComponent {
@Input() hero: Hero;
@Output() selected = new EventEmitter<Hero>();
click() { this.selected.emit(this.hero); }
}
// #enddocregion component
// #enddocregion component, class

View File

@ -1,24 +1,24 @@
import { Router } from '@angular/router';
import { DashboardComponent } from './dashboard.component';
import { Hero } from '../model';
import { Hero } from '../model/hero';
import { addMatchers } from '../../testing';
import { FakeHeroService } from '../model/testing';
import { TestHeroService, HeroService } from '../model/testing/test-hero.service';
class FakeRouter {
navigateByUrl(url: string) { return url; }
}
describe('DashboardComponent: w/o Angular TestBed', () => {
describe('DashboardComponent class only', () => {
let comp: DashboardComponent;
let heroService: FakeHeroService;
let heroService: TestHeroService;
let router: Router;
beforeEach(() => {
addMatchers();
router = new FakeRouter() as any as Router;
heroService = new FakeHeroService();
heroService = new TestHeroService();
comp = new DashboardComponent(router, heroService);
});
@ -35,17 +35,19 @@ describe('DashboardComponent: w/o Angular TestBed', () => {
it('should HAVE heroes after HeroService gets them', (done: DoneFn) => {
comp.ngOnInit(); // ngOnInit -> getHeroes
heroService.lastPromise // the one from getHeroes
.then(() => {
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');
})
.then(done, done.fail);
done();
},
done.fail);
});
it('should tell ROUTER to navigate by hero id', () => {
const hero = new Hero(42, 'Abbracadabra');
const hero: Hero = {id: 42, name: 'Abbracadabra' };
const spy = spyOn(router, 'navigateByUrl');
comp.gotoDetail(hero);

View File

@ -2,9 +2,9 @@
import { async, inject, ComponentFixture, TestBed
} from '@angular/core/testing';
import { addMatchers, click } from '../../testing';
import { HeroService } from '../model';
import { FakeHeroService } from '../model/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';
@ -12,12 +12,6 @@ import { Router } from '@angular/router';
import { DashboardComponent } from './dashboard.component';
import { DashboardModule } from './dashboard.module';
// #docregion router-stub
class RouterStub {
navigateByUrl(url: string) { return url; }
}
// #enddocregion router-stub
beforeEach ( addMatchers );
let comp: DashboardComponent;
@ -37,8 +31,8 @@ describe('DashboardComponent (deep)', () => {
tests(clickForDeep);
function clickForDeep() {
// get first <div class="hero"> DebugElement
const heroEl = fixture.debugElement.query(By.css('.hero'));
// get first <div class="hero">
const heroEl: HTMLElement = fixture.nativeElement.querySelector('.hero');
click(heroEl);
}
});
@ -61,24 +55,32 @@ describe('DashboardComponent (shallow)', () => {
function clickForShallow() {
// get first <dashboard-hero> DebugElement
const heroEl = fixture.debugElement.query(By.css('dashboard-hero'));
heroEl.triggerEventHandler('selected', comp.heroes[0]);
const heroDe = fixture.debugElement.query(By.css('dashboard-hero'));
heroDe.triggerEventHandler('selected', comp.heroes[0]);
}
});
/** Add TestBed providers, compile, and create DashboardComponent */
function compileAndCreate() {
// #docregion compile-and-create-body
beforeEach( async(() => {
beforeEach(async(() => {
// #docregion router-spy
const routerSpy = jasmine.createSpyObj('Router', ['navigateByUrl']);
const heroServiceSpy = jasmine.createSpyObj('HeroService', ['getHeroes']);
TestBed.configureTestingModule({
providers: [
{ provide: HeroService, useClass: FakeHeroService },
{ provide: Router, useClass: RouterStub }
{ provide: HeroService, useValue: heroServiceSpy },
{ provide: Router, useValue: routerSpy }
]
})
// #enddocregion router-spy
.compileComponents().then(() => {
fixture = TestBed.createComponent(DashboardComponent);
comp = fixture.componentInstance;
// getHeroes spy returns observable of test heroes
heroServiceSpy.getHeroes.and.returnValue(asyncData(getTestHeroes()));
});
// #enddocregion compile-and-create-body
}));
@ -104,8 +106,11 @@ function tests(heroClick: Function) {
describe('after get dashboard heroes', () => {
let router: Router;
// Trigger component so it gets heroes and binds to them
beforeEach( async(() => {
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
@ -119,29 +124,25 @@ function tests(heroClick: Function) {
it('should DISPLAY heroes', () => {
// Find and examine the displayed heroes
// Look for them in the DOM by css class
const heroes = fixture.debugElement.queryAll(By.css('dashboard-hero'));
const heroes = fixture.nativeElement.querySelectorAll('dashboard-hero');
expect(heroes.length).toBe(4, 'should display 4 heroes');
});
// #docregion navigate-test, inject
it('should tell ROUTER to navigate when hero clicked',
inject([Router], (router: Router) => { // ...
// #enddocregion inject
const spy = spyOn(router, 'navigateByUrl');
// #docregion navigate-test
it('should tell ROUTER to navigate when hero clicked', () => {
heroClick(); // trigger click on first inner <div class="hero">
// args passed to router.navigateByUrl()
// 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');
// #docregion inject
}));
// #enddocregion navigate-test, inject
});
// #enddocregion navigate-test
});
}

View File

@ -2,7 +2,7 @@
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { Hero } from '../model/hero';
import { Hero } from '../model/hero';
import { HeroService } from '../model/hero.service';
@Component({
@ -23,7 +23,7 @@ export class DashboardComponent implements OnInit {
ngOnInit() {
this.heroService.getHeroes()
.then(heroes => this.heroes = heroes.slice(1, 5));
.subscribe(heroes => this.heroes = heroes.slice(1, 5));
}
// #docregion goto-detail

View File

@ -1,7 +1,8 @@
// tslint:disable-next-line:no-unused-variable
import { async, fakeAsync, tick } from '@angular/core/testing';
import { Observable } from 'rxjs/Observable';
import { of } from 'rxjs/observable/of';
import { delay } from 'rxjs/operators';
describe('Angular async helper', () => {
let actuallyDone = false;
@ -34,8 +35,8 @@ describe('Angular async helper', () => {
// Use done. Cannot use setInterval with async or fakeAsync
// See https://github.com/angular/angular/issues/10127
it('should run async test with successful delayed Observable', (done: any) => {
const source = Observable.of(true).delay(10);
it('should run async test with successful delayed Observable', (done: DoneFn) => {
const source = of(true).pipe(delay(10));
source.subscribe(
val => actuallyDone = true,
err => fail(err),
@ -46,7 +47,7 @@ describe('Angular async helper', () => {
// Cannot use setInterval from within an async zone test
// See https://github.com/angular/angular/issues/10127
// xit('should run async test with successful delayed Observable', async(() => {
// const source = Observable.of(true).delay(10);
// const source = of(true).pipe(delay(10));
// source.subscribe(
// val => actuallyDone = true,
// err => fail(err)
@ -56,7 +57,7 @@ describe('Angular async helper', () => {
// // Fail message: Error: 1 periodic timer(s) still in the queue
// // See https://github.com/angular/angular/issues/10127
// xit('should run async test with successful delayed Observable', fakeAsync(() => {
// const source = Observable.of(true).delay(10);
// const source = of(true).pipe(delay(10));
// source.subscribe(
// val => actuallyDone = true,
// err => fail(err)

View File

@ -1,5 +1,5 @@
// main app entry point
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { BagModule } from './bag';
import { DemoModule } from './demo';
platformBrowserDynamic().bootstrapModule(BagModule);
platformBrowserDynamic().bootstrapModule(DemoModule);

View File

@ -0,0 +1,153 @@
// #docplaster
import {
LightswitchComponent,
MasterService,
ValueService,
ReversePipe
} from './demo';
///////// Fakes /////////
export class FakeValueService extends ValueService {
value = 'faked service value';
}
////////////////////////
describe('demo (no TestBed):', () => {
// #docregion ValueService
// Straight Jasmine testing without Angular's testing support
describe('ValueService', () => {
let service: ValueService;
beforeEach(() => { service = new ValueService(); });
it('#getValue should return real value', () => {
expect(service.getValue()).toBe('real value');
});
it('#getObservableValue should return value from observable',
(done: DoneFn) => {
service.getObservableValue().subscribe(value => {
expect(value).toBe('observable value');
done();
});
});
it('#getPromiseValue should return value from a promise',
(done: DoneFn) => {
service.getPromiseValue().then(value => {
expect(value).toBe('promise value');
done();
});
});
});
// #enddocregion ValueService
// MasterService requires injection of a ValueService
// #docregion MasterService
describe('MasterService without Angular testing support', () => {
let masterService: MasterService;
it('#getValue should return real value from the real service', () => {
masterService = new MasterService(new ValueService());
expect(masterService.getValue()).toBe('real value');
});
it('#getValue should return faked value from a fakeService', () => {
masterService = new MasterService(new FakeValueService());
expect(masterService.getValue()).toBe('faked service value');
});
it('#getValue should return faked value from a fake object', () => {
const fake = { getValue: () => 'fake value' };
masterService = new MasterService(fake as ValueService);
expect(masterService.getValue()).toBe('fake value');
});
it('#getValue should return stubbed value from a spy', () => {
// create `getValue` spy on an object representing the ValueService
const valueServiceSpy =
jasmine.createSpyObj('ValueService', ['getValue']);
// set the value to return when the `getValue` spy is called.
const stubValue = 'stub value';
valueServiceSpy.getValue.and.returnValue(stubValue);
masterService = new MasterService(valueServiceSpy);
expect(masterService.getValue())
.toBe(stubValue, 'service returned stub value');
expect(valueServiceSpy.getValue.calls.count())
.toBe(1, 'spy method was called once');
expect(valueServiceSpy.getValue.calls.mostRecent().returnValue)
.toBe(stubValue);
});
});
// #enddocregion MasterService
describe('MasterService (no beforeEach)', () => {
// #docregion no-before-each-test
it('#getValue should return stubbed value from a spy', () => {
// #docregion no-before-each-setup-call
const { masterService, stubValue, valueServiceSpy } = setup();
// #enddocregion no-before-each-setup-call
expect(masterService.getValue())
.toBe(stubValue, 'service returned stub value');
expect(valueServiceSpy.getValue.calls.count())
.toBe(1, 'spy method was called once');
expect(valueServiceSpy.getValue.calls.mostRecent().returnValue)
.toBe(stubValue);
});
// #enddocregion no-before-each-test
// #docregion no-before-each-setup
function setup() {
const valueServiceSpy =
jasmine.createSpyObj('ValueService', ['getValue']);
const stubValue = 'stub value';
const masterService = new MasterService(valueServiceSpy);
valueServiceSpy.getValue.and.returnValue(stubValue);
return { masterService, stubValue, valueServiceSpy };
}
// #enddocregion no-before-each-setup
});
// #docregion ReversePipe
describe('ReversePipe', () => {
let pipe: ReversePipe;
beforeEach(() => { pipe = new ReversePipe(); });
it('transforms "abc" to "cba"', () => {
expect(pipe.transform('abc')).toBe('cba');
});
it('no change to palindrome: "able was I ere I saw elba"', () => {
const palindrome = 'able was I ere I saw elba';
expect(pipe.transform(palindrome)).toBe(palindrome);
});
});
// #enddocregion ReversePipe
// #docregion Lightswitch
describe('LightswitchComp', () => {
it('#clicked() should toggle #isOn', () => {
const comp = new LightswitchComponent();
expect(comp.isOn).toBe(false, 'off at first');
comp.clicked();
expect(comp.isOn).toBe(true, 'on after click');
comp.clicked();
expect(comp.isOn).toBe(false, 'off after second click');
});
it('#clicked() should set #message to "is on"', () => {
const comp = new LightswitchComponent();
expect(comp.message).toMatch(/is off/i, 'off at first');
comp.clicked();
expect(comp.message).toMatch(/is on/i, 'on after clicked');
});
});
// #enddocregion Lightswitch
});

View File

@ -0,0 +1,706 @@
// #docplaster
import {
DemoModule,
BankAccountComponent, BankAccountParentComponent,
LightswitchComponent,
Child1Component, Child2Component, Child3Component,
MasterService,
ValueService,
ExternalTemplateComponent,
InputComponent,
IoComponent, IoParentComponent,
MyIfComponent, MyIfChildComponent, MyIfParentComponent,
NeedsContentComponent, ParentComponent,
TestProvidersComponent, TestViewProvidersComponent,
ReversePipeComponent, ShellComponent
} from './demo';
import { By } from '@angular/platform-browser';
import { Component,
DebugElement,
Injectable } from '@angular/core';
import { FormsModule } from '@angular/forms';
// Forms symbols imported only for a specific test below
import { NgModel, NgControl } from '@angular/forms';
import { async, ComponentFixture, fakeAsync, inject, TestBed, tick
} from '@angular/core/testing';
import { addMatchers, newEvent, click } from '../../testing';
export class NotProvided extends ValueService { /* example below */}
beforeEach( addMatchers );
describe('demo (with TestBed):', () => {
//////// Service Tests /////////////
// #docregion ValueService
describe('ValueService', () => {
// #docregion value-service-before-each
let service: ValueService;
// #docregion value-service-inject-before-each
beforeEach(() => {
TestBed.configureTestingModule({ providers: [ValueService] });
// #enddocregion value-service-before-each
service = TestBed.get(ValueService);
// #docregion value-service-before-each
});
// #enddocregion value-service-before-each, value-service-inject-before-each
// #docregion value-service-inject-it
it('should use ValueService', () => {
service = TestBed.get(ValueService);
expect(service.getValue()).toBe('real value');
});
// #enddocregion value-service-inject-it
it('can inject a default value when service is not provided', () => {
// #docregion testbed-get-w-null
service = TestBed.get(NotProvided, null); // service is null
// #enddocregion testbed-get-w-null
});
it('test should wait for ValueService.getPromiseValue', async(() => {
service.getPromiseValue().then(
value => expect(value).toBe('promise value')
);
}));
it('test should wait for ValueService.getObservableValue', async(() => {
service.getObservableValue().subscribe(
value => expect(value).toBe('observable value')
);
}));
// Must use done. See https://github.com/angular/angular/issues/10127
it('test should wait for ValueService.getObservableDelayValue', (done: DoneFn) => {
service.getObservableDelayValue().subscribe(value => {
expect(value).toBe('observable delay value');
done();
});
});
it('should allow the use of fakeAsync', fakeAsync(() => {
let value: any;
service.getPromiseValue().then((val: any) => value = val);
tick(); // Trigger JS engine cycle until all promises resolve.
expect(value).toBe('promise value');
}));
});
// #enddocregion ValueService
describe('MasterService', () => {
// #docregion master-service-before-each
let masterService: MasterService;
let valueServiceSpy: jasmine.SpyObj<ValueService>;
beforeEach(() => {
const spy = jasmine.createSpyObj('ValueService', ['getValue']);
TestBed.configureTestingModule({
// Provide both the service-to-test and its (spy) dependency
providers: [
MasterService,
{ provide: ValueService, useValue: spy }
]
});
// Inject both the service-to-test and its (spy) dependency
masterService = TestBed.get(MasterService);
valueServiceSpy = TestBed.get(ValueService);
});
// #enddocregion master-service-before-each
// #docregion master-service-it
it('#getValue should return stubbed value from a spy', () => {
const stubValue = 'stub value';
valueServiceSpy.getValue.and.returnValue(stubValue);
expect(masterService.getValue())
.toBe(stubValue, 'service returned stub value');
expect(valueServiceSpy.getValue.calls.count())
.toBe(1, 'spy method was called once');
expect(valueServiceSpy.getValue.calls.mostRecent().returnValue)
.toBe(stubValue);
});
// #enddocregion master-service-it
});
describe('use inject within `it`', () => {
beforeEach(() => {
TestBed.configureTestingModule({ providers: [ValueService] });
});
it('should use modified providers',
inject([ValueService], (service: ValueService) => {
service.setValue('value modified in beforeEach');
expect(service.getValue())
.toBe('value modified in beforeEach');
})
);
});
describe('using async(inject) within beforeEach', () => {
let serviceValue: string;
beforeEach(() => {
TestBed.configureTestingModule({ providers: [ValueService] });
});
beforeEach(async(inject([ValueService], (service: ValueService) => {
service.getPromiseValue().then(value => serviceValue = value);
})));
it('should use asynchronously modified value ... in synchronous test', () => {
expect(serviceValue).toBe('promise value');
});
});
/////////// Component Tests //////////////////
describe('TestBed component tests', () => {
beforeEach(async(() => {
TestBed
.configureTestingModule({
imports: [DemoModule],
})
// Compile everything in DemoModule
.compileComponents();
}));
it('should create a component with inline template', () => {
const fixture = TestBed.createComponent(Child1Component);
fixture.detectChanges();
expect(fixture).toHaveText('Child');
});
it('should create a component with external template', () => {
const fixture = TestBed.createComponent(ExternalTemplateComponent);
fixture.detectChanges();
expect(fixture).toHaveText('from external template');
});
it('should allow changing members of the component', () => {
const fixture = TestBed.createComponent(MyIfComponent);
fixture.detectChanges();
expect(fixture).toHaveText('MyIf()');
fixture.componentInstance.showMore = true;
fixture.detectChanges();
expect(fixture).toHaveText('MyIf(More)');
});
it('should create a nested component bound to inputs/outputs', () => {
const fixture = TestBed.createComponent(IoParentComponent);
fixture.detectChanges();
const heroes = fixture.debugElement.queryAll(By.css('.hero'));
expect(heroes.length).toBeGreaterThan(0, 'has heroes');
const comp = fixture.componentInstance;
const hero = comp.heroes[0];
click(heroes[0]);
fixture.detectChanges();
const selected = fixture.debugElement.query(By.css('p'));
expect(selected).toHaveText(hero.name);
});
it('can access the instance variable of an `*ngFor` row component', () => {
const fixture = TestBed.createComponent(IoParentComponent);
const comp = fixture.componentInstance;
const heroName = comp.heroes[0].name; // first hero's name
fixture.detectChanges();
const ngForRow = fixture.debugElement.query(By.directive(IoComponent)); // first hero ngForRow
const hero = ngForRow.context['hero']; // the hero object passed into the row
expect(hero.name).toBe(heroName, 'ngRow.context.hero');
const rowComp = ngForRow.componentInstance;
// jasmine.any is an "instance-of-type" test.
expect(rowComp).toEqual(jasmine.any(IoComponent), 'component is IoComp');
expect(rowComp.hero.name).toBe(heroName, 'component.hero');
});
// #docregion ButtonComp
it('should support clicking a button', () => {
const fixture = TestBed.createComponent(LightswitchComponent);
const btn = fixture.debugElement.query(By.css('button'));
const span = fixture.debugElement.query(By.css('span')).nativeElement;
fixture.detectChanges();
expect(span.textContent).toMatch(/is off/i, 'before click');
click(btn);
fixture.detectChanges();
expect(span.textContent).toMatch(/is on/i, 'after click');
});
// #enddocregion ButtonComp
// ngModel is async so we must wait for it with promise-based `whenStable`
it('should support entering text in input box (ngModel)', async(() => {
const expectedOrigName = 'John';
const expectedNewName = 'Sally';
const fixture = TestBed.createComponent(InputComponent);
fixture.detectChanges();
const comp = fixture.componentInstance;
const input = <HTMLInputElement> fixture.debugElement.query(By.css('input')).nativeElement;
expect(comp.name).toBe(expectedOrigName,
`At start name should be ${expectedOrigName} `);
// wait until ngModel binds comp.name to input box
fixture.whenStable().then(() => {
expect(input.value).toBe(expectedOrigName,
`After ngModel updates input box, input.value should be ${expectedOrigName} `);
// simulate user entering new name in input
input.value = expectedNewName;
// that change doesn't flow to the component immediately
expect(comp.name).toBe(expectedOrigName,
`comp.name should still be ${expectedOrigName} after value change, before binding happens`);
// dispatch a DOM event so that Angular learns of input value change.
// then wait while ngModel pushes input.box value to comp.name
input.dispatchEvent(newEvent('input'));
return fixture.whenStable();
})
.then(() => {
expect(comp.name).toBe(expectedNewName,
`After ngModel updates the model, comp.name should be ${expectedNewName} `);
});
}));
// fakeAsync version of ngModel input test enables sync test style
// synchronous `tick` replaces asynchronous promise-base `whenStable`
it('should support entering text in input box (ngModel) - fakeAsync', fakeAsync(() => {
const expectedOrigName = 'John';
const expectedNewName = 'Sally';
const fixture = TestBed.createComponent(InputComponent);
fixture.detectChanges();
const comp = fixture.componentInstance;
const input = <HTMLInputElement> fixture.debugElement.query(By.css('input')).nativeElement;
expect(comp.name).toBe(expectedOrigName,
`At start name should be ${expectedOrigName} `);
// wait until ngModel binds comp.name to input box
tick();
expect(input.value).toBe(expectedOrigName,
`After ngModel updates input box, input.value should be ${expectedOrigName} `);
// simulate user entering new name in input
input.value = expectedNewName;
// that change doesn't flow to the component immediately
expect(comp.name).toBe(expectedOrigName,
`comp.name should still be ${expectedOrigName} after value change, before binding happens`);
// dispatch a DOM event so that Angular learns of input value change.
// then wait a tick while ngModel pushes input.box value to comp.name
input.dispatchEvent(newEvent('input'));
tick();
expect(comp.name).toBe(expectedNewName,
`After ngModel updates the model, comp.name should be ${expectedNewName} `);
}));
// #docregion ReversePipeComp
it('ReversePipeComp should reverse the input text', fakeAsync(() => {
const inputText = 'the quick brown fox.';
const expectedText = '.xof nworb kciuq eht';
const fixture = TestBed.createComponent(ReversePipeComponent);
fixture.detectChanges();
const comp = fixture.componentInstance;
const input = fixture.debugElement.query(By.css('input')).nativeElement as HTMLInputElement;
const span = fixture.debugElement.query(By.css('span')).nativeElement as HTMLElement;
// simulate user entering new name in input
input.value = inputText;
// dispatch a DOM event so that Angular learns of input value change.
// then wait a tick while ngModel pushes input.box value to comp.text
// and Angular updates the output span
input.dispatchEvent(newEvent('input'));
tick();
fixture.detectChanges();
expect(span.textContent).toBe(expectedText, 'output span');
expect(comp.text).toBe(inputText, 'component.text');
}));
// #enddocregion ReversePipeComp
// Use this technique to find attached directives of any kind
it('can examine attached directives and listeners', () => {
const fixture = TestBed.createComponent(InputComponent);
fixture.detectChanges();
const inputEl = fixture.debugElement.query(By.css('input'));
expect(inputEl.providerTokens).toContain(NgModel, 'NgModel directive');
const ngControl = inputEl.injector.get(NgControl);
expect(ngControl).toEqual(jasmine.any(NgControl), 'NgControl directive');
expect(inputEl.listeners.length).toBeGreaterThan(2, 'several listeners attached');
});
// #docregion dom-attributes
it('BankAccountComponent should set attributes, styles, classes, and properties', () => {
const fixture = TestBed.createComponent(BankAccountParentComponent);
fixture.detectChanges();
const comp = fixture.componentInstance;
// the only child is debugElement of the BankAccount component
const el = fixture.debugElement.children[0];
const childComp = el.componentInstance as BankAccountComponent;
expect(childComp).toEqual(jasmine.any(BankAccountComponent));
expect(el.context).toBe(childComp, 'context is the child component');
expect(el.attributes['account']).toBe(childComp.id, 'account attribute');
expect(el.attributes['bank']).toBe(childComp.bank, 'bank attribute');
expect(el.classes['closed']).toBe(true, 'closed class');
expect(el.classes['open']).toBe(false, 'open class');
expect(el.styles['color']).toBe(comp.color, 'color style');
expect(el.styles['width']).toBe(comp.width + 'px', 'width style');
// #enddocregion dom-attributes
// Removed on 12/02/2016 when ceased public discussion of the `Renderer`. Revive in future?
// expect(el.properties['customProperty']).toBe(true, 'customProperty');
// #docregion dom-attributes
});
// #enddocregion dom-attributes
});
describe('TestBed component overrides:', () => {
it('should override ChildComp\'s template', () => {
const fixture = TestBed.configureTestingModule({
declarations: [Child1Component],
})
.overrideComponent(Child1Component, {
set: { template: '<span>Fake</span>' }
})
.createComponent(Child1Component);
fixture.detectChanges();
expect(fixture).toHaveText('Fake');
});
it('should override TestProvidersComp\'s ValueService provider', () => {
const fixture = TestBed.configureTestingModule({
declarations: [TestProvidersComponent],
})
.overrideComponent(TestProvidersComponent, {
remove: { providers: [ValueService]},
add: { providers: [{ provide: ValueService, useClass: FakeValueService }] },
// Or replace them all (this component has only one provider)
// set: { providers: [{ provide: ValueService, useClass: FakeValueService }] },
})
.createComponent(TestProvidersComponent);
fixture.detectChanges();
expect(fixture).toHaveText('injected value: faked value', 'text');
// Explore the providerTokens
const tokens = fixture.debugElement.providerTokens;
expect(tokens).toContain(fixture.componentInstance.constructor, 'component ctor');
expect(tokens).toContain(TestProvidersComponent, 'TestProvidersComp');
expect(tokens).toContain(ValueService, 'ValueService');
});
it('should override TestViewProvidersComp\'s ValueService viewProvider', () => {
const fixture = TestBed.configureTestingModule({
declarations: [TestViewProvidersComponent],
})
.overrideComponent(TestViewProvidersComponent, {
// remove: { viewProviders: [ValueService]},
// add: { viewProviders: [{ provide: ValueService, useClass: FakeValueService }] },
// Or replace them all (this component has only one viewProvider)
set: { viewProviders: [{ provide: ValueService, useClass: FakeValueService }] },
})
.createComponent(TestViewProvidersComponent);
fixture.detectChanges();
expect(fixture).toHaveText('injected value: faked value');
});
it('injected provider should not be same as component\'s provider', () => {
// TestComponent is parent of TestProvidersComponent
@Component({ template: '<my-service-comp></my-service-comp>' })
class TestComponent {}
// 3 levels of ValueService provider: module, TestCompomponent, TestProvidersComponent
const fixture = TestBed.configureTestingModule({
declarations: [TestComponent, TestProvidersComponent],
providers: [ValueService]
})
.overrideComponent(TestComponent, {
set: { providers: [{ provide: ValueService, useValue: {} }] }
})
.overrideComponent(TestProvidersComponent, {
set: { providers: [{ provide: ValueService, useClass: FakeValueService }] }
})
.createComponent(TestComponent);
let testBedProvider: ValueService;
let tcProvider: ValueService;
let tpcProvider: FakeValueService;
// `inject` uses TestBed's injector
inject([ValueService], (s: ValueService) => testBedProvider = s)();
tcProvider = fixture.debugElement.injector.get(ValueService) as ValueService;
tpcProvider = fixture.debugElement.children[0].injector.get(ValueService) as FakeValueService;
expect(testBedProvider).not.toBe(tcProvider, 'testBed/tc not same providers');
expect(testBedProvider).not.toBe(tpcProvider, 'testBed/tpc not same providers');
expect(testBedProvider instanceof ValueService).toBe(true, 'testBedProvider is ValueService');
expect(tcProvider).toEqual({} as ValueService, 'tcProvider is {}');
expect(tpcProvider instanceof FakeValueService).toBe(true, 'tpcProvider is FakeValueService');
});
it('can access template local variables as references', () => {
const fixture = TestBed.configureTestingModule({
declarations: [ShellComponent, NeedsContentComponent, Child1Component, Child2Component, Child3Component],
})
.overrideComponent(ShellComponent, {
set: {
selector: 'test-shell',
template: `
<needs-content #nc>
<child-1 #content text="My"></child-1>
<child-2 #content text="dog"></child-2>
<child-2 text="has"></child-2>
<child-3 #content text="fleas"></child-3>
<div #content>!</div>
</needs-content>
`
}
})
.createComponent(ShellComponent);
fixture.detectChanges();
// NeedsContentComp is the child of ShellComp
const el = fixture.debugElement.children[0];
const comp = el.componentInstance;
expect(comp.children.toArray().length).toBe(4,
'three different child components and an ElementRef with #content');
expect(el.references['nc']).toBe(comp, '#nc reference to component');
// #docregion custom-predicate
// Filter for DebugElements with a #content reference
const contentRefs = el.queryAll( de => de.references['content']);
// #enddocregion custom-predicate
expect(contentRefs.length).toBe(4, 'elements w/ a #content reference');
});
});
describe('nested (one-deep) component override', () => {
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ParentComponent, FakeChildComponent]
});
});
it('ParentComp should use Fake Child component', () => {
const fixture = TestBed.createComponent(ParentComponent);
fixture.detectChanges();
expect(fixture).toHaveText('Parent(Fake Child)');
});
});
describe('nested (two-deep) component override', () => {
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ParentComponent, FakeChildWithGrandchildComponent, FakeGrandchildComponent]
});
});
it('should use Fake Grandchild component', () => {
const fixture = TestBed.createComponent(ParentComponent);
fixture.detectChanges();
expect(fixture).toHaveText('Parent(Fake Child(Fake Grandchild))');
});
});
describe('lifecycle hooks w/ MyIfParentComp', () => {
let fixture: ComponentFixture<MyIfParentComponent>;
let parent: MyIfParentComponent;
let child: MyIfChildComponent;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [FormsModule],
declarations: [MyIfChildComponent, MyIfParentComponent]
});
fixture = TestBed.createComponent(MyIfParentComponent);
parent = fixture.componentInstance;
});
it('should instantiate parent component', () => {
expect(parent).not.toBeNull('parent component should exist');
});
it('parent component OnInit should NOT be called before first detectChanges()', () => {
expect(parent.ngOnInitCalled).toBe(false);
});
it('parent component OnInit should be called after first detectChanges()', () => {
fixture.detectChanges();
expect(parent.ngOnInitCalled).toBe(true);
});
it('child component should exist after OnInit', () => {
fixture.detectChanges();
getChild();
expect(child instanceof MyIfChildComponent).toBe(true, 'should create child');
});
it('should have called child component\'s OnInit ', () => {
fixture.detectChanges();
getChild();
expect(child.ngOnInitCalled).toBe(true);
});
it('child component called OnChanges once', () => {
fixture.detectChanges();
getChild();
expect(child.ngOnChangesCounter).toBe(1);
});
it('changed parent value flows to child', () => {
fixture.detectChanges();
getChild();
parent.parentValue = 'foo';
fixture.detectChanges();
expect(child.ngOnChangesCounter).toBe(2,
'expected 2 changes: initial value and changed value');
expect(child.childValue).toBe('foo',
'childValue should eq changed parent value');
});
// must be async test to see child flow to parent
it('changed child value flows to parent', async(() => {
fixture.detectChanges();
getChild();
child.childValue = 'bar';
return new Promise(resolve => {
// Wait one JS engine turn!
setTimeout(() => resolve(), 0);
})
.then(() => {
fixture.detectChanges();
expect(child.ngOnChangesCounter).toBe(2,
'expected 2 changes: initial value and changed value');
expect(parent.parentValue).toBe('bar',
'parentValue should eq changed parent value');
});
}));
it('clicking "Close Child" triggers child OnDestroy', () => {
fixture.detectChanges();
getChild();
const btn = fixture.debugElement.query(By.css('button'));
click(btn);
fixture.detectChanges();
expect(child.ngOnDestroyCalled).toBe(true);
});
////// helpers ///
/**
* Get the MyIfChildComp from parent; fail w/ good message if cannot.
*/
function getChild() {
let childDe: DebugElement; // DebugElement that should hold the MyIfChildComp
// The Hard Way: requires detailed knowledge of the parent template
try {
childDe = fixture.debugElement.children[4].children[0];
} catch (err) { /* we'll report the error */ }
// DebugElement.queryAll: if we wanted all of many instances:
childDe = fixture.debugElement
.queryAll(function (de) { return de.componentInstance instanceof MyIfChildComponent; })[0];
// WE'LL USE THIS APPROACH !
// DebugElement.query: find first instance (if any)
childDe = fixture.debugElement
.query(function (de) { return de.componentInstance instanceof MyIfChildComponent; });
if (childDe && childDe.componentInstance) {
child = childDe.componentInstance;
} else {
fail('Unable to find MyIfChildComp within MyIfParentComp');
}
return child;
}
});
});
////////// Fakes ///////////
@Component({
selector: 'child-1',
template: `Fake Child`
})
class FakeChildComponent { }
@Component({
selector: 'child-1',
template: `Fake Child(<grandchild-1></grandchild-1>)`
})
class FakeChildWithGrandchildComponent { }
@Component({
selector: 'grandchild-1',
template: `Fake Grandchild`
})
class FakeGrandchildComponent { }
@Injectable()
class FakeValueService extends ValueService {
value = 'faked value';
}

View File

@ -6,9 +6,8 @@ import { Component, ContentChildren, Directive, EventEmitter,
Pipe, PipeTransform,
SimpleChange } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import 'rxjs/add/operator/delay';
import { of } from 'rxjs/observable/of';
import { delay } from 'rxjs/operators';
////////// The App: Services and Components for the tests. //////////////
@ -17,37 +16,31 @@ export class Hero {
}
////////// Services ///////////////
// #docregion FancyService
// #docregion ValueService
@Injectable()
export class FancyService {
export class ValueService {
protected value = 'real value';
getValue() { return this.value; }
setValue(value: string) { this.value = value; }
getAsyncValue() { return Promise.resolve('async value'); }
getObservableValue() { return of('observable value'); }
getObservableValue() { return Observable.of('observable value'); }
getTimeoutValue() {
return new Promise((resolve) => {
setTimeout(() => { resolve('timeout value'); }, 10);
});
}
getPromiseValue() { return Promise.resolve('promise value'); }
getObservableDelayValue() {
return Observable.of('observable delay value').delay(10);
return of('observable delay value').pipe(delay(10));
}
}
// #enddocregion FancyService
// #enddocregion ValueService
// #docregion DependentService
// #docregion MasterService
@Injectable()
export class DependentService {
constructor(private dependentService: FancyService) { }
getValue() { return this.dependentService.getValue(); }
export class MasterService {
constructor(private masterService: ValueService) { }
getValue() { return this.masterService.getValue(); }
}
// #enddocregion DependentService
// #enddocregion MasterService
/////////// Pipe ////////////////
/*
@ -102,19 +95,19 @@ export class BankAccountParentComponent {
isClosed = true;
}
// #docregion ButtonComp
// #docregion LightswitchComp
@Component({
selector: 'button-comp',
selector: 'lightswitch-comp',
template: `
<button (click)="clicked()">Click me!</button>
<span>{{message}}</span>`
})
export class ButtonComponent {
export class LightswitchComponent {
isOn = false;
clicked() { this.isOn = !this.isOn; }
get message() { return `The light is ${this.isOn ? 'On' : 'Off'}`; }
}
// #enddocregion ButtonComp
// #enddocregion LightswitchComp
@Component({
selector: 'child-1',
@ -231,31 +224,31 @@ export class MyIfComponent {
@Component({
selector: 'my-service-comp',
template: `injected value: {{fancyService.value}}`,
providers: [FancyService]
template: `injected value: {{valueService.value}}`,
providers: [ValueService]
})
export class TestProvidersComponent {
constructor(public fancyService: FancyService) {}
constructor(public valueService: ValueService) {}
}
@Component({
selector: 'my-service-comp',
template: `injected value: {{fancyService.value}}`,
viewProviders: [FancyService]
template: `injected value: {{valueService.value}}`,
viewProviders: [ValueService]
})
export class TestViewProvidersComponent {
constructor(public fancyService: FancyService) {}
constructor(public valueService: ValueService) {}
}
@Component({
selector: 'external-template-comp',
templateUrl: './bag-external-template.html'
templateUrl: './demo-external-template.html'
})
export class ExternalTemplateComponent implements OnInit {
serviceValue: string;
constructor(@Optional() private service: FancyService) { }
constructor(@Optional() private service: ValueService) { }
ngOnInit() {
if (this.service) { this.serviceValue = this.service.getValue(); }
@ -376,9 +369,9 @@ export class ReversePipeComponent {
export class ShellComponent { }
@Component({
selector: 'bag-comp',
selector: 'demo-comp',
template: `
<h1>Specs Bag</h1>
<h1>Specs Demo</h1>
<my-if-parent-comp></my-if-parent-comp>
<hr>
<h3>Input/Output Component</h3>
@ -397,7 +390,7 @@ export class ShellComponent { }
<input-value-comp></input-value-comp>
<hr>
<h3>Button Component</h3>
<button-comp></button-comp>
<lightswitch-comp></lightswitch-comp>
<hr>
<h3>Needs Content</h3>
<needs-content #nc>
@ -409,13 +402,13 @@ export class ShellComponent { }
</needs-content>
`
})
export class BagComponent { }
export class DemoComponent { }
//////// Aggregations ////////////
export const bagDeclarations = [
BagComponent,
export const demoDeclarations = [
DemoComponent,
BankAccountComponent, BankAccountParentComponent,
ButtonComponent,
LightswitchComponent,
Child1Component, Child2Component, Child3Component,
ExternalTemplateComponent, InnerCompWithExternalTemplateComponent,
InputComponent,
@ -427,7 +420,7 @@ export const bagDeclarations = [
ReversePipe, ReversePipeComponent, ShellComponent
];
export const bagProviders = [DependentService, FancyService];
export const demoProviders = [MasterService, ValueService];
////////////////////
////////////
@ -437,10 +430,10 @@ import { FormsModule } from '@angular/forms';
@NgModule({
imports: [BrowserModule, FormsModule],
declarations: bagDeclarations,
providers: bagProviders,
entryComponents: [BagComponent],
bootstrap: [BagComponent]
declarations: demoDeclarations,
providers: demoProviders,
entryComponents: [DemoComponent],
bootstrap: [DemoComponent]
})
export class BagModule { }
export class DemoModule { }

View File

@ -0,0 +1,15 @@
// These unused NgModules keep the Angular Language Service happy.
// The AppModule registers the final versions of these components
import { NgModule } from '@angular/core';
import { AppComponent as app_initial } from './app-initial.component';
@NgModule({ declarations: [ app_initial ] })
export class AppModuleInitial {}
import { BannerComponent as bc_initial } from './banner/banner-initial.component';
@NgModule({ declarations: [ bc_initial ] })
export class BannerModuleInitial {}
import { BannerComponent as bc_external } from './banner/banner-external.component';
@NgModule({ declarations: [ bc_external ] })
export class BannerModuleExternal {}

View File

@ -1,7 +1,7 @@
import { HeroDetailComponent } from './hero-detail.component';
import { Hero } from '../model';
import { asyncData, ActivatedRouteStub } from '../../testing';
import { ActivatedRouteStub } from '../../testing';
import { HeroDetailComponent } from './hero-detail.component';
import { Hero } from '../model/hero';
////////// Tests ////////////////////
@ -12,22 +12,21 @@ describe('HeroDetailComponent - no TestBed', () => {
let hds: any;
let router: any;
beforeEach((done: any) => {
expectedHero = new Hero(42, 'Bubba');
activatedRoute = new ActivatedRouteStub();
activatedRoute.testParamMap = { id: expectedHero.id };
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(Promise.resolve(expectedHero));
hds.saveHero.and.returnValue(Promise.resolve(expectedHero));
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.then(done);
hds.getHero.calls.first().returnValue.subscribe(done);
});
it('should expose the hero retrieved from the service', () => {
@ -45,11 +44,11 @@ describe('HeroDetailComponent - no TestBed', () => {
expect(router.navigate.calls.any()).toBe(false, 'router.navigate not called yet');
});
it('should navigate when click save resolves', (done: any) => {
it('should navigate when click save resolves', (done: DoneFn) => {
comp.save();
// waits for async save to complete before navigating
hds.saveHero.calls.first().returnValue
.then(() => {
.subscribe(() => {
expect(router.navigate.calls.any()).toBe(true, 'router.navigate called');
done();
});

View File

@ -3,21 +3,20 @@ import {
async, ComponentFixture, fakeAsync, inject, TestBed, tick
} from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { Router } from '@angular/router';
import {
ActivatedRoute, ActivatedRouteStub, click, newEvent, Router, RouterStub
ActivatedRoute, ActivatedRouteStub, asyncData, click, newEvent
} from '../../testing';
import { Hero } from '../model';
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 comp: HeroDetailComponent;
let component: HeroDetailComponent;
let fixture: ComponentFixture<HeroDetailComponent>;
let page: Page;
@ -32,36 +31,38 @@ describe('HeroDetailComponent', () => {
describe('with SharedModule setup', sharedModuleSetup);
});
////////////////////
///////////////////
function overrideSetup() {
// #docregion hds-spy
class HeroDetailServiceSpy {
testHero = new Hero(42, 'Test Hero');
testHero: Hero = {id: 42, name: 'Test Hero' };
/* emit cloned test hero */
getHero = jasmine.createSpy('getHero').and.callFake(
() => Promise
.resolve(true)
.then(() => Object.assign({}, this.testHero))
() => asyncData(Object.assign({}, this.testHero))
);
/* emit clone of test hero, with changes merged in */
saveHero = jasmine.createSpy('saveHero').and.callFake(
(hero: Hero) => Promise
.resolve(true)
.then(() => Object.assign(this.testHero, hero))
(hero: Hero) => asyncData(Object.assign(this.testHero, hero))
);
}
// #enddocregion hds-spy
// the `id` value is irrelevant because ignored by service stub
beforeEach(() => activatedRoute.testParamMap = { id: 99999 } );
beforeEach(() => activatedRoute.setParamMap({ id: 99999 }));
// #docregion setup-override
beforeEach( async(() => {
beforeEach(async(() => {
const routerSpy = createRouterSpy();
TestBed.configureTestingModule({
imports: [ HeroModule ],
providers: [
{ provide: ActivatedRoute, useValue: activatedRoute },
{ provide: Router, useClass: RouterStub},
{ provide: Router, useValue: routerSpy},
// #enddocregion setup-override
// HeroDetailService at this level is IRRELEVANT!
{ provide: HeroDetailService, useValue: {} }
@ -87,7 +88,7 @@ function overrideSetup() {
// #docregion override-tests
let hdsSpy: HeroDetailServiceSpy;
beforeEach( async(() => {
beforeEach(async(() => {
createComponent();
// get the component's injected HeroDetailServiceSpy
hdsSpy = fixture.debugElement.injector.get(HeroDetailService) as any;
@ -108,7 +109,7 @@ function overrideSetup() {
page.nameInput.value = newName;
page.nameInput.dispatchEvent(newEvent('input')); // tell Angular
expect(comp.hero.name).toBe(newName, 'component hero has new name');
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);
@ -116,36 +117,40 @@ function overrideSetup() {
tick(); // wait for async save to complete
expect(hdsSpy.testHero.name).toBe(newName, 'service hero has new name after save');
expect(page.navSpy.calls.any()).toBe(true, 'router.navigate called');
expect(page.navigateSpy.calls.any()).toBe(true, 'router.navigate called');
}));
// #enddocregion override-tests
it('fixture injected service is not the component injected service',
inject([HeroDetailService], (service: HeroDetailService) => {
// inject gets the service from the fixture
inject([HeroDetailService], (fixtureService: HeroDetailService) => {
expect(service).toEqual(<any> {}, 'service injected from fixture');
expect(hdsSpy).toBeTruthy('service injected into component');
// 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 { HEROES, FakeHeroService } from '../model/testing';
import { HeroService } from '../model';
import { getTestHeroes, TestHeroService, HeroService } from '../model/testing/test-hero.service';
const firstHero = HEROES[0];
const firstHero = getTestHeroes()[0];
function heroModuleSetup() {
// #docregion setup-hero-module
beforeEach( async(() => {
TestBed.configureTestingModule({
beforeEach(async(() => {
const routerSpy = createRouterSpy();
TestBed.configureTestingModule({
imports: [ HeroModule ],
// #enddocregion setup-hero-module
// declarations: [ HeroDetailComponent ], // NO! DOUBLE DECLARATION
// #docregion setup-hero-module
providers: [
{ provide: ActivatedRoute, useValue: activatedRoute },
{ provide: HeroService, useClass: FakeHeroService },
{ provide: Router, useClass: RouterStub},
{ provide: HeroService, useClass: TestHeroService },
{ provide: Router, useValue: routerSpy},
]
})
.compileComponents();
@ -156,9 +161,9 @@ function heroModuleSetup() {
describe('when navigate to existing hero', () => {
let expectedHero: Hero;
beforeEach( async(() => {
beforeEach(async(() => {
expectedHero = firstHero;
activatedRoute.testParamMap = { id: expectedHero.id };
activatedRoute.setParamMap({ id: expectedHero.id });
createComponent();
}));
@ -170,7 +175,7 @@ function heroModuleSetup() {
it('should navigate when click cancel', () => {
click(page.cancelBtn);
expect(page.navSpy.calls.any()).toBe(true, 'router.navigate called');
expect(page.navigateSpy.calls.any()).toBe(true, 'router.navigate called');
});
it('should save when click save but not navigate immediately', () => {
@ -181,30 +186,31 @@ function heroModuleSetup() {
click(page.saveBtn);
expect(saveSpy.calls.any()).toBe(true, 'HeroDetailService.save called');
expect(page.navSpy.calls.any()).toBe(false, 'router.navigate not 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.navSpy.calls.any()).toBe(true, 'router.navigate called');
expect(page.navigateSpy.calls.any()).toBe(true, 'router.navigate called');
}));
// #docregion title-case-pipe
it('should convert hero name to Title Case', () => {
const inputName = 'quick BROWN fox';
const titleCaseName = 'Quick Brown Fox';
const { nameInput, nameDisplay } = page;
// simulate user entering new name into the input box
page.nameInput.value = inputName;
nameInput.value = inputName;
// dispatch a DOM event so that Angular learns of input value change.
page.nameInput.dispatchEvent(newEvent('input'));
nameInput.dispatchEvent(newEvent('input'));
// Tell Angular to update the output span through the title pipe
fixture.detectChanges();
expect(page.nameDisplay.textContent).toBe(titleCaseName);
expect(nameDisplay.textContent).toBe(titleCaseName);
});
// #enddocregion title-case-pipe
// #enddocregion selected-tests
@ -214,10 +220,10 @@ function heroModuleSetup() {
// #docregion route-no-id
describe('when navigate with no hero id', () => {
beforeEach( async( createComponent ));
beforeEach(async( createComponent ));
it('should have hero.id === 0', () => {
expect(comp.hero.id).toBe(0);
expect(component.hero.id).toBe(0);
});
it('should display empty hero name', () => {
@ -228,14 +234,14 @@ function heroModuleSetup() {
// #docregion route-bad-id
describe('when navigate to non-existent hero id', () => {
beforeEach( async(() => {
activatedRoute.testParamMap = { id: 99999 };
beforeEach(async(() => {
activatedRoute.setParamMap({ id: 99999 });
createComponent();
}));
it('should try to navigate back to hero list', () => {
expect(page.gotoSpy.calls.any()).toBe(true, 'comp.gotoList called');
expect(page.navSpy.calls.any()).toBe(true, 'router.navigate called');
expect(page.gotoListSpy.calls.any()).toBe(true, 'comp.gotoList called');
expect(page.navigateSpy.calls.any()).toBe(true, 'router.navigate called');
});
});
// #enddocregion route-bad-id
@ -263,23 +269,25 @@ import { TitleCasePipe } from '../shared/title-case.pipe';
function formsModuleSetup() {
// #docregion setup-forms-module
beforeEach( async(() => {
TestBed.configureTestingModule({
beforeEach(async(() => {
const routerSpy = createRouterSpy();
TestBed.configureTestingModule({
imports: [ FormsModule ],
declarations: [ HeroDetailComponent, TitleCasePipe ],
providers: [
{ provide: ActivatedRoute, useValue: activatedRoute },
{ provide: HeroService, useClass: FakeHeroService },
{ provide: Router, useClass: RouterStub},
{ provide: HeroService, useClass: TestHeroService },
{ provide: Router, useValue: routerSpy},
]
})
.compileComponents();
}));
// #enddocregion setup-forms-module
it('should display 1st hero\'s name', fakeAsync(() => {
it('should display 1st hero\'s name', async(() => {
const expectedHero = firstHero;
activatedRoute.testParamMap = { id: expectedHero.id };
activatedRoute.setParamMap({ id: expectedHero.id });
createComponent().then(() => {
expect(page.nameDisplay.textContent).toBe(expectedHero.name);
});
@ -291,23 +299,25 @@ import { SharedModule } from '../shared/shared.module';
function sharedModuleSetup() {
// #docregion setup-shared-module
beforeEach( async(() => {
beforeEach(async(() => {
const routerSpy = createRouterSpy();
TestBed.configureTestingModule({
imports: [ SharedModule ],
declarations: [ HeroDetailComponent ],
providers: [
{ provide: ActivatedRoute, useValue: activatedRoute },
{ provide: HeroService, useClass: FakeHeroService },
{ provide: Router, useClass: RouterStub},
{ provide: HeroService, useClass: TestHeroService },
{ provide: Router, useValue: routerSpy},
]
})
.compileComponents();
}));
// #enddocregion setup-shared-module
it('should display 1st hero\'s name', fakeAsync(() => {
it('should display 1st hero\'s name', async(() => {
const expectedHero = firstHero;
activatedRoute.testParamMap = { id: expectedHero.id };
activatedRoute.setParamMap({ id: expectedHero.id });
createComponent().then(() => {
expect(page.nameDisplay.textContent).toBe(expectedHero.name);
});
@ -320,45 +330,51 @@ function sharedModuleSetup() {
/** Create the HeroDetailComponent, initialize it, set test variables */
function createComponent() {
fixture = TestBed.createComponent(HeroDetailComponent);
comp = fixture.componentInstance;
page = new Page();
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();
page.addPageElements();
});
}
// #enddocregion create-component
// #docregion page
class Page {
gotoSpy: jasmine.Spy;
navSpy: jasmine.Spy;
// 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'); }
saveBtn: DebugElement;
cancelBtn: DebugElement;
nameDisplay: HTMLElement;
nameInput: HTMLInputElement;
gotoListSpy: jasmine.Spy;
navigateSpy: jasmine.Spy;
constructor() {
const router = TestBed.get(Router); // get router from root injector
this.gotoSpy = spyOn(comp, 'gotoList').and.callThrough();
this.navSpy = spyOn(router, 'navigate');
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();
}
/** Add page elements after hero arrives */
addPageElements() {
if (comp.hero) {
// have a hero so these elements are now in the DOM
const buttons = fixture.debugElement.queryAll(By.css('button'));
this.saveBtn = buttons[0];
this.cancelBtn = buttons[1];
this.nameDisplay = fixture.debugElement.query(By.css('span')).nativeElement;
this.nameInput = fixture.debugElement.query(By.css('input')).nativeElement;
}
//// query helpers ////
private query<T>(selector: string): T {
return fixture.nativeElement.querySelector(selector);
}
private queryAll<T>(selector: string): T[] {
return fixture.nativeElement.querySelectorAll(selector);
}
}
// #enddocregion page
function createRouterSpy() {
return jasmine.createSpyObj('Router', ['navigate']);
}

View File

@ -2,7 +2,6 @@
// #docplaster
import { Component, Input, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import 'rxjs/add/operator/map';
import { Hero } from '../model/hero';
import { HeroDetailService } from './hero-detail.service';
@ -29,18 +28,18 @@ export class HeroDetailComponent implements OnInit {
// #docregion ng-on-init
ngOnInit(): void {
// get hero when `id` param changes
this.route.paramMap.subscribe(p => this.getHero(p.has('id') && p.get('id')));
this.route.paramMap.subscribe(pmap => this.getHero(pmap.get('id')));
}
// #enddocregion ng-on-init
private getHero(id: string): void {
// when no id or id===0, create new hero
// when no id or id===0, create new blank hero
if (!id) {
this.hero = new Hero();
this.hero = { id: 0, name: '' } as Hero;
return;
}
this.heroDetailService.getHero(id).then(hero => {
this.heroDetailService.getHero(id).subscribe(hero => {
if (hero) {
this.hero = hero;
} else {
@ -50,7 +49,7 @@ export class HeroDetailComponent implements OnInit {
}
save(): void {
this.heroDetailService.saveHero(this.hero).then(() => this.gotoList());
this.heroDetailService.saveHero(this.hero).subscribe(() => this.gotoList());
}
cancel() { this.gotoList(); }

View File

@ -1,5 +1,8 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { map } from 'rxjs/operators';
import { Hero } from '../model/hero';
import { HeroService } from '../model/hero.service';
@ -10,13 +13,15 @@ export class HeroDetailService {
// #enddocregion prototype
// Returns a clone which caller may modify safely
getHero(id: number | string): Promise<Hero> {
getHero(id: number | string): Observable<Hero> {
if (typeof id === 'string') {
id = parseInt(id as string, 10);
}
return this.heroService.getHero(id).then(hero => {
return hero ? Object.assign({}, hero) : null; // clone or null
});
return this.heroService.getHero(id).pipe(
map(hero => {
return hero ? Object.assign({}, hero) : null; // clone or null
})
);
}
saveHero(hero: Hero) {

View File

@ -6,7 +6,7 @@
margin: 0 0 2em 0;
list-style-type: none;
padding: 0;
width: 10em;
width: 15em;
}
.heroes li {
cursor: pointer;

View File

@ -4,15 +4,18 @@ import { async, ComponentFixture, fakeAsync, TestBed, tick
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { addMatchers, newEvent, Router, RouterStub
} from '../../testing';
import { Router } from '@angular/router';
import { HEROES, FakeHeroService } from '../model/testing';
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';
import { HeroService } from '../model/hero.service';
const HEROES = getTestHeroes();
let comp: HeroListComponent;
let fixture: ComponentFixture<HeroListComponent>;
@ -22,13 +25,15 @@ let page: Page;
describe('HeroListComponent', () => {
beforeEach( async(() => {
beforeEach(async(() => {
addMatchers();
const routerSpy = jasmine.createSpyObj('Router', ['navigate']);
TestBed.configureTestingModule({
imports: [HeroModule],
providers: [
{ provide: HeroService, useClass: FakeHeroService },
{ provide: Router, useClass: RouterStub}
{ provide: HeroService, useClass: TestHeroService },
{ provide: Router, useValue: routerSpy}
]
})
.compileComponents()
@ -125,15 +130,14 @@ class Page {
navSpy: jasmine.Spy;
constructor() {
this.heroRows = fixture.debugElement.queryAll(By.css('li')).map(de => de.nativeElement);
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 and spy on it
const router = fixture.debugElement.injector.get(Router);
this.navSpy = spyOn(router, 'navigate');
// Get the component's injected router navigation spy
const routerSpy = fixture.debugElement.injector.get(Router);
this.navSpy = routerSpy.navigate as jasmine.Spy;
};
}

View File

@ -1,6 +1,8 @@
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { Hero } from '../model/hero';
import { HeroService } from '../model/hero.service';
@ -10,7 +12,7 @@ import { HeroService } from '../model/hero.service';
styleUrls: [ './hero-list.component.css' ]
})
export class HeroListComponent implements OnInit {
heroes: Promise<Hero[]>;
heroes: Observable<Hero[]>;
selectedHero: Hero;
constructor(

View File

@ -0,0 +1,26 @@
// #docregion , init
import { InMemoryDbService } from 'angular-in-memory-web-api';
import { QUOTES } from './twain/twain.data';
// Adjust to reduce number of quotes
const maxQuotes = Infinity; // 0;
/** Create in-memory database of heroes and quotes */
export class InMemoryDataService implements InMemoryDbService {
createDb() {
const heroes = [
{ id: 11, name: 'Mr. Nice' },
{ id: 12, name: 'Narco' },
{ id: 13, name: 'Bombasto' },
{ id: 14, name: 'Celeritas' },
{ id: 15, name: 'Magneta' },
{ id: 16, name: 'RubberMan' },
{ id: 17, name: 'Dynama' },
{ id: 18, name: 'Dr IQ' },
{ id: 19, name: 'Magma' },
{ id: 20, name: 'Tornado' }
];
return { heroes, quotes: QUOTES.slice(0, maxQuotes) };
}
}

View File

@ -0,0 +1,215 @@
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
// Other imports
import { TestBed } from '@angular/core/testing';
import { HttpClient, HttpResponse, HttpErrorResponse } from '@angular/common/http';
import { asyncData, asyncError } from '../../testing/async-observable-helpers';
import { Hero } from './hero';
import { HeroService } from './hero.service';
describe ('HeroesService (with spies)', () => {
// #docregion test-with-spies
let httpClientSpy: { get: jasmine.Spy };
let heroService: HeroService;
beforeEach(() => {
// Todo: spy on other methods too
httpClientSpy = jasmine.createSpyObj('HttpClient', ['get']);
heroService = new HeroService(<any> httpClientSpy);
});
it('should return expected heroes (HttpClient called once)', () => {
const expectedHeroes: Hero[] =
[{ id: 1, name: 'A' }, { id: 2, name: 'B' }];
httpClientSpy.get.and.returnValue(asyncData(expectedHeroes));
heroService.getHeroes().subscribe(
heroes => expect(heroes).toEqual(expectedHeroes, 'expected heroes'),
fail
);
expect(httpClientSpy.get.calls.count()).toBe(1, 'one call');
});
it('should return an error when the server returns a 404', () => {
const errorResponse = new HttpErrorResponse({
error: 'test 404 error',
status: 404, statusText: 'Not Found'
});
httpClientSpy.get.and.returnValue(asyncError(errorResponse));
heroService.getHeroes().subscribe(
heroes => fail('expected an error, not heroes'),
error => expect(error.message).toContain('test 404 error')
);
});
// #enddocregion test-with-spies
});
describe('HeroesService (with mocks)', () => {
let httpClient: HttpClient;
let httpTestingController: HttpTestingController;
let heroService: HeroService;
beforeEach(() => {
TestBed.configureTestingModule({
// Import the HttpClient mocking services
imports: [ HttpClientTestingModule ],
// Provide the service-under-test
providers: [ HeroService ]
});
// Inject the http, test controller, and service-under-test
// as they will be referenced by each test.
httpClient = TestBed.get(HttpClient);
httpTestingController = TestBed.get(HttpTestingController);
heroService = TestBed.get(HeroService);
});
afterEach(() => {
// After every test, assert that there are no more pending requests.
httpTestingController.verify();
});
/// HeroService method tests begin ///
describe('#getHeroes', () => {
let expectedHeroes: Hero[];
beforeEach(() => {
heroService = TestBed.get(HeroService);
expectedHeroes = [
{ id: 1, name: 'A' },
{ id: 2, name: 'B' },
] as Hero[];
});
it('should return expected heroes (called once)', () => {
heroService.getHeroes().subscribe(
heroes => expect(heroes).toEqual(expectedHeroes, 'should return expected heroes'),
fail
);
// HeroService should have made one request to GET heroes from expected URL
const req = httpTestingController.expectOne(heroService.heroesUrl);
expect(req.request.method).toEqual('GET');
// Respond with the mock heroes
req.flush(expectedHeroes);
});
it('should be OK returning no heroes', () => {
heroService.getHeroes().subscribe(
heroes => expect(heroes.length).toEqual(0, 'should have empty heroes array'),
fail
);
const req = httpTestingController.expectOne(heroService.heroesUrl);
req.flush([]); // Respond with no heroes
});
it('should turn 404 into a user-friendly error', () => {
const msg = 'Deliberate 404';
heroService.getHeroes().subscribe(
heroes => fail('expected to fail'),
error => expect(error.message).toContain(msg)
);
const req = httpTestingController.expectOne(heroService.heroesUrl);
// respond with a 404 and the error message in the body
req.flush(msg, {status: 404, statusText: 'Not Found'});
});
it('should return expected heroes (called multiple times)', () => {
heroService.getHeroes().subscribe();
heroService.getHeroes().subscribe();
heroService.getHeroes().subscribe(
heroes => expect(heroes).toEqual(expectedHeroes, 'should return expected heroes'),
fail
);
const requests = httpTestingController.match(heroService.heroesUrl);
expect(requests.length).toEqual(3, 'calls to getHeroes()');
// Respond to each request with different mock hero results
requests[0].flush([]);
requests[1].flush([{id: 1, name: 'bob'}]);
requests[2].flush(expectedHeroes);
});
});
describe('#updateHero', () => {
// Expecting the query form of URL so should not 404 when id not found
const makeUrl = (id: number) => `${heroService.heroesUrl}/?id=${id}`;
it('should update a hero and return it', () => {
const updateHero: Hero = { id: 1, name: 'A' };
heroService.updateHero(updateHero).subscribe(
data => expect(data).toEqual(updateHero, 'should return the hero'),
fail
);
// HeroService should have made one request to PUT hero
const req = httpTestingController.expectOne(heroService.heroesUrl);
expect(req.request.method).toEqual('PUT');
expect(req.request.body).toEqual(updateHero);
// Expect server to return the hero after PUT
const expectedResponse = new HttpResponse(
{ status: 200, statusText: 'OK', body: updateHero });
req.event(expectedResponse);
});
it('should turn 404 error into user-facing error', () => {
const msg = 'Deliberate 404';
const updateHero: Hero = { id: 1, name: 'A' };
heroService.updateHero(updateHero).subscribe(
heroes => fail('expected to fail'),
error => expect(error.message).toContain(msg)
);
const req = httpTestingController.expectOne(heroService.heroesUrl);
// respond with a 404 and the error message in the body
req.flush(msg, {status: 404, statusText: 'Not Found'});
});
// #docregion network-error
it('should turn network error into user-facing error', () => {
const emsg = 'simulated network error';
const updateHero: Hero = { id: 1, name: 'A' };
heroService.updateHero(updateHero).subscribe(
heroes => fail('expected to fail'),
error => expect(error.message).toContain(emsg)
);
const req = httpTestingController.expectOne(heroService.heroesUrl);
// Create mock ErrorEvent, raised when something goes wrong at the network level.
// Connection timeout, DNS error, offline, etc
const errorEvent = new ErrorEvent('so sad', {
message: emsg,
// #enddocregion network-error
// The rest of this is optional and not used.
// Just showing that you could provide this too.
filename: 'HeroService.ts',
lineno: 42,
colno: 21
// #docregion network-error
});
// Respond with mock error
req.error(errorEvent);
});
// #enddocregion network-error
});
// TODO: test other HeroService methods
});

View File

@ -1,30 +1,98 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
import { Hero } from './hero';
import { HEROES } from './test-heroes';
import { Observable } from 'rxjs/Observable';
import { of } from 'rxjs/observable/of';
import { catchError, map, tap } from 'rxjs/operators';
import { Hero } from './hero';
const httpOptions = {
headers: new HttpHeaders({ 'Content-Type': 'application/json' })
};
@Injectable()
/** Dummy HeroService. Pretend it makes real http requests */
export class HeroService {
getHeroes() {
return Promise.resolve(HEROES);
readonly heroesUrl = 'api/heroes'; // URL to web api
constructor(private http: HttpClient) { }
/** GET heroes from the server */
getHeroes (): Observable<Hero[]> {
return this.http.get<Hero[]>(this.heroesUrl)
.pipe(
tap(heroes => this.log(`fetched heroes`)),
catchError(this.handleError('getHeroes'))
) as Observable<Hero[]>;
}
getHero(id: number | string): Promise<Hero> {
/** GET hero by id. Return `undefined` when id not found */
getHero<Data>(id: number | string): Observable<Hero> {
if (typeof id === 'string') {
id = parseInt(id as string, 10);
}
return this.getHeroes().then(
heroes => heroes.find(hero => hero.id === id)
const url = `${this.heroesUrl}/?id=${id}`;
return this.http.get<Hero[]>(url)
.pipe(
map(heroes => heroes[0]), // returns a {0|1} element array
tap(h => {
const outcome = h ? `fetched` : `did not find`;
this.log(`${outcome} hero id=${id}`);
}),
catchError(this.handleError<Hero>(`getHero id=${id}`))
);
}
//////// Save methods //////////
/** POST: add a new hero to the server */
addHero (hero: Hero): Observable<Hero> {
return this.http.post<Hero>(this.heroesUrl, hero, httpOptions).pipe(
tap((hero: Hero) => this.log(`added hero w/ id=${hero.id}`)),
catchError(this.handleError<Hero>('addHero'))
);
}
/** DELETE: delete the hero from the server */
deleteHero (hero: Hero | number): Observable<Hero> {
const id = typeof hero === 'number' ? hero : hero.id;
const url = `${this.heroesUrl}/${id}`;
return this.http.delete<Hero>(url, httpOptions).pipe(
tap(_ => this.log(`deleted hero id=${id}`)),
catchError(this.handleError<Hero>('deleteHero'))
);
}
updateHero(hero: Hero): Promise<Hero> {
return this.getHero(hero.id).then(h => {
if (!h) {
throw new Error(`Hero ${hero.id} not found`);
}
return Object.assign(h, hero);
});
/** PUT: update the hero on the server */
updateHero (hero: Hero): Observable<any> {
return this.http.put(this.heroesUrl, hero, httpOptions).pipe(
tap(_ => this.log(`updated hero id=${hero.id}`)),
catchError(this.handleError<any>('updateHero'))
);
}
/**
* Returns a function that handles Http operation failures.
* This error handler lets the app continue to run as if no error occurred.
* @param operation - name of the operation that failed
*/
private handleError<T> (operation = 'operation') {
return (error: HttpErrorResponse): Observable<T> => {
// TODO: send the error to remote logging infrastructure
console.error(error); // log to console instead
const message = (error.error instanceof ErrorEvent) ?
error.error.message :
`server returned code ${error.status} with body "${error.error}"`;
// TODO: better job of transforming error for user consumption
throw new Error(`${operation} failed: ${message}`);
};
}
private log(message: string) {
console.log('HeroService: ' + message);
}
}

View File

@ -1,20 +0,0 @@
// #docregion
import { Hero } from './hero';
describe('Hero', () => {
it('has name', () => {
const hero = new Hero(1, 'Super Cat');
expect(hero.name).toBe('Super Cat');
});
it('has id', () => {
const hero = new Hero(1, 'Super Cat');
expect(hero.id).toBe(1);
});
it('can clone itself', () => {
const hero = new Hero(1, 'Super Cat');
const clone = hero.clone();
expect(hero).toEqual(clone);
});
});

View File

@ -1,4 +1,8 @@
export class Hero {
constructor(public id = 0, public name = '') { }
clone() { return new Hero(this.id, this.name); }
export interface Hero {
id: number;
name: string;
}
// SystemJS bug:
// TS file must export something real in JS, not just interfaces
export const _dummy = undefined;

View File

@ -1,3 +1,6 @@
/**
* Test the HeroService when implemented with the OLD HttpModule
*/
import {
async, inject, TestBed
} from '@angular/core/testing';
@ -12,14 +15,11 @@ import {
} from '@angular/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/toPromise';
import { of } from 'rxjs/observable/of';
import { catchError, tap } from 'rxjs/operators';
import { Hero } from './hero';
import { HttpHeroService as HeroService } from './http-hero.service';
import { HttpHeroService } from './http-hero.service';
const makeHeroData = () => [
{ id: 1, name: 'Windstorm' },
@ -29,99 +29,100 @@ const makeHeroData = () => [
] as Hero[];
//////// Tests /////////////
describe('Http-HeroService (mockBackend)', () => {
describe('HttpHeroService (using old HttpModule)', () => {
let backend: MockBackend;
let service: HttpHeroService;
beforeEach( async(() => {
beforeEach( () => {
TestBed.configureTestingModule({
imports: [ HttpModule ],
providers: [
HeroService,
HttpHeroService,
{ provide: XHRBackend, useClass: MockBackend }
]
})
.compileComponents();
}));
});
});
it('can instantiate service when inject service',
inject([HeroService], (service: HeroService) => {
expect(service instanceof HeroService).toBe(true);
}));
it('can instantiate service via DI', () => {
service = TestBed.get(HttpHeroService);
expect(service instanceof HttpHeroService).toBe(true);
});
it('can instantiate service with "new"', inject([Http], (http: Http) => {
it('can instantiate service with "new"', () => {
const http = TestBed.get(Http);
expect(http).not.toBeNull('http should be provided');
let service = new HeroService(http);
expect(service instanceof HeroService).toBe(true, 'new service should be ok');
}));
let service = new HttpHeroService(http);
expect(service instanceof HttpHeroService).toBe(true, 'new service should be ok');
});
it('can provide the mockBackend as XHRBackend',
inject([XHRBackend], (backend: MockBackend) => {
expect(backend).not.toBeNull('backend should be provided');
}));
it('can provide the mockBackend as XHRBackend', () => {
const backend = TestBed.get(XHRBackend);
expect(backend).not.toBeNull('backend should be provided');
});
describe('when getHeroes', () => {
let backend: MockBackend;
let service: HeroService;
let fakeHeroes: Hero[];
let response: Response;
let fakeHeroes: Hero[];
let http: Http;
let response: Response;
beforeEach(inject([Http, XHRBackend], (http: Http, be: MockBackend) => {
backend = be;
service = new HeroService(http);
fakeHeroes = makeHeroData();
let options = new ResponseOptions({status: 200, body: {data: fakeHeroes}});
response = new Response(options);
}));
beforeEach(() => {
it('should have expected fake heroes (then)', async(inject([], () => {
backend.connections.subscribe((c: MockConnection) => c.mockRespond(response));
backend = TestBed.get(XHRBackend);
http = TestBed.get(Http);
service.getHeroes().toPromise()
// .then(() => Promise.reject('deliberate'))
.then(heroes => {
expect(heroes.length).toBe(fakeHeroes.length,
'should have expected no. of heroes');
});
})));
service = new HttpHeroService(http);
fakeHeroes = makeHeroData();
let options = new ResponseOptions({status: 200, body: {data: fakeHeroes}});
response = new Response(options);
});
it('should have expected fake heroes (Observable.do)', async(inject([], () => {
backend.connections.subscribe((c: MockConnection) => c.mockRespond(response));
it('should have expected fake heroes (then)', () => {
backend.connections.subscribe((c: MockConnection) => c.mockRespond(response));
service.getHeroes()
.do(heroes => {
expect(heroes.length).toBe(fakeHeroes.length,
'should have expected no. of heroes');
})
.toPromise();
})));
service.getHeroes().toPromise()
// .then(() => Promise.reject('deliberate'))
.then(heroes => {
expect(heroes.length).toBe(fakeHeroes.length,
'should have expected no. of heroes');
})
.catch(fail);
});
it('should have expected fake heroes (Observable tap)', () => {
backend.connections.subscribe((c: MockConnection) => c.mockRespond(response));
service.getHeroes().subscribe(
heroes => {
expect(heroes.length).toBe(fakeHeroes.length,
'should have expected no. of heroes');
},
fail
);
});
it('should be OK returning no heroes', async(inject([], () => {
let resp = new Response(new ResponseOptions({status: 200, body: {data: []}}));
backend.connections.subscribe((c: MockConnection) => c.mockRespond(resp));
it('should be OK returning no heroes', () => {
let resp = new Response(new ResponseOptions({status: 200, body: {data: []}}));
backend.connections.subscribe((c: MockConnection) => c.mockRespond(resp));
service.getHeroes()
.do(heroes => {
expect(heroes.length).toBe(0, 'should have no heroes');
})
.toPromise();
})));
service.getHeroes().subscribe(
heroes => {
expect(heroes.length).toBe(0, 'should have no heroes');
},
fail
);
});
it('should treat 404 as an Observable error', async(inject([], () => {
let resp = new Response(new ResponseOptions({status: 404}));
backend.connections.subscribe((c: MockConnection) => c.mockRespond(resp));
it('should treat 404 as an Observable error', () => {
let resp = new Response(new ResponseOptions({status: 404}));
backend.connections.subscribe((c: MockConnection) => c.mockRespond(resp));
service.getHeroes()
.do(heroes => {
fail('should not respond with heroes');
})
.catch(err => {
expect(err).toMatch(/Bad response status/, 'should catch bad response status code');
return Observable.of(null); // failure is the expected test result
})
.toPromise();
})));
service.getHeroes().subscribe(
heroes => fail('should not respond with heroes'),
err => {
expect(err).toMatch(/Bad response status/, 'should catch bad response status code');
return of(null); // failure is the expected test result
});
});
});
});

View File

@ -1,3 +1,4 @@
// The OLD Http module. See HeroService for use of the current HttpClient
// #docplaster
// #docregion
import { Injectable } from '@angular/core';
@ -5,12 +6,9 @@ import { Http, Response } from '@angular/http';
import { Headers, RequestOptions } from '@angular/http';
import { Hero } from './hero';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/throw';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/map';
import { Observable } from 'rxjs/Observable';
import { ErrorObservable } from 'rxjs/observable/ErrorObservable';
import { catchError, map, tap } from 'rxjs/operators';
@Injectable()
export class HttpHeroService {
@ -19,16 +17,17 @@ export class HttpHeroService {
constructor (private http: Http) {}
getHeroes (): Observable<Hero[]> {
return this.http.get(this._heroesUrl)
.map(this.extractData)
// .do(data => console.log(data)) // eyeball results in the console
.catch(this.handleError);
return this.http.get(this._heroesUrl).pipe(
map(this.extractData),
// tap(data => console.log(data)), // eyeball results in the console
catchError(this.handleError)
);
}
getHero(id: number | string) {
return this.http
.get('app/heroes/?id=${id}')
.map((r: Response) => r.json().data as Hero[]);
return this.http.get('app/heroes/?id=${id}').pipe(
map((r: Response) => r.json().data as Hero[])
);
}
addHero (name: string): Observable<Hero> {
@ -36,9 +35,10 @@ export class HttpHeroService {
let headers = new Headers({ 'Content-Type': 'application/json' });
let options = new RequestOptions({ headers: headers });
return this.http.post(this._heroesUrl, body, options)
.map(this.extractData)
.catch(this.handleError);
return this.http.post(this._heroesUrl, body, options).pipe(
map(this.extractData),
catchError(this.handleError)
);
}
updateHero (hero: Hero): Observable<Hero> {
@ -46,9 +46,10 @@ export class HttpHeroService {
let headers = new Headers({ 'Content-Type': 'application/json' });
let options = new RequestOptions({ headers: headers });
return this.http.put(this._heroesUrl, body, options)
.map(this.extractData)
.catch(this.handleError);
return this.http.put(this._heroesUrl, body, options).pipe(
map(this.extractData),
catchError(this.handleError)
);
}
private extractData(res: Response) {
@ -63,6 +64,6 @@ export class HttpHeroService {
// In a real world app, we might send the error to remote logging infrastructure
let errMsg = error.message || 'Server error';
console.error(errMsg); // log to console instead
return Observable.throw(errMsg);
return new ErrorObservable(errMsg);
}
}

View File

@ -1,11 +0,0 @@
// #docregion
import { Hero } from './hero';
export const HEROES: Hero[] = [
new Hero(11, 'Mr. Nice'),
new Hero(12, 'Narco'),
new Hero(13, 'Bombasto'),
new Hero(14, 'Celeritas'),
new Hero(15, 'Magneta'),
new Hero(16, 'RubberMan')
];

View File

@ -1,41 +0,0 @@
// re-export for tester convenience
export { Hero } from '../hero';
export { HeroService } from '../hero.service';
import { Hero } from '../hero';
import { HeroService } from '../hero.service';
export const HEROES: Hero[] = [
new Hero(41, 'Bob'),
new Hero(42, 'Carol'),
new Hero(43, 'Ted'),
new Hero(44, 'Alice'),
new Hero(45, 'Speedy'),
new Hero(46, 'Stealthy')
];
export class FakeHeroService implements HeroService {
heroes = HEROES.map(h => h.clone());
lastPromise: Promise<any>; // remember so we can spy on promise calls
getHero(id: number | string) {
if (typeof id === 'string') {
id = parseInt(id as string, 10);
}
let hero = this.heroes.find(h => h.id === id);
return this.lastPromise = Promise.resolve(hero);
}
getHeroes() {
return this.lastPromise = Promise.resolve<Hero[]>(this.heroes);
}
updateHero(hero: Hero): Promise<Hero> {
return this.lastPromise = this.getHero(hero.id).then(h => {
return h ?
Object.assign(h, hero) :
Promise.reject(`Hero ${hero.id} not found`) as any as Promise<Hero>;
});
}
}

View File

@ -0,0 +1,192 @@
// #docplaster
// #docregion imports
// Http testing module and mocking controller
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
// Other imports
import { TestBed } from '@angular/core/testing';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
// #enddocregion imports
import { HttpHeaders } from '@angular/common/http';
interface Data {
name: string;
}
const testUrl = '/data';
// #docregion setup
describe('HttpClient testing', () => {
let httpClient: HttpClient;
let httpTestingController: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ HttpClientTestingModule ]
});
// Inject the http service and test controller for each test
httpClient = TestBed.get(HttpClient);
httpTestingController = TestBed.get(HttpTestingController);
});
// #enddocregion setup
// #docregion afterEach
afterEach(() => {
// After every test, assert that there are no more pending requests.
httpTestingController.verify();
});
// #enddocregion afterEach
// #docregion setup
/// Tests begin ///
// #enddocregion setup
// #docregion get-test
it('can test HttpClient.get', () => {
const testData: Data = {name: 'Test Data'};
// Make an HTTP GET request
httpClient.get<Data>(testUrl)
.subscribe(data =>
// When observable resolves, result should match test data
expect(data).toEqual(testData)
);
// The following `expectOne()` will match the request's URL.
// If no requests or multiple requests matched that URL
// `expectOne()` would throw.
const req = httpTestingController.expectOne('/data');
// Assert that the request is a GET.
expect(req.request.method).toEqual('GET');
// Respond with mock data, causing Observable to resolve.
// Subscribe callback asserts that correct data was returned.
req.flush(testData);
// Finally, assert that there are no outstanding requests.
httpTestingController.verify();
});
// #enddocregion get-test
it('can test HttpClient.get with matching header', () => {
const testData: Data = {name: 'Test Data'};
// Make an HTTP GET request with specific header
httpClient.get<Data>(testUrl, {
headers: new HttpHeaders({'Authorization': 'my-auth-token'})
})
.subscribe(data =>
expect(data).toEqual(testData)
);
// Find request with a predicate function.
// #docregion predicate
// Expect one request with an authorization header
const req = httpTestingController.expectOne(
req => req.headers.has('Authorization')
);
// #enddocregion predicate
req.flush(testData);
});
it('can test multiple requests', () => {
let testData: Data[] = [
{ name: 'bob' }, { name: 'carol' },
{ name: 'ted' }, { name: 'alice' }
];
// Make three requests in a row
httpClient.get<Data[]>(testUrl)
.subscribe(d => expect(d.length).toEqual(0, 'should have no data'));
httpClient.get<Data[]>(testUrl)
.subscribe(d => expect(d).toEqual([testData[0]], 'should be one element array'));
httpClient.get<Data[]>(testUrl)
.subscribe(d => expect(d).toEqual(testData, 'should be expected data'));
// #docregion multi-request
// get all pending requests that match the given URL
const requests = httpTestingController.match(testUrl);
expect(requests.length).toEqual(3);
// Respond to each request with different results
requests[0].flush([]);
requests[1].flush([testData[0]]);
requests[2].flush(testData);
// #enddocregion multi-request
});
// #docregion 404
it('can test for 404 error', () => {
const emsg = 'deliberate 404 error';
httpClient.get<Data[]>(testUrl).subscribe(
data => fail('should have failed with the 404 error'),
(error: HttpErrorResponse) => {
expect(error.status).toEqual(404, 'status');
expect(error.error).toEqual(emsg, 'message');
}
);
const req = httpTestingController.expectOne(testUrl);
// Respond with mock error
req.flush(emsg, { status: 404, statusText: 'Not Found' });
});
// #enddocregion 404
// #docregion network-error
it('can test for network error', () => {
const emsg = 'simulated network error';
httpClient.get<Data[]>(testUrl).subscribe(
data => fail('should have failed with the network error'),
(error: HttpErrorResponse) => {
expect(error.error.message).toEqual(emsg, 'message');
}
);
const req = httpTestingController.expectOne(testUrl);
// Create mock ErrorEvent, raised when something goes wrong at the network level.
// Connection timeout, DNS error, offline, etc
const errorEvent = new ErrorEvent('so sad', {
message: emsg,
// #enddocregion network-error
// The rest of this is optional and not used.
// Just showing that you could provide this too.
filename: 'HeroService.ts',
lineno: 42,
colno: 21
// #docregion network-error
});
// Respond with mock error
req.error(errorEvent);
});
// #enddocregion network-error
it('httpTestingController.verify should fail if HTTP response not simulated', () => {
// Sends request
httpClient.get('some/api').subscribe();
// verify() should fail because haven't handled the pending request.
expect(() => httpTestingController.verify()).toThrow();
// Now get and flush the request so that afterEach() doesn't fail
const req = httpTestingController.expectOne('some/api');
req.flush(null);
});
// Proves that verify in afterEach() really would catch error
// if test doesn't simulate the HTTP response.
//
// Must disable this test because can't catch an error in an afterEach().
// Uncomment if you want to confirm that afterEach() does the job.
// it('afterEach() should fail when HTTP response not simulated',() => {
// // Sends request which is never handled by this test
// httpClient.get('some/api').subscribe();
// });
// #docregion setup
});
// #enddocregion setup

View File

@ -0,0 +1,61 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { asyncData } from '../../../testing';
import { map } from 'rxjs/operators';
// re-export for tester convenience
export { Hero } from '../hero';
export { HeroService } from '../hero.service';
export { getTestHeroes } from './test-heroes';
import { Hero } from '../hero';
import { HeroService } from '../hero.service';
import { getTestHeroes } from './test-heroes';
@Injectable()
/**
* FakeHeroService pretends to make real http requests.
* implements only as much of HeroService as is actually consumed by the app
*/
export class TestHeroService extends HeroService {
constructor() {
super(null);
}
heroes = getTestHeroes();
lastResult: Observable<any>; // result from last method call
addHero(hero: Hero): Observable<Hero> {
throw new Error('Method not implemented.');
}
deleteHero(hero: number | Hero): Observable<Hero> {
throw new Error('Method not implemented.');
}
getHeroes(): Observable<Hero[]> {
return this.lastResult = asyncData(this.heroes);
}
getHero(id: number | string): Observable<Hero> {
if (typeof id === 'string') {
id = parseInt(id as string, 10);
}
let hero = this.heroes.find(h => h.id === id);
return this.lastResult = asyncData(hero);
}
updateHero(hero: Hero): Observable<Hero> {
return this.lastResult = this.getHero(hero.id).pipe(
map(h => {
if (h) {
return Object.assign(h, hero);
}
throw new Error(`Hero ${hero.id} not found`);
})
);
}
}

View File

@ -0,0 +1,13 @@
import { Hero } from '../hero';
/** return fresh array of test heroes */
export function getTestHeroes(): Hero[] {
return [
{id: 41, name: 'Bob' },
{id: 42, name: 'Carol' },
{id: 43, name: 'Ted' },
{id: 44, name: 'Alice' },
{id: 45, name: 'Speedy' },
{id: 46, name: 'Stealthy' }
];
}

View File

@ -4,12 +4,16 @@ import { FormsModule } from '@angular/forms';
import { HighlightDirective } from './highlight.directive';
import { TitleCasePipe } from './title-case.pipe';
import { TwainComponent } from './twain.component';
@NgModule({
imports: [ CommonModule ],
exports: [ CommonModule, FormsModule,
HighlightDirective, TitleCasePipe, TwainComponent ],
declarations: [ HighlightDirective, TitleCasePipe, TwainComponent ]
imports: [ CommonModule ],
exports: [
CommonModule,
// SharedModule importers won't have to import FormsModule too
FormsModule,
HighlightDirective,
TitleCasePipe
],
declarations: [ HighlightDirective, TitleCasePipe ]
})
export class SharedModule { }

View File

@ -1,92 +0,0 @@
// #docplaster
import { async, fakeAsync, ComponentFixture, TestBed, tick } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { TwainService } from './twain.service';
import { TwainComponent } from './twain.component';
describe('TwainComponent', () => {
let comp: TwainComponent;
let fixture: ComponentFixture<TwainComponent>;
let spy: jasmine.Spy;
let de: DebugElement;
let el: HTMLElement;
let twainService: TwainService; // the actually injected service
const testQuote = 'Test Quote';
// #docregion setup
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ TwainComponent ],
providers: [ TwainService ],
});
fixture = TestBed.createComponent(TwainComponent);
comp = fixture.componentInstance;
// TwainService actually injected into the component
twainService = fixture.debugElement.injector.get(TwainService);
// Setup spy on the `getQuote` method
// #docregion spy
spy = spyOn(twainService, 'getQuote')
.and.returnValue(Promise.resolve(testQuote));
// #enddocregion spy
// Get the Twain quote element by CSS selector (e.g., by class name)
de = fixture.debugElement.query(By.css('.twain'));
el = de.nativeElement;
});
// #enddocregion setup
// #docregion tests
it('should not show quote before OnInit', () => {
expect(el.textContent).toBe('', 'nothing displayed');
expect(spy.calls.any()).toBe(false, 'getQuote not yet called');
});
it('should still not show quote after component initialized', () => {
fixture.detectChanges();
// getQuote service is async => still has not returned with quote
expect(el.textContent).toBe('...', 'no quote yet');
expect(spy.calls.any()).toBe(true, 'getQuote called');
});
// #docregion async-test
it('should show quote after getQuote promise (async)', async(() => {
fixture.detectChanges();
fixture.whenStable().then(() => { // wait for async getQuote
fixture.detectChanges(); // update view with quote
expect(el.textContent).toBe(testQuote);
});
}));
// #enddocregion async-test
// #docregion fake-async-test
it('should show quote after getQuote promise (fakeAsync)', fakeAsync(() => {
fixture.detectChanges();
tick(); // wait for async getQuote
fixture.detectChanges(); // update view with quote
expect(el.textContent).toBe(testQuote);
}));
// #enddocregion fake-async-test
// #enddocregion tests
// #docregion done-test
it('should show quote after getQuote promise (done)', (done: any) => {
fixture.detectChanges();
// get the spy promise and wait for it to resolve
spy.calls.mostRecent().returnValue.then(() => {
fixture.detectChanges(); // update view with quote
expect(el.textContent).toBe(testQuote);
done();
});
});
// #enddocregion done-test
});

View File

@ -1,116 +0,0 @@
// #docplaster
// When AppComponent learns to present quote with intervalTimer
import { async, discardPeriodicTasks, fakeAsync, ComponentFixture, TestBed, tick } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { TwainService } from './model';
import { TwainComponent } from './twain.component';
xdescribe('TwainComponent', () => {
let comp: TwainComponent;
let fixture: ComponentFixture<TwainComponent>;
const quotes = [
'Test Quote 1',
'Test Quote 2',
'Test Quote 3'
];
let spy: jasmine.Spy;
let twainEl: DebugElement; // the element with the Twain quote
let twainService: TwainService; // the actually injected service
function getQuote() { return twainEl.nativeElement.textContent; }
// #docregion setup
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ TwainComponent ],
providers: [ TwainService ],
});
fixture = TestBed.createComponent(TwainComponent);
comp = fixture.componentInstance;
// TwainService actually injected into the component
twainService = fixture.debugElement.injector.get(TwainService);
// Setup spy on the `getQuote` method
spy = spyOn(twainService, 'getQuote')
.and.returnValues(...quotes.map(q => Promise.resolve(q)));
// Get the Twain quote element by CSS selector (e.g., by class name)
twainEl = fixture.debugElement.query(By.css('.twain'));
});
afterEach(() => {
// destroy component to stop the component timer
fixture.destroy();
});
// #enddocregion setup
// #docregion tests
it('should not show quote before OnInit', () => {
expect(getQuote()).toBe('');
});
it('should still not show quote after component initialized', () => {
// because the getQuote service is async
fixture.detectChanges(); // trigger data binding
expect(getQuote()).toContain('not initialized');
});
// WIP
// If go this way, add jasmine.clock().uninstall(); to afterEach
// it('should show quote after Angular "settles"', async(() => {
// //jasmine.clock().install();
// fixture.detectChanges(); // trigger data binding
// fixture.whenStable().then(() => {
// fixture.detectChanges(); // update view with the quote
// expect(getQuote()).toBe(quotes[0]);
// });
// // jasmine.clock().tick(5000);
// // fixture.whenStable().then(() => {
// // fixture.detectChanges(); // update view with the quote
// // expect(getQuote()).toBe(quotes[1]);
// // });
// }));
it('should show quote after getQuote promise returns', fakeAsync(() => {
fixture.detectChanges(); // trigger data binding
tick(); // wait for first async getQuote to return
fixture.detectChanges(); // update view with the quote
expect(getQuote()).toBe(quotes[0]);
// destroy component to stop the component timer before test ends
// else test errors because still have timer in the queue
fixture.destroy();
}));
it('should show 2nd quote after 5 seconds pass', fakeAsync(() => {
fixture.detectChanges(); // trigger data binding
tick(5000); // wait for second async getQuote to return
fixture.detectChanges(); // update view with the quote
expect(getQuote()).toBe(quotes[1]);
// still have intervalTimer queuing requres
// discardPeriodicTasks() else test errors
discardPeriodicTasks();
}));
fit('should show 3rd quote after 10 seconds pass', fakeAsync(() => {
fixture.detectChanges(); // trigger data binding
tick(5000); // wait for second async getQuote to return
fixture.detectChanges(); // update view with the 2nd quote
tick(5000); // wait for third async getQuote to return
fixture.detectChanges(); // update view with the 3rd quote
expect(getQuote()).toBe(quotes[2]);
// still have intervalTimer queuing requres
// discardPeriodicTasks() else test errors
discardPeriodicTasks();
}));
// #enddocregion tests
});

View File

@ -1,27 +0,0 @@
// #docregion
import { Component, OnInit, OnDestroy } from '@angular/core';
import { TwainService } from './twain.service';
@Component({
selector: 'twain-quote',
template: '<p class="twain"><i>{{quote}}</i></p>'
})
export class TwainComponent implements OnInit, OnDestroy {
intervalId: number;
quote = '-- not initialized yet --';
constructor(private twainService: TwainService) { }
getQuote() {
this.twainService.getQuote().then(quote => this.quote = quote);
}
ngOnInit(): void {
this.getQuote();
this.intervalId = window.setInterval(() => this.getQuote(), 5000);
}
ngOnDestroy(): void {
clearInterval(this.intervalId);
}
}

View File

@ -1,20 +0,0 @@
// #docregion
import { Component, OnInit } from '@angular/core';
import { TwainService } from './twain.service';
// #docregion component
@Component({
selector: 'twain-quote',
template: '<p class="twain"><i>{{quote}}</i></p>'
})
export class TwainComponent implements OnInit {
intervalId: number;
quote = '...';
constructor(private twainService: TwainService) { }
ngOnInit(): void {
this.twainService.getQuote().then(quote => this.quote = quote);
}
}
// #enddocregion component

View File

@ -1,32 +0,0 @@
import { Injectable } from '@angular/core';
const quotes = [
'Always do right. This will gratify some people and astonish the rest.',
'I have never let my schooling interfere with my education.',
'Don\'t go around saying the world owes you a living. The world owes you nothing. It was here first.',
'Whenever you find yourself on the side of the majority, it is time to pause and reflect.',
'If you tell the truth, you don\'t have to remember anything.',
'Clothes make the man. Naked people have little or no influence on society.',
'It\'s not the size of the dog in the fight, it\'s the size of the fight in the dog.',
'Truth is stranger than fiction, but it is because Fiction is obliged to stick to possibilities; Truth isn\'t.',
'The man who does not read good books has no advantage over the man who cannot read them.',
'Get your facts first, and then you can distort them as much as you please.',
];
@Injectable()
export class TwainService {
private next = 0;
// Imaginary todo: get quotes from a remote quote service
// returns quote after delay simulating server latency
getQuote(): Promise<string> {
return new Promise(resolve => {
setTimeout( () => resolve(this.nextQuote()), 500 );
});
}
private nextQuote() {
if (this.next === quotes.length) { this.next = 0; }
return quotes[ this.next++ ];
}
}

View File

@ -0,0 +1,4 @@
export class Quote {
id: number;
quote: string;
}

View File

@ -0,0 +1,93 @@
// #docplaster
import { async, fakeAsync, ComponentFixture, TestBed, tick } from '@angular/core/testing';
// #docregion import-marbles
import { cold, getTestScheduler } from 'jasmine-marbles';
// #enddocregion import-marbles
import { Observable } from 'rxjs/Observable';
import { of } from 'rxjs/observable/of';
import { ErrorObservable } from 'rxjs/observable/ErrorObservable';
import { last } from 'rxjs/operators';
import { TwainService } from './twain.service';
import { TwainComponent } from './twain.component';
describe('TwainComponent (marbles)', () => {
let component: TwainComponent;
let fixture: ComponentFixture<TwainComponent>;
let getQuoteSpy: jasmine.Spy;
let quoteEl: HTMLElement;
let testQuote: string;
// Helper function to get the error message element value
// An *ngIf keeps it out of the DOM until there is an error
const errorMessage = () => {
const el = fixture.nativeElement.querySelector('.error');
return el ? el.textContent : null;
};
beforeEach(() => {
// Create a fake TwainService object with a `getQuote()` spy
const twainService = jasmine.createSpyObj('TwainService', ['getQuote']);
getQuoteSpy = twainService.getQuote;
TestBed.configureTestingModule({
declarations: [ TwainComponent ],
providers: [
{ provide: TwainService, useValue: twainService }
]
});
fixture = TestBed.createComponent(TwainComponent);
component = fixture.componentInstance;
quoteEl = fixture.nativeElement.querySelector('.twain');
testQuote = 'Test Quote';
});
// A synchronous test that simulates async behavior
// #docregion get-quote-test
it('should show quote after getQuote (marbles)', () => {
// observable test quote value and complete(), after delay
// #docregion test-quote-marbles
const q$ = cold('---x|', { x: testQuote });
// #enddocregion test-quote-marbles
getQuoteSpy.and.returnValue( q$ );
fixture.detectChanges(); // ngOnInit()
expect(quoteEl.textContent).toBe('...', 'should show placeholder');
// #docregion test-scheduler-flush
getTestScheduler().flush(); // flush the observables
// #enddocregion test-scheduler-flush
fixture.detectChanges(); // update view
expect(quoteEl.textContent).toBe(testQuote, 'should show quote');
expect(errorMessage()).toBeNull('should not show error');
});
// #enddocregion get-quote-test
// Still need fakeAsync() because of component's setTimeout()
// #docregion error-test
it('should display error when TwainService fails', fakeAsync(() => {
// observable error after delay
// #docregion error-marbles
const q$ = cold('---#|', null, new Error('TwainService test failure'));
// #enddocregion error-marbles
getQuoteSpy.and.returnValue( q$ );
fixture.detectChanges(); // ngOnInit()
expect(quoteEl.textContent).toBe('...', 'should show placeholder');
getTestScheduler().flush(); // flush the observables
tick(); // component shows error after a setTimeout()
fixture.detectChanges(); // update error message
expect(errorMessage()).toMatch(/test failure/, 'should display error');
expect(quoteEl.textContent).toBe('...', 'should show placeholder');
}));
// #enddocregion error-test
});

View File

@ -0,0 +1,184 @@
// #docplaster
import { async, fakeAsync, ComponentFixture, TestBed, tick } from '@angular/core/testing';
import { asyncData, asyncError } from '../../testing';
import { Observable } from 'rxjs/Observable';
import { of } from 'rxjs/observable/of';
import { ErrorObservable } from 'rxjs/observable/ErrorObservable';
import { last } from 'rxjs/operators';
import { TwainService } from './twain.service';
import { TwainComponent } from './twain.component';
describe('TwainComponent', () => {
let component: TwainComponent;
let fixture: ComponentFixture<TwainComponent>;
let getQuoteSpy: jasmine.Spy;
let quoteEl: HTMLElement;
let testQuote: string;
// Helper function to get the error message element value
// An *ngIf keeps it out of the DOM until there is an error
const errorMessage = () => {
const el = fixture.nativeElement.querySelector('.error');
return el ? el.textContent : null;
};
// #docregion setup
beforeEach(() => {
testQuote = 'Test Quote';
// #docregion spy
// Create a fake TwainService object with a `getQuote()` spy
const twainService = jasmine.createSpyObj('TwainService', ['getQuote']);
// Make the spy return a synchronous Observable with the test data
getQuoteSpy = twainService.getQuote.and.returnValue( of(testQuote) );
// #enddocregion spy
TestBed.configureTestingModule({
declarations: [ TwainComponent ],
providers: [
{ provide: TwainService, useValue: twainService }
]
});
fixture = TestBed.createComponent(TwainComponent);
component = fixture.componentInstance;
quoteEl = fixture.nativeElement.querySelector('.twain');
});
// #enddocregion setup
describe('when test with synchronous observable', () => {
it('should not show quote before OnInit', () => {
expect(quoteEl.textContent).toBe('', 'nothing displayed');
expect(errorMessage()).toBeNull('should not show error element');
expect(getQuoteSpy.calls.any()).toBe(false, 'getQuote not yet called');
});
// The quote would not be immediately available if the service were truly async.
// #docregion sync-test
it('should show quote after component initialized', () => {
fixture.detectChanges(); // onInit()
// sync spy result shows testQuote immediately after init
expect(quoteEl.textContent).toBe(testQuote);
expect(getQuoteSpy.calls.any()).toBe(true, 'getQuote called');
});
// #enddocregion sync-test
// The error would not be immediately available if the service were truly async.
// Use `fakeAsync` because the component error calls `setTimeout`
// #docregion error-test
it('should display error when TwainService fails', fakeAsync(() => {
// tell spy to return an error observable
getQuoteSpy.and.returnValue(
new ErrorObservable('TwainService test failure'));
fixture.detectChanges(); // onInit()
// sync spy errors immediately after init
tick(); // flush the component's setTimeout()
fixture.detectChanges(); // update errorMessage within setTimeout()
expect(errorMessage()).toMatch(/test failure/, 'should display error');
expect(quoteEl.textContent).toBe('...', 'should show placeholder');
}));
// #enddocregion error-test
});
describe('when test with asynchronous observable', () => {
beforeEach(() => {
// #docregion async-setup
// Simulate delayed observable values with the `asyncData()` helper
getQuoteSpy.and.returnValue(asyncData(testQuote));
// #enddocregion async-setup
});
it('should not show quote before OnInit', () => {
expect(quoteEl.textContent).toBe('', 'nothing displayed');
expect(errorMessage()).toBeNull('should not show error element');
expect(getQuoteSpy.calls.any()).toBe(false, 'getQuote not yet called');
});
it('should still not show quote after component initialized', () => {
fixture.detectChanges();
// getQuote service is async => still has not returned with quote
// so should show the start value, '...'
expect(quoteEl.textContent).toBe('...', 'should show placeholder');
expect(errorMessage()).toBeNull('should not show error');
expect(getQuoteSpy.calls.any()).toBe(true, 'getQuote called');
});
// #docregion fake-async-test
it('should show quote after getQuote (fakeAsync)', fakeAsync(() => {
fixture.detectChanges(); // ngOnInit()
expect(quoteEl.textContent).toBe('...', 'should show placeholder');
tick(); // flush the observable to get the quote
fixture.detectChanges(); // update view
expect(quoteEl.textContent).toBe(testQuote, 'should show quote');
expect(errorMessage()).toBeNull('should not show error');
}));
// #enddocregion fake-async-test
// #docregion async-test
it('should show quote after getQuote (async)', async(() => {
fixture.detectChanges(); // ngOnInit()
expect(quoteEl.textContent).toBe('...', 'should show placeholder');
fixture.whenStable().then(() => { // wait for async getQuote
fixture.detectChanges(); // update view with quote
expect(quoteEl.textContent).toBe(testQuote);
expect(errorMessage()).toBeNull('should not show error');
});
}));
// #enddocregion async-test
// #docregion quote-done-test
it('should show last quote (quote done)', (done: DoneFn) => {
fixture.detectChanges();
component.quote.pipe( last() ).subscribe(() => {
fixture.detectChanges(); // update view with quote
expect(quoteEl.textContent).toBe(testQuote);
expect(errorMessage()).toBeNull('should not show error');
done();
});
});
// #enddocregion quote-done-test
// #docregion spy-done-test
it('should show quote after getQuote (spy done)', (done: DoneFn) => {
fixture.detectChanges();
// the spy's most recent call returns the observable with the test quote
getQuoteSpy.calls.mostRecent().returnValue.subscribe(() => {
fixture.detectChanges(); // update view with quote
expect(quoteEl.textContent).toBe(testQuote);
expect(errorMessage()).toBeNull('should not show error');
done();
});
});
// #enddocregion spy-done-test
// #docregion async-error-test
it('should display error when TwainService fails', fakeAsync(() => {
// tell spy to return an async error observable
getQuoteSpy.and.returnValue(asyncError<string>('TwainService test failure'));
fixture.detectChanges();
tick(); // component shows error after a setTimeout()
fixture.detectChanges(); // update error message
expect(errorMessage()).toMatch(/test failure/, 'should display error');
expect(quoteEl.textContent).toBe('...', 'should show placeholder');
}));
// #enddocregion async-error-test
});
});

View File

@ -0,0 +1,49 @@
// #docregion
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { of } from 'rxjs/observable/of';
import { catchError, startWith } from 'rxjs/operators';
import { TwainService } from './twain.service';
// #docregion component
@Component({
selector: 'twain-quote',
// #docregion template
template: `
<p class="twain"><i>{{quote | async}}</i></p>
<button (click)="getQuote()">Next quote</button>
<p class="error" *ngIf="errorMessage">{{ errorMessage }}</p>`,
// #enddocregion template
styles: [
`.twain { font-style: italic; } .error { color: red; }`
]
})
export class TwainComponent implements OnInit {
errorMessage: string;
quote: Observable<string>;
constructor(private twainService: TwainService) {}
ngOnInit(): void {
this.getQuote();
}
// #docregion get-quote
getQuote() {
this.errorMessage = '';
this.quote = this.twainService.getQuote().pipe(
startWith('...'),
catchError( (err: any) => {
// Wait a turn because errorMessage already set once this turn
setTimeout(() => this.errorMessage = err.message || err.toString());
return of('...'); // reset message to placeholder
})
);
// #enddocregion get-quote
}
}
// #enddocregion component

View File

@ -0,0 +1,15 @@
import { Quote } from './quote';
export const QUOTES: Quote[] = [
'Always do right. This will gratify some people and astonish the rest.',
'I have never let my schooling interfere with my education.',
'Don\'t go around saying the world owes you a living. The world owes you nothing. It was here first.',
'Whenever you find yourself on the side of the majority, it is time to pause and reflect.',
'If you tell the truth, you don\'t have to remember anything.',
'Clothes make the man. Naked people have little or no influence on society.',
'It\'s not the size of the dog in the fight, it\'s the size of the fight in the dog.',
'Truth is stranger than fiction, but it is because Fiction is obliged to stick to possibilities; Truth isn\'t.',
'The man who does not read good books has no advantage over the man who cannot read them.',
'Get your facts first, and then you can distort them as much as you please.',
]
.map((q, i) => ({ id: i + 1, quote: q }));

View File

@ -0,0 +1,47 @@
// Mark Twain Quote service gets quotes from server
import { Injectable } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import { ErrorObservable } from 'rxjs/observable/ErrorObservable';
import { of } from 'rxjs/observable/of';
import { concat, map, retryWhen, switchMap, take, tap } from 'rxjs/operators';
import { Quote } from './quote';
@Injectable()
export class TwainService {
constructor(private http: HttpClient) { }
private nextId = 1;
getQuote(): Observable<string> {
return Observable.create(observer => observer.next(this.nextId++)).pipe(
// tap((id: number) => console.log(id)),
// tap((id: number) => { throw new Error('Simulated server error'); }),
switchMap((id: number) => this.http.get<Quote>(`api/quotes/${id}`)),
// tap((q : Quote) => console.log(q)),
map((q: Quote) => q.quote),
// `errors` is observable of http.get errors
retryWhen(errors => errors.pipe(
switchMap((error: HttpErrorResponse) => {
if (error.status === 404) {
// Queried for quote that doesn't exist.
this.nextId = 1; // retry with quote id:1
return of(null); // signal OK to retry
}
// Some other HTTP error.
console.error(error);
return new ErrorObservable('Cannot get Twain quotes from the server');
}),
take(2),
// If a second retry value, then didn't find id:1 and triggers the following error
concat(new ErrorObservable('There are no Twain quotes')) // didn't find id:1
))
);
}
}

View File

@ -1,26 +1,66 @@
// #docplaster
import { ComponentFixture, inject, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { UserService } from './model';
import { UserService } from '../model/user.service';
import { WelcomeComponent } from './welcome.component';
// #docregion mock-user-service
class MockUserService {
isLoggedIn = true;
user = { name: 'Test User'};
};
// #enddocregion mock-user-service
describe('WelcomeComponent (class only)', () => {
let comp: WelcomeComponent;
let userService: UserService;
// #docregion class-only-before-each
beforeEach(() => {
TestBed.configureTestingModule({
// provide the component-under-test and dependent service
providers: [
WelcomeComponent,
{ provide: UserService, useClass: MockUserService }
]
});
// inject both the component and the dependent service.
comp = TestBed.get(WelcomeComponent);
userService = TestBed.get(UserService);
});
// #enddocregion class-only-before-each
// #docregion class-only-tests
it('should not have welcome message after construction', () => {
expect(comp.welcome).toBeUndefined();
});
it('should welcome logged in user after Angular calls ngOnInit', () => {
comp.ngOnInit();
expect(comp.welcome).toContain(userService.user.name);
});
it('should ask user to log in if not logged in after ngOnInit', () => {
userService.isLoggedIn = false;
comp.ngOnInit();
expect(comp.welcome).not.toContain(userService.user.name);
expect(comp.welcome).toContain('log in');
});
// #enddocregion class-only-tests
});
describe('WelcomeComponent', () => {
let comp: WelcomeComponent;
let fixture: ComponentFixture<WelcomeComponent>;
let componentUserService: UserService; // the actually injected service
let userService: UserService; // the TestBed injected service
let de: DebugElement; // the DebugElement with the welcome message
let el: HTMLElement; // the DOM element with the welcome message
let userServiceStub: {
isLoggedIn: boolean;
user: { name: string}
};
// #docregion setup, user-service-stub
let userServiceStub: Partial<UserService>;
// #docregion setup
// #enddocregion user-service-stub
beforeEach(() => {
// stub UserService for test purposes
// #docregion user-service-stub
@ -57,8 +97,7 @@ describe('WelcomeComponent', () => {
// #enddocregion inject-from-testbed
// get the "welcome" element by CSS selector (e.g., by class name)
de = fixture.debugElement.query(By.css('.welcome'));
el = de.nativeElement;
el = fixture.nativeElement.querySelector('.welcome');
});
// #enddocregion setup
@ -85,12 +124,10 @@ describe('WelcomeComponent', () => {
});
// #enddocregion tests
// #docregion inject-it
it('should inject the component\'s UserService instance',
inject([UserService], (service: UserService) => {
expect(service).toBe(componentUserService);
}));
// #enddocregion inject-it
it('TestBed and Component UserService should be the same', () => {
expect(userService === componentUserService).toBe(true);

View File

@ -1,19 +1,20 @@
// #docregion
import { Component, OnInit } from '@angular/core';
import { UserService } from '../model/user.service';
import { UserService } from './model/user.service';
// #docregion component
@Component({
selector: 'app-welcome',
template: '<h3 class="welcome" ><i>{{welcome}}</i></h3>'
template: '<h3 class="welcome"><i>{{welcome}}</i></h3>'
})
// #docregion class
export class WelcomeComponent implements OnInit {
welcome = '-- not initialized yet --';
welcome: string;
constructor(private userService: UserService) { }
ngOnInit(): void {
this.welcome = this.userService.isLoggedIn ?
'Welcome, ' + this.userService.user.name :
'Please log in.';
'Welcome, ' + this.userService.user.name : 'Please log in.';
}
}
// #enddocregion component, class

View File

@ -1,26 +0,0 @@
<!-- Run the bag source as an app -->
<!DOCTYPE html>
<html>
<head>
<base href="/">
<title>Specs Bag</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
<!-- Polyfills -->
<script src="node_modules/core-js/client/shim.min.js"></script>
<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="systemjs.config.js"></script>
<script>
System.import('app/bag/bag-main').catch(function(err){ console.error(err); });
</script>
</head>
<body>
<bag-comp>Loading ...</bag-comp>
</body>
</html>

View File

@ -0,0 +1,29 @@
// export for convenience.
export { ActivatedRoute } from '@angular/router';
// #docregion activated-route-stub
import { ReplaySubject } from 'rxjs/ReplaySubject';
import { convertToParamMap, ParamMap, Params } from '@angular/router';
/**
* An ActivateRoute test double with a `paramMap` observable.
* Use the `setParamMap()` method to add the next `paramMap` value.
*/
export class ActivatedRouteStub {
// Use a ReplaySubject to share previous values with subscribers
// and pump new values into the `paramMap` observable
private subject = new ReplaySubject<ParamMap>();
constructor(initialParams?: Params) {
this.setParamMap(initialParams);
}
/** The mock paramMap observable */
readonly paramMap = this.subject.asObservable();
/** Set the paramMap observables's next value */
setParamMap(params?: Params) {
this.subject.next(convertToParamMap(params));
};
}
// #enddocregion activated-route-stub

View File

@ -0,0 +1,30 @@
/*
* Mock async observables that return asynchronously.
* The observable either emits once and completes or errors.
*
* Must call `tick()` when test with `fakeAsync()`.
*
* THE FOLLOWING DON'T WORK
* Using `of().delay()` triggers TestBed errors;
* see https://github.com/angular/angular/issues/10127 .
*
* Using `asap` scheduler - as in `of(value, asap)` - doesn't work either.
*/
import { Observable } from 'rxjs/Observable';
import { defer } from 'rxjs/observable/defer';
// #docregion async-data
/** Create async observable that emits-once and completes
* after a JS engine turn */
export function asyncData<T>(data: T) {
return defer(() => Promise.resolve(data));
}
// #enddocregion async-data
// #docregion async-error
/** Create async observable error that errors
* after a JS engine turn */
export function asyncError<T>(errorObject: any) {
return defer(() => Promise.reject(errorObject));
}
// #enddocregion async-error

View File

@ -1,8 +1,10 @@
import { DebugElement } from '@angular/core';
import { tick, ComponentFixture } from '@angular/core/testing';
export * from './async-observable-helpers';
export * from './activated-route-stub';
export * from './jasmine-matchers';
export * from './router-stubs';
export * from './router-link-directive-stub';
///// Short utilities /////

View File

@ -0,0 +1,30 @@
import { Directive, Input } from '@angular/core';
// export for convenience.
export { RouterLink} from '@angular/router';
/* tslint:disable:directive-class-suffix */
// #docregion router-link
@Directive({
selector: '[routerLink]',
host: { '(click)': 'onClick()' }
})
export class RouterLinkDirectiveStub {
@Input('routerLink') linkParams: any;
navigatedTo: any = null;
onClick() {
this.navigatedTo = this.linkParams;
}
}
// #enddocregion router-link
/// Dummy module to satisfy Angular Language service. Never used.
import { NgModule } from '@angular/core';
@NgModule({
declarations: [
RouterLinkDirectiveStub
]
})
export class RouterStubsModule {}

View File

@ -1,58 +0,0 @@
// export for convenience.
export { ActivatedRoute, Router, RouterLink, RouterOutlet} from '@angular/router';
import { Component, Directive, Injectable, Input } from '@angular/core';
import { NavigationExtras } from '@angular/router';
// #docregion router-link
@Directive({
selector: '[routerLink]',
host: {
'(click)': 'onClick()'
}
})
export class RouterLinkStubDirective {
@Input('routerLink') linkParams: any;
navigatedTo: any = null;
onClick() {
this.navigatedTo = this.linkParams;
}
}
// #enddocregion router-link
@Component({selector: 'router-outlet', template: ''})
export class RouterOutletStubComponent { }
@Injectable()
export class RouterStub {
navigate(commands: any[], extras?: NavigationExtras) { }
}
// Only implements params and part of snapshot.paramMap
// #docregion activated-route-stub
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { convertToParamMap, ParamMap } from '@angular/router';
@Injectable()
export class ActivatedRouteStub {
// ActivatedRoute.paramMap is Observable
private subject = new BehaviorSubject(convertToParamMap(this.testParamMap));
paramMap = this.subject.asObservable();
// Test parameters
private _testParamMap: ParamMap;
get testParamMap() { return this._testParamMap; }
set testParamMap(params: {}) {
this._testParamMap = convertToParamMap(params);
this.subject.next(this._testParamMap);
}
// ActivatedRoute.snapshot.paramMap
get snapshot() {
return { paramMap: this.testParamMap };
}
}
// #enddocregion activated-route-stub

View File

@ -0,0 +1,64 @@
<!-- Run application specs in a browser -->
<!-- #docregion -->
<!DOCTYPE html>
<html>
<head>
<base href="/">
<title>Sample App Specs</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
<link rel="stylesheet" href="node_modules/jasmine-core/lib/jasmine-core/jasmine.css">
</head>
<body>
<!-- Polyfills -->
<script src="node_modules/core-js/client/shim.min.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="node_modules/jasmine-core/lib/jasmine-core/jasmine.js"></script>
<script src="node_modules/jasmine-core/lib/jasmine-core/jasmine-html.js"></script>
<script src="node_modules/jasmine-core/lib/jasmine-core/boot.js"></script>
<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="node_modules/zone.js/dist/long-stack-trace-zone.js"></script>
<script src="node_modules/zone.js/dist/proxy.js"></script>
<script src="node_modules/zone.js/dist/sync-test.js"></script>
<script src="node_modules/zone.js/dist/jasmine-patch.js"></script>
<script src="node_modules/zone.js/dist/async-test.js"></script>
<script src="node_modules/zone.js/dist/fake-async-test.js"></script>
<script>
var __spec_files__ = [
'app/about/about.component.spec',
'app/app-initial.component.spec',
'app/app.component.router.spec',
'app/app.component.spec',
'app/banner/banner-initial.component.spec',
'app/banner/banner.component.spec',
'app/banner/banner.component.detect-changes.spec',
'app/banner/banner-external.component.spec',
'app/dashboard/dashboard-hero.component.spec',
'app/dashboard/dashboard.component.no-testbed.spec',
'app/dashboard/dashboard.component.spec',
'app/hero/hero-detail.component.no-testbed.spec',
'app/hero/hero-detail.component.spec',
'app/hero/hero-list.component.spec',
'app/model/hero.service.spec',
'app/model/http-hero.service.spec',
'app/shared/highlight.directive.spec',
'app/shared/title-case.pipe.spec',
'app/twain/twain.component.spec',
// 'app/twain/twain.component.marbles.spec',
'app/welcome/welcome.component.spec',
'app/demo/async-helper.spec',
'app/demo/demo.spec',
'app/demo/demo.testbed.spec',
];
</script>
<script src="browser-test-shim.js"></script>
</body>
</html>

View File

@ -1,18 +1,26 @@
// Import spec files individually for Stackblitz
import './app/about.component.spec.ts';
import './app/app.component.spec.ts';
import './app/app.component.router.spec.ts';
import './app/banner.component.spec.ts';
import './app/banner.component.detect-changes.spec.ts';
import './app/banner-inline.component.spec.ts';
import './app/dashboard/dashboard.component.spec.ts';
import './app/dashboard/dashboard.component.no-testbed.spec.ts';
import './app/dashboard/dashboard-hero.component.spec.ts';
import './app/hero/hero-list.component.spec.ts';
import './app/hero/hero-detail.component.spec.ts';
import './app/hero/hero-detail.component.no-testbed.spec.ts';
import './app/model/hero.spec.ts';
import './app/model/http-hero.service.spec.ts';
import './app/shared/title-case.pipe.spec.ts';
import './app/shared/twain.component.spec.ts';
import './app/welcome.component.spec.ts';
import 'app/about/about.component.spec.ts';
import 'app/app-initial.component.spec.ts';
import 'app/app.component.router.spec.ts';
import 'app/app.component.spec.ts';
import 'app/banner/banner-initial.component.spec.ts';
import 'app/banner/banner.component.spec.ts';
import 'app/banner/banner.component.detect-changes.spec.ts';
import 'app/banner/banner-external.component.spec.ts';
import 'app/dashboard/dashboard-hero.component.spec.ts';
import 'app/dashboard/dashboard.component.no-testbed.spec.ts';
import 'app/dashboard/dashboard.component.spec.ts';
import 'app/demo/async-helper.spec.ts';
import 'app/demo/demo.spec.ts';
import 'app/demo/demo.testbed.spec.ts';
import 'app/hero/hero-detail.component.no-testbed.spec.ts';
import 'app/hero/hero-detail.component.spec.ts';
import 'app/hero/hero-list.component.spec.ts';
import 'app/model/hero.service.spec.ts';
import 'app/model/http-hero.service.spec.ts';
import 'app/model/testing/http-client.spec.ts';
import 'app/shared/highlight.directive.spec.ts';
import 'app/shared/title-case.pipe.spec.ts';
import 'app/twain/twain.component.spec.ts';
import 'app/twain/twain.component.marbles.spec.ts';
import 'app/welcome/welcome.component.spec.ts';

View File

@ -37,6 +37,7 @@ System.config({
map: {
'@angular/core/testing': 'npm:@angular/core/bundles/core-testing.umd.js',
'@angular/common/testing': 'npm:@angular/common/bundles/common-testing.umd.js',
'@angular/common/http/testing': 'npm:@angular/common/bundles/common-http-testing.umd.js',
'@angular/compiler/testing': 'npm:@angular/compiler/bundles/compiler-testing.umd.js',
'@angular/platform-browser/testing': 'npm:@angular/platform-browser/bundles/platform-browser-testing.umd.js',
'@angular/platform-browser-dynamic/testing': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic-testing.umd.js',

View File

@ -83,8 +83,8 @@ HTTP guide.
## Testing: added component test plunkers (2016-12-02)
Added two plunkers that each test _one simple component_ so you can write a component test plunker of your own: <live-example name="setup" stackblitz="quickstart-specs">one</live-example> for the QuickStart seed's `AppComponent` and <live-example name="testing" stackblitz="banner-specs">another</live-example> for the Testing guide's `BannerComponent`.
Linked to these plunkers in [Testing](guide/testing#live-examples) and [Setup anatomy](guide/setup-systemjs-anatomy) guides.
Added two plunkers that each test _one simple component_ so you can write a component test plunker of your own: <live-example name="setup" plnkr="quickstart-specs">one</live-example> for the QuickStart seed's `AppComponent` and <live-example name="testing" plnkr="banner-specs">another</live-example> for the Testing guide's `BannerComponent`.
Linked to these plunkers in "Testing" and "Setup anatomy" guides.
## Internationalization: pluralization and _select_ (2016-11-30)

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -72,12 +72,40 @@ describe('CodeExampleComponent', () => {
expect(codeExampleDe.nativeElement.getAttribute('title')).toEqual(null);
});
it('should pass hideCopy to CodeComonent', () => {
it('should pass hideCopy to CodeComponent', () => {
TestBed.overrideComponent(HostComponent, {
set: {template: '<code-example hideCopy="true"></code-example>'}});
createComponent(oneLineCode);
expect(codeComponent.hideCopy).toBe(true);
});
it('should have `avoidFile` class when `avoid` atty present', () => {
TestBed.overrideComponent(HostComponent, {
set: {template: '<code-example avoid></code-example>'}});
createComponent(oneLineCode);
const classes: DOMTokenList = codeExampleDe.nativeElement.classList;
expect(classes.contains('avoidFile')).toBe(true, 'has avoidFile class');
expect(codeExampleComponent.isAvoid).toBe(true, 'isAvoid flag');
expect(codeComponent.hideCopy).toBe(true, 'hiding copy button');
});
it('should have `avoidFile` class when `.avoid` in path', () => {
TestBed.overrideComponent(HostComponent, {
set: {template: '<code-example path="test.avoid.ts"></code-example>'}});
createComponent(oneLineCode);
const classes: DOMTokenList = codeExampleDe.nativeElement.classList;
expect(classes.contains('avoidFile')).toBe(true, 'has avoidFile class');
expect(codeExampleComponent.isAvoid).toBe(true, 'isAvoid flag');
expect(codeComponent.hideCopy).toBe(true, 'hide copy button flag');
});
it('should not have `avoidFile` class in normal case', () => {
createComponent(oneLineCode);
const classes: DOMTokenList = codeExampleDe.nativeElement.classList;
expect(classes.contains('avoidFile')).toBe(false, 'avoidFile class');
expect(codeExampleComponent.isAvoid).toBe(false, 'isAvoid flag');
expect(codeComponent.hideCopy).toBe(false, 'hide copy button flag');
});
});
//// Test helpers ////

View File

@ -48,7 +48,8 @@ export class CodeExampleComponent implements OnInit {
// Now remove the title attribute to prevent unwanted tooltip popups when hovering over the code.
element.removeAttribute('title');
this.isAvoid = this.path.indexOf('.avoid.') !== -1;
const avoid = getBoolFromAttribute(element, 'avoid');
this.isAvoid = avoid || this.path.indexOf('.avoid.') !== -1;
this.hideCopy = this.isAvoid || getBoolFromAttribute(element, ['hidecopy', 'hide-copy']);
this.classes = {

View File

@ -35,6 +35,7 @@
"@types/node": "~6.0.60",
"codelyzer": "^4.0.1",
"jasmine-core": "~2.8.0",
"jasmine-marbles": "~0.2.0",
"jasmine-spec-reporter": "~4.2.1",
"karma": "~2.0.0",
"karma-chrome-launcher": "~2.2.0",

View File

@ -1,51 +0,0 @@
{
"name": "angular.io-example",
"version": "0.0.0",
"license": "MIT",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"
},
"private": true,
"dependencies": {
"@angular/animations": "^5.0.0",
"@angular/common": "^5.0.0",
"@angular/compiler": "^5.0.0",
"@angular/core": "^5.0.0",
"@angular/forms": "^5.0.0",
"@angular/http": "^5.0.0",
"@angular/platform-browser": "^5.0.0",
"@angular/platform-browser-dynamic": "^5.0.0",
"@angular/router": "^5.0.0",
"angular-in-memory-web-api": "~0.5.0",
"core-js": "^2.4.1",
"rxjs": "^5.5.2",
"zone.js": "^0.8.14"
},
"devDependencies": {
"@angular/cli": "1.5.4",
"@angular/compiler-cli": "^5.0.0",
"@angular/language-service": "^5.0.0",
"@types/jasmine": "~2.8.0",
"@types/jasminewd2": "~2.0.2",
"@types/node": "~6.0.60",
"codelyzer": "^4.0.1",
"jasmine-core": "~2.8.0",
"jasmine-marbles": "~2.8.0",
"jasmine-spec-reporter": "~4.1.0",
"karma": "~1.7.0",
"karma-chrome-launcher": "~2.1.1",
"karma-cli": "~1.0.1",
"karma-coverage-istanbul-reporter": "^1.2.1",
"karma-jasmine": "~1.1.0",
"karma-jasmine-html-reporter": "^0.2.2",
"protractor": "~5.1.2",
"ts-node": "~3.2.0",
"tslint": "~5.7.0",
"typescript": "~2.4.2"
}
}

View File

@ -3844,12 +3844,6 @@ jasmine-marbles@^0.2.0:
dependencies:
lodash "^4.5.0"
jasmine-spec-reporter@^4.2.1:
version "4.2.1"
resolved "https://registry.yarnpkg.com/jasmine-spec-reporter/-/jasmine-spec-reporter-4.2.1.tgz#1d632aec0341670ad324f92ba84b4b32b35e9e22"
dependencies:
colors "1.1.2"
jasmine@^2.5.3, jasmine@~2.8.0:
version "2.8.0"
resolved "https://registry.yarnpkg.com/jasmine/-/jasmine-2.8.0.tgz#6b089c0a11576b1f16df11b80146d91d4e8b8a3e"
@ -3858,6 +3852,12 @@ jasmine@^2.5.3, jasmine@~2.8.0:
glob "^7.0.6"
jasmine-core "~2.8.0"
jasmine-spec-reporter@^4.2.1:
version "4.2.1"
resolved "https://registry.yarnpkg.com/jasmine-spec-reporter/-/jasmine-spec-reporter-4.2.1.tgz#1d632aec0341670ad324f92ba84b4b32b35e9e22"
dependencies:
colors "1.1.2"
jasminewd2@^2.1.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/jasminewd2/-/jasminewd2-2.2.0.tgz#e37cf0b17f199cce23bea71b2039395246b4ec4e"

View File

@ -19,9 +19,10 @@ class StackblitzBuilder {
var packageJson = require(path.join(__dirname, '../examples/shared/boilerplate/cli/package.json'));
this.examplePackageDependencies = packageJson.dependencies;
// Add jasmine-core (which is a devDependency) for unit test examples.
// Add unit test packages from devDependency for unit test examples
var devDependencies = packageJson.devDependencies;
this.examplePackageDependencies['jasmine-core'] = devDependencies['jasmine-core'];
this.examplePackageDependencies['jasmine-marbles'] = devDependencies['jasmine-marbles'];
this.copyrights = {};