chore(testing): add testing files to examples

This commit is contained in:
Filipe Silva 2016-03-17 14:07:54 +00:00 committed by Ward Bell
parent c1ea652bc9
commit 11160f09ab
45 changed files with 2488 additions and 253 deletions

View File

@ -17,7 +17,7 @@
<script src="node_modules/angular2/bundles/angular2.dev.js"></script>
<script>
System.config({
packages: {
packages: {
app: {
format: 'register',
defaultExtension: 'js'

View File

@ -0,0 +1 @@
**/*.js

View File

@ -0,0 +1,20 @@
// #docplaster
// #docregion it
it('true is true', () => expect(true).toEqual(true));
// #enddocregion it
// #docregion describe
describe('1st tests', () => {
it('true is true', () => expect(true).toEqual(true));
// #enddocregion describe
// #docregion another-test
it('null is not the same thing as undefined',
() => expect(null).not.toEqual(undefined)
);
// #enddocregion another-test
// #docregion describe
});
// #enddocregion describe

View File

@ -0,0 +1,14 @@
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

@ -0,0 +1,11 @@
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,16 @@
// @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

@ -0,0 +1,3 @@
.hero-detail div {padding:0.2em;}
.hero-detail div input {position: absolute; left:9em; }
.hero-id {position: absolute; left:7.5em; }

View File

@ -0,0 +1,24 @@
<!-- #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>

View File

@ -0,0 +1,218 @@
///// 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 = '<button (click)="onDelete()">Delete</button>';
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<Hero>((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 = <Promise<Hero>> 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 =
`<div>
<button id="update" (click)="onUpdate()" [disabled]="!hero">Update</button>
<input [(ngModel)]="hero.name"/>
</div>`
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 = `<input [(ngModel)]="hero.name"/>`
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}} - <input [(ngModel)]="hero.name"/>`
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');
});
}));
});
});

View File

@ -0,0 +1,35 @@
import {Component, Directive, EventEmitter , ElementRef} from 'angular2/core';
import {Hero} from './hero';
import {InitCapsPipe} from './init-caps-pipe';
@Directive({selector: 'button'})
class DecoratorDirective {
constructor(el: ElementRef){
console.log(el)
}
}
@Component({
selector: 'my-hero-detail',
templateUrl: 'app/hero-detail.component.html',
inputs: ['hero', 'userName'], // inputs
outputs: ['delete'], // outputs
directives: [DecoratorDirective],
styleUrls: ['app/hero-detail.component.css'],
pipes: [InitCapsPipe]
})
export class HeroDetailComponent {
hero: Hero;
delete = new EventEmitter();
onDelete() { this.delete.next(this.hero) }
onUpdate() {
if (this.hero) {
this.hero.name += 'x';
}
}
userName: string;
}

View File

@ -0,0 +1,144 @@
///// 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, '<button (click)="onDelete()" [disabled]="!hero">Delete</button>')
.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: `<my-hero-detail [hero]="currentHero" [user-name]="userName" (delete)="onDelete()"></my-hero-detail>`,
directives: [HeroDetailComponent]
})
class TestWrapper {
currentHero = new Hero(42, 'Cat Woman');
userName = 'Sally';
testCallback() {} // monkey-punched in a test
onDelete() { this.testCallback(); }
}
@View({
template: `
<div>
<h2>{{hero.name}} | {{userName}}</h2>
<button id="delete" (click)="onDelete()" [disabled]="!hero">Delete</button>
<button id="update" (click)="onUpdate()" [disabled]="!hero">Update</button>
<div id="id">{{hero.id}}</div>
<input [(ngModel)]="hero.name"/>
</div>`,
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;
}

View File

@ -0,0 +1,198 @@
// 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<Hero[]>(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);
}

View File

@ -0,0 +1,250 @@
/*
* 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); // dont 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 = <BackendService>{
// return a promise for fake heroes that resolves as quickly as possible
fetchAllHeroesAsync: () => Promise.resolve<Hero[]>(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 = <BackendService>{
// return a promise for fake heroes that resolves as quickly as possible
fetchAllHeroesAsync: () => Promise.resolve<Hero[]>(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 = <BackendService>{
// return a promise for fake heroes that resolves as quickly as possible
fetchAllHeroesAsync: () => Promise.resolve<Hero[]>(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 = <BackendService>{
// return a promise for fake heroes that resolves as quickly as possible
fetchAllHeroesAsync: () => Promise.resolve<Hero[]>(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 = <BackendService>{
// return a promise for fake heroes that resolves as quickly as possible
fetchAllHeroesAsync: () => Promise.resolve<Hero[]>(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 = <BackendService>{
// return a promise for fake heroes that resolves as quickly as possible
fetchAllHeroesAsync: () => Promise.resolve<Hero[]>(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 = <BackendService> {
// return a promise for fake heroes that resolves as quickly as possible
fetchAllHeroesAsync: () => Promise.resolve<Hero[]>(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<Hero[]>(heroData.map(h => h.clone()));
}

View File

@ -0,0 +1,150 @@
// 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<Hero[]>(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 <Promise<Hero[]> because of TS typing bug.
fetchAllHeroesAsync = () =>
<Promise<Hero[]>><any>Promise.reject(testError);
}

View File

@ -0,0 +1,33 @@
//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';
@Injectable()
export class HeroService {
heroes: Hero[] = []; // cache of heroes
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[]>;
}

View File

@ -0,0 +1,50 @@
// #docregion
// #docplaster
// #docregion base-hero-spec
import {Hero} from './hero';
describe('Hero', () => {
it('has name given in the constructor', () => {
let hero = new Hero(1, 'Super Cat');
expect(hero.name).toEqual('Super Cat');
});
it('has id given in the constructor', () => {
let hero = new Hero(1, '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

View File

@ -0,0 +1,19 @@
// #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; }
}

View File

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

@ -0,0 +1,18 @@
.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: lightblue; color: #369; }
.message {padding: 0.4em 0; font-size: 20px; color: #888}

View File

@ -0,0 +1,17 @@
<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>
</div>

View File

@ -0,0 +1,276 @@
///// 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 = () => (<any>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 = `<h1>{{userName}}'s Heroes</h1>`;
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 = <jasmine.Spy><any> 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 = <jasmine.Spy><any> 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 =
'<ul><li *ngFor="#h of heroes">{{h.name}}</li></ul>';
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 =
'<ul><li *ngFor="#h of heroes">{{h.name}}</li></ul>';
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 =
`<ul><li *ngFor="#hero of heroes"
${ngClass}
(click)="onSelect(hero)">
({{hero.id}}) {{hero.name}}
</li></ul>`;
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);
});
})
}

View File

@ -0,0 +1,229 @@
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 = () => (<any>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 = <any> 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 = <jasmine.Spy><any> service.refresh;
expect(spy.calls.count()).toEqual(0);
});
it('onInit calls HeroService.refresh', () => {
let spy = <jasmine.Spy><any> 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);
});
}
}

View File

@ -0,0 +1,53 @@
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';
@Component({
selector: 'my-heroes',
templateUrl: 'app/heroes.component.html',
directives: [HeroDetailComponent],
styleUrls: ['app/heroes.component.css']
})
export class HeroesComponent implements OnInit {
heroes: Hero[] = [];
currentHero: Hero;
userName: string;
constructor(private _heroService: HeroService, private _user: User) {
this.userName = this._user.name || 'someone';
}
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.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));
}
}

View File

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

@ -0,0 +1,15 @@
// #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,70 @@
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));

View File

@ -0,0 +1,18 @@
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);
});
});

View File

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

View File

@ -0,0 +1,27 @@
<!DOCTYPE html>
<html>
<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>
<body>
<my-heroes></my-heroes>
<script>
System.config({
packages: {
'app': {defaultExtension: 'js'}
}
});
System.import('app/bootstrap.js');
</script>
</body>
</html>

View File

@ -0,0 +1,18 @@
/////// 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");
}

View File

@ -0,0 +1,103 @@
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<any>;
type PromiseLikeTcbTestFn = (tcb: TestComponentBuilder, ...args:any[]) => PromiseLike<any>;
/** 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<any>): FunctionWithParamTokens;
export function injectTcb(dependencies: any[], testFn: PromiseLikeTcbTestFn): FunctionWithParamTokens;
export function injectTcb(dependencies: any[] | PromiseLikeTcbTestFn, testFn?: PromiseLikeTcbTestFn) {
if (typeof dependencies === 'function' ){
testFn = <PromiseLikeTcbTestFn>dependencies;
dependencies = [];
}
return injectAsync([TestComponentBuilder, ...(<any[]>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);
});
}

View File

@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "es5",
"module": "system",
"moduleResolution": "node",
"sourceMap": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"removeComments": false,
"noImplicitAny": true,
"suppressImplicitAnyIndexErrors": true
},
"exclude": [
"node_modules",
"typings/main",
"typings/main.d.ts"
]
}

View File

@ -0,0 +1,28 @@
<!-- #docregion -->
<!-- #docplaster -->
<!-- #docregion no-script -->
<!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>
<!-- #docregion body -->
<body>
<!-- #enddocregion no-script -->
<!-- Unit Testing Chapter #1: Proof of life. -->
<script>
it('true is true', function(){ expect(true).toEqual(true); });
</script>
<!-- #docregion no-script -->
</body>
<!-- #enddocregion body -->
</html>
<!-- #enddocregion no-script -->

View File

@ -0,0 +1,21 @@
<!-- #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>
<!-- #docregion script -->
<script src="1st.spec.js"></script>
<!-- #enddocregion script -->
</head>
<body>
</body>
</html>

View File

@ -0,0 +1,27 @@
<!-- #docregion -->
<!-- #docplaster -->
<!-- #docregion test-runner-base -->
<!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>
<!-- #enddocregion test-runner-base -->
<!-- #docregion load-hero-and-spec -->
<script src="app/hero.js"></script>
<script src="app/hero.spec.js"></script>
<!-- #enddocregion load-hero-and-spec -->
<!-- #docregion test-runner-base -->
</body>
</html>
<!-- #enddocregion test-runner-base -->

View File

@ -0,0 +1,42 @@
<!-- #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>
<!-- #docregion systemjs -->
<body>
<!-- #1. add the system.js library -->
<script src="node_modules/systemjs/dist/system-polyfills.js"></script>
<script src="node_modules/systemjs/dist/system.src.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 file explicitly
System.import('app/hero.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>
<!-- #enddocregion systemjs -->
</html>

View File

@ -0,0 +1,52 @@
<!-- #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 -->
<!-- #docregion promise-all -->
<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')
])
// #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>
<!-- #enddocregion promise-all -->
</body>
</html>

View File

@ -0,0 +1,44 @@
<!-- #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

@ -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

@ -0,0 +1,57 @@
<!-- #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.1.spec',
'test-helpers/dom-setup', // Essential for specs that touch the DOM
'app/hero.service.no-ng.spec',
'app/hero.service.ng.spec',
'app/heroes.component.no-ng.spec',
'app/heroes.component.ng.spec',
'app/hero-detail.component.spec',
'@empty' // safe sentinel
].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

@ -4,14 +4,6 @@
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.
.alert.is-important
:marked
These testing chapters were written before the Angular 2 Beta release
and are scheduled for significant updates.
Much of the material remains accurate and relevant but references to
specific features of Angular 2 and the Angular 2 testing library
may not be correct. Please bear with us.
a(id="top")
:marked
@ -101,4 +93,4 @@ a(href="#top").to-top Back to top
.alert.is-important
:marked
The testing chapter is still under development.
Please bear with us as we both update and complete it.
Please bear with us as we both update and complete it.

View File

@ -26,24 +26,10 @@ include ../_util-fns
Locate the `src` folder that contains the application `index.html`
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.
+makeExample('testing/ts/unit-tests-2.html', 'test-runner-base', 'unit-tests.html')
```
<html>
<head>
<title>1st Jasmine 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>
</body>
</html>
```
:marked
We're picking up right where we left off. All we've done is change the title.
.l-main-section
@ -74,58 +60,16 @@ pre.prettyprint.lang-bash
:marked
## First app tests
Believe it or not … we could start testing *some* of our app right away. For example, we can test the `Hero` class:
```
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;
}
```
Let's add a couple of simple tests in the `<body>` element.
First, we'll load the JavaScript file that defines the `Hero` class.
code-example(format="" language="html").
&lt;!-- load the application's Hero definition -->
&lt;script src="app/hero.js">&lt;/script>
We can start testing *some* of our app right away. For example, we can test the `Hero` class:
+makeExample('testing/ts/app/hero.ts')
:marked
Next, we'll add an inline script element with the `Hero`tests themselves
Let's add a couple of simple tests in a new file.
```
<script>
// Demo only!
describe('Hero', function() {
it('has name given in the constructor', function() {
var hero = new Hero(1, 'Super Cat');
expect(hero.name).toEqual('Super Cat');
});
it('has the id given in the constructor', function() {
var hero = new Hero(1, 'Super Cat');
expect(hero.id).toEqual(1);
});
});
</script>
```
+makeExample('testing/ts/app/hero.spec.ts', 'base-hero-spec')
:marked
That's the basic Jasmine we learned back in "Jasmine 101".
Notice that we surrounded our tests with ** `describe('Hero')` **.
@ -133,32 +77,8 @@ code-example(format="" language="html").
**By convention, our test always begin with a `describe` that identifies the application part under test.**
The description should be sufficient to identify the tested application part and its source file. Almost any convention will do as long as you and your team follow it consistently and are never confused.
.l-main-section
:marked
## Run the tests
Open one terminal window and run the watching compiler command: `npm run tsc`
Open another terminal window and run live-server: `npm test`
The browser should launch and display the two passing tests:
figure.image-display
img(src='/resources/images/devguide/first-app-tests/passed-2-specs-0-failures.png' style="width:400px;" alt="Two passing tests")
.l-main-section
:marked
## Critique
Is this `Hero` class even worth testing? It's essentially a property bag with almost no logic. Maybe we should have tested the cloning feature. Maybe we should have tested id generation. We didn't bother because there wasn't much to learn by doing that.
It's more important to take note of the `//Demo only` comment in the `unit-tests.html`.
** We'll never write real tests in the HTML this way**. It's nice that we can write *some* of our application tests directly in the HTML. But dumping all of our tests into HTML is not sustainable and even if we didn't mind that approach, we could only test a tiny fraction of our app this way.
We need to relocate these tests to a separate file. Let's do that next.
But we haven't saved this test yet.
.l-main-section
:marked
@ -191,40 +111,23 @@ figure.image-display
.alert.is-important All of our unit test files follow this .spec naming pattern.
:marked
Move the tests we just wrote in`unit-tests.html` to `hero.spec.ts` and convert them from JavaScript into TypeScript:
Save the tests we just made in `hero.spec.ts`:
```
import {Hero} from './hero';
describe('Hero', () => {
it('has name given in the constructor', () => {
let hero = new Hero(1, 'Super Cat');
expect(hero.name).toEqual('Super Cat');
});
it('has id given in the constructor', () => {
let hero = new Hero(1, 'Super Cat');
expect(hero.id).toEqual(1);
});
})
```
+makeExample('testing/ts/app/hero.spec.ts', 'base-hero-spec')
:marked
### Import the part we're testing
During our conversion to TypeScript, we added an `import {Hero} from './hero' ` statement.
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.
TypeScript doesn't know what a `Hero` is. It doesn't know about the script tag back in the `unit-tests.html` that loads the `hero.js` file.
### Update unit-tests.html
Next we update the `unit-tests.html` with a reference to our new `hero.spec.ts` file. Delete the inline test code. The revised pertinent HTML looks like this:
code-example(format="" language="html").
&lt;script src="app/hero.js">&lt;/script>
&lt;script src="app/hero.spec.js">&lt;/script>
+makeExample('testing/ts/unit-tests-2.html', 'load-hero-and-spec')(format=".")
:marked
### Run and Fail
@ -239,7 +142,7 @@ figure.image-display
Open the browser's Developer Tools (F12, Ctrl-Shift-i). There's an error:
code-example(format="" language="html").
Uncaught ReferenceError: exports is not defined
Uncaught ReferenceError: System is not defined
.l-main-section
:marked
@ -250,7 +153,7 @@ code-example(format="" language="html").
It wasn't a problem until we tried to `import` the `Hero` class 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.
Apparently we can't simply load our application and test scripts like we do with 3rd party JavaScript libraries.
We are committed to module loading in our application.
Our app will call `import`. Our tests must do so too.
@ -265,33 +168,9 @@ code-example(format="" language="html").
These steps are all clearly visible, in exactly that order, in the following lines that
replace the `<body>` contents in `unit-tests.html`:
```
<body>
<!-- #1. add the system.js library -->
<script src="../node_modules/systemjs/dist/system.src.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 file explicitly
System.import('app/hero.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>
```
+makeExample('testing/ts/unit-tests-3.html', 'systemjs')(format=".")
:marked
Look in the browser window. Our tests pass once again.
figure.image-display

View File

@ -38,31 +38,15 @@ pre.prettyprint.lang-bash
:marked
Create a new file called`unit-tests.html` and enter the following:
```
<html>
<head>
<title>1st Jasmine 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>
</body>
</html>
```
+makeExample('testing/ts/unit-tests-0.html', 'no-script', 'unit-tests.html')
:marked
In the head we have three Jasmine scripts and one Jasmine css file. Thats the foundation for running any tests.
Well write our first test with inline JavaScript inside the body tag:
code-example(format="" language="html").
&lt;script>
it('true is true', function(){ expect(true).toEqual(true); });
&lt;/script>
+makeExample('testing/ts/unit-tests-0.html', 'body')(format='.')
:marked
Now open `unit-tests.html` in a browser and see the Jasmine HTML test output:
@ -85,14 +69,16 @@ figure.image-display
:marked
The test we wrote is valid TypeScript because any JavaScript is valid TypeScript. But lets make it more modern with an arrow function:
code-example(format="" ).
it('true is true', () => expect(true).toEqual(true));
+makeExample('testing/ts/1st.spec.ts', 'it', '1st.spec.ts')
:marked
Now modify `unit-tests.html` to load the script:
code-example(format="" language="html").
&lt;script src="1st.spec.js">&lt;/script>
+makeExample('testing/ts/unit-tests-1.html', 'script')
:marked
Hold on! We wrote a TypeScript file but were loading a JavaScript file?
Hold on! We wrote a TypeScript file but were loading a JavaScript file?
Thats a reminder that we need to compile our TypeScript test files as we do our TypeScript application files. Do that next.
@ -103,23 +89,12 @@ code-example(format="" language="html").
As weve seen before, we first have to tell the compiler how to compile our TypeScript files with
a ** `tsconfig.json` **.
We can copy one from an application we wrote previously and paste it into our src sub-folder.
We can copy one from the quickstart we wrote previously and paste it into our src sub-folder.
It should look something like this:
```
{
"compilerOptions": {
"target": "ES5",
"module": "commonjs",
"sourceMap": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true
},
"exclude": [
"node_modules"
]
}
```
+makeExample('testing/ts/tsconfig.1.json', null, 'tsconfig.json')
:marked
## Compile and Run
Compile in the terminal window using the npm script command
@ -157,13 +132,10 @@ pre.prettyprint.lang-bash
We should wrap this test into something that identifies the file. In Jasmine that “something” is a `describe` function. Every test file should have at least one `describe` that identifies the file holding the test(s).
Heres what our revised `1st.spec.ts` looks like when wrapped in a `describe`:
```
describe('1st tests', () => {
it('true is true', () => expect(true).toEqual(true));
+makeExample('testing/ts/1st.spec.ts', 'describe')
});
```
:marked
And heres how the test report displays it.
figure.image-display
@ -171,10 +143,9 @@ figure.image-display
:marked
Lets add another Jasmine test to `1st.spec.ts`
code-example(format="" ).
it('null is not the same thing as undefined',
() => expect(null).not.toEqual(undefined)
);
+makeExample('testing/ts/1st.spec.ts', 'another-test')(format=".")
:marked
You knew that right? Lets prove it with this test. The browser should refresh after you paste that test, and show:

View File

@ -9,25 +9,14 @@ include ../_util-fns
We use it our `hero-detail.component.html` template to turn a hero name like “eeny weenie” into “Eeny Weenie”
code-example(format="." language="html" escape="html").
<h2>{{hero.name | initCaps}} is {{userName}}'s current super hero!</h2>
+makeExample('testing/ts/app/hero-detail.component.html', 'pipe-usage')
:marked
The code for `InitCapsPipe` in `init-caps-pipe.ts` is quite brief:
```
import {Pipe, PipeTransform} from 'angular2/core';
@Pipe({ name: 'initCaps' })
export class InitCapsPipe implements PipeTransform{
transform(value: string) {
return value.toLowerCase().replace(/(?:^|\s)[a-z]/g, function(m) {
return m.toUpperCase();
});
}
}
```
+makeExample('testing/ts/app/init-caps-pipe.ts')
:marked
In this chapter we will:
- add the Angular 2 library to our test harness
- test this custom Angular pipe class
@ -67,21 +56,18 @@ code-example(format="" language="html" escape="html").
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:
code-example(format="").
import {Pipe, PipeTransform} from 'angular2/core';
+makeExample('testing/ts/app/init-caps-pipe.ts', 'depends-on-angular')(format=".")
@Pipe({ name: 'initCaps' })
export class InitCapsPipe implements PipeTransform { ... }
:marked
**Open** `unit-tests.html`
**Find** the `src="../node_modules/systemjs/dist/system.src.js"></script>`
**Replace** Step #1 with these two scripts:
code-example(format="" language="html").
&lt;!-- #1. add the system.js and angular libraries -->
&lt;script src="../node_modules/systemjs/dist/system.src.js">&lt;/script>
&lt;script src="../node_modules/angular2/bundles/angular2.dev.js">&lt;/script>
+makeExample('testing/ts/unit-tests-4.html', 'import-angular')(format=".")
:marked
## Add another spec file
@ -90,29 +76,10 @@ code-example(format="" language="html").
**Stop and restart the TypeScript compiler** to ensure we compile the new file.
**Add** the following lines of rather obvious Jasmine test code
```
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');
});
});
```
+makeExample('testing/ts/app/init-caps-pipe.spec.ts', 'base-pipe-spec')
:marked
Note that each test is short (one line in our case).
It has a clear label that accurately describes the test. And it makes exactly one expectation.
@ -135,12 +102,9 @@ code-example(format="" language="html").
Fortunately, we can create a new `Promise` that wraps both import promises and waits
for both to finish loading.
code-example(format="").
// #3. Import the spec files explicitly
Promise.all([
System.import('app/hero.spec'),
System.import('app/init-caps-pipe.spec')
])
+makeExample('testing/ts/unit-tests-4.html', 'promise-all')(format=".")
:marked
Try it. The browser should refresh and show