docs(testing): update testing to use toh-5

closes #1003
This commit is contained in:
Filipe Silva 2016-03-24 19:30:26 +00:00 committed by Ward Bell
parent 45bc6ed844
commit 803414ae80
47 changed files with 835 additions and 572 deletions

View File

@ -0,0 +1,31 @@
/* #docplaster */
/* #docregion css */
h1 {
font-size: 1.2em;
color: #999;
margin-bottom: 0;
}
h2 {
font-size: 2em;
margin-top: 0;
padding-top: 0;
}
nav a {
padding: 5px 10px;
text-decoration: none;
margin-top: 10px;
display: inline-block;
background-color: #eee;
border-radius: 4px;
}
nav a:visited, a:link {
color: #607D8B;
}
nav a:hover {
color: #039be5;
background-color: #CFD8DC;
}
nav a.router-link-active {
color: #039be5;
}
/* #enddocregion css */

View File

@ -0,0 +1,59 @@
// #docplaster
// #docregion
import { Component } from 'angular2/core';
import { RouteConfig, ROUTER_DIRECTIVES, ROUTER_PROVIDERS } from 'angular2/router';
import { HeroService } from './hero.service';
import { DashboardComponent } from './dashboard.component';
import { HeroesComponent } from './heroes.component';
// #docregion hero-detail-import
import { HeroDetailComponent } from './hero-detail.component';
// #enddocregion hero-detail-import
@Component({
selector: 'my-app',
// #docregion template
template: `
<h1>{{title}}</h1>
<nav>
<a [routerLink]="['Dashboard']">Dashboard</a>
<a [routerLink]="['Heroes']">Heroes</a>
</nav>
<router-outlet></router-outlet>
`,
// #enddocregion template
// #docregion style-urls
styleUrls: ['app/app.component.css'],
// #enddocregion style-urls
directives: [ROUTER_DIRECTIVES],
providers: [
ROUTER_PROVIDERS,
HeroService
]
})
@RouteConfig([
// #docregion dashboard-route
{
path: '/dashboard',
name: 'Dashboard',
component: DashboardComponent,
useAsDefault: true
},
// #enddocregion dashboard-route
// #docregion hero-detail-route
{
path: '/detail/:id',
name: 'HeroDetail',
component: HeroDetailComponent
},
// #enddocregion hero-detail-route
{
path: '/heroes',
name: 'Heroes',
component: HeroesComponent
}
])
export class AppComponent {
title = 'Tour of Heroes';
}
// #enddocregion

View File

@ -1,14 +0,0 @@
import {Hero} from './hero';
import {HEROES} from './mock-heroes';
let delay = 1000; // ms delay in return of data
export class BackendService {
fetchAllHeroesAsync(): Promise<Hero[]> {
return new Promise((resolve, reject) => {
// simulate latency by resolving promise after a delay
setTimeout(() => resolve(HEROES.map(h => h.clone())), delay)
})
}
}

View File

@ -1,11 +0,0 @@
import {bootstrap} from 'angular2/platform/browser';
// Application root component
import {HeroesComponent} from './heroes.component';
// Application-wide "injectables""
import {BackendService} from './backend.service';
import {HeroService} from './hero.service';
import {User} from './user';
bootstrap(HeroesComponent, [BackendService, HeroService, User]);

View File

