docs(aio): update migrated content from anguar.io
This commit is contained in:

committed by
Pete Bacon Darwin

parent
ff82756415
commit
fd72fad8fd
5
aio/content/examples/testing/src/app/1st.spec.ts
Normal file
5
aio/content/examples/testing/src/app/1st.spec.ts
Normal file
@ -0,0 +1,5 @@
|
||||
// #docplaster
|
||||
// #docregion
|
||||
describe('1st tests', () => {
|
||||
it('true is true', () => expect(true).toBe(true));
|
||||
});
|
27
aio/content/examples/testing/src/app/about.component.spec.ts
Normal file
27
aio/content/examples/testing/src/app/about.component.spec.ts
Normal 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
|
||||
});
|
9
aio/content/examples/testing/src/app/about.component.ts
Normal file
9
aio/content/examples/testing/src/app/about.component.ts
Normal 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 { }
|
16
aio/content/examples/testing/src/app/app-routing.module.ts
Normal file
16
aio/content/examples/testing/src/app/app-routing.module.ts
Normal 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 { };
|
11
aio/content/examples/testing/src/app/app.component.html
Normal file
11
aio/content/examples/testing/src/app/app.component.html
Normal 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>
|
@ -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;
|
||||
}
|
148
aio/content/examples/testing/src/app/app.component.spec.ts
Normal file
148
aio/content/examples/testing/src/app/app.component.spec.ts
Normal 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
|
||||
}
|
7
aio/content/examples/testing/src/app/app.component.ts
Normal file
7
aio/content/examples/testing/src/app/app.component.ts
Normal file
@ -0,0 +1,7 @@
|
||||
// #docregion
|
||||
import { Component } from '@angular/core';
|
||||
@Component({
|
||||
selector: 'my-app',
|
||||
templateUrl: './app.component.html'
|
||||
})
|
||||
export class AppComponent { }
|
29
aio/content/examples/testing/src/app/app.module.ts
Normal file
29
aio/content/examples/testing/src/app/app.module.ts
Normal 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 { }
|
@ -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();
|
||||
// }));
|
||||
|
||||
});
|
@ -0,0 +1 @@
|
||||
<span>from external template</span>
|
5
aio/content/examples/testing/src/app/bag/bag-main.ts
Normal file
5
aio/content/examples/testing/src/app/bag/bag-main.ts
Normal file
@ -0,0 +1,5 @@
|
||||
// main app entry point
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||
import { BagModule } from './bag';
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(BagModule);
|
130
aio/content/examples/testing/src/app/bag/bag.no-testbed.spec.ts
Normal file
130
aio/content/examples/testing/src/app/bag/bag.no-testbed.spec.ts
Normal 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
|
680
aio/content/examples/testing/src/app/bag/bag.spec.ts
Normal file
680
aio/content/examples/testing/src/app/bag/bag.spec.ts
Normal 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';
|
||||
}
|
454
aio/content/examples/testing/src/app/bag/bag.ts
Normal file
454
aio/content/examples/testing/src/app/bag/bag.ts
Normal 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 { }
|
||||
|
@ -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
|
@ -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';
|
||||
}
|
||||
|
@ -0,0 +1 @@
|
||||
h1 { color: green; font-size: 350%}
|
@ -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
|
||||
});
|
@ -0,0 +1 @@
|
||||
<h1>{{title}}</h1>
|
@ -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');
|
||||
});
|
||||
|
||||
});
|
12
aio/content/examples/testing/src/app/banner.component.ts
Normal file
12
aio/content/examples/testing/src/app/banner.component.ts
Normal 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';
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
<!-- #docregion -->
|
||||
<div (click)="click()" class="hero">
|
||||
{{hero.name | uppercase}}
|
||||
</div>
|
@ -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
|
@ -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
|
@ -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;
|
||||
}
|
||||
}
|
@ -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>
|
@ -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');
|
||||
});
|
||||
|
||||
});
|
@ -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
|
||||
});
|
||||
}
|
||||
|
@ -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`;
|
||||
}
|
||||
}
|
@ -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 { }
|
@ -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;
|
||||
}
|
@ -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>
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
@ -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
|
@ -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
|
@ -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
|
@ -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;
|
||||
}
|
@ -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>
|
@ -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');
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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 ]);
|
||||
}
|
||||
}
|
@ -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 {}
|
9
aio/content/examples/testing/src/app/hero/hero.module.ts
Normal file
9
aio/content/examples/testing/src/app/hero/hero.module.ts
Normal 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 { }
|
30
aio/content/examples/testing/src/app/model/hero.service.ts
Normal file
30
aio/content/examples/testing/src/app/model/hero.service.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
20
aio/content/examples/testing/src/app/model/hero.spec.ts
Normal file
20
aio/content/examples/testing/src/app/model/hero.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
4
aio/content/examples/testing/src/app/model/hero.ts
Normal file
4
aio/content/examples/testing/src/app/model/hero.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export class Hero {
|
||||
constructor(public id = 0, public name = '') { }
|
||||
clone() { return new Hero(this.id, this.name); }
|
||||
}
|
@ -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();
|
||||
})));
|
||||
});
|
||||
});
|
@ -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);
|
||||
}
|
||||
}
|
7
aio/content/examples/testing/src/app/model/index.ts
Normal file
7
aio/content/examples/testing/src/app/model/index.ts
Normal 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';
|
11
aio/content/examples/testing/src/app/model/test-heroes.ts
Normal file
11
aio/content/examples/testing/src/app/model/test-heroes.ts
Normal 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')
|
||||
];
|
@ -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>;
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
export * from './fake-hero.service';
|
@ -0,0 +1,7 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
isLoggedIn = true;
|
||||
user = {name: 'Sam Spade'};
|
||||
}
|
@ -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);
|
||||
});
|
||||
});
|
@ -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;
|
||||
}
|
||||
}
|
15
aio/content/examples/testing/src/app/shared/shared.module.ts
Normal file
15
aio/content/examples/testing/src/app/shared/shared.module.ts
Normal 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 { }
|
@ -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
|
@ -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() ));
|
||||
}
|
||||
}
|
@ -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
|
||||
});
|
@ -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
|
||||
});
|
@ -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);
|
||||
}
|
||||
}
|
@ -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
|
32
aio/content/examples/testing/src/app/shared/twain.service.ts
Normal file
32
aio/content/examples/testing/src/app/shared/twain.service.ts
Normal 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++ ];
|
||||
}
|
||||
}
|
108
aio/content/examples/testing/src/app/welcome.component.spec.ts
Normal file
108
aio/content/examples/testing/src/app/welcome.component.spec.ts
Normal 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
|
||||
});
|
18
aio/content/examples/testing/src/app/welcome.component.ts
Normal file
18
aio/content/examples/testing/src/app/welcome.component.ts
Normal 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.';
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user