docs(aio): update migrated content from anguar.io

This commit is contained in:
Peter Bacon Darwin
2017-03-27 16:08:53 +01:00
committed by Pete Bacon Darwin
parent ff82756415
commit fd72fad8fd
1901 changed files with 20145 additions and 45127 deletions

View File

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

View File

@ -0,0 +1,27 @@
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';
let fixture: ComponentFixture<AboutComponent>;
describe('AboutComponent (highlightDirective)', () => {
// #docregion tests
beforeEach(() => {
fixture = TestBed.configureTestingModule({
declarations: [ AboutComponent, HighlightDirective],
schemas: [ NO_ERRORS_SCHEMA ]
})
.createComponent(AboutComponent);
fixture.detectChanges(); // initial binding
});
it('should have skyblue <h2>', () => {
const de = fixture.debugElement.query(By.css('h2'));
const bgColor = de.nativeElement.style.backgroundColor;
expect(bgColor).toBe('skyblue');
});
// #enddocregion tests
});

View File

@ -0,0 +1,9 @@
// #docregion
import { Component } from '@angular/core';
@Component({
template: `
<h2 highlight="skyblue">About</h2>
<twain-quote></twain-quote>
<p>All about this sample</p>`
})
export class AboutComponent { }

View File

@ -0,0 +1,16 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { AboutComponent } from './about.component';
@NgModule({
imports: [
RouterModule.forRoot([
{ path: '', redirectTo: 'dashboard', pathMatch: 'full'},
{ path: 'about', component: AboutComponent },
{ path: 'heroes', loadChildren: 'app/hero/hero.module#HeroModule'}
])
],
exports: [ RouterModule ] // re-export the module declarations
})
export class AppRoutingModule { };

View File

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

View File

@ -0,0 +1,198 @@
// For more examples:
// https://github.com/angular/angular/blob/master/modules/@angular/router/test/integration.spec.ts
import { async, ComponentFixture, fakeAsync, TestBed, tick,
} from '@angular/core/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';
import { By } from '@angular/platform-browser';
import { DebugElement, Type } from '@angular/core';
import { Location } from '@angular/common';
import { AppModule } from './app.module';
import { AppComponent } from './app.component';
import { AboutComponent } from './about.component';
import { DashboardHeroComponent } from './dashboard/dashboard-hero.component';
import { TwainService } from './shared/twain.service';
let comp: AppComponent;
let fixture: ComponentFixture<AppComponent>;
let page: Page;
let router: Router;
let location: SpyLocation;
describe('AppComponent & RouterTestingModule', () => {
beforeEach( async(() => {
TestBed.configureTestingModule({
imports: [ AppModule, RouterTestingModule ]
})
.compileComponents();
}));
it('should navigate to "Dashboard" immediately', fakeAsync(() => {
createComponent();
expect(location.path()).toEqual('/dashboard', 'after initialNavigation()');
expectElementOf(DashboardHeroComponent);
}));
it('should navigate to "About" on click', fakeAsync(() => {
createComponent();
click(page.aboutLinkDe);
// page.aboutLinkDe.nativeElement.click(); // ok but fails in phantom
advance();
expectPathToBe('/about');
expectElementOf(AboutComponent);
page.expectEvents([
[r.NavigationStart, '/about'], [r.RoutesRecognized, '/about'],
[r.NavigationEnd, '/about']
]);
}));
it('should navigate to "About" w/ browser location URL change', fakeAsync(() => {
createComponent();
location.simulateHashChange('/about');
// location.go('/about'); // also works ... except in plunker
advance();
expectPathToBe('/about');
expectElementOf(AboutComponent);
}));
// Can't navigate to lazy loaded modules with this technique
xit('should navigate to "Heroes" on click', fakeAsync(() => {
createComponent();
page.heroesLinkDe.nativeElement.click();
advance();
expectPathToBe('/heroes');
}));
});
///////////////
import { NgModuleFactoryLoader } from '@angular/core';
import { SpyNgModuleFactoryLoader } from '@angular/router/testing';
import { HeroModule } from './hero/hero.module'; // should be lazy loaded
import { HeroListComponent } from './hero/hero-list.component';
let loader: SpyNgModuleFactoryLoader;
///////// Can't get lazy loaded Heroes to work yet
xdescribe('AppComponent & Lazy Loading', () => {
beforeEach( async(() => {
TestBed.configureTestingModule({
imports: [ AppModule, RouterTestingModule ]
})
.compileComponents();
}));
beforeEach(fakeAsync(() => {
createComponent();
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();
expectPathToBe('/heroes');
expectElementOf(HeroListComponent);
}));
xit('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 */
function advance(): void {
tick();
fixture.detectChanges();
}
function createComponent() {
fixture = TestBed.createComponent(AppComponent);
comp = fixture.componentInstance;
const injector = fixture.debugElement.injector;
location = injector.get(Location);
router = injector.get(Router);
router.initialNavigation();
spyOn(injector.get(TwainService), 'getQuote')
.and.returnValue(Promise.resolve('Test Quote')); // fakes it
advance();
page = new Page();
}
class Page {
aboutLinkDe: DebugElement;
dashboardLinkDe: DebugElement;
heroesLinkDe: DebugElement;
recordedEvents: any[] = [];
// for debugging
comp: AppComponent;
location: SpyLocation;
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];
this.heroesLinkDe = links[1];
// for debugging
this.comp = comp;
this.fixture = fixture;
this.router = router;
}
}
function expectPathToBe(path: string, expectationFailOutput?: any) {
expect(location.path()).toEqual(path, expectationFailOutput || 'location.path()');
}
function expectElementOf(type: Type<any>): any {
const el = fixture.debugElement.query(By.directive(type));
expect(el).toBeTruthy('expected an element for ' + type.name);
return el;
}

View File

@ -0,0 +1,148 @@
// #docplaster
import { async, ComponentFixture, TestBed
} from '@angular/core/testing';
import { DebugElement } 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';
// #enddocregion setup-schemas
@Component({selector: 'app-welcome', template: ''})
class WelcomeStubComponent {}
// #enddocregion setup-stubs-w-imports
let comp: AppComponent;
let fixture: ComponentFixture<AppComponent>;
describe('AppComponent & TestModule', () => {
// #docregion setup-stubs, setup-stubs-w-imports
beforeEach( async(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent,
BannerComponent, WelcomeStubComponent,
RouterLinkStubDirective, RouterOutletStubComponent
]
})
.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(() => {
TestBed.configureTestingModule({
declarations: [ AppComponent, RouterLinkStubDirective ],
schemas: [ NO_ERRORS_SCHEMA ]
})
.compileComponents()
.then(() => {
fixture = TestBed.createComponent(AppComponent);
comp = fixture.componentInstance;
});
}));
// #enddocregion setup-schemas
tests();
});
//////// Testing w/ real root module //////
// Tricky because we are disabling the router and its configuration
// Better to use RouterTestingModule
import { AppModule } from './app.module';
import { AppRoutingModule } from './app-routing.module';
describe('AppComponent & AppModule', () => {
beforeEach( async(() => {
TestBed.configureTestingModule({
imports: [ AppModule ]
})
// Get rid of app's Router configuration otherwise many failures.
// Doing so removes Router declarations; add the Router stubs
.overrideModule(AppModule, {
remove: {
imports: [ AppRoutingModule ]
},
add: {
declarations: [ RouterLinkStubDirective, RouterOutletStubComponent ]
}
})
.compileComponents()
.then(() => {
fixture = TestBed.createComponent(AppComponent);
comp = fixture.componentInstance;
});
}));
tests();
});
function tests() {
let links: RouterLinkStubDirective[];
let linkDes: DebugElement[];
// #docregion test-setup
beforeEach(() => {
// trigger initial data binding
fixture.detectChanges();
// find DebugElements with an attached RouterLinkStubDirective
linkDes = fixture.debugElement
.queryAll(By.directive(RouterLinkStubDirective));
// get the attached link directive instances using the DebugElement injectors
links = linkDes
.map(de => de.injector.get(RouterLinkStubDirective) as RouterLinkStubDirective);
});
// #enddocregion test-setup
it('can instantiate it', () => {
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', '1st link should go to Heroes');
});
it('can click Heroes link in template', () => {
const heroesLinkDe = linkDes[1];
const heroesLink = links[1];
expect(heroesLink.navigatedTo).toBeNull('link should not have navigated yet');
heroesLinkDe.triggerEventHandler('click', null);
fixture.detectChanges();
expect(heroesLink.navigatedTo).toBe('/heroes');
});
// #enddocregion tests
}

View File

@ -0,0 +1,7 @@
// #docregion
import { Component } from '@angular/core';
@Component({
selector: 'my-app',
templateUrl: './app.component.html'
})
export class AppComponent { }

View File

@ -0,0 +1,29 @@
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { AboutComponent } from './about.component';
import { BannerComponent } from './banner.component';
import { HeroService,
UserService } from './model';
import { TwainService } from './shared/twain.service';
import { WelcomeComponent } from './welcome.component';
import { DashboardModule } from './dashboard/dashboard.module';
import { SharedModule } from './shared/shared.module';
@NgModule({
imports: [
BrowserModule,
DashboardModule,
AppRoutingModule,
SharedModule
],
providers: [ HeroService, TwainService, UserService ],
declarations: [ AppComponent, AboutComponent, BannerComponent, WelcomeComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule { }

View File

@ -0,0 +1,68 @@
// tslint:disable-next-line:no-unused-variable
import { async, fakeAsync, tick } from '@angular/core/testing';
import { Observable } from 'rxjs/Observable';
describe('Angular async helper', () => {
let actuallyDone = false;
beforeEach(() => { actuallyDone = false; });
afterEach(() => { expect(actuallyDone).toBe(true, 'actuallyDone should be true'); });
it('should run normal test', () => { actuallyDone = true; });
it('should run normal async test', (done: DoneFn) => {
setTimeout(() => {
actuallyDone = true;
done();
}, 0);
});
it('should run async test with task',
async(() => { setTimeout(() => { actuallyDone = true; }, 0); }));
it('should run async test with successful promise', async(() => {
const p = new Promise(resolve => { setTimeout(resolve, 10); });
p.then(() => { actuallyDone = true; });
}));
it('should run async test with failed promise', async(() => {
const p = new Promise((resolve, reject) => { setTimeout(reject, 10); });
p.catch(() => { actuallyDone = true; });
}));
// 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 => {
const source = Observable.of(true).delay(10);
source.subscribe(
val => actuallyDone = true,
err => fail(err),
done
);
});
// 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);
// source.subscribe(
// val => actuallyDone = true,
// err => fail(err)
// );
// }));
// // 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);
// source.subscribe(
// val => actuallyDone = true,
// err => fail(err)
// );
// tick();
// }));
});

View File

@ -0,0 +1 @@
<span>from external template</span>

View File

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

View File

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

View File

@ -0,0 +1,680 @@
// #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 => {
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', () => {
const fixture = TestBed.createComponent(IoParentComponent);
const comp = fixture.componentInstance;
fixture.detectChanges();
const heroEl = fixture.debugElement.query(By.css('.hero')); // first hero
const ngForRow = heroEl.parent; // Angular's NgForRow wrapper element
// jasmine.any is instance-of-type test.
expect(ngForRow.componentInstance).toEqual(jasmine.any(IoComponent), 'component is IoComp');
const hero = ngForRow.context['$implicit']; // the hero object
expect(hero.name).toBe(comp.heroes[0].name, '1st hero\'s name');
});
// #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(comp, 'context is the parent 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);
expect(testBedProvider).not.toBe(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: string = 'faked value';
}

View File

@ -0,0 +1,454 @@
/* tslint:disable:forin */
import { Component, ContentChildren, Directive, EventEmitter,
Injectable, Input, Output, Optional,
HostBinding, HostListener,
OnInit, OnChanges, OnDestroy,
Pipe, PipeTransform,
SimpleChange } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import 'rxjs/add/operator/delay';
////////// The App: Services and Components for the tests. //////////////
export class Hero {
name: string;
}
////////// Services ///////////////
// #docregion FancyService
@Injectable()
export class FancyService {
protected value: string = 'real value';
getValue() { return this.value; }
setValue(value: string) { this.value = value; }
getAsyncValue() { return Promise.resolve('async value'); }
getObservableValue() { return Observable.of('observable value'); }
getTimeoutValue() {
return new Promise((resolve) => {
setTimeout(() => { resolve('timeout value'); }, 10);
});
}
getObservableDelayValue() {
return Observable.of('observable delay value').delay(10);
}
}
// #enddocregion FancyService
// #docregion DependentService
@Injectable()
export class DependentService {
constructor(private dependentService: FancyService) { }
getValue() { return this.dependentService.getValue(); }
}
// #enddocregion DependentService
/////////// Pipe ////////////////
/*
* Reverse the input string.
*/
// #docregion ReversePipe
@Pipe({ name: 'reverse' })
export class ReversePipe implements PipeTransform {
transform(s: string) {
let r = '';
for (let i = s.length; i; ) { r += s[--i]; };
return r;
}
}
// #enddocregion ReversePipe
//////////// Components /////////////
@Component({
selector: 'bank-account',
template: `
Bank Name: {{bank}}
Account Id: {{id}}
`
})
export class BankAccountComponent {
@Input() bank: string;
@Input('account') id: string;
// Removed on 12/02/2016 when ceased public discussion of the `Renderer`. Revive in future?
// constructor(private renderer: Renderer, private el: ElementRef ) {
// renderer.setElementProperty(el.nativeElement, 'customProperty', true);
// }
}
/** A component with attributes, styles, classes, and property setting */
@Component({
selector: 'bank-account-parent',
template: `
<bank-account
bank="RBC"
account="4747"
[style.width.px]="width"
[style.color]="color"
[class.closed]="isClosed"
[class.open]="!isClosed">
</bank-account>
`
})
export class BankAccountParentComponent {
width = 200;
color = 'red';
isClosed = true;
}
// #docregion ButtonComp
@Component({
selector: 'button-comp',
template: `
<button (click)="clicked()">Click me!</button>
<span>{{message}}</span>`
})
export class ButtonComponent {
isOn = false;
clicked() { this.isOn = !this.isOn; }
get message() { return `The light is ${this.isOn ? 'On' : 'Off'}`; }
}
// #enddocregion ButtonComp
@Component({
selector: 'child-1',
template: `<span>Child-1({{text}})</span>`
})
export class Child1Component {
@Input() text = 'Original';
}
@Component({
selector: 'child-2',
template: '<div>Child-2({{text}})</div>'
})
export class Child2Component {
@Input() text: string;
}
@Component({
selector: 'child-3',
template: '<div>Child-3({{text}})</div>'
})
export class Child3Component {
@Input() text: string;
}
@Component({
selector: 'input-comp',
template: `<input [(ngModel)]="name">`
})
export class InputComponent {
name = 'John';
}
/* Prefer this metadata syntax */
// @Directive({
// selector: 'input[value]',
// host: {
// '[value]': 'value',
// '(input)': 'valueChange.emit($event.target.value)'
// },
// inputs: ['value'],
// outputs: ['valueChange']
// })
// export class InputValueBinderDirective {
// value: any;
// valueChange: EventEmitter<any> = new EventEmitter();
// }
// As the style-guide recommends
@Directive({ selector: 'input[value]' })
export class InputValueBinderDirective {
@HostBinding()
@Input()
value: any;
@Output()
valueChange: EventEmitter<any> = new EventEmitter();
@HostListener('input', ['$event.target.value'])
onInput(value: any) { this.valueChange.emit(value); }
}
@Component({
selector: 'input-value-comp',
template: `
Name: <input [(value)]="name"> {{name}}
`
})
export class InputValueBinderComponent {
name = 'Sally'; // initial value
}
@Component({
selector: 'parent-comp',
template: `Parent(<child-1></child-1>)`
})
export class ParentComponent { }
@Component({
selector: 'io-comp',
template: `<div class="hero" (click)="click()">Original {{hero.name}}</div>`
})
export class IoComponent {
@Input() hero: Hero;
@Output() selected = new EventEmitter<Hero>();
click() { this.selected.emit(this.hero); }
}
@Component({
selector: 'io-parent-comp',
template: `
<p *ngIf="!selectedHero"><i>Click to select a hero</i></p>
<p *ngIf="selectedHero">The selected hero is {{selectedHero.name}}</p>
<io-comp
*ngFor="let hero of heroes"
[hero]=hero
(selected)="onSelect($event)">
</io-comp>
`
})
export class IoParentComponent {
heroes: Hero[] = [ {name: 'Bob'}, {name: 'Carol'}, {name: 'Ted'}, {name: 'Alice'} ];
selectedHero: Hero;
onSelect(hero: Hero) { this.selectedHero = hero; }
}
@Component({
selector: 'my-if-comp',
template: `MyIf(<span *ngIf="showMore">More</span>)`
})
export class MyIfComponent {
showMore = false;
}
@Component({
selector: 'my-service-comp',
template: `injected value: {{fancyService.value}}`,
providers: [FancyService]
})
export class TestProvidersComponent {
constructor(private fancyService: FancyService) {}
}
@Component({
selector: 'my-service-comp',
template: `injected value: {{fancyService.value}}`,
viewProviders: [FancyService]
})
export class TestViewProvidersComponent {
constructor(private fancyService: FancyService) {}
}
@Component({
selector: 'external-template-comp',
templateUrl: './bag-external-template.html'
})
export class ExternalTemplateComponent implements OnInit {
serviceValue: string;
constructor(@Optional() private service: FancyService) { }
ngOnInit() {
if (this.service) { this.serviceValue = this.service.getValue(); }
}
}
@Component({
selector: 'comp-w-ext-comp',
template: `
<h3>comp-w-ext-comp</h3>
<external-template-comp></external-template-comp>
`
})
export class InnerCompWithExternalTemplateComponent { }
@Component({
selector: 'bad-template-comp',
templateUrl: './non-existant.html'
})
export class BadTemplateUrlComponent { }
@Component({selector: 'needs-content', template: '<ng-content></ng-content>'})
export class NeedsContentComponent {
// children with #content local variable
@ContentChildren('content') children: any;
}
///////// MyIfChildComp ////////
@Component({
selector: 'my-if-child-1',
template: `
<h4>MyIfChildComp</h4>
<div>
<label>Child value: <input [(ngModel)]="childValue"> </label>
</div>
<p><i>Change log:</i></p>
<div *ngFor="let log of changeLog; let i=index">{{i + 1}} - {{log}}</div>`
})
export class MyIfChildComponent implements OnInit, OnChanges, OnDestroy {
@Input() value = '';
@Output() valueChange = new EventEmitter<string>();
get childValue() { return this.value; }
set childValue(v: string) {
if (this.value === v) { return; }
this.value = v;
this.valueChange.emit(v);
}
changeLog: string[] = [];
ngOnInitCalled = false;
ngOnChangesCounter = 0;
ngOnDestroyCalled = false;
ngOnInit() {
this.ngOnInitCalled = true;
this.changeLog.push('ngOnInit called');
}
ngOnDestroy() {
this.ngOnDestroyCalled = true;
this.changeLog.push('ngOnDestroy called');
}
ngOnChanges(changes: {[propertyName: string]: SimpleChange}) {
for (let propName in changes) {
this.ngOnChangesCounter += 1;
let prop = changes[propName];
let cur = JSON.stringify(prop.currentValue);
let prev = JSON.stringify(prop.previousValue);
this.changeLog.push(`${propName}: currentValue = ${cur}, previousValue = ${prev}`);
}
}
}
///////// MyIfParentComp ////////
@Component({
selector: 'my-if-parent-comp',
template: `
<h3>MyIfParentComp</h3>
<label>Parent value:
<input [(ngModel)]="parentValue">
</label>
<button (click)="clicked()">{{toggleLabel}} Child</button><br>
<div *ngIf="showChild"
style="margin: 4px; padding: 4px; background-color: aliceblue;">
<my-if-child-1 [(value)]="parentValue"></my-if-child-1>
</div>
`
})
export class MyIfParentComponent implements OnInit {
ngOnInitCalled = false;
parentValue = 'Hello, World';
showChild = false;
toggleLabel = 'Unknown';
ngOnInit() {
this.ngOnInitCalled = true;
this.clicked();
}
clicked() {
this.showChild = !this.showChild;
this.toggleLabel = this.showChild ? 'Close' : 'Show';
}
}
@Component({
selector: 'reverse-pipe-comp',
template: `
<input [(ngModel)]="text">
<span>{{text | reverse}}</span>
`
})
export class ReversePipeComponent {
text = 'my dog has fleas.';
}
@Component({template: '<div>Replace Me</div>'})
export class ShellComponent { }
@Component({
selector: 'bag-comp',
template: `
<h1>Specs Bag</h1>
<my-if-parent-comp></my-if-parent-comp>
<hr>
<h3>Input/Output Component</h3>
<io-parent-comp></io-parent-comp>
<hr>
<h3>External Template Component</h3>
<external-template-comp></external-template-comp>
<hr>
<h3>Component With External Template Component</h3>
<comp-w-ext-comp></comp-w-ext-comp>
<hr>
<h3>Reverse Pipe</h3>
<reverse-pipe-comp></reverse-pipe-comp>
<hr>
<h3>InputValueBinder Directive</h3>
<input-value-comp></input-value-comp>
<hr>
<h3>Button Component</h3>
<button-comp></button-comp>
<hr>
<h3>Needs Content</h3>
<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>
`
})
export class BagComponent { }
//////// Aggregations ////////////
export const bagDeclarations = [
BagComponent,
BankAccountComponent, BankAccountParentComponent,
ButtonComponent,
Child1Component, Child2Component, Child3Component,
ExternalTemplateComponent, InnerCompWithExternalTemplateComponent,
InputComponent,
InputValueBinderDirective, InputValueBinderComponent,
IoComponent, IoParentComponent,
MyIfComponent, MyIfChildComponent, MyIfParentComponent,
NeedsContentComponent, ParentComponent,
TestProvidersComponent, TestViewProvidersComponent,
ReversePipe, ReversePipeComponent, ShellComponent
];
export const bagProviders = [DependentService, FancyService];
////////////////////
////////////
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
@NgModule({
imports: [BrowserModule, FormsModule],
declarations: bagDeclarations,
providers: bagProviders,
entryComponents: [BagComponent],
bootstrap: [BagComponent]
})
export class BagModule { }

View File

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

View File

@ -0,0 +1,11 @@
// #docregion
import { Component } from '@angular/core';
@Component({
selector: 'app-banner',
template: '<h1>{{title}}</h1>'
})
export class BannerComponent {
title = 'Test Tour of Heroes';
}

View File

@ -0,0 +1 @@
h1 { color: green; font-size: 350%}

View File

@ -0,0 +1,59 @@
// #docplaster
// #docregion
// #docregion import-async
import { async } from '@angular/core/testing';
// #enddocregion import-async
// #docregion import-ComponentFixtureAutoDetect
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;
beforeEach(async(() => {
// #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;
});
// #docregion auto-detect-tests
it('should display original title', () => {
// Hooray! No `fixture.detectChanges()` needed
expect(el.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);
});
it('should display updated title after detectChanges', () => {
comp.title = 'Test Title';
fixture.detectChanges(); // detect changes explicitly
expect(el.textContent).toContain(comp.title);
});
// #enddocregion auto-detect-tests
});

View File

@ -0,0 +1 @@
<h1>{{title}}</h1>

View File

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

View File

@ -0,0 +1,12 @@
// #docregion
import { Component } from '@angular/core';
@Component({
selector: 'app-banner',
templateUrl: './banner.component.html',
styleUrls: ['./banner.component.css']
})
export class BannerComponent {
title = 'Test Tour of Heroes';
}

View File

@ -0,0 +1,28 @@
.hero {
padding: 20px;
position: relative;
text-align: center;
color: #eee;
max-height: 120px;
min-width: 120px;
background-color: #607D8B;
border-radius: 2px;
}
.hero:hover {
background-color: #EEE;
cursor: pointer;
color: #607d8b;
}
@media (max-width: 600px) {
.hero {
font-size: 10px;
max-height: 75px; }
}
@media (max-width: 1024px) {
.hero {
min-width: 60px;
}
}

View File

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

View File

@ -0,0 +1,124 @@
import { async, ComponentFixture, TestBed
} from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { addMatchers, click } from '../../testing';
import { Hero } from '../model/hero';
import { DashboardHeroComponent } from './dashboard-hero.component';
beforeEach( addMatchers );
describe('DashboardHeroComponent when tested directly', () => {
let comp: DashboardHeroComponent;
let expectedHero: Hero;
let fixture: ComponentFixture<DashboardHeroComponent>;
let heroEl: DebugElement;
// #docregion setup, compile-components
// async beforeEach
beforeEach( async(() => {
TestBed.configureTestingModule({
declarations: [ DashboardHeroComponent ],
})
.compileComponents(); // compile template and css
}));
// #enddocregion compile-components
// synchronous beforeEach
beforeEach(() => {
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');
comp.hero = expectedHero;
fixture.detectChanges(); // trigger initial data binding
});
// #enddocregion setup
// #docregion name-test
it('should display hero name', () => {
const expectedPipedName = expectedHero.name.toUpperCase();
expect(heroEl.nativeElement.textContent).toContain(expectedPipedName);
});
// #enddocregion name-test
// #docregion click-test
it('should raise selected event when clicked', () => {
let selectedHero: Hero;
comp.selected.subscribe((hero: Hero) => selectedHero = hero);
// #docregion trigger-event-handler
heroEl.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);
click(heroEl); // triggerEventHandler helper
expect(selectedHero).toBe(expectedHero);
});
// #enddocregion click-test-2
});
//////////////////
describe('DashboardHeroComponent when inside a test host', () => {
let testHost: TestHostComponent;
let fixture: ComponentFixture<TestHostComponent>;
let heroEl: DebugElement;
// #docregion test-host-setup
beforeEach( async(() => {
TestBed.configureTestingModule({
declarations: [ DashboardHeroComponent, TestHostComponent ], // declare both
}).compileComponents();
}));
beforeEach(() => {
// create TestHostComponent instead of DashboardHeroComponent
fixture = TestBed.createComponent(TestHostComponent);
testHost = fixture.componentInstance;
heroEl = fixture.debugElement.query(By.css('.hero')); // find hero
fixture.detectChanges(); // trigger initial data binding
});
// #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);
});
it('should raise selected event when clicked', () => {
click(heroEl);
// selected hero should be the same data bound hero
expect(testHost.selectedHero).toBe(testHost.hero);
});
// #enddocregion test-host-tests
});
////// Test Host Component //////
import { Component } from '@angular/core';
// #docregion test-host
@Component({
template: `
<dashboard-hero [hero]="hero" (selected)="onSelected($event)"></dashboard-hero>`
})
class TestHostComponent {
hero = new Hero(42, 'Test Name');
selectedHero: Hero;
onSelected(hero: Hero) { this.selectedHero = hero; }
}
// #enddocregion test-host

View File

@ -0,0 +1,17 @@
// #docregion
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { Hero } from '../model';
// #docregion component
@Component({
selector: 'dashboard-hero',
templateUrl: './dashboard-hero.component.html',
styleUrls: [ './dashboard-hero.component.css' ]
})
export class DashboardHeroComponent {
@Input() hero: Hero;
@Output() selected = new EventEmitter<Hero>();
click() { this.selected.emit(this.hero); }
}
// #enddocregion component

View File

@ -0,0 +1,35 @@
[class*='col-'] {
float: left;
}
*, *:after, *:before {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
h3 {
text-align: center; margin-bottom: 0;
}
[class*='col-'] {
padding-right: 20px;
padding-bottom: 20px;
}
[class*='col-']:last-of-type {
padding-right: 0;
}
.grid {
margin: 0;
}
.col-1-4 {
width: 25%;
}
.grid-pad {
padding: 10px 0;
}
.grid-pad > [class*='col-']:last-of-type {
padding-right: 20px;
}
@media (max-width: 1024px) {
.grid {
margin: 0;
}
}

View File

@ -0,0 +1,9 @@
<h2 highlight>{{title}}</h2>
<div class="grid grid-pad">
<!-- #docregion dashboard-hero -->
<dashboard-hero *ngFor="let hero of heroes" class="col-1-4"
[hero]=hero (selected)="gotoDetail($event)" >
</dashboard-hero>
<!-- #enddocregion dashboard-hero -->
</div>

View File

@ -0,0 +1,57 @@
import { Router } from '@angular/router';
import { DashboardComponent } from './dashboard.component';
import { Hero } from '../model';
import { addMatchers } from '../../testing';
import { FakeHeroService } from '../model/testing';
class FakeRouter {
navigateByUrl(url: string) { return url; }
}
describe('DashboardComponent: w/o Angular TestBed', () => {
let comp: DashboardComponent;
let heroService: FakeHeroService;
let router: Router;
beforeEach(() => {
addMatchers();
router = new FakeRouter() as any as Router;
heroService = new FakeHeroService();
comp = new DashboardComponent(router, heroService);
});
it('should NOT have heroes before calling OnInit', () => {
expect(comp.heroes.length).toBe(0,
'should not have heroes before OnInit');
});
it('should NOT have heroes immediately after OnInit', () => {
comp.ngOnInit(); // ngOnInit -> getHeroes
expect(comp.heroes.length).toBe(0,
'should not have heroes until service promise resolves');
});
it('should HAVE heroes after HeroService gets them', (done: DoneFn) => {
comp.ngOnInit(); // ngOnInit -> getHeroes
heroService.lastPromise // the one from getHeroes
.then(() => {
// 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);
});
it('should tell ROUTER to navigate by hero id', () => {
const hero = new Hero(42, 'Abbracadabra');
const spy = spyOn(router, 'navigateByUrl');
comp.gotoDetail(hero);
const navArgs = spy.calls.mostRecent().args[0];
expect(navArgs).toBe('/heroes/42', 'should nav to HeroDetail for Hero 42');
});
});

View File

@ -0,0 +1,147 @@
// #docplaster
import { async, inject, ComponentFixture, TestBed
} from '@angular/core/testing';
import { addMatchers, click } from '../../testing';
import { HeroService } from '../model';
import { FakeHeroService } from '../model/testing';
import { By } from '@angular/platform-browser';
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;
let fixture: ComponentFixture<DashboardComponent>;
//////// Deep ////////////////
describe('DashboardComponent (deep)', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ DashboardModule ]
});
});
compileAndCreate();
tests(clickForDeep);
function clickForDeep() {
// get first <div class="hero"> DebugElement
const heroEl = fixture.debugElement.query(By.css('.hero'));
click(heroEl);
}
});
//////// Shallow ////////////////
import { NO_ERRORS_SCHEMA } from '@angular/core';
describe('DashboardComponent (shallow)', () => {
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ DashboardComponent ],
schemas: [NO_ERRORS_SCHEMA]
});
});
compileAndCreate();
tests(clickForShallow);
function clickForShallow() {
// get first <dashboard-hero> DebugElement
const heroEl = fixture.debugElement.query(By.css('dashboard-hero'));
heroEl.triggerEventHandler('selected', comp.heroes[0]);
}
});
/** Add TestBed providers, compile, and create DashboardComponent */
function compileAndCreate() {
// #docregion compile-and-create-body
beforeEach( async(() => {
TestBed.configureTestingModule({
providers: [
{ provide: HeroService, useClass: FakeHeroService },
{ provide: Router, useClass: RouterStub }
]
})
.compileComponents().then(() => {
fixture = TestBed.createComponent(DashboardComponent);
comp = fixture.componentInstance;
});
// #enddocregion compile-and-create-body
}));
}
/**
* The (almost) same tests for both.
* Only change: the way that the first hero is clicked
*/
function tests(heroClick: Function) {
it('should NOT have heroes before ngOnInit', () => {
expect(comp.heroes.length).toBe(0,
'should not have heroes before ngOnInit');
});
it('should NOT have heroes immediately after ngOnInit', () => {
fixture.detectChanges(); // runs initial lifecycle hooks
expect(comp.heroes.length).toBe(0,
'should not have heroes until service promise resolves');
});
describe('after get dashboard heroes', () => {
// Trigger component so it gets heroes and binds to them
beforeEach( async(() => {
fixture.detectChanges(); // runs ngOnInit -> getHeroes
fixture.whenStable() // No need for the `lastPromise` hack!
.then(() => fixture.detectChanges()); // bind to heroes
}));
it('should HAVE heroes', () => {
expect(comp.heroes.length).toBeGreaterThan(0,
'should have heroes after service promise resolves');
});
it('should DISPLAY heroes', () => {
// Find and examine the displayed heroes
// Look for them in the DOM by css class
const heroes = fixture.debugElement.queryAll(By.css('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');
heroClick(); // trigger click on first inner <div class="hero">
// args passed to router.navigateByUrl()
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
});
}

View File

@ -0,0 +1,40 @@
// #docregion
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { Hero, HeroService } from '../model';
@Component({
selector: 'app-dashboard',
templateUrl: './dashboard.component.html',
styleUrls: [ './dashboard.component.css' ]
})
export class DashboardComponent implements OnInit {
heroes: Hero[] = [];
// #docregion ctor
constructor(
private router: Router,
private heroService: HeroService) {
}
// #enddocregion ctor
ngOnInit() {
this.heroService.getHeroes()
.then(heroes => this.heroes = heroes.slice(1, 5));
}
// #docregion goto-detail
gotoDetail(hero: Hero) {
let url = `/heroes/${hero.id}`;
this.router.navigateByUrl(url);
}
// #enddocregion goto-detail
get title() {
let cnt = this.heroes.length;
return cnt === 0 ? 'No Heroes' :
cnt === 1 ? 'Top Hero' : `Top ${cnt} Heroes`;
}
}

View File

@ -0,0 +1,20 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { SharedModule } from '../shared/shared.module';
import { DashboardComponent } from './dashboard.component';
import { DashboardHeroComponent } from './dashboard-hero.component';
const routes: Routes = [
{ path: 'dashboard', component: DashboardComponent },
];
@NgModule({
imports: [
SharedModule,
RouterModule.forChild(routes)
],
declarations: [ DashboardComponent, DashboardHeroComponent ]
})
export class DashboardModule { }

View File

@ -0,0 +1,29 @@
label {
display: inline-block;
width: 3em;
margin: .5em 0;
color: #607D8B;
font-weight: bold;
}
input {
height: 2em;
font-size: 1em;
padding-left: .4em;
}
button {
margin-top: 20px;
font-family: Arial;
background-color: #eee;
border: none;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer; cursor: hand;
}
button:hover {
background-color: #cfd8dc;
}
button:disabled {
background-color: #eee;
color: #ccc;
cursor: auto;
}

View File

@ -0,0 +1,12 @@
<!-- #docregion -->
<div *ngIf="hero">
<h2><span>{{hero.name | titlecase}}</span> Details</h2>
<div>
<label>id: </label>{{hero.id}}</div>
<div>
<label for="name">name: </label>
<input id="name" [(ngModel)]="hero.name" placeholder="name" />
</div>
<button (click)="save()">Save</button>
<button (click)="cancel()">Cancel</button>
</div>

View File

@ -0,0 +1,58 @@
import { HeroDetailComponent } from './hero-detail.component';
import { Hero } from '../model';
import { ActivatedRouteStub } from '../../testing';
////////// Tests ////////////////////
describe('HeroDetailComponent - no TestBed', () => {
let activatedRoute: ActivatedRouteStub;
let comp: HeroDetailComponent;
let expectedHero: Hero;
let hds: any;
let router: any;
beforeEach( done => {
expectedHero = new Hero(42, 'Bubba');
activatedRoute = new ActivatedRouteStub();
activatedRoute.testParams = { 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));
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);
});
it('should expose the hero retrieved from the service', () => {
expect(comp.hero).toBe(expectedHero);
});
it('should navigate when click cancel', () => {
comp.cancel();
expect(router.navigate.calls.any()).toBe(true, 'router.navigate called');
});
it('should save when click save', () => {
comp.save();
expect(hds.saveHero.calls.any()).toBe(true, 'HeroDetailService.save called');
expect(router.navigate.calls.any()).toBe(false, 'router.navigate not called yet');
});
it('should navigate when click save resolves', done => {
comp.save();
// waits for async save to complete before navigating
hds.saveHero.calls.first().returnValue
.then(() => {
expect(router.navigate.calls.any()).toBe(true, 'router.navigate called');
done();
});
});
});

View File

@ -0,0 +1,364 @@
// #docplaster
import {
async, ComponentFixture, fakeAsync, inject, TestBed, tick
} from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import {
ActivatedRoute, ActivatedRouteStub, click, newEvent, Router, RouterStub
} from '../../testing';
import { Hero } from '../model';
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 fixture: ComponentFixture<HeroDetailComponent>;
let page: Page;
////// Tests //////
describe('HeroDetailComponent', () => {
beforeEach(() => {
activatedRoute = new ActivatedRouteStub();
});
describe('with HeroModule setup', heroModuleSetup);
describe('when override its provided HeroDetailService', overrideSetup);
describe('with FormsModule setup', formsModuleSetup);
describe('with SharedModule setup', sharedModuleSetup);
});
////////////////////
function overrideSetup() {
// #docregion hds-spy
class HeroDetailServiceSpy {
testHero = new Hero(42, 'Test Hero');
getHero = jasmine.createSpy('getHero').and.callFake(
() => Promise
.resolve(true)
.then(() => Object.assign({}, this.testHero))
);
saveHero = jasmine.createSpy('saveHero').and.callFake(
(hero: Hero) => Promise
.resolve(true)
.then(() => Object.assign(this.testHero, hero))
);
}
// #enddocregion hds-spy
// the `id` value is irrelevant because ignored by service stub
beforeEach(() => activatedRoute.testParams = { id: 99999 } );
// #docregion setup-override
beforeEach( async(() => {
TestBed.configureTestingModule({
imports: [ HeroModule ],
providers: [
{ provide: ActivatedRoute, useValue: activatedRoute },
{ provide: Router, useClass: RouterStub},
// #enddocregion setup-override
// HeroDetailService at this level is IRRELEVANT!
{ provide: HeroDetailService, useValue: {} }
// #docregion setup-override
]
})
// Override component's own provider
// #docregion override-component-method
.overrideComponent(HeroDetailComponent, {
set: {
providers: [
{ provide: HeroDetailService, useClass: HeroDetailServiceSpy }
]
}
})
// #enddocregion override-component-method
.compileComponents();
}));
// #enddocregion setup-override
// #docregion override-tests
let hdsSpy: HeroDetailServiceSpy;
beforeEach( async(() => {
createComponent();
// get the component's injected HeroDetailServiceSpy
hdsSpy = fixture.debugElement.injector.get(HeroDetailService);
}));
it('should have called `getHero`', () => {
expect(hdsSpy.getHero.calls.count()).toBe(1, 'getHero called once');
});
it('should display stub hero\'s name', () => {
expect(page.nameDisplay.textContent).toBe(hdsSpy.testHero.name);
});
it('should save stub hero change', fakeAsync(() => {
const origName = hdsSpy.testHero.name;
const newName = 'New Name';
page.nameInput.value = newName;
page.nameInput.dispatchEvent(newEvent('input')); // tell Angular
expect(comp.hero.name).toBe(newName, 'component hero has new name');
expect(hdsSpy.testHero.name).toBe(origName, 'service hero unchanged before save');
click(page.saveBtn);
expect(hdsSpy.saveHero.calls.count()).toBe(1, 'saveHero called once');
tick(); // wait for async save to complete
expect(hdsSpy.testHero.name).toBe(newName, 'service hero has new name after save');
expect(page.navSpy.calls.any()).toBe(true, 'router.navigate called');
}));
// #enddocregion override-tests
it('fixture injected service is not the component injected service',
inject([HeroDetailService], (service: HeroDetailService) => {
expect(service).toEqual({}, 'service injected from fixture');
expect(hdsSpy).toBeTruthy('service injected into component');
}));
}
////////////////////
import { HEROES, FakeHeroService } from '../model/testing';
import { HeroService } from '../model';
const firstHero = HEROES[0];
function heroModuleSetup() {
// #docregion setup-hero-module
beforeEach( async(() => {
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},
]
})
.compileComponents();
}));
// #enddocregion setup-hero-module
// #docregion route-good-id
describe('when navigate to existing hero', () => {
let expectedHero: Hero;
beforeEach( async(() => {
expectedHero = firstHero;
activatedRoute.testParams = { id: expectedHero.id };
createComponent();
}));
// #docregion selected-tests
it('should display that hero\'s name', () => {
expect(page.nameDisplay.textContent).toBe(expectedHero.name);
});
// #enddocregion route-good-id
it('should navigate when click cancel', () => {
click(page.cancelBtn);
expect(page.navSpy.calls.any()).toBe(true, 'router.navigate called');
});
it('should save when click save but not navigate immediately', () => {
// Get service injected into component and spy on its`saveHero` method.
// It delegates to fake `HeroService.updateHero` which delivers a safe test result.
const hds = fixture.debugElement.injector.get(HeroDetailService);
const saveSpy = spyOn(hds, 'saveHero').and.callThrough();
click(page.saveBtn);
expect(saveSpy.calls.any()).toBe(true, 'HeroDetailService.save called');
expect(page.navSpy.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');
}));
// #docregion title-case-pipe
it('should convert hero name to Title Case', () => {
const inputName = 'quick BROWN fox';
const titleCaseName = 'Quick Brown Fox';
// simulate user entering new name into the input box
page.nameInput.value = inputName;
// dispatch a DOM event so that Angular learns of input value change.
page.nameInput.dispatchEvent(newEvent('input'));
// Tell Angular to update the output span through the title pipe
fixture.detectChanges();
expect(page.nameDisplay.textContent).toBe(titleCaseName);
});
// #enddocregion title-case-pipe
// #enddocregion selected-tests
// #docregion route-good-id
});
// #enddocregion route-good-id
// #docregion route-no-id
describe('when navigate with no hero id', () => {
beforeEach( async( createComponent ));
it('should have hero.id === 0', () => {
expect(comp.hero.id).toBe(0);
});
it('should display empty hero name', () => {
expect(page.nameDisplay.textContent).toBe('');
});
});
// #enddocregion route-no-id
// #docregion route-bad-id
describe('when navigate to non-existant hero id', () => {
beforeEach( async(() => {
activatedRoute.testParams = { 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');
});
});
// #enddocregion route-bad-id
// Why we must use `fixture.debugElement.injector` in `Page()`
it('cannot use `inject` to get component\'s provided HeroDetailService', () => {
let service: HeroDetailService;
fixture = TestBed.createComponent(HeroDetailComponent);
expect(
// Throws because `inject` only has access to TestBed's injector
// which is an ancestor of the component's injector
inject([HeroDetailService], (hds: HeroDetailService) => service = hds )
)
.toThrowError(/No provider for HeroDetailService/);
// get `HeroDetailService` with component's own injector
service = fixture.debugElement.injector.get(HeroDetailService);
expect(service).toBeDefined('debugElement.injector');
});
}
/////////////////////
import { FormsModule } from '@angular/forms';
import { TitleCasePipe } from '../shared/title-case.pipe';
function formsModuleSetup() {
// #docregion setup-forms-module
beforeEach( async(() => {
TestBed.configureTestingModule({
imports: [ FormsModule ],
declarations: [ HeroDetailComponent, TitleCasePipe ],
providers: [
{ provide: ActivatedRoute, useValue: activatedRoute },
{ provide: HeroService, useClass: FakeHeroService },
{ provide: Router, useClass: RouterStub},
]
})
.compileComponents();
}));
// #enddocregion setup-forms-module
it('should display 1st hero\'s name', fakeAsync(() => {
const expectedHero = firstHero;
activatedRoute.testParams = { id: expectedHero.id };
createComponent().then(() => {
expect(page.nameDisplay.textContent).toBe(expectedHero.name);
});
}));
}
///////////////////////
import { SharedModule } from '../shared/shared.module';
function sharedModuleSetup() {
// #docregion setup-shared-module
beforeEach( async(() => {
TestBed.configureTestingModule({
imports: [ SharedModule ],
declarations: [ HeroDetailComponent ],
providers: [
{ provide: ActivatedRoute, useValue: activatedRoute },
{ provide: HeroService, useClass: FakeHeroService },
{ provide: Router, useClass: RouterStub},
]
})
.compileComponents();
}));
// #enddocregion setup-shared-module
it('should display 1st hero\'s name', fakeAsync(() => {
const expectedHero = firstHero;
activatedRoute.testParams = { id: expectedHero.id };
createComponent().then(() => {
expect(page.nameDisplay.textContent).toBe(expectedHero.name);
});
}));
}
/////////// Helpers /////
// #docregion create-component
/** Create the HeroDetailComponent, initialize it, set test variables */
function createComponent() {
fixture = TestBed.createComponent(HeroDetailComponent);
comp = fixture.componentInstance;
page = new Page();
// 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;
saveBtn: DebugElement;
cancelBtn: DebugElement;
nameDisplay: HTMLElement;
nameInput: HTMLInputElement;
constructor() {
const router = TestBed.get(Router); // get router from root injector
this.gotoSpy = spyOn(comp, 'gotoList').and.callThrough();
this.navSpy = spyOn(router, 'navigate');
}
/** 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;
}
}
}
// #enddocregion page

View File

@ -0,0 +1,63 @@
/* tslint:disable:member-ordering */
// #docplaster
import { Component, Input, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import 'rxjs/add/operator/map';
import { Hero } from '../model';
import { HeroDetailService } from './hero-detail.service';
// #docregion prototype
@Component({
selector: 'app-hero-detail',
templateUrl: './hero-detail.component.html',
styleUrls: ['./hero-detail.component.css' ],
providers: [ HeroDetailService ]
})
export class HeroDetailComponent implements OnInit {
// #docregion ctor
constructor(
private heroDetailService: HeroDetailService,
private route: ActivatedRoute,
private router: Router) {
}
// #enddocregion ctor
// #enddocregion prototype
@Input() hero: Hero;
// #docregion ng-on-init
ngOnInit(): void {
// get hero when `id` param changes
this.route.params.subscribe(p => this.getHero(p && p['id']));
}
// #enddocregion ng-on-init
private getHero(id: string): void {
// when no id or id===0, create new hero
if (!id) {
this.hero = new Hero();
return;
}
this.heroDetailService.getHero(id).then(hero => {
if (hero) {
this.hero = hero;
} else {
this.gotoList(); // id not found; navigate to list
}
});
}
save(): void {
this.heroDetailService.saveHero(this.hero).then(() => this.gotoList());
}
cancel() { this.gotoList(); }
gotoList() {
this.router.navigate(['../'], {relativeTo: this.route});
}
// #docregion prototype
}
// #enddocregion prototype

View File

@ -0,0 +1,26 @@
import { Injectable } from '@angular/core';
import { Hero, HeroService } from '../model';
// #docregion prototype
@Injectable()
export class HeroDetailService {
constructor(private heroService: HeroService) { }
// #enddocregion prototype
// Returns a clone which caller may modify safely
getHero(id: number | string): Promise<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
});
}
saveHero(hero: Hero) {
return this.heroService.updateHero(hero);
}
// #docregion prototype
}
// #enddocregion prototype