@ -0,0 +1,63 @@
/* #docplaster */
/* #docregion */
[class*='col-'] {
float: left;
}
*, *:after, *:before {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
h3 {
text-align: center; margin-bottom: 0;
}
[class*='col-'] {
padding-right: 20px;
padding-bottom: 20px;
}
[class*='col-']:last-of-type {
padding-right: 0;
}
.grid {
margin: 0;
}
.col-1-4 {
width: 25%;
}
.module {
padding: 20px;
text-align: center;
color: #eee;
max-height: 120px;
min-width: 120px;
background-color: #607D8B;
border-radius: 2px;
}
h4 {
position: relative;
}
.module:hover {
background-color: #EEE;
cursor: pointer;
color: #607d8b;
}
.grid-pad {
padding: 10px 0;
}
.grid-pad > [class*='col-']:last-of-type {
padding-right: 20px;
}
@media (max-width: 600px) {
.module {
font-size: 10px;
max-height: 75px; }
}
@media (max-width: 1024px) {
.grid {
margin: 0;
}
.module {
min-width: 60px;
}
}
/* #enddocregion */

View File

@ -0,0 +1,11 @@
<!-- #docregion -->
<h3>Top Heroes</h3>
<div class="grid grid-pad">
<!-- #docregion click -->
<div *ngFor="#hero of heroes" (click)="gotoDetail(hero)" class="col-1-4">
<!-- #enddocregion click -->
<div class="module hero">
<h4>{{hero.name}}</h4>
</div>
</div>
</div>

View File

@ -0,0 +1,44 @@
// #docplaster
// #docregion
import { Component, OnInit } from 'angular2/core';
// #docregion import-router
import { Router } from 'angular2/router';
// #enddocregion import-router
import { Hero } from './hero';
import { HeroService } from './hero.service';
@Component({
selector: 'my-dashboard',
// #docregion template-url
templateUrl: 'app/dashboard.component.html',
// #enddocregion template-url
// #docregion css
styleUrls: ['app/dashboard.component.css']
// #enddocregion css
})
// #docregion component
export class DashboardComponent implements OnInit {
heroes: Hero[] = [];
// #docregion ctor
constructor(
private _router: Router,
private _heroService: HeroService) {
}
// #enddocregion ctor
ngOnInit() {
this._heroService.getHeroes()
.then(heroes => this.heroes = heroes.slice(1,5));
}
// #docregion goto-detail
gotoDetail(hero: Hero) {
let link = ['HeroDetail', { id: hero.id }];
this._router.navigate(link);
}
// #enddocregion goto-detail
}
// #enddocregion

View File

@ -1,16 +0,0 @@
// @Injectable is a placeholder decorator
// whose sole purpose is to trigger the TS compiler to
// generate the metadata that Angular DI needs for injection.
//
// Metadata generation happens IFF the class has a decorator ... any decorator
// See the `"emitDecoratorMetadata": true` flag in tsconfig.json
//
// For Angular-agnostic classes we can avoid importing from Angular
// and get the metadata generation side-effect
// by creating our own @Injectable decorator
// for the hip Functional Programmer:
export const Injectable = () => (cls:any) => cls;
// for everyone else, this is the same thing
//export function Injectable() { return (cls:any) => cls; }

View File

@ -1,3 +1,30 @@
.hero-detail div {padding:0.2em;}
.hero-detail div input {position: absolute; left:9em; }
.hero-id {position: absolute; left:7.5em; }
/* #docregion */
label {
display: inline-block;
width: 3em;
margin: .5em 0;
color: #607D8B;
font-weight: bold;
}
input {
height: 2em;
font-size: 1em;
padding-left: .4em;
}
button {
margin-top: 20px;
font-family: Arial;
background-color: #eee;
border: none;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer; cursor: hand;
}
button:hover {
background-color: #cfd8dc;
}
button:disabled {
background-color: #eee;
color: #ccc;
cursor: auto;
}

View File

@ -1,24 +1,14 @@
<!-- #docplaster -->
<!-- #docregion -->
<div class="hero-detail">
<!-- #docregion pipe-usage -->
<h2>{{hero.name | initCaps}} is {{userName}}'s current super hero!</h2>
<!-- #enddocregion pipe-usage -->
<div>
<button (click)="onDelete()" [disabled]="!hero">Delete</button>
<button (click)="onUpdate()" [disabled]="!hero">Update</button>
</div>
<div>
<label>Id: </label><span class="hero-id">{{hero.id}}</span></div>
<div>
<label>Name: </label>
<input [(ngModel)]="hero.name" placeholder="name">
</div>
<div>
<label>Power: </label>
<input [(ngModel)]="hero.power" placeholder="super power">
</div>
<div>
<label>Alter Ego: </label>
<input [(ngModel)]="hero.alterEgo" placeholder="alter ego">
</div>
</div>
<div *ngIf="hero">
<h2>{{hero.name}} details!</h2>
<div>
<label>id: </label>{{hero.id}}</div>
<div>
<label>name: </label>
<input [(ngModel)]="hero.name" placeholder="name" />
</div>
<!-- #docregion back-button -->
<button (click)="goBack()">Back</button>
<!-- #enddocregion back-button -->
</div>

View File

@ -1,35 +1,57 @@
import {Component, Directive, EventEmitter , ElementRef} from 'angular2/core';
// #docplaster
// #docregion
// #docregion v2
// #docregion import-oninit
import { Component, OnInit } from 'angular2/core';
// #enddocregion import-oninit
// #docregion import-route-params
import {RouteParams} from 'angular2/router';
// #enddocregion import-route-params
import {Hero} from './hero';
import {InitCapsPipe} from './init-caps-pipe';
import { Hero } from './hero';
// #docregion import-hero-service
import { HeroService } from './hero.service';
// #enddocregion import-hero-service
@Directive({selector: 'button'})
class DecoratorDirective {
constructor(el: ElementRef){
console.log(el)
}
}
// #docregion extract-template
@Component({
selector: 'my-hero-detail',
// #docregion template-url
templateUrl: 'app/hero-detail.component.html',
inputs: ['hero', 'userName'], // inputs
outputs: ['delete'], // outputs
directives: [DecoratorDirective],
// #enddocregion template-url
// #enddocregion v2
styleUrls: ['app/hero-detail.component.css'],
pipes: [InitCapsPipe]
inputs: ['hero']
// #docregion v2
})
export class HeroDetailComponent {
// #enddocregion extract-template
// #docregion implement
export class HeroDetailComponent implements OnInit {
// #enddocregion implement
hero: Hero;
delete = new EventEmitter();
onDelete() { this.delete.next(this.hero) }
onUpdate() {
if (this.hero) {
this.hero.name += 'x';
}
// #docregion ctor
constructor(
private _heroService: HeroService,
private _routeParams: RouteParams) {
}
userName: string;
// #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

View File

@ -1,33 +1,28 @@
//import {Injectable} from 'angular2/angular2'; // Don't get it from Angular
import {Injectable} from './decorators'; // Use the app's version
import {Hero} from './hero';
import {BackendService} from './backend.service';
// #docplaster
// #docregion
import { Hero } from './hero';
import { HEROES } from './mock-heroes';
import { Injectable } from 'angular2/core';
@Injectable()
export class HeroService {
getHeroes() {
return Promise.resolve(HEROES);
}
heroes: Hero[] = []; // cache of heroes
// See the "Take it slow" appendix
getHeroesSlowly() {
return new Promise<Hero[]>(resolve =>
setTimeout(()=>resolve(HEROES), 2000) // 2 seconds
);
}
constructor(protected _backend: BackendService) { }
refresh() : Promise<Hero[]> { // refresh heroes w/ latest from the server
this.heroes.length = 0;
return <Promise<Hero[]>> this._backend.fetchAllHeroesAsync()
.then(heroes => {
this.heroes.push(...heroes);
return this.heroes;
})
.catch(e => this._fetchFailed(e));
}
protected _fetchFailed(error:any) {
console.error(error);
return Promise.reject(error);
}
}
// FOR DOCUMENTATION ONLY. NOT USED
interface IHeroService {
heroes : Hero[];
refresh() : Promise<Hero[]>;
//#docregion get-hero
getHero(id: number) {
return Promise.resolve(HEROES).then(
heroes => heroes.filter(hero => hero.id === id)[0]
);
}
//#enddocregion get-hero
}
// #enddocregion

View File

@ -1,17 +1,17 @@
// #docregion
// #docplaster
// #docregion base-hero-spec
import {Hero} from './hero';
import { Hero } from './hero';
describe('Hero', () => {
it('has name given in the constructor', () => {
let hero = new Hero(1, 'Super Cat');
it('has name', () => {
let hero: Hero = {id: 1, name: 'Super Cat'};
expect(hero.name).toEqual('Super Cat');
});
it('has id given in the constructor', () => {
let hero = new Hero(1, 'Super Cat');
it('has id', () => {
let hero: Hero = {id: 1, name: 'Super Cat'};
expect(hero.id).toEqual(1);
});
// #enddocregion base-hero-spec

View File

@ -1,19 +1,5 @@
// #docregion
let nextId = 30;
export class Hero {
constructor(
public id?: number,
public name?: string,
public power?: string,
public alterEgo?: string
) {
this.id = id || nextId++;
}
clone() { return Hero.clone(this); }
static clone = (h:any) => new Hero(h.id, h.name, h.alterEgo, h.power);
static setNextId(next:number) { nextId = next; }
export interface Hero {
id: number;
name: string;
}

View File

@ -1,19 +0,0 @@
// #docregion
let nextId = 30;
class Hero {
constructor(
public id?: number,
public name?: string,
public power?: string,
public alterEgo?: string
) {
this.id = id || nextId++;
}
clone() { return Hero.clone(this); }
static clone = (h:any) => new Hero(h.id, h.name, h.alterEgo, h.power);
static setNextId(next:number) { nextId = next; }
}

View File

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

View File

@ -1,17 +1,21 @@
<div class="container">
<h1>{{userName}}'s Super Heroes</h1>
<button (click)="onRefresh(h)">Refresh</button>
<div class="message" *ngIf="!heroes.length" class="heroes">Loading heroes...</div>
<div class="message" *ngIf="heroes.length" class="heroes">Pick a hero</div> <ul class="heroes">
<li *ngFor="#hero of heroes"
[ngClass]="getSelectedClass(hero)"
(click)="onSelect(hero)">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</li>
</ul>
<div *ngIf="currentHero">
<hr/>
<my-hero-detail [hero]="currentHero" [user-name]="userName" (delete)="onDelete()">
</my-hero-detail>
</div>
<!-- #docplaster -->
<!-- #docregion -->
<h2>My Heroes</h2>
<ul class="heroes">
<li *ngFor="#hero of heroes"
[class.selected]="hero === selectedHero"
(click)="onSelect(hero)">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</li>
</ul>
<!-- #docregion mini-detail -->
<div *ngIf="selectedHero">
<h2>
<!-- #docregion pipe -->
{{selectedHero.name | uppercase}} is my hero
<!-- #enddocregion pipe -->
</h2>
<button (click)="gotoDetail()">View Details</button>
</div>
<!-- #enddocregion mini-detail -->
<!-- #enddocregion -->

View File

@ -1,53 +1,50 @@
import {Component, OnInit} from 'angular2/core';
import {HeroDetailComponent} from './hero-detail.component';
import {HeroService} from './hero.service';
import {Hero} from './hero';
import {User} from './user';
// #docplaster
// #docregion
import { Component, OnInit } from 'angular2/core';
import { Router } from 'angular2/router';
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',
directives: [HeroDetailComponent],
styleUrls: ['app/heroes.component.css']
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 {
heroes: Hero[] = [];
currentHero: Hero;
userName: string;
// #enddocregion heroes-component-renaming
heroes: Hero[];
selectedHero: Hero;
constructor(private _heroService: HeroService, private _user: User) {
this.userName = this._user.name || 'someone';
constructor(
private _router: Router,
private _heroService: HeroService) { }
getHeroes() {
this._heroService.getHeroes().then(heroes => this.heroes = heroes);
}
getSelectedClass(hero: Hero) {return { selected: hero === this.currentHero }};
onDelete(hero?: Hero) {
hero = hero || this.currentHero;
let i = this.heroes.indexOf(hero);
if (i > -1) {
this.heroes.splice(i, 1);
}
this.currentHero = this.heroes[i] || this.heroes[i - 1];
ngOnInit() {
this.getHeroes();
}
ngOnInit(){
this.heroes = this.onRefresh();
}
onRefresh() {
//console.log('Refreshing heroes');
// clear the decks
this.currentHero = undefined;
this.heroes = [];
this._heroService.refresh()
.then(heroes => this.heroes = heroes);
return this.heroes;
}
onSelect(hero: Hero) {
this.currentHero = hero;
console.log(`Hero selected: ` + JSON.stringify(hero));
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

View File

@ -1,39 +0,0 @@
// #docregion
// #docplaster
// #docregion base-pipe-spec
import {InitCapsPipe} from './init-caps-pipe';
describe('InitCapsPipe', () => {
let pipe:InitCapsPipe;
beforeEach(() => {
pipe = new InitCapsPipe();
});
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 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

View File

@ -1,15 +0,0 @@
// #docregion
// #docregion depends-on-angular
import {Pipe, PipeTransform} from 'angular2/core';
@Pipe({ name: 'initCaps' })
export class InitCapsPipe implements PipeTransform {
// #enddocregion depends-on-angular
transform(value: string) {
return value.toLowerCase().replace(/(?:^|\s)[a-z]/g, function(m) {
return m.toUpperCase();
});
}
// #docregion depends-on-angular
}
// #enddocregion depends-on-angular

View File

@ -0,0 +1,4 @@
import { bootstrap } from 'angular2/platform/browser';
import { AppComponent } from './app.component';
bootstrap(AppComponent);

View File

@ -1,70 +1,16 @@
import {Hero} from './hero';
// #docregion
import { Hero } from './hero';
export var HEROES: Hero[] = [
{
"id": 11,
"name": "Mr. Nice",
"alterEgo": "Walter Meek",
"power": "Empathy"
},
{
"id": 12,
"name": "Narco",
"alterEgo": "Nancy Knight",
"power": "Drowsiness"
},
{
"id": 13,
"name": "Bombasto",
"alterEgo": "Bob LaRue",
"power": "Hypersound"
},
{
"id": 14,
"name": "Celeritas",
"alterEgo": "Larry Plodder",
"power": "Super speed"
},
{
"id": 15,
"name": "Magneta",
"alterEgo": "Julie Ohm",
"power": "Master of electro-magnetic fields"
},
{
"id": 16,
"name": "Rubber Man",
"alterEgo": "Jimmy Longfellow",
"power": "Super flexible"
},
{
"id": 17,
"name": "Dynama",
"alterEgo": "Shirley Knots",
"power": "Incredible strength"
},
{
"id": 18,
"name": "Dr IQ",
"alterEgo": "Chuck Overstreet",
"power": "Really smart"
},
{
"id": 19,
"name": "Magma",
"alterEgo": "Harvey Klue",
"power": "Super hot"
},
{
"id": 20,
"name": "Tornado",
"alterEgo": "Ted Baxter",
"power": "Weather changer"
},
{
"id": 21,
"name": "eeny weenie",
"alterEgo": "Ima Small",
"power": "shrink to infinitesimal size"
}
].map(h => Hero.clone(h));
{"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

View File

@ -0,0 +1,9 @@
// #docregion
import {Pipe, PipeTransform} from 'angular2/core';
@Pipe({ name: 'my-uppercase' })
export class MyUppercasePipe implements PipeTransform {
transform(value: string) {
return value;
}
}

View File

@ -0,0 +1,41 @@
// #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

View File

@ -0,0 +1,13 @@
// #docregion
// #docregion depends-on-angular
import {Pipe, PipeTransform} from 'angular2/core';
// #enddocregion depends-on-angular
@Pipe({ name: 'my-uppercase' })
export class MyUppercasePipe implements PipeTransform {
// #docregion uppercase
transform(value: string) {
return value.toUpperCase();
}
// #enddocregion uppercase
}

View File

@ -1,6 +0,0 @@
// imagine this is the result of a login
export class User {
id = 42;
name = 'Bongo';
email = 'bongo@amazing.io'
};

View File

@ -1,27 +1,39 @@
<!DOCTYPE html>
<html>
<!-- #docregion head -->
<!-- #docregion base-href -->
<head>
<base href="/">
<!-- #enddocregion base-href -->
<title>Angular 2 Tour of Heroes</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<head>
<link rel="stylesheet" href="styles.css">
<script src="node_modules/es6-shim/es6-shim.min.js"></script>
<script src="node_modules/systemjs/dist/system-polyfills.js"></script>
<script src="node_modules/angular2/es6/dev/src/testing/shims_for_IE.js"></script>
<script src="node_modules/angular2/bundles/angular2-polyfills.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="node_modules/rxjs/bundles/Rx.js"></script>
<script src="node_modules/angular2/bundles/angular2.dev.js"></script>
</head>
<!-- #enddocregion head -->
<!-- #docregion css -->
<link rel="stylesheet" href="styles.css">
<!-- #enddocregion css -->
<!-- #docregion head -->
<!-- IE required polyfills, in this exact order -->
<script src="node_modules/es6-shim/es6-shim.min.js"></script>
<script src="node_modules/systemjs/dist/system-polyfills.js"></script>
<script src="node_modules/angular2/es6/dev/src/testing/shims_for_IE.js"></script>
<body>
<my-heroes></my-heroes>
<script>
System.config({
packages: {
'app': {defaultExtension: 'js'}
}
});
System.import('app/bootstrap.js');
</script>
</body>
<script src="node_modules/angular2/bundles/angular2-polyfills.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="node_modules/rxjs/bundles/Rx.js"></script>
<script src="node_modules/angular2/bundles/angular2.dev.js"></script>
<!-- #docregion router -->
<script src="node_modules/angular2/bundles/router.dev.js"></script>
<!-- #enddocregion router -->
<script>
System.config({packages: {app: {format: 'register', defaultExtension: 'js'}}});
System.import('app/main')
.then(null, console.error.bind(console));
</script>
</head>
<!-- #enddocregion head -->
<body>
<my-app>Loading...</my-app>
</body>
</html>

View File

@ -12,16 +12,8 @@
</head>
<body>
<!-- #docregion import-angular -->
<!-- #1. add the system.js and angular libraries -->
<script src="node_modules/es6-shim/es6-shim.min.js"></script>
<script src="node_modules/systemjs/dist/system-polyfills.js"></script>
<script src="node_modules/angular2/es6/dev/src/testing/shims_for_IE.js"></script>
<script src="node_modules/angular2/bundles/angular2-polyfills.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="node_modules/rxjs/bundles/Rx.js"></script>
<script src="node_modules/angular2/bundles/angular2.dev.js"></script>
<!-- #enddocregion import-angular -->
<!-- #docregion promise-all -->
<script>
@ -36,7 +28,7 @@
// #3. Import the spec files explicitly
Promise.all([
System.import('app/hero.spec'),
System.import('app/init-caps-pipe.spec')
System.import('app/my-uppercase.pipe.spec')
])
// #4. wait for all imports to load ...

View File

@ -0,0 +1,50 @@
<!-- #docregion -->
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<title>Ng App Unit Tests</title>
<link rel="stylesheet" href="node_modules/jasmine-core/lib/jasmine-core/jasmine.css">
<script src="node_modules/jasmine-core/lib/jasmine-core/jasmine.js"></script>
<script src="node_modules/jasmine-core/lib/jasmine-core/jasmine-html.js"></script>
<script src="node_modules/jasmine-core/lib/jasmine-core/boot.js"></script>
</head>
<body>
<!-- #docregion import-angular -->
<!-- #1. add the system.js and angular libraries -->
<script src="node_modules/es6-shim/es6-shim.min.js"></script>
<script src="node_modules/systemjs/dist/system-polyfills.js"></script>
<script src="node_modules/angular2/es6/dev/src/testing/shims_for_IE.js"></script>
<script src="node_modules/angular2/bundles/angular2-polyfills.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="node_modules/rxjs/bundles/Rx.js"></script>
<script src="node_modules/angular2/bundles/angular2.dev.js"></script>
<!-- #enddocregion import-angular -->
<script>
// #2. Configure systemjs to use the .js extension
// for imports from the app folder
System.config({
packages: {
'app': {defaultExtension: 'js'}
}
});
// #3. Import the spec files explicitly
Promise.all([
System.import('app/hero.spec'),
System.import('app/my-uppercase.pipe.spec')
])
// #4. wait for all imports to load ...
// then re-execute `window.onload` which
// triggers the Jasmine test-runner start
// or explain what went wrong.
.then(window.onload)
.catch(console.error.bind(console));
</script>
</body>
</html>

View File

@ -1,44 +0,0 @@
<!-- #docregion -->
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<title>Ng App Unit Tests</title>
<link rel="stylesheet" href="../node_modules/jasmine-core/lib/jasmine-core/jasmine.css">
<script src="../node_modules/jasmine-core/lib/jasmine-core/jasmine.js"></script>
<script src="../node_modules/jasmine-core/lib/jasmine-core/jasmine-html.js"></script>
<script src="../node_modules/jasmine-core/lib/jasmine-core/boot.js"></script>
</head>
<body>
<!-- #1. add the system.js and angular libraries -->
<script src="../node_modules/systemjs/dist/system.src.js"></script>
<script src="../node_modules/angular2/bundles/angular2.dev.js"></script>
<script>
// #2. Configure systemjs to use the .js extension
// for imports from the app folder
System.config({
packages: {
'app': {defaultExtension: 'js'}
}
});
// #3. Import the spec files explicitly
Promise.all([
System.import('app/hero.spec'),
System.import('app/init-caps-pipe.spec'),
System.import('app/hero.service.no-ng.1.spec')
])
// #4. wait for all imports to load ...
// then re-execute `window.onload` which
// triggers the Jasmine test-runner start
// or explain what went wrong.
.then(window.onload)
.catch(console.error.bind(console));
</script>
</body>
</html>

View File

@ -9,37 +9,35 @@
<script src="../node_modules/jasmine-core/lib/jasmine-core/jasmine.js"></script>
<script src="../node_modules/jasmine-core/lib/jasmine-core/jasmine-html.js"></script>
<script src="../node_modules/jasmine-core/lib/jasmine-core/boot.js"></script>
<script src="../node_modules/systemjs/dist/system.src.js"></script>
<script src="../node_modules/angular2/bundles/angular2.dev.js"></script>
<script src="../node_modules/angular2/bundles/testing.js"></script>
<script src="../node_modules/zone.js/dist/jasmine-patch.js"></script>
</head>
<body>
<!-- #1. add the system.js and angular libraries -->
<script src="../node_modules/systemjs/dist/system.src.js"></script>
<script src="../node_modules/angular2/bundles/angular2.dev.js"></script>
<script>
(function() {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 100;
// #2. Configure systemjs to use the .js extension
// for imports from the app folder
System.config({
packages: {
'app': {defaultExtension: 'js'}
}
});
System.config({
packages: {
'app': {defaultExtension: 'js'},
'test-helpers': {defaultExtension: 'js'}
}
});
// #3. Import the spec files explicitly
Promise.all([
System.import('app/hero.spec'),
System.import('app/init-caps-pipe.spec'),
System.import('app/hero.service.no-ng.1.spec')
])
var imports = [
'app/hero.spec',
'app/init-caps-pipe.spec',
'app/hero.service.no-ng.spec',
'app/hero.service.ng.spec',
].map(function(spec) {return System.import(spec);});
Promise.all(imports)
.then(window.onload) // re-execute Jasmine's buildup
.catch(console.error.bind(console));
})();
// #4. wait for all imports to load ...
// then re-execute `window.onload` which
// triggers the Jasmine test-runner start
// or explain what went wrong.
.then(window.onload)
.catch(console.error.bind(console));
</script>
</body>

View File

@ -0,0 +1,46 @@
<!-- #docregion -->
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<title>Ng App Unit Tests</title>
<link rel="stylesheet" href="../node_modules/jasmine-core/lib/jasmine-core/jasmine.css">
<script src="../node_modules/jasmine-core/lib/jasmine-core/jasmine.js"></script>
<script src="../node_modules/jasmine-core/lib/jasmine-core/jasmine-html.js"></script>
<script src="../node_modules/jasmine-core/lib/jasmine-core/boot.js"></script>
<script src="../node_modules/systemjs/dist/system.src.js"></script>
<script src="../node_modules/angular2/bundles/angular2.dev.js"></script>
<script src="../node_modules/angular2/bundles/testing.js"></script>
<script src="../node_modules/zone.js/dist/jasmine-patch.js"></script>
</head>
<body>
<script>
(function() {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 100;
System.config({
packages: {
'app': {defaultExtension: 'js'},
'test-helpers': {defaultExtension: 'js'}
}
});
var imports = [
'app/hero.spec',
'app/init-caps-pipe.spec',
'app/hero.service.no-ng.spec',
'app/hero.service.ng.spec',
].map(function(spec) {return System.import(spec);});
Promise.all(imports)
.then(window.onload) // re-execute Jasmine's buildup
.catch(console.error.bind(console));
})();
</script>
</body>
</html>

View File

@ -17,15 +17,16 @@ a(id="top")
1. [The Application Under Test](#aut)
1. [Test a class](#first-app-tests)
- test a simple application class outside of Angular
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. [Test a Pipe](#pipe-testing)
- test a simple Angular Pipe class
- add the Angular 2 library to the test harness
- load multiple test files using system.js
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
@ -79,14 +80,14 @@ a(href="#top").to-top Back to top
.l-hr
a(id="first-app-tests")
:marked
# Testing a Class
# First app test
!= partial("../testing/first-app-tests")
a(href="#top").to-top Back to top
.l-hr
a(id="pipe-testing")
:marked
# Testing an Angular Pipe
# Pipe driven development
!= partial("../testing/testing-an-angular-pipe")
a(href="#top").to-top Back to top

View File

@ -1,33 +1,10 @@
include ../_util-fns
:marked
Well need an Angular application to test, one as simple as possible while having all the angular features we want to test.
Well need an Angular application to test, one as simple as possible while having most of the angular features we want to test.
<!-- TODO We have such an app that you can download [here](./#). -->Its a one-screen variation on the “Tour of Heroes” that should be familiar to you as a reader of this Developers Guide.
Our test app displays a list of heroes - all favorites of the user named “Bongo”. It looks like this:
figure.image-display
img(src='/resources/images/devguide/application-under-test/bongos-heroes.png'
style="width:250px;" alt="Bongo's Heroes")
:marked
At the top is a master list of heroes; at the bottom the detail for the current hero. Click a hero in the list to change the current hero. Change the name in the textbox and that name updates everywhere. The *Update* button modifies the `Hero.name` in an arbitrary way and that change also propagates everywhere on screen. The *Delete* button deletes the hero from the list and a new hero becomes current. *Refresh* clears both the list and detail, then restores the original list of heroes.
<!-- TODO You can see a short video of the app in action [here](./#) -->
This simple app illustrates a number of Angular features that wed like to test.
- A simple service that presents the `username` (“Bongo”)
- A dataservice that fetches and caches the list of heroes.
- The dataservice depends in turn on another “backend” service that handles the interaction with a remote web api
- A master `HeroesComponent` presents the list
- The master communicates with a detail component `HeroDetailComponent` about the current hero both through an attribute and an event.
- The details template is nested within the master components template.
- The `name` textbox illustrates two-way databinding
- The update button demonstrates that a programmatic change to the databound model propagates to both component views
- The delete button triggers an event that is caught by the parent component
<!-- TODO - [TBD: need to add a filter and a directive to this sample] -->
<!-- TODO - [TBD: need to shoehorn the router in somehow] -->
Well examine the implementation details as we evolve our tests.
What better app than our own [The Tour of Heroes](../tutorial/toh-pt5)? We're already quite familiar with it and it fits our criteria, so let's try to test what we've done there.
We might end up modifying it a bit, because it doesn't have everything we want to test, but it's the perfect starting point.
Create a copy of the Tour of Heroes app so that we can fiddle without fear.

View File

@ -3,7 +3,7 @@ include ../_util-fns
:marked
In this chapter we'll setup the environment for testing our sample application and write a few easy Jasmine tests of the app's simplest parts.
We'll learn:
- to test one of our application classes
- to test one of our application files
- why we prefer our test files to be next to their corresponding source files
- to run tests with an `npm` command
- load the test file with SystemJS
@ -21,9 +21,7 @@ include ../_util-fns
:marked
## Create the test-runner HTML
Step away from the Jasmine 101 folder and turn to the root folder of the application that we downloaded in the previous chapter.
Locate the `src` folder that contains the application `index.html`
Locate the folder that contains the application `index.html` for your testing copy of Tour of Heroes.
Create a new, sibling HTML file, ** `unit-tests.html` ** and copy over the same basic material from the `unit-tests.html` in the [Jasmine 101](./jasmine-testing-101.html) chapter.
@ -36,10 +34,6 @@ include ../_util-fns
:marked
## Update `package.json` for testing
We'll assume that the application has `package.json` file that looks more or less like
the one we prescribed in the in the "Install npm packages locally" section of the
[QuickStart](../quickstart.html).
We must install the Jasmine package as well:
pre.prettyprint.lang-bash
@ -50,17 +44,19 @@ pre.prettyprint.lang-bash
:marked
Let's make one more change to the `package.json` script commands.
**Open the `package.json` ** and scroll to the `scripts` node. Look for the command named `test`. Change it to:
**Open the `package.json` ** and scroll to the `scripts` node and add in a new one:
"test": "live-server --open=src/unit-tests.html"
code-example(format="").
"test": "live-server --open=unit-tests.html"
:marked
That command will launch `live-server` and open a browser to the `unit-tests.html` page we just wrote.
.l-main-section
:marked
## First app tests
We can start testing *some* of our app right away. For example, we can test the `Hero` class:
We can start testing *some* of our app right away. For example, we can test the `Hero` interface:
+makeExample('testing/ts/app/hero.ts')
@ -104,7 +100,7 @@ pre.prettyprint.lang-bash
:marked
## First spec file
**Create** a new file, ** `hero.spec.ts` ** in `src/app` next to `hero.ts`.
**Create** a new file, ** `hero.spec.ts` ** in `app` next to `hero.ts`.
Notice the ".spec" suffix in the test file's filename, appended to the name of the file holding the application part we're testing.
@ -120,7 +116,7 @@ pre.prettyprint.lang-bash
We have an `import {Hero} from './hero' ` statement.
If we forgot this import, a TypeScript-aware editor would warn us, with a squiggly red underline, that it can't find the definition of the `Hero` class.
If we forgot this import, a TypeScript-aware editor would warn us, with a squiggly red underline, that it can't find the definition of the `Hero` interface.
### Update unit-tests.html
@ -150,7 +146,7 @@ code-example(format="" language="html").
The immediate cause of the error is the `export` statement in `hero.ts`.
That error was there all along.
It wasn't a problem until we tried to `import` the `Hero` class in our tests.
It wasn't a problem until we tried to `import` the `Hero` interface in our tests.
Our test environment lacks support for module loading.
Apparently we can't simply load our application and test scripts like we do with 3rd party JavaScript libraries.
@ -211,7 +207,7 @@ figure.image-display
:marked
## What's Next?
We are able to test a part of our application with simple Jasmine tests.
The part was a stand-alone class that made no mention or use of Angular.
The part was a stand-alone interface that made no mention or use of Angular.
That's not rare but it's not typical either.
Most of our application parts make some use of the Angular framework.

View File

@ -1,26 +1,45 @@
include ../_util-fns
:marked
Well test an Angular pipe in this chapter
Well test an Angular pipe in this chapter.
An Angular pipe is a declarative way in HTML to transform some input into some displayable output.
We don't have a pipe though, since in Tour of Heroes we didn't create any pipes. It uses a pipe though, the `uppercase` pipe that comes with Angular 2.
We can make our own `my-uppercase` pipe that does exactly the same as the `uppercase` pipe and test that.
Since we're getting ready to write some code we want to test, let's take this opportunity to talk just a little bit about [Test Driven Development](https://en.wikipedia.org/wiki/Test-driven_development). There's a lot written about this topic so we don't want to have an exhaustive description here, but rather a practical application.
We already know *exactly* what we want the `uppercase` pipe to do. We could say our ...expectations... of it are very well defined.
We always use expectations our expectations to guide development, but sometimes it's hard to see the forest for the trees when we're right in the middle of coding. This is especially evident in larger tasks.
So one thing we can do is put those expectations down as cold hard test code. We were going to test things manually anyway, so doing it *before* we have even one line of code isn't going to hurt.
Worst thing that can happen is have that test fail, but on the way to fixing it we'll end up creating our pipe. So in a sense, the failing test will *tell you what it wants* to pass.
We're just putting down expectations, nothing more. If we were to put them down on paper, they would look like this:
Well look at our apps custom `InitCapsPipe` that converts a string of words into a string of capitalized words.
We use it our `hero-detail.component.html` template to turn a hero name like “eeny weenie” into “Eeny Weenie”
+makeExample('testing/ts/app/hero-detail.component.html', 'pipe-usage')
:marked
The code for `InitCapsPipe` in `init-caps-pipe.ts` is quite brief:
+makeExample('testing/ts/app/init-caps-pipe.ts')
```
MyUppercasePipe
transforms "abc" to "ABC"
transforms "abc def" to "ABC DEF"
leaves "ABC DEF" unchanged
```
All we need to know to put down our expectations as code is how a pipe class looks like from the outside. From the [pipe developer guide](pipes#custom-pipes) we know that a pipe implements a `transform` method.
Putting it down as Jasmine expectations, they would look something like this:
+makeExample('testing/ts/app/my-uppercase.pipe.spec.ts', 'expectations')
:marked
In this chapter we will:
- add the Angular 2 library to our test harness
- test this custom Angular pipe class
- 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
.callout.is-helpful
header Prior Knowledge
@ -31,53 +50,16 @@ include ../_util-fns
the [Tour of Heroes](../tutorial/) tutorial
such as <code>npm</code>, <code>gulp</code>, and <code>live-server</code>.
:marked
## Add the Angular library
Looking back at `unit-tests.html` we realize that we have not loaded the Angular library.
Yet we were able to load and test the applications `Hero` class.
**We were lucky!** The `Hero` class has no dependence on Angular.
If it had depended on Angular, wed still be staring at the Jasmine “big-time fail” screen:
figure.image-display
img(src='/resources/images/devguide/testing-an-angular-pipe/big-time-fail-screen.png'
style="width:400px;" alt="Jasmine's' big time fail screen")
:marked
If we then opened the browsers Developer Tools (F12, Ctrl-Shift-I) and looked
in the console window, we would see that SystemJS
tried to load Angular and couldn't find it.
code-example(format="" language="html" escape="html").
GET http://127.0.0.1:8080/src/angular2/core 404 (Not Found)
:marked
We are writing an Angular application afterall and
we were going to need Angular sooner or later. That time has come.
The `InitCapsPipe` depends on Angular as is clear in the first few lines:
+makeExample('testing/ts/app/init-caps-pipe.ts', 'depends-on-angular')(format=".")
:marked
**Open** `unit-tests.html`
**Find** the `src="../node_modules/systemjs/dist/system.src.js"></script>`
**Replace** Step #1 with these two scripts:
+makeExample('testing/ts/unit-tests-4.html', 'import-angular')(format=".")
:marked
## Add another spec file
**Create** an `init-caps-pipe.spec.ts` next to `init-caps-pipes.ts` in `src/app`
**Create** a `my-uppercase.pipe.spec.ts` in `app/`.
**Stop and restart the TypeScript compiler** to ensure we compile the new file.
**Add** the following lines of rather obvious Jasmine test code
**Add** the following lines of rather obvious Jasmine test code.
+makeExample('testing/ts/app/init-caps-pipe.spec.ts', 'base-pipe-spec')
+makeExample('testing/ts/app/my-uppercase.pipe.spec.ts', 'base-pipe-spec', 'app/my-uppercase.pipe.spec.ts')
:marked
Note that each test is short (one line in our case).
@ -95,7 +77,7 @@ code-example(format="" language="html" escape="html").
Open `unit-tests.html`. Find `System.import('app/hero.spec')`.
Hmm. We cant just add `System.import('app/init-caps-pipe.spec')`.
Hmm. We cant just add `System.import('app/my-uppercase.pipe.spec')`.
The first `System.import` returns a promise as does this second import.
We cant run any of the Jasmine tests until **both imports are finished**.
@ -105,19 +87,109 @@ code-example(format="" language="html" escape="html").
+makeExample('testing/ts/unit-tests-4.html', 'promise-all')(format=".")
:marked
Try it. The browser should refresh and show
figure.image-display
img(src='/resources/images/devguide/testing-an-angular-pipe/5-specs-0-failures.png'
style="width:400px;" alt="import promises 5 specs, 0 failures")
:marked
We have a pattern for adding new tests.
In future, when we add a new spec, we add another `System.import('app/some.spec')` to
the array argument passed to `Promise.all`.
Try it. The browser should refresh and show the following in the console:
code-example(format="" language="html" escape="html").
GET http://localhost:8080/app/my-uppercase.pipe.js 404 (Not Found)
:marked
Our test failed, as expected. We're importing something that doesn't exist and our test fails saying that. All is going according to plan.
:marked
## The pipe, if you please
The test is asking for a pipe, and we shall deliver.
**Create** a `my-uppercase.pipe.ts` in `app/`.
**Stop and restart the TypeScript compiler** to ensure we compile the new file.
**Add** a basic pipe that doesn't do anything. We know how to make strings uppercase, but we since we're letting the test take the lead let's wait for it to tell us what's next. Maybe it'll surprise us.
+makeExample('testing/ts/app/my-uppercase.pipe.1.ts', null, 'app/my-uppercase.pipe.ts')
:marked
Reload our test page and...
code-example(format="" language="html" escape="html").
GET http://localhost:8080/angular2/core 404 (Not Found)
:marked
## The Angular library, if you please
Looking back at `unit-tests.html` we realize that we have not loaded the Angular library.
Yet we were able to load and test the applications `Hero` interface.
**We were lucky!** The `Hero` interface has no dependence on Angular.
If it had depended on Angular, wed still be staring at the Jasmine “big-time fail” screen:
figure.image-display
img(src='/resources/images/devguide/testing-an-angular-pipe/big-time-fail-screen.png'
alt="Jasmine's' big time fail screen")
:marked
We are writing an Angular application after all and
we were going to need Angular sooner or later. That time has come.
`MyUppercasePipe` depends on Angular as is clear in the first few lines:
+makeExample('testing/ts/app/my-uppercase.pipe.ts', 'depends-on-angular')(format=".")
:marked
**Open** `unit-tests.html`
**Find** the `<!-- #1. add the system.js library -->` comment.
**Replace** the scripts tags beneath it with the all the needed angular scripts:
+makeExample('testing/ts/unit-tests-5.html', 'import-angular')(format=".")
:marked
We should now be ready to see our 3 expectations fail when reloading our test page.
figure.image-display
img(src='/resources/images/devguide/testing-an-angular-pipe/two-failures.png' alt="2 failed tests")
:marked
## Uppercase, if you please
The first two tests that passed were our old `hero` interface tests, so it makes sense that those passed. Of our three new expectations, one still passed though.
```
MyUppercasePipe
transforms "abc" to "ABC"
transforms "abc def" to "ABC DEF"
leaves "ABC DEF" unchanged
```
Ah but of course! Our simple pipe doesn't transform the input at all, and the third test expected
input to not be changed.
All we have to do now is actually transform text to uppercase in our pipe.
+makeExample('testing/ts/app/my-uppercase.pipe.ts', 'uppercase')(format=".")
:marked
Are we done now?
figure.image-display
img(src='/resources/images/devguide/testing-an-angular-pipe/zero-failures.png' alt="0 failed tests")
:marked
The glorious green is back with us again!
We tried a bit of test driven development and it seems to have guided us to success.
But it's not always feasible. For instance, sometimes we need to write tests for existing functionality, like what we're about to do with the rest of Tour of Heroes.
If we are writing new code though, writing tests might just be what we need to help us track our progress and keep the end result in sight at all times.
:marked
## Whats Next?
Now we can test parts of our application that we *load* asynchronously with system.js.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB