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);
 | 
						|
    });
 | 
						|
  })
 | 
						|
}
 |