View File

@ -0,0 +1,59 @@
.selected {
background-color: #CFD8DC !important;
color: white;
}
.heroes {
margin: 0 0 2em 0;
list-style-type: none;
padding: 0;
width: 10em;
}
.heroes li {
cursor: pointer;
position: relative;
left: 0;
background-color: #EEE;
margin: .5em;
padding: .3em 0;
height: 1.6em;
border-radius: 4px;
}
.heroes li:hover {
color: #607D8B;
background-color: #DDD;
left: .1em;
}
.heroes li.selected:hover {
background-color: #BBD8DC !important;
color: white;
}
.heroes .text {
position: relative;
top: -3px;
}
.heroes .badge {
display: inline-block;
font-size: small;
color: white;
padding: 0.8em 0.7em 0 0.7em;
background-color: #607D8B;
line-height: 1em;
position: relative;
left: -1px;
top: -4px;
height: 1.8em;
margin-right: .8em;
border-radius: 4px 0 0 4px;
}
button {
font-family: Arial;
background-color: #eee;
border: none;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
cursor: hand;
}
button:hover {
background-color: #cfd8dc;
}

View File

@ -0,0 +1,8 @@
<h2 highlight="gold">My Heroes</h2>
<ul class="heroes">
<li *ngFor="let hero of heroes | async "
[class.selected]="hero === selectedHero"
(click)="onSelect(hero)">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</li>
</ul>

View File

@ -0,0 +1,139 @@
import { async, ComponentFixture, fakeAsync, TestBed, tick
} from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { addMatchers, newEvent, Router, RouterStub
} from '../../testing';
import { HEROES, FakeHeroService } from '../model/testing';
import { HeroModule } from './hero.module';
import { HeroListComponent } from './hero-list.component';
import { HighlightDirective } from '../shared/highlight.directive';
import { HeroService } from '../model';
let comp: HeroListComponent;
let fixture: ComponentFixture<HeroListComponent>;
let page: Page;
/////// Tests //////
describe('HeroListComponent', () => {
beforeEach( async(() => {
addMatchers();
TestBed.configureTestingModule({
imports: [HeroModule],
providers: [
{ provide: HeroService, useClass: FakeHeroService },
{ provide: Router, useClass: RouterStub}
]
})
.compileComponents()
.then(createComponent);
}));
it('should display heroes', () => {
expect(page.heroRows.length).toBeGreaterThan(0);
});
it('1st hero should match 1st test hero', () => {
const expectedHero = HEROES[0];
const actualHero = page.heroRows[0].textContent;
expect(actualHero).toContain(expectedHero.id, 'hero.id');
expect(actualHero).toContain(expectedHero.name, 'hero.name');
});
it('should select hero on click', fakeAsync(() => {
const expectedHero = HEROES[1];
const li = page.heroRows[1];
li.dispatchEvent(newEvent('click'));
tick();
// `.toEqual` because selectedHero is clone of expectedHero; see FakeHeroService
expect(comp.selectedHero).toEqual(expectedHero);
}));
it('should navigate to selected hero detail on click', fakeAsync(() => {
const expectedHero = HEROES[1];
const li = page.heroRows[1];
li.dispatchEvent(newEvent('click'));
tick();
// should have navigated
expect(page.navSpy.calls.any()).toBe(true, 'navigate called');
// composed hero detail will be URL like 'heroes/42'
// expect link array with the route path and hero id
// first argument to router.navigate is link array
const navArgs = page.navSpy.calls.first().args[0];
expect(navArgs[0]).toContain('heroes', 'nav to heroes detail URL');
expect(navArgs[1]).toBe(expectedHero.id, 'expected hero.id');
}));
it('should find `HighlightDirective` with `By.directive', () => {
// #docregion by
// Can find DebugElement either by css selector or by directive
const h2 = fixture.debugElement.query(By.css('h2'));
const directive = fixture.debugElement.query(By.directive(HighlightDirective));
// #enddocregion by
expect(h2).toBe(directive);
});
it('should color header with `HighlightDirective`', () => {
const h2 = page.highlightDe.nativeElement as HTMLElement;
const bgColor = h2.style.backgroundColor;
// different browsers report color values differently
const isExpectedColor = bgColor === 'gold' || bgColor === 'rgb(255, 215, 0)';
expect(isExpectedColor).toBe(true, 'backgroundColor');
});
it('the `HighlightDirective` is among the element\'s providers', () => {
expect(page.highlightDe.providerTokens).toContain(HighlightDirective, 'HighlightDirective');
});
});
/////////// Helpers /////
/** Create the component and set the `page` test variables */
function createComponent() {
fixture = TestBed.createComponent(HeroListComponent);
comp = fixture.componentInstance;
// change detection triggers ngOnInit which gets a hero
fixture.detectChanges();
return fixture.whenStable().then(() => {
// got the heroes and updated component
// change detection updates the view
fixture.detectChanges();
page = new Page();
});
}
class Page {
/** Hero line elements */
heroRows: HTMLLIElement[];
/** Highlighted element */
highlightDe: DebugElement;
/** Spy on router navigate method */
navSpy: jasmine.Spy;
constructor() {
this.heroRows = fixture.debugElement.queryAll(By.css('li')).map(de => de.nativeElement);
// 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');
};
}

View File

@ -0,0 +1,27 @@
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { Hero, HeroService } from '../model';
@Component({
selector: 'app-heroes',
templateUrl: './hero-list.component.html',
styleUrls: [ './hero-list.component.css' ]
})
export class HeroListComponent implements OnInit {
heroes: Promise<Hero[]>;
selectedHero: Hero;
constructor(
private router: Router,
private heroService: HeroService) { }
ngOnInit() {
this.heroes = this.heroService.getHeroes();
}
onSelect(hero: Hero) {
this.selectedHero = hero;
this.router.navigate(['../heroes', this.selectedHero.id ]);
}
}

View File

@ -0,0 +1,18 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HeroListComponent } from './hero-list.component';
import { HeroDetailComponent } from './hero-detail.component';
const routes: Routes = [
{ path: '', component: HeroListComponent },
{ path: ':id', component: HeroDetailComponent }
];
export const routedComponents = [HeroDetailComponent, HeroListComponent];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class HeroRoutingModule {}

View File

@ -0,0 +1,9 @@
import { NgModule } from '@angular/core';
import { SharedModule } from '../shared/shared.module';
import { routedComponents, HeroRoutingModule } from './hero-routing.module';
@NgModule({
imports: [ SharedModule, HeroRoutingModule ],
declarations: [ routedComponents ]
})
export class HeroModule { }

View File

@ -0,0 +1,30 @@
import { Injectable } from '@angular/core';
import { Hero } from './hero';
import { HEROES } from './test-heroes';
@Injectable()
/** Dummy HeroService. Pretend it makes real http requests */
export class HeroService {
getHeroes() {
return Promise.resolve(HEROES);
}
getHero(id: number | string): Promise<Hero> {
if (typeof id === 'string') {
id = parseInt(id as string, 10);
}
return this.getHeroes().then(
heroes => heroes.find(hero => hero.id === id)
);
}
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);
});
}
}

View File

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

View File

@ -0,0 +1,4 @@
export class Hero {
constructor(public id = 0, public name = '') { }
clone() { return new Hero(this.id, this.name); }
}

View File

@ -0,0 +1,127 @@
import {
async, inject, TestBed
} from '@angular/core/testing';
import {
MockBackend,
MockConnection
} from '@angular/http/testing';
import {
HttpModule, Http, XHRBackend, Response, ResponseOptions
} 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 { Hero } from './hero';
import { HttpHeroService as HeroService } from './http-hero.service';
const makeHeroData = () => [
{ id: 1, name: 'Windstorm' },
{ id: 2, name: 'Bombasto' },
{ id: 3, name: 'Magneta' },
{ id: 4, name: 'Tornado' }
] as Hero[];
//////// Tests /////////////
describe('Http-HeroService (mockBackend)', () => {
beforeEach( async(() => {
TestBed.configureTestingModule({
imports: [ HttpModule ],
providers: [
HeroService,
{ 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 with "new"', inject([Http], (http: 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');
}));
it('can provide the mockBackend as XHRBackend',
inject([XHRBackend], (backend: MockBackend) => {
expect(backend).not.toBeNull('backend should be provided');
}));
describe('when getHeroes', () => {
let backend: MockBackend;
let service: HeroService;
let fakeHeroes: Hero[];
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);
}));
it('should have expected fake heroes (then)', async(inject([], () => {
backend.connections.subscribe((c: MockConnection) => c.mockRespond(response));
service.getHeroes().toPromise()
// .then(() => Promise.reject('deliberate'))
.then(heroes => {
expect(heroes.length).toBe(fakeHeroes.length,
'should have expected no. of heroes');
});
})));
it('should have expected fake heroes (Observable.do)', async(inject([], () => {
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();
})));
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));
service.getHeroes()
.do(heroes => {
expect(heroes.length).toBe(0, 'should have no heroes');
})
.toPromise();
})));
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));
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();
})));
});
});

View File

@ -0,0 +1,68 @@
// #docplaster
// #docregion
import { Injectable } from '@angular/core';
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';
@Injectable()
export class HttpHeroService {
private _heroesUrl = 'app/heroes'; // URL to web api
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);
}
getHero(id: number | string) {
return this.http
.get('app/heroes/?id=${id}')
.map((r: Response) => r.json().data as Hero[]);
}
addHero (name: string): Observable<Hero> {
let body = JSON.stringify({ name });
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);
}
updateHero (hero: Hero): Observable<Hero> {
let body = JSON.stringify(hero);
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);
}
private extractData(res: Response) {
if (res.status < 200 || res.status >= 300) {
throw new Error('Bad response status: ' + res.status);
}
let body = res.json();
return body.data || { };
}
private handleError (error: any) {
// 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);
}
}

View File

@ -0,0 +1,7 @@
// Model barrel
export * from './hero';
export * from './hero.service';
export * from './http-hero.service';
export * from './test-heroes';
export * from './user.service';

View File

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

View File

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

View File

@ -0,0 +1 @@
export * from './fake-hero.service';

View File

@ -0,0 +1,7 @@
import { Injectable } from '@angular/core';
@Injectable()
export class UserService {
isLoggedIn = true;
user = {name: 'Sam Spade'};
}

View File

@ -0,0 +1,104 @@
import { Component, DebugElement } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { HighlightDirective } from './highlight.directive';
import { newEvent } from '../../testing';
// #docregion test-component
@Component({
template: `
<h2 highlight="yellow">Something Yellow</h2>
<h2 highlight>The Default (Gray)</h2>
<h2>No Highlight</h2>
<input #box [highlight]="box.value" value="cyan"/>`
})
class TestComponent { }
// #enddocregion test-component
describe('HighlightDirective', () => {
let fixture: ComponentFixture<TestComponent>;
let des: DebugElement[]; // the three elements w/ the directive
let bareH2: DebugElement; // the <h2> w/o the directive
// #docregion selected-tests
beforeEach(() => {
fixture = TestBed.configureTestingModule({
declarations: [ HighlightDirective, TestComponent ]
})
.createComponent(TestComponent);
fixture.detectChanges(); // initial binding
// all elements with an attached HighlightDirective
des = fixture.debugElement.queryAll(By.directive(HighlightDirective));
// the h2 without the HighlightDirective
bareH2 = fixture.debugElement.query(By.css('h2:not([highlight])'));
});
// color tests
it('should have three highlighted elements', () => {
expect(des.length).toBe(3);
});
it('should color 1st <h2> background "yellow"', () => {
const bgColor = des[0].nativeElement.style.backgroundColor;
expect(bgColor).toBe('yellow');
});
it('should color 2nd <h2> background w/ default color', () => {
const dir = des[1].injector.get(HighlightDirective) as HighlightDirective;
const bgColor = des[1].nativeElement.style.backgroundColor;
expect(bgColor).toBe(dir.defaultColor);
});
it('should bind <input> background to value color', () => {
// easier to work with nativeElement
const input = des[2].nativeElement as HTMLInputElement;
expect(input.style.backgroundColor).toBe('cyan', 'initial backgroundColor');
// dispatch a DOM event so that Angular responds to the input value change.
input.value = 'green';
input.dispatchEvent(newEvent('input'));
fixture.detectChanges();
expect(input.style.backgroundColor).toBe('green', 'changed backgroundColor');
});
it('bare <h2> should not have a customProperty', () => {
expect(bareH2.properties['customProperty']).toBeUndefined();
});
// #enddocregion selected-tests
// Removed on 12/02/2016 when ceased public discussion of the `Renderer`. Revive in future?
// // customProperty tests
// it('all highlighted elements should have a true customProperty', () => {
// const allTrue = des.map(de => !!de.properties['customProperty']).every(v => v === true);
// expect(allTrue).toBe(true);
// });
// injected directive
// attached HighlightDirective can be injected
it('can inject `HighlightDirective` in 1st <h2>', () => {
const dir = des[0].injector.get(HighlightDirective);
expect(dir).toBeTruthy();
});
it('cannot inject `HighlightDirective` in 3rd <h2>', () => {
const dir = bareH2.injector.get(HighlightDirective, null);
expect(dir).toBe(null);
});
// DebugElement.providerTokens
// attached HighlightDirective should be listed in the providerTokens
it('should have `HighlightDirective` in 1st <h2> providerTokens', () => {
expect(des[0].providerTokens).toContain(HighlightDirective);
});
it('should not have `HighlightDirective` in 3rd <h2> providerTokens', () => {
expect(bareH2.providerTokens).not.toContain(HighlightDirective);
});
});

View File

@ -0,0 +1,20 @@
// #docregion
import { Directive, ElementRef, Input, OnChanges } from '@angular/core';
@Directive({ selector: '[highlight]' })
/** Set backgroundColor for the attached element to highlight color
* and set the element's customProperty to true */
export class HighlightDirective implements OnChanges {
defaultColor = 'rgb(211, 211, 211)'; // lightgray
@Input('highlight') bgColor: string;
constructor(private el: ElementRef) {
el.nativeElement.style.customProperty = true;
}
ngOnChanges() {
this.el.nativeElement.style.backgroundColor = this.bgColor || this.defaultColor;
}
}

View File

@ -0,0 +1,15 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
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 ]
})
export class SharedModule { }

View File

@ -0,0 +1,34 @@
// #docplaster
// #docregion
import { TitleCasePipe } from './title-case.pipe';
// #docregion excerpt, mini-excerpt
describe('TitleCasePipe', () => {
// This pipe is a pure, stateless function so no need for BeforeEach
let pipe = new TitleCasePipe();
it('transforms "abc" to "Abc"', () => {
expect(pipe.transform('abc')).toBe('Abc');
});
// #enddocregion mini-excerpt
it('transforms "abc def" to "Abc Def"', () => {
expect(pipe.transform('abc def')).toBe('Abc Def');
});
// ... more tests ...
// #enddocregion excerpt
it('leaves "Abc Def" unchanged', () => {
expect(pipe.transform('Abc Def')).toBe('Abc Def');
});
it('transforms "abc-def" to "Abc-def"', () => {
expect(pipe.transform('abc-def')).toBe('Abc-def');
});
it('transforms " abc def" to " Abc Def" (preserves spaces) ', () => {
expect(pipe.transform(' abc def')).toBe(' Abc Def');
});
// #docregion excerpt, mini-excerpt
});
// #enddocregion excerpt, mini-excerpt

View File

@ -0,0 +1,11 @@
// #docregion
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({name: 'titlecase', pure: false})
/** Transform to Title Case: uppercase the first letter of the words in a string.*/
export class TitleCasePipe implements PipeTransform {
transform(input: string): string {
return input.length === 0 ? '' :
input.replace(/\w\S*/g, (txt => txt[0].toUpperCase() + txt.substr(1).toLowerCase() ));
}
}

View File

@ -0,0 +1,92 @@
// #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 => {
fixture.detectChanges();
// get the spy promise and wait for it to resolve
spy.calls.mostRecent().returnValue.then(() => {
fixture.detectChanges(); // update view with quote
expect(el.textContent).toBe(testQuote);
done();
});
});
// #enddocregion done-test
});

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,108 @@
// #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 { WelcomeComponent } from './welcome.component';
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
beforeEach(() => {
// stub UserService for test purposes
// #docregion user-service-stub
userServiceStub = {
isLoggedIn: true,
user: { name: 'Test User'}
};
// #enddocregion user-service-stub
// #docregion config-test-module
TestBed.configureTestingModule({
declarations: [ WelcomeComponent ],
// #enddocregion setup
// providers: [ UserService ] // NO! Don't provide the real service!
// Provide a test-double instead
// #docregion setup
providers: [ {provide: UserService, useValue: userServiceStub } ]
});
// #enddocregion config-test-module
fixture = TestBed.createComponent(WelcomeComponent);
comp = fixture.componentInstance;
// #enddocregion setup
// #docregion injected-service
// UserService actually injected into the component
userService = fixture.debugElement.injector.get(UserService);
// #enddocregion injected-service
componentUserService = userService;
// #docregion setup
// #docregion inject-from-testbed
// UserService from the root injector
userService = TestBed.get(UserService);
// #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;
});
// #enddocregion setup
// #docregion tests
it('should welcome the user', () => {
fixture.detectChanges();
const content = el.textContent;
expect(content).toContain('Welcome', '"Welcome ..."');
expect(content).toContain('Test User', 'expected name');
});
it('should welcome "Bubba"', () => {
userService.user.name = 'Bubba'; // welcome message hasn't been shown yet
fixture.detectChanges();
expect(el.textContent).toContain('Bubba');
});
it('should request login if not logged in', () => {
userService.isLoggedIn = false; // welcome message hasn't been shown yet
fixture.detectChanges();
const content = el.textContent;
expect(content).not.toContain('Welcome', 'not welcomed');
expect(content).toMatch(/log in/i, '"log in"');
});
// #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);
});
// #docregion stub-not-injected
it('stub object and injected UserService should not be the same', () => {
expect(userServiceStub === userService).toBe(false);
// Changing the stub object has no effect on the injected service
userServiceStub.isLoggedIn = false;
expect(userService.isLoggedIn).toBe(true);
});
// #enddocregion stub-not-injected
});

View File

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