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:
parent
1f599818bd
commit
a7e1f236ff
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"description": "Testing - app.specs",
|
||||
"description": "Testing - specs",
|
||||
"files":[
|
||||
"src/styles.css",
|
||||
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
// #docplaster
|
||||
// #docregion
|
||||
describe('1st tests', () => {
|
||||
it('true is true', () => expect(true).toBe(true));
|
||||
});
|
|
@ -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
|
|
@ -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 { }
|
|
@ -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!');
|
||||
});
|
||||
});
|
|
@ -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';
|
||||
}
|
|
@ -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: [
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 { }
|
||||
|
|
|
@ -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
|
|
@ -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';
|
||||
}
|
|
@ -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
|
|
@ -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');
|
||||
});
|
||||
|
||||
});
|
|
@ -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');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -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';
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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 { }
|
|
@ -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
|
||||
});
|
|
@ -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
|
||||
});
|
|
@ -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
|
|
@ -1,4 +0,0 @@
|
|||
<!-- #docregion -->
|
||||
<div (click)="click()" class="hero">
|
||||
{{hero.name | uppercase}}
|
||||
</div>
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
|
@ -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);
|
|
@ -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
|
||||
|
||||
});
|
|
@ -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';
|
||||
}
|
|
@ -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 { }
|
||||
|
|
@ -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 {}
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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']);
|
||||
}
|
||||
|
|
|
@ -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(); }
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
margin: 0 0 2em 0;
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
width: 10em;
|
||||
width: 15em;
|
||||
}
|
||||
.heroes li {
|
||||
cursor: pointer;
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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) };
|
||||
}
|
||||
}
|
|
@ -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
|
||||
});
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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')
|
||||
];
|
|
@ -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>;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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`);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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' }
|
||||
];
|
||||
}
|
|
@ -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 { }
|
||||
|
|
|
@ -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
|
||||
});
|
|
@ -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
|
||||
});
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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++ ];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
export class Quote {
|
||||
id: number;
|
||||
quote: string;
|
||||
}
|
|
@ -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
|
||||
});
|
|
@ -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
|
||||
});
|
||||
});
|
|
@ -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
|
|
@ -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 }));
|
|
@ -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
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
|
@ -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
|
|
@ -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>
|
|
@ -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
|
|
@ -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
|
|
@ -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 /////
|
||||
|
||||
|
|
|
@ -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 {}
|
|
@ -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
|
|
@ -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>
|
|
@ -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';
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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 |
|
@ -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 ////
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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 = {};
|
||||
|
||||
|
|
Loading…
Reference in New Issue