chore(testing): add testing files to examples
This commit is contained in:
parent
c1ea652bc9
commit
11160f09ab
|
@ -17,7 +17,7 @@
|
|||
<script src="node_modules/angular2/bundles/angular2.dev.js"></script>
|
||||
<script>
|
||||
System.config({
|
||||
packages: {
|
||||
packages: {
|
||||
app: {
|
||||
format: 'register',
|
||||
defaultExtension: 'js'
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
**/*.js
|
|
@ -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
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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]);
|
|
@ -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; }
|
|
@ -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; }
|
|
@ -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>
|
|
@ -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');
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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); // don’t know how many to expect yet
|
||||
});
|
||||
});
|
||||
|
||||
// better ... but not async!
|
||||
it('refresh promise returns expected # of heroes when fulfilled', () => {
|
||||
|
||||
heroData = [new Hero(1, 'Foo'), new Hero(2, 'Bar'), new Hero(3, 'Baz')];
|
||||
|
||||
let backend = <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()));
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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[]>;
|
||||
}
|
|
@ -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
|
|
@ -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; }
|
||||
}
|
|
@ -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; }
|
||||
}
|
|
@ -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}
|
|
@ -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>
|
|
@ -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);
|
||||
});
|
||||
})
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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));
|
|
@ -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);
|
||||
});
|
||||
|
||||
});
|
|
@ -0,0 +1,6 @@
|
|||
// imagine this is the result of a login
|
||||
export class User {
|
||||
id = 42;
|
||||
name = 'Bongo';
|
||||
email = 'bongo@amazing.io'
|
||||
};
|
|
@ -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>
|
|
@ -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");
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
}
|
|
@ -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"
|
||||
]
|
||||
}
|
|
@ -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 -->
|
|
@ -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>
|
|
@ -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 -->
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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.
|
||||
|
|
|
@ -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").
|
||||
<!-- load the application's Hero definition -->
|
||||
<script src="app/hero.js"></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").
|
||||
<script src="app/hero.js"></script>
|
||||
<script src="app/hero.spec.js"></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
|
||||
|
|
|
@ -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. That’s the foundation for running any tests.
|
||||
|
||||
We’ll write our first test with inline JavaScript inside the body tag:
|
||||
|
||||
code-example(format="" language="html").
|
||||
<script>
|
||||
it('true is true', function(){ expect(true).toEqual(true); });
|
||||
</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 let’s 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").
|
||||
<script src="1st.spec.js"></script>
|
||||
|
||||
+makeExample('testing/ts/unit-tests-1.html', 'script')
|
||||
|
||||
:marked
|
||||
Hold on! We wrote a TypeScript file but we’re loading a JavaScript file?
|
||||
Hold on! We wrote a TypeScript file but we’re loading a JavaScript file?
|
||||
|
||||
That’s 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 we’ve 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).
|
||||
|
||||
Here’s 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 here’s how the test report displays it.
|
||||
|
||||
figure.image-display
|
||||
|
@ -171,10 +143,9 @@ figure.image-display
|
|||
|
||||
:marked
|
||||
Let’s 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? Let’s prove it with this test. The browser should refresh after you paste that test, and show:
|
||||
|
||||
|
|
|
@ -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").
|
||||
<!-- #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>
|
||||
|
||||
+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
|
||||
|
||||
|
|
Loading…
Reference in New Issue