\ No newline at end of file
diff --git a/public/docs/_examples/testing/ts/app/hero-detail.component.ts b/public/docs/_examples/testing/ts/app/hero-detail.component.ts
deleted file mode 100644
index 3fcbf071e0..0000000000
--- a/public/docs/_examples/testing/ts/app/hero-detail.component.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-/* tslint:disable */
-// #docplaster
-// #docregion
-// #docregion v2
-// #docregion import-oninit
-import { Component, OnInit } from '@angular/core';
-// #enddocregion import-oninit
-// #docregion import-route-params
-import { RouteParams } from '@angular/router-deprecated';
-// #enddocregion import-route-params
-
-import { Hero } from './hero';
-// #docregion import-hero-service
-import { HeroService } from './hero.service';
-// #enddocregion import-hero-service
-
-// #docregion extract-template
-@Component({
- selector: 'my-hero-detail',
- // #docregion template-url
- templateUrl: 'app/hero-detail.component.html',
- // #enddocregion template-url
-// #enddocregion v2
- styleUrls: ['app/hero-detail.component.css'],
- inputs: ['hero']
-// #docregion v2
-})
-// #enddocregion extract-template
-// #docregion implement
-export class HeroDetailComponent implements OnInit {
-// #enddocregion implement
- hero: Hero;
-
-// #docregion ctor
- constructor(
- private _heroService: HeroService,
- private _routeParams: RouteParams) {
- }
-// #enddocregion ctor
-
-// #docregion ng-oninit
- ngOnInit() {
- // #docregion get-id
- let id = +this._routeParams.get('id');
- // #enddocregion get-id
- this._heroService.getHero(id)
- .then(hero => this.hero = hero);
- }
-// #enddocregion ng-oninit
-
-// #docregion go-back
- goBack() {
- window.history.back();
- }
-// #enddocregion go-back
-}
-// #enddocregion v2
-// #enddocregion
diff --git a/public/docs/_examples/testing/ts/app/hero.service.ts b/public/docs/_examples/testing/ts/app/hero.service.ts
deleted file mode 100644
index e9473e4038..0000000000
--- a/public/docs/_examples/testing/ts/app/hero.service.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-// #docplaster
-// #docregion
-import { Hero } from './hero';
-import { HEROES } from './mock-heroes';
-import { Injectable } from '@angular/core';
-
-@Injectable()
-export class HeroService {
- getHeroes() {
- return Promise.resolve(HEROES);
- }
-
- // See the "Take it slow" appendix
- getHeroesSlowly() {
- return new Promise(resolve =>
- setTimeout(() => resolve(HEROES), 2000) // 2 seconds
- );
- }
-
- // #docregion get-hero
- getHero(id: number) {
- return Promise.resolve(HEROES).then(
- heroes => heroes.find(hero => hero.id === id)
- );
- }
- // #enddocregion get-hero
-}
-// #enddocregion
diff --git a/public/docs/_examples/testing/ts/app/hero.spec.ts b/public/docs/_examples/testing/ts/app/hero.spec.ts
deleted file mode 100644
index 78a73ad6b0..0000000000
--- a/public/docs/_examples/testing/ts/app/hero.spec.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-// #docregion
-// #docplaster
-// #docregion base-hero-spec
-import { Hero } from './hero';
-
-describe('Hero', () => {
-
- it('has name', () => {
- let hero: Hero = {id: 1, name: 'Super Cat'};
- expect(hero.name).toEqual('Super Cat');
- });
-
- it('has id', () => {
- let hero: Hero = {id: 1, name: 'Super Cat'};
- expect(hero.id).toEqual(1);
- });
- // #enddocregion base-hero-spec
-
-
- /* more tests we could run
-
- it('can clone itself', () => {
- let hero = new Hero(1, 'Super Cat');
- let clone = hero.clone();
- expect(hero).toEqual(clone);
- });
-
- it('has expected generated id when id not given in the constructor', () => {
- Hero.setNextId(100); // reset the `nextId` seed
- let hero = new Hero(null, 'Cool Kitty');
- expect(hero.id).toEqual(100);
- });
-
- it('has expected generated id when id=0 in the constructor', () => {
- Hero.setNextId(100);
- let hero = new Hero(0, 'Cool Kitty');
- expect(hero.id).toEqual(100);
- })
-
- it('increments generated id for each new Hero w/o an id', () => {
- Hero.setNextId(100);
- let hero1 = new Hero(0, 'Cool Kitty');
- let hero2 = new Hero(null, 'Hip Cat');
- expect(hero2.id).toEqual(101);
- });
-
- */
- // #docregion base-hero-spec
-});
-// #enddocregion base-hero-spec
diff --git a/public/docs/_examples/testing/ts/app/hero.ts b/public/docs/_examples/testing/ts/app/hero.ts
deleted file mode 100644
index 8f7cc205c8..0000000000
--- a/public/docs/_examples/testing/ts/app/hero.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-// #docregion
-export class Hero {
- id: number;
- name: string;
-}
diff --git a/public/docs/_examples/testing/ts/app/hero-detail.component.css b/public/docs/_examples/testing/ts/app/hero/hero-detail.component.css
similarity index 93%
rename from public/docs/_examples/testing/ts/app/hero-detail.component.css
rename to public/docs/_examples/testing/ts/app/hero/hero-detail.component.css
index ab2437efd8..f6139ba274 100644
--- a/public/docs/_examples/testing/ts/app/hero-detail.component.css
+++ b/public/docs/_examples/testing/ts/app/hero/hero-detail.component.css
@@ -1,4 +1,3 @@
-/* #docregion */
label {
display: inline-block;
width: 3em;
@@ -25,6 +24,6 @@ button:hover {
}
button:disabled {
background-color: #eee;
- color: #ccc;
+ color: #ccc;
cursor: auto;
}
diff --git a/public/docs/_examples/testing/ts/app/hero/hero-detail.component.html b/public/docs/_examples/testing/ts/app/hero/hero-detail.component.html
new file mode 100644
index 0000000000..6927fc83ad
--- /dev/null
+++ b/public/docs/_examples/testing/ts/app/hero/hero-detail.component.html
@@ -0,0 +1,11 @@
+
+
{{hero.name | titlecase}} Details
+
+ {{hero.id}}
+
+
+
+
+
+
+
diff --git a/public/docs/_examples/testing/ts/app/hero/hero-detail.component.no-testbed.spec.ts b/public/docs/_examples/testing/ts/app/hero/hero-detail.component.no-testbed.spec.ts
new file mode 100644
index 0000000000..73c22f29e7
--- /dev/null
+++ b/public/docs/_examples/testing/ts/app/hero/hero-detail.component.no-testbed.spec.ts
@@ -0,0 +1,58 @@
+import { HeroDetailComponent } from './hero-detail.component';
+import { Hero } from '../model';
+
+import { FakeActivatedRoute } from '../../testing';
+
+////////// Tests ////////////////////
+
+describe('HeroDetailComponent - no TestBed', () => {
+ let activatedRoute: FakeActivatedRoute;
+ let comp: HeroDetailComponent;
+ let expectedHero: Hero;
+ let hds: any;
+ let router: any;
+
+ beforeEach( done => {
+ expectedHero = new Hero(42, 'Bubba');
+ activatedRoute = new FakeActivatedRoute();
+ 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, 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();
+ });
+ });
+
+});
diff --git a/public/docs/_examples/testing/ts/app/hero/hero-detail.component.spec.ts b/public/docs/_examples/testing/ts/app/hero/hero-detail.component.spec.ts
new file mode 100644
index 0000000000..0dd52ed54e
--- /dev/null
+++ b/public/docs/_examples/testing/ts/app/hero/hero-detail.component.spec.ts
@@ -0,0 +1,196 @@
+import {
+ async, ComponentFixture, fakeAsync, inject, TestBed, tick
+} from '@angular/core/testing';
+
+import { By } from '@angular/platform-browser';
+import { DebugElement } from '@angular/core';
+
+import {
+ addMatchers, newEvent,
+ ActivatedRoute, FakeActivatedRoute, Router, FakeRouter
+} from '../../testing';
+
+import { HEROES, FakeHeroService } from '../model/testing';
+
+import { HeroModule } from './hero.module';
+import { HeroDetailComponent } from './hero-detail.component';
+import { HeroDetailService } from './hero-detail.service';
+import { Hero, HeroService } from '../model';
+
+////// Testing Vars //////
+let activatedRoute: FakeActivatedRoute;
+let comp: HeroDetailComponent;
+let fixture: ComponentFixture;
+let page: Page;
+
+////////// Tests ////////////////////
+
+describe('HeroDetailComponent', () => {
+
+ beforeEach( async(() => {
+ addMatchers();
+ activatedRoute = new FakeActivatedRoute();
+
+ TestBed.configureTestingModule({
+ imports: [ HeroModule ],
+
+ // DON'T RE-DECLARE because already declared in HeroModule
+ // declarations: [HeroDetailComponent, TitleCasePipe], // No!
+
+ providers: [
+ { provide: ActivatedRoute, useValue: activatedRoute },
+ { provide: HeroService, useClass: FakeHeroService },
+ { provide: Router, useClass: FakeRouter},
+ ]
+ })
+ .compileComponents();
+ }));
+
+ describe('when navigate to hero id=' + HEROES[0].id, () => {
+ let expectedHero: Hero;
+
+ beforeEach( async(() => {
+ expectedHero = HEROES[0];
+ activatedRoute.testParams = { id: expectedHero.id };
+ createComponent();
+ }));
+
+ it('should display that hero\'s name', () => {
+ expect(page.nameDisplay.textContent).toBe(expectedHero.name);
+ });
+
+ it('should navigate when click cancel', () => {
+ page.cancelBtn.triggerEventHandler('click', null);
+ expect(page.navSpy.calls.any()).toBe(true, 'router.navigate called');
+ });
+
+ it('should save when click save', () => {
+ page.saveBtn.triggerEventHandler('click', null);
+ expect(page.saveSpy.calls.any()).toBe(true, 'HeroDetailService.save called');
+ });
+
+ it('should navigate when click click save resolves', fakeAsync(() => {
+ page.saveBtn.triggerEventHandler('click', null);
+ tick(); // waits for async save to "complete" before navigating
+ expect(page.navSpy.calls.any()).toBe(true, 'router.navigate called');
+ }));
+
+
+ // #docregion title-case-pipe
+ it('should convert original hero name to Title Case', () => {
+ expect(page.nameDisplay.textContent).toBe(comp.hero.name);
+ });
+ // #enddocregion title-case-pipe
+
+ it('should convert hero name to Title Case', fakeAsync(() => {
+ const inputName = 'quick BROWN fox';
+ const expectedName = 'Quick Brown Fox';
+
+ // simulate user entering new name in input
+ page.nameInput.value = inputName;
+
+ // dispatch a DOM event so that Angular learns of input value change.
+ // detectChanges() makes ngModel push input value to component property
+ // and Angular updates the output span
+ page.nameInput.dispatchEvent(newEvent('input'));
+ fixture.detectChanges();
+ expect(page.nameDisplay.textContent).toBe(expectedName, 'hero name display');
+ expect(comp.hero.name).toBe(inputName, 'comp.hero.name');
+ }));
+
+ });
+
+ 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('');
+ });
+ });
+
+ 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');
+ });
+ });
+
+ ///////////////////////////
+
+ // Why we must use `fixture.debugElement.injector` in `Page()`
+ it('cannot use `inject` to get component\'s provided service', () => {
+ 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');
+ });
+});
+
+/////////// Helpers /////
+
+/** Create the HeroDetailComponent, initialize it, set test variables */
+function createComponent() {
+ fixture = TestBed.createComponent(HeroDetailComponent);
+ comp = fixture.componentInstance;
+ page = new Page();
+
+ // change detection triggers ngOnInit which gets a hero
+ fixture.detectChanges();
+ return fixture.whenStable().then(() => {
+ // got the hero and updated component
+ // change detection updates the view
+ fixture.detectChanges();
+ page.addPageElements();
+ });
+}
+
+class Page {
+ gotoSpy: jasmine.Spy;
+ navSpy: jasmine.Spy;
+ saveSpy: jasmine.Spy;
+
+ saveBtn: DebugElement;
+ cancelBtn: DebugElement;
+ nameDisplay: HTMLElement;
+ nameInput: HTMLInputElement;
+
+ constructor() {
+ // Use component's injector to see the services it injected.
+ let compInjector = fixture.debugElement.injector;
+ let hds = compInjector.get(HeroDetailService);
+ let router = compInjector.get(Router);
+ this.gotoSpy = spyOn(comp, 'gotoList').and.callThrough();
+ this.saveSpy = spyOn(hds, 'saveHero').and.callThrough();
+ this.navSpy = spyOn(router, 'navigate').and.callThrough();
+ }
+
+ /** Add page elements after page initializes */
+ addPageElements() {
+ if (comp.hero) {
+ // have a hero so these DOM elements can be reached
+ let 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;
+ }
+ }
+}
+
diff --git a/public/docs/_examples/testing/ts/app/hero/hero-detail.component.ts b/public/docs/_examples/testing/ts/app/hero/hero-detail.component.ts
new file mode 100644
index 0000000000..9350c369af
--- /dev/null
+++ b/public/docs/_examples/testing/ts/app/hero/hero-detail.component.ts
@@ -0,0 +1,52 @@
+import { Component, Input, OnInit } from '@angular/core';
+import { ActivatedRoute, Router } from '@angular/router';
+
+import { Hero } from '../model';
+import { HeroDetailService } from './hero-detail.service';
+
+@Component({
+ selector: 'app-hero-detail',
+ templateUrl: 'app/hero/hero-detail.component.html',
+ styleUrls: [
+ 'app/shared/styles.css',
+ 'app/hero/hero-detail.component.css'
+ ],
+ providers: [ HeroDetailService ]
+})
+export class HeroDetailComponent implements OnInit {
+ @Input() hero: Hero;
+
+ constructor(
+ private heroDetailService: HeroDetailService,
+ private route: ActivatedRoute,
+ private router: Router) {
+ }
+
+ ngOnInit() {
+ let id = this.route.snapshot.params['id'];
+
+ // tslint:disable-next-line:triple-equals
+ if (id == undefined) {
+ // no id; act as if is new
+ this.hero = new Hero();
+ } else {
+ this.heroDetailService.getHero(id).then(hero => {
+ if (hero) {
+ this.hero = hero;
+ } else {
+ this.gotoList(); // id not found; navigate to list
+ }
+ });
+ }
+ }
+
+ save() {
+ this.heroDetailService.saveHero(this.hero).then(() => this.gotoList());
+ }
+
+ cancel() { this.gotoList(); }
+
+ gotoList() {
+ this.router.navigate(['../'], {relativeTo: this.route});
+ }
+}
diff --git a/public/docs/_examples/testing/ts/app/hero/hero-detail.service.ts b/public/docs/_examples/testing/ts/app/hero/hero-detail.service.ts
new file mode 100644
index 0000000000..970cb1b98b
--- /dev/null
+++ b/public/docs/_examples/testing/ts/app/hero/hero-detail.service.ts
@@ -0,0 +1,21 @@
+import { Injectable } from '@angular/core';
+
+import { Hero, HeroService } from '../model';
+
+@Injectable()
+export class HeroDetailService {
+ constructor(private heroService: HeroService) { }
+
+ getHero(id: number | string): Promise {
+ 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);
+ }
+}
diff --git a/public/docs/_examples/testing/ts/app/heroes.component.css b/public/docs/_examples/testing/ts/app/hero/hero-list.component.css
similarity index 100%
rename from public/docs/_examples/testing/ts/app/heroes.component.css
rename to public/docs/_examples/testing/ts/app/hero/hero-list.component.css
diff --git a/public/docs/_examples/testing/ts/app/hero/hero-list.component.html b/public/docs/_examples/testing/ts/app/hero/hero-list.component.html
new file mode 100644
index 0000000000..cd37301fd6
--- /dev/null
+++ b/public/docs/_examples/testing/ts/app/hero/hero-list.component.html
@@ -0,0 +1,8 @@
+
My Heroes
+
+
+ {{hero.id}} {{hero.name}}
+
+
diff --git a/public/docs/_examples/testing/ts/app/hero/hero-list.component.spec.ts b/public/docs/_examples/testing/ts/app/hero/hero-list.component.spec.ts
new file mode 100644
index 0000000000..f997cf787e
--- /dev/null
+++ b/public/docs/_examples/testing/ts/app/hero/hero-list.component.spec.ts
@@ -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, FakeRouter
+} 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;
+let page: Page;
+
+/////// Tests //////
+
+describe('HeroListComponent', () => {
+
+ beforeEach( async(() => {
+ addMatchers();
+ TestBed.configureTestingModule({
+ imports: [HeroModule],
+ providers: [
+ { provide: HeroService, useClass: FakeHeroService },
+ { provide: Router, useClass: FakeRouter}
+ ]
+ })
+ .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').and.callThrough();
+ };
+}
+
+
diff --git a/public/docs/_examples/testing/ts/app/hero/hero-list.component.ts b/public/docs/_examples/testing/ts/app/hero/hero-list.component.ts
new file mode 100644
index 0000000000..d4ad30b019
--- /dev/null
+++ b/public/docs/_examples/testing/ts/app/hero/hero-list.component.ts
@@ -0,0 +1,30 @@
+import { Component, OnInit } from '@angular/core';
+import { Router } from '@angular/router';
+
+import { Hero, HeroService } from '../model';
+
+@Component({
+ selector: 'app-heroes',
+ templateUrl: 'app/hero/hero-list.component.html',
+ styleUrls: [
+ 'app/shared/styles.css',
+ 'app/hero/hero-list.component.css'
+ ]
+})
+export class HeroListComponent implements OnInit {
+ heroes: Promise;
+ 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 ]);
+ }
+}
diff --git a/public/docs/_examples/testing/ts/app/hero/hero.module.ts b/public/docs/_examples/testing/ts/app/hero/hero.module.ts
new file mode 100644
index 0000000000..541d49103f
--- /dev/null
+++ b/public/docs/_examples/testing/ts/app/hero/hero.module.ts
@@ -0,0 +1,9 @@
+import { NgModule } from '@angular/core';
+import { SharedModule } from '../shared/shared.module';
+import { routedComponents, routing } from './hero.routing';
+
+@NgModule({
+ imports: [ SharedModule, routing ],
+ declarations: [ routedComponents ]
+})
+export class HeroModule { }
diff --git a/public/docs/_examples/testing/ts/app/hero/hero.routing.ts b/public/docs/_examples/testing/ts/app/hero/hero.routing.ts
new file mode 100644
index 0000000000..9530bc3953
--- /dev/null
+++ b/public/docs/_examples/testing/ts/app/hero/hero.routing.ts
@@ -0,0 +1,12 @@
+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];
+export const routing = RouterModule.forChild(routes);
diff --git a/public/docs/_examples/testing/ts/app/heroes.component.html b/public/docs/_examples/testing/ts/app/heroes.component.html
deleted file mode 100644
index cce1853d30..0000000000
--- a/public/docs/_examples/testing/ts/app/heroes.component.html
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
My Heroes
-
-
- {{hero.id}} {{hero.name}}
-
-
-
-
-
-
- {{selectedHero.name | uppercase}} is my hero
-
-
-
-
-
-
diff --git a/public/docs/_examples/testing/ts/app/heroes.component.ts b/public/docs/_examples/testing/ts/app/heroes.component.ts
deleted file mode 100644
index 1e2651f256..0000000000
--- a/public/docs/_examples/testing/ts/app/heroes.component.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-// #docplaster
-// #docregion
-import { Component, OnInit } from '@angular/core';
-import { Router } from '@angular/router-deprecated';
-
-import { Hero } from './hero';
-import { HeroDetailComponent } from './hero-detail.component';
-import { HeroService } from './hero.service';
-
-// #docregion metadata
-// #docregion heroes-component-renaming
-@Component({
- selector: 'my-heroes',
-// #enddocregion heroes-component-renaming
- templateUrl: 'app/heroes.component.html',
- styleUrls: ['app/heroes.component.css'],
- directives: [HeroDetailComponent]
-// #docregion heroes-component-renaming
-})
-// #enddocregion heroes-component-renaming
-// #enddocregion metadata
-// #docregion class
-// #docregion heroes-component-renaming
-export class HeroesComponent implements OnInit {
-// #enddocregion heroes-component-renaming
- heroes: Hero[];
- selectedHero: Hero;
-
- constructor(
- private _router: Router,
- private _heroService: HeroService) { }
-
- getHeroes() {
- this._heroService.getHeroes().then(heroes => this.heroes = heroes);
- }
-
- ngOnInit() {
- this.getHeroes();
- }
-
- onSelect(hero: Hero) { this.selectedHero = hero; }
-
- gotoDetail() {
- this._router.navigate(['HeroDetail', { id: this.selectedHero.id }]);
- }
-// #docregion heroes-component-renaming
-}
-// #enddocregion heroes-component-renaming
-// #enddocregion class
-// #enddocregion
diff --git a/public/docs/_examples/testing/ts/app/main.ts b/public/docs/_examples/testing/ts/app/main.ts
index 5f185788a3..2c89d35a81 100644
--- a/public/docs/_examples/testing/ts/app/main.ts
+++ b/public/docs/_examples/testing/ts/app/main.ts
@@ -1,5 +1,5 @@
-import { bootstrap } from '@angular/platform-browser-dynamic';
-import { AppComponent } from './app.component';
-
-bootstrap(AppComponent);
+// main app entry point
+import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
+import { AppModule } from './app.module';
+platformBrowserDynamic().bootstrapModule(AppModule);
diff --git a/public/docs/_examples/testing/ts/app/mock-hero.service.ts b/public/docs/_examples/testing/ts/app/mock-hero.service.ts
deleted file mode 100644
index b1538be366..0000000000
--- a/public/docs/_examples/testing/ts/app/mock-hero.service.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { HEROES } from './mock-heroes';
-import { Hero } from './hero';
-import { HeroService } from './hero.service';
-
-export { Hero } from './hero';
-export { HeroService } from './hero.service';
-
-export class MockHeroService implements HeroService {
-
- mockHeroes = HEROES.slice();
- lastPromise: Promise; // so we can spy on promise calls
-
- getHero(id: number) {
- return this.lastPromise = Promise.resolve(this.mockHeroes[0]);
- }
-
- getHeroes() {
- return this.lastPromise = Promise.resolve(this.mockHeroes);
- }
-
- getHeroesSlowly() { return this.getHeroes(); }
-}
diff --git a/public/docs/_examples/testing/ts/app/mock-heroes.ts b/public/docs/_examples/testing/ts/app/mock-heroes.ts
deleted file mode 100644
index ddd36d7868..0000000000
--- a/public/docs/_examples/testing/ts/app/mock-heroes.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-// #docregion
-import { Hero } from './hero';
-
-export var HEROES: Hero[] = [
- {id: 11, name: 'Mr. Nice'},
- {id: 12, name: 'Narco'},
- {id: 13, name: 'Bombasto'},
- {id: 14, name: 'Celeritas'},
- {id: 15, name: 'Magneta'},
- {id: 16, name: 'RubberMan'},
- {id: 17, name: 'Dynama'},
- {id: 18, name: 'Dr IQ'},
- {id: 19, name: 'Magma'},
- {id: 20, name: 'Tornado'}
-];
-// #enddocregion
diff --git a/public/docs/_examples/testing/ts/app/mock-router.ts b/public/docs/_examples/testing/ts/app/mock-router.ts
deleted file mode 100644
index a49763f7cf..0000000000
--- a/public/docs/_examples/testing/ts/app/mock-router.ts
+++ /dev/null
@@ -1,217 +0,0 @@
-/* tslint:disable */
-export * from '@angular/router-deprecated';
-
-import { Directive, DynamicComponentLoader, ViewContainerRef,
- Injectable, Optional, Input } from '@angular/core';
-
-import { ComponentInstruction, Instruction,
- Router, RouterOutlet} from '@angular/router-deprecated';
-
-let _resolveToTrue = Promise.resolve(true);
-
-const NOT_IMPLEMENTED = (what: string) => {
- throw new Error (`"${what}" is not implemented`);
-};
-
-
-@Directive({
- selector: '[routerLink]',
- host: {
- '(click)': 'onClick()',
- '[attr.href]': 'visibleHref',
- '[class.router-link-active]': 'isRouteActive'
- }
-})
-export class MockRouterLink {
-
- isRouteActive = false;
- visibleHref: string; // the url displayed on the anchor element.
-
- @Input('routerLink') routeParams: any[];
- @Input() target: string;
- navigatedTo: any[] = null;
-
- constructor(public router: Router) { }
-
- onClick() {
- this.navigatedTo = null;
-
- // If no target, or if target is _self, prevent default browser behavior
- if (!this.target || typeof this.target !== 'string' || this.target === '_self') {
- this.navigatedTo = this.routeParams;
- return false;
- }
- return true;
- }
-}
-
-@Directive({selector: 'router-outlet'})
-export class MockRouterOutlet extends RouterOutlet {
- name: string = null;
-
- constructor(
- _viewContainerRef: ViewContainerRef,
- @Optional() _loader: DynamicComponentLoader,
- _parentRouter: Router,
- nameAttr: string) {
- super(_viewContainerRef, _loader, _parentRouter, nameAttr);
- if (nameAttr) {
- this.name = nameAttr;
- }
- }
-
- /**
- * Called by the Router to instantiate a new component during the commit phase of a navigation.
- * This method in turn is responsible for calling the `routerOnActivate` hook of its child.
- */
- activate(nextInstruction: ComponentInstruction): Promise { NOT_IMPLEMENTED('activate'); return _resolveToTrue; }
-
- /**
- * Called by the {@link Router} during the commit phase of a navigation when an outlet
- * reuses a component between different routes.
- * This method in turn is responsible for calling the `routerOnReuse` hook of its child.
- */
- reuse(nextInstruction: ComponentInstruction): Promise { NOT_IMPLEMENTED('reuse'); return _resolveToTrue; }
-
- /**
- * Called by the {@link Router} when an outlet disposes of a component's contents.
- * This method in turn is responsible for calling the `routerOnDeactivate` hook of its child.
- */
- deactivate(nextInstruction: ComponentInstruction): Promise { NOT_IMPLEMENTED('deactivate'); return _resolveToTrue; }
-
- /**
- * Called by the {@link Router} during recognition phase of a navigation.
- *
- * If this resolves to `false`, the given navigation is cancelled.
- *
- * This method delegates to the child component's `routerCanDeactivate` hook if it exists,
- * and otherwise resolves to true.
- */
- routerCanDeactivate(nextInstruction: ComponentInstruction): Promise {
- NOT_IMPLEMENTED('routerCanDeactivate'); return _resolveToTrue;
- }
-
- /**
- * Called by the {@link Router} during recognition phase of a navigation.
- *
- * If the new child component has a different Type than the existing child component,
- * this will resolve to `false`. You can't reuse an old component when the new component
- * is of a different Type.
- *
- * Otherwise, this method delegates to the child component's `routerCanReuse` hook if it exists,
- * or resolves to true if the hook is not present.
- */
- routerCanReuse(nextInstruction: ComponentInstruction): Promise { NOT_IMPLEMENTED('routerCanReuse'); return _resolveToTrue; }
-
-}
-
-@Injectable()
-export class MockRouter extends Router {
-
- mockIsRouteActive = false;
- mockRecognizedInstruction: Instruction;
- outlet: RouterOutlet = null;
-
- constructor() {
- super(null, null, null, null);
- }
-
- auxRouter(hostComponent: any): Router { return new MockChildRouter(this, hostComponent); }
- childRouter(hostComponent: any): Router { return new MockChildRouter(this, hostComponent); }
-
- commit(instruction: Instruction, _skipLocationChange = false): Promise {
- NOT_IMPLEMENTED('commit'); return _resolveToTrue;
- }
-
- deactivate(instruction: Instruction, _skipLocationChange = false): Promise {
- NOT_IMPLEMENTED('deactivate'); return _resolveToTrue;
- }
-
- /**
- * Generate an `Instruction` based on the provided Route Link DSL.
- */
- generate(linkParams: any[]): Instruction {
- NOT_IMPLEMENTED('generate'); return null;
- }
-
- isRouteActive(instruction: Instruction): boolean { return this.mockIsRouteActive; }
-
- /**
- * Navigate based on the provided Route Link DSL. It's preferred to navigate with this method
- * over `navigateByUrl`.
- *
- * ### Usage
- *
- * This method takes an array representing the Route Link DSL:
- * ```
- * ['./MyCmp', {param: 3}]
- * ```
- * See the {@link RouterLink} directive for more.
- */
- navigate(linkParams: any[]): Promise {
- return Promise.resolve(linkParams);
- }
-
- /**
- * Navigate to a URL. Returns a promise that resolves when navigation is complete.
- * It's preferred to navigate with `navigate` instead of this method, since URLs are more brittle.
- *
- * If the given URL begins with a `/`, router will navigate absolutely.
- * If the given URL does not begin with `/`, the router will navigate relative to this component.
- */
- navigateByUrl(url: string, _skipLocationChange = false): Promise {
- return Promise.resolve(url);
- }
-
-
- /**
- * Navigate via the provided instruction. Returns a promise that resolves when navigation is
- * complete.
- */
- navigateByInstruction(instruction: Instruction, _skipLocationChange = false): Promise {
- return Promise.resolve(instruction);
- }
-
- /**
- * Subscribe to URL updates from the router
- */
- subscribe(onNext: (v: any) => void, onError?: (v: any) => void) {
- return {onNext, onError};
- }
-
- /**
- * Given a URL, returns an instruction representing the component graph
- */
- recognize(url: string): Promise {
- return Promise.resolve(this.mockRecognizedInstruction);
- }
-
- registerPrimaryOutlet(outlet: RouterOutlet): Promise {
- this.outlet = outlet;
- return super.registerPrimaryOutlet(outlet);
- }
-
- unregisterPrimaryOutlet(outlet: RouterOutlet) {
- super.unregisterPrimaryOutlet(outlet);
- this.outlet = null;
- }
-}
-
-class MockChildRouter extends MockRouter {
- constructor(parent: MockRouter, hostComponent: any) {
- super();
- this.parent = parent;
- }
-
-
- navigateByUrl(url: string, _skipLocationChange = false): Promise {
- // Delegate navigation to the root router
- return this.parent.navigateByUrl(url, _skipLocationChange);
- }
-
- navigateByInstruction(instruction: Instruction, _skipLocationChange = false):
- Promise {
- // Delegate navigation to the root router
- return this.parent.navigateByInstruction(instruction, _skipLocationChange);
- }
-}
diff --git a/public/docs/_examples/testing/ts/app/model/hero.service.ts b/public/docs/_examples/testing/ts/app/model/hero.service.ts
new file mode 100644
index 0000000000..7f2931a7f6
--- /dev/null
+++ b/public/docs/_examples/testing/ts/app/model/hero.service.ts
@@ -0,0 +1,29 @@
+import { Injectable } from '@angular/core';
+
+import { Hero } from './hero';
+import { HEROES } from './test-heroes';
+
+@Injectable()
+/** Dummy HeroService that pretends to be real */
+export class HeroService {
+ getHeroes() {
+ return Promise.resolve(HEROES);
+ }
+
+ getHero(id: number | string): Promise {
+ 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 {
+ return this.getHero(hero.id).then(h => {
+ return h ?
+ Object.assign(h, hero) :
+ Promise.reject(`Hero ${hero.id} not found`) as any as Promise;
+ });
+ }
+}
diff --git a/public/docs/_examples/testing/ts/app/model/hero.spec.ts b/public/docs/_examples/testing/ts/app/model/hero.spec.ts
new file mode 100644
index 0000000000..e8acf913f2
--- /dev/null
+++ b/public/docs/_examples/testing/ts/app/model/hero.spec.ts
@@ -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);
+ });
+});
diff --git a/public/docs/_examples/testing/ts/app/model/hero.ts b/public/docs/_examples/testing/ts/app/model/hero.ts
new file mode 100644
index 0000000000..6a98f0dfdc
--- /dev/null
+++ b/public/docs/_examples/testing/ts/app/model/hero.ts
@@ -0,0 +1,4 @@
+export class Hero {
+ constructor(public id = 0, public name = '') { }
+ clone() { return new Hero(this.id, this.name); }
+}
diff --git a/public/docs/_examples/testing/ts/app/http-hero.service.spec.ts b/public/docs/_examples/testing/ts/app/model/http-hero.service.spec.ts
similarity index 67%
rename from public/docs/_examples/testing/ts/app/http-hero.service.spec.ts
rename to public/docs/_examples/testing/ts/app/model/http-hero.service.spec.ts
index 375efde560..c16b421274 100644
--- a/public/docs/_examples/testing/ts/app/http-hero.service.spec.ts
+++ b/public/docs/_examples/testing/ts/app/model/http-hero.service.spec.ts
@@ -1,59 +1,54 @@
-/* tslint:disable:no-unused-variable */
import {
- addProviders,
- async, inject, withProviders
+ async, inject, TestBed
} from '@angular/core/testing';
-import { TestComponentBuilder } from '@angular/core/testing';
-
import {
MockBackend,
- MockConnection } from '@angular/http/testing';
+ MockConnection
+} from '@angular/http/testing';
import {
- Http, HTTP_PROVIDERS,
- ConnectionBackend, XHRBackend,
- Request, RequestMethod, BaseRequestOptions, RequestOptions,
- Response, ResponseOptions,
- URLSearchParams
+ HttpModule, Http, XHRBackend, Response, ResponseOptions
} from '@angular/http';
-// Add all operators to Observable
-import 'rxjs/Rx';
import { Observable } from 'rxjs/Observable';
+import 'rxjs/add/observable/of';
-import { Hero } from './hero';
-import { HeroService } from './http-hero.service';
+import 'rxjs/add/operator/catch';
+import 'rxjs/add/operator/do';
+import 'rxjs/add/operator/toPromise';
-type HeroData = {id: string, name: string}
+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' }
-];
+ { id: 1, name: 'Windstorm' },
+ { id: 2, name: 'Bombasto' },
+ { id: 3, name: 'Magneta' },
+ { id: 4, name: 'Tornado' }
+] as Hero[];
-// HeroService expects response data like {data: {the-data}}
-const makeResponseData = (data: {}) => {return { data }; };
-
-//////// SPECS /////////////
+//////// Tests /////////////
describe('Http-HeroService (mockBackend)', () => {
- beforeEach(() => {
- addProviders([
- HTTP_PROVIDERS,
- { provide: XHRBackend, useClass: MockBackend }
- ]);
- });
+ beforeEach( async(() => {
+ TestBed.configureTestingModule({
+ imports: [ HttpModule ],
+ providers: [
+ HeroService,
+ { provide: XHRBackend, useClass: MockBackend }
+ ]
+ })
+ .compileComponents();
+ }));
it('can instantiate service when inject service',
- withProviders(() => [HeroService])
- .inject([HeroService], (service: HeroService) => {
- expect(service instanceof HeroService).toBe(true);
+ 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);
@@ -69,10 +64,9 @@ describe('Http-HeroService (mockBackend)', () => {
describe('when getHeroes', () => {
let backend: MockBackend;
let service: HeroService;
- let fakeHeroes: HeroData[];
+ let fakeHeroes: Hero[];
let response: Response;
-
beforeEach(inject([Http, XHRBackend], (http: Http, be: MockBackend) => {
backend = be;
service = new HeroService(http);
@@ -87,7 +81,7 @@ describe('Http-HeroService (mockBackend)', () => {
service.getHeroes().toPromise()
// .then(() => Promise.reject('deliberate'))
.then(heroes => {
- expect(heroes.length).toEqual(fakeHeroes.length,
+ expect(heroes.length).toBe(fakeHeroes.length,
'should have expected no. of heroes');
});
})));
@@ -97,7 +91,7 @@ describe('Http-HeroService (mockBackend)', () => {
service.getHeroes()
.do(heroes => {
- expect(heroes.length).toEqual(fakeHeroes.length,
+ expect(heroes.length).toBe(fakeHeroes.length,
'should have expected no. of heroes');
})
.toPromise();
@@ -110,7 +104,7 @@ describe('Http-HeroService (mockBackend)', () => {
service.getHeroes()
.do(heroes => {
- expect(heroes.length).toEqual(0, 'should have no heroes');
+ expect(heroes.length).toBe(0, 'should have no heroes');
})
.toPromise();
})));
diff --git a/public/docs/_examples/testing/ts/app/http-hero.service.ts b/public/docs/_examples/testing/ts/app/model/http-hero.service.ts
similarity index 68%
rename from public/docs/_examples/testing/ts/app/http-hero.service.ts
rename to public/docs/_examples/testing/ts/app/model/http-hero.service.ts
index bfde5bfdc8..a5fe46b801 100644
--- a/public/docs/_examples/testing/ts/app/http-hero.service.ts
+++ b/public/docs/_examples/testing/ts/app/model/http-hero.service.ts
@@ -4,10 +4,16 @@ 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 HeroService {
+export class HttpHeroService {
private _heroesUrl = 'app/heroes'; // URL to web api
constructor (private http: Http) {}
@@ -19,6 +25,12 @@ export class HeroService {
.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 {
let body = JSON.stringify({ name });
let headers = new Headers({ 'Content-Type': 'application/json' });
@@ -29,6 +41,16 @@ export class HeroService {
.catch(this.handleError);
}
+ updateHero (hero: Hero): Observable {
+ 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);
diff --git a/public/docs/_examples/testing/ts/app/model/index.ts b/public/docs/_examples/testing/ts/app/model/index.ts
new file mode 100644
index 0000000000..227004d5be
--- /dev/null
+++ b/public/docs/_examples/testing/ts/app/model/index.ts
@@ -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';
diff --git a/public/docs/_examples/testing/ts/app/model/test-heroes.ts b/public/docs/_examples/testing/ts/app/model/test-heroes.ts
new file mode 100644
index 0000000000..d40ce5d564
--- /dev/null
+++ b/public/docs/_examples/testing/ts/app/model/test-heroes.ts
@@ -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')
+];
diff --git a/public/docs/_examples/testing/ts/app/model/testing/fake-hero.service.ts b/public/docs/_examples/testing/ts/app/model/testing/fake-hero.service.ts
new file mode 100644
index 0000000000..79a865cc44
--- /dev/null
+++ b/public/docs/_examples/testing/ts/app/model/testing/fake-hero.service.ts
@@ -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; // 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(this.heroes);
+ }
+
+ updateHero(hero: Hero): Promise {
+ 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;
+ });
+ }
+}
diff --git a/public/docs/_examples/testing/ts/app/model/testing/index.ts b/public/docs/_examples/testing/ts/app/model/testing/index.ts
new file mode 100644
index 0000000000..6da76e67db
--- /dev/null
+++ b/public/docs/_examples/testing/ts/app/model/testing/index.ts
@@ -0,0 +1 @@
+export * from './fake-hero.service';
diff --git a/public/docs/_examples/testing/ts/app/model/user.service.ts b/public/docs/_examples/testing/ts/app/model/user.service.ts
new file mode 100644
index 0000000000..280efefeec
--- /dev/null
+++ b/public/docs/_examples/testing/ts/app/model/user.service.ts
@@ -0,0 +1,7 @@
+import { Injectable } from '@angular/core';
+
+@Injectable()
+export class UserService {
+ isLoggedIn = true;
+ user = {name: 'Sam Spade'};
+}
diff --git a/public/docs/_examples/testing/ts/app/my-uppercase.pipe.1.ts b/public/docs/_examples/testing/ts/app/my-uppercase.pipe.1.ts
deleted file mode 100644
index 94b5bc45ce..0000000000
--- a/public/docs/_examples/testing/ts/app/my-uppercase.pipe.1.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-// #docregion
-import { Pipe, PipeTransform } from '@angular/core';
-
-@Pipe({ name: 'my-uppercase' })
-export class MyUppercasePipe implements PipeTransform {
- transform(value: string) {
- return value;
- }
-}
diff --git a/public/docs/_examples/testing/ts/app/my-uppercase.pipe.spec.ts b/public/docs/_examples/testing/ts/app/my-uppercase.pipe.spec.ts
deleted file mode 100644
index 731b2ed965..0000000000
--- a/public/docs/_examples/testing/ts/app/my-uppercase.pipe.spec.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-// #docregion
-// #docplaster
-// #docregion base-pipe-spec
-import { MyUppercasePipe } from './my-uppercase.pipe';
-
-describe('MyUppercasePipe', () => {
- let pipe: MyUppercasePipe;
-
- beforeEach(() => {
- pipe = new MyUppercasePipe();
- });
-
- // #docregion expectations
- it('transforms "abc" to "ABC"', () => {
- expect(pipe.transform('abc')).toEqual('ABC');
- });
-
- it('transforms "abc def" to "ABC DEF"', () => {
- expect(pipe.transform('abc def')).toEqual('ABC DEF');
- });
-
- it('leaves "ABC DEF" unchanged', () => {
- expect(pipe.transform('ABC DEF')).toEqual('ABC DEF');
- });
- // #enddocregion expectations
- // #enddocregion base-pipe-spec
-
- /* more tests we could run
-
- it('transforms "abc-def" to "Abc-def"', () => {
- expect(pipe.transform('abc-def')).toEqual('Abc-def');
- });
-
- it('transforms " abc def" to " Abc Def" (preserves spaces) ', () => {
- expect(pipe.transform(' abc def')).toEqual(' Abc Def');
- });
-
- */
- // #docregion base-pipe-spec
-});
-// #enddocregion base-pipe-spec
diff --git a/public/docs/_examples/testing/ts/app/my-uppercase.pipe.ts b/public/docs/_examples/testing/ts/app/my-uppercase.pipe.ts
deleted file mode 100644
index 6584f92ef6..0000000000
--- a/public/docs/_examples/testing/ts/app/my-uppercase.pipe.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-// #docregion
-// #docregion depends-on-angular
-import { Pipe, PipeTransform } from '@angular/core';
-// #enddocregion depends-on-angular
-
-@Pipe({ name: 'my-uppercase' })
-export class MyUppercasePipe implements PipeTransform {
- // #docregion uppercase
- transform(value: string) {
- return value.toUpperCase();
- }
- // #enddocregion uppercase
-}
diff --git a/public/docs/_examples/testing/ts/app/old-specs/hero-detail.component.spec.ts.not-yet b/public/docs/_examples/testing/ts/app/old-specs/hero-detail.component.spec.ts.not-yet
deleted file mode 100644
index 80c210be5d..0000000000
--- a/public/docs/_examples/testing/ts/app/old-specs/hero-detail.component.spec.ts.not-yet
+++ /dev/null
@@ -1,218 +0,0 @@
-///// Boiler Plate ////
-import {bind, By, Component, Directive, EventEmitter, FORM_DIRECTIVES} from 'angular2/angular2';
-
-// Angular 2 Test Bed
-import {
-beforeEachProviders, inject, injectAsync, RootTestComponent as RTC,
-beforeEach, ddescribe, xdescribe, describe, expect, iit, it, xit // Jasmine wrappers
-} from 'angular2/testing';
-
-import {dispatchEvent, DoneFn, injectTcb, tick} from '../test-helpers/test-helpers';
-
-///// Testing this component ////
-import {HeroDetailComponent} from './hero-detail.component';
-import {Hero} from './hero';
-
-describe('HeroDetailComponent', () => {
-
- /////////// Component Tests without DOM interaction /////////////
- describe('(No DOM)', () => {
- it('can be created', () => {
- let hdc = new HeroDetailComponent();
- expect(hdc instanceof HeroDetailComponent).toEqual(true); // proof of life
- });
-
- it('onDelete method should raise delete event', (done: DoneFn) => {
- let hdc = new HeroDetailComponent();
-
- // Listen for the HeroComponent.delete EventEmitter's event
- hdc.delete.toRx().subscribe(() => {
- console.log('HeroComponent.delete event raised');
- done(); // it must have worked
- }, (error: any) => { fail(error); done() });
-
- hdc.onDelete();
- });
-
- // Disable until toPromise() works again
- xit('onDelete method should raise delete event (w/ promise)', (done: DoneFn) => {
-
- let hdc = new HeroDetailComponent();
-
- // Listen for the HeroComponent.delete EventEmitter's event
- let p = hdc.delete.toRx()
- .toPromise()
- .then(() => {
- console.log('HeroComponent.delete event raised in promise');
- })
- .then(done, done.fail);
-
- hdc.delete.toRx()
- .subscribe(() => {
- console.log('HeroComponent.delete event raised in subscription')
- });
-
- hdc.onDelete();
-
- // toPromise() does not fulfill until emitter is completed by `return()`
- hdc.delete.return();
- });
-
- it('onUpdate method should modify hero', () => {
- let hdc = new HeroDetailComponent();
- hdc.hero = new Hero(42, 'Cat Woman');
- let origNameLength = hdc.hero.name.length;
-
- hdc.onUpdate();
- expect(hdc.hero.name.length).toBeGreaterThan(origNameLength);
- });
- });
-
-
- /////////// Component tests that check the DOM /////////////
- describe('(DOM)', () => {
- // Disable until toPromise() works again
- xit('Delete button should raise delete event', injectTcb(tcb => {
-
- // We only care about the button
- let template = '';
-
- return tcb
- .overrideTemplate(HeroDetailComponent, template)
- .createAsync(HeroDetailComponent)
- .then((rootTC: RTC) => {
- let hdc: HeroDetailComponent = rootTC.debugElement.componentInstance;
-
- // // USE PROMISE WRAPPING AN OBSERVABLE UNTIL can get `toPromise` working again
- // let p = new Promise((resolve) => {
- // // Listen for the HeroComponent.delete EventEmitter's event with observable
- // hdc.delete.toRx().subscribe((hero: Hero) => {
- // console.log('Observable heard HeroComponent.delete event raised');
- // resolve(hero);
- // });
- // })
-
- //Listen for the HeroComponent.delete EventEmitter's event with promise
- let p = > hdc.delete.toRx().toPromise()
- .then((hero:Hero) => {
- console.log('Promise heard HeroComponent.delete event raised');
- });
-
- // trigger the 'click' event on the HeroDetailComponent delete button
- let el = rootTC.debugElement.query(By.css('button'));
- el.triggerEventHandler('click', null);
-
- // toPromise() does not fulfill until emitter is completed by `return()`
- hdc.delete.return();
-
- return p;
- });
-
- }));
-
- it('Update button should modify hero', injectTcb(tcb => {
-
- let template =
- `
-
-
-
`
-
- return tcb
- .overrideTemplate(HeroDetailComponent, template)
- .createAsync(HeroDetailComponent)
- .then((rootTC: RTC) => {
-
- let hdc: HeroDetailComponent = rootTC.debugElement.componentInstance;
- hdc.hero = new Hero(42, 'Cat Woman');
- let origNameLength = hdc.hero.name.length;
-
- // trigger the 'click' event on the HeroDetailComponent update button
- rootTC.debugElement.query(By.css('#update'))
- .triggerEventHandler('click', null);
-
- expect(hdc.hero.name.length).toBeGreaterThan(origNameLength);
- });
- }));
-
- it('Entering hero name in textbox changes hero', injectTcb(tcb => {
-
- let hdc: HeroDetailComponent
- let template = ``
-
- return tcb
- .overrideTemplate(HeroDetailComponent, template)
- .createAsync(HeroDetailComponent)
- .then((rootTC: RTC) => {
-
- hdc = rootTC.debugElement.componentInstance;
-
- hdc.hero = new Hero(42, 'Cat Woman');
- rootTC.detectChanges();
-
- // get the HTML element and change its value in the DOM
- var input = rootTC.debugElement.query(By.css('input')).nativeElement;
- input.value = "Dog Man"
- dispatchEvent(input, 'change'); // event triggers Ng to update model
-
- rootTC.detectChanges();
- // model update hasn't happened yet, despite `detectChanges`
- expect(hdc.hero.name).toEqual('Cat Woman');
-
- })
- .then(tick) // must wait a tick for the model update
- .then(() => {
- expect(hdc.hero.name).toEqual('Dog Man');
- });
- }));
-
- // Simulates ...
- // 1. change a hero
- // 2. select a different hero
- // 3 re-select the first hero
- // 4. confirm that the change is preserved in HTML
- // Reveals 2-way binding bug in alpha-36, fixed in pull #3715 for alpha-37
-
- it('toggling heroes after modifying name preserves the change on screen', injectTcb(tcb => {
-
- let hdc: HeroDetailComponent;
- let hero1 = new Hero(1, 'Cat Woman');
- let hero2 = new Hero(2, 'Goat Boy');
- let input: HTMLInputElement;
- let rootTC: RTC;
- let template = `{{hero.id}} - `
-
- return tcb
- .overrideTemplate(HeroDetailComponent, template)
- .createAsync(HeroDetailComponent)
- .then((rtc: RTC) => {
- rootTC = rtc;
- hdc = rootTC.debugElement.componentInstance;
-
- hdc.hero = hero1; // start with hero1
- rootTC.detectChanges();
-
- // get the HTML element and change its value in the DOM
- input = rootTC.debugElement.query(By.css('input')).nativeElement;
- input.value = "Dog Man"
- dispatchEvent(input, 'change'); // event triggers Ng to update model
- })
- .then(tick) // must wait a tick for the model update
- .then(() => {
- expect(hdc.hero.name).toEqual('Dog Man');
-
- hdc.hero = hero2 // switch to hero2
- rootTC.detectChanges();
-
- hdc.hero = hero1 // switch back to hero1
- rootTC.detectChanges();
-
- // model value will be the same changed value (of course)
- expect(hdc.hero.name).toEqual('Dog Man');
-
- // the view should reflect the same changed value
- expect(input.value).toEqual('Dog Man');
- });
- }));
- });
-});
diff --git a/public/docs/_examples/testing/ts/app/old-specs/hero-detail.component.wrapped-tests.spec.ts.not-yet b/public/docs/_examples/testing/ts/app/old-specs/hero-detail.component.wrapped-tests.spec.ts.not-yet
deleted file mode 100644
index 319ac93ebf..0000000000
--- a/public/docs/_examples/testing/ts/app/old-specs/hero-detail.component.wrapped-tests.spec.ts.not-yet
+++ /dev/null
@@ -1,144 +0,0 @@
-///// Boiler Plate ////
-import {bind, Component, Directive, EventEmitter, FORM_DIRECTIVES, View} from 'angular2/angular2';
-
-// Angular 2 Test Bed
-import {
- beforeEachProviders, By, DebugElement, RootTestComponent as RTC,
- beforeEach, ddescribe, xdescribe, describe, expect, iit, it, xit // Jasmine wrappers
-} from 'angular2/testing';
-
-import {injectAsync, injectTcb} from '../test-helpers/test-helpers';
-
-///// Testing this component ////
-import {HeroDetailComponent} from './hero-detail.component';
-import {Hero} from './hero';
-
-describe('HeroDetailComponent', () => {
-
- it('can be created', () => {
- let hc = new HeroDetailComponent()
- expect(hc instanceof HeroDetailComponent).toEqual(true); // proof of life
- });
-
- it('parent "currentHero" flows down to HeroDetailComponent', injectTcb( tcb => {
- return tcb
- .createAsync(TestWrapper)
- .then((rootTC:RTC) => {
- let hc:HeroDetailComponent = rootTC.componentViewChildren[0].componentInstance;
- let hw:TestWrapper = rootTC.componentInstance;
-
- rootTC.detectChanges(); // trigger view binding
-
- expect(hw.currentHero).toBe(hc.hero);
- });
- }));
-
- it('delete button should raise delete event for parent component', injectTcb( tcb => {
-
- return tcb
- //.overrideTemplate(HeroDetailComponent, '')
- .overrideDirective(TestWrapper, HeroDetailComponent, mockHDC)
- .createAsync(TestWrapper)
- .then((rootTC:RTC) => {
-
- let hw:TestWrapper = rootTC.componentInstance;
- let hdcElement = rootTC.componentViewChildren[0];
- let hdc:HeroDetailComponent = hdcElement.componentInstance;
-
- rootTC.detectChanges(); // trigger view binding
-
- // We can watch the HeroComponent.delete EventEmitter's event
- let subscription = hdc.delete.toRx().subscribe(() => {
- console.log('HeroComponent.delete event raised');
- subscription.dispose();
- });
-
- // We can EITHER invoke HeroComponent delete button handler OR
- // trigger the 'click' event on the delete HeroComponent button
- // BUT DON'T DO BOTH
-
- // Trigger event
- // FRAGILE because assumes precise knowledge of HeroComponent template
- hdcElement
- .query(By.css('#delete'))
- .triggerEventHandler('click', {});
-
- hw.testCallback = () => {
- // if wrapper.onDelete is called, HeroComponent.delete event must have been raised
- //console.log('HeroWrapper.onDelete called');
- expect(true).toEqual(true);
- }
- // hc.onDelete();
- });
- }), 500); // needs some time for event to complete; 100ms is not long enough
-
- it('update button should modify hero', injectTcb( tcb => {
-
- return tcb
- .createAsync(TestWrapper)
- .then((rootTC:RTC) => {
-
- let hc:HeroDetailComponent = rootTC.componentViewChildren[0].componentInstance;
- let hw:TestWrapper = rootTC.componentInstance;
- let origNameLength = hw.currentHero.name.length;
-
- rootTC.detectChanges(); // trigger view binding
-
- // We can EITHER invoke HeroComponent update button handler OR
- // trigger the 'click' event on the HeroComponent update button
- // BUT DON'T DO BOTH
-
- // Trigger event
- // FRAGILE because assumes precise knowledge of HeroComponent template
- rootTC.componentViewChildren[0]
- .componentViewChildren[2]
- .triggerEventHandler('click', {});
-
- // hc.onUpdate(); // Invoke button handler
- expect(hw.currentHero.name.length).toBeGreaterThan(origNameLength);
- });
- }));
-
-});
-
-///// Test Components ////////
-
-// TestWrapper is a convenient way to communicate w/ HeroDetailComponent in a test
-@Component({selector: 'hero-wrapper'})
-@View({
- template: ``,
- directives: [HeroDetailComponent]
-})
-class TestWrapper {
- currentHero = new Hero(42, 'Cat Woman');
- userName = 'Sally';
- testCallback() {} // monkey-punched in a test
- onDelete() { this.testCallback(); }
-}
-
-@View({
- template: `
-
-
{{hero.name}} | {{userName}}
-
-
-
{{hero.id}}
-
-
`,
- directives: [FORM_DIRECTIVES]
-})
-class mockHDC //extends HeroDetailComponent { }
-{
- hero: Hero;
-
- delete = new EventEmitter();
-
- onDelete() { this.delete.next(this.hero) }
-
- onUpdate() {
- if (this.hero) {
- this.hero.name += 'x';
- }
- }
- userName: string;
-}
\ No newline at end of file
diff --git a/public/docs/_examples/testing/ts/app/old-specs/hero.service.ng.spec.ts.not-yet b/public/docs/_examples/testing/ts/app/old-specs/hero.service.ng.spec.ts.not-yet
deleted file mode 100644
index a3e4c0c6a4..0000000000
--- a/public/docs/_examples/testing/ts/app/old-specs/hero.service.ng.spec.ts.not-yet
+++ /dev/null
@@ -1,198 +0,0 @@
-// Test a service when Angular DI is in play
-
-// Angular 2 Test Bed
-import {
- beforeEach, xdescribe, describe, it, xit, // Jasmine wrappers
- beforeEachProviders, inject, injectAsync,
-} from 'angular2/testing';
-
-import {bind} from 'angular2/core';
-
-// Service related imports
-import {HeroService} from './hero.service';
-import {BackendService} from './backend.service';
-import {Hero} from './hero';
-
-////// tests ////////////
-
-describe('HeroService (with angular DI)', () => {
-
- beforeEachProviders(() => [HeroService]);
-
- describe('creation', () => {
-
- beforeEachProviders( () => [bind(BackendService).toValue(null)] );
-
- it('can instantiate the service',
- inject([HeroService], (service: HeroService) => {
- expect(service).toBeDefined();
- }));
-
- it('service.heroes is empty',
- inject([HeroService], (service: HeroService) => {
- expect(service.heroes.length).toEqual(0);
- }));
- });
-
- describe('#refresh', () => {
-
- describe('when backend provides data', () => {
-
- beforeEach(() => {
- heroData = [new Hero(1, 'Foo'), new Hero(2, 'Bar'), new Hero(3,'Baz')];
- });
-
- beforeEachProviders(() =>
- [bind(BackendService).toClass(HappyBackendService)]
- );
-
- it('refresh promise returns expected # of heroes when fulfilled',
- injectAsync([HeroService], (service: HeroService) => {
-
- return service.refresh().then(heroes =>
- expect(heroes.length).toEqual(heroData.length)
- );
- }));
-
- it('service.heroes has expected # of heroes when fulfilled',
- injectAsync([HeroService], (service: HeroService) => {
-
- return service.refresh().then(() =>
- expect(service.heroes.length).toEqual(heroData.length)
- );
- }));
-
- it('service.heroes remains empty until fulfilled',
- inject([HeroService], (service: HeroService) => {
-
- service.refresh();
-
- // executed before refresh completes
- expect(service.heroes.length).toEqual(0);
- }));
-
- it('service.heroes remains empty when the server returns no data',
- injectAsync([HeroService], (service: HeroService) => {
-
- heroData = []; // simulate no heroes from the backend
-
- return service.refresh().then(() =>
- expect(service.heroes.length).toEqual(0)
- );
- }));
-
- it('resets service.heroes w/ original data after re-refresh',
- injectAsync([HeroService], (service: HeroService) => {
-
- let firstHeroes: Hero[];
- let changedName = 'Gerry Mander';
-
- return service.refresh().then(heroes => {
- firstHeroes = heroes; // remember array reference
-
- // Changes to cache! Should disappear after refresh
- service.heroes[0].name = changedName;
- service.heroes.push(new Hero(33, 'Hercules'));
- return service.refresh()
- })
- .then(() => {
- expect(firstHeroes).toBe(service.heroes); // same object
- expect(service.heroes.length).toEqual(heroData.length); // no Hercules
- expect(service.heroes[0].name).not.toEqual(changedName); // reverted name change
- });
- }));
-
- it('clears service.heroes while waiting for re-refresh',
- injectAsync([HeroService], (service: HeroService) => {
-
- return service.refresh().then(() => {
- service.refresh();
- expect(service.heroes.length).toEqual(0);
- });
- }));
- // the paranoid will verify not only that the array lengths are the same
- // but also that the contents are the same.
- it('service.heroes has expected heroes when fulfilled (paranoia)',
- injectAsync([HeroService], (service: HeroService) => {
-
- return service.refresh().then(() => {
- expect(service.heroes.length).toEqual(heroData.length);
- service.heroes.forEach(h =>
- expect(heroData.some(
- // hero instances are not the same objects but
- // each hero in result matches an original hero by value
- hd => hd.name === h.name && hd.id === h.id)
- )
- );
- });
- }));
-
- });
-
- describe('when backend throws an error', () => {
-
- beforeEachProviders(() =>
- [bind(BackendService).toClass(FailingBackendService)]
- );
-
- it('returns failed promise with the server error',
- injectAsync([HeroService], (service: HeroService) => {
-
- return service.refresh()
- .then(() => fail('refresh should have failed'))
- .catch(err => expect(err).toBe(testError));
- }));
-
- it('resets heroes array to empty',
- injectAsync([HeroService], (service: HeroService) => {
-
- return service.refresh()
- .then(() => fail('refresh should have failed'))
- .catch(err => expect(service.heroes.length).toEqual(0))
- }));
- });
-
- describe('when backend throws an error (spy version)', () => {
-
- beforeEachProviders(() => [BackendService]);
-
- beforeEach(inject([BackendService], (backend: BackendService) =>
- spyOn(backend, 'fetchAllHeroesAsync').and.callFake(() => Promise.reject(testError)
- )));
-
- it('returns failed promise with the server error',
- injectAsync([HeroService], (service: HeroService) => {
-
- return service.refresh()
- .then(() => fail('refresh should have failed'))
- .catch(err => expect(err).toBe(testError));
- }));
-
- it('resets heroes array to empty',
- injectAsync([HeroService], (service: HeroService) => {
-
- return service.refresh()
- .then(() => fail('refresh should have failed'))
- .catch(err => expect(service.heroes.length).toEqual(0))
- }));
- });
-
- });
-});
-///////// test helpers /////////
-var service: HeroService;
-var heroData: Hero[];
-
-class HappyBackendService {
- // return a promise for fake heroes that resolves as quickly as possible
- fetchAllHeroesAsync = () =>
- Promise.resolve(heroData.map(h => h.clone()));
-}
-
-var testError = 'BackendService.fetchAllHeroesAsync failed on purpose';
-
-class FailingBackendService {
- // return a promise that fails as quickly as possible
- fetchAllHeroesAsync = () =>
- Promise.reject(testError);
-}
diff --git a/public/docs/_examples/testing/ts/app/old-specs/hero.service.no-ng.1.spec.ts.not-yet b/public/docs/_examples/testing/ts/app/old-specs/hero.service.no-ng.1.spec.ts.not-yet
deleted file mode 100644
index 89cbc597f8..0000000000
--- a/public/docs/_examples/testing/ts/app/old-specs/hero.service.no-ng.1.spec.ts.not-yet
+++ /dev/null
@@ -1,250 +0,0 @@
-/*
- * Dev Guide steps to hero.service.no-ng.spec
- * Try it with unit-tests-4.html
- */
-
-// The phase of hero-service-spec
-// when we're outlining what we want to test
-describe('HeroService (test plan)', () => {
-
- describe('creation', () => {
- xit('can instantiate the service');
- xit('service.heroes is empty');
- });
-
- describe('#refresh', () => {
-
- describe('when server provides heroes', () => {
- xit('refresh promise returns expected # of heroes when fulfilled');
- xit('service.heroes has expected # of heroes when fulfilled');
- xit('service.heroes remains empty until fulfilled');
- xit('service.heroes remains empty when the server returns no data');
- xit('resets service.heroes w/ original data after re-refresh');
- xit('clears service.heroes while waiting for re-refresh');
- });
-
- describe('when the server fails', () => {
- xit('returns failed promise with the server error');
- xit('clears service.heroes');
- });
-
- });
-
-});
-
-import {HeroService} from './hero.service';
-
-describe('HeroService (beginning tests - 1)', () => {
-
- describe('creation', () => {
- it('can instantiate the service', () => {
- let service = new HeroService(null);
- expect(service).toBeDefined();
- });
-
- it('heroes is empty', () => {
- let service = new HeroService(null);
- expect(service.heroes.length).toEqual(0);
- });
-
- });
-
-});
-
-import {BackendService} from './backend.service';
-import {Hero} from './hero';
-
-xdescribe('HeroService (beginning tests - 2 [dont run])', () => {
- let heroData:Hero[];
-
- // No good!
- it('refresh promise returns expected # of heroes when fulfilled', () => {
- let service = new HeroService(null);
- service.refresh().then(heroes => {
- expect(heroes.length).toBeGreaterThan(0); // don’t know how many to expect yet
- });
- });
-
- // better ... but not async!
- it('refresh promise returns expected # of heroes when fulfilled', () => {
-
- heroData = [new Hero(1, 'Foo'), new Hero(2, 'Bar'), new Hero(3, 'Baz')];
-
- let backend = {
- // return a promise for fake heroes that resolves as quickly as possible
- fetchAllHeroesAsync: () => Promise.resolve(heroData)
- };
-
- let service = new HeroService(backend);
- service.refresh().then(heroes => {
- expect(heroes.length).toEqual(heroData.length); // is it?
- expect(heroes.length).not.toEqual(heroData.length); // or is it not?
- console.log('** inside callback **');
- });
-
- console.log('** end of test **');
- });
-
- // better ... but forgot to call done!
- it('refresh promise returns expected # of heroes when fulfilled', done => {
-
- heroData = [new Hero(1, 'Foo'), new Hero(2, 'Bar'), new Hero(3, 'Baz')];
-
- let backend = {
- // return a promise for fake heroes that resolves as quickly as possible
- fetchAllHeroesAsync: () => Promise.resolve(heroData)
- };
-
- let service = new HeroService(backend);
- service.refresh().then(heroes => {
- expect(heroes.length).toEqual(heroData.length); // is it?
- expect(heroes.length).not.toEqual(heroData.length); // or is it not?
- console.log('** inside callback **');
- });
-
- console.log('** end of test **');
- });
-});
-
-describe('HeroService (beginning tests - 3 [async])', () => {
-
- let heroData:Hero[];
- // Now it's proper async!
- it('refresh promise returns expected # of heroes when fulfilled', done => {
-
- heroData = [new Hero(1, 'Foo'), new Hero(2, 'Bar'), new Hero(3, 'Baz')];
-
- let backend = {
- // return a promise for fake heroes that resolves as quickly as possible
- fetchAllHeroesAsync: () => Promise.resolve(heroData)
- };
-
- let service = new HeroService(backend);
- service.refresh().then(heroes => {
- expect(heroes.length).toEqual(heroData.length); // is it?
- //expect(heroes.length).not.toEqual(heroData.length); // or is it not?
- console.log('** inside callback **');
- done();
- });
-
- console.log('** end of test **');
- });
-
- // Final before catch
- it('refresh promise returns expected # of heroes when fulfilled', done => {
-
- heroData = [new Hero(1, 'Foo'), new Hero(2, 'Bar'), new Hero(3, 'Baz')];
-
- let backend = {
- // return a promise for fake heroes that resolves as quickly as possible
- fetchAllHeroesAsync: () => Promise.resolve(heroData)
- };
-
- let service = new HeroService(backend);
- service.refresh().then(heroes => {
- expect(heroes.length).toEqual(heroData.length);
- })
- .then(done);
- });
-
- // Final before beforeEach refactoring
- it('refresh promise returns expected # of heroes when fulfilled', done => {
-
- heroData = [new Hero(1, 'Foo'), new Hero(2, 'Bar'), new Hero(3, 'Baz')];
-
- let backend = {
- // return a promise for fake heroes that resolves as quickly as possible
- fetchAllHeroesAsync: () => Promise.resolve(heroData)
- };
-
- let service = new HeroService(backend);
- service.refresh().then(heroes => {
- expect(heroes.length).toEqual(heroData.length);
- })
- .then(done, done.fail);
- });
-
- it('service.heroes remains empty until fulfilled', () => {
- heroData = [new Hero(1, 'Foo'), new Hero(2, 'Bar'), new Hero(3, 'Baz')];
-
- let backend = {
- // return a promise for fake heroes that resolves as quickly as possible
- fetchAllHeroesAsync: () => Promise.resolve(heroData)
- };
-
- let service = new HeroService(backend);
- service.refresh();
-
- // executed before refresh completes
- expect(service.heroes.length).toEqual(0);
- });
-});
-
-
-describe('HeroService (beginning tests - 4 [beforeEach])', () => {
- let heroData:Hero[];
- let service:HeroService; // local to describe so tests can see it
-
- // before beforEach refactoring
- beforeEach(() => {
- heroData = [new Hero(1, 'Foo'), new Hero(2, 'Bar'), new Hero(3,'Baz')];
-
- let backend = {
- // return a promise for fake heroes that resolves as quickly as possible
- fetchAllHeroesAsync: () => Promise.resolve(heroData)
- };
-
- service = new HeroService(backend);
- });
-
- it('refresh promise returns expected # of heroes when fulfilled', done => {
- service.refresh().then(heroes =>
- expect(heroes.length).toEqual(heroData.length)
- )
- .then(done, done.fail);
- });
-
- it('service.heroes remains empty until fulfilled', () => {
- service.refresh();
-
- // executed before refresh completes
- expect(service.heroes.length).toEqual(0);
- });
-
-});
-
-describe('HeroService (beginning tests - 5 [refactored beforeEach])', () => {
-
- describe('when backend provides data', () => {
- beforeEach(() => {
- heroData = [new Hero(1, 'Foo'), new Hero(2, 'Bar'), new Hero(3,'Baz')];
- service = new HeroService(new HappyBackendService());
- });
-
- it('refresh promise returns expected # of heroes when fulfilled', done => {
- service.refresh().then(() =>
- expect(service.heroes.length).toEqual(heroData.length)
- )
- .then(done, done.fail);
- });
-
- it('service.heroes remains empty until fulfilled', () => {
- service.refresh();
-
- // executed before refresh completes
- expect(service.heroes.length).toEqual(0);
- });
- });
-
-});
-
-
-///////// test helpers /////////
-var service: HeroService;
-var heroData: Hero[];
-
-class HappyBackendService {
- // return a promise for fake heroes that resolves as quickly as possible
- fetchAllHeroesAsync = () =>
- Promise.resolve(heroData.map(h => h.clone()));
-}
diff --git a/public/docs/_examples/testing/ts/app/old-specs/hero.service.no-ng.spec.ts.not-yet b/public/docs/_examples/testing/ts/app/old-specs/hero.service.no-ng.spec.ts.not-yet
deleted file mode 100644
index c2deb56c7c..0000000000
--- a/public/docs/_examples/testing/ts/app/old-specs/hero.service.no-ng.spec.ts.not-yet
+++ /dev/null
@@ -1,150 +0,0 @@
-// Test a service without referencing Angular (no Angular DI)
-import {HeroService} from './hero.service';
-import {BackendService} from './backend.service';
-import {Hero} from './hero';
-
-////// tests ////////////
-
-describe('HeroService (no-angular)', () => {
-
- describe('creation', () => {
- it('can instantiate the service', () => {
- let service = new HeroService(null);
- expect(service).toBeDefined();
- });
-
- it('service.heroes is empty', () => {
- let service = new HeroService(null);
- expect(service.heroes.length).toEqual(0);
- });
- });
-
- describe('#refresh', () => {
-
- describe('when backend provides data', () => {
-
- beforeEach(() => {
- heroData = [new Hero(1, 'Foo'), new Hero(2, 'Bar'), new Hero(3, 'Baz')];
- service = new HeroService(new HappyBackendService());
- });
-
-
- it('refresh promise returns expected # of heroes when fulfilled', done => {
- service.refresh().then(heroes =>
- expect(heroes.length).toEqual(heroData.length)
- )
- .then(done, done.fail);
- });
-
- it('service.heroes has expected # of heroes when fulfilled', done => {
- service.refresh().then(() =>
- expect(service.heroes.length).toEqual(heroData.length)
- )
- .then(done, done.fail);
- });
-
- it('service.heroes remains empty until fulfilled', () => {
- service.refresh();
-
- // executed before refresh completes
- expect(service.heroes.length).toEqual(0);
- });
-
- it('service.heroes remains empty when the server returns no data', done => {
- heroData = []; // simulate no heroes from the backend
-
- service.refresh().then(() =>
- expect(service.heroes.length).toEqual(0)
- )
- .then(done, done.fail);
- });
-
- it('resets service.heroes w/ original data after re-refresh', done => {
- let firstHeroes: Hero[];
- let changedName = 'Gerry Mander';
-
- service.refresh().then(() => {
- firstHeroes = service.heroes; // remember array reference
-
- // Changes to cache! Should disappear after refresh
- service.heroes[0].name = changedName;
- service.heroes.push(new Hero(33, 'Hercules'));
- return service.refresh()
- })
- .then(() => {
- expect(firstHeroes).toBe(service.heroes); // same array
- expect(service.heroes.length).toEqual(heroData.length); // no Hercules
- expect(service.heroes[0].name).not.toEqual(changedName); // reverted name change
- })
- .then(done, done.fail);
- });
-
- it('clears service.heroes while waiting for re-refresh', done => {
- service.refresh().then(() => {
- service.refresh();
- expect(service.heroes.length).toEqual(0);
- })
- .then(done, done.fail);
- });
-
- // the paranoid will verify not only that the array lengths are the same
- // but also that the contents are the same.
- it('service.heroes has expected heroes when fulfilled (paranoia)', done => {
- service.refresh().then(() => {
- expect(service.heroes.length).toEqual(heroData.length);
- service.heroes.forEach(h =>
- expect(heroData.some(
- // hero instances are not the same objects but
- // each hero in result matches an original hero by value
- hd => hd.name === h.name && hd.id === h.id)
- )
- );
- })
- .then(done, done.fail);
- });
-
- });
-
- describe('when backend throws an error', () => {
-
- beforeEach(() => {
- service = new HeroService(new FailingBackendService());
- });
-
- it('returns failed promise with the server error', done => {
- service.refresh()
- .then(() => fail('refresh should have failed'))
- .catch(err => expect(err).toEqual(testError))
- .then(done, done.fail);
- });
-
- it('clears service.heroes', done => {
- service.refresh()
- .then(() => fail('refresh should have failed'))
- .catch(err => expect(service.heroes.length).toEqual(0))
- .then(done, done.fail);
- });
-
- });
- });
-});
-
-///////// test helpers /////////
-
-var service: HeroService;
-var heroData: Hero[];
-
-class HappyBackendService {
- // return a promise for fake heroes that resolves as quickly as possible
- fetchAllHeroesAsync = () =>
- Promise.resolve(heroData.map(h => h.clone()));
-}
-
-var testError = 'BackendService.fetchAllHeroesAsync failed on purpose';
-
-class FailingBackendService {
- // return a promise that fails as quickly as possible
- // force-cast it to because of TS typing bug.
- fetchAllHeroesAsync = () =>
- >Promise.reject(testError);
-}
diff --git a/public/docs/_examples/testing/ts/app/old-specs/heroes.component.ng.spec.ts.not-yet b/public/docs/_examples/testing/ts/app/old-specs/heroes.component.ng.spec.ts.not-yet
deleted file mode 100644
index 489745ca2b..0000000000
--- a/public/docs/_examples/testing/ts/app/old-specs/heroes.component.ng.spec.ts.not-yet
+++ /dev/null
@@ -1,276 +0,0 @@
-///// Angular 2 Test Bed ////
-import {bind, By} from 'angular2/angular2';
-
-import {
- beforeEach, xdescribe, describe, it, xit, // Jasmine wrappers
- beforeEachProviders,
- injectAsync,
- RootTestComponent as RTC,
- TestComponentBuilder as TCB
-} from 'angular2/testing';
-
-import {
- expectSelectedHtml,
- expectViewChildHtml,
- expectViewChildClass,
- injectTcb, tick} from '../test-helpers/test-helpers';
-
-///// Testing this component ////
-import {HeroesComponent} from './heroes.component';
-import {Hero} from './hero';
-import {HeroService} from './hero.service';
-import {User} from './user';
-
-let hc: HeroesComponent;
-let heroData: Hero[]; // fresh heroes for each test
-let mockUser: User;
-let service: HeroService;
-
-// get the promise from the refresh spy;
-// casting required because of inadequate d.ts for Jasmine
-let refreshPromise = () => (service.refresh).calls.mostRecent().returnValue;
-
-describe('HeroesComponent (with Angular)', () => {
-
- beforeEach(() => {
- heroData = [new Hero(1, 'Foo'), new Hero(2, 'Bar'), new Hero(3, 'Baz')];
- mockUser = new User();
- });
-
- // Set up DI bindings required by component (and its nested components?)
- // else hangs silently forever
- beforeEachProviders(() => [
- bind(HeroService).toClass(HappyHeroService),
- bind(User).toValue(mockUser)
- ]);
-
- // test-lib bug? first test fails unless this no-op test runs first
- it('ignore this test', () => expect(true).toEqual(true)); // hack
-
- it('can be created and has userName', injectTcb((tcb:TCB) => {
- let template = '';
- return tcb
- .overrideTemplate(HeroesComponent, template)
- .createAsync(HeroesComponent)
- .then((rootTC: RTC) => {
- hc = rootTC.debugElement.componentInstance;
- expect(hc).toBeDefined();// proof of life
- expect(hc.userName).toEqual(mockUser.name);
- });
- }));
-
- it('binds view to userName', injectTcb((tcb:TCB) => {
- let template = `
{{userName}}'s Heroes
`;
- return tcb
- .overrideTemplate(HeroesComponent, template)
- .createAsync(HeroesComponent)
- .then((rootTC: RTC) => {
- hc = rootTC.debugElement.componentInstance;
-
- rootTC.detectChanges(); // trigger component property binding
- expectSelectedHtml(rootTC, 'h1').toMatch(hc.userName);
- expectViewChildHtml(rootTC).toMatch(hc.userName);
- });
- }));
-
- describe('#onInit', () => {
- let template = '';
-
- it('HeroService.refresh not called immediately',
- injectTcb([HeroService], (tcb:TCB, heroService:HeroService) => {
-
- return tcb
- .overrideTemplate(HeroesComponent, template)
- .createAsync(HeroesComponent)
- .then(() => {
- let spy = heroService.refresh;
- expect(spy.calls.count()).toEqual(0);
- });
- }));
-
- it('onInit calls HeroService.refresh',
- injectTcb([HeroService], (tcb:TCB, heroService:HeroService) => {
-
- return tcb
- .overrideTemplate(HeroesComponent, template)
- .createAsync(HeroesComponent)
- .then((rootTC: RTC) => {
- hc = rootTC.debugElement.componentInstance;
- let spy = heroService.refresh;
- hc.ngOnInit(); // Angular framework calls when it creates the component
- expect(spy.calls.count()).toEqual(1);
- });
- }));
-
- it('onInit is called after the test calls detectChanges', injectTcb((tcb:TCB) => {
-
- return tcb
- .overrideTemplate(HeroesComponent, template)
- .createAsync(HeroesComponent)
- .then((rootTC: RTC) => {
- hc = rootTC.debugElement.componentInstance;
- let spy = spyOn(hc, 'onInit').and.callThrough();
-
- expect(spy.calls.count()).toEqual(0);
- rootTC.detectChanges();
- expect(spy.calls.count()).toEqual(1);
- });
- }));
- })
-
- describe('#heroes', () => {
- // focus on the part of the template that displays heroe names
- let template =
- '
{{h.name}}
';
-
- it('binds view to heroes', injectTcb((tcb:TCB) => {
- return tcb
- .overrideTemplate(HeroesComponent, template)
- .createAsync(HeroesComponent)
- .then((rootTC: RTC) => {
- // trigger {{heroes}} binding
- rootTC.detectChanges();
-
- // hc.heroes is still empty; need a JS cycle to get the data
- return rootTC;
- })
- .then((rootTC: RTC) => {
- hc = rootTC.debugElement.componentInstance;
- // now heroes are available for binding
- expect(hc.heroes.length).toEqual(heroData.length);
-
- rootTC.detectChanges(); // trigger component property binding
-
- // confirm hero list is displayed by looking for a known hero
- expect(rootTC.debugElement.nativeElement.innerHTML).toMatch(heroData[0].name);
- });
- }));
-
- // ... add more tests of component behavior affecting the heroes list
-
- });
-
- describe('#onSelected', () => {
-
- it('no hero is selected by default', injectHC(hc => {
- expect(hc.currentHero).not.toBeDefined();
- }));
-
- it('sets the "currentHero"', injectHC(hc => {
- hc.onSelect(heroData[1]); // select the second hero
- expect(hc.currentHero).toEqual(heroData[1]);
- }));
-
- it('no hero is selected after onRefresh() called', injectHC(hc => {
- hc.onSelect(heroData[1]); // select the second hero
- hc.onRefresh();
- expect(hc.currentHero).not.toBeDefined();
- }));
-
- // TODO: Remove `withNgClass=true` ONCE BUG IS FIXED
- xit('the view of the "currentHero" has the "selected" class (NG2 BUG)', injectHC((hc, rootTC) => {
- hc.onSelect(heroData[1]); // select the second hero
-
- rootTC.detectChanges();
-
- // The 3rd ViewChild is 2nd hero; the 1st is for the template
- expectViewChildClass(rootTC, 2).toMatch('selected');
- }, true /* true == include ngClass */));
-
- it('the view of a non-selected hero does NOT have the "selected" class', injectHC((hc, rootTC) => {
- hc.onSelect(heroData[1]); // select the second hero
- rootTC.detectChanges();
- // The 4th ViewChild is 3rd hero; the 1st is for the template
- expectViewChildClass(rootTC, 4).not.toMatch('selected');
- }));
-
- });
-
- // Most #onDelete tests not re-implemented because
- // writing those tests w/in Angular adds little value and
- // is far more painful than writing them to run outside Angular
- // Only bother with the one test that checks the DOM
- describe('#onDeleted', () => {
- let template =
- '
{{h.name}}
';
-
- it('the list view does not contain the "deleted" currentHero', injectTcb((tcb:TCB) => {
- return tcb
- .overrideTemplate(HeroesComponent, template)
- .createAsync(HeroesComponent)
- .then((rootTC: RTC) => {
- hc = rootTC.debugElement.componentInstance;
- // trigger {{heroes}} binding
- rootTC.detectChanges();
- return rootTC; // wait for heroes to arrive
- })
- .then((rootTC: RTC) => {
- hc.currentHero = heroData[1];
- hc.onDelete()
- rootTC.detectChanges(); // trigger component property binding
-
- // confirm hero list is not displayed by looking for removed hero
- expect(rootTC.debugElement.nativeElement.innerHTML).not.toMatch(heroData[1].name);
- });
- }));
- });
-});
-
-////// Helpers //////
-
-class HappyHeroService {
-
- constructor() {
- spyOn(this, 'refresh').and.callThrough();
- }
-
- heroes: Hero[];
-
- refresh() {
- this.heroes = [];
- // updates cached heroes after one JavaScript cycle
- return new Promise((resolve, reject) => {
- this.heroes.push(...heroData);
- resolve(this.heroes);
- });
- }
-}
-
-
-// The same setup for every test in the #onSelected suite
-// TODO: Remove `withNgClass` and always include in template ONCE BUG IS FIXED
-function injectHC(testFn: (hc: HeroesComponent, rootTC?: RTC) => void, withNgClass:boolean = false) {
-
- // This is the bad boy: [ngClass]="getSelectedClass(hero)"
- let ngClass = withNgClass ? '[ngClass]="getSelectedClass(hero)"' : '';
-
- // focus on the part of the template that displays heroes
- let template =
- `
- ({{hero.id}}) {{hero.name}}
-
`;
-
- return injectTcb((tcb:TCB) => {
- let hc: HeroesComponent;
-
- return tcb
- .overrideTemplate(HeroesComponent, template)
- .createAsync(HeroesComponent)
- .then((rootTC:RTC) => {
- hc = rootTC.debugElement.componentInstance;
- rootTC.detectChanges();// trigger {{heroes}} binding
- return rootTC;
- })
- .then((rootTC:RTC) => { // wait a tick until heroes are fetched
-console.error("WAS THIS FIXED??");
- // CRASHING HERE IF TEMPLATE HAS '[ngClass]="getSelectedClass(hero)"'
- // WITH EXCEPTION:
- // "Expression 'getSelectedClass(hero) in null' has changed after it was checked."
-
- rootTC.detectChanges(); // show the list
- testFn(hc, rootTC);
- });
- })
-}
diff --git a/public/docs/_examples/testing/ts/app/old-specs/heroes.component.no-ng.spec.ts.not-yet b/public/docs/_examples/testing/ts/app/old-specs/heroes.component.no-ng.spec.ts.not-yet
deleted file mode 100644
index b1c1f2cff8..0000000000
--- a/public/docs/_examples/testing/ts/app/old-specs/heroes.component.no-ng.spec.ts.not-yet
+++ /dev/null
@@ -1,229 +0,0 @@
-import {HeroesComponent} from './heroes.component';
-import {Hero} from './hero';
-import {HeroService} from './hero.service';
-import {User} from './user';
-
-describe('HeroesComponent (Test Plan)', () => {
- xit('can be created');
- xit('has expected userName');
-
- describe('#onInit', () => {
- xit('HeroService.refresh not called immediately');
- xit('onInit calls HeroService.refresh');
- });
-
- describe('#heroes', () => {
- xit('lacks heroes when created');
- xit('has heroes after cache loaded');
- xit('restores heroes after refresh called again');
-
- xit('binds view to heroes');
- });
-
- describe('#onSelected', () => {
- xit('no hero is selected by default');
- xit('sets the "currentHero"');
- xit('no hero is selected after onRefresh() called');
-
- xit('the view of the "currentHero" has the "selected" class (NG2 BUG)');
- xit('the view of a non-selected hero does NOT have the "selected" class');
- });
-
- describe('#onDelete', () => {
- xit('removes the supplied hero (only) from the list');
- xit('removes the currentHero from the list if no hero argument');
- xit('is harmless if no supplied or current hero');
- xit('is harmless if hero not in list');
- xit('is harmless if the list is empty');
- xit('the new currentHero is the one after the removed hero');
- xit('the new currentHero is the one before the removed hero if none after');
-
- xit('the list view does not contain the "deleted" currentHero');
- });
-});
-
-let hc:HeroesComponent;
-let heroData: Hero[]; // fresh heroes for each test
-let mockUser: User;
-let service: HeroService;
-
-// get the promise from the refresh spy;
-// casting required because of inadequate d.ts for Jasmine
-let refreshPromise = () => (service.refresh).calls.mostRecent().returnValue;
-
-describe('HeroesComponent (no Angular)', () => {
-
- beforeEach(()=> {
- heroData = [new Hero(1, 'Foo'), new Hero(2, 'Bar'), new Hero(3, 'Baz')];
- mockUser = new User();
- });
-
- beforeEach(()=> {
- service = new HappyHeroService();
- hc = new HeroesComponent(service, mockUser)
- });
-
- it('can be created', () => {
- expect(hc instanceof HeroesComponent).toEqual(true); // proof of life
- });
-
- it('has expected userName', () => {
- expect(hc.userName).toEqual(mockUser.name);
- });
-
- describe('#onInit', () => {
- it('HeroService.refresh not called immediately', () => {
- let spy = service.refresh;
- expect(spy.calls.count()).toEqual(0);
- });
-
- it('onInit calls HeroService.refresh', () => {
- let spy = service.refresh;
- hc.ngOnInit(); // Angular framework calls when it creates the component
- expect(spy.calls.count()).toEqual(1);
- });
- })
-
- describe('#heroes', () => {
-
- it('lacks heroes when created', () => {
- let heroes = hc.heroes;
- expect(heroes.length).toEqual(0); // not filled yet
- });
-
- it('has heroes after cache loaded', done => {
- hc.ngOnInit(); // Angular framework calls when it creates the component
-
- refreshPromise().then(() => {
- let heroes = hc.heroes; // now the component has heroes to show
- expect(heroes.length).toEqual(heroData.length);
- })
- .then(done, done.fail);
- });
-
- it('restores heroes after refresh called again', done => {
- hc.ngOnInit(); // component initialization triggers service
- let heroes: Hero[];
-
- refreshPromise().then(() => {
- heroes = hc.heroes; // now the component has heroes to show
- heroes[0].name = 'Wotan';
- heroes.push(new Hero(33, 'Thor'));
- hc.onRefresh();
- })
- .then(() => {
- heroes = hc.heroes; // get it again (don't reuse old array!)
- expect(heroes[0]).not.toEqual('Wotan'); // change reversed
- expect(heroes.length).toEqual(heroData.length); // orig num of heroes
- })
- .then(done, done.fail);
- });
- });
-
- describe('#onSelected', () => {
-
- it('no hero is selected by default', () => {
- expect(hc.currentHero).not.toBeDefined();
- });
-
- it('sets the "currentHero"', () => {
- hc.onSelect(heroData[1]); // select the second hero
- expect(hc.currentHero).toEqual(heroData[1]);
- });
-
- it('no hero is selected after onRefresh() called', () => {
- hc.onSelect(heroData[1]); // select the second hero
- hc.onRefresh();
- expect(hc.currentHero).not.toBeDefined();
- });
- });
-
-
- describe('#onDelete', () => {
-
- // Load the heroes asynchronously before each test
- // Getting the async out of the way in the beforeEach
- // means tests can be synchronous
- // Note: could have cheated and simply plugged hc.heroes with fake data
- // that trick would fail if we reimplemented hc.heroes as a readonly property
- beforeEach(done => {
- hc.ngOnInit(); // Angular framework calls when it creates the component
- refreshPromise().then(done, done.fail);
- });
-
- it('removes the supplied hero (only) from the list', () => {
- hc.currentHero = heroData[1];
- let hero = heroData[2];
- hc.onDelete(hero);
-
- expect(hc.heroes).not.toContain(hero);
- expect(hc.heroes).toContain(heroData[1]); // left current in place
- expect(hc.heroes.length).toEqual(heroData.length - 1);
- });
-
- it('removes the currentHero from the list if no hero argument', () => {
- hc.currentHero = heroData[1];
- hc.onDelete();
- expect(hc.heroes).not.toContain(heroData[1]);
- });
-
- it('is harmless if no supplied or current hero', () => {
- hc.currentHero = null;
- hc.onDelete();
- expect(hc.heroes.length).toEqual(heroData.length);
- });
-
- it('is harmless if hero not in list', () => {
- let hero = heroData[1].clone(); // object reference matters, not id
- hc.onDelete(hero);
- expect(hc.heroes.length).toEqual(heroData.length);
- });
-
- // must go async to get hc to clear its heroes list
- it('is harmless if the list is empty', done => {
- let hero = heroData[1];
- heroData = [];
- hc.onRefresh();
- refreshPromise().then(() => {
- hc.onDelete(hero); // shouldn't fail
- })
- .then(done, done.fail);
- });
-
- it('the new currentHero is the one after the removed hero', () => {
- hc.currentHero = heroData[1];
- let expectedCurrent = heroData[2];
- hc.onDelete();
- expect(hc.currentHero).toBe(expectedCurrent);
- });
-
- it('the new currentHero is the one before the removed hero if none after', () => {
- hc.currentHero = heroData[heroData.length - 1]; // last hero
- let expectedCurrent = heroData[heroData.length - 2]; // penultimate hero
- hc.onDelete();
- expect(hc.currentHero).toBe(expectedCurrent);
- });
- });
-
-});
-
-
-////// Helpers //////
-
-class HappyHeroService {
-
- constructor() {
- spyOn(this, 'refresh').and.callThrough();
- }
-
- heroes: Hero[];
-
- refresh() {
- this.heroes = [];
- // updates cached heroes after one JavaScript cycle
- return new Promise((resolve, reject) => {
- this.heroes.push(...heroData);
- resolve(this.heroes);
- });
- }
-}
diff --git a/public/docs/_examples/testing/ts/app/old-specs/user.spec.ts.not-yet b/public/docs/_examples/testing/ts/app/old-specs/user.spec.ts.not-yet
deleted file mode 100644
index f65ac89a5e..0000000000
--- a/public/docs/_examples/testing/ts/app/old-specs/user.spec.ts.not-yet
+++ /dev/null
@@ -1,18 +0,0 @@
-import {User} from './user';
-
-describe('User', () => {
- let user:User;
-
- beforeEach(() => {
- user = new User();
- });
-
- it('has id === 42', () => {
- expect(user.id).toEqual(42);
- });
-
- it('has an email address', () => {
- expect(user.email.length).toBeGreaterThan(0);
- });
-
-});
\ No newline at end of file
diff --git a/public/docs/_examples/testing/ts/app/shared/highlight.directive.spec.ts b/public/docs/_examples/testing/ts/app/shared/highlight.directive.spec.ts
new file mode 100644
index 0000000000..c5f13934b9
--- /dev/null
+++ b/public/docs/_examples/testing/ts/app/shared/highlight.directive.spec.ts
@@ -0,0 +1,58 @@
+import { Component, DebugElement } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+
+import { HighlightDirective } from './highlight.directive';
+
+// Component to test directive
+@Component({
+ template: `
+
'
+})
+export class TwainComponent implements OnInit {
+ intervalId: number;
+ quote = '...';
+ constructor(private twainService: TwainService) { }
+
+ ngOnInit(): void {
+ this.twainService.getQuote().then(quote => this.quote = quote);
+ }
+}
+// #enddocregion component
diff --git a/public/docs/_examples/testing/ts/app/shared/twain.service.ts b/public/docs/_examples/testing/ts/app/shared/twain.service.ts
new file mode 100644
index 0000000000..9e394df1ee
--- /dev/null
+++ b/public/docs/_examples/testing/ts/app/shared/twain.service.ts
@@ -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 {
+ return new Promise(resolve => {
+ setTimeout( () => resolve(this.nextQuote()), 500 );
+ });
+ }
+
+ private nextQuote() {
+ if (this.next === quotes.length) { this.next = 0; }
+ return quotes[ this.next++ ];
+ }
+}
diff --git a/public/docs/_examples/testing/ts/app/welcome.component.spec.ts b/public/docs/_examples/testing/ts/app/welcome.component.spec.ts
new file mode 100644
index 0000000000..ec59ef5bc2
--- /dev/null
+++ b/public/docs/_examples/testing/ts/app/welcome.component.spec.ts
@@ -0,0 +1,83 @@
+// #docplaster
+import { ComponentFixture, 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;
+ let userService: UserService; // the actually injected service
+ let welcomeEl: DebugElement; // the element with the welcome message
+
+ // #docregion setup
+ beforeEach(() => {
+ // fake UserService for test purposes
+ // #docregion fake-userservice
+ const fakeUserService = {
+ isLoggedIn: true,
+ user: { name: 'Test User'}
+ };
+ // #enddocregion fake-userservice
+
+ // #docregion config-test-module
+ TestBed.configureTestingModule({
+ declarations: [ WelcomeComponent ],
+ // #enddocregion setup
+ // providers: [ UserService ] // a real service would be a problem!
+ // #docregion setup
+ providers: [ {provide: UserService, useValue: fakeUserService } ]
+ });
+ // #enddocregion config-test-module
+
+ fixture = TestBed.createComponent(WelcomeComponent);
+ comp = fixture.componentInstance;
+
+ // #enddocregion setup
+ // #docregion inject-from-testbed
+ // UserService provided to the TestBed
+ userService = TestBed.get(UserService);
+ // #enddocregion inject-from-testbed
+ // #docregion setup
+ // #docregion injected-service
+ // UserService actually injected into the component
+ userService = fixture.debugElement.injector.get(UserService);
+ // #enddocregion injected-service
+
+ // get the "welcome" element by CSS selector (e.g., by class name)
+ welcomeEl = fixture.debugElement.query(By.css('.welcome'));
+ });
+ // #enddocregion setup
+
+ // #docregion tests
+ it('should welcome the user', () => {
+ fixture.detectChanges(); // trigger data binding
+
+ let content = welcomeEl.nativeElement.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(); // trigger data binding
+
+ let content = welcomeEl.nativeElement.textContent;
+ expect(content).toContain('Bubba');
+ });
+
+ it('should request login if not logged in', () => {
+ userService.isLoggedIn = false; // welcome message hasn't been shown yet
+
+ fixture.detectChanges(); // trigger data binding
+
+ let content = welcomeEl.nativeElement.textContent;
+ expect(content).not.toContain('Welcome', 'not welcomed');
+ expect(content).toMatch(/log in/i, '"log in"');
+ });
+ // #enddocregion tests
+});
diff --git a/public/docs/_examples/testing/ts/app/welcome.component.ts b/public/docs/_examples/testing/ts/app/welcome.component.ts
new file mode 100644
index 0000000000..35958cc5c9
--- /dev/null
+++ b/public/docs/_examples/testing/ts/app/welcome.component.ts
@@ -0,0 +1,18 @@
+// #docregion
+import { Component, OnInit } from '@angular/core';
+import { UserService } from './model';
+
+@Component({
+ selector: 'app-welcome',
+ template: '
{{welcome}}
'
+})
+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.';
+ }
+}
diff --git a/public/docs/_examples/testing/ts/bag-specs.html b/public/docs/_examples/testing/ts/bag-specs.html
new file mode 100644
index 0000000000..792ebc113f
--- /dev/null
+++ b/public/docs/_examples/testing/ts/bag-specs.html
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+ Specs Bag
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/docs/_examples/testing/ts/bag-specs.plnkr.json b/public/docs/_examples/testing/ts/bag-specs.plnkr.json
new file mode 100644
index 0000000000..89d86da28a
--- /dev/null
+++ b/public/docs/_examples/testing/ts/bag-specs.plnkr.json
@@ -0,0 +1,20 @@
+{
+ "description": "Testing - bag.specs",
+ "files":[
+ "browser-test-shim.js",
+ "systemjs.config.extras.js",
+ "styles.css",
+
+ "app/bag/**/*.html",
+ "app/bag/**/*.ts",
+ "app/bag/**/*.spec.ts",
+
+ "!app/bag/bag-main.ts",
+
+ "testing/*.ts",
+
+ "bag-specs.html"
+ ],
+ "main": "bag-specs.html",
+ "tags": ["testing"]
+}
diff --git a/public/docs/_examples/testing/ts/bag.html b/public/docs/_examples/testing/ts/bag.html
new file mode 100644
index 0000000000..35ff270025
--- /dev/null
+++ b/public/docs/_examples/testing/ts/bag.html
@@ -0,0 +1,27 @@
+
+
+
+
+
+ Specs Bag
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Loading ...
+
+
diff --git a/public/docs/_examples/testing/ts/bag.plnkr.json b/public/docs/_examples/testing/ts/bag.plnkr.json
new file mode 100644
index 0000000000..96e0b79b65
--- /dev/null
+++ b/public/docs/_examples/testing/ts/bag.plnkr.json
@@ -0,0 +1,13 @@
+{
+ "description": "Running the bag",
+ "files":[
+ "styles.css",
+
+ "app/bag/bag.ts",
+ "app/bag/bag-external-template.html",
+ "app/bag/bag-main.ts",
+ "bag.html"
+ ],
+ "main": "bag.html",
+ "tags": ["testing"]
+}
diff --git a/public/docs/_examples/testing/ts/browser-test-shim.js b/public/docs/_examples/testing/ts/browser-test-shim.js
new file mode 100644
index 0000000000..1573c72ebd
--- /dev/null
+++ b/public/docs/_examples/testing/ts/browser-test-shim.js
@@ -0,0 +1,87 @@
+// BROWSER TESTING SHIM
+// Keep it in-sync with what karma-test-shim does
+// #docregion
+/*global jasmine, __karma__, window*/
+(function () {
+
+Error.stackTraceLimit = 0; // "No stacktrace"" is usually best for app testing.
+
+// Uncomment to get full stacktrace output. Sometimes helpful, usually not.
+// Error.stackTraceLimit = Infinity; //
+
+jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000;
+
+var baseURL = document.baseURI;
+baseURL = baseURL + baseURL[baseURL.length-1] ? '' : '/';
+
+System.config({
+ baseURL: baseURL,
+ // Extend usual application package list with test folder
+ packages: { 'testing': { main: 'index.js', defaultExtension: 'js' } },
+
+ // Assume npm: is set in `paths` in systemjs.config
+ // Map the angular testing umd bundles
+ map: {
+ '@angular/core/testing': 'npm:@angular/core/bundles/core-testing.umd.js',
+ '@angular/common/testing': 'npm:@angular/common/bundles/common-testing.umd.js',
+ '@angular/compiler/testing': 'npm:@angular/compiler/bundles/compiler-testing.umd.js',
+ '@angular/platform-browser/testing': 'npm:@angular/platform-browser/bundles/platform-browser-testing.umd.js',
+ '@angular/platform-browser-dynamic/testing': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic-testing.umd.js',
+ '@angular/http/testing': 'npm:@angular/http/bundles/http-testing.umd.js',
+ '@angular/router/testing': 'npm:@angular/router/bundles/router-testing.umd.js',
+ '@angular/forms/testing': 'npm:@angular/forms/bundles/forms-testing.umd.js',
+ },
+});
+
+System.import('systemjs.config.js')
+ .then(importSystemJsExtras)
+ .then(initTestBed)
+ .then(initTesting);
+
+/** Optional SystemJS configuration extras. Keep going w/o it */
+function importSystemJsExtras(){
+ return System.import('systemjs.config.extras.js')
+ .catch(function(reason) {
+ console.log(
+ 'Warning: System.import could not load the optional "systemjs.config.extras.js". Did you omit it by accident? Continuing without it.'
+ );
+ console.log(reason);
+ });
+}
+
+function initTestBed(){
+ return Promise.all([
+ System.import('@angular/core/testing'),
+ System.import('@angular/platform-browser-dynamic/testing')
+ ])
+
+ .then(function (providers) {
+ var coreTesting = providers[0];
+ var browserTesting = providers[1];
+
+ coreTesting.TestBed.initTestEnvironment(
+ browserTesting.BrowserDynamicTestingModule,
+ browserTesting.platformBrowserDynamicTesting());
+ })
+}
+
+// Import all spec files defined in the html (__spec_files__)
+// and start Jasmine testrunner
+function initTesting () {
+ console.log('loading spec files: '+__spec_files__.join(', '));
+ return Promise.all(
+ __spec_files__.map(function(spec) {
+ return System.import(spec);
+ })
+ )
+ // After all imports load, re-execute `window.onload` which
+ // triggers the Jasmine test-runner start or explain what went wrong
+ .then(success, console.error.bind(console));
+
+ function success () {
+ console.log('Spec files loaded; starting Jasmine testrunner');
+ window.onload();
+ }
+}
+
+})();
diff --git a/public/docs/_examples/testing/ts/index.html b/public/docs/_examples/testing/ts/index.html
index bfde80afe3..b50b69ec18 100644
--- a/public/docs/_examples/testing/ts/index.html
+++ b/public/docs/_examples/testing/ts/index.html
@@ -1,8 +1,9 @@
+
- Testing Tour of Heroes
+ App Under Test
@@ -15,6 +16,7 @@
+
diff --git a/public/docs/_examples/testing/ts/karma-test-shim.js b/public/docs/_examples/testing/ts/karma-test-shim.js
new file mode 100644
index 0000000000..19fcc89fe9
--- /dev/null
+++ b/public/docs/_examples/testing/ts/karma-test-shim.js
@@ -0,0 +1,89 @@
+// #docregion
+// /*global jasmine, __karma__, window*/
+Error.stackTraceLimit = 0; // "No stacktrace"" is usually best for app testing.
+
+// Uncomment to get full stacktrace output. Sometimes helpful, usually not.
+// Error.stackTraceLimit = Infinity; //
+
+jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000;
+
+var builtPath = '/base/app/';
+
+__karma__.loaded = function () { };
+
+function isJsFile(path) {
+ return path.slice(-3) == '.js';
+}
+
+function isSpecFile(path) {
+ return /\.spec\.(.*\.)?js$/.test(path);
+}
+
+function isBuiltFile(path) {
+ return isJsFile(path) && (path.substr(0, builtPath.length) == builtPath);
+}
+
+var allSpecFiles = Object.keys(window.__karma__.files)
+ .filter(isSpecFile)
+ .filter(isBuiltFile);
+
+System.config({
+ baseURL: '/base',
+ // Extend usual application package list with test folder
+ packages: { 'testing': { main: 'index.js', defaultExtension: 'js' } },
+
+ // Assume npm: is set in `paths` in systemjs.config
+ // Map the angular testing umd bundles
+ map: {
+ '@angular/core/testing': 'npm:@angular/core/bundles/core-testing.umd.js',
+ '@angular/common/testing': 'npm:@angular/common/bundles/common-testing.umd.js',
+ '@angular/compiler/testing': 'npm:@angular/compiler/bundles/compiler-testing.umd.js',
+ '@angular/platform-browser/testing': 'npm:@angular/platform-browser/bundles/platform-browser-testing.umd.js',
+ '@angular/platform-browser-dynamic/testing': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic-testing.umd.js',
+ '@angular/http/testing': 'npm:@angular/http/bundles/http-testing.umd.js',
+ '@angular/router/testing': 'npm:@angular/router/bundles/router-testing.umd.js',
+ '@angular/forms/testing': 'npm:@angular/forms/bundles/forms-testing.umd.js',
+ },
+});
+
+System.import('systemjs.config.js')
+ .then(importSystemJsExtras)
+ .then(initTestBed)
+ .then(initTesting);
+
+/** Optional SystemJS configuration extras. Keep going w/o it */
+function importSystemJsExtras(){
+ return System.import('systemjs.config.extras.js')
+ .catch(function(reason) {
+ console.log(
+ 'Warning: System.import could not load the optional "systemjs.config.extras.js". Did you omit it by accident? Continuing without it.'
+ );
+ console.log(reason);
+ });
+}
+
+function initTestBed(){
+ return Promise.all([
+ System.import('@angular/core/testing'),
+ System.import('@angular/platform-browser-dynamic/testing')
+ ])
+
+ .then(function (providers) {
+ var coreTesting = providers[0];
+ var browserTesting = providers[1];
+
+ coreTesting.TestBed.initTestEnvironment(
+ browserTesting.BrowserDynamicTestingModule,
+ browserTesting.platformBrowserDynamicTesting());
+ })
+}
+
+// Import all spec files and start karma
+function initTesting () {
+ return Promise.all(
+ allSpecFiles.map(function (moduleName) {
+ return System.import(moduleName);
+ })
+ )
+ .then(__karma__.start, __karma__.error);
+}
diff --git a/public/docs/_examples/karma.conf.js b/public/docs/_examples/testing/ts/karma.conf.js
similarity index 60%
rename from public/docs/_examples/karma.conf.js
rename to public/docs/_examples/testing/ts/karma.conf.js
index faa52df98e..1e2d293721 100644
--- a/public/docs/_examples/karma.conf.js
+++ b/public/docs/_examples/testing/ts/karma.conf.js
@@ -1,7 +1,12 @@
+// #docregion
module.exports = function(config) {
- var appBase = 'app/'; // transpiled app JS files
- var appAssets ='/base/app/'; // component assets fetched by Angular's compiler
+ var appBase = 'app/'; // transpiled app JS and map files
+ var appSrcBase = 'app/'; // app source TS files
+ var appAssets = '/base/app/'; // component assets fetched by Angular's compiler
+
+ var testBase = 'testing/'; // transpiled test JS and map files
+ var testSrcBase = 'testing/'; // test source TS files
config.set({
basePath: '',
@@ -9,7 +14,8 @@ module.exports = function(config) {
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
- require('karma-htmlfile-reporter')
+ require('karma-jasmine-html-reporter'), // click "Debug" in browser to see it
+ require('karma-htmlfile-reporter') // crashing w/ strange socket error
],
customLaunchers: {
@@ -26,39 +32,48 @@ module.exports = function(config) {
// Polyfills
'node_modules/core-js/client/shim.js',
-
- // Reflect and Zone.js
'node_modules/reflect-metadata/Reflect.js',
+
+ // zone.js
'node_modules/zone.js/dist/zone.js',
+ 'node_modules/zone.js/dist/long-stack-trace-zone.js',
+ 'node_modules/zone.js/dist/proxy.js',
+ 'node_modules/zone.js/dist/sync-test.js',
'node_modules/zone.js/dist/jasmine-patch.js',
'node_modules/zone.js/dist/async-test.js',
'node_modules/zone.js/dist/fake-async-test.js',
- // RxJs.
+ // RxJs
{ pattern: 'node_modules/rxjs/**/*.js', included: false, watched: false },
{ pattern: 'node_modules/rxjs/**/*.js.map', included: false, watched: false },
- // Angular 2 itself and the testing library
+ // Paths loaded via module imports:
+ // Angular itself
{pattern: 'node_modules/@angular/**/*.js', included: false, watched: false},
{pattern: 'node_modules/@angular/**/*.js.map', included: false, watched: false},
{pattern: 'systemjs.config.js', included: false, watched: false},
+ {pattern: 'systemjs.config.extras.js', included: false, watched: false},
'karma-test-shim.js',
// transpiled application & spec code paths loaded via module imports
{pattern: appBase + '**/*.js', included: false, watched: true},
+ {pattern: testBase + '**/*.js', included: false, watched: true},
- // asset (HTML & CSS) paths loaded via Angular's component compiler
+
+ // Asset (HTML & CSS) paths loaded via Angular's component compiler
// (these paths need to be rewritten, see proxies section)
{pattern: appBase + '**/*.html', included: false, watched: true},
{pattern: appBase + '**/*.css', included: false, watched: true},
- // paths for debugging with source maps in dev tools
- {pattern: appBase + '**/*.ts', included: false, watched: false},
- {pattern: appBase + '**/*.js.map', included: false, watched: false}
+ // Paths for debugging with source maps in dev tools
+ {pattern: appSrcBase + '**/*.ts', included: false, watched: false},
+ {pattern: appBase + '**/*.js.map', included: false, watched: false},
+ {pattern: testSrcBase + '**/*.ts', included: false, watched: false},
+ {pattern: testBase + '**/*.js.map', included: false, watched: false}
],
- // proxied base paths for loading assets
+ // Proxied base paths for loading assets
proxies: {
// required for component assets fetched by Angular's compiler
"/app/": appAssets
@@ -66,7 +81,8 @@ module.exports = function(config) {
exclude: [],
preprocessors: {},
- reporters: ['progress', 'html'],
+ // disabled HtmlReporter; suddenly crashing w/ strange socket error
+ reporters: ['progress', 'kjhtml'],//'html'],
// HtmlReporter configuration
htmlReporter: {
diff --git a/public/docs/_examples/testing/ts/liteserver-test-config.json b/public/docs/_examples/testing/ts/liteserver-test-config.json
deleted file mode 100644
index 6b1a2b5466..0000000000
--- a/public/docs/_examples/testing/ts/liteserver-test-config.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "startPath": "unit-tests.html"
-}
\ No newline at end of file
diff --git a/public/docs/_examples/testing/ts/plnkr.json b/public/docs/_examples/testing/ts/plnkr.json
new file mode 100644
index 0000000000..e3573a6b6d
--- /dev/null
+++ b/public/docs/_examples/testing/ts/plnkr.json
@@ -0,0 +1,16 @@
+{
+ "description": "Heroes Test App",
+ "files":[
+ "styles.css",
+ "systemjs.config.extras.js",
+
+ "app/**/*.css",
+ "app/**/*.html",
+ "app/**/*.ts",
+
+ "!app/bag/*.*",
+
+ "index.html"
+ ],
+ "tags": ["testing"]
+}
diff --git a/public/docs/_examples/testing/ts/systemjs.config.extras.js b/public/docs/_examples/testing/ts/systemjs.config.extras.js
new file mode 100644
index 0000000000..218e65715c
--- /dev/null
+++ b/public/docs/_examples/testing/ts/systemjs.config.extras.js
@@ -0,0 +1,9 @@
+// #docregion
+/** App specific SystemJS configuration */
+System.config({
+ packages: {
+ // barrels
+ 'app/model': {main:'index.js', defaultExtension:'js'},
+ 'app/model/testing': {main:'index.js', defaultExtension:'js'}
+ }
+});
diff --git a/public/docs/_examples/testing/ts/test-helpers/dom-setup.ts.not-yet b/public/docs/_examples/testing/ts/test-helpers/dom-setup.ts.not-yet
deleted file mode 100644
index 5e8f6d03a7..0000000000
--- a/public/docs/_examples/testing/ts/test-helpers/dom-setup.ts.not-yet
+++ /dev/null
@@ -1,18 +0,0 @@
-/////// MUST IMPORT AND EXECUTE BEFORE TestComponentBuilder TESTS ////////////
-
-// CRAZY BUG WORKAROUND:
-// Must FIRST import and mention something (anything?) from angular
-// else this file hangs systemjs for almost a minute
-import { bind } from 'angular2/angular2';
-function noop() { return bind; }
-
-/////// THIS SECTION REALLY SHOULD BE EXECUTED FOR US BY ANGULAR ////////////
-// should be in `angular2/test` or `angular2/angular2` but it isn't yet
-import {BrowserDomAdapter} from 'angular2/src/core/dom/browser_adapter';
-
-if (BrowserDomAdapter) {
- // MUST be called before any specs involving the TestComponentBuilder
- BrowserDomAdapter.makeCurrent();
-} else {
- console.log("BrowserDomAdapter not found; TestComponentBuilder tests will fail");
-}
diff --git a/public/docs/_examples/testing/ts/test-helpers/test-helpers.ts.not-yet b/public/docs/_examples/testing/ts/test-helpers/test-helpers.ts.not-yet
deleted file mode 100644
index e39f4ae8ee..0000000000
--- a/public/docs/_examples/testing/ts/test-helpers/test-helpers.ts.not-yet
+++ /dev/null
@@ -1,103 +0,0 @@
-import {FunctionWithParamTokens, injectAsync,RootTestComponent, TestComponentBuilder} from 'angular2/testing';
-import {By} from 'angular2/angular2'
-
-///////// Should be in testing /////////
-
-export type DoneFn = {
- fail: (err?:any) => void,
- (done?:any): () => void
-}
-
-///////// injectAsync extensions ///
-
-type PromiseLikeTestFn = (...args:any[]) => PromiseLike;
-type PromiseLikeTcbTestFn = (tcb: TestComponentBuilder, ...args:any[]) => PromiseLike;
-
-/** Run an async component test within Angular test bed using TestComponentBuilder
-// Example
-// it('async Component test', tcb => {
-// // your test here
-// // your test here
-// // your test here
-// return aPromise;
-// });
-//
-// May precede the test fn with some injectables which will be passed as args AFTER the TestComponentBuilder
-// Example:
-// it('async Component test w/ injectables', [HeroService], (tcb, service:HeroService) => {
-// // your test here
-// return aPromise;
-// });
-*/
-export function injectTcb(testFn: (tcb: TestComponentBuilder) => PromiseLike): FunctionWithParamTokens;
-export function injectTcb(dependencies: any[], testFn: PromiseLikeTcbTestFn): FunctionWithParamTokens;
-export function injectTcb(dependencies: any[] | PromiseLikeTcbTestFn, testFn?: PromiseLikeTcbTestFn) {
-
- if (typeof dependencies === 'function' ){
- testFn = dependencies;
- dependencies = [];
- }
-
- return injectAsync([TestComponentBuilder, ...(dependencies)], testFn);
-}
-///////// inspectors and expectations /////////
-
-export function getSelectedHtml(rootTC: RootTestComponent, selector: string) {
- var debugElement = rootTC.debugElement.query(By.css(selector));
- return debugElement && debugElement.nativeElement && debugElement.nativeElement.innerHTML;
-}
-
-export function expectSelectedHtml(rootTC: RootTestComponent, selector: string) {
- return expect(getSelectedHtml(rootTC, selector));
-}
-
-export function getSelectedClassName(rootTC: RootTestComponent, selector: string) {
- var debugElement = rootTC.debugElement.query(By.css(selector));
- return debugElement && debugElement.nativeElement && debugElement.nativeElement.className;
-}
-
-export function expectSelectedClassName(rootTC: RootTestComponent, selector: string) {
- return expect(getSelectedClassName(rootTC, selector));
-}
-
-export function getViewChildHtml(rootTC: RootTestComponent, elIndex: number = 0) {
- let child = rootTC.debugElement.componentViewChildren[elIndex];
- return child && child.nativeElement && child.nativeElement.innerHTML
-}
-
-export function expectViewChildHtml(rootTC: RootTestComponent, elIndex: number = 0) {
- return expect(getViewChildHtml(rootTC, elIndex));
-}
-
-export function expectViewChildClass(rootTC: RootTestComponent, elIndex: number = 0) {
- let child = rootTC.debugElement.componentViewChildren[elIndex];
- return expect(child && child.nativeElement && child.nativeElement.className);
-}
-
-export function dispatchEvent(element: Element, eventType: string) {
- element.dispatchEvent(new Event(eventType));
-}
-
-/** Let time pass so that DOM or Ng can react
-// returns a promise that returns ("passes through")
-// the value resolved in the previous `then` (if any)
-// after delaying for [millis] which is zero by default.
-// Example (passing along the rootTC w/ no delay):
-// ...
-// return rootTC; // optional
-// })
-// .then(tick)
-// .then(rootTC:RTC => { .. do something ..});
-//
-// Example (passing along nothing in particular w/ 10ms delay):
-// ...
-// // don't care if it returns something or not
-// })
-// .then(_ => tick(_, 10)) // ten milliseconds pass
-// .then(() => { .. do something ..});
-*/
-export function tick(passThru?: any, millis: number = 0){
- return new Promise((resolve, reject) =>{
- setTimeout(() => resolve(passThru), millis);
- });
-}
\ No newline at end of file
diff --git a/public/docs/_examples/testing/ts/test-shim.js b/public/docs/_examples/testing/ts/test-shim.js
deleted file mode 100644
index 31e1998e69..0000000000
--- a/public/docs/_examples/testing/ts/test-shim.js
+++ /dev/null
@@ -1,48 +0,0 @@
-/*global jasmine, __karma__, window*/
-
-// Browser testing shim
-(function () {
-
-// Error.stackTraceLimit = Infinity;
-
-jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000;
-
-// Configure systemjs to use the .js extension for imports from the app folder
-System.config({
- packages: {
- app: {
- format: 'register',
- defaultExtension: 'js'
- }
- }
-});
-
-// Configure Angular for the browser and with test versions of the platform providers
-System.import('angular2/testing')
- .then(function (testing) {
- return System.import('angular2/platform/testing/browser')
- .then(function (providers) {
- testing.setBaseTestProviders(
- providers.TEST_BROWSER_PLATFORM_PROVIDERS,
- providers.TEST_BROWSER_APPLICATION_PROVIDERS
- );
- });
- })
-
- // Load the spec files (__spec_files__) explicitly
- .then(function () {
- console.log('loading spec files: '+__spec_files__.join(', '));
- return Promise.all(__spec_files__.map(function(spec) { return System.import(spec);} ));
- })
-
- // After all imports load, re-execute `window.onload` which
- // triggers the Jasmine test-runner start or explain what went wrong
- .then(success, console.error.bind(console));
-
-function success () {
- console.log('Spec files loaded; starting Jasmine testrunner');
- window.onload();
-}
-
-
-})();
diff --git a/public/docs/_examples/testing/ts/testing/fake-router.ts b/public/docs/_examples/testing/ts/testing/fake-router.ts
new file mode 100644
index 0000000000..d42a3f8ad9
--- /dev/null
+++ b/public/docs/_examples/testing/ts/testing/fake-router.ts
@@ -0,0 +1,49 @@
+ // export for convenience.
+export { ActivatedRoute, Router, RouterLink, RouterOutlet} from '@angular/router';
+
+import { Component, Directive, Injectable, Input } from '@angular/core';
+import { NavigationExtras } from '@angular/router';
+
+@Directive({
+ selector: '[routerLink]',
+ host: {
+ '(click)': 'onClick()',
+ '[attr.href]': 'visibleHref',
+ '[class.router-link-active]': 'isRouteActive'
+ }
+})
+export class FakeRouterLinkDirective {
+
+ isRouteActive = false;
+ visibleHref: string; // the url displayed on the anchor element.
+
+ @Input('routerLink') linkParams: any;
+ navigatedTo: any = null;
+
+ onClick() {
+ this.navigatedTo = this.linkParams;
+ }
+}
+
+@Component({selector: 'router-outlet', template: ''})
+export class FakeRouterOutletComponent { }
+
+@Injectable()
+export class FakeRouter {
+ lastCommand: any[];
+ navigate(commands: any[], extras?: NavigationExtras) {
+ this.lastCommand = commands;
+ return commands;
+ }
+}
+
+@Injectable()
+export class FakeActivatedRoute {
+ testParams: {} = {};
+
+ get snapshot() {
+ return {
+ params: this.testParams
+ };
+ }
+}
diff --git a/public/docs/_examples/testing/ts/testing/index.ts b/public/docs/_examples/testing/ts/testing/index.ts
new file mode 100644
index 0000000000..f648a212e9
--- /dev/null
+++ b/public/docs/_examples/testing/ts/testing/index.ts
@@ -0,0 +1,23 @@
+import { tick, ComponentFixture } from '@angular/core/testing';
+
+export * from './jasmine-matchers';
+export * from './fake-router';
+
+// Short utilities
+/**
+ * Create custom DOM event the old fashioned way
+ *
+ * https://developer.mozilla.org/en-US/docs/Web/API/Event/initEvent
+ * Although officially deprecated, some browsers (phantom) don't accept the preferred "new Event(eventName)"
+ */
+export function newEvent(eventName: string, bubbles = false, cancelable = false) {
+ let evt = document.createEvent('CustomEvent'); // MUST be 'CustomEvent'
+ evt.initCustomEvent(eventName, bubbles, cancelable, null);
+ return evt;
+}
+
+/** Wait a tick, then detect changes */
+export function advance(f: ComponentFixture): void {
+ tick();
+ f.detectChanges();
+}
diff --git a/public/docs/_examples/testing/ts/testing/jasmine-matchers.d.ts b/public/docs/_examples/testing/ts/testing/jasmine-matchers.d.ts
new file mode 100644
index 0000000000..f1c5acf77c
--- /dev/null
+++ b/public/docs/_examples/testing/ts/testing/jasmine-matchers.d.ts
@@ -0,0 +1,5 @@
+declare namespace jasmine {
+ interface Matchers {
+ toHaveText(actual: any, expectationFailOutput?: any): jasmine.CustomMatcher;
+ }
+}
diff --git a/public/docs/_examples/testing/ts/testing/jasmine-matchers.ts b/public/docs/_examples/testing/ts/testing/jasmine-matchers.ts
new file mode 100644
index 0000000000..4cab02e148
--- /dev/null
+++ b/public/docs/_examples/testing/ts/testing/jasmine-matchers.ts
@@ -0,0 +1,45 @@
+///
+
+//// Jasmine Custom Matchers ////
+// Be sure to extend jasmine-matchers.d.ts when adding matchers
+
+export function addMatchers(): void {
+ jasmine.addMatchers({
+ toHaveText: toHaveText
+ });
+}
+
+function toHaveText(): jasmine.CustomMatcher {
+ return {
+ compare: function (actual: any, expectedText: string, expectationFailOutput?: any): jasmine.CustomMatcherResult {
+ const actualText = elementText(actual);
+ const pass = actualText.indexOf(expectedText) > -1;
+ const message = pass ? '' : composeMessage();
+ return { pass, message };
+
+ function composeMessage () {
+ const a = (actualText.length < 100 ? actualText : actualText.substr(0, 100) + '...');
+ const efo = expectationFailOutput ? ` '${expectationFailOutput}'` : '';
+ return `Expected element to have text content '${expectedText}' instead of '${a}'${efo}`;
+ }
+ }
+ };
+}
+
+function elementText(n: any): string {
+ if (n instanceof Array) {
+ return n.map(elementText).join('');
+ }
+
+ if (n.nodeType === Node.COMMENT_NODE) {
+ return '';
+ }
+
+ if (n.nodeType === Node.ELEMENT_NODE && n.hasChildNodes()) {
+ return elementText(Array.prototype.slice.call(n.childNodes));
+ }
+
+ if (n.nativeElement) { n = n.nativeElement; }
+
+ return n.textContent;
+}
diff --git a/public/docs/_examples/testing/ts/tsconfig.1.json b/public/docs/_examples/testing/ts/tsconfig.1.json
deleted file mode 100644
index 062cf1bcb4..0000000000
--- a/public/docs/_examples/testing/ts/tsconfig.1.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "compilerOptions": {
- "target": "es5",
- "module": "system",
- "moduleResolution": "node",
- "sourceMap": true,
- "emitDecoratorMetadata": true,
- "experimentalDecorators": true,
- "removeComments": false,
- "noImplicitAny": true,
- "suppressImplicitAnyIndexErrors": true
- }
-}
diff --git a/public/docs/_examples/testing/ts/unit-tests-0.html b/public/docs/_examples/testing/ts/unit-tests-0.html
deleted file mode 100644
index af7d1b9192..0000000000
--- a/public/docs/_examples/testing/ts/unit-tests-0.html
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
-
-
-
-
- Ng App Unit Tests
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/public/docs/_examples/testing/ts/unit-tests-1.html b/public/docs/_examples/testing/ts/unit-tests-1.html
deleted file mode 100644
index b370ca053a..0000000000
--- a/public/docs/_examples/testing/ts/unit-tests-1.html
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
-
-
- Ng App Unit Tests
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/public/docs/_examples/testing/ts/unit-tests-2.html b/public/docs/_examples/testing/ts/unit-tests-2.html
deleted file mode 100644
index d47b4d1f60..0000000000
--- a/public/docs/_examples/testing/ts/unit-tests-2.html
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
-
-
-
-
- Ng App Unit Tests
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/public/docs/_examples/testing/ts/unit-tests-3.html b/public/docs/_examples/testing/ts/unit-tests-3.html
deleted file mode 100644
index 349606bd6d..0000000000
--- a/public/docs/_examples/testing/ts/unit-tests-3.html
+++ /dev/null
@@ -1,41 +0,0 @@
-
-
-
-
-
- Ng App Unit Tests
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/public/docs/_examples/testing/ts/unit-tests-4.html b/public/docs/_examples/testing/ts/unit-tests-4.html
deleted file mode 100644
index a3e252fdb0..0000000000
--- a/public/docs/_examples/testing/ts/unit-tests-4.html
+++ /dev/null
@@ -1,43 +0,0 @@
-
-
-
-
- Ng App Unit Tests
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/public/docs/_examples/testing/ts/unit-tests-5.html b/public/docs/_examples/testing/ts/unit-tests-5.html
deleted file mode 100644
index b95b36760a..0000000000
--- a/public/docs/_examples/testing/ts/unit-tests-5.html
+++ /dev/null
@@ -1,41 +0,0 @@
-
-
-
-
- Ng App Unit Tests
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/public/docs/_examples/testing/ts/unit-tests-6.html.not-yet b/public/docs/_examples/testing/ts/unit-tests-6.html.not-yet
deleted file mode 100644
index df8e3704ba..0000000000
--- a/public/docs/_examples/testing/ts/unit-tests-6.html.not-yet
+++ /dev/null
@@ -1,44 +0,0 @@
-
-
-
-
-
- Ng App Unit Tests
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/public/docs/_examples/testing/ts/unit-tests-7.html.not-yet b/public/docs/_examples/testing/ts/unit-tests-7.html.not-yet
deleted file mode 100644
index d5449711ee..0000000000
--- a/public/docs/_examples/testing/ts/unit-tests-7.html.not-yet
+++ /dev/null
@@ -1,46 +0,0 @@
-
-
-
-
-
- Ng App Unit Tests
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/public/docs/_examples/testing/ts/unit-tests-bag.html b/public/docs/_examples/testing/ts/unit-tests-bag.html
deleted file mode 100644
index f373c387e8..0000000000
--- a/public/docs/_examples/testing/ts/unit-tests-bag.html
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
-
- Bag of Unit Tests
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/public/docs/_examples/testing/ts/unit-tests.html.not-yet b/public/docs/_examples/testing/ts/unit-tests.html.not-yet
deleted file mode 100644
index f1b8ab444d..0000000000
--- a/public/docs/_examples/testing/ts/unit-tests.html.not-yet
+++ /dev/null
@@ -1,57 +0,0 @@
-
-
-
-
-
- Ng App Unit Tests
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/public/docs/_examples/testing/ts/wallaby.js b/public/docs/_examples/testing/ts/wallaby.js
new file mode 100644
index 0000000000..acc34d35f5
--- /dev/null
+++ b/public/docs/_examples/testing/ts/wallaby.js
@@ -0,0 +1,119 @@
+// Configuration for the Wallaby Visual Studio Code testing extension
+// https://marketplace.visualstudio.com/items?itemName=WallabyJs.wallaby-vscode
+// Note: Wallaby is not open source and costs money
+
+module.exports = function () {
+ return {
+ files: [
+ // System.js for module loading
+ {pattern: 'node_modules/systemjs/dist/system.js', instrument: false},
+ {pattern: 'systemjs.config.js', instrument: false},
+ {pattern: 'systemjs.config.extras.js', instrument: false},
+
+ // Polyfills
+ {pattern: 'node_modules/core-js/client/shim.min.js', instrument: false},
+ {pattern: 'node_modules/reflect-metadata/Reflect.js', instrument: false},
+
+ // zone.js
+ {pattern: 'node_modules/zone.js/dist/zone.js', instrument: false},
+ {pattern: 'node_modules/zone.js/dist/long-stack-trace-zone.js', instrument: false},
+ {pattern: 'node_modules/zone.js/dist/proxy.js', instrument: false},
+ {pattern: 'node_modules/zone.js/dist/sync-test.js', instrument: false},
+ {pattern: 'node_modules/zone.js/dist/jasmine-patch.js', instrument: false},
+ {pattern: 'node_modules/zone.js/dist/async-test.js', instrument: false},
+ {pattern: 'node_modules/zone.js/dist/fake-async-test.js', instrument: false},
+
+ // application (but not specs) loaded via module imports
+ {pattern: 'app/**/*+(ts|html|css)', load: false},
+ {pattern: 'app/**/*.spec.ts', ignore: true},
+
+ {pattern: 'testing/**/*+(ts|html|css)', load: false},
+ ],
+
+ tests: [
+ {pattern: 'app/**/*.spec.ts', load: false}
+ ],
+
+ middleware: function (app, express) {
+ app.use('/node_modules', express.static(require('path').join(__dirname, 'node_modules')));
+ },
+
+ testFramework: 'jasmine',
+
+ debug: true,
+
+ bootstrap: bootstrap
+ };
+};
+
+// Like karma-test-shim.js
+function bootstrap (wallaby) {
+ wallaby.delayStart();
+
+ System.config({
+ // Extend usual application package list with test folder
+ packages: { 'testing': { main: 'index.js', defaultExtension: 'js' } },
+
+ // Assume npm: is set in `paths` in systemjs.config
+ // Map the angular testing umd bundles
+ map: {
+ '@angular/core/testing': 'npm:@angular/core/bundles/core-testing.umd.js',
+ '@angular/common/testing': 'npm:@angular/common/bundles/common-testing.umd.js',
+ '@angular/compiler/testing': 'npm:@angular/compiler/bundles/compiler-testing.umd.js',
+ '@angular/platform-browser/testing': 'npm:@angular/platform-browser/bundles/platform-browser-testing.umd.js',
+ '@angular/platform-browser-dynamic/testing': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic-testing.umd.js',
+ '@angular/http/testing': 'npm:@angular/http/bundles/http-testing.umd.js',
+ '@angular/router/testing': 'npm:@angular/router/bundles/router-testing.umd.js',
+ '@angular/forms/testing': 'npm:@angular/forms/bundles/forms-testing.umd.js',
+ },
+ });
+
+ System.import('systemjs.config.js')
+ .then(importSystemJsExtras)
+ .then(initTestBed)
+ .then(initTesting);
+
+ /** Optional SystemJS configuration extras. Keep going w/o it */
+ function importSystemJsExtras(){
+ return System.import('systemjs.config.extras.js')
+ .catch(function(reason) {
+ console.log(
+ 'Warning: System.import could not load the optional "systemjs.config.extras.js". Did you omit it by accident? Continuing without it.'
+ );
+ console.log(reason);
+ });
+ }
+
+ function initTestBed(){
+ return Promise.all([
+ System.import('@angular/core/testing'),
+ System.import('@angular/platform-browser-dynamic/testing')
+ ])
+
+ .then(function (providers) {
+ var coreTesting = providers[0];
+ var browserTesting = providers[1];
+
+ coreTesting.TestBed.initTestEnvironment(
+ browserTesting.BrowserDynamicTestingModule,
+ browserTesting.platformBrowserDynamicTesting());
+ })
+ }
+
+ // Load all spec files and start wallaby
+ function initTesting () {
+ return Promise.all(
+ wallaby.tests.map(function (specFile) {
+ return System.import(specFile);
+ })
+ )
+ .then(function () {
+ wallaby.start();
+ })
+ .catch(function (e) {
+ setTimeout(function () {
+ throw e;
+ }, 0);
+ });
+ }
+}
diff --git a/public/docs/_examples/toh-6/ts/app/hero-search.service.ts b/public/docs/_examples/toh-6/ts/app/hero-search.service.ts
index ae2e47670a..a7fb3cf520 100644
--- a/public/docs/_examples/toh-6/ts/app/hero-search.service.ts
+++ b/public/docs/_examples/toh-6/ts/app/hero-search.service.ts
@@ -12,7 +12,7 @@ export class HeroSearchService {
search(term: string): Observable {
return this.http
- .get(`app/heroes/?name=${term}`)
+ .get('app/heroes/?name=${term}')
.map((r: Response) => r.json().data as Hero[]);
}
}
diff --git a/public/docs/_examples/wallaby.js b/public/docs/_examples/wallaby.js
deleted file mode 100644
index 28053a11fe..0000000000
--- a/public/docs/_examples/wallaby.js
+++ /dev/null
@@ -1,77 +0,0 @@
-// Configuration for the Wallaby Visual Studio Code testing extension
-// https://marketplace.visualstudio.com/items?itemName=WallabyJs.wallaby-vscode
-// Note: Wallaby is not open source and costs money
-
-module.exports = function () {
-
- return {
- files: [
- // System.js for module loading
- {pattern: 'node_modules/systemjs/dist/system.js', instrument: false},
- {pattern: 'systemjs.config.js', instrument: false},
-
- // Polyfills
- {pattern: 'node_modules/core-js/client/shim.min.js', instrument: false},
-
- // Reflect, Zone.js, and test shims
- // Rx.js, Angular 2 itself, and the testing library not here because loaded by systemjs
- {pattern: 'node_modules/reflect-metadata/Reflect.js', instrument: false},
- {pattern: 'node_modules/zone.js/dist/zone.js', instrument: false},
- {pattern: 'node_modules/zone.js/dist/jasmine-patch.js', instrument: false},
- {pattern: 'node_modules/zone.js/dist/async-test.js', instrument: false},
- {pattern: 'node_modules/zone.js/dist/fake-async-test.js', instrument: false},
-
- {pattern: 'app/**/*+(ts|html|css)', load: false},
- {pattern: 'app/**/*.spec.ts', ignore: true}
- ],
-
- tests: [
- {pattern: 'app/**/*.spec.ts', load: false}
- ],
-
- middleware: function (app, express) {
- app.use('/node_modules', express.static(require('path').join(__dirname, 'node_modules')));
- },
-
- testFramework: 'jasmine',
-
- debug: true,
-
- bootstrap: function (wallaby) {
- wallaby.delayStart();
-
- System.config({
- packageWithIndex: true // sadly, we can't use umd packages (yet?)
- });
-
- System.import('systemjs.config.js')
- .then(function () {
- return Promise.all([
- System.import('@angular/core/testing'),
- System.import('@angular/platform-browser-dynamic/testing')
- ])
- })
- .then(function (providers) {
- var testing = providers[0];
- var testingBrowser = providers[1];
-
- testing.setBaseTestProviders(
- testingBrowser.TEST_BROWSER_DYNAMIC_PLATFORM_PROVIDERS,
- testingBrowser.TEST_BROWSER_DYNAMIC_APPLICATION_PROVIDERS);
-
- // Load all spec files
- return Promise.all(wallaby.tests.map(function (specFile) {
- return System.import(specFile);
- }));
- })
- .then(function () {
- wallaby.start();
- })
- .catch(function (e) {
- setTimeout(function () {
- throw e;
- }, 0);
- });
- }
- };
-};
diff --git a/public/docs/ts/latest/guide/testing.jade b/public/docs/ts/latest/guide/testing.jade
index 7777219c47..dd8fafa036 100644
--- a/public/docs/ts/latest/guide/testing.jade
+++ b/public/docs/ts/latest/guide/testing.jade
@@ -1,102 +1,1785 @@
-.alert.is-important
- :marked
- We are still preparing the testing guide with all the new testing features
- introduced in RC5 and will update it very soon.
+block includes
+ include ../_util-fns
+ - var _JavaScript = 'JavaScript';
+ //- Double underscore means don't escape var, use !{__var}.
+ - var __chaining_op = '; or ,';
+ - var __new_op = 'new';
+ - var __objectAsMap = 'object';
:marked
- We write **unit tests** to explore and confirm the **behavior** of parts of our application.
+ This chapter offers tips and techniques for testing Angular applications.
+ Along the way you will learn some general testing principles and techniques but the focus is on
+ Angular testing.
- 1. They **guard** against breaking existing code (“regressions”) when we make changes.
- 1. They **clarify** what the code does both when used as intended and when faced with deviant conditions.
- 1. They **reveal** mistakes in design and implementation. Tests force us to look at our code from many angles. When a part of our application seems hard to test, we may have discovered a design flaw, something we can cure now rather than later when it becomes expensive to fix.
-
-a(id="top")
+a#top
:marked
- # Table of Contents
-
- 1. [Jasmine Testing 101](#jasmine-101)
- - setup to run Jasmine tests in the browser
- - basic Jasmine testing skills
- - write simple Jasmine tests in TypeScript
- - debug a test in the browser
-
- 1. [The Application Under Test](#aut)
-
- 1. [First app test](#first-app-tests)
- - test a simple application interface outside of Angular
- - where to put the test file
- - load a test file with systemJS
-
- 1. [Pipe driven development](#pipe-testing)
- - create a test before creating a class
- - load multiple test files in our test harness, using system.js
- - add the Angular 2 library to our test harness
- - watch the new test fail, and fix it
-
- 1. Test an Asynchronous Service (forthcoming)
- - test an asynchronous service class outside of Angular
- - write a test plan in code
- - fake a dependency
- - master the `catch(fail).then(done)` pattern
- - move setup to `beforeEach`
- - test when a dependency fails
- - control async test timeout
-
- 1. The Angular Test Environment (forthcoming)
- - the Angular test environment and why we need help
- - add the Angular Test libraries to the test harness
- - test the same async service using Angular Dependency Injection
- - reduce friction with test helpers
- - introducing spies
-
- 1. Test a Component (forthcoming)
- - test the component outside of Angular
- - mock the dependent asynchronous service
- - simulate interaction with the view (no DOM)
- - use a spy-promise to control asynchronous test flow
-
- 1. Test a Component in the DOM (forthcoming
- - test the component inside the Angular test environment
- - use the `TestComponentBuilder`
- - more test helpers
- - interact with the DOM
- - bind to a mock dependent asynchronous service
-
- 1. Run the tests with karma (forthcoming)
+ # Contents
+ * [Introduction to Angular Testing](#testing-101)
+ * [Setup](#setup)
+ * [The first karma test](#1st-karma-test)
+ * [The Angular Testing Platform (ATP) ](#atp-intro)
+ * [The sample application and its tests](#sample-app)
+ * [A simple component test](#simple-component-test)
+ * [Test a component with a service dependency](#component-with-dependency)
+ * [Test a component with an async service](#component-with-async-service)
+ * [Test a component with an external template](#component-with-external-template)
+ * [Test a component with inputs and outputs](#component-with-inputs-output)
+ * [Test a component inside a test host component](#component-inside-test-host)
+ * [Test a routed component](#routed-component)
+ * [Isolated tests](#testing-without-atp "Testing without the Angular Testing Platform")
+ * [_TestBed_ API](#atp-api)
+ * [FAQ](#faq "Frequently asked questions")
+:marked
It’s a big agenda. Fortunately, you can learn a little bit at a time and put each lesson to use.
+ # Live examples
+ The chapter sample code is available as live examples for inspection, experiment, and download.
+
+ * The sample application
+ * The first spec
+ * The complete application specs
+ * A grab bag of demonstration specs
a(href="#top").to-top Back to top
.l-hr
-a(id="jasmine-101")
+a#testing-101
:marked
- # Jasmine Testing 101
-!= partial("../testing/jasmine-testing-101")
-a(href="#top").to-top Back to top
+ # Introduction to Angular Testing
+
+ You write tests to explore and confirm the behavior of the application.
+
+ 1. They **guard** against changes that break existing code (“regressions”).
+
+ 1. They **clarify** what the code does both when used as intended and when faced with deviant conditions.
+
+ 1. They **reveal** mistakes in design and implementation.
+ Tests shine a harsh light on the code from many angles.
+ When a part of the application seems hard to test, the root cause is often a design flaw,
+ something to cure now rather than later when it becomes expensive to fix.
+
+ This chapter assumes that you know something about testing. Don't worry if you don't.
+ There are plenty of books and online resources to get up to speed.
+
+
+
+
+ ## Tools and Technologies
+
+ You can write and run Angular tests with a variety of tools and technologies.
+ This chapter describes specific choices that are known to work well.
+
+table(width="100%")
+ col(width="20%")
+ col(width="80%")
+ tr
+ th Technology
+ th Purpose
+ tr(style=top)
+ td(style="vertical-align: top") Jasmine
+ td
+ :marked
+ The [Jasmine test framework](http://jasmine.github.io/2.4/introduction.html).
+ provides everything needed to write basic tests.
+ It ships with an HTML test runner that executes tests in the browser.
+ tr(style=top)
+ td(style="vertical-align: top") Angular Testing Platform
+ td
+ :marked
+ The Angular Testing Platform creates a test environment and harness
+ for the application code under test.
+ Use it to condition and control parts of the application as they
+ interact _within_ the Angular environment.
+ tr(style=top)
+ td(style="vertical-align: top") Karma
+ td
+ :marked
+ The [karma test runner](https://karma-runner.github.io/1.0/index.html)
+ is ideal for writing and running tests while developing the application.
+ It can be an integral part of the application build process.
+ This chapter describes how to setup and run tests with karma.
+ tr(style=top)
+ td(style="vertical-align: top") Protractor
+ td
+ :marked
+ Use protractor to write and run _end-to-end_ (e2e) tests.
+ End-to-end tests explore the application _as users experience it_.
+ In e2e testing, one process runs the real application
+ and a second process runs protractor tests that simulate user behavior
+ and assert that the application responds in the browser as expected.
.l-hr
-a(id="aut")
+a#setup
:marked
- # The Application to Test
-!= partial("../testing/application-under-test")
-a(href="#top").to-top Back to top
+ # Setup
+
+ Many think writing tests is fun.
+ Few enjoy setting up the test environment.
+ To get to the fun as quickly as possible,
+ the deep details of setup appear later in the chapter (_forthcoming_).
+ A bare minimum of discussion plus the downloadable source code must suffice for now.
+
+ There are two fast paths to getting started.
+ 1. Start a new project following the instructions in the
+ [QuickStart github repository](https://github.com/angular/quickstart/blob/master/README.md).
+
+ 1. Start a new project with the
+ [Angular CLI](https://github.com/angular/angular-cli/blob/master/README.md).
+
+ Both approaches install **npm packages, files, and scripts** pre-configured for applications
+ built in their respective modalities.
+ Their artifacts and procedures differ slightly but their essentials are the same
+ and there are no differences in the test code.
+
+ In this chapter, the application and its tests are based on the QuickStart repo.
+
+.alert.is-helpful
+ :marked
+ If youur application was based on the QuickStart repository,
+ you can skip the rest of this section and get on with your first test.
+ The QuickStart repo provides all necessary setup.
+
+:marked
+ Here's brief description of the setup files.
+
+table(width="100%")
+ col(width="20%")
+ col(width="80%")
+ tr
+ th File
+ th Description
+ tr
+ td(style="vertical-align: top") karma.conf.js
+ td
+ :marked
+ The karma configuration file that specifies which plug-ins to use,
+ which application and test files to load, which browser(s) to use,
+ and how to report test results.
+
+ It loads three other setup files:
+ * `systemjs.config.js`
+ * `systemjs.config.extras.js`
+ * `karma-test-shim.js`
+ tr
+ td(style="vertical-align: top") karma-test-shim.js
+ td
+ :marked
+ This shim prepares karma specifically for the Angular test environment
+ and launches karma itself.
+ It loads the `systemjs.config.js` file as part of that process.
+ tr
+ td(style="vertical-align: top") systemjs.config.js
+ td
+ :marked
+ [SystemJS](https://github.com/systemjs/systemjs/blob/master/README.md)
+ loads the application and test modules.
+ This script tells SystemJS where to find the module files and how to load them.
+ It's the same version of the file used by QuickStart-based applications.
+ tr
+ td(style="vertical-align: top") systemjs.config.extras.js
+ td
+ :marked
+ An optional file that supplements the SystemJS configuration in `systemjs.config.js` with
+ configuration for the specific needs of the application itself.
+
+ A stock `systemjs.config.js` can't anticipate those needs.
+ You fill the gaps here.
+
+ The sample version for this chapter adds the **model barrel**
+ to the SystemJs `packages` configuration.
+ tr
+ td(colspan="2")
+ +makeExample('testing/ts/systemjs.config.extras.js', '', 'systemjs.config.extras.js')(format='.')
+
+:marked
+ ### npm packages
+
+ The sample tests are written to run in Jasmine and karma.
+ The two "fast path" setups added the appropriate Jasmine and karma npm packages to the
+ `devDependencies` section of the `package.json`.
+ They were installed when you ran `npm install`.
.l-hr
-a(id="first-app-tests")
+a#1st-karma-test
:marked
- # First app test
-!= partial("../testing/first-app-tests")
-a(href="#top").to-top Back to top
+ # The first karma test
-.l-hr
-a(id="pipe-testing")
-:marked
- # Pipe driven development
-!= partial("../testing/testing-an-angular-pipe")
-a(href="#top").to-top Back to top
+ Start with a simple test to make sure the setup works properly.
+
+ Create a new file called `1st.spec.ts` in the application root folder, `app/`
.alert.is-important
:marked
- The testing chapter is still under development.
- Please bear with us as we both update and complete it.
+ Tests written in Jasmine are called _specs_ .
+ **The filename extension must be `.spec.ts`**,
+ the convention adhered to by `karma.conf.js` and other tooling.
+
+:marked
+ **Put spec files somewhere within the `app/` folder.**
+ The `karma.conf.js` tells karma to look for spec files there,
+ for reasons explained [below](#spec-file-location).
+
+ Add the following code to `app/1st.spec.ts`.
++makeExample('testing/ts/app/1st.spec.ts', '', 'app/1st.spec.ts')(format='.')
+:marked
+ ## Run karma
+ Compile and run it in karma from the command line.
+
+.l-sub-section
+ :marked
+ The QuickStart repo adds the following command to the `scripts` section in `package.json`.
+
+ code-example(format="." language="bash").
+ "test": "tsc && concurrently \"tsc -w\" \"karma start karma.conf.js\"",
+ :marked
+ Add that to your `package.json` if it's not there already.
+
+:marked
+ Open a terminal or command window and enter
+code-example(format="." language="bash").
+ npm test
+:marked
+ The command compiles the application and test code a first time.
+ If the compile fails, the command aborts.
+
+ If it succeeds, the command re-compiles (this time in watch mode) in one process
+ and starts karma in another.
+ Both processes watch pertinent files and re-run when they detect changes.
+
+ After a few moments, karma opens a browser ...
+figure.image-display
+ img(src='/resources/images/devguide/testing/karma-browser.png' style="width:400px;" alt="Karma browser")
+:marked
+ ... and starts writing to the console.
+
+ Hide (don't close!) the browser and focus on the console output which should look something like this.
+
+code-example(format="." language="bash").
+ > npm test
+ > tsc && concurrently "tsc -w" "karma start karma.conf.js"
+
+ [0] 1:37:03 PM - Compilation complete. Watching for file changes.
+ [1] 24 07 2016 13:37:09.310:WARN [karma]: No captured browser, open http://localhost:9876/
+ [1] 24 07 2016 13:37:09.361:INFO [karma]: Karma v0.13.22 server started at http://localhost:9876/
+ [1] 24 07 2016 13:37:09.370:INFO [launcher]: Starting browser Chrome
+ [1] 24 07 2016 13:37:10.974:INFO [Chrome 51.0.2704]: Connected on socket /#Cf6A5PkvMzjbbtn1AAAA with id 24600087
+ [1] Chrome 51.0.2704: Executed 0 of 0 SUCCESS
+ Chrome 51.0.2704: Executed 1 of 1 SUCCESS
+ SUCCESS (0.005 secs / 0.005 secs)
+
+:marked
+ Both the compiler and karma continue to run. The compiler output is preceeded by `[0]`;
+ the karma output by `[1]`.
+
+ Change the expectation from `true` to `false`.
+
+ The _compiler_ watcher detects the change and recompiles.
+
+code-example(format="." language="bash").
+ [0] 1:49:21 PM - File change detected. Starting incremental compilation...
+ [0] 1:49:25 PM - Compilation complete. Watching for file changes.
+
+:marked
+ The _karma_ watcher detects the change to the compilation output and re-runs the test.
+code-example(format="." language="bash").
+ [1] Chrome 51.0.2704: Executed 0 of 1 SUCCESS
+ Chrome 51.0.2704 1st tests true is true FAILED
+ [1] Expected false to equal true.
+ [1] Chrome 51.0.2704: Executed 1 of 1 (1 FAILED) (0.005 secs / 0.005 secs)
+
+:marked
+ It failed of course.
+
+ Restore the expectation from `false` back to `true`.
+ Both processes detect the change, re-run, and karma reports complete success.
+
+.alert.is-helpful
+ :marked
+ The console log can be quite long. Keep your eye on the last line.
+ It says `SUCCESS` when all is well.
+
+ If it says `FAILED`, scroll up to look for the error or, if that's too painful,
+ pipe the console output to a file and inspect with your favorite editor.
+ code-example(format="." language="json").
+ npm test > spec-output.txt
+
+:marked
+ ## Test debugging
+
+ Debug specs in the browser in the same way you debug an application.
+
+ - Reveal the karma browser window (hidden earlier).
+ - Open the browser's “Developer Tools” (F12 or Ctrl-Shift-I).
+ - Pick the “sources” section
+ - Open the `1st.spec.ts` test file (Ctrl-P, then start typing the name of the file).
+ - Set a breakpoint in the test
+ - Refresh the browser … and it stops at the breakpoint.
+
+figure.image-display
+ img(src='/resources/images/devguide/testing/karma-1st-spec-debug.png' style="width:700px;" alt="Karma debugging")
+
+a(href="#top").to-top Back to top
+
+.l-hr
+a#atp-intro
+:marked
+ # The Angular Testing Platform (ATP)
+
+ Many tests explore how applications classes interact with Angular and the DOM while under Angular's control.
+
+ Such tests are easy to write with the help of the _Angular Testing Platform_ (ATP)
+ which consists of the `TestBed` class and some helper functions.
+
+ Tests written with the _Angular Testing Platform_ are the main focus of this chapter.
+ But they are not the only tests you should write.
+
+ ### Isolated unit tests
+
+ You can and should write [isolated unit tests](#testing-without-atp "Testing without the Angular Testing Platform")
+ for components, directives, pipes, and services.
+ Isolated unit tests examine an instance of a class all by itself without
+ any dependence on Angular or any injected values.
+ The tester creates a test instance of the class with new, supplying fake constructor parameters as needed, and
+ then probes the test instance API surface.
+
+ Isolated tests don't reveal how the class interacts with Angular.
+ In particular, they can't reveal how a component class interacts with its own template or with other components.
+
+ Those tests require the Angular Testing Platform.
+
+ ### Testing with the _ Angular Testing Platform_
+
+ The _Angular Testing Platform_ consists of the `TestBed` class and some helper functions from `@angular/core/testing`.
+.alert.is-important
+ :marked
+ The _TestBed_ is officially _experimental_ and thus subject to change.
+ Consult the [API reference](../api/core/testing/index/TestBed-class.html) for the latest status.
+:marked
+ The `TestBed` creates an Angular test module — an `@NgModule` class —
+ that you configure to produce the module environment for the class you want to test.
+ You tell the `TestBed` to create an instance of the test component and probe that instance with tests.
+
+ That's the `TestBed` in a nutshell.
+
+ In practice, you work with the static methods of the `TestBed` class.
+ These static methods create and update a fresh hidden `TestBed` instance before each Jasmine `it`.
+.l-sub-section
+ :marked
+ You can access that hidden instance anytime by calling `getTestBed()`;
+:marked
+ This `TestBed` instance comes pre-configured with a baseline of default providers and declarables (components, directives, and pipes)
+ that almost everyone needs.
+ This chapter tests a browser application so the default includes the `CommonModule` declarables from `@angular/common`
+ and the `BrowserModule` providers (some of them mocked) from `@angular/platform-browser`.
+
+ You refine the default test module configuration with application and test specifics
+ so that it can produce an instance of the test component in the Angular environment suitable for your tests.
+
+ Start by calling `TestBed.configureTestingModule` with an object that looks like `@NgModule` metadata.
+ This object defines additional imports, declarations, providers and schemas.
+
+ After configuring the `TestBed`, tell it to create an instance of the test component and the test fixture
+ you'll need to inspect and control the component's immediate environment.
+
++makeExample('testing/ts/app/banner.component.spec.ts', 'simple-example-before-each', 'app/banner.component.spec.ts (simplified)')(format='.')
+:marked
+ Angular tests can interact with the HTML in the test DOM,
+ simulate user activity, tell Angular to perform specific task (such as change detection),
+ and see the effects of these actions both in the test component and in the test DOM.
++makeExample('testing/ts/app/banner.component.spec.ts', 'simple-example-it', 'app/banner.component.spec.ts (simplified)')(format='.')
+:marked
+ A comprehensive review of the _TestBed_ API appears [later in the chapter](#atp-api).
+ Let's dive right into Angular testing, starting with with the components of a sample application.
+
+a(href="#top").to-top Back to top
+
+.l-hr
+
+a#sample-app
+:marked
+ # The sample application and its tests
+
+ This chapter tests a cut-down version of the _Tour of Heroes_ [tutorial app](../tutorial).
+
+ The following live example shows how it works and provides the complete source code.
+
+
+:marked
+ The following live example runs all the tests of this application
+ inside the browser, using the Jasmine Test Runner instead of karma.
+
+ It includes the tests discussed in this chapter and additional tests for you to explore.
+ This live example contains both application and test code.
+ It is large and can take several minutes to start. Please be patient.
+
+
+a(href="#top").to-top Back to top
+.l-hr
+
+a#simple-component-test
+:marked
+ # Test a component
+
+:marked
+ The top of the screen displays application title, presented by the `BannerComponent` in `app/banner.component.ts`.
++makeExample('testing/ts/app/banner.component.ts', '', 'app/banner.component.ts')(format='.')
+:marked
+ `BannerComponent` has an inline template and an interpolation binding, about as simple as it gets.
+ Probably too simple to be worth testing in real life but perfect for a first encounter with the `TestBed`.
+
+ The corresponding `app/banner-component.spec.ts` sits in the same folder as the component,
+ for reasons explained [here](#q-spec-file-location);
+
+ Start with ES6 import statements to get access to symbols referenced in the spec.
++makeExample('testing/ts/app/banner.component.spec.ts', 'imports', 'app/banner.component.spec.ts (imports)')(format='.')
+:marked
+ Here's the setup for the tests followed by observations about the `beforeEach`:
++makeExample('testing/ts/app/banner.component.spec.ts', 'setup', 'app/banner.component.spec.ts (imports)')(format='.')
+:marked
+ `TestBed.configureTestingModule` takes an `@NgModule`-like metadata object.
+ This one simply declares the component to test, `BannerComponent`.
+
+ It lacks `imports` because (a) it extends the default test module configuration which
+ already has what `BannerComponent` needs
+ and (b) `BannerComponent` doesn't interact with any other components.
+
+ The configuration could have imported `AppModule` (which declares `BannerComponent`).
+ But that would lead to tons more configuration in order to support the other components within `AppModule`
+ that have nothing to do with `BannerComponent`.
+
+ `TestBed.createComponent` creates an instance of `BannerComponent` to test.
+ The method returns a `ComponentFixture`, a handle on the test environment surrounding the created component.
+ The fixture provides access to the component instance itself and
+ to the `DebugElement` which is a handle on the component's DOM element.
+
+ Query the `DebugElement` by CSS selector for the `
` sub-element that holds the actual title.
+
+
+ ### _createComponent_ closes configuration
+ `TestBed.createComponent` closes the current `TestBed` instance to further configuration.
+ You cannot call any more `TestBed` configuration methods, not `configureTestModule`
+ nor any of the `override...` methods. The `TestBed` throws an error if you try.
+
+.alert.is-important
+ :marked
+ Do not configure the `TestBed` after calling `createComponent`.
+:marked
+ ### The tests
+ Jasmine runs this `beforeEach` before each test of which there are two
++makeExample('testing/ts/app/banner.component.spec.ts', 'tests', 'app/banner.component.spec.ts (tests)')(format='.')
+:markdown
+ These tests ask the `DebugElement` for the native HTML element to satisfy their expectations.
+
+a#fixture-detect-changes
+:marked
+ ### _detectChanges_: Angular change detection under test
+
+ Each test tells Angular when to perform change detection by calling `fixture.detectChanges()`.
+ The first test does so immediately, triggering data binding and propagation of the `title` property
+ to the DOM element.
+
+ The second test changes the component's `title` property _and only then_ calls `fixture.detectChanges()`;
+ the new value appears in the DOM element.
+
+ In production, change detection kicks in automatically
+ when Angular creates a component or the user enters a keystroke or
+ an asynchronous activity (e.g., AJAX) completes.
+
+ The `TestBed.createComponent` does _not_ trigger change detection.
+ The fixture does not automatically push the component's `title` property value into the data bound element,
+ a fact demonstrated in the following test:
++makeExample('testing/ts/app/banner.component.spec.ts', 'test-w-o-detect-changes', 'app/banner.component.spec.ts (no detectChanges)')(format='.')
+:marked
+ This behavior (or lack of it) is intentional.
+ It gives the tester an opportunity to investigate the state of
+ the component _before Angular initiates data binding or calls lifecycle hooks_.
+
+a#automatic-change-detection
+:marked
+ ### Automatic change detection
+ Some testers prefer that the Angular test environment run change detection automatically.
+ That's possible by configuring the `TestBed` with the _AutoDetect_ provider:
++makeExample('testing/ts/app/banner.component.spec.ts', 'auto-detect', 'app/banner.component.spec.ts (AutoDetect)')(format='.')
+:marked
+ Here are three tests that illustrate how _auto-detect_ works.
++makeExample('testing/ts/app/banner.component.spec.ts', 'auto-detect-tests', 'app/banner.component.spec.ts (AutoDetect Tests)')(format='.')
+:marked
+ The first test shows the benefit of automatic change detection.
+
+ The second and third test remind us that Angular does _not_ know about changes to component property
+ values unless Angular itself (or some asynchronous process) makes the change.
+ This is as true in production as it is in test.
+
+ In production, external forces rarely change component properties like this,
+ whereas these kinds of probing changes are typical in unit tests.
+ The tester will have to call `fixture.detectChanges()` quite often
+ despite having opted into auto detect.
+
+.alert.is-helpful
+ :marked
+ Rather than wonder when the test fixture will or won't perform change detection,
+ the samples in this chapter _always call_ `detectChanges()` _explicitly_.
+
+a(href="#top").to-top Back to top
+
+.l-hr
+
+a#component-with-dependency
+:marked
+ # Test a component with a dependency
+ Components often have service dependencies.
+ The `WelcomeComponent` displays a welcome message to the logged in user.
+ It knows who the user is based on a property of the injected `UserService`:
++makeExample('testing/ts/app/welcome.component.ts', '', 'app/welcome.component.ts')(format='.')
+:marked
+ The `WelcomeComponent` has decision logic that interacts with the service;
+ such logic makes this component worth testing.
+ Here's the test module configuration for the spec file, `app/welcome.component.spec.ts`:
++makeExample('testing/ts/app/welcome.component.spec.ts', 'config-test-module', 'app/welcome.component.spec.ts')(format='.')
+:marked
+ This time, in addition to declaring the component under test,
+ the configurations sets the `providers` list with the dependent `UserService`.
+
+ This example configures the test module with a _fake_ `UserService`.
+
+ ## Provide service fakes
+
+ A component under test doesn't have to be injected with real services.
+ In fact, it is usually better if they are fakes.
+ The purpose of the spec is to test the component, not the service,
+ and real services can be trouble.
+
+ Injecting the real `UserService` could be a nightmare.
+ The real service might try to ask the user for login credentials and
+ try to reach an authentication server.
+ These behaviors could be hard to intercept.
+ It is far easier to create and register a fake `UserService`.
+
+ There are many ways to fake a service.
+ This test suit supplies a minimal `UserService` that satisfies the needs of the `WelcomeComponent`
+ and its tests:
++makeExample('testing/ts/app/welcome.component.spec.ts', 'fake-userservice')(format='.')
+
+a#injected-service-reference
+:marked
+ ## Referencing injected services
+ The tests need access to the injected (fake) `UserService`.
+
+ You cannot reference the `fakeUserService` object provided to the test module.
+ **It does not work!**
+ Surprisingly, the instance actually injected into the component is _not the same_
+ as the provided `fakeUserService` object.
+
+.alert.is-important
+ :marked
+ Always use an injector to get a reference to an injected service.
+:marked
+ Where do you get the injector?
+ Angular has an hierarchical injection system.
+ In a test there can be injectors at multiple levels.
+ The current `TestBed` injector creates a top-level injector.
+ The `WelcomeComponent` injector is a child of that injector created specifically for the component.
+
+ You can get a `UserService` from the current `TestBed` injector by calling `TestBed.get`.
++makeExample('testing/ts/app/welcome.component.spec.ts', 'inject-from-testbed', 'TestBed injector')(format='.')
+.l-sub-section
+ :marked
+ The [inject](#inject) function is another way to inject one or more services into a test.
+:marked
+ That happens to work for testing the `WelcomeComponent` because the `UserService` instance from the `TestBed`
+ is the same as the `UserService` instance injected into the component.
+
+ That won't always be the case.
+ Be absolutely sure to reference the service instance that the component is _actually receiving_,
+ Call `get` on the component's injector which is `fixture.debugElement.injector`:
++makeExample('testing/ts/app/welcome.component.spec.ts', 'injected-service', 'Component\'s injector')(format='.')
+.alert.is-important
+ :marked
+ Use the component's own injector to get the component's injected service.
+a#welcome-spec-setup
+:marked
+ Here's the complete, preferred `beforeEach`:
++makeExample('testing/ts/app/welcome.component.spec.ts', 'setup', 'app/welcome.component.spec.ts')(format='.')
+:marked
+ And here are some tests:
++makeExample('testing/ts/app/welcome.component.spec.ts', 'tests', 'app/welcome.component.spec.ts')(format='.')
+:marked
+ The first is a sanity test; it confirms that the fake `UserService` is working.
+ The remaining tests confirm the logic of the component when the service returns different values.
+ The second test validates the effect of changing the user name.
+ The third test checks that the component displays the proper message when there is no logged-in user.
+
+a(href="#top").to-top Back to top
+
+.l-hr
+
+a#component-with-async-service
+:marked
+ # Test a component with an async service
+ Many services return values asynchronously.
+ Most data services make an HTTP request to a remote server and the response is necessarily asynchronous.
+
+ The "About" view in this sample displays Mark Twain quotes.
+ The `TwainComponent` handles the display, delegating the server request to the `TwainService`.
+ Both are in the `app/shared` folder because the author intends to display Twain quotes on other pages someday.
+ Here is the `TwainComponent`.
++makeExample('testing/ts/app/shared/twain.component.ts', 'component', 'app/shared/twain.component.ts')(format='.')
+:marked
+ The `TwainService` implementation is irrelevant at this point.
+ It is sufficient to see within `ngOnInit` that `twainService.getQuote` returns a promise which means it is asynchronous.
+
+ In general, tests should not make calls to remote servers.
+ They should fake such calls. The setup in this `app/shared/twain.component.spec.ts` shows one way to do that:
++makeExample('testing/ts/app/shared/twain.component.spec.ts', 'setup', 'app/shared/twain.component.spec.ts (setup)')(format='.')
+
+a#service-spy
+:marked
+ ### Spying on the real service
+
+ This setup is similar to the [`welcome.component.spec` setup](#welcome-spec-setup).
+ But instead of creating a fake service object, it injects the _real_ service (see the test module `providers`) and
+ replaces the critical `getQuote` method with a Jasmine spy.
++makeExample('testing/ts/app/shared/twain.component.spec.ts', 'spy')(format='.')
+:marked
+ The spy is designed such that any call to `getQuote` receives an immediately resolved promise with a test quote.
+ The spy bypasses the actual `getQuote` method and therefore will not contact the server.
+
+.l-sub-section
+ :marked
+ Faking a service instance and spying on the real service are _both_ great options.
+ Pick the one that seems easiest for the current test suite. Don't be afraid to change your mind.
+:marked
+ Here are the tests with commentary to follow:
++makeExample('testing/ts/app/shared/twain.component.spec.ts', 'tests', 'app/shared/twain.component.spec.ts (tests)')
+:marked
+ ### Synchronous tests
+ The first two tests are synchronous.
+ Neither test can prove that a value from the service will be displayed.
+
+ Thanks to the spy, the second test verifies that `getQuote` is called.
+ But the quote itself has not arrived, despite the fact that the spy returns a resolved promise.
+
+ This test must wait at least one full turn of the JavaScript engine, a least one "tick", before the
+ value becomes available. By that time, the test runner has moved on to the next test in the suite.
+
+ The test must become an "async test" ... like the third test
+
+a#async-fn-in-it
+:marked
+ ## The _async_ function in _it_
+
+ Notice the `async` in the third test.
++makeExample('testing/ts/app/shared/twain.component.spec.ts', 'async-test', 'app/shared/twain.component.spec.ts (async test)')(format='.')
+:marked
+ The `async` function is part of the _Angular TestBed_ feature set.
+ It _takes_ a parameterless function and _returns_ a parameterless function
+ which becomes the argument to the Jasmine `it` call.
+
+ The body of the `async` argument looks much like the body of a normal `it` argument.
+ There is nothing obviously asynchronous about it. For example, it doesn't return a promise.
+
+ The `async` function arranges for the tester's code to run in a special _async test zone_
+ that almost hides the mechanics of asynchronous execution.
+
+ Almost but not completely.
+
+a#when-stable
+:marked
+ ## _whenStable_
+ The test must wait for the `getQuote` promise to resolve.
+
+ The `getQuote` promise promise resolves in the next turn of the JavaScript engine, thanks to the spy.
+ But a different test implementation of `getQuote` could take longer.
+ An integration test might call the _real_ `getQuote`, resulting in an XHR request
+ that took many seconds to respond.
+
+ This test has no direct access to the promise returned by the call to `testService.getQuote`
+ which is private and inaccessible inside `TwainComponent`.
+
+ Fortunately, the `getQuote` promise is accessible to the _async test zone_
+ which intercepts all promises issued within the _async_ method call.
+
+ The `ComponentFixture.whenStable` method returns its own promise which resolves when the `getQuote` promise completes.
+ In fact, the _whenStable_ promise resolves when _all pending asynchronous activities_ complete ... the definition of "stable".
+
+ Then the testing continues.
+ The test kicks off another round of change detection (`fixture.detechChanges`) which tells Angular to update the DOM with the quote.
+ The `getQuote` helper method extracts the display element text and the expectation confirms that the text matches the test quote.
+
+a#fakeAsync
+a#fake-async
+:marked
+ ## The _fakeAsync_ function
+
+ The fourth test verifies the same component behavior in a different way.
++makeExample('testing/ts/app/shared/twain.component.spec.ts', 'fake-async-test', 'app/shared/twain.component.spec.ts (fakeAsync test)')(format='.')
+:marked
+ Notice that `fakeAsync` replaces `async` as the `it` argument.
+ The `fakeAsync` function is also part of the _Angular TestBed_ feature set.
+ Like `async`, it too _takes_ a parameterless function and _returns_ a parameterless function
+ which becomes the argument to the Jasmine `it` call.
+
+ The `async` function arranges for the tester's code to run in a special _fakeAsync test zone_.
+
+ The key advantage of `fakeAsync` is that the test body looks entirely synchronous.
+ There are no promises at all.
+ No `then(...)` chains to disrupt the visible flow of control.
+
+.l-sub-section
+ :marked
+ There are limitations. For example, you cannot make an XHR call from within a `fakeAsync`.
+:marked
+
+a#tick
+a#tick-first-look
+:marked
+ ## The _tick_ function
+ Compare the third and fourth tests. Notice that `fixture.whenStable` is gone, replaced by `tick()`.
+
+ The `tick` function is a part of the _Angular TestBed_ feature set and a companion to `fakeAsync`.
+ It can only be called within a `fakeAsync` body.
+
+ Calling `tick()` simulates the passage of time until all pending asynchronous activities complete,
+ including the resolution of the `getQuote` promise in this test case.
+
+ It returns nothing. There is no promise to wait for.
+ Proceed with the same test code as formerly appeared within the `whenStable.then()` callback.
+
+ Even this simple example is easier to read than the third test.
+ To more fully appreciate the improvement, imagine a succession of asynchronous operations,
+ chained in a long sequence of promise callbacks.
+
+a#jasmine-done
+:marked
+ ## _jasmine.done_
+
+ While `fakeAsync` and even `async` function greatly simplify Angular asynchronous testing,
+ you can still fallback to the traditional Jasmine asynchronous testing technique.
+
+ You can still pass `it` a function that takes a
+ [`done` callback](http://jasmine.github.io/2.0/introduction.html#section-Asynchronous_Support).
+ Now you are responsible for chaining promises, handling errors, and calling `done` at the appropriate moment.
+
+ Here is a `done` version of the previous two tests:
++makeExample('testing/ts/app/shared/twain.component.spec.ts', 'done-test', 'app/shared/twain.component.spec.ts (done test)')(format='.')
+:marked
+ Although we have no direct access to the `getQuote` promise inside `TwainComponent`,
+ the spy does and that makes it possible to wait for `getQuote` to finish.
+
+ The `jasmine.done` technique, while discouraged, may become necessary when neither `async` nor `fakeAsync`
+ can tolerate a particular asynchronous activity. That's rare but it happens.
+
+a(href="#top").to-top Back to top
+
+.l-hr
+
+a#component-with-external-template
+:marked
+ # Test a component with an external template
+ The `TestBed.createComponent` is a synchronous method.
+ It assumes that everything it could need is already in memory.
+
+ That has been true so far.
+ Each tested component's `@Component` metadata has a `template` property specifying an _inline templates_.
+ Neither component had a `styleUrls` property.
+ Everything necessary to compile them was in memory at test runtime.
+
+ The `DashboardHeroComponent` is different.
+ It has an external template and external css file, specified in `templateUrl` and `styleUrls` properties.
++makeExample('testing/ts/app/dashboard/dashboard-hero.component.ts', 'component', 'app/dashboard/dashboard-hero.component.ts (component)')(format='.')
+:marked
+ The compiler must read these files from a file system before it can create a component instance.
+
+ The `TestBed.compileComponents` method asynchronously compiles all the components configured in its
+ current test module. After it completes, external templates and css files, have been "inlined"
+ and `TestBed.createComponent` can do its job synchronously.
+.l-sub-section
+ :marked
+ WebPack developers need not call `compileComponents` because it inlines templates and css
+ as part of the automated build process that precedes running the test.
+:marked
+ The `app/dashboard/dashboard-hero.component.spec.ts` demonstrates the pre-compilation process:
++makeExample('testing/ts/app/dashboard/dashboard-hero.component.spec.ts', 'compile-components', 'app/dashboard/dashboard-hero.component.spec.ts (compileComponents)')(format='.')
+
+a#async-fn-in-before-each
+:marked
+ ## The _async_ function in _beforeEach_
+
+ Notice the `async` call in the `beforeEach`.
+
+ The `async` function is part of the _Angular TestBed_ feature set.
+ It _takes_ a parameterless function and _returns_ a parameterless function
+ which becomes the argument to the Jasmine `beforeEach` call.
+
+ The body of the `async` argument looks much like the body of a normal `beforEach` argument.
+ There is nothing obviously asynchronous about it. For example, it doesn't return a promise.
+
+ The `async` function arranges for the tester's code to run in a special _async test zone_
+ that hides the mechanics of asynchronous execution.
+
+a#compile-components
+:marked
+ ## _compileComponents_
+ In this example, `Testbed.compileComponents` compiles one component, the `DashboardComponent`.
+ It's the only declared component in this test module.
+
+ Tests later in this chapter have more declared components and some of them import application
+ modules that declare yet more components.
+ Some or all of these components could have external templates and css files.
+ `TestBed.compileComponents` compiles them all asynchonously at one time.
+
+ The `compileComponents` method returns a promise so you can perform additional tasks _after_ it finishes.
+
+ ### _compileComponents_ closes configuration
+ After `compileComponents` runs, the current `TestBed` instance is closed to further configuration.
+ You cannot call any more `TestBed` configuration methods, not `configureTestModule`
+ nor any of the `override...` methods. The `TestBed` throws an error if you try.
+
+.alert.is-important
+ :marked
+ Do not configure the `TestBed` after calling `compileComponents`.
+ Make `compileComponents` the last step
+ before calling `TestBed.createInstance` to instantiate the test component.
+:marked
+ The `DashboardHeroComponent` spec follows the asynchonous `beforeEach` with a
+ _synchronous_ `beforeEach` that completes the setup steps and runs tests ... as described in the next section.
+
+.l-hr
+
+a#component-with-inputs-outputs
+:marked
+ # Test a component with inputs and outputs
+ A component with inputs and outputs typically appears inside the view template of a host component.
+ The host uses a property binding to set the input property and uses an event binding to
+ listen to events raised by the output property.
+
+ The testing goal is to verify that such bindings work as expected.
+ The tests should set input values and listen for output events.
+
+ The `DashboardHeroComponent` is tiny example of a component in this role.
+ It displays an individual heroe provided by the `DashboardComponent`.
+ Clicking that hero tells the the `DashboardComponent` that the user has selected the hero.
+
+ The `DashboardHeroComponent` is embedded in the `DashboardComponent` template like this:
++makeExample('testing/ts/app/dashboard/dashboard.component.html', 'dashboard-hero', 'app/dashboard/dashboard.component.html (excerpt)')(format='.')
+:marked
+ The `DashboardHeroComponent` appears in an `*ngFor` repeater which sets each component's `hero` input property
+ to the iteration value and listens for the components `selected` event.
+
+ Here's the component's definition again:
++makeExample('testing/ts/app/dashboard/dashboard-hero.component.ts', 'component', 'app/dashboard/dashboard-hero.component.ts (component)')(format='.')
+:marked
+ While testing a component this simple has little intrinsic value, it's worth knowing how.
+ Three approaches come to mind:
+ 1. Test it as used by `DashboardComponent`
+ 1. Test it as a stand-alone component
+ 1. Test it as used by a substitute for `DashboardComponent`
+
+ A quick look at the `DashboardComponent` constructor discourages the first approach:
++makeExample('testing/ts/app/dashboard/dashboard.component.ts', 'ctor', 'app/dashboard/dashboard.component.ts (constructor)')(format='.')
+:marked
+ The `DashboardComponent` depends upon the Angular router and the `HeroService`.
+ You'd probably have to fake them both and that's a lot of work. The router is particularly challenging (see below).
+
+ The immediate goal is to test the `DashboardHeroComponent`, not the `DashboardComponent`, and there's no need
+ to work hard unnecessarily. Let's try the second and third options.
+
+ ## Test _DashboardHeroComponent_ stand-alone
+
+ Here's the spec file setup.
++makeExample('testing/ts/app/dashboard/dashboard-hero.component.spec.ts', 'setup', 'app/dashboard/dashboard-hero.component.spec.ts (setup)')(format='.')
+
+:marked
+ The async `beforeEach` was discussed [above](#component-with-external-template).
+ Having compiled the components asynchronously with `compileComponents`, the rest of the setup
+ proceeds _synchronously_ in a _second_ `beforeEach`, using the basic techniques described [earlier](#simple-component-test).
+
+ Note how the setup code assigns a test hero (`expectedHero`) to the component's `hero` property, emulating
+ the way the `DashboardComponent` would set it via the property binding in its repeater.
+
+ The first test follows:
++makeExample('testing/ts/app/dashboard/dashboard-hero.component.spec.ts', 'name-test', 'app/dashboard/dashboard-hero.component.spec.ts (name test)')(format='.')
+:marked
+ It verifies that the hero name is propagated through to template with a binding.
+ There's a twist. The template passes the hero name through the Angular `UpperCasePipe` so the
+ test must match the element value with the uppercased name:
++makeExample('testing/ts/app/dashboard/dashboard-hero.component.html')(format='.')
+:marked
+.alert.is-helpful
+ :marked
+ This small test demonstrates how Angular tests can verify a component's visual representation
+ — something not possible with [isolated unit tests](#isolated-component-tests) —
+ at low cost and without resorting to much slower and more complicated end-to-end tests.
+
+:marked
+ The second test verifies click behavior. Clicking the hero should rais a `selected` event that the
+ host component (`DashboardComponent` presumably) can hear:
++makeExample('testing/ts/app/dashboard/dashboard-hero.component.spec.ts', 'click-test', 'app/dashboard/dashboard-hero.component.spec.ts (click test)')(format='.')
+:marked
+ The component exposes an `EventEmitter` property. The test subscribes to it just as the host component would do.
+
+ The Angular `DebugElement.triggerEventHandler` lets the test raise _any data-bound event_.
+ In this example, the component's template binds to the hero `
`.
+
+ The test has a reference to that `
` in `heroEl` so triggering the `heroEl` click event should cause Angular
+ to call `DashboardHeroComponent.click`.
+
+ If the component behaves as expected, its `selected` property should emit the `hero` object,
+ the test detects that emission through its subscription, and the test will pass.
+
+.l-hr
+
+a#component-inside-test-host
+:marked
+ # Test a component inside a test host component
+
+ In the previous approach the tests themselves played the role of the host `DashboardComponent`.
+ A nagging suspicion remains.
+ Will the `DashboardHeroComponent` work properly when properly data-bound to a host component?
+
+ Testing with the actual `DashboardComponent` host is doable but seems more trouble than its worth.
+ It's easier to emulate the `DashboardComponent` host with a _test host_ like this one:
++makeExample('testing/ts/app/dashboard/dashboard-hero.component.spec.ts', 'test-host', 'app/dashboard/dashboard-hero.component.spec.ts (test host)')(format='.')
+:marked
+ The test host binds to `DashboardHeroComponent` as the `DashboardComponent` would but without
+ the distraction of the `Router`, the `HeroService` or even the `*ngFor` repeater.
+
+ The test host sets the component's `hero` input property with its test hero.
+ It binds the component's `selected` event with its `onSelected` handler that records the emitted hero
+ in its `selectedHero` property. Later the tests check that property to verify that the
+ `DashboardHeroComponent.selected` event really did emit the right hero.
+
+ The setup for the test-host tests is similar to the setup for the stand-alone tests:
++makeExample('testing/ts/app/dashboard/dashboard-hero.component.spec.ts', 'test-host-setup', 'app/dashboard/dashboard-hero.component.spec.ts (test host setup)')(format='.')
+:marked
+ This test module configuration shows two important differences:
+ 1. It _declares_ both the `DashboardHeroComponent` and the `TestHostComponent`.
+ 1. It _creates_ the `TestHostComponent` instead of the `DashboardHeroComponent`.
+
+ The `fixture` returned by `createComponent` holds an instance of `TestHostComponent` instead of an instance of `DashboardHeroComponent`.
+
+ Of course creating the `TestHostComponent` has the side-effect of creating a `DashboardHeroComponent`
+ because the latter appears within the template of the former.
+ The query for the hero element (`heroEl`) still finds it in the test DOM
+ albeit at greater depth in the element tree than before.
+
+ The tests themselves are almost identical to the stand-alone version
++makeExample('testing/ts/app/dashboard/dashboard-hero.component.spec.ts', 'test-host-tests', 'app/dashboard/dashboard-hero.component.spec.ts (test-host)')(format='.')
+:marked
+ Only the selected event test differs. It confirms that the selected `DashboardHeroComponent` hero
+ really does find its way up through the event binding to the host component.
+
+a(href="#top").to-top Back to top
+
+.l-hr
+
+a#routed-component
+:marked
+ # Test a routed component
+
+ Testing the actual `DashboardComponent` seemed daunting because it injects the `Router`.
++makeExample('testing/ts/app/dashboard/dashboard.component.ts', 'ctor', 'app/dashboard/dashboard.component.ts (constructor)')(format='.')
+:marked
+ It also injects the `HeroService` but faking that is a [familiar story](#component-with-async-servic).
+ The `Router` has a complicated API and is entwined with other services and application pre-conditions.
+
+ Fortunately, the `DashboardComponent` isn't doing much with the `Router`
++makeExample('testing/ts/app/dashboard/dashboard.component.ts', 'goto-detail', 'app/dashboard/dashboard.component.ts (goToDetail)')(format='.')
+:marked
+ This is often the case.
+ As a rule you test the component, not the router,
+ and care only if the component navigates with the right address under the given conditions.
+ Faking the router is an easy option. This should do the trick:
++makeExample('testing/ts/app/dashboard/dashboard.component.spec.ts', 'fake-router', 'app/dashboard/dashboard.component.spec.ts (fakeRouter)')(format='.')
+:marked
+ Now we setup the test module with the `fakeRouter` and a fake `HeroService` and
+ create a test instance of the `DashbaordComponent` for subsequent testing.
++makeExample('testing/ts/app/dashboard/dashboard.component.spec.ts', 'compile-and-create-body', 'app/dashboard/dashboard.component.spec.ts (compile and create)')(format='.')
+:marked
+ The following test clicks the displayed hero and confirms (with the help of a spy) that `Router.navigateByUrl` is called with the expected url.
++makeExample('testing/ts/app/dashboard/dashboard.component.spec.ts', 'navigate-test', 'app/dashboard/dashboard.component.spec.ts (navigate test)')(format='.')
+
+a#inject
+:marked
+ ## The _inject_ function
+
+ Notice the `inject` function in the second `it` argument.
++makeExample('testing/ts/app/dashboard/dashboard.component.spec.ts', 'inject')(format='.')
+:marked
+ The `inject` function is part of the _Angular TestBed_ feature set.
+ It injects services into the test function where you can alter, spy on, and manipulate them.
+
+ The `inject` function has two parameters
+ 1. an array of Angular dependency injection tokens
+ 1. a test function whose parameters correspond exactly to each item in the injection token array
+
+.callout.is-important
+ header inject uses the TestBed Injector
+ :marked
+ The `inject` function uses the current `TestBed` injector and can only return services provided at that level.
+ It does not return services from component providers.
+
+:marked
+ This example injects the `Router` from the current `TestBed` injector.
+ That's fine for this test because the `Router` is (and must be) provided by the application root injector.
+
+ If you need a service provided by the component's _own_ injector, call `fixture.debugElement.injector.get` instead:
++makeExample('testing/ts/app/welcome.component.spec.ts', 'injected-service', 'Component\'s injector')(format='.')
+.alert.is-important
+ :marked
+ Use the component's own injector to get the service actually injected into the component.
+
+:marked
+ The `inject` function closes the current `TestBed` instance to further configuration.
+ You cannot call any more `TestBed` configuration methods, not `configureTestModule`
+ nor any of the `override...` methods. The `TestBed` throws an error if you try.
+
+.alert.is-important
+ :marked
+ Do not configure the `TestBed` after calling `inject`.
+a(href="#top").to-top Back to top
+
+.l-hr
+
+a#isolated-tests
+a#testing-without-atp
+:marked
+ # Testing without the Angular Testing Platform
+
+ Testing applications with the help of the Angular Testing Platform (ATP) is the main focus of this chapter.
+
+ However, it's often more productive to explore the inner logic of application classes
+ with _isolated_ unit tests that don't use the ATP.
+ Such tests are often smaller, easier to read,
+ and easier to write and maintain.
+
+ They don't
+ * import from the Angular test libraries
+ * configure a module
+ * prepare dependency injection `providers`
+ * call `inject` or `async` or `fakeAsync`
+
+ They do
+ * exhibit standard, Angular-agnostic testing techniques
+ * create instances directly with `new`
+ * use stubs, spys, and mocks to fake dependencies.
+
+.callout.is-important
+ header Write both kinds of tests
+ :marked
+ Good developers write both kinds of tests for the same application part, often in the same spec file.
+ Write simple _isolated_ unit tests to validate the part in isolation.
+ Write _Angular_ tests to validate the part as it interacts with Angular,
+ updates the DOM, and collaborates with the rest of the application.
+
+:marked
+ ## Services
+ Services are good candidates for vanilla unit testing.
+ Here are some synchronous and asynchronous unit tests of the `FancyService`
+ written without assistance from Angular Testing Platform.
+
++makeExample('testing/ts/app/bag/bag.no-testbed.spec.ts', 'FancyService', 'app/bag/bag.no-testbed.spec.ts')
+:marked
+ A rough line count suggests that these tests are about 25% smaller than equivalent ATP tests.
+ That's telling but not decisive.
+ The benefit comes from reduced setup and code complexity.
+
+ Compare these equivalent tests of `FancyService.getTimeoutValue`.
++makeTabs(
+ `testing/ts/app/bag/bag.no-testbed.spec.ts, testing/ts/app/bag/bag.spec.ts`,
+ 'getTimeoutValue, getTimeoutValue',
+ `app/bag/bag.no-testbed.spec.ts, app/bag/bag.spec.ts (with ATP)`)
+:marked
+ They have about the same line-count.
+ The ATP version has more moving parts, including a couple of helper functions (`async` and `inject`).
+ Both work and it's not much of an issue if you're using the Angular Testing Platform nearby for other reasons.
+ On the other hand, why burden simple service tests with ATP complexity?
+
+ Pick the approach that suits you.
+
+ ### Services with dependencies
+
+ Services often depend on other services that Angular injects into the constructor.
+ You can test these services _without_ the testbed.
+ In many cases, it's easier to create and _inject_ dependencies by hand.
+
+ The `DependentService` is a simple example
++makeExample('testing/ts/app/bag/bag.ts', 'DependentService', 'app/bag/bag.ts')(format='.')
+:marked
+ It delegates it's only method, `getValue`, to the injected `FancyService`.
+
+ Here are several ways to test it.
++makeExample('testing/ts/app/bag/bag.no-testbed.spec.ts', 'DependentService', 'app/bag/bag.no-testbed.spec.ts')
+:marked
+ The first test creates a `FancyService` with `new` and passes it to the `DependentService` constructor.
+
+ It's rarely that simple. The injected service can be difficult to create or control.
+ You can mock the dependency, or use a fake value, or stub the pertinent service method
+ with a substitute method that is easy to control.
+
+ These _isolated_ unit testing techniques are great for exploring the inner logic of a service or its
+ simple integration with a component class.
+ Use the Angular Testing Platform when writing tests that validate how a service interacts with components
+ _within the Angular runtime environment_.
+
+ ## Pipes
+ Pipes are easy to test without the Angular Testing Platform (ATP).
+
+ A pipe class has one method, `transform`, that turns an input to an output.
+ The `transform` implementation rarely interacts with the DOM.
+ Most pipes have no dependence on Angular other than the `@Pipe`
+ metadata and an interface.
+
+ Consider a `TitleCasePipe` that capitalizes the first letter of each word.
+ Here's a naive implementation implemented with a regular expression.
++makeExample('testing/ts/app/shared/title-case.pipe.ts', '', 'app/shared/title-case.pipe.ts')(format='.')
+:marked
+ Anything that uses a regular expression is worth testing thoroughly.
+ Use simple Jasmine to explore the expected cases and the edge cases.
++makeExample('testing/ts/app/shared/title-case.pipe.spec.ts', 'excerpt', 'app/shared/title-case.pipe.spec.ts')
+:marked
+ ## Write ATP tests too
+ These are tests of the pipe _in isolation_.
+ They can't tell if the `TitleCasePipe` is working properly
+ as applied in the application components.
+
+ Consider adding ATP component tests such as this one.
++makeExample('testing/ts/app/hero/hero-detail.component.spec.ts', 'title-case-pipe', 'app/hero/hero-detail.component.spec.ts (pipe test)')
+
+a#isolated-component-tests
+:marked
+ ## Components
+
+ Component tests typically examine how a component class interacts with its own template or with collaborating components.
+ The Angular Testing Platform is specifically designed to facilitate such tests.
+
+ Consider this `ButtonComp` component.
++makeExample('testing/ts/app/bag/bag.ts', 'ButtonComp', 'app/bag/bag.ts (ButtonComp)')(format='.')
+:marked
+ The following ATP test demonstrates that clicking a button in the template leads
+ to an update of the on-screen message.
++makeExample('testing/ts/app/bag/bag.spec.ts', 'ButtonComp', 'app/bag/bag.spec.ts (ButtonComp)')(format='.')
+:marked
+ The assertions verify the data binding flow from one HTML control (the `