277 lines
		
	
	
		
			8.8 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
		
		
			
		
	
	
			277 lines
		
	
	
		
			8.8 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
|  | ///// 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); | ||
|  |     }); | ||
|  |   }) | ||
|  | } |