diff --git a/gulpfile.js b/gulpfile.js index 456d892b5c..36185cfb30 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -91,15 +91,12 @@ var _excludeMatchers = _excludePatterns.map(function(excludePattern){ var _exampleBoilerplateFiles = [ '.editorconfig', 'a2docs.css', - 'karma.conf.js', - 'karma-test-shim.js', 'package.json', 'styles.css', 'systemjs.config.js', 'tsconfig.json', 'tslint.json', - 'typings.json', - 'wallaby.js' + 'typings.json' ]; var _exampleDartWebBoilerPlateFiles = ['a2docs.css', 'styles.css']; @@ -636,7 +633,7 @@ gulp.task('build-dart-api-docs', ['_shred-api-examples', 'dartdoc'], function() // Using the --build flag will use systemjs.config.plunker.build.js (for preview builds) gulp.task('build-plunkers', ['_copy-example-boilerplate'], function() { regularPlunker.buildPlunkers(EXAMPLES_PATH, LIVE_EXAMPLES_PATH, { errFn: gutil.log, build: argv.build }); - return embeddedPlunker.buildPlunkers(EXAMPLES_PATH, LIVE_EXAMPLES_PATH, { errFn: gutil.log, build: argv.build }); + return embeddedPlunker.buildPlunkers(EXAMPLES_PATH, LIVE_EXAMPLES_PATH, { errFn: gutil.log, build: argv.build, targetSelf: argv.targetSelf }); }); gulp.task('build-dart-cheatsheet', [], function() { diff --git a/public/docs/_examples/karma-test-shim.js b/public/docs/_examples/karma-test-shim.js deleted file mode 100644 index 5fb73d0301..0000000000 --- a/public/docs/_examples/karma-test-shim.js +++ /dev/null @@ -1,50 +0,0 @@ -// /*global jasmine, __karma__, window*/ -Error.stackTraceLimit = Infinity; -jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000; - -__karma__.loaded = function () { -}; - -function isJsFile(path) { - return path.slice(-3) == '.js'; -} - -function isSpecFile(path) { - return /\.spec\.js$/.test(path); -} - -function isBuiltFile(path) { - var builtPath = '/base/app/'; - return isJsFile(path) && (path.substr(0, builtPath.length) == builtPath); -} - -var allSpecFiles = Object.keys(window.__karma__.files) - .filter(isSpecFile) - .filter(isBuiltFile); - -System.config({ - baseURL: '/base', - packageWithIndex: true // sadly, we can't use umd packages (yet?) -}); - -System.import('systemjs.config.js') - .then(() => Promise.all([ - System.import('@angular/core/testing'), - System.import('@angular/platform-browser-dynamic/testing') - ])) - .then((providers) => { - var coreTesting = providers[0]; - var browserTesting = providers[1]; - coreTesting.TestBed.initTestEnvironment( - browserTesting.BrowserDynamicTestingModule, - browserTesting.platformBrowserDynamicTesting()); - }) - .then(function () { - // Finally, load all spec files. - // This will run the tests directly. - return Promise.all( - allSpecFiles.map(function (moduleName) { - return System.import(moduleName); - })); - }) - .then(__karma__.start, __karma__.error); diff --git a/public/docs/_examples/package.json b/public/docs/_examples/package.json index 30b6d93c00..ee913df8f7 100644 --- a/public/docs/_examples/package.json +++ b/public/docs/_examples/package.json @@ -60,6 +60,7 @@ "karma-cli": "^1.0.1", "karma-htmlfile-reporter": "^0.3.4", "karma-jasmine": "^1.0.2", + "karma-jasmine-html-reporter": "^0.2.2", "karma-phantomjs-launcher": "^1.0.2", "karma-sourcemap-loader": "^0.3.7", "karma-webpack": "^1.8.0", diff --git a/public/docs/_examples/testing/ts/1st-specs.html b/public/docs/_examples/testing/ts/1st-specs.html new file mode 100644 index 0000000000..098b607f81 --- /dev/null +++ b/public/docs/_examples/testing/ts/1st-specs.html @@ -0,0 +1,40 @@ + + + + + + + 1st Specs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/docs/_examples/testing/ts/1st-specs.plnkr.json b/public/docs/_examples/testing/ts/1st-specs.plnkr.json new file mode 100644 index 0000000000..945ec90060 --- /dev/null +++ b/public/docs/_examples/testing/ts/1st-specs.plnkr.json @@ -0,0 +1,12 @@ +{ + "description": "Testing - 1st.specs", + "files":[ + "browser-test-shim.js", + "styles.css", + + "app/1st.spec.ts", + "1st-specs.html" + ], + "main": "1st-specs.html", + "tags": ["testing"] +} diff --git a/public/docs/_examples/testing/ts/1st.spec.ts b/public/docs/_examples/testing/ts/1st.spec.ts deleted file mode 100644 index bcac7a1aa7..0000000000 --- a/public/docs/_examples/testing/ts/1st.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -// #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 diff --git a/public/docs/_examples/testing/ts/app-specs.html b/public/docs/_examples/testing/ts/app-specs.html new file mode 100644 index 0000000000..c51f6cdedc --- /dev/null +++ b/public/docs/_examples/testing/ts/app-specs.html @@ -0,0 +1,52 @@ + + + + + + + Sample App Specs + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/docs/_examples/testing/ts/app-specs.plnkr.json b/public/docs/_examples/testing/ts/app-specs.plnkr.json new file mode 100644 index 0000000000..afd126bb45 --- /dev/null +++ b/public/docs/_examples/testing/ts/app-specs.plnkr.json @@ -0,0 +1,23 @@ +{ + "description": "Testing - app.specs", + "files":[ + "browser-test-shim.js", + "systemjs.config.extras.js", + "styles.css", + + "app/**/*.css", + "app/**/*.html", + "app/**/*.ts", + "app/**/*.spec.ts", + + "testing/*.ts", + + "!app/main.ts", + "!app/bag/*.*", + "!app/1st.spec.ts", + + "app-specs.html" + ], + "main": "app-specs.html", + "tags": ["testing"] +} diff --git a/public/docs/_examples/testing/ts/app/1st.spec.ts b/public/docs/_examples/testing/ts/app/1st.spec.ts new file mode 100644 index 0000000000..63f1ab134c --- /dev/null +++ b/public/docs/_examples/testing/ts/app/1st.spec.ts @@ -0,0 +1,5 @@ +// #docplaster +// #docregion +describe('1st tests', () => { + it('true is true', () => expect(true).toBe(true)); +}); diff --git a/public/docs/_examples/testing/ts/app/about.component.ts b/public/docs/_examples/testing/ts/app/about.component.ts new file mode 100644 index 0000000000..b2690f5a93 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/about.component.ts @@ -0,0 +1,12 @@ +// #docregion +import { Component } from '@angular/core'; + +@Component({ + template: ` +

About

+ +

All about this sample

+ `, + styleUrls: ['app/shared/styles.css'] +}) +export class AboutComponent { } diff --git a/public/docs/_examples/testing/ts/app/app.component.css b/public/docs/_examples/testing/ts/app/app.component.css deleted file mode 100644 index 137e9be7be..0000000000 --- a/public/docs/_examples/testing/ts/app/app.component.css +++ /dev/null @@ -1,31 +0,0 @@ -/* #docplaster */ -/* #docregion css */ -h1 { - font-size: 1.2em; - color: #999; - margin-bottom: 0; -} -h2 { - font-size: 2em; - margin-top: 0; - padding-top: 0; -} -nav a { - padding: 5px 10px; - text-decoration: none; - margin-top: 10px; - display: inline-block; - background-color: #eee; - border-radius: 4px; -} -nav a:visited, a:link { - color: #607D8B; -} -nav a:hover { - color: #039be5; - background-color: #CFD8DC; -} -nav a.router-link-active { - color: #039be5; -} -/* #enddocregion css */ diff --git a/public/docs/_examples/testing/ts/app/app.component.html b/public/docs/_examples/testing/ts/app/app.component.html new file mode 100644 index 0000000000..3690f3cf11 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/app.component.html @@ -0,0 +1,10 @@ + + + + + + diff --git a/public/docs/_examples/testing/ts/app/app.component.router.spec.ts b/public/docs/_examples/testing/ts/app/app.component.router.spec.ts new file mode 100644 index 0000000000..36e34a983e --- /dev/null +++ b/public/docs/_examples/testing/ts/app/app.component.router.spec.ts @@ -0,0 +1,201 @@ +// For more examples: +// https://github.com/angular/angular/blob/master/modules/@angular/router/test/integration.spec.ts + +import { async, ComponentFixture, fakeAsync, TestBed, tick, +} from '@angular/core/testing'; + +import { RouterTestingModule } from '@angular/router/testing'; +import { SpyLocation } from '@angular/common/testing'; + +// tslint:disable:no-unused-variable +import { newEvent } from '../testing'; +// tslint:enable:no-unused-variable + +// r - for relatively obscure router symbols +import * as r from '@angular/router'; +import { Router, RouterLinkWithHref } from '@angular/router'; + +import { By } from '@angular/platform-browser'; +import { DebugElement, Type } from '@angular/core'; +import { Location } from '@angular/common'; + +import { AppModule } from './app.module'; +import { AppComponent } from './app.component'; +import { AboutComponent } from './about.component'; +import { DashboardHeroComponent } from './dashboard/dashboard-hero.component'; +import { TwainService } from './shared/twain.service'; + +let comp: AppComponent; +let fixture: ComponentFixture; +let page: Page; +let router: Router; +let location: SpyLocation; + +describe('AppComponent & RouterTestingModule', () => { + + beforeEach( async(() => { + TestBed.configureTestingModule({ + imports: [ AppModule, RouterTestingModule ] + }) + .compileComponents(); + })); + + it('should navigate to "Dashboard" immediately', fakeAsync(() => { + createComponent(); + expect(location.path()).toEqual('/dashboard', 'after initialNavigation()'); + expectElementOf(DashboardHeroComponent); + })); + + it('should navigate to "About" on click', fakeAsync(() => { + createComponent(); + // page.aboutLinkDe.triggerEventHandler('click', null); // fails + // page.aboutLinkDe.nativeElement.dispatchEvent(newEvent('click')); // fails + page.aboutLinkDe.nativeElement.click(); // fails in phantom + + advance(); + expectPathToBe('/about'); + expectElementOf(AboutComponent); + + page.expectEvents([ + [r.NavigationStart, '/about'], [r.RoutesRecognized, '/about'], + [r.NavigationEnd, '/about'] + ]); + })); + + it('should navigate to "About" w/ browser location URL change', fakeAsync(() => { + createComponent(); + location.simulateHashChange('/about'); + // location.go('/about'); // also works ... except in plunker + advance(); + expectPathToBe('/about'); + expectElementOf(AboutComponent); + })); + + // Can't navigate to lazy loaded modules with this technique + xit('should navigate to "Heroes" on click', fakeAsync(() => { + createComponent(); + page.heroesLinkDe.nativeElement.click(); + advance(); + expectPathToBe('/heroes'); + })); + +}); + + +/////////////// +import { NgModuleFactoryLoader } from '@angular/core'; +import { SpyNgModuleFactoryLoader } from '@angular/router/testing'; + +import { HeroModule } from './hero/hero.module'; // should be lazy loaded +import { HeroListComponent } from './hero/hero-list.component'; + +let loader: SpyNgModuleFactoryLoader; + +///////// Can't get lazy loaded Heroes to work yet +xdescribe('AppComponent & Lazy Loading', () => { + + beforeEach( async(() => { + TestBed.configureTestingModule({ + imports: [ AppModule, RouterTestingModule ] + }) + .compileComponents(); + })); + + beforeEach(fakeAsync(() => { + createComponent(); + loader = TestBed.get(NgModuleFactoryLoader); + loader.stubbedModules = {expected: HeroModule}; + router.resetConfig([{path: 'heroes', loadChildren: 'expected'}]); + })); + + it('dummy', () => expect(true).toBe(true) ); + + + it('should navigate to "Heroes" on click', async(() => { + page.heroesLinkDe.nativeElement.click(); + advance(); + expectPathToBe('/heroes'); + expectElementOf(HeroListComponent); + })); + + xit('can navigate to "Heroes" w/ browser location URL change', fakeAsync(() => { + location.go('/heroes'); + advance(); + expectPathToBe('/heroes'); + expectElementOf(HeroListComponent); + + page.expectEvents([ + [r.NavigationStart, '/heroes'], [r.RoutesRecognized, '/heroes'], + [r.NavigationEnd, '/heroes'] + ]); + })); +}); + +////// Helpers ///////// + +/** Wait a tick, then detect changes */ +function advance(): void { + tick(); + fixture.detectChanges(); +} + +function createComponent() { + fixture = TestBed.createComponent(AppComponent); + comp = fixture.componentInstance; + + const injector = fixture.debugElement.injector; + location = injector.get(Location); + router = injector.get(Router); + router.initialNavigation(); + spyOn(injector.get(TwainService), 'getQuote') + .and.returnValue(Promise.resolve('Test Quote')); // fakes it + + advance(); + + page = new Page(); +} + +class Page { + aboutLinkDe: DebugElement; + dashboardLinkDe: DebugElement; + heroesLinkDe: DebugElement; + recordedEvents: any[] = []; + + // for debugging + comp: AppComponent; + location: SpyLocation; + router: Router; + fixture: ComponentFixture; + + expectEvents(pairs: any[]) { + const events = this.recordedEvents; + expect(events.length).toEqual(pairs.length, 'actual/expected events length mismatch'); + for (let i = 0; i < events.length; ++i) { + expect((events[i].constructor).name).toBe(pairs[i][0].name, 'unexpected event name'); + expect((events[i]).url).toBe(pairs[i][1], 'unexpected event url'); + } + } + + constructor() { + router.events.forEach(e => this.recordedEvents.push(e)); + const links = fixture.debugElement.queryAll(By.directive(RouterLinkWithHref)); + this.aboutLinkDe = links[2]; + this.dashboardLinkDe = links[0]; + this.heroesLinkDe = links[1]; + + // for debugging + this.comp = comp; + this.fixture = fixture; + this.router = router; + } +} + +function expectPathToBe(path: string, expectationFailOutput?: any) { + expect(location.path()).toEqual(path, expectationFailOutput || 'location.path()'); +} + +function expectElementOf(type: Type): any { + const el = fixture.debugElement.query(By.directive(type)); + expect(el).toBeTruthy('expected an element for ' + type.name); + return el; +} diff --git a/public/docs/_examples/testing/ts/app/app.component.spec.ts b/public/docs/_examples/testing/ts/app/app.component.spec.ts index f6c30b0e76..c9fd54535f 100644 --- a/public/docs/_examples/testing/ts/app/app.component.spec.ts +++ b/public/docs/_examples/testing/ts/app/app.component.spec.ts @@ -1,83 +1,119 @@ -/* tslint:disable:no-unused-variable */ -import { AppComponent } from './app.component'; - -import { By } from '@angular/platform-browser'; -import { DebugElement } from '@angular/core'; - -import { - async, inject +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { ComponentFixture, TestComponentBuilder } from '@angular/core/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { By } from '@angular/platform-browser'; -import { Hero, HeroService, MockHeroService } from './mock-hero.service'; +import { AppComponent } from './app.component'; +import { BannerComponent } from './banner.component'; +import { SharedModule } from './shared/shared.module'; -import { Router, MockRouter, - RouterLink, MockRouterLink, - RouterOutlet, MockRouterOutlet } from './mock-router'; +import { Router, FakeRouter, FakeRouterLinkDirective, FakeRouterOutletComponent +} from '../testing'; -describe('AppComponent', () => { - let fixture: ComponentFixture; - let comp: AppComponent; - beforeEach(async(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { - tcb - .overrideDirective(AppComponent, RouterLink, MockRouterLink) - .overrideDirective(AppComponent, RouterOutlet, MockRouterOutlet) - .overrideProviders(AppComponent, [ - { provide: HeroService, useClass: MockHeroService}, - { provide: Router, useClass: MockRouter}, - ]) - .createAsync(AppComponent) - .then(fix => { - fixture = fix; - comp = fixture.debugElement.componentInstance; - }); - }))); +let comp: AppComponent; +let fixture: ComponentFixture; + +describe('AppComponent & TestModule', () => { + beforeEach( async(() => { + TestBed.configureTestingModule({ + declarations: [ + AppComponent, BannerComponent, + FakeRouterLinkDirective, FakeRouterOutletComponent + ], + providers: [{ provide: Router, useClass: FakeRouter }], + schemas: [NO_ERRORS_SCHEMA] + }) + + .compileComponents() + + .then(() => { + fixture = TestBed.createComponent(AppComponent); + comp = fixture.componentInstance; + }); + + })); + + tests(); +}); + +function tests() { it('can instantiate it', () => { expect(comp).not.toBeNull(); }); - it('can get title from template', () => { - fixture.detectChanges(); - let titleEl = fixture.debugElement.query(By.css('h1')).nativeElement; - expect(titleEl.textContent).toContain(comp.title); - }); - it('can get RouterLinks from template', () => { fixture.detectChanges(); - let links = fixture.debugElement - .queryAll(By.directive(MockRouterLink)) - .map(de => de.injector.get(MockRouterLink) ); + const links = fixture.debugElement + // find all elements with an attached FakeRouterLink directive + .queryAll(By.directive(FakeRouterLinkDirective)) + // use injector to get the RouterLink directive instance attached to each element + .map(de => de.injector.get(FakeRouterLinkDirective) as FakeRouterLinkDirective); - expect(links.length).toEqual(2, 'should have 2 links'); - expect(links[0].routeParams[0]).toEqual('Dashboard', '1st link should go to Dashboard'); - expect(links[1].routeParams[0]).toEqual('Heroes', '1st link should go to Heroes'); - - let result = links[1].onClick(); - expect(result).toEqual(false, 'click should prevent default browser behavior'); + expect(links.length).toBe(3, 'should have 3 links'); + expect(links[0].linkParams).toBe('/dashboard', '1st link should go to Dashboard'); + expect(links[1].linkParams).toBe('/heroes', '1st link should go to Heroes'); }); it('can click Heroes link in template', () => { fixture.detectChanges(); // Heroes RouterLink DebugElement - let heroesDe = fixture.debugElement - .queryAll(By.directive(MockRouterLink))[1]; + const heroesLinkDe = fixture.debugElement + .queryAll(By.directive(FakeRouterLinkDirective))[1]; - expect(heroesDe).toBeDefined('should have a 2nd RouterLink'); + expect(heroesLinkDe).toBeDefined('should have a 2nd RouterLink'); - let link = heroesDe.injector.get(MockRouterLink); + const link = heroesLinkDe.injector.get(FakeRouterLinkDirective) as FakeRouterLinkDirective; expect(link.navigatedTo).toBeNull('link should not have navigate yet'); - heroesDe.triggerEventHandler('click', null); + heroesLinkDe.triggerEventHandler('click', null); fixture.detectChanges(); - expect(link.navigatedTo[0]).toEqual('Heroes'); - + expect(link.navigatedTo).toBe('/heroes'); }); +} + +//////// Testing w/ real root module ////// +// Best to avoid +// Tricky because we are disabling the router and its configuration +import { AppModule } from './app.module'; + +describe('AppComponent & AppModule', () => { + + beforeEach( async(() => { + + TestBed.configureTestingModule({ + imports: [ AppModule ], + }) + + .overrideModule(AppModule, { + // Must get rid of `RouterModule.forRoot` to prevent attempt to configure a router + // Can't remove it because it doesn't have a known type (`forRoot` returns an object) + // therefore, must reset the entire `imports` with just the necessary stuff + set: { imports: [ SharedModule ]} + }) + + // Separate override because cannot both `set` and `add/remove` in same override + .overrideModule(AppModule, { + add: { + declarations: [ FakeRouterLinkDirective, FakeRouterOutletComponent ], + providers: [{ provide: Router, useClass: FakeRouter }] + } + }) + + .compileComponents() + + .then(() => { + fixture = TestBed.createComponent(AppComponent); + comp = fixture.componentInstance; + }); + })); + + tests(); }); diff --git a/public/docs/_examples/testing/ts/app/app.component.ts b/public/docs/_examples/testing/ts/app/app.component.ts index f2da2da067..156feee06d 100644 --- a/public/docs/_examples/testing/ts/app/app.component.ts +++ b/public/docs/_examples/testing/ts/app/app.component.ts @@ -1,53 +1,8 @@ -// #docplaster // #docregion -import { Component } from '@angular/core'; - -// Can't test with ROUTER_DIRECTIVES yet -// import { RouteConfig, ROUTER_DIRECTIVES, ROUTER_PROVIDERS } from '@angular/router-deprecated'; - -import { RouteConfig, RouterLink, - RouterOutlet, ROUTER_PROVIDERS } from '@angular/router-deprecated'; - -import { DashboardComponent } from './dashboard.component'; -import { HeroesComponent } from './heroes.component'; -import { HeroDetailComponent } from './hero-detail.component'; -import { HeroService } from './hero.service'; - -import { BAG_DIRECTIVES, BAG_PROVIDERS } from './bag'; +import { Component } from '@angular/core'; @Component({ selector: 'my-app', - template: ` -

{{title}}

- - -
-

Bag-a-specs

- -

External Template Comp

- -

Comp With External Template Comp

- - `, - /* - - */ - styleUrls: ['app/app.component.css'], - directives: [RouterLink, RouterOutlet, BAG_DIRECTIVES], - providers: [ - ROUTER_PROVIDERS, - HeroService, - BAG_PROVIDERS - ] + templateUrl: 'app/app.component.html' }) -@RouteConfig([ - { path: '/dashboard', name: 'Dashboard', component: DashboardComponent, useAsDefault: true }, - { path: '/detail/:id', name: 'HeroDetail', component: HeroDetailComponent }, - { path: '/heroes', name: 'Heroes', component: HeroesComponent } -]) -export class AppComponent { - title = 'Tour of Heroes'; -} +export class AppComponent { } diff --git a/public/docs/_examples/testing/ts/app/app.module.ts b/public/docs/_examples/testing/ts/app/app.module.ts new file mode 100644 index 0000000000..adea748781 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/app.module.ts @@ -0,0 +1,33 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { RouterModule } from '@angular/router'; + + +import { AppComponent } from './app.component'; +import { AboutComponent } from './about.component'; +import { BannerComponent } from './banner.component'; +import { HeroService, + UserService } from './model'; +import { TwainService } from './shared/twain.service'; +import { WelcomeComponent } from './welcome.component'; + + +import { DashboardModule } from './dashboard/dashboard.module'; +import { SharedModule } from './shared/shared.module'; + +@NgModule({ + imports: [ + BrowserModule, + DashboardModule, + RouterModule.forRoot([ + { path: '', redirectTo: 'dashboard', pathMatch: 'full'}, + { path: 'about', component: AboutComponent }, + { path: 'heroes', loadChildren: 'app/hero/hero.module#HeroModule'} + ]), + SharedModule + ], + providers: [ HeroService, TwainService, UserService ], + declarations: [ AppComponent, AboutComponent, BannerComponent, WelcomeComponent ], + bootstrap: [ AppComponent ] +}) +export class AppModule { } diff --git a/public/docs/_examples/testing/ts/app/bad-tests.spec.ts b/public/docs/_examples/testing/ts/app/bad-tests.spec.ts deleted file mode 100644 index d73882372c..0000000000 --- a/public/docs/_examples/testing/ts/app/bad-tests.spec.ts +++ /dev/null @@ -1,163 +0,0 @@ -// Based on https://github.com/angular/angular/blob/master/modules/angular2/test/testing/testing_public_spec.ts -/* tslint:disable:no-unused-variable */ -/** - * Tests that show what goes wrong when the tests are incorrectly written or have a problem - */ -import { - BadTemplateUrlComp, ButtonComp, - ChildChildComp, ChildComp, ChildWithChildComp, - ExternalTemplateComp, - FancyService, MockFancyService, - InputComp, - MyIfComp, MyIfChildComp, MyIfParentComp, - MockChildComp, MockChildChildComp, - ParentComp, - TestProvidersComp, TestViewProvidersComp -} from './bag'; - -import { DebugElement } from '@angular/core'; -import { By } from '@angular/platform-browser'; - -import { - addProviders, - async, inject -} from '@angular/core/testing'; - -import { ComponentFixture, TestComponentBuilder } from '@angular/core/testing'; - -import { ViewMetadata } from '@angular/core'; -import { Observable } from 'rxjs/Rx'; - -//////// SPECS ///////////// - -xdescribe('async & inject testing errors', () => { - let originalJasmineIt: any; - let originalJasmineBeforeEach: any; - - let patchJasmineIt = () => { - return new Promise((resolve, reject) => { - originalJasmineIt = jasmine.getEnv().it; - jasmine.getEnv().it = (description: string, fn: Function): jasmine.Spec => { - let done = () => { resolve(); }; - (done).fail = (err: any) => { reject(err); }; - fn(done); - return null; - }; - }); - }; - - let restoreJasmineIt = () => { jasmine.getEnv().it = originalJasmineIt; }; - - let patchJasmineBeforeEach = () => { - return new Promise((resolve, reject) => { - originalJasmineBeforeEach = jasmine.getEnv().beforeEach; - jasmine.getEnv().beforeEach = (fn: any): void => { - let done = () => { resolve(); }; - (done).fail = (err: any) => { reject(err); }; - fn(done); - return null; - }; - }); - }; - - let restoreJasmineBeforeEach = - () => { jasmine.getEnv().beforeEach = originalJasmineBeforeEach; }; - - const shouldNotSucceed = - (done: DoneFn) => () => done.fail( 'Expected an error, but did not get one.'); - - const shouldFail = - (done: DoneFn, emsg: string) => (err: any) => { expect(err).toEqual(emsg); done(); }; - - it('should fail when an asynchronous error is thrown', (done: DoneFn) => { - let itPromise = patchJasmineIt(); - - it('throws an async error', - async(inject([], () => { setTimeout(() => { throw new Error('bar'); }, 0); }))); - - itPromise.then( - shouldNotSucceed(done), - err => { - expect(err).toEqual('bar'); - done(); - }); - restoreJasmineIt(); - }); - - it('should fail when a returned promise is rejected', (done: DoneFn) => { - let itPromise = patchJasmineIt(); - - it('should fail with an error from a promise', async(() => { - return Promise.reject('baz'); - })); - - itPromise.then( - shouldNotSucceed(done), - err => { - expect(err).toEqual('Uncaught (in promise): baz'); - done(); - }); - restoreJasmineIt(); - }); - - it('should fail when an error occurs inside inject', (done: DoneFn) => { - let itPromise = patchJasmineIt(); - - it('throws an error', inject([], () => { throw new Error('foo'); })); - - itPromise.then( - shouldNotSucceed(done), - shouldFail(done, 'foo') - ); - restoreJasmineIt(); - }); - - // TODO(juliemr): reenable this test when we are using a test zone and can capture this error. - it('should fail when an asynchronous error is thrown', (done: DoneFn) => { - let itPromise = patchJasmineIt(); - - it('throws an async error', - async(inject([], () => { setTimeout(() => { throw new Error('bar'); }, 0); }))); - - itPromise.then( - shouldNotSucceed(done), - shouldFail(done, 'bar') - ); - restoreJasmineIt(); - }); - - it('should fail when XHR loading of a template fails', (done: DoneFn) => { - let itPromise = patchJasmineIt(); - - it('should fail with an error from a promise', - async(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { - tcb.createAsync(BadTemplateUrlComp); - }))); - - itPromise.then( - shouldNotSucceed(done), - shouldFail(done, 'Uncaught (in promise): Failed to load non-existant.html') - ); - restoreJasmineIt(); - }, 10000); - - describe('using addProviders', () => { - addProviders([{ provide: FancyService, useValue: new FancyService() }]); - - beforeEach( - inject([FancyService], (service: FancyService) => { expect(service.value).toEqual('real value'); })); - - describe('nested addProviders', () => { - - it('should fail when the injector has already been used', () => { - patchJasmineBeforeEach(); - expect(() => { - addProviders([{ provide: FancyService, useValue: new FancyService() }]); - }) - .toThrowError('addProviders was called after the injector had been used ' + - 'in a beforeEach or it block. This invalidates the test injector'); - restoreJasmineBeforeEach(); - }); - }); - }); -}); diff --git a/public/docs/_examples/testing/ts/app/bag.spec.ts b/public/docs/_examples/testing/ts/app/bag.spec.ts deleted file mode 100644 index 7218e91f1b..0000000000 --- a/public/docs/_examples/testing/ts/app/bag.spec.ts +++ /dev/null @@ -1,460 +0,0 @@ -// Based on https://github.com/angular/angular/blob/master/modules/angular2/test/testing/testing_public_spec.ts -/* tslint:disable */ -import { - ButtonComp, - ChildChildComp, ChildComp, ChildWithChildComp, - ExternalTemplateComp, - FancyService, MockFancyService, - InputComp, - MyIfComp, MyIfChildComp, MyIfParentComp, - MockChildComp, MockChildChildComp, - ParentComp, - TestProvidersComp, TestViewProvidersComp -} from './bag'; - -import { DebugElement } from '@angular/core'; -import { By } from '@angular/platform-browser'; - -import { - addProviders, - inject, async, - fakeAsync, tick, withProviders -} from '@angular/core/testing'; - -import { ComponentFixture, TestComponentBuilder } from '@angular/core/testing'; - -import { ViewMetadata } from '@angular/core'; - -import { Observable } from 'rxjs/Rx'; - -//////// SPECS ///////////// - -describe('using the async helper', () => { - let actuallyDone = false; - - beforeEach(() => { actuallyDone = false; }); - - afterEach(() => { expect(actuallyDone).toEqual(true); }); - - it('should run normal test', () => { actuallyDone = true; }); - - it('should run normal async test', (done: DoneFn) => { - setTimeout(() => { - actuallyDone = true; - done(); - }, 0); - }); - - it('should run async test with task', - async(() => { setTimeout(() => { actuallyDone = true; }, 0); })); - - it('should run async test with successful promise', async(() => { - let p = new Promise(resolve => { setTimeout(resolve, 10); }); - p.then(() => { actuallyDone = true; }); - })); - - it('should run async test with failed promise', async(() => { - let p = new Promise((resolve, reject) => { setTimeout(reject, 10); }); - p.catch(() => { actuallyDone = true; }); - })); - - xit('should run async test with successful Observable', async(() => { - let source = Observable.of(true).delay(10); - source.subscribe( - val => {}, - err => fail(err), - () => { actuallyDone = true; } // completed - ); - })); -}); - -describe('using the test injector with the inject helper', () => { - - describe('setting up Providers with FancyService', () => { - beforeEach(() => { - addProviders([ - { provide: FancyService, useValue: new FancyService() } - ]); - }); - - it('should use FancyService', - inject([FancyService], (service: FancyService) => { - expect(service.value).toEqual('real value'); - })); - - it('test should wait for FancyService.getAsyncValue', - async(inject([FancyService], (service: FancyService) => { - service.getAsyncValue().then( - value => { expect(value).toEqual('async value'); }); - }))); - - it('test should wait for FancyService.getTimeoutValue', - async(inject([FancyService], (service: FancyService) => { - service.getTimeoutValue().then( - value => { expect(value).toEqual('timeout value'); }); - }))); - - it('test should wait for FancyService.getObservableValue', - async(inject([FancyService], (service: FancyService) => { - service.getObservableValue().subscribe( - value => { expect(value).toEqual('observable value'); } - ); - }))); - - xit('test should wait for FancyService.getObservableDelayValue', - async(inject([FancyService], (service: FancyService) => { - service.getObservableDelayValue().subscribe( - value => { expect(value).toEqual('observable delay value'); } - ); - }))); - - it('should allow the use of fakeAsync (Experimental)', - fakeAsync(inject([FancyService], (service: FancyService) => { - let value: any; - service.getAsyncValue().then((val: any) => value = val); - tick(); // Trigger JS engine cycle until all promises resolve. - expect(value).toEqual('async value'); - }))); - - describe('using inner beforeEach to inject-and-modify FancyService', () => { - beforeEach(inject([FancyService], (service: FancyService) => { - service.value = 'value modified in beforeEach'; - })); - - it('should use modified providers', - inject([FancyService], (service: FancyService) => { - expect(service.value).toEqual('value modified in beforeEach'); - })); - }); - - describe('using async within beforeEach', () => { - beforeEach(async(inject([FancyService], (service: FancyService) => { - service.getAsyncValue().then(value => { service.value = value; }); - }))); - - it('should use asynchronously modified value ... in synchronous test', - inject([FancyService], (service: FancyService) => { - expect(service.value).toEqual('async value'); })); - }); - }); - - describe('using `withProviders` for per-test provision', () => { - it('should inject test-local FancyService for this test', - // `withProviders`: set up providers at individual test level - withProviders(() => [{ provide: FancyService, useValue: {value: 'fake value' }}]) - - // now inject and test - .inject([FancyService], (service: FancyService) => { - expect(service.value).toEqual('fake value'); - })); - }); -}); - -describe('test component builder', function() { - it('should instantiate a component with valid DOM', - async(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { - - tcb.createAsync(ChildComp).then(fixture => { - fixture.detectChanges(); - expect(fixture.nativeElement.textContent).toContain('Original Child'); - }); - }))); - - it('should allow changing members of the component', - async(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { - - tcb.createAsync(MyIfComp).then(fixture => { - fixture.detectChanges(); - expect(fixture.nativeElement.textContent).toContain('MyIf()'); - - fixture.debugElement.componentInstance.showMore = true; - fixture.detectChanges(); - expect(fixture.nativeElement.textContent).toContain('MyIf(More)'); - }); - }))); - - it('should support clicking a button', - async(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { - - tcb.createAsync(ButtonComp).then(fixture => { - - let comp = fixture.componentInstance; - expect(comp.wasClicked).toEqual(false, 'wasClicked should be false at start'); - - let btn = fixture.debugElement.query(By.css('button')); - // let btn = fixture.debugElement.query(el => el.name === 'button'); // the hard way - - btn.triggerEventHandler('click', null); - // btn.nativeElement.click(); // this often works too ... but not all the time! - expect(comp.wasClicked).toEqual(true, 'wasClicked should be true after click'); - }); - }))); - - it('should support entering text in input box (ngModel)', - async(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { - let origName = 'John'; - let newName = 'Sally'; - - tcb.createAsync(InputComp).then(fixture => { - - let comp = fixture.componentInstance; - expect(comp.name).toEqual(origName, `At start name should be ${origName} `); - - let inputBox = fixture.debugElement.query(By.css('input')).nativeElement; - fixture.detectChanges(); - expect(inputBox.value).toEqual(origName, `At start input box value should be ${origName} `); - - inputBox.value = newName; - expect(comp.name).toEqual(origName, - `Name should still be ${origName} after value change, before detectChanges`); - - fixture.detectChanges(); - expect(inputBox.value).toEqual(newName, - `After value change and detectChanges, name should now be ${newName} `); - }); - }))); - - it('should override a template', - async(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { - - tcb.overrideTemplate(MockChildComp, 'Mock') - .createAsync(MockChildComp) - .then(fixture => { - fixture.detectChanges(); - expect(fixture.nativeElement.textContent).toContain('Mock'); - }); - }))); - - it('should override a view', - async(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { - - tcb.overrideView( - ChildComp, - new ViewMetadata({template: 'Modified {{childBinding}}'}) - ) - .createAsync(ChildComp) - .then(fixture => { - fixture.detectChanges(); - expect(fixture.nativeElement.textContent).toContain('Modified Child'); - - }); - }))); - - it('should override component directives', - async(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { - - tcb.overrideDirective(ParentComp, ChildComp, MockChildComp) - .createAsync(ParentComp) - .then(fixture => { - fixture.detectChanges(); - expect(fixture.nativeElement.textContent).toContain('Parent(Mock)'); - - }); - }))); - - - it('should override child component\'s directives', - async(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { - - tcb.overrideDirective(ParentComp, ChildComp, ChildWithChildComp) - .overrideDirective(ChildWithChildComp, ChildChildComp, MockChildChildComp) - .createAsync(ParentComp) - .then(fixture => { - fixture.detectChanges(); - expect(fixture.nativeElement.textContent) - .toContain('Parent(Original Child(ChildChild Mock))'); - - }); - }))); - - it('should override a provider', - async(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { - - tcb.overrideProviders( - TestProvidersComp, - [{ provide: FancyService, useClass: MockFancyService }] - ) - .createAsync(TestProvidersComp) - .then(fixture => { - fixture.detectChanges(); - expect(fixture.nativeElement.textContent) - .toContain('injected value: mocked out value'); - }); - }))); - - it('should override a viewProvider', - async(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { - - tcb.overrideViewProviders( - TestViewProvidersComp, - [{ provide: FancyService, useClass: MockFancyService }] - ) - .createAsync(TestViewProvidersComp) - .then(fixture => { - fixture.detectChanges(); - expect(fixture.nativeElement.textContent) - .toContain('injected value: mocked out value'); - }); - }))); - - it('should allow an external templateUrl', - async(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { - - tcb.createAsync(ExternalTemplateComp) - .then(fixture => { - fixture.detectChanges(); - expect(fixture.nativeElement.textContent) - .toContain('from external template\n'); - }); - })), 10000); // Long timeout because this test makes an actual XHR. - - describe('(lifecycle hooks w/ MyIfParentComp)', () => { - let fixture: ComponentFixture; - let parent: MyIfParentComp; - let child: MyIfChildComp; - - /** - * Get the MyIfChildComp from parent; fail w/ good message if cannot. - */ - function getChild() { - - let childDe: DebugElement; // DebugElement that should hold the MyIfChildComp - - // The Hard Way: requires detailed knowledge of the parent template - try { - childDe = fixture.debugElement.children[4].children[0]; - } catch (err) { /* we'll report the error */ } - - // DebugElement.queryAll: if we wanted all of many instances: - childDe = fixture.debugElement - .queryAll(function (de) { return de.componentInstance instanceof MyIfChildComp; })[0]; - - // WE'LL USE THIS APPROACH ! - // DebugElement.query: find first instance (if any) - childDe = fixture.debugElement - .query(function (de) { return de.componentInstance instanceof MyIfChildComp; }); - - if (childDe && childDe.componentInstance) { - child = childDe.componentInstance; - } else { - fail('Unable to find MyIfChildComp within MyIfParentComp'); - } - - return child; - } - - // Create MyIfParentComp TCB and component instance before each test (async beforeEach) - beforeEach(async(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { - tcb.createAsync(MyIfParentComp) - .then(fix => { - fixture = fix; - parent = fixture.debugElement.componentInstance; - }); - }))); - - it('should instantiate parent component', () => { - expect(parent).not.toBeNull('parent component should exist'); - }); - - it('parent component OnInit should NOT be called before first detectChanges()', () => { - expect(parent.ngOnInitCalled).toEqual(false); - }); - - it('parent component OnInit should be called after first detectChanges()', () => { - fixture.detectChanges(); - expect(parent.ngOnInitCalled).toEqual(true); - }); - - it('child component should exist after OnInit', () => { - fixture.detectChanges(); - getChild(); - expect(child instanceof MyIfChildComp).toEqual(true, 'should create child'); - }); - - it('should have called child component\'s OnInit ', () => { - fixture.detectChanges(); - getChild(); - expect(child.ngOnInitCalled).toEqual(true); - }); - - it('child component called OnChanges once', () => { - fixture.detectChanges(); - getChild(); - expect(child.ngOnChangesCounter).toEqual(1); - }); - - it('changed parent value flows to child', () => { - fixture.detectChanges(); - getChild(); - - parent.parentValue = 'foo'; - fixture.detectChanges(); - - expect(child.ngOnChangesCounter).toEqual(2, - 'expected 2 changes: initial value and changed value'); - expect(child.childValue).toEqual('foo', - 'childValue should eq changed parent value'); - }); - - it('changed child value flows to parent', async(() => { - fixture.detectChanges(); - getChild(); - - child.childValue = 'bar'; - - return new Promise(resolve => { - // Wait one JS engine turn! - setTimeout(() => resolve(), 0); - }).then(() => { - fixture.detectChanges(); - - expect(child.ngOnChangesCounter).toEqual(2, - 'expected 2 changes: initial value and changed value'); - expect(parent.parentValue).toEqual('bar', - 'parentValue should eq changed parent value'); - }); - - })); - - it('clicking "Close Child" triggers child OnDestroy', () => { - fixture.detectChanges(); - getChild(); - - let btn = fixture.debugElement.query(By.css('button')); - btn.triggerEventHandler('click', null); - - fixture.detectChanges(); - expect(child.ngOnDestroyCalled).toEqual(true); - }); - - }); -}); - - -//////// Testing Framework Bugs? ///// -import { HeroService } from './hero.service'; -import { Component } from '@angular/core'; - -@Component({ - selector: 'another-comp', - template: `AnotherProvidersComp()`, - providers: [FancyService] // <======= BOOM! if we comment out - // Failed: 'undefined' is not an object (evaluating 'dm.providers.concat') -}) -export class AnotherProvidersComp { - constructor( - private _heroService: HeroService - ) { } -} - -describe('tcb.overrideProviders', () => { - it('Component must have at least one provider else crash', - async(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { - - tcb.overrideProviders( - AnotherProvidersComp, - [{ provide: HeroService, useValue: {}} ] - ) - .createAsync(AnotherProvidersComp); - }))); -}); diff --git a/public/docs/_examples/testing/ts/app/bag.ts b/public/docs/_examples/testing/ts/app/bag.ts deleted file mode 100644 index 26f47e8b3d..0000000000 --- a/public/docs/_examples/testing/ts/app/bag.ts +++ /dev/null @@ -1,255 +0,0 @@ -// Based on https://github.com/angular/angular/blob/master/modules/angular2/test/testing/testing_public_spec.ts -/* tslint:disable */ -import { Component, EventEmitter, Injectable, Input, Output, Optional, - OnInit, OnChanges, OnDestroy, SimpleChange } from '@angular/core'; - -import { Observable } from 'rxjs/Rx'; - -////////// The App: Services and Components for the tests. ////////////// - -////////// Services /////////////// - -@Injectable() -export class FancyService { - value: string = 'real value'; - - getValue() { return this.value; } - - getAsyncValue() { return Promise.resolve('async value'); } - - getObservableValue() { return Observable.of('observable value'); } - - getTimeoutValue() { - return new Promise((resolve, reject) => { setTimeout(() => {resolve('timeout value'); }, 10); }); - } - - getObservableDelayValue() { return Observable.of('observable delay value').delay(10); } -} - -@Injectable() -export class MockFancyService extends FancyService { - value: string = 'mocked out value'; -} - -//////////// Components ///////////// - -@Component({ - selector: 'button-comp', - template: `` -}) -export class ButtonComp { - wasClicked = false; - clicked() { this.wasClicked = true; } -} - -@Component({ - selector: 'input-comp', - template: `` -}) -export class InputComp { - name = 'John'; -} - -@Component({ - selector: 'child-comp', - template: `Original {{childBinding}}` -}) -export class ChildComp { - childBinding = 'Child'; -} - - -@Component({ - selector: 'child-comp', - template: `Mock` -}) -export class MockChildComp { } - - -@Component({ - selector: 'parent-comp', - template: `Parent()`, - directives: [ChildComp] -}) -export class ParentComp { } - - -@Component({ - selector: 'my-if-comp', - template: `MyIf(More)` -}) -export class MyIfComp { - showMore = false; -} - -@Component({ - selector: 'child-child-comp', - template: 'ChildChild' -}) -export class ChildChildComp { } - - -@Component({ - selector: 'child-comp', - template: `Original {{childBinding}}()`, - directives: [ChildChildComp] -}) -export class ChildWithChildComp { - childBinding = 'Child'; -} - - -@Component({ - selector: 'child-child-comp', - template: `ChildChild Mock` -}) -export class MockChildChildComp { } - - -@Component({ - selector: 'my-service-comp', - template: `injected value: {{fancyService.value}}`, - providers: [FancyService] -}) -export class TestProvidersComp { - constructor(private fancyService: FancyService) {} -} - - -@Component({ - selector: 'my-service-comp', - template: `injected value: {{fancyService.value}}`, - viewProviders: [FancyService] -}) -export class TestViewProvidersComp { - constructor(private fancyService: FancyService) {} -} - -@Component({ - moduleId: module.id, - selector: 'external-template-comp', - templateUrl: 'bag-external-template.html' -}) -export class ExternalTemplateComp { - serviceValue: string; - - constructor(@Optional() private service: FancyService) { } - - ngOnInit() { - if (this.service) { this.serviceValue = this.service.getValue(); } - } -} - -@Component({ - selector: 'comp-w-ext-comp', - template: ` -

comp-w-ext-comp

- - `, - directives: [ExternalTemplateComp] -}) -export class CompWithCompWithExternalTemplate { } - -@Component({ - selector: 'bad-template-comp', - templateUrl: 'non-existant.html' -}) -export class BadTemplateUrlComp { } - - -///////// MyIfChildComp //////// -@Component({ - selector: 'my-if-child-comp', - - template: ` -

MyIfChildComp

-
- -
-

Change log:

-
{{i + 1}} - {{log}}
` -}) -export class MyIfChildComp implements OnInit, OnChanges, OnDestroy { - @Input() value = ''; - @Output() valueChange = new EventEmitter(); - - get childValue() { return this.value; } - set childValue(v: string) { - if (this.value === v) { return; } - this.value = v; - this.valueChange.emit(v); - } - - changeLog: string[] = []; - - ngOnInitCalled = false; - ngOnChangesCounter = 0; - ngOnDestroyCalled = false; - - ngOnInit() { - this.ngOnInitCalled = true; - this.changeLog.push('ngOnInit called'); - } - - ngOnDestroy() { - this.ngOnDestroyCalled = true; - this.changeLog.push('ngOnDestroy called'); - } - - ngOnChanges(changes: {[propertyName: string]: SimpleChange}) { - for (let propName in changes) { - this.ngOnChangesCounter += 1; - let prop = changes[propName]; - let cur = JSON.stringify(prop.currentValue); - let prev = JSON.stringify(prop.previousValue); - this.changeLog.push(`${propName}: currentValue = ${cur}, previousValue = ${prev}`); - } - } -} - -///////// MyIfParentComp //////// - -@Component({ - selector: 'my-if-parent-comp', - template: ` -

MyIfParentComp

- -
-
- -
- `, - directives: [MyIfChildComp] -}) -export class MyIfParentComp implements OnInit { - ngOnInitCalled = false; - parentValue = 'Hello, World'; - showChild = false; - toggleLabel = 'Unknown'; - - ngOnInit() { - this.ngOnInitCalled = true; - this.clicked(); - } - - clicked() { - this.showChild = !this.showChild; - this.toggleLabel = this.showChild ? 'Close' : 'Show'; - } -} - -export const BAG_PROVIDERS = [FancyService]; - -export const BAG_DIRECTIVES = [ - ButtonComp, - ChildChildComp, ChildComp, ChildWithChildComp, - ExternalTemplateComp, CompWithCompWithExternalTemplate, - InputComp, - MyIfComp, MyIfChildComp, MyIfParentComp, - MockChildComp, MockChildChildComp, - ParentComp, - TestProvidersComp, TestViewProvidersComp -]; diff --git a/public/docs/_examples/testing/ts/app/bag/async-helper.spec.ts b/public/docs/_examples/testing/ts/app/bag/async-helper.spec.ts new file mode 100644 index 0000000000..90ed17e92b --- /dev/null +++ b/public/docs/_examples/testing/ts/app/bag/async-helper.spec.ts @@ -0,0 +1,56 @@ +import { async, fakeAsync, tick } from '@angular/core/testing'; + +import { Observable } from 'rxjs/Observable'; + +describe('Angular async helper', () => { + let actuallyDone = false; + + beforeEach(() => { actuallyDone = false; }); + + afterEach(() => { expect(actuallyDone).toBe(true, 'actuallyDone should be true'); }); + + it('should run normal test', () => { actuallyDone = true; }); + + it('should run normal async test', (done: DoneFn) => { + setTimeout(() => { + actuallyDone = true; + done(); + }, 0); + }); + + it('should run async test with task', + async(() => { setTimeout(() => { actuallyDone = true; }, 0); })); + + it('should run async test with successful promise', async(() => { + const p = new Promise(resolve => { setTimeout(resolve, 10); }); + p.then(() => { actuallyDone = true; }); + })); + + it('should run async test with failed promise', async(() => { + const p = new Promise((resolve, reject) => { setTimeout(reject, 10); }); + p.catch(() => { actuallyDone = true; }); + })); + + // Fail message: Cannot use setInterval from within an async zone test + // See https://github.com/angular/angular/issues/10127 + xit('should run async test with successful delayed Observable', async(() => { + const source = Observable.of(true).delay(10); + source.subscribe( + val => actuallyDone = true, + err => fail(err) + ); + })); + + // Fail message: Error: 1 periodic timer(s) still in the queue + // See https://github.com/angular/angular/issues/10127 + xit('should run async test with successful delayed Observable', fakeAsync(() => { + const source = Observable.of(true).delay(10); + source.subscribe( + val => actuallyDone = true, + err => fail(err) + ); + + tick(); + })); + +}); diff --git a/public/docs/_examples/testing/ts/app/bag-external-template.html b/public/docs/_examples/testing/ts/app/bag/bag-external-template.html similarity index 100% rename from public/docs/_examples/testing/ts/app/bag-external-template.html rename to public/docs/_examples/testing/ts/app/bag/bag-external-template.html diff --git a/public/docs/_examples/testing/ts/app/bag/bag-main.ts b/public/docs/_examples/testing/ts/app/bag/bag-main.ts new file mode 100644 index 0000000000..27b78200ae --- /dev/null +++ b/public/docs/_examples/testing/ts/app/bag/bag-main.ts @@ -0,0 +1,5 @@ +// main app entry point +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; +import { BagModule } from './bag'; + +platformBrowserDynamic().bootstrapModule(BagModule); diff --git a/public/docs/_examples/testing/ts/app/bag/bag.no-testbed.spec.ts b/public/docs/_examples/testing/ts/app/bag/bag.no-testbed.spec.ts new file mode 100644 index 0000000000..6bdbe86cd0 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/bag/bag.no-testbed.spec.ts @@ -0,0 +1,130 @@ +// #docplaster +import { DependentService, FancyService } from './bag'; + +///////// Fakes ///////// +export class FakeFancyService extends FancyService { + value: string = 'faked value'; +} +//////////////////////// +// #docregion FancyService +// Straight Jasmine - no imports from Angular test libraries + +describe('FancyService without the TestBed', () => { + let service: FancyService; + + beforeEach(() => { service = new FancyService(); }); + + it('#getValue should return real value', () => { + expect(service.getValue()).toBe('real value'); + }); + + it('#getAsyncValue should return async value', done => { + service.getAsyncValue().then(value => { + expect(value).toBe('async value'); + done(); + }); + }); + + // #docregion getTimeoutValue + it('#getTimeoutValue should return timeout value', done => { + service = new FancyService(); + service.getTimeoutValue().then(value => { + expect(value).toBe('timeout value'); + done(); + }); + }); + // #enddocregion getTimeoutValue + + it('#getObservableValue should return observable value', done => { + service.getObservableValue().subscribe(value => { + expect(value).toBe('observable value'); + done(); + }); + }); + +}); +// #enddocregion FancyService + +// DependentService requires injection of a FancyService +// #docregion DependentService +describe('DependentService without the TestBed', () => { + let service: DependentService; + + it('#getValue should return real value by way of the real FancyService', () => { + service = new DependentService(new FancyService()); + expect(service.getValue()).toBe('real value'); + }); + + it('#getValue should return faked value by way of a fakeService', () => { + service = new DependentService(new FakeFancyService()); + expect(service.getValue()).toBe('faked value'); + }); + + it('#getValue should return faked value from a fake object', () => { + const fake = { getValue: () => 'fake value' }; + service = new DependentService(fake as FancyService); + expect(service.getValue()).toBe('fake value'); + }); + + it('#getValue should return stubbed value from a FancyService spy', () => { + const fancy = new FancyService(); + const stubValue = 'stub value'; + const spy = spyOn(fancy, 'getValue').and.returnValue(stubValue); + service = new DependentService(fancy); + + expect(service.getValue()).toBe(stubValue, 'service returned stub value'); + expect(spy.calls.count()).toBe(1, 'stubbed method was called once'); + expect(spy.calls.mostRecent().returnValue).toBe(stubValue); + }); +}); +// #enddocregion DependentService + +// #docregion ReversePipe +import { ReversePipe } from './bag'; + +describe('ReversePipe', () => { + let pipe: ReversePipe; + + beforeEach(() => { pipe = new ReversePipe(); }); + + it('transforms "abc" to "cba"', () => { + expect(pipe.transform('abc')).toBe('cba'); + }); + + it('no change to palindrome: "able was I ere I saw elba"', () => { + const palindrome = 'able was I ere I saw elba'; + expect(pipe.transform(palindrome)).toBe(palindrome); + }); + +}); +// #enddocregion ReversePipe + + +import { ButtonComponent } from './bag'; +// #docregion ButtonComp +describe('ButtonComp', () => { + let comp: ButtonComponent; + beforeEach(() => comp = new ButtonComponent()); + + it('#isOn should be false initially', () => { + expect(comp.isOn).toBe(false); + }); + + it('#clicked() should set #isOn to true', () => { + comp.clicked(); + expect(comp.isOn).toBe(true); + }); + + it('#clicked() should set #message to "is on"', () => { + comp.clicked(); + expect(comp.message).toMatch(/is on/i); + }); + + it('#clicked() should toggle #isOn', () => { + comp.clicked(); + expect(comp.isOn).toBe(true); + comp.clicked(); + expect(comp.isOn).toBe(false); + }); +}); +// #enddocregion ButtonComp diff --git a/public/docs/_examples/testing/ts/app/bag/bag.spec.ts b/public/docs/_examples/testing/ts/app/bag/bag.spec.ts new file mode 100644 index 0000000000..1fede16bd7 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/bag/bag.spec.ts @@ -0,0 +1,674 @@ +// #docplaster +import { + BagModule, + BankAccountComponent, BankAccountParentComponent, + ButtonComponent, + Child1Component, Child2Component, Child3Component, + FancyService, + ExternalTemplateComponent, + InputComponent, + IoComponent, IoParentComponent, + MyIfComponent, MyIfChildComponent, MyIfParentComponent, + NeedsContentComponent, ParentComponent, + TestProvidersComponent, TestViewProvidersComponent, + ReversePipeComponent, ShellComponent +} from './bag'; + +import { By } from '@angular/platform-browser'; +import { Component, + DebugElement, + Injectable } from '@angular/core'; +import { FormsModule } from '@angular/forms'; + +// Forms symbols imported only for a specific test below +import { NgModel, NgControl } from '@angular/forms'; + +import { async, ComponentFixture, fakeAsync, inject, TestBed, tick +} from '@angular/core/testing'; + +import { addMatchers, newEvent } from '../../testing'; + +beforeEach( addMatchers ); + +//////// Service Tests ///////////// +// #docregion FancyService +describe('use inject helper in beforeEach', () => { + let service: FancyService; + + beforeEach(() => { + TestBed.configureTestingModule({ providers: [FancyService] }); + + // `TestBed.get` returns the injectable or an + // alternative object (including null) if the service provider is not found. + // Of course it will be found in this case because we're providing it. + // #docregion testbed-get + service = TestBed.get(FancyService, null); + // #enddocregion testbed-get + }); + + it('should use FancyService', () => { + expect(service.getValue()).toBe('real value'); + }); + + it('should use FancyService', () => { + expect(service.getValue()).toBe('real value'); + }); + + it('test should wait for FancyService.getAsyncValue', async(() => { + service.getAsyncValue().then( + value => expect(value).toBe('async value') + ); + })); + + it('test should wait for FancyService.getTimeoutValue', async(() => { + service.getTimeoutValue().then( + value => expect(value).toBe('timeout value') + ); + })); + + it('test should wait for FancyService.getObservableValue', async(() => { + service.getObservableValue().subscribe( + value => expect(value).toBe('observable value') + ); + })); + + // #enddocregion FancyService + // See https://github.com/angular/angular/issues/10127 + xit('test should wait for FancyService.getObservableDelayValue', async(() => { + service.getObservableDelayValue().subscribe( + value => expect(value).toBe('observable delay value') + ); + })); + // #docregion FancyService + it('should allow the use of fakeAsync', fakeAsync(() => { + let value: any; + service.getAsyncValue().then((val: any) => value = val); + tick(); // Trigger JS engine cycle until all promises resolve. + expect(value).toBe('async value'); + })); +}); +// #enddocregion FancyService + +describe('use inject within `it`', () => { + // #docregion getTimeoutValue + beforeEach(() => { + TestBed.configureTestingModule({ providers: [FancyService] }); + }); + + // #enddocregion getTimeoutValue + + it('should use modified providers', + inject([FancyService], (service: FancyService) => { + service.setValue('value modified in beforeEach'); + expect(service.getValue()).toBe('value modified in beforeEach'); + }) + ); + + // #docregion getTimeoutValue + it('test should wait for FancyService.getTimeoutValue', + async(inject([FancyService], (service: FancyService) => { + + service.getTimeoutValue().then( + value => expect(value).toBe('timeout value') + ); + }))); + // #enddocregion getTimeoutValue +}); + +describe('using async(inject) within beforeEach', () => { + let serviceValue: string; + + beforeEach(() => { + TestBed.configureTestingModule({ providers: [FancyService] }); + }); + + beforeEach( async(inject([FancyService], (service: FancyService) => { + service.getAsyncValue().then(value => serviceValue = value); + }))); + + it('should use asynchronously modified value ... in synchronous test', () => { + expect(serviceValue).toBe('async value'); + }); +}); + + +/////////// Component Tests ////////////////// + +describe('TestBed Component Tests', () => { + + beforeEach( async(() => { + TestBed + .configureTestingModule({ + imports: [BagModule], + }) + // Compile everything in BagModule + .compileComponents(); + })); + + it('should create a component with inline template', () => { + const fixture = TestBed.createComponent(Child1Component); + fixture.detectChanges(); + + expect(fixture).toHaveText('Child'); + }); + + it('should create a component with external template', () => { + const fixture = TestBed.createComponent(ExternalTemplateComponent); + fixture.detectChanges(); + + expect(fixture).toHaveText('from external template'); + }); + + it('should allow changing members of the component', () => { + const fixture = TestBed.createComponent(MyIfComponent); + + fixture.detectChanges(); + expect(fixture).toHaveText('MyIf()'); + + fixture.componentInstance.showMore = true; + fixture.detectChanges(); + expect(fixture).toHaveText('MyIf(More)'); + }); + + it('should create a nested component bound to inputs/outputs', () => { + const fixture = TestBed.createComponent(IoParentComponent); + + fixture.detectChanges(); + const heroes = fixture.debugElement.queryAll(By.css('.hero')); + expect(heroes.length).toBeGreaterThan(0, 'has heroes'); + + const comp = fixture.componentInstance; + const hero = comp.heroes[0]; + + heroes[0].triggerEventHandler('click', null); + fixture.detectChanges(); + + const selected = fixture.debugElement.query(By.css('p')); + expect(selected).toHaveText(hero.name); + }); + + it('can access the instance variable of an `*ngFor` row', () => { + const fixture = TestBed.createComponent(IoParentComponent); + const comp = fixture.componentInstance; + + fixture.detectChanges(); + const heroEl = fixture.debugElement.query(By.css('.hero')); // first hero + + const ngForRow = heroEl.parent; // Angular's NgForRow wrapper element + + // jasmine.any is instance-of-type test. + expect(ngForRow.componentInstance).toEqual(jasmine.any(IoComponent), 'component is IoComp'); + + const hero = ngForRow.context['$implicit']; // the hero object + expect(hero.name).toBe(comp.heroes[0].name, '1st hero\'s name'); + }); + + + // #docregion ButtonComp + it('should support clicking a button', () => { + const fixture = TestBed.createComponent(ButtonComponent); + const btn = fixture.debugElement.query(By.css('button')); + const span = fixture.debugElement.query(By.css('span')).nativeElement; + + fixture.detectChanges(); + expect(span.textContent).toMatch(/is off/i, 'before click'); + + btn.triggerEventHandler('click', null); + fixture.detectChanges(); + expect(span.textContent).toMatch(/is on/i, 'after click'); + }); + // #enddocregion ButtonComp + + // ngModel is async so we must wait for it with promise-based `whenStable` + it('should support entering text in input box (ngModel)', async(() => { + const expectedOrigName = 'John'; + const expectedNewName = 'Sally'; + + const fixture = TestBed.createComponent(InputComponent); + fixture.detectChanges(); + + const comp = fixture.componentInstance; + const input = fixture.debugElement.query(By.css('input')).nativeElement; + + expect(comp.name).toBe(expectedOrigName, + `At start name should be ${expectedOrigName} `); + + // wait until ngModel binds comp.name to input box + fixture.whenStable().then(() => { + expect(input.value).toBe(expectedOrigName, + `After ngModel updates input box, input.value should be ${expectedOrigName} `); + + // simulate user entering new name in input + input.value = expectedNewName; + + // that change doesn't flow to the component immediately + expect(comp.name).toBe(expectedOrigName, + `comp.name should still be ${expectedOrigName} after value change, before binding happens`); + + // dispatch a DOM event so that Angular learns of input value change. + // then wait while ngModel pushes input.box value to comp.name + input.dispatchEvent(newEvent('input')); + return fixture.whenStable(); + }) + .then(() => { + expect(comp.name).toBe(expectedNewName, + `After ngModel updates the model, comp.name should be ${expectedNewName} `); + }); + })); + + // fakeAsync version of ngModel input test enables sync test style + // synchronous `tick` replaces asynchronous promise-base `whenStable` + it('should support entering text in input box (ngModel) - fakeAsync', fakeAsync(() => { + const expectedOrigName = 'John'; + const expectedNewName = 'Sally'; + + const fixture = TestBed.createComponent(InputComponent); + fixture.detectChanges(); + + const comp = fixture.componentInstance; + const input = fixture.debugElement.query(By.css('input')).nativeElement; + + expect(comp.name).toBe(expectedOrigName, + `At start name should be ${expectedOrigName} `); + + // wait until ngModel binds comp.name to input box + tick(); + expect(input.value).toBe(expectedOrigName, + `After ngModel updates input box, input.value should be ${expectedOrigName} `); + + // simulate user entering new name in input + input.value = expectedNewName; + + // that change doesn't flow to the component immediately + expect(comp.name).toBe(expectedOrigName, + `comp.name should still be ${expectedOrigName} after value change, before binding happens`); + + // dispatch a DOM event so that Angular learns of input value change. + // then wait a tick while ngModel pushes input.box value to comp.name + input.dispatchEvent(newEvent('input')); + tick(); + expect(comp.name).toBe(expectedNewName, + `After ngModel updates the model, comp.name should be ${expectedNewName} `); + })); + + // #docregion ReversePipeComp + it('ReversePipeComp should reverse the input text', fakeAsync(() => { + const inputText = 'the quick brown fox.'; + const expectedText = '.xof nworb kciuq eht'; + + const fixture = TestBed.createComponent(ReversePipeComponent); + fixture.detectChanges(); + + const comp = fixture.componentInstance; + const input = fixture.debugElement.query(By.css('input')).nativeElement as HTMLInputElement; + const span = fixture.debugElement.query(By.css('span')).nativeElement as HTMLElement; + + // simulate user entering new name in input + input.value = inputText; + + // dispatch a DOM event so that Angular learns of input value change. + // then wait a tick while ngModel pushes input.box value to comp.text + // and Angular updates the output span + input.dispatchEvent(newEvent('input')); + tick(); + fixture.detectChanges(); + expect(span.textContent).toBe(expectedText, 'output span'); + expect(comp.text).toBe(inputText, 'component.text'); + })); + // #enddocregion ReversePipeComp + + // Use this technique to find attached directives of any kind + it('can examine attached directives and listeners', () => { + const fixture = TestBed.createComponent(InputComponent); + fixture.detectChanges(); + + const inputEl = fixture.debugElement.query(By.css('input')); + + expect(inputEl.providerTokens).toContain(NgModel, 'NgModel directive'); + + const ngControl = inputEl.injector.get(NgControl); + expect(ngControl).toEqual(jasmine.any(NgControl), 'NgControl directive'); + + expect(inputEl.listeners.length).toBeGreaterThan(2, 'several listeners attached'); + }); + + // #docregion debug-dom-renderer + it('DebugDomRender should set attributes, styles, classes, and properties', () => { + const fixture = TestBed.createComponent(BankAccountParentComponent); + fixture.detectChanges(); + const comp = fixture.componentInstance; + + // the only child is debugElement of the BankAccount component + const el = fixture.debugElement.children[0]; + const childComp = el.componentInstance as BankAccountComponent; + expect(childComp).toEqual(jasmine.any(BankAccountComponent)); + + expect(el.context).toBe(comp, 'context is the parent component'); + + expect(el.attributes['account']).toBe(childComp.id, 'account attribute'); + expect(el.attributes['bank']).toBe(childComp.bank, 'bank attribute'); + + expect(el.classes['closed']).toBe(true, 'closed class'); + expect(el.classes['open']).toBe(false, 'open class'); + + expect(el.properties['customProperty']).toBe(true, 'customProperty'); + + expect(el.styles['color']).toBe(comp.color, 'color style'); + expect(el.styles['width']).toBe(comp.width + 'px', 'width style'); + }); + // #enddocregion debug-dom-renderer +}); + +describe('TestBed Component Overrides:', () => { + + it('should override ChildComp\'s template', () => { + + const fixture = TestBed.configureTestingModule({ + declarations: [Child1Component], + }) + .overrideComponent(Child1Component, { + set: { template: 'Fake' } + }) + .createComponent(Child1Component); + + fixture.detectChanges(); + expect(fixture).toHaveText('Fake'); + }); + + it('should override TestProvidersComp\'s FancyService provider', () => { + const fixture = TestBed.configureTestingModule({ + declarations: [TestProvidersComponent], + }) + .overrideComponent(TestProvidersComponent, { + remove: { providers: [FancyService]}, + add: { providers: [{ provide: FancyService, useClass: FakeFancyService }] }, + + // Or replace them all (this component has only one provider) + // set: { providers: [{ provide: FancyService, useClass: FakeFancyService }] }, + }) + .createComponent(TestProvidersComponent); + + fixture.detectChanges(); + expect(fixture).toHaveText('injected value: faked value', 'text'); + + // Explore the providerTokens + const tokens = fixture.debugElement.providerTokens; + expect(tokens).toContain(fixture.componentInstance.constructor, 'component ctor'); + expect(tokens).toContain(TestProvidersComponent, 'TestProvidersComp'); + expect(tokens).toContain(FancyService, 'FancyService'); + }); + + it('should override TestViewProvidersComp\'s FancyService viewProvider', () => { + const fixture = TestBed.configureTestingModule({ + declarations: [TestViewProvidersComponent], + }) + .overrideComponent(TestViewProvidersComponent, { + // remove: { viewProviders: [FancyService]}, + // add: { viewProviders: [{ provide: FancyService, useClass: FakeFancyService }] }, + + // Or replace them all (this component has only one viewProvider) + set: { viewProviders: [{ provide: FancyService, useClass: FakeFancyService }] }, + }) + .createComponent(TestViewProvidersComponent); + + fixture.detectChanges(); + expect(fixture).toHaveText('injected value: faked value'); + }); + + it('injected provider should not be same as component\'s provider', () => { + + // TestComponent is parent of TestProvidersComponent + @Component({ template: '' }) + class TestComponent {} + + // 3 levels of FancyService provider: module, TestCompomponent, TestProvidersComponent + const fixture = TestBed.configureTestingModule({ + declarations: [TestComponent, TestProvidersComponent], + providers: [FancyService] + }) + .overrideComponent(TestComponent, { + set: { providers: [{ provide: FancyService, useValue: {} }] } + }) + .overrideComponent(TestProvidersComponent, { + set: { providers: [{ provide: FancyService, useClass: FakeFancyService }] } + }) + .createComponent(TestComponent); + + let testBedProvider: FancyService; + let tcProvider: {}; + let tpcProvider: FakeFancyService; + + // `inject` uses TestBed's injector + inject([FancyService], (s: FancyService) => testBedProvider = s)(); + tcProvider = fixture.debugElement.injector.get(FancyService); + tpcProvider = fixture.debugElement.children[0].injector.get(FancyService); + + expect(testBedProvider).not.toBe(tcProvider, 'testBed/tc not same providers'); + expect(testBedProvider).not.toBe(tpcProvider, 'testBed/tpc not same providers'); + + expect(testBedProvider instanceof FancyService).toBe(true, 'testBedProvider is FancyService'); + expect(tcProvider).toEqual({}, 'tcProvider is {}'); + expect(tpcProvider instanceof FakeFancyService).toBe(true, 'tpcProvider is FakeFancyService'); + }); + + it('can access template local variables as references', () => { + const fixture = TestBed.configureTestingModule({ + declarations: [ShellComponent, NeedsContentComponent, Child1Component, Child2Component, Child3Component], + }) + .overrideComponent(ShellComponent, { + set: { + selector: 'test-shell', + template: ` + + + + + +
!
+
+ ` + } + }) + .createComponent(ShellComponent); + + fixture.detectChanges(); + + // NeedsContentComp is the child of ShellComp + const el = fixture.debugElement.children[0]; + const comp = el.componentInstance; + + expect(comp.children.toArray().length).toBe(4, + 'three different child components and an ElementRef with #content'); + + expect(el.references['nc']).toBe(comp, '#nc reference to component'); + + // #docregion custom-predicate + // Filter for DebugElements with a #content reference + const contentRefs = el.queryAll( de => de.references['content']); + // #enddocregion custom-predicate + expect(contentRefs.length).toBe(4, 'elements w/ a #content reference'); + }); + +}); + +describe('Nested (one-deep) component override', () => { + + beforeEach( async(() => { + TestBed.configureTestingModule({ + declarations: [ParentComponent, FakeChildComponent] + }) + .compileComponents(); + })); + + it('ParentComp should use Fake Child component', () => { + const fixture = TestBed.createComponent(ParentComponent); + fixture.detectChanges(); + expect(fixture).toHaveText('Parent(Fake Child)'); + }); +}); + +describe('Nested (two-deep) component override', () => { + + beforeEach( async(() => { + TestBed.configureTestingModule({ + declarations: [ParentComponent, FakeChildWithGrandchildComponent, FakeGrandchildComponent] + }) + .compileComponents(); + })); + + it('should use Fake Grandchild component', () => { + const fixture = TestBed.createComponent(ParentComponent); + fixture.detectChanges(); + expect(fixture).toHaveText('Parent(Fake Child(Fake Grandchild))'); + }); +}); + +describe('Lifecycle hooks w/ MyIfParentComp', () => { + let fixture: ComponentFixture; + let parent: MyIfParentComponent; + let child: MyIfChildComponent; + + beforeEach( async(() => { + TestBed.configureTestingModule({ + imports: [FormsModule], + declarations: [MyIfChildComponent, MyIfParentComponent] + }) + .compileComponents().then(() => { + fixture = TestBed.createComponent(MyIfParentComponent); + parent = fixture.componentInstance; + }); + })); + + it('should instantiate parent component', () => { + expect(parent).not.toBeNull('parent component should exist'); + }); + + it('parent component OnInit should NOT be called before first detectChanges()', () => { + expect(parent.ngOnInitCalled).toBe(false); + }); + + it('parent component OnInit should be called after first detectChanges()', () => { + fixture.detectChanges(); + expect(parent.ngOnInitCalled).toBe(true); + }); + + it('child component should exist after OnInit', () => { + fixture.detectChanges(); + getChild(); + expect(child instanceof MyIfChildComponent).toBe(true, 'should create child'); + }); + + it('should have called child component\'s OnInit ', () => { + fixture.detectChanges(); + getChild(); + expect(child.ngOnInitCalled).toBe(true); + }); + + it('child component called OnChanges once', () => { + fixture.detectChanges(); + getChild(); + expect(child.ngOnChangesCounter).toBe(1); + }); + + it('changed parent value flows to child', () => { + fixture.detectChanges(); + getChild(); + + parent.parentValue = 'foo'; + fixture.detectChanges(); + + expect(child.ngOnChangesCounter).toBe(2, + 'expected 2 changes: initial value and changed value'); + expect(child.childValue).toBe('foo', + 'childValue should eq changed parent value'); + }); + + // must be async test to see child flow to parent + it('changed child value flows to parent', async(() => { + fixture.detectChanges(); + getChild(); + + child.childValue = 'bar'; + + return new Promise(resolve => { + // Wait one JS engine turn! + setTimeout(() => resolve(), 0); + }) + .then(() => { + fixture.detectChanges(); + + expect(child.ngOnChangesCounter).toBe(2, + 'expected 2 changes: initial value and changed value'); + expect(parent.parentValue).toBe('bar', + 'parentValue should eq changed parent value'); + }); + + })); + + it('clicking "Close Child" triggers child OnDestroy', () => { + fixture.detectChanges(); + getChild(); + + const btn = fixture.debugElement.query(By.css('button')); + btn.triggerEventHandler('click', null); + + fixture.detectChanges(); + expect(child.ngOnDestroyCalled).toBe(true); + }); + + ////// helpers /// + /** + * Get the MyIfChildComp from parent; fail w/ good message if cannot. + */ + function getChild() { + + let childDe: DebugElement; // DebugElement that should hold the MyIfChildComp + + // The Hard Way: requires detailed knowledge of the parent template + try { + childDe = fixture.debugElement.children[4].children[0]; + } catch (err) { /* we'll report the error */ } + + // DebugElement.queryAll: if we wanted all of many instances: + childDe = fixture.debugElement + .queryAll(function (de) { return de.componentInstance instanceof MyIfChildComponent; })[0]; + + // WE'LL USE THIS APPROACH ! + // DebugElement.query: find first instance (if any) + childDe = fixture.debugElement + .query(function (de) { return de.componentInstance instanceof MyIfChildComponent; }); + + if (childDe && childDe.componentInstance) { + child = childDe.componentInstance; + } else { + fail('Unable to find MyIfChildComp within MyIfParentComp'); + } + + return child; + } +}); + +////////// Fakes /////////// + +@Component({ + selector: 'child-1', + template: `Fake Child` +}) +class FakeChildComponent { } + +@Component({ + selector: 'child-1', + template: `Fake Child()` +}) +class FakeChildWithGrandchildComponent { } + +@Component({ + selector: 'grandchild-1', + template: `Fake Grandchild` +}) +class FakeGrandchildComponent { } + +@Injectable() +class FakeFancyService extends FancyService { + value: string = 'faked value'; +} diff --git a/public/docs/_examples/testing/ts/app/bag/bag.ts b/public/docs/_examples/testing/ts/app/bag/bag.ts new file mode 100644 index 0000000000..cbe88f55f5 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/bag/bag.ts @@ -0,0 +1,454 @@ +/* tslint:disable:forin */ +import { Component, ContentChildren, Directive, ElementRef, EventEmitter, + Injectable, Input, Output, Optional, + HostBinding, HostListener, + OnInit, OnChanges, OnDestroy, + Pipe, PipeTransform, + Renderer, SimpleChange } from '@angular/core'; + +import { Observable } from 'rxjs/Observable'; +import 'rxjs/add/observable/of'; +import 'rxjs/add/operator/delay'; + +////////// The App: Services and Components for the tests. ////////////// + +export class Hero { + name: string; +} + +////////// Services /////////////// +// #docregion FancyService +@Injectable() +export class FancyService { + protected value: string = 'real value'; + + getValue() { return this.value; } + setValue(value: string) { this.value = value; } + + getAsyncValue() { return Promise.resolve('async value'); } + + getObservableValue() { return Observable.of('observable value'); } + + getTimeoutValue() { + return new Promise((resolve) => { + setTimeout(() => { resolve('timeout value'); }, 10); + }); + } + + getObservableDelayValue() { + return Observable.of('observable delay value').delay(10); + } +} +// #enddocregion FancyService + +// #docregion DependentService +@Injectable() +export class DependentService { + constructor(private dependentService: FancyService) { } + getValue() { return this.dependentService.getValue(); } +} +// #enddocregion DependentService + +/////////// Pipe //////////////// +/* + * Reverse the input string. +*/ +// #docregion ReversePipe +@Pipe({ name: 'reverse' }) +export class ReversePipe implements PipeTransform { + transform(s: string) { + let r = ''; + for (let i = s.length; i; ) { r += s[--i]; }; + return r; + } +} +// #enddocregion ReversePipe + +//////////// Components ///////////// +@Component({ + selector: 'bank-account', + template: ` + Bank Name: {{bank}} + Account Id: {{id}} + ` +}) +export class BankAccountComponent { + @Input() bank: string; + @Input('account') id: string; + + constructor(private renderer: Renderer, private el: ElementRef ) { + renderer.setElementProperty(el.nativeElement, 'customProperty', true); + } +} + +/** A component with attributes, styles, classes, and property setting */ +@Component({ + selector: 'bank-account-parent', + template: ` + + + ` +}) +export class BankAccountParentComponent { + width = 200; + color = 'red'; + isClosed = true; +} + +// #docregion ButtonComp +@Component({ + selector: 'button-comp', + template: ` + + {{message}}` +}) +export class ButtonComponent { + isOn = false; + clicked() { this.isOn = !this.isOn; } + get message() { return `The light is ${this.isOn ? 'On' : 'Off'}`; } +} +// #enddocregion ButtonComp + +@Component({ + selector: 'child-1', + template: `Child-1({{text}})` +}) +export class Child1Component { + @Input() text = 'Original'; +} + +@Component({ + selector: 'child-2', + template: '
Child-2({{text}})
' +}) +export class Child2Component { + @Input() text: string; +} + +@Component({ + selector: 'child-3', + template: '
Child-3({{text}})
' +}) +export class Child3Component { + @Input() text: string; +} + +@Component({ + selector: 'input-comp', + template: `` +}) +export class InputComponent { + name = 'John'; +} + +/* Prefer this metadata syntax */ +// @Directive({ +// selector: 'input[value]', +// host: { +// '[value]': 'value', +// '(input)': 'valueChange.next($event.target.value)' +// }, +// inputs: ['value'], +// outputs: ['valueChange'] +// }) +// export class InputValueBinderDirective { +// value: any; +// valueChange: EventEmitter = new EventEmitter(); +// } + +// As the style-guide recommends +@Directive({ selector: 'input[value]' }) +export class InputValueBinderDirective { + @HostBinding() + @Input() + value: any; + + @Output() + valueChange: EventEmitter = new EventEmitter(); + + @HostListener('input', ['$event.target.value']) + onInput(value: any) { this.valueChange.next(value); } +} + +@Component({ + selector: 'input-value-comp', + template: ` + Name: {{name}} + ` +}) +export class InputValueBinderComponent { + name = 'Sally'; // initial value +} + +@Component({ + selector: 'parent-comp', + template: `Parent()` +}) +export class ParentComponent { } + +@Component({ + selector: 'io-comp', + template: `
Original {{hero.name}}
` +}) +export class IoComponent { + @Input() hero: Hero; + @Output() selected = new EventEmitter(); + click() { this.selected.emit(this.hero); } +} + +@Component({ + selector: 'io-parent-comp', + template: ` +

Click to select a hero

+

The selected hero is {{selectedHero.name}}

+ + + ` +}) +export class IoParentComponent { + heroes: Hero[] = [ {name: 'Bob'}, {name: 'Carol'}, {name: 'Ted'}, {name: 'Alice'} ]; + selectedHero: Hero; + onSelect(hero: Hero) { this.selectedHero = hero; } +} + +@Component({ + selector: 'my-if-comp', + template: `MyIf(More)` +}) +export class MyIfComponent { + showMore = false; +} + +@Component({ + selector: 'my-service-comp', + template: `injected value: {{fancyService.value}}`, + providers: [FancyService] +}) +export class TestProvidersComponent { + constructor(private fancyService: FancyService) {} +} + + +@Component({ + selector: 'my-service-comp', + template: `injected value: {{fancyService.value}}`, + viewProviders: [FancyService] +}) +export class TestViewProvidersComponent { + constructor(private fancyService: FancyService) {} +} + +@Component({ + moduleId: module.id, + selector: 'external-template-comp', + templateUrl: 'bag-external-template.html' +}) +export class ExternalTemplateComponent implements OnInit { + serviceValue: string; + + constructor(@Optional() private service: FancyService) { } + + ngOnInit() { + if (this.service) { this.serviceValue = this.service.getValue(); } + } +} + +@Component({ + selector: 'comp-w-ext-comp', + template: ` +

comp-w-ext-comp

+ + ` +}) +export class InnerCompWithExternalTemplateComponent { } + +@Component({ + selector: 'bad-template-comp', + templateUrl: 'non-existant.html' +}) +export class BadTemplateUrlComponent { } + + + +@Component({selector: 'needs-content', template: ''}) +export class NeedsContentComponent { + // children with #content local variable + @ContentChildren('content') children: any; +} + +///////// MyIfChildComp //////// +@Component({ + selector: 'my-if-child-1', + + template: ` +

MyIfChildComp

+
+ +
+

Change log:

+
{{i + 1}} - {{log}}
` +}) +export class MyIfChildComponent implements OnInit, OnChanges, OnDestroy { + @Input() value = ''; + @Output() valueChange = new EventEmitter(); + + get childValue() { return this.value; } + set childValue(v: string) { + if (this.value === v) { return; } + this.value = v; + this.valueChange.emit(v); + } + + changeLog: string[] = []; + + ngOnInitCalled = false; + ngOnChangesCounter = 0; + ngOnDestroyCalled = false; + + ngOnInit() { + this.ngOnInitCalled = true; + this.changeLog.push('ngOnInit called'); + } + + ngOnDestroy() { + this.ngOnDestroyCalled = true; + this.changeLog.push('ngOnDestroy called'); + } + + ngOnChanges(changes: {[propertyName: string]: SimpleChange}) { + for (let propName in changes) { + this.ngOnChangesCounter += 1; + let prop = changes[propName]; + let cur = JSON.stringify(prop.currentValue); + let prev = JSON.stringify(prop.previousValue); + this.changeLog.push(`${propName}: currentValue = ${cur}, previousValue = ${prev}`); + } + } +} + +///////// MyIfParentComp //////// + +@Component({ + selector: 'my-if-parent-comp', + template: ` +

MyIfParentComp

+ +
+
+ +
+ ` +}) +export class MyIfParentComponent implements OnInit { + ngOnInitCalled = false; + parentValue = 'Hello, World'; + showChild = false; + toggleLabel = 'Unknown'; + + ngOnInit() { + this.ngOnInitCalled = true; + this.clicked(); + } + + clicked() { + this.showChild = !this.showChild; + this.toggleLabel = this.showChild ? 'Close' : 'Show'; + } +} + + +@Component({ + selector: 'reverse-pipe-comp', + template: ` + + {{text | reverse}} + ` +}) +export class ReversePipeComponent { + text = 'my dog has fleas.'; +} + +@Component({template: '
Replace Me
'}) +export class ShellComponent { } + +@Component({ + selector: 'bag-comp', + template: ` +

Specs Bag

+ +
+

Input/Output Component

+ +
+

External Template Component

+ +
+

Component With External Template Component

+ +
+

Reverse Pipe

+ +
+

InputValueBinder Directive

+ +
+

Button Component

+ +
+

Needs Content

+ + + + + +
!
+
+ ` +}) +export class BagComponent { } +//////// Aggregations //////////// + +export const bagDeclarations = [ + BagComponent, + BankAccountComponent, BankAccountParentComponent, + ButtonComponent, + Child1Component, Child2Component, Child3Component, + ExternalTemplateComponent, InnerCompWithExternalTemplateComponent, + InputComponent, + InputValueBinderDirective, InputValueBinderComponent, + IoComponent, IoParentComponent, + MyIfComponent, MyIfChildComponent, MyIfParentComponent, + NeedsContentComponent, ParentComponent, + TestProvidersComponent, TestViewProvidersComponent, + ReversePipe, ReversePipeComponent, ShellComponent +]; + +export const bagProviders = [DependentService, FancyService]; + +//////////////////// +//////////// +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { FormsModule } from '@angular/forms'; + +@NgModule({ + imports: [BrowserModule, FormsModule], + declarations: bagDeclarations, + providers: bagProviders, + entryComponents: [BagComponent], + bootstrap: [BagComponent] +}) +export class BagModule { } + diff --git a/public/docs/_examples/testing/ts/app/banner.component.spec.ts b/public/docs/_examples/testing/ts/app/banner.component.spec.ts new file mode 100644 index 0000000000..c9af53a805 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/banner.component.spec.ts @@ -0,0 +1,127 @@ +// #docplaster +// #docregion +// #docregion imports +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { DebugElement } from '@angular/core'; + +import { BannerComponent } from './banner.component'; +// #enddocregion imports + +// #docregion setup +let comp: BannerComponent; +let fixture: ComponentFixture; +let el: DebugElement; + +describe('BannerComponent', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [ BannerComponent ], // declare the test component + }); + + fixture = TestBed.createComponent(BannerComponent); + + comp = fixture.componentInstance; // BannerComponent test instance + + // get title DebugElement by element name + el = fixture.debugElement.query(By.css('h1')); + }); +// #enddocregion setup + // #docregion tests + it('should display original title', () => { + fixture.detectChanges(); // trigger data binding + expect(el.nativeElement.textContent).toContain(comp.title); + }); + + it('should display a different test title', () => { + comp.title = 'Test Title'; + fixture.detectChanges(); // trigger data binding + expect(el.nativeElement.textContent).toContain('Test Title'); + }); + // #enddocregion tests + // #docregion test-w-o-detect-changes + it('no title in the DOM until manually call `detectChanges`', () => { + expect(el.nativeElement.textContent).toEqual(''); + }); + // #enddocregion test-w-o-detect-changes + +// #docregion setup +}); +// #enddocregion setup + +///////// With AutoChangeDetect ///// +import { ComponentFixtureAutoDetect } from '@angular/core/testing'; + +describe('BannerComponent with AutoChangeDetect', () => { + + beforeEach(() => { + // #docregion auto-detect + fixture = TestBed.configureTestingModule({ + declarations: [ BannerComponent ], + providers: [ + { provide: ComponentFixtureAutoDetect, + useValue: true } + ] + }) + // #enddocregion auto-detect + .createComponent(BannerComponent); + + comp = fixture.componentInstance; // BannerComponent test instance + + // find title DebugElement by element name + el = fixture.debugElement.query(By.css('h1')); + }); + + // #docregion auto-detect-tests + it('should display original title', () => { + // Hooray! No `fixture.detectChanges()` needed + expect(el.nativeElement.textContent).toContain(comp.title); + }); + + it('should still see original title after comp.title change', () => { + const oldTitle = comp.title; + comp.title = 'Test Title'; + // Displayed title is old because Angular didn't hear the change :( + expect(el.nativeElement.textContent).toContain(oldTitle); + }); + + it('should display updated title after detectChanges', () => { + comp.title = 'Test Title'; + fixture.detectChanges(); // detect changes explicitly + expect(el.nativeElement.textContent).toContain(comp.title); + }); + // #enddocregion auto-detect-tests +}); + + +describe('BannerComponent (simpified)', () => { + // #docregion simple-example-before-each + beforeEach(() => { + + // refine the test module by declaring the test component + TestBed.configureTestingModule({ + declarations: [ BannerComponent ], + }); + + // create component and test fixture + fixture = TestBed.createComponent(BannerComponent); + + // get test component from the fixture + comp = fixture.componentInstance; + }); + // #enddocregion simple-example-before-each + + // #docregion simple-example-it + it('should display original title', () => { + + // trigger data binding to update the view + fixture.detectChanges(); + + // find the title element in the DOM using a CSS selector + el = fixture.debugElement.query(By.css('h1')); + + // confirm the element's content + expect(el.nativeElement.textContent).toContain(comp.title); + }); + // #enddocregion simple-example-it +}); diff --git a/public/docs/_examples/testing/ts/app/banner.component.ts b/public/docs/_examples/testing/ts/app/banner.component.ts new file mode 100644 index 0000000000..c220c1482b --- /dev/null +++ b/public/docs/_examples/testing/ts/app/banner.component.ts @@ -0,0 +1,11 @@ +// #docregion +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-banner', + template: '

{{title}}

' +}) +export class BannerComponent { + title = 'Test Tour of Heroes'; +} + diff --git a/public/docs/_examples/testing/ts/app/dashboard.component.html b/public/docs/_examples/testing/ts/app/dashboard.component.html deleted file mode 100644 index 028eab6eb3..0000000000 --- a/public/docs/_examples/testing/ts/app/dashboard.component.html +++ /dev/null @@ -1,11 +0,0 @@ - -

Top Heroes

-
- -
- -
-

{{hero.name}}

-
-
-
diff --git a/public/docs/_examples/testing/ts/app/dashboard.component.spec.ts b/public/docs/_examples/testing/ts/app/dashboard.component.spec.ts deleted file mode 100644 index 1b573c32f3..0000000000 --- a/public/docs/_examples/testing/ts/app/dashboard.component.spec.ts +++ /dev/null @@ -1,164 +0,0 @@ -/* tslint:disable:no-unused-variable */ -import { DashboardComponent } from './dashboard.component'; - -import { By } from '@angular/platform-browser'; - -import { - addProviders, - async, inject -} from '@angular/core/testing'; - -import { ComponentFixture, TestComponentBuilder } from '@angular/core/testing'; - -import { Hero, HeroService, MockHeroService } from './mock-hero.service'; -import { Router, MockRouter } from './mock-router'; - -describe('DashboardComponent', () => { - - //////// WITHOUT ANGULAR INVOLVED /////// - describe('w/o Angular', () => { - let comp: DashboardComponent; - let mockHeroService: MockHeroService; - let router: MockRouter; - - beforeEach(() => { - router = new MockRouter(); - mockHeroService = new MockHeroService(); - comp = new DashboardComponent(router, mockHeroService); - }); - - it('should NOT have heroes before calling OnInit', () => { - expect(comp.heroes.length).toEqual(0, - 'should not have heroes before OnInit'); - }); - - it('should NOT have heroes immediately after OnInit', () => { - comp.ngOnInit(); // ngOnInit -> getHeroes - expect(comp.heroes.length).toEqual(0, - 'should not have heroes until service promise resolves'); - }); - - it('should HAVE heroes after HeroService gets them', (done: DoneFn) => { - comp.ngOnInit(); // ngOnInit -> getHeroes - mockHeroService.lastPromise // the one from getHeroes - .then(() => { - // throw new Error('deliberate error'); // see it fail gracefully - expect(comp.heroes.length).toBeGreaterThan(0, - 'should have heroes after service promise resolves'); - }) - .then(done, done.fail); - }); - - it('should tell ROUTER to navigate by hero id', () => { - let hero: Hero = {id: 42, name: 'Abbracadabra' }; - let spy = spyOn(router, 'navigate').and.callThrough(); - - comp.gotoDetail(hero); - - let linkParams = spy.calls.mostRecent().args[0]; - expect(linkParams[0]).toEqual('HeroDetail', 'should nav to "HeroDetail"'); - expect(linkParams[1].id).toEqual(hero.id, 'should nav to fake hero\'s id'); - }); - - }); - - - ////// WITH ANGULAR TEST INFRASTRUCTURE /////// - describe('using TCB', () => { - let comp: DashboardComponent; - let mockHeroService: MockHeroService; - - beforeEach(() => { - mockHeroService = new MockHeroService(); - addProviders([ - { provide: Router, useClass: MockRouter}, - { provide: MockRouter, useExisting: Router}, - { provide: HeroService, useValue: mockHeroService } - ]); - }); - - it('can instantiate it', - async(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { - tcb.createAsync(DashboardComponent); - }))); - - it('should NOT have heroes before OnInit', - async(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { - tcb.createAsync(DashboardComponent).then(fixture => { - comp = fixture.debugElement.componentInstance; - - expect(comp.heroes.length).toEqual(0, - 'should not have heroes before OnInit'); - }); - }))); - - it('should NOT have heroes immediately after OnInit', - async(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { - tcb.createAsync(DashboardComponent).then(fixture => { - comp = fixture.debugElement.componentInstance; - fixture.detectChanges(); // runs initial lifecycle hooks - - expect(comp.heroes.length).toEqual(0, - 'should not have heroes until service promise resolves'); - }); - }))); - - it('should HAVE heroes after HeroService gets them', - async(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { - - tcb.createAsync(DashboardComponent).then(fixture => { - comp = fixture.debugElement.componentInstance; - fixture.detectChanges(); // runs ngOnInit -> getHeroes - - mockHeroService.lastPromise // the one from getHeroes - .then(() => { - expect(comp.heroes.length).toBeGreaterThan(0, - 'should have heroes after service promise resolves'); - }); - - }); - }))); - - it('should DISPLAY heroes after HeroService gets them', - async(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { - - tcb.createAsync(DashboardComponent).then(fixture => { - comp = fixture.debugElement.componentInstance; - fixture.detectChanges(); // runs ngOnInit -> getHeroes - - mockHeroService.lastPromise // the one from getHeroes - .then(() => { - - // Find and examine the displayed heroes - fixture.detectChanges(); // update bindings - let heroNames = fixture.debugElement.queryAll(By.css('h4')); - - expect(heroNames.length).toEqual(4, 'should display 4 heroes'); - - // the 4th displayed hero should be the 5th mock hero - expect(heroNames[3].nativeElement.textContent) - .toContain(mockHeroService.mockHeroes[4].name); - }); - - }); - }))); - - it('should tell ROUTER to navigate by hero id', - async(inject([TestComponentBuilder, Router], - (tcb: TestComponentBuilder, router: MockRouter) => { - - let spy = spyOn(router, 'navigate').and.callThrough(); - - tcb.createAsync(DashboardComponent).then(fixture => { - let hero: Hero = {id: 42, name: 'Abbracadabra' }; - comp = fixture.debugElement.componentInstance; - comp.gotoDetail(hero); - - let linkParams = spy.calls.mostRecent().args[0]; - expect(linkParams[0]).toEqual('HeroDetail', 'should nav to "HeroDetail"'); - expect(linkParams[1].id).toEqual(hero.id, 'should nav to fake hero\'s id'); - - }); - }))); - }); -}); diff --git a/public/docs/_examples/testing/ts/app/dashboard.component.ts b/public/docs/_examples/testing/ts/app/dashboard.component.ts deleted file mode 100644 index 69a9c5cce6..0000000000 --- a/public/docs/_examples/testing/ts/app/dashboard.component.ts +++ /dev/null @@ -1,44 +0,0 @@ -// #docplaster -// #docregion -import { Component, OnInit } from '@angular/core'; -// #docregion import-router -import { Router } from '@angular/router-deprecated'; -// #enddocregion import-router - -import { Hero } from './hero'; -import { HeroService } from './hero.service'; - -@Component({ - selector: 'my-dashboard', - // #docregion template-url - templateUrl: 'app/dashboard.component.html', - // #enddocregion template-url - // #docregion css - styleUrls: ['app/dashboard.component.css'] - // #enddocregion css -}) -// #docregion component -export class DashboardComponent implements OnInit { - - heroes: Hero[] = []; - -// #docregion ctor - constructor( - private _router: Router, - private _heroService: HeroService) { - } -// #enddocregion ctor - - ngOnInit() { - this._heroService.getHeroes() - .then(heroes => this.heroes = heroes.slice(1, 5)); - } - - // #docregion goto-detail - gotoDetail(hero: Hero) { - let link = ['HeroDetail', { id: hero.id }]; - this._router.navigate(link); - } - // #enddocregion goto-detail -} -// #enddocregion diff --git a/public/docs/_examples/testing/ts/app/dashboard/dashboard-hero.component.css b/public/docs/_examples/testing/ts/app/dashboard/dashboard-hero.component.css new file mode 100644 index 0000000000..eb54d181d8 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/dashboard/dashboard-hero.component.css @@ -0,0 +1,28 @@ +.hero { + padding: 20px; + position: relative; + text-align: center; + color: #eee; + max-height: 120px; + min-width: 120px; + background-color: #607D8B; + border-radius: 2px; +} + +.hero:hover { + background-color: #EEE; + cursor: pointer; + color: #607d8b; +} + +@media (max-width: 600px) { + .hero { + font-size: 10px; + max-height: 75px; } +} + +@media (max-width: 1024px) { + .hero { + min-width: 60px; + } +} diff --git a/public/docs/_examples/testing/ts/app/dashboard/dashboard-hero.component.html b/public/docs/_examples/testing/ts/app/dashboard/dashboard-hero.component.html new file mode 100644 index 0000000000..ff49bd17a5 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/dashboard/dashboard-hero.component.html @@ -0,0 +1,4 @@ + +
+ {{hero.name | uppercase}} +
diff --git a/public/docs/_examples/testing/ts/app/dashboard/dashboard-hero.component.spec.ts b/public/docs/_examples/testing/ts/app/dashboard/dashboard-hero.component.spec.ts new file mode 100644 index 0000000000..86e0a88d2e --- /dev/null +++ b/public/docs/_examples/testing/ts/app/dashboard/dashboard-hero.component.spec.ts @@ -0,0 +1,113 @@ +import { async, ComponentFixture, TestBed +} from '@angular/core/testing'; + +import { By } from '@angular/platform-browser'; +import { DebugElement } from '@angular/core'; + +import { addMatchers } from '../../testing'; + +import { Hero } from '../model/hero'; +import { DashboardHeroComponent } from './dashboard-hero.component'; + +beforeEach( addMatchers ); + +describe('DashboardHeroComponent when tested directly', () => { + + let comp: DashboardHeroComponent; + let expectedHero: Hero; + let fixture: ComponentFixture; + let heroEl: DebugElement; + + // #docregion setup, compile-components + // asynch beforeEach + beforeEach( async(() => { + TestBed.configureTestingModule({ + declarations: [ DashboardHeroComponent ], + }) + .compileComponents(); // compile template and css + })); + // #enddocregion compile-components + + // synchronous beforeEach + beforeEach(() => { + fixture = TestBed.createComponent(DashboardHeroComponent); + comp = fixture.componentInstance; + heroEl = fixture.debugElement.query(By.css('.hero')); // find hero element + + // pretend that it was wired to something that supplied a hero + expectedHero = new Hero(42, 'Test Name'); + comp.hero = expectedHero; + fixture.detectChanges(); // trigger initial data binding + }); + // #enddocregion setup + + // #docregion name-test + it('should display hero name', () => { + const expectedPipedName = expectedHero.name.toUpperCase(); + expect(heroEl.nativeElement.textContent).toContain(expectedPipedName); + }); + // #enddocregion name-test + + // #docregion click-test + it('should raise selected event when clicked', () => { + let selectedHero: Hero; + comp.selected.subscribe((hero: Hero) => selectedHero = hero); + + heroEl.triggerEventHandler('click', null); + expect(selectedHero).toBe(expectedHero); + }); + // #enddocregion click-test +}); + +////////////////// + +describe('DashboardHeroComponent when inside a test host', () => { + let testHost: TestHostComponent; + let fixture: ComponentFixture; + let heroEl: DebugElement; + + // #docregion test-host-setup + beforeEach( async(() => { + TestBed.configureTestingModule({ + declarations: [ DashboardHeroComponent, TestHostComponent ], // declare both + }).compileComponents(); + })); + + beforeEach(() => { + // create TestHosComponent instead of DashboardHeroComponent + fixture = TestBed.createComponent(TestHostComponent); + testHost = fixture.componentInstance; + heroEl = fixture.debugElement.query(By.css('.hero')); // find hero + fixture.detectChanges(); // trigger initial data binding + }); + // #enddocregion test-host-setup + + // #docregion test-host-tests + it('should display hero name', () => { + const expectedPipedName = testHost.hero.name.toUpperCase(); + expect(heroEl.nativeElement.textContent).toContain(expectedPipedName); + }); + + it('should raise selected event when clicked', () => { + heroEl.triggerEventHandler('click', null); + // selected hero should be the same data bound hero + expect(testHost.selectedHero).toBe(testHost.hero); + }); + // #enddocregion test-host-tests +}); + +////// Test Host Component ////// +import { Component } from '@angular/core'; + +// #docregion test-host +@Component({ + template: ` + + ` +}) +class TestHostComponent { + hero = new Hero(42, 'Test Name'); + selectedHero: Hero; + onSelected(hero: Hero) { this.selectedHero = hero; } +} +// #enddocregion test-host diff --git a/public/docs/_examples/testing/ts/app/dashboard/dashboard-hero.component.ts b/public/docs/_examples/testing/ts/app/dashboard/dashboard-hero.component.ts new file mode 100644 index 0000000000..3d8ee8a177 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/dashboard/dashboard-hero.component.ts @@ -0,0 +1,17 @@ +// #docregion +import { Component, EventEmitter, Input, Output } from '@angular/core'; + +import { Hero } from '../model'; + +// #docregion component +@Component({ + selector: 'dashboard-hero', + templateUrl: 'app/dashboard/dashboard-hero.component.html', + styleUrls: ['app/dashboard/dashboard-hero.component.css'] +}) +export class DashboardHeroComponent { + @Input() hero: Hero; + @Output() selected = new EventEmitter(); + click() { this.selected.next(this.hero); } +} +// #enddocregion component diff --git a/public/docs/_examples/testing/ts/app/dashboard.component.css b/public/docs/_examples/testing/ts/app/dashboard/dashboard.component.css similarity index 54% rename from public/docs/_examples/testing/ts/app/dashboard.component.css rename to public/docs/_examples/testing/ts/app/dashboard/dashboard.component.css index ce6e963a5f..98130b61c6 100644 --- a/public/docs/_examples/testing/ts/app/dashboard.component.css +++ b/public/docs/_examples/testing/ts/app/dashboard/dashboard.component.css @@ -1,5 +1,3 @@ -/* #docplaster */ -/* #docregion */ [class*='col-'] { float: left; } @@ -24,40 +22,14 @@ h3 { .col-1-4 { width: 25%; } -.module { - padding: 20px; - text-align: center; - color: #eee; - max-height: 120px; - min-width: 120px; - background-color: #607D8B; - border-radius: 2px; -} -h4 { - position: relative; -} -.module:hover { - background-color: #EEE; - cursor: pointer; - color: #607d8b; -} .grid-pad { padding: 10px 0; } .grid-pad > [class*='col-']:last-of-type { padding-right: 20px; } -@media (max-width: 600px) { - .module { - font-size: 10px; - max-height: 75px; } -} @media (max-width: 1024px) { .grid { margin: 0; } - .module { - min-width: 60px; - } } -/* #enddocregion */ diff --git a/public/docs/_examples/testing/ts/app/dashboard/dashboard.component.html b/public/docs/_examples/testing/ts/app/dashboard/dashboard.component.html new file mode 100644 index 0000000000..b26e16b828 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/dashboard/dashboard.component.html @@ -0,0 +1,9 @@ +

{{title}}

+ +
+ + + + +
diff --git a/public/docs/_examples/testing/ts/app/dashboard/dashboard.component.no-testbed.spec.ts b/public/docs/_examples/testing/ts/app/dashboard/dashboard.component.no-testbed.spec.ts new file mode 100644 index 0000000000..125e5193b7 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/dashboard/dashboard.component.no-testbed.spec.ts @@ -0,0 +1,57 @@ +import { Router } from '@angular/router'; + +import { DashboardComponent } from './dashboard.component'; +import { Hero } from '../model'; + +import { addMatchers } from '../../testing'; +import { FakeHeroService } from '../model/testing'; + +class FakeRouter { + navigateByUrl(url: string) { return url; } +} + +describe('DashboardComponent: w/o Angular TestBed', () => { + let comp: DashboardComponent; + let heroService: FakeHeroService; + let router: Router; + + beforeEach(() => { + addMatchers(); + router = new FakeRouter() as any as Router; + heroService = new FakeHeroService(); + comp = new DashboardComponent(router, heroService); + }); + + it('should NOT have heroes before calling OnInit', () => { + expect(comp.heroes.length).toBe(0, + 'should not have heroes before OnInit'); + }); + + it('should NOT have heroes immediately after OnInit', () => { + comp.ngOnInit(); // ngOnInit -> getHeroes + expect(comp.heroes.length).toBe(0, + 'should not have heroes until service promise resolves'); + }); + + it('should HAVE heroes after HeroService gets them', (done: DoneFn) => { + comp.ngOnInit(); // ngOnInit -> getHeroes + heroService.lastPromise // the one from getHeroes + .then(() => { + // throw new Error('deliberate error'); // see it fail gracefully + expect(comp.heroes.length).toBeGreaterThan(0, + 'should have heroes after service promise resolves'); + }) + .then(done, done.fail); + }); + + it('should tell ROUTER to navigate by hero id', () => { + const hero = new Hero(42, 'Abbracadabra'); + const spy = spyOn(router, 'navigateByUrl'); + + comp.gotoDetail(hero); + + const navArgs = spy.calls.mostRecent().args[0]; + expect(navArgs).toBe('/heroes/42', 'should nav to HeroDetail for Hero 42'); + }); + +}); diff --git a/public/docs/_examples/testing/ts/app/dashboard/dashboard.component.spec.ts b/public/docs/_examples/testing/ts/app/dashboard/dashboard.component.spec.ts new file mode 100644 index 0000000000..981e51db0f --- /dev/null +++ b/public/docs/_examples/testing/ts/app/dashboard/dashboard.component.spec.ts @@ -0,0 +1,147 @@ +// #docplaster +import { async, inject, ComponentFixture, TestBed +} from '@angular/core/testing'; + +import { addMatchers } from '../../testing'; +import { HeroService } from '../model'; +import { FakeHeroService } from '../model/testing'; + +import { By } from '@angular/platform-browser'; +import { Router } from '@angular/router'; + +import { DashboardComponent } from './dashboard.component'; +import { DashboardModule } from './dashboard.module'; + +// #docregion fake-router +class FakeRouter { + navigateByUrl(url: string) { return url; } +} +// #enddocregion fake-router + +beforeEach ( addMatchers ); + +let comp: DashboardComponent; +let fixture: ComponentFixture; + +//////// Deep //////////////// + +describe('DashboardComponent (deep)', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ DashboardModule ] + }); + }); + + compileAndCreate(); + + tests(clickForDeep); + + function clickForDeep() { + // get first
DebugElement + const heroEl = fixture.debugElement.query(By.css('.hero')); + heroEl.triggerEventHandler('click', null); + } +}); + +//////// Shallow //////////////// + +import { NO_ERRORS_SCHEMA } from '@angular/core'; + +describe('DashboardComponent (shallow)', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [ DashboardComponent ], + schemas: [NO_ERRORS_SCHEMA] + }); + }); + + compileAndCreate(); + + tests(clickForShallow); + + function clickForShallow() { + // get first DebugElement + const heroEl = fixture.debugElement.query(By.css('dashboard-hero')); + heroEl.triggerEventHandler('selected', comp.heroes[0]); + } +}); + +/** Add TestBed providers, compile, and create DashboardComponent */ +function compileAndCreate() { + // #docregion compile-and-create-body + beforeEach( async(() => { + TestBed.configureTestingModule({ + providers: [ + { provide: HeroService, useClass: FakeHeroService }, + { provide: Router, useClass: FakeRouter } + ] + }) + .compileComponents().then(() => { + fixture = TestBed.createComponent(DashboardComponent); + comp = fixture.componentInstance; + }); + // #enddocregion compile-and-create-body + })); +} + +/** + * The (almost) same tests for both. + * Only change: the way that the first hero is clicked + */ +function tests(heroClick: Function) { + + it('should NOT have heroes before ngOnInit', () => { + expect(comp.heroes.length).toBe(0, + 'should not have heroes before ngOnInit'); + }); + + it('should NOT have heroes immediately after ngOnInit', () => { + fixture.detectChanges(); // runs initial lifecycle hooks + + expect(comp.heroes.length).toBe(0, + 'should not have heroes until service promise resolves'); + }); + + describe('after get dashboard heroes', () => { + + // Trigger component so it gets heroes and binds to them + beforeEach( async(() => { + fixture.detectChanges(); // runs ngOnInit -> getHeroes + fixture.whenStable() // No need for the `lastPromise` hack! + .then(() => fixture.detectChanges()); // bind to heroes + })); + + it('should HAVE heroes', () => { + expect(comp.heroes.length).toBeGreaterThan(0, + 'should have heroes after service promise resolves'); + }); + + it('should DISPLAY heroes', () => { + // Find and examine the displayed heroes + // Look for them in the DOM by css class + const heroes = fixture.debugElement.queryAll(By.css('dashboard-hero')); + expect(heroes.length).toBe(4, 'should display 4 heroes'); + }); + + // #docregion navigate-test, inject + it('should tell ROUTER to navigate when hero clicked', + inject([Router], (router: Router) => { // ... + // #enddocregion inject + + const spy = spyOn(router, 'navigateByUrl'); + + heroClick(); // trigger click on first inner
+ + // args passed to router.navigateByUrl() + const navArgs = spy.calls.first().args[0]; + + // expecting to navigate to id of the component's first hero + const id = comp.heroes[0].id; + expect(navArgs).toBe('/heroes/' + id, + 'should nav to HeroDetail for first hero'); + // #docregion inject + })); + // #enddocregion navigate-test, inject + }); +} + diff --git a/public/docs/_examples/testing/ts/app/dashboard/dashboard.component.ts b/public/docs/_examples/testing/ts/app/dashboard/dashboard.component.ts new file mode 100644 index 0000000000..7c7f4cc077 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/dashboard/dashboard.component.ts @@ -0,0 +1,43 @@ +// #docregion +import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; + +import { Hero, HeroService } from '../model'; + +@Component({ + selector: 'app-dashboard', + templateUrl: 'app/dashboard/dashboard.component.html', + styleUrls: [ + 'app/shared/styles.css', + 'app/dashboard/dashboard.component.css' + ] +}) +export class DashboardComponent implements OnInit { + + heroes: Hero[] = []; + + // #docregion ctor + constructor( + private router: Router, + private heroService: HeroService) { + } + // #enddocregion ctor + + ngOnInit() { + this.heroService.getHeroes() + .then(heroes => this.heroes = heroes.slice(1, 5)); + } + + // #docregion goto-detail + gotoDetail(hero: Hero) { + let url = `/heroes/${hero.id}`; + this.router.navigateByUrl(url); + } + // #enddocregion goto-detail + + get title() { + let cnt = this.heroes.length; + return cnt === 0 ? 'No Heroes' : + cnt === 1 ? 'Top Hero' : `Top ${cnt} Heroes`; + } +} diff --git a/public/docs/_examples/testing/ts/app/dashboard/dashboard.module.ts b/public/docs/_examples/testing/ts/app/dashboard/dashboard.module.ts new file mode 100644 index 0000000000..8a70c851a0 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/dashboard/dashboard.module.ts @@ -0,0 +1,20 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { SharedModule } from '../shared/shared.module'; + +import { DashboardComponent } from './dashboard.component'; +import { DashboardHeroComponent } from './dashboard-hero.component'; + +const routes: Routes = [ + { path: 'dashboard', component: DashboardComponent }, +]; + +@NgModule({ + imports: [ + SharedModule, + RouterModule.forChild(routes) + ], + declarations: [ DashboardComponent, DashboardHeroComponent ] +}) +export class DashboardModule { } diff --git a/public/docs/_examples/testing/ts/app/hero-detail.component.html b/public/docs/_examples/testing/ts/app/hero-detail.component.html deleted file mode 100644 index cf96fc2169..0000000000 --- a/public/docs/_examples/testing/ts/app/hero-detail.component.html +++ /dev/null @@ -1,14 +0,0 @@ - - -
-

{{hero.name}} details!

-
- {{hero.id}}
-
- - -
- - - -
\ No newline at end of file diff --git a/public/docs/_examples/testing/ts/app/hero-detail.component.ts b/public/docs/_examples/testing/ts/app/hero-detail.component.ts deleted file mode 100644 index 3fcbf071e0..0000000000 --- a/public/docs/_examples/testing/ts/app/hero-detail.component.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* tslint:disable */ -// #docplaster -// #docregion -// #docregion v2 -// #docregion import-oninit -import { Component, OnInit } from '@angular/core'; -// #enddocregion import-oninit -// #docregion import-route-params -import { RouteParams } from '@angular/router-deprecated'; -// #enddocregion import-route-params - -import { Hero } from './hero'; -// #docregion import-hero-service -import { HeroService } from './hero.service'; -// #enddocregion import-hero-service - -// #docregion extract-template -@Component({ - selector: 'my-hero-detail', - // #docregion template-url - templateUrl: 'app/hero-detail.component.html', - // #enddocregion template-url -// #enddocregion v2 - styleUrls: ['app/hero-detail.component.css'], - inputs: ['hero'] -// #docregion v2 -}) -// #enddocregion extract-template -// #docregion implement -export class HeroDetailComponent implements OnInit { -// #enddocregion implement - hero: Hero; - -// #docregion ctor - constructor( - private _heroService: HeroService, - private _routeParams: RouteParams) { - } -// #enddocregion ctor - -// #docregion ng-oninit - ngOnInit() { - // #docregion get-id - let id = +this._routeParams.get('id'); - // #enddocregion get-id - this._heroService.getHero(id) - .then(hero => this.hero = hero); - } -// #enddocregion ng-oninit - -// #docregion go-back - goBack() { - window.history.back(); - } -// #enddocregion go-back -} -// #enddocregion v2 -// #enddocregion diff --git a/public/docs/_examples/testing/ts/app/hero.service.ts b/public/docs/_examples/testing/ts/app/hero.service.ts deleted file mode 100644 index e9473e4038..0000000000 --- a/public/docs/_examples/testing/ts/app/hero.service.ts +++ /dev/null @@ -1,28 +0,0 @@ -// #docplaster -// #docregion -import { Hero } from './hero'; -import { HEROES } from './mock-heroes'; -import { Injectable } from '@angular/core'; - -@Injectable() -export class HeroService { - getHeroes() { - return Promise.resolve(HEROES); - } - - // See the "Take it slow" appendix - getHeroesSlowly() { - return new Promise(resolve => - setTimeout(() => resolve(HEROES), 2000) // 2 seconds - ); - } - - // #docregion get-hero - getHero(id: number) { - return Promise.resolve(HEROES).then( - heroes => heroes.find(hero => hero.id === id) - ); - } - // #enddocregion get-hero -} -// #enddocregion diff --git a/public/docs/_examples/testing/ts/app/hero.spec.ts b/public/docs/_examples/testing/ts/app/hero.spec.ts deleted file mode 100644 index 78a73ad6b0..0000000000 --- a/public/docs/_examples/testing/ts/app/hero.spec.ts +++ /dev/null @@ -1,50 +0,0 @@ -// #docregion -// #docplaster -// #docregion base-hero-spec -import { Hero } from './hero'; - -describe('Hero', () => { - - it('has name', () => { - let hero: Hero = {id: 1, name: 'Super Cat'}; - expect(hero.name).toEqual('Super Cat'); - }); - - it('has id', () => { - let hero: Hero = {id: 1, name: '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 diff --git a/public/docs/_examples/testing/ts/app/hero.ts b/public/docs/_examples/testing/ts/app/hero.ts deleted file mode 100644 index 8f7cc205c8..0000000000 --- a/public/docs/_examples/testing/ts/app/hero.ts +++ /dev/null @@ -1,5 +0,0 @@ -// #docregion -export class Hero { - id: number; - name: string; -} diff --git a/public/docs/_examples/testing/ts/app/hero-detail.component.css b/public/docs/_examples/testing/ts/app/hero/hero-detail.component.css similarity index 93% rename from public/docs/_examples/testing/ts/app/hero-detail.component.css rename to public/docs/_examples/testing/ts/app/hero/hero-detail.component.css index ab2437efd8..f6139ba274 100644 --- a/public/docs/_examples/testing/ts/app/hero-detail.component.css +++ b/public/docs/_examples/testing/ts/app/hero/hero-detail.component.css @@ -1,4 +1,3 @@ -/* #docregion */ label { display: inline-block; width: 3em; @@ -25,6 +24,6 @@ button:hover { } button:disabled { background-color: #eee; - color: #ccc; + color: #ccc; cursor: auto; } diff --git a/public/docs/_examples/testing/ts/app/hero/hero-detail.component.html b/public/docs/_examples/testing/ts/app/hero/hero-detail.component.html new file mode 100644 index 0000000000..6927fc83ad --- /dev/null +++ b/public/docs/_examples/testing/ts/app/hero/hero-detail.component.html @@ -0,0 +1,11 @@ +
+

{{hero.name | titlecase}} Details

+
+ {{hero.id}}
+
+ + +
+ + +
diff --git a/public/docs/_examples/testing/ts/app/hero/hero-detail.component.no-testbed.spec.ts b/public/docs/_examples/testing/ts/app/hero/hero-detail.component.no-testbed.spec.ts new file mode 100644 index 0000000000..73c22f29e7 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/hero/hero-detail.component.no-testbed.spec.ts @@ -0,0 +1,58 @@ +import { HeroDetailComponent } from './hero-detail.component'; +import { Hero } from '../model'; + +import { FakeActivatedRoute } from '../../testing'; + +////////// Tests //////////////////// + +describe('HeroDetailComponent - no TestBed', () => { + let activatedRoute: FakeActivatedRoute; + let comp: HeroDetailComponent; + let expectedHero: Hero; + let hds: any; + let router: any; + + beforeEach( done => { + expectedHero = new Hero(42, 'Bubba'); + activatedRoute = new FakeActivatedRoute(); + activatedRoute.testParams = { id: expectedHero.id }; + + router = jasmine.createSpyObj('router', ['navigate']); + + hds = jasmine.createSpyObj('HeroDetailService', ['getHero', 'saveHero']); + hds.getHero.and.returnValue(Promise.resolve(expectedHero)); + hds.saveHero.and.returnValue(Promise.resolve(expectedHero)); + + comp = new HeroDetailComponent(hds, activatedRoute, router); + comp.ngOnInit(); + + // OnInit calls HDS.getHero; wait for it to get the fake hero + hds.getHero.calls.first().returnValue.then(done); + }); + + it('should expose the hero retrieved from the service', () => { + expect(comp.hero).toBe(expectedHero); + }); + + it('should navigate when click cancel', () => { + comp.cancel(); + expect(router.navigate.calls.any()).toBe(true, 'router.navigate called'); + }); + + it('should save when click save', () => { + comp.save(); + expect(hds.saveHero.calls.any()).toBe(true, 'HeroDetailService.save called'); + expect(router.navigate.calls.any()).toBe(false, 'router.navigate not called yet'); + }); + + it('should navigate when click save resolves', done => { + comp.save(); + // waits for async save to complete before navigating + hds.saveHero.calls.first().returnValue + .then(() => { + expect(router.navigate.calls.any()).toBe(true, 'router.navigate called'); + done(); + }); + }); + +}); diff --git a/public/docs/_examples/testing/ts/app/hero/hero-detail.component.spec.ts b/public/docs/_examples/testing/ts/app/hero/hero-detail.component.spec.ts new file mode 100644 index 0000000000..0dd52ed54e --- /dev/null +++ b/public/docs/_examples/testing/ts/app/hero/hero-detail.component.spec.ts @@ -0,0 +1,196 @@ +import { + async, ComponentFixture, fakeAsync, inject, TestBed, tick +} from '@angular/core/testing'; + +import { By } from '@angular/platform-browser'; +import { DebugElement } from '@angular/core'; + +import { + addMatchers, newEvent, + ActivatedRoute, FakeActivatedRoute, Router, FakeRouter +} from '../../testing'; + +import { HEROES, FakeHeroService } from '../model/testing'; + +import { HeroModule } from './hero.module'; +import { HeroDetailComponent } from './hero-detail.component'; +import { HeroDetailService } from './hero-detail.service'; +import { Hero, HeroService } from '../model'; + +////// Testing Vars ////// +let activatedRoute: FakeActivatedRoute; +let comp: HeroDetailComponent; +let fixture: ComponentFixture; +let page: Page; + +////////// Tests //////////////////// + +describe('HeroDetailComponent', () => { + + beforeEach( async(() => { + addMatchers(); + activatedRoute = new FakeActivatedRoute(); + + TestBed.configureTestingModule({ + imports: [ HeroModule ], + + // DON'T RE-DECLARE because already declared in HeroModule + // declarations: [HeroDetailComponent, TitleCasePipe], // No! + + providers: [ + { provide: ActivatedRoute, useValue: activatedRoute }, + { provide: HeroService, useClass: FakeHeroService }, + { provide: Router, useClass: FakeRouter}, + ] + }) + .compileComponents(); + })); + + describe('when navigate to hero id=' + HEROES[0].id, () => { + let expectedHero: Hero; + + beforeEach( async(() => { + expectedHero = HEROES[0]; + activatedRoute.testParams = { id: expectedHero.id }; + createComponent(); + })); + + it('should display that hero\'s name', () => { + expect(page.nameDisplay.textContent).toBe(expectedHero.name); + }); + + it('should navigate when click cancel', () => { + page.cancelBtn.triggerEventHandler('click', null); + expect(page.navSpy.calls.any()).toBe(true, 'router.navigate called'); + }); + + it('should save when click save', () => { + page.saveBtn.triggerEventHandler('click', null); + expect(page.saveSpy.calls.any()).toBe(true, 'HeroDetailService.save called'); + }); + + it('should navigate when click click save resolves', fakeAsync(() => { + page.saveBtn.triggerEventHandler('click', null); + tick(); // waits for async save to "complete" before navigating + expect(page.navSpy.calls.any()).toBe(true, 'router.navigate called'); + })); + + + // #docregion title-case-pipe + it('should convert original hero name to Title Case', () => { + expect(page.nameDisplay.textContent).toBe(comp.hero.name); + }); + // #enddocregion title-case-pipe + + it('should convert hero name to Title Case', fakeAsync(() => { + const inputName = 'quick BROWN fox'; + const expectedName = 'Quick Brown Fox'; + + // simulate user entering new name in input + page.nameInput.value = inputName; + + // dispatch a DOM event so that Angular learns of input value change. + // detectChanges() makes ngModel push input value to component property + // and Angular updates the output span + page.nameInput.dispatchEvent(newEvent('input')); + fixture.detectChanges(); + expect(page.nameDisplay.textContent).toBe(expectedName, 'hero name display'); + expect(comp.hero.name).toBe(inputName, 'comp.hero.name'); + })); + + }); + + describe('when navigate with no hero id', () => { + beforeEach( async( createComponent )); + + it('should have hero.id === 0', () => { + expect(comp.hero.id).toBe(0); + }); + + it('should display empty hero name', () => { + expect(page.nameDisplay.textContent).toBe(''); + }); + }); + + describe('when navigate to non-existant hero id', () => { + beforeEach( async(() => { + activatedRoute.testParams = { id: 99999 }; + createComponent(); + })); + + it('should try to navigate back to hero list', () => { + expect(page.gotoSpy.calls.any()).toBe(true, 'comp.gotoList called'); + expect(page.navSpy.calls.any()).toBe(true, 'router.navigate called'); + }); + }); + + /////////////////////////// + + // Why we must use `fixture.debugElement.injector` in `Page()` + it('cannot use `inject` to get component\'s provided service', () => { + let service: HeroDetailService; + fixture = TestBed.createComponent(HeroDetailComponent); + expect( + // Throws because `inject` only has access to TestBed's injector + // which is an ancestor of the component's injector + inject([HeroDetailService], (hds: HeroDetailService) => service = hds ) + ) + .toThrowError(/No provider for HeroDetailService/); + + // get `HeroDetailService` with component's own injector + service = fixture.debugElement.injector.get(HeroDetailService); + expect(service).toBeDefined('debugElement.injector'); + }); +}); + +/////////// Helpers ///// + +/** Create the HeroDetailComponent, initialize it, set test variables */ +function createComponent() { + fixture = TestBed.createComponent(HeroDetailComponent); + comp = fixture.componentInstance; + page = new Page(); + + // change detection triggers ngOnInit which gets a hero + fixture.detectChanges(); + return fixture.whenStable().then(() => { + // got the hero and updated component + // change detection updates the view + fixture.detectChanges(); + page.addPageElements(); + }); +} + +class Page { + gotoSpy: jasmine.Spy; + navSpy: jasmine.Spy; + saveSpy: jasmine.Spy; + + saveBtn: DebugElement; + cancelBtn: DebugElement; + nameDisplay: HTMLElement; + nameInput: HTMLInputElement; + + constructor() { + // Use component's injector to see the services it injected. + let compInjector = fixture.debugElement.injector; + let hds = compInjector.get(HeroDetailService); + let router = compInjector.get(Router); + this.gotoSpy = spyOn(comp, 'gotoList').and.callThrough(); + this.saveSpy = spyOn(hds, 'saveHero').and.callThrough(); + this.navSpy = spyOn(router, 'navigate').and.callThrough(); + } + + /** Add page elements after page initializes */ + addPageElements() { + if (comp.hero) { + // have a hero so these DOM elements can be reached + let buttons = fixture.debugElement.queryAll(By.css('button')); + this.saveBtn = buttons[0]; + this.cancelBtn = buttons[1]; + this.nameDisplay = fixture.debugElement.query(By.css('span')).nativeElement; + this.nameInput = fixture.debugElement.query(By.css('input')).nativeElement; + } + } +} + diff --git a/public/docs/_examples/testing/ts/app/hero/hero-detail.component.ts b/public/docs/_examples/testing/ts/app/hero/hero-detail.component.ts new file mode 100644 index 0000000000..9350c369af --- /dev/null +++ b/public/docs/_examples/testing/ts/app/hero/hero-detail.component.ts @@ -0,0 +1,52 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; + +import { Hero } from '../model'; +import { HeroDetailService } from './hero-detail.service'; + +@Component({ + selector: 'app-hero-detail', + templateUrl: 'app/hero/hero-detail.component.html', + styleUrls: [ + 'app/shared/styles.css', + 'app/hero/hero-detail.component.css' + ], + providers: [ HeroDetailService ] +}) +export class HeroDetailComponent implements OnInit { + @Input() hero: Hero; + + constructor( + private heroDetailService: HeroDetailService, + private route: ActivatedRoute, + private router: Router) { + } + + ngOnInit() { + let id = this.route.snapshot.params['id']; + + // tslint:disable-next-line:triple-equals + if (id == undefined) { + // no id; act as if is new + this.hero = new Hero(); + } else { + this.heroDetailService.getHero(id).then(hero => { + if (hero) { + this.hero = hero; + } else { + this.gotoList(); // id not found; navigate to list + } + }); + } + } + + save() { + this.heroDetailService.saveHero(this.hero).then(() => this.gotoList()); + } + + cancel() { this.gotoList(); } + + gotoList() { + this.router.navigate(['../'], {relativeTo: this.route}); + } +} diff --git a/public/docs/_examples/testing/ts/app/hero/hero-detail.service.ts b/public/docs/_examples/testing/ts/app/hero/hero-detail.service.ts new file mode 100644 index 0000000000..970cb1b98b --- /dev/null +++ b/public/docs/_examples/testing/ts/app/hero/hero-detail.service.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@angular/core'; + +import { Hero, HeroService } from '../model'; + +@Injectable() +export class HeroDetailService { + constructor(private heroService: HeroService) { } + + getHero(id: number | string): Promise { + if (typeof id === 'string') { + id = parseInt(id as string, 10); + } + return this.heroService.getHero(id).then(hero => { + return hero ? Object.assign({}, hero) : null; // clone or null + }); + } + + saveHero(hero: Hero) { + return this.heroService.updateHero(hero); + } +} diff --git a/public/docs/_examples/testing/ts/app/heroes.component.css b/public/docs/_examples/testing/ts/app/hero/hero-list.component.css similarity index 100% rename from public/docs/_examples/testing/ts/app/heroes.component.css rename to public/docs/_examples/testing/ts/app/hero/hero-list.component.css diff --git a/public/docs/_examples/testing/ts/app/hero/hero-list.component.html b/public/docs/_examples/testing/ts/app/hero/hero-list.component.html new file mode 100644 index 0000000000..cd37301fd6 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/hero/hero-list.component.html @@ -0,0 +1,8 @@ +

My Heroes

+
    +
  • + {{hero.id}} {{hero.name}} +
  • +
diff --git a/public/docs/_examples/testing/ts/app/hero/hero-list.component.spec.ts b/public/docs/_examples/testing/ts/app/hero/hero-list.component.spec.ts new file mode 100644 index 0000000000..f997cf787e --- /dev/null +++ b/public/docs/_examples/testing/ts/app/hero/hero-list.component.spec.ts @@ -0,0 +1,139 @@ +import { async, ComponentFixture, fakeAsync, TestBed, tick +} from '@angular/core/testing'; + +import { By } from '@angular/platform-browser'; +import { DebugElement } from '@angular/core'; + +import { addMatchers, newEvent, Router, FakeRouter +} from '../../testing'; + +import { HEROES, FakeHeroService } from '../model/testing'; + +import { HeroModule } from './hero.module'; +import { HeroListComponent } from './hero-list.component'; +import { HighlightDirective } from '../shared/highlight.directive'; +import { HeroService } from '../model'; + +let comp: HeroListComponent; +let fixture: ComponentFixture; +let page: Page; + +/////// Tests ////// + +describe('HeroListComponent', () => { + + beforeEach( async(() => { + addMatchers(); + TestBed.configureTestingModule({ + imports: [HeroModule], + providers: [ + { provide: HeroService, useClass: FakeHeroService }, + { provide: Router, useClass: FakeRouter} + ] + }) + .compileComponents() + .then(createComponent); + })); + + it('should display heroes', () => { + expect(page.heroRows.length).toBeGreaterThan(0); + }); + + it('1st hero should match 1st test hero', () => { + const expectedHero = HEROES[0]; + const actualHero = page.heroRows[0].textContent; + expect(actualHero).toContain(expectedHero.id, 'hero.id'); + expect(actualHero).toContain(expectedHero.name, 'hero.name'); + }); + + it('should select hero on click', fakeAsync(() => { + const expectedHero = HEROES[1]; + const li = page.heroRows[1]; + li.dispatchEvent(newEvent('click')); + tick(); + // `.toEqual` because selectedHero is clone of expectedHero; see FakeHeroService + expect(comp.selectedHero).toEqual(expectedHero); + })); + + it('should navigate to selected hero detail on click', fakeAsync(() => { + const expectedHero = HEROES[1]; + const li = page.heroRows[1]; + li.dispatchEvent(newEvent('click')); + tick(); + + // should have navigated + expect(page.navSpy.calls.any()).toBe(true, 'navigate called'); + + // composed hero detail will be URL like 'heroes/42' + // expect link array with the route path and hero id + // first argument to router.navigate is link array + const navArgs = page.navSpy.calls.first().args[0]; + expect(navArgs[0]).toContain('heroes', 'nav to heroes detail URL'); + expect(navArgs[1]).toBe(expectedHero.id, 'expected hero.id'); + + })); + + it('should find `HighlightDirective` with `By.directive', () => { + // #docregion by + // Can find DebugElement either by css selector or by directive + const h2 = fixture.debugElement.query(By.css('h2')); + const directive = fixture.debugElement.query(By.directive(HighlightDirective)); + // #enddocregion by + expect(h2).toBe(directive); + }); + + it('should color header with `HighlightDirective`', () => { + const h2 = page.highlightDe.nativeElement as HTMLElement; + const bgColor = h2.style.backgroundColor; + + // different browsers report color values differently + const isExpectedColor = bgColor === 'gold' || bgColor === 'rgb(255, 215, 0)'; + expect(isExpectedColor).toBe(true, 'backgroundColor'); + }); + + it('the `HighlightDirective` is among the element\'s providers', () => { + expect(page.highlightDe.providerTokens).toContain(HighlightDirective, 'HighlightDirective'); + }); +}); + +/////////// Helpers ///// + +/** Create the component and set the `page` test variables */ +function createComponent() { + fixture = TestBed.createComponent(HeroListComponent); + comp = fixture.componentInstance; + + // change detection triggers ngOnInit which gets a hero + fixture.detectChanges(); + + return fixture.whenStable().then(() => { + // got the heroes and updated component + // change detection updates the view + fixture.detectChanges(); + page = new Page(); + }); +} + +class Page { + /** Hero line elements */ + heroRows: HTMLLIElement[]; + + /** Highlighted element */ + highlightDe: DebugElement; + + /** Spy on router navigate method */ + navSpy: jasmine.Spy; + + constructor() { + this.heroRows = fixture.debugElement.queryAll(By.css('li')).map(de => de.nativeElement); + + // Find the first element with an attached HighlightDirective + this.highlightDe = fixture.debugElement.query(By.directive(HighlightDirective)); + + // Get the component's injected router and spy on it + const router = fixture.debugElement.injector.get(Router); + this.navSpy = spyOn(router, 'navigate').and.callThrough(); + }; +} + + diff --git a/public/docs/_examples/testing/ts/app/hero/hero-list.component.ts b/public/docs/_examples/testing/ts/app/hero/hero-list.component.ts new file mode 100644 index 0000000000..d4ad30b019 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/hero/hero-list.component.ts @@ -0,0 +1,30 @@ +import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; + +import { Hero, HeroService } from '../model'; + +@Component({ + selector: 'app-heroes', + templateUrl: 'app/hero/hero-list.component.html', + styleUrls: [ + 'app/shared/styles.css', + 'app/hero/hero-list.component.css' + ] +}) +export class HeroListComponent implements OnInit { + heroes: Promise; + selectedHero: Hero; + + constructor( + private router: Router, + private heroService: HeroService) { } + + ngOnInit() { + this.heroes = this.heroService.getHeroes(); + } + + onSelect(hero: Hero) { + this.selectedHero = hero; + this.router.navigate(['../heroes', this.selectedHero.id ]); + } +} diff --git a/public/docs/_examples/testing/ts/app/hero/hero.module.ts b/public/docs/_examples/testing/ts/app/hero/hero.module.ts new file mode 100644 index 0000000000..541d49103f --- /dev/null +++ b/public/docs/_examples/testing/ts/app/hero/hero.module.ts @@ -0,0 +1,9 @@ +import { NgModule } from '@angular/core'; +import { SharedModule } from '../shared/shared.module'; +import { routedComponents, routing } from './hero.routing'; + +@NgModule({ + imports: [ SharedModule, routing ], + declarations: [ routedComponents ] +}) +export class HeroModule { } diff --git a/public/docs/_examples/testing/ts/app/hero/hero.routing.ts b/public/docs/_examples/testing/ts/app/hero/hero.routing.ts new file mode 100644 index 0000000000..9530bc3953 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/hero/hero.routing.ts @@ -0,0 +1,12 @@ +import { RouterModule, Routes } from '@angular/router'; + +import { HeroListComponent } from './hero-list.component'; +import { HeroDetailComponent } from './hero-detail.component'; + +const routes: Routes = [ + { path: '', component: HeroListComponent }, + { path: ':id', component: HeroDetailComponent } +]; + +export const routedComponents = [HeroDetailComponent, HeroListComponent]; +export const routing = RouterModule.forChild(routes); diff --git a/public/docs/_examples/testing/ts/app/heroes.component.html b/public/docs/_examples/testing/ts/app/heroes.component.html deleted file mode 100644 index cce1853d30..0000000000 --- a/public/docs/_examples/testing/ts/app/heroes.component.html +++ /dev/null @@ -1,21 +0,0 @@ - - -

My Heroes

-
    -
  • - {{hero.id}} {{hero.name}} -
  • -
- -
-

- - {{selectedHero.name | uppercase}} is my hero - -

- -
- - diff --git a/public/docs/_examples/testing/ts/app/heroes.component.ts b/public/docs/_examples/testing/ts/app/heroes.component.ts deleted file mode 100644 index 1e2651f256..0000000000 --- a/public/docs/_examples/testing/ts/app/heroes.component.ts +++ /dev/null @@ -1,50 +0,0 @@ -// #docplaster -// #docregion -import { Component, OnInit } from '@angular/core'; -import { Router } from '@angular/router-deprecated'; - -import { Hero } from './hero'; -import { HeroDetailComponent } from './hero-detail.component'; -import { HeroService } from './hero.service'; - -// #docregion metadata -// #docregion heroes-component-renaming -@Component({ - selector: 'my-heroes', -// #enddocregion heroes-component-renaming - templateUrl: 'app/heroes.component.html', - styleUrls: ['app/heroes.component.css'], - directives: [HeroDetailComponent] -// #docregion heroes-component-renaming -}) -// #enddocregion heroes-component-renaming -// #enddocregion metadata -// #docregion class -// #docregion heroes-component-renaming -export class HeroesComponent implements OnInit { -// #enddocregion heroes-component-renaming - heroes: Hero[]; - selectedHero: Hero; - - constructor( - private _router: Router, - private _heroService: HeroService) { } - - getHeroes() { - this._heroService.getHeroes().then(heroes => this.heroes = heroes); - } - - ngOnInit() { - this.getHeroes(); - } - - onSelect(hero: Hero) { this.selectedHero = hero; } - - gotoDetail() { - this._router.navigate(['HeroDetail', { id: this.selectedHero.id }]); - } -// #docregion heroes-component-renaming -} -// #enddocregion heroes-component-renaming -// #enddocregion class -// #enddocregion diff --git a/public/docs/_examples/testing/ts/app/main.ts b/public/docs/_examples/testing/ts/app/main.ts index 5f185788a3..2c89d35a81 100644 --- a/public/docs/_examples/testing/ts/app/main.ts +++ b/public/docs/_examples/testing/ts/app/main.ts @@ -1,5 +1,5 @@ -import { bootstrap } from '@angular/platform-browser-dynamic'; -import { AppComponent } from './app.component'; - -bootstrap(AppComponent); +// main app entry point +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; +import { AppModule } from './app.module'; +platformBrowserDynamic().bootstrapModule(AppModule); diff --git a/public/docs/_examples/testing/ts/app/mock-hero.service.ts b/public/docs/_examples/testing/ts/app/mock-hero.service.ts deleted file mode 100644 index b1538be366..0000000000 --- a/public/docs/_examples/testing/ts/app/mock-hero.service.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { HEROES } from './mock-heroes'; -import { Hero } from './hero'; -import { HeroService } from './hero.service'; - -export { Hero } from './hero'; -export { HeroService } from './hero.service'; - -export class MockHeroService implements HeroService { - - mockHeroes = HEROES.slice(); - lastPromise: Promise; // so we can spy on promise calls - - getHero(id: number) { - return this.lastPromise = Promise.resolve(this.mockHeroes[0]); - } - - getHeroes() { - return this.lastPromise = Promise.resolve(this.mockHeroes); - } - - getHeroesSlowly() { return this.getHeroes(); } -} diff --git a/public/docs/_examples/testing/ts/app/mock-heroes.ts b/public/docs/_examples/testing/ts/app/mock-heroes.ts deleted file mode 100644 index ddd36d7868..0000000000 --- a/public/docs/_examples/testing/ts/app/mock-heroes.ts +++ /dev/null @@ -1,16 +0,0 @@ -// #docregion -import { Hero } from './hero'; - -export var HEROES: Hero[] = [ - {id: 11, name: 'Mr. Nice'}, - {id: 12, name: 'Narco'}, - {id: 13, name: 'Bombasto'}, - {id: 14, name: 'Celeritas'}, - {id: 15, name: 'Magneta'}, - {id: 16, name: 'RubberMan'}, - {id: 17, name: 'Dynama'}, - {id: 18, name: 'Dr IQ'}, - {id: 19, name: 'Magma'}, - {id: 20, name: 'Tornado'} -]; -// #enddocregion diff --git a/public/docs/_examples/testing/ts/app/mock-router.ts b/public/docs/_examples/testing/ts/app/mock-router.ts deleted file mode 100644 index a49763f7cf..0000000000 --- a/public/docs/_examples/testing/ts/app/mock-router.ts +++ /dev/null @@ -1,217 +0,0 @@ -/* tslint:disable */ -export * from '@angular/router-deprecated'; - -import { Directive, DynamicComponentLoader, ViewContainerRef, - Injectable, Optional, Input } from '@angular/core'; - -import { ComponentInstruction, Instruction, - Router, RouterOutlet} from '@angular/router-deprecated'; - -let _resolveToTrue = Promise.resolve(true); - -const NOT_IMPLEMENTED = (what: string) => { - throw new Error (`"${what}" is not implemented`); -}; - - -@Directive({ - selector: '[routerLink]', - host: { - '(click)': 'onClick()', - '[attr.href]': 'visibleHref', - '[class.router-link-active]': 'isRouteActive' - } -}) -export class MockRouterLink { - - isRouteActive = false; - visibleHref: string; // the url displayed on the anchor element. - - @Input('routerLink') routeParams: any[]; - @Input() target: string; - navigatedTo: any[] = null; - - constructor(public router: Router) { } - - onClick() { - this.navigatedTo = null; - - // If no target, or if target is _self, prevent default browser behavior - if (!this.target || typeof this.target !== 'string' || this.target === '_self') { - this.navigatedTo = this.routeParams; - return false; - } - return true; - } -} - -@Directive({selector: 'router-outlet'}) -export class MockRouterOutlet extends RouterOutlet { - name: string = null; - - constructor( - _viewContainerRef: ViewContainerRef, - @Optional() _loader: DynamicComponentLoader, - _parentRouter: Router, - nameAttr: string) { - super(_viewContainerRef, _loader, _parentRouter, nameAttr); - if (nameAttr) { - this.name = nameAttr; - } - } - - /** - * Called by the Router to instantiate a new component during the commit phase of a navigation. - * This method in turn is responsible for calling the `routerOnActivate` hook of its child. - */ - activate(nextInstruction: ComponentInstruction): Promise { NOT_IMPLEMENTED('activate'); return _resolveToTrue; } - - /** - * Called by the {@link Router} during the commit phase of a navigation when an outlet - * reuses a component between different routes. - * This method in turn is responsible for calling the `routerOnReuse` hook of its child. - */ - reuse(nextInstruction: ComponentInstruction): Promise { NOT_IMPLEMENTED('reuse'); return _resolveToTrue; } - - /** - * Called by the {@link Router} when an outlet disposes of a component's contents. - * This method in turn is responsible for calling the `routerOnDeactivate` hook of its child. - */ - deactivate(nextInstruction: ComponentInstruction): Promise { NOT_IMPLEMENTED('deactivate'); return _resolveToTrue; } - - /** - * Called by the {@link Router} during recognition phase of a navigation. - * - * If this resolves to `false`, the given navigation is cancelled. - * - * This method delegates to the child component's `routerCanDeactivate` hook if it exists, - * and otherwise resolves to true. - */ - routerCanDeactivate(nextInstruction: ComponentInstruction): Promise { - NOT_IMPLEMENTED('routerCanDeactivate'); return _resolveToTrue; - } - - /** - * Called by the {@link Router} during recognition phase of a navigation. - * - * If the new child component has a different Type than the existing child component, - * this will resolve to `false`. You can't reuse an old component when the new component - * is of a different Type. - * - * Otherwise, this method delegates to the child component's `routerCanReuse` hook if it exists, - * or resolves to true if the hook is not present. - */ - routerCanReuse(nextInstruction: ComponentInstruction): Promise { NOT_IMPLEMENTED('routerCanReuse'); return _resolveToTrue; } - -} - -@Injectable() -export class MockRouter extends Router { - - mockIsRouteActive = false; - mockRecognizedInstruction: Instruction; - outlet: RouterOutlet = null; - - constructor() { - super(null, null, null, null); - } - - auxRouter(hostComponent: any): Router { return new MockChildRouter(this, hostComponent); } - childRouter(hostComponent: any): Router { return new MockChildRouter(this, hostComponent); } - - commit(instruction: Instruction, _skipLocationChange = false): Promise { - NOT_IMPLEMENTED('commit'); return _resolveToTrue; - } - - deactivate(instruction: Instruction, _skipLocationChange = false): Promise { - NOT_IMPLEMENTED('deactivate'); return _resolveToTrue; - } - - /** - * Generate an `Instruction` based on the provided Route Link DSL. - */ - generate(linkParams: any[]): Instruction { - NOT_IMPLEMENTED('generate'); return null; - } - - isRouteActive(instruction: Instruction): boolean { return this.mockIsRouteActive; } - - /** - * Navigate based on the provided Route Link DSL. It's preferred to navigate with this method - * over `navigateByUrl`. - * - * ### Usage - * - * This method takes an array representing the Route Link DSL: - * ``` - * ['./MyCmp', {param: 3}] - * ``` - * See the {@link RouterLink} directive for more. - */ - navigate(linkParams: any[]): Promise { - return Promise.resolve(linkParams); - } - - /** - * Navigate to a URL. Returns a promise that resolves when navigation is complete. - * It's preferred to navigate with `navigate` instead of this method, since URLs are more brittle. - * - * If the given URL begins with a `/`, router will navigate absolutely. - * If the given URL does not begin with `/`, the router will navigate relative to this component. - */ - navigateByUrl(url: string, _skipLocationChange = false): Promise { - return Promise.resolve(url); - } - - - /** - * Navigate via the provided instruction. Returns a promise that resolves when navigation is - * complete. - */ - navigateByInstruction(instruction: Instruction, _skipLocationChange = false): Promise { - return Promise.resolve(instruction); - } - - /** - * Subscribe to URL updates from the router - */ - subscribe(onNext: (v: any) => void, onError?: (v: any) => void) { - return {onNext, onError}; - } - - /** - * Given a URL, returns an instruction representing the component graph - */ - recognize(url: string): Promise { - return Promise.resolve(this.mockRecognizedInstruction); - } - - registerPrimaryOutlet(outlet: RouterOutlet): Promise { - this.outlet = outlet; - return super.registerPrimaryOutlet(outlet); - } - - unregisterPrimaryOutlet(outlet: RouterOutlet) { - super.unregisterPrimaryOutlet(outlet); - this.outlet = null; - } -} - -class MockChildRouter extends MockRouter { - constructor(parent: MockRouter, hostComponent: any) { - super(); - this.parent = parent; - } - - - navigateByUrl(url: string, _skipLocationChange = false): Promise { - // Delegate navigation to the root router - return this.parent.navigateByUrl(url, _skipLocationChange); - } - - navigateByInstruction(instruction: Instruction, _skipLocationChange = false): - Promise { - // Delegate navigation to the root router - return this.parent.navigateByInstruction(instruction, _skipLocationChange); - } -} diff --git a/public/docs/_examples/testing/ts/app/model/hero.service.ts b/public/docs/_examples/testing/ts/app/model/hero.service.ts new file mode 100644 index 0000000000..7f2931a7f6 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/model/hero.service.ts @@ -0,0 +1,29 @@ +import { Injectable } from '@angular/core'; + +import { Hero } from './hero'; +import { HEROES } from './test-heroes'; + +@Injectable() +/** Dummy HeroService that pretends to be real */ +export class HeroService { + getHeroes() { + return Promise.resolve(HEROES); + } + + getHero(id: number | string): Promise { + if (typeof id === 'string') { + id = parseInt(id as string, 10); + } + return this.getHeroes().then( + heroes => heroes.find(hero => hero.id === id) + ); + } + + updateHero(hero: Hero): Promise { + return this.getHero(hero.id).then(h => { + return h ? + Object.assign(h, hero) : + Promise.reject(`Hero ${hero.id} not found`) as any as Promise; + }); + } +} diff --git a/public/docs/_examples/testing/ts/app/model/hero.spec.ts b/public/docs/_examples/testing/ts/app/model/hero.spec.ts new file mode 100644 index 0000000000..e8acf913f2 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/model/hero.spec.ts @@ -0,0 +1,20 @@ +// #docregion +import { Hero } from './hero'; + +describe('Hero', () => { + it('has name', () => { + const hero = new Hero(1, 'Super Cat'); + expect(hero.name).toBe('Super Cat'); + }); + + it('has id', () => { + const hero = new Hero(1, 'Super Cat'); + expect(hero.id).toBe(1); + }); + + it('can clone itself', () => { + const hero = new Hero(1, 'Super Cat'); + const clone = hero.clone(); + expect(hero).toEqual(clone); + }); +}); diff --git a/public/docs/_examples/testing/ts/app/model/hero.ts b/public/docs/_examples/testing/ts/app/model/hero.ts new file mode 100644 index 0000000000..6a98f0dfdc --- /dev/null +++ b/public/docs/_examples/testing/ts/app/model/hero.ts @@ -0,0 +1,4 @@ +export class Hero { + constructor(public id = 0, public name = '') { } + clone() { return new Hero(this.id, this.name); } +} diff --git a/public/docs/_examples/testing/ts/app/http-hero.service.spec.ts b/public/docs/_examples/testing/ts/app/model/http-hero.service.spec.ts similarity index 67% rename from public/docs/_examples/testing/ts/app/http-hero.service.spec.ts rename to public/docs/_examples/testing/ts/app/model/http-hero.service.spec.ts index 375efde560..c16b421274 100644 --- a/public/docs/_examples/testing/ts/app/http-hero.service.spec.ts +++ b/public/docs/_examples/testing/ts/app/model/http-hero.service.spec.ts @@ -1,59 +1,54 @@ -/* tslint:disable:no-unused-variable */ import { - addProviders, - async, inject, withProviders + async, inject, TestBed } from '@angular/core/testing'; -import { TestComponentBuilder } from '@angular/core/testing'; - import { MockBackend, - MockConnection } from '@angular/http/testing'; + MockConnection +} from '@angular/http/testing'; import { - Http, HTTP_PROVIDERS, - ConnectionBackend, XHRBackend, - Request, RequestMethod, BaseRequestOptions, RequestOptions, - Response, ResponseOptions, - URLSearchParams + HttpModule, Http, XHRBackend, Response, ResponseOptions } from '@angular/http'; -// Add all operators to Observable -import 'rxjs/Rx'; import { Observable } from 'rxjs/Observable'; +import 'rxjs/add/observable/of'; -import { Hero } from './hero'; -import { HeroService } from './http-hero.service'; +import 'rxjs/add/operator/catch'; +import 'rxjs/add/operator/do'; +import 'rxjs/add/operator/toPromise'; -type HeroData = {id: string, name: string} +import { Hero } from './hero'; +import { HttpHeroService as HeroService } from './http-hero.service'; const makeHeroData = () => [ - { id: '1', name: 'Windstorm' }, - { id: '2', name: 'Bombasto' }, - { id: '3', name: 'Magneta' }, - { id: '4', name: 'Tornado' } -]; + { id: 1, name: 'Windstorm' }, + { id: 2, name: 'Bombasto' }, + { id: 3, name: 'Magneta' }, + { id: 4, name: 'Tornado' } +] as Hero[]; -// HeroService expects response data like {data: {the-data}} -const makeResponseData = (data: {}) => {return { data }; }; - -//////// SPECS ///////////// +//////// Tests ///////////// describe('Http-HeroService (mockBackend)', () => { - beforeEach(() => { - addProviders([ - HTTP_PROVIDERS, - { provide: XHRBackend, useClass: MockBackend } - ]); - }); + beforeEach( async(() => { + TestBed.configureTestingModule({ + imports: [ HttpModule ], + providers: [ + HeroService, + { provide: XHRBackend, useClass: MockBackend } + ] + }) + .compileComponents(); + })); it('can instantiate service when inject service', - withProviders(() => [HeroService]) - .inject([HeroService], (service: HeroService) => { - expect(service instanceof HeroService).toBe(true); + inject([HeroService], (service: HeroService) => { + expect(service instanceof HeroService).toBe(true); })); + it('can instantiate service with "new"', inject([Http], (http: Http) => { expect(http).not.toBeNull('http should be provided'); let service = new HeroService(http); @@ -69,10 +64,9 @@ describe('Http-HeroService (mockBackend)', () => { describe('when getHeroes', () => { let backend: MockBackend; let service: HeroService; - let fakeHeroes: HeroData[]; + let fakeHeroes: Hero[]; let response: Response; - beforeEach(inject([Http, XHRBackend], (http: Http, be: MockBackend) => { backend = be; service = new HeroService(http); @@ -87,7 +81,7 @@ describe('Http-HeroService (mockBackend)', () => { service.getHeroes().toPromise() // .then(() => Promise.reject('deliberate')) .then(heroes => { - expect(heroes.length).toEqual(fakeHeroes.length, + expect(heroes.length).toBe(fakeHeroes.length, 'should have expected no. of heroes'); }); }))); @@ -97,7 +91,7 @@ describe('Http-HeroService (mockBackend)', () => { service.getHeroes() .do(heroes => { - expect(heroes.length).toEqual(fakeHeroes.length, + expect(heroes.length).toBe(fakeHeroes.length, 'should have expected no. of heroes'); }) .toPromise(); @@ -110,7 +104,7 @@ describe('Http-HeroService (mockBackend)', () => { service.getHeroes() .do(heroes => { - expect(heroes.length).toEqual(0, 'should have no heroes'); + expect(heroes.length).toBe(0, 'should have no heroes'); }) .toPromise(); }))); diff --git a/public/docs/_examples/testing/ts/app/http-hero.service.ts b/public/docs/_examples/testing/ts/app/model/http-hero.service.ts similarity index 68% rename from public/docs/_examples/testing/ts/app/http-hero.service.ts rename to public/docs/_examples/testing/ts/app/model/http-hero.service.ts index bfde5bfdc8..a5fe46b801 100644 --- a/public/docs/_examples/testing/ts/app/http-hero.service.ts +++ b/public/docs/_examples/testing/ts/app/model/http-hero.service.ts @@ -4,10 +4,16 @@ import { Injectable } from '@angular/core'; import { Http, Response } from '@angular/http'; import { Headers, RequestOptions } from '@angular/http'; import { Hero } from './hero'; + import { Observable } from 'rxjs/Observable'; +import 'rxjs/add/observable/throw'; + +import 'rxjs/add/operator/do'; +import 'rxjs/add/operator/catch'; +import 'rxjs/add/operator/map'; @Injectable() -export class HeroService { +export class HttpHeroService { private _heroesUrl = 'app/heroes'; // URL to web api constructor (private http: Http) {} @@ -19,6 +25,12 @@ export class HeroService { .catch(this.handleError); } + getHero(id: number | string) { + return this.http + .get('app/heroes/?id=${id}') + .map((r: Response) => r.json().data as Hero[]); + } + addHero (name: string): Observable { let body = JSON.stringify({ name }); let headers = new Headers({ 'Content-Type': 'application/json' }); @@ -29,6 +41,16 @@ export class HeroService { .catch(this.handleError); } + updateHero (hero: Hero): Observable { + let body = JSON.stringify(hero); + let headers = new Headers({ 'Content-Type': 'application/json' }); + let options = new RequestOptions({ headers: headers }); + + return this.http.put(this._heroesUrl, body, options) + .map(this.extractData) + .catch(this.handleError); + } + private extractData(res: Response) { if (res.status < 200 || res.status >= 300) { throw new Error('Bad response status: ' + res.status); diff --git a/public/docs/_examples/testing/ts/app/model/index.ts b/public/docs/_examples/testing/ts/app/model/index.ts new file mode 100644 index 0000000000..227004d5be --- /dev/null +++ b/public/docs/_examples/testing/ts/app/model/index.ts @@ -0,0 +1,7 @@ +// Model barrel +export * from './hero'; +export * from './hero.service'; +export * from './http-hero.service'; +export * from './test-heroes'; + +export * from './user.service'; diff --git a/public/docs/_examples/testing/ts/app/model/test-heroes.ts b/public/docs/_examples/testing/ts/app/model/test-heroes.ts new file mode 100644 index 0000000000..d40ce5d564 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/model/test-heroes.ts @@ -0,0 +1,11 @@ +// #docregion +import { Hero } from './hero'; + +export var HEROES: Hero[] = [ + new Hero(11, 'Mr. Nice'), + new Hero(12, 'Narco'), + new Hero(13, 'Bombasto'), + new Hero(14, 'Celeritas'), + new Hero(15, 'Magneta'), + new Hero(16, 'RubberMan') +]; diff --git a/public/docs/_examples/testing/ts/app/model/testing/fake-hero.service.ts b/public/docs/_examples/testing/ts/app/model/testing/fake-hero.service.ts new file mode 100644 index 0000000000..79a865cc44 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/model/testing/fake-hero.service.ts @@ -0,0 +1,41 @@ +// re-export for tester convenience +export { Hero } from '../hero'; +export { HeroService } from '../hero.service'; + +import { Hero } from '../hero'; +import { HeroService } from '../hero.service'; + +export var HEROES: Hero[] = [ + new Hero(41, 'Bob'), + new Hero(42, 'Carol'), + new Hero(43, 'Ted'), + new Hero(44, 'Alice'), + new Hero(45, 'Speedy'), + new Hero(46, 'Stealthy') +]; + +export class FakeHeroService implements HeroService { + + heroes = HEROES.map(h => h.clone()); + lastPromise: Promise; // remember so we can spy on promise calls + + getHero(id: number | string) { + if (typeof id === 'string') { + id = parseInt(id as string, 10); + } + let hero = this.heroes.find(h => h.id === id); + return this.lastPromise = Promise.resolve(hero); + } + + getHeroes() { + return this.lastPromise = Promise.resolve(this.heroes); + } + + updateHero(hero: Hero): Promise { + return this.lastPromise = this.getHero(hero.id).then(h => { + return h ? + Object.assign(h, hero) : + Promise.reject(`Hero ${hero.id} not found`) as any as Promise; + }); + } +} diff --git a/public/docs/_examples/testing/ts/app/model/testing/index.ts b/public/docs/_examples/testing/ts/app/model/testing/index.ts new file mode 100644 index 0000000000..6da76e67db --- /dev/null +++ b/public/docs/_examples/testing/ts/app/model/testing/index.ts @@ -0,0 +1 @@ +export * from './fake-hero.service'; diff --git a/public/docs/_examples/testing/ts/app/model/user.service.ts b/public/docs/_examples/testing/ts/app/model/user.service.ts new file mode 100644 index 0000000000..280efefeec --- /dev/null +++ b/public/docs/_examples/testing/ts/app/model/user.service.ts @@ -0,0 +1,7 @@ +import { Injectable } from '@angular/core'; + +@Injectable() +export class UserService { + isLoggedIn = true; + user = {name: 'Sam Spade'}; +} diff --git a/public/docs/_examples/testing/ts/app/my-uppercase.pipe.1.ts b/public/docs/_examples/testing/ts/app/my-uppercase.pipe.1.ts deleted file mode 100644 index 94b5bc45ce..0000000000 --- a/public/docs/_examples/testing/ts/app/my-uppercase.pipe.1.ts +++ /dev/null @@ -1,9 +0,0 @@ -// #docregion -import { Pipe, PipeTransform } from '@angular/core'; - -@Pipe({ name: 'my-uppercase' }) -export class MyUppercasePipe implements PipeTransform { - transform(value: string) { - return value; - } -} diff --git a/public/docs/_examples/testing/ts/app/my-uppercase.pipe.spec.ts b/public/docs/_examples/testing/ts/app/my-uppercase.pipe.spec.ts deleted file mode 100644 index 731b2ed965..0000000000 --- a/public/docs/_examples/testing/ts/app/my-uppercase.pipe.spec.ts +++ /dev/null @@ -1,41 +0,0 @@ -// #docregion -// #docplaster -// #docregion base-pipe-spec -import { MyUppercasePipe } from './my-uppercase.pipe'; - -describe('MyUppercasePipe', () => { - let pipe: MyUppercasePipe; - - beforeEach(() => { - pipe = new MyUppercasePipe(); - }); - - // #docregion expectations - it('transforms "abc" to "ABC"', () => { - expect(pipe.transform('abc')).toEqual('ABC'); - }); - - it('transforms "abc def" to "ABC DEF"', () => { - expect(pipe.transform('abc def')).toEqual('ABC DEF'); - }); - - it('leaves "ABC DEF" unchanged', () => { - expect(pipe.transform('ABC DEF')).toEqual('ABC DEF'); - }); - // #enddocregion expectations - // #enddocregion base-pipe-spec - - /* more tests we could run - - it('transforms "abc-def" to "Abc-def"', () => { - expect(pipe.transform('abc-def')).toEqual('Abc-def'); - }); - - it('transforms " abc def" to " Abc Def" (preserves spaces) ', () => { - expect(pipe.transform(' abc def')).toEqual(' Abc Def'); - }); - - */ - // #docregion base-pipe-spec -}); -// #enddocregion base-pipe-spec diff --git a/public/docs/_examples/testing/ts/app/my-uppercase.pipe.ts b/public/docs/_examples/testing/ts/app/my-uppercase.pipe.ts deleted file mode 100644 index 6584f92ef6..0000000000 --- a/public/docs/_examples/testing/ts/app/my-uppercase.pipe.ts +++ /dev/null @@ -1,13 +0,0 @@ -// #docregion -// #docregion depends-on-angular -import { Pipe, PipeTransform } from '@angular/core'; -// #enddocregion depends-on-angular - -@Pipe({ name: 'my-uppercase' }) -export class MyUppercasePipe implements PipeTransform { - // #docregion uppercase - transform(value: string) { - return value.toUpperCase(); - } - // #enddocregion uppercase -} diff --git a/public/docs/_examples/testing/ts/app/old-specs/hero-detail.component.spec.ts.not-yet b/public/docs/_examples/testing/ts/app/old-specs/hero-detail.component.spec.ts.not-yet deleted file mode 100644 index 80c210be5d..0000000000 --- a/public/docs/_examples/testing/ts/app/old-specs/hero-detail.component.spec.ts.not-yet +++ /dev/null @@ -1,218 +0,0 @@ -///// 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 = ''; - - 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((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 = > 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 = - `
- - -
` - - 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 = `` - - 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}} - ` - - 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'); - }); - })); - }); -}); diff --git a/public/docs/_examples/testing/ts/app/old-specs/hero-detail.component.wrapped-tests.spec.ts.not-yet b/public/docs/_examples/testing/ts/app/old-specs/hero-detail.component.wrapped-tests.spec.ts.not-yet deleted file mode 100644 index 319ac93ebf..0000000000 --- a/public/docs/_examples/testing/ts/app/old-specs/hero-detail.component.wrapped-tests.spec.ts.not-yet +++ /dev/null @@ -1,144 +0,0 @@ -///// 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, '') - .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: ``, - directives: [HeroDetailComponent] -}) -class TestWrapper { - currentHero = new Hero(42, 'Cat Woman'); - userName = 'Sally'; - testCallback() {} // monkey-punched in a test - onDelete() { this.testCallback(); } -} - -@View({ - template: ` -
-

{{hero.name}} | {{userName}}

- - -
{{hero.id}}
- -
`, - 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; -} \ No newline at end of file diff --git a/public/docs/_examples/testing/ts/app/old-specs/hero.service.ng.spec.ts.not-yet b/public/docs/_examples/testing/ts/app/old-specs/hero.service.ng.spec.ts.not-yet deleted file mode 100644 index a3e4c0c6a4..0000000000 --- a/public/docs/_examples/testing/ts/app/old-specs/hero.service.ng.spec.ts.not-yet +++ /dev/null @@ -1,198 +0,0 @@ -// 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(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); -} diff --git a/public/docs/_examples/testing/ts/app/old-specs/hero.service.no-ng.1.spec.ts.not-yet b/public/docs/_examples/testing/ts/app/old-specs/hero.service.no-ng.1.spec.ts.not-yet deleted file mode 100644 index 89cbc597f8..0000000000 --- a/public/docs/_examples/testing/ts/app/old-specs/hero.service.no-ng.1.spec.ts.not-yet +++ /dev/null @@ -1,250 +0,0 @@ -/* - * 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 = { - // return a promise for fake heroes that resolves as quickly as possible - fetchAllHeroesAsync: () => Promise.resolve(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 = { - // return a promise for fake heroes that resolves as quickly as possible - fetchAllHeroesAsync: () => Promise.resolve(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 = { - // return a promise for fake heroes that resolves as quickly as possible - fetchAllHeroesAsync: () => Promise.resolve(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 = { - // return a promise for fake heroes that resolves as quickly as possible - fetchAllHeroesAsync: () => Promise.resolve(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 = { - // return a promise for fake heroes that resolves as quickly as possible - fetchAllHeroesAsync: () => Promise.resolve(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 = { - // return a promise for fake heroes that resolves as quickly as possible - fetchAllHeroesAsync: () => Promise.resolve(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 = { - // return a promise for fake heroes that resolves as quickly as possible - fetchAllHeroesAsync: () => Promise.resolve(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(heroData.map(h => h.clone())); -} diff --git a/public/docs/_examples/testing/ts/app/old-specs/hero.service.no-ng.spec.ts.not-yet b/public/docs/_examples/testing/ts/app/old-specs/hero.service.no-ng.spec.ts.not-yet deleted file mode 100644 index c2deb56c7c..0000000000 --- a/public/docs/_examples/testing/ts/app/old-specs/hero.service.no-ng.spec.ts.not-yet +++ /dev/null @@ -1,150 +0,0 @@ -// 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(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 because of TS typing bug. - fetchAllHeroesAsync = () => - >Promise.reject(testError); -} diff --git a/public/docs/_examples/testing/ts/app/old-specs/heroes.component.ng.spec.ts.not-yet b/public/docs/_examples/testing/ts/app/old-specs/heroes.component.ng.spec.ts.not-yet deleted file mode 100644 index 489745ca2b..0000000000 --- a/public/docs/_examples/testing/ts/app/old-specs/heroes.component.ng.spec.ts.not-yet +++ /dev/null @@ -1,276 +0,0 @@ -///// 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 = () => (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 = `

{{userName}}'s Heroes

`; - 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 = 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 = 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 = - '
  • {{h.name}}
'; - - 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 = - '
  • {{h.name}}
'; - - 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 = - `
  • - ({{hero.id}}) {{hero.name}} -
`; - - 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); - }); - }) -} diff --git a/public/docs/_examples/testing/ts/app/old-specs/heroes.component.no-ng.spec.ts.not-yet b/public/docs/_examples/testing/ts/app/old-specs/heroes.component.no-ng.spec.ts.not-yet deleted file mode 100644 index b1c1f2cff8..0000000000 --- a/public/docs/_examples/testing/ts/app/old-specs/heroes.component.no-ng.spec.ts.not-yet +++ /dev/null @@ -1,229 +0,0 @@ -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 = () => (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 = 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 = service.refresh; - expect(spy.calls.count()).toEqual(0); - }); - - it('onInit calls HeroService.refresh', () => { - let spy = 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); - }); - } -} diff --git a/public/docs/_examples/testing/ts/app/old-specs/user.spec.ts.not-yet b/public/docs/_examples/testing/ts/app/old-specs/user.spec.ts.not-yet deleted file mode 100644 index f65ac89a5e..0000000000 --- a/public/docs/_examples/testing/ts/app/old-specs/user.spec.ts.not-yet +++ /dev/null @@ -1,18 +0,0 @@ -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); - }); - -}); \ No newline at end of file diff --git a/public/docs/_examples/testing/ts/app/shared/highlight.directive.spec.ts b/public/docs/_examples/testing/ts/app/shared/highlight.directive.spec.ts new file mode 100644 index 0000000000..c5f13934b9 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/shared/highlight.directive.spec.ts @@ -0,0 +1,58 @@ +import { Component, DebugElement } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; + +import { HighlightDirective } from './highlight.directive'; + +// Component to test directive +@Component({ + template: ` +

Something Yellow

+

Something Gray

+

Something White

+ ` + +}) +class TestComponent { } + +////// Tests ////////// +describe('HighlightDirective', () => { + + let fixture: ComponentFixture; + let h2Des: DebugElement[]; + + beforeEach(() => { + fixture = TestBed.configureTestingModule({ + declarations: [ HighlightDirective, TestComponent ] + }) + .createComponent(TestComponent); + + h2Des = fixture.debugElement.queryAll(By.css('h2')); + }); + + it('should have `HighlightDirective`', () => { + // The HighlightDirective listed in

tokens means it is attached + expect(h2Des[0].providerTokens).toContain(HighlightDirective, 'HighlightDirective'); + }); + + it('should color first

background "yellow"', () => { + fixture.detectChanges(); + const h2 = h2Des[0].nativeElement as HTMLElement; + expect(h2.style.backgroundColor).toBe('yellow'); + }); + + it('should color second

background w/ default color', () => { + fixture.detectChanges(); + const h2 = h2Des[1].nativeElement as HTMLElement; + expect(h2.style.backgroundColor).toBe(HighlightDirective.defaultColor); + }); + + it('should NOT color third

(no directive)', () => { + // no directive + expect(h2Des[2].providerTokens).not.toContain(HighlightDirective, 'HighlightDirective'); + fixture.detectChanges(); + + const h2 = h2Des[2].nativeElement as HTMLElement; + expect(h2.style.backgroundColor).toBe('', 'backgroundColor'); + }); +}); diff --git a/public/docs/_examples/testing/ts/app/shared/highlight.directive.ts b/public/docs/_examples/testing/ts/app/shared/highlight.directive.ts new file mode 100644 index 0000000000..9c091b3638 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/shared/highlight.directive.ts @@ -0,0 +1,24 @@ +import { Directive, ElementRef, Input, OnChanges, Renderer } from '@angular/core'; + +@Directive({ selector: '[highlight]' }) +/** + * Set backgroundColor for the attached element ton highlight color and + * set element `customProperty` = true + */ +export class HighlightDirective implements OnChanges { + + static defaultColor = 'rgb(211, 211, 211)'; // lightgray + + @Input('highlight') bgColor: string; + + constructor(private renderer: Renderer, private el: ElementRef) { + renderer.setElementProperty(el.nativeElement, 'customProperty', true); + } + + ngOnChanges() { + this.renderer.setElementStyle( + this.el.nativeElement, 'backgroundColor', + this.bgColor || HighlightDirective.defaultColor ); + } +} + diff --git a/public/docs/_examples/testing/ts/app/shared/shared.module.ts b/public/docs/_examples/testing/ts/app/shared/shared.module.ts new file mode 100644 index 0000000000..17c41c0410 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/shared/shared.module.ts @@ -0,0 +1,15 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; + +import { HighlightDirective } from './highlight.directive'; +import { TitleCasePipe } from './title-case.pipe'; +import { TwainComponent } from './twain.component'; + +@NgModule({ + imports: [ CommonModule ], + exports: [ CommonModule, FormsModule, + HighlightDirective, TitleCasePipe, TwainComponent ], + declarations: [ HighlightDirective, TitleCasePipe, TwainComponent ] +}) +export class SharedModule { } diff --git a/public/docs/_examples/testing/ts/app/shared/styles.css b/public/docs/_examples/testing/ts/app/shared/styles.css new file mode 100644 index 0000000000..b26317fa5e --- /dev/null +++ b/public/docs/_examples/testing/ts/app/shared/styles.css @@ -0,0 +1 @@ +/* MISSING */ diff --git a/public/docs/_examples/testing/ts/app/shared/title-case.pipe.spec.ts b/public/docs/_examples/testing/ts/app/shared/title-case.pipe.spec.ts new file mode 100644 index 0000000000..7481537c10 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/shared/title-case.pipe.spec.ts @@ -0,0 +1,33 @@ +// #docplaster +// #docregion +import { TitleCasePipe } from './title-case.pipe'; + +// #docregion excerpt +describe('TitleCasePipe', () => { + // This pipe is a pure function so no need for BeforeEach + let pipe = new TitleCasePipe(); + + it('transforms "abc" to "Abc"', () => { + expect(pipe.transform('abc')).toBe('Abc'); + }); + + it('transforms "abc def" to "Abc Def"', () => { + expect(pipe.transform('abc def')).toBe('Abc Def'); + }); + + // ... more tests ... +// #enddocregion excerpt + it('leaves "Abc Def" unchanged', () => { + expect(pipe.transform('Abc Def')).toBe('Abc Def'); + }); + + it('transforms "abc-def" to "Abc-def"', () => { + expect(pipe.transform('abc-def')).toBe('Abc-def'); + }); + + it('transforms " abc def" to " Abc Def" (preserves spaces) ', () => { + expect(pipe.transform(' abc def')).toBe(' Abc Def'); + }); +// #docregion excerpt +}); +// #enddocregion excerpt diff --git a/public/docs/_examples/testing/ts/app/shared/title-case.pipe.ts b/public/docs/_examples/testing/ts/app/shared/title-case.pipe.ts new file mode 100644 index 0000000000..df2567778d --- /dev/null +++ b/public/docs/_examples/testing/ts/app/shared/title-case.pipe.ts @@ -0,0 +1,11 @@ +// #docregion +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({name: 'titlecase', pure: false}) +/** Transform to Title Case: uppercase the first letter of the words in a string.*/ +export class TitleCasePipe implements PipeTransform { + transform(input: string): string { + return input.length === 0 ? '' : + input.replace(/\w\S*/g, (txt => txt[0].toUpperCase() + txt.substr(1).toLowerCase() )); + } +} diff --git a/public/docs/_examples/testing/ts/app/shared/twain.component.spec.ts b/public/docs/_examples/testing/ts/app/shared/twain.component.spec.ts new file mode 100644 index 0000000000..767e1ec2ca --- /dev/null +++ b/public/docs/_examples/testing/ts/app/shared/twain.component.spec.ts @@ -0,0 +1,92 @@ +// #docplaster +import { async, fakeAsync, ComponentFixture, TestBed, tick } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { DebugElement } from '@angular/core'; + +import { TwainService } from './twain.service'; +import { TwainComponent } from './twain.component'; + +describe('TwainComponent', () => { + + let comp: TwainComponent; + let fixture: ComponentFixture; + + let spy: jasmine.Spy; + let twainEl: DebugElement; // the element with the Twain quote + let twainService: TwainService; // the actually injected service + + const testQuote = 'Test Quote'; + + // #docregion setup + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [ TwainComponent ], + providers: [ TwainService ], + }); + + fixture = TestBed.createComponent(TwainComponent); + comp = fixture.componentInstance; + + // TwainService actually injected into the component + twainService = fixture.debugElement.injector.get(TwainService); + + // Setup spy on the `getQuote` method + // #docregion spy + spy = spyOn(twainService, 'getQuote') + .and.returnValue(Promise.resolve(testQuote)); + // #enddocregion spy + + // Get the Twain quote element by CSS selector (e.g., by class name) + twainEl = fixture.debugElement.query(By.css('.twain')); + }); + // #enddocregion setup + + // #docregion tests + function getQuote() { return twainEl.nativeElement.textContent; } + + it('should not show quote before OnInit', () => { + expect(getQuote()).toBe('', 'nothing displayed'); + expect(spy.calls.any()).toBe(false, 'getQuote not yet called'); + }); + + it('should still not show quote after component initialized', () => { + fixture.detectChanges(); // trigger data binding + // getQuote service is async => still has not returned with quote + expect(getQuote()).toBe('...', 'no quote yet'); + expect(spy.calls.any()).toBe(true, 'getQuote called'); + }); + + // #docregion async-test + it('should show quote after getQuote promise (async)', async(() => { + fixture.detectChanges(); // trigger data binding + + fixture.whenStable().then(() => { // wait for async getQuote + fixture.detectChanges(); // update view with quote + expect(getQuote()).toBe(testQuote); + }); + })); + // #enddocregion async-test + + // #docregion fake-async-test + it('should show quote after getQuote promise (fakeAsync)', fakeAsync(() => { + fixture.detectChanges(); // trigger data binding + tick(); // wait for async getQuote + fixture.detectChanges(); // update view with quote + expect(getQuote()).toBe(testQuote); + })); + // #enddocregion fake-async-test + // #enddocregion tests + + // #docregion done-test + it('should show quote after getQuote promise (done)', done => { + fixture.detectChanges(); // trigger data binding + + // get the spy promise and wait for it to resolve + spy.calls.mostRecent().returnValue.then(() => { + fixture.detectChanges(); // update view with quote + expect(getQuote()).toBe(testQuote); + done(); + }); + }); + // #enddocregion done-test +}); diff --git a/public/docs/_examples/testing/ts/app/shared/twain.component.timer.spec.ts.no-work b/public/docs/_examples/testing/ts/app/shared/twain.component.timer.spec.ts.no-work new file mode 100644 index 0000000000..74dec3e766 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/shared/twain.component.timer.spec.ts.no-work @@ -0,0 +1,116 @@ +// #docplaster +// When AppComponent learns to present quote with intervalTimer +import { async, discardPeriodicTasks, fakeAsync, ComponentFixture, TestBed, tick } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { DebugElement } from '@angular/core'; + +import { TwainService } from './model'; +import { TwainComponent } from './twain.component'; + +xdescribe('TwainComponent', () => { + + let comp: TwainComponent; + let fixture: ComponentFixture; + + const quotes = [ + 'Test Quote 1', + 'Test Quote 2', + 'Test Quote 3' + ]; + + let spy: jasmine.Spy; + let twainEl: DebugElement; // the element with the Twain quote + let twainService: TwainService; // the actually injected service + + function getQuote() { return twainEl.nativeElement.textContent; } + + // #docregion setup + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [ TwainComponent ], + providers: [ TwainService ], + }); + + fixture = TestBed.createComponent(TwainComponent); + comp = fixture.componentInstance; + + // TwainService actually injected into the component + twainService = fixture.debugElement.injector.get(TwainService); + + // Setup spy on the `getQuote` method + spy = spyOn(twainService, 'getQuote') + .and.returnValues(...quotes.map(q => Promise.resolve(q))); + + // Get the Twain quote element by CSS selector (e.g., by class name) + twainEl = fixture.debugElement.query(By.css('.twain')); + }); + + afterEach(() => { + // destroy component to stop the component timer + fixture.destroy(); + }); + // #enddocregion setup + + // #docregion tests + it('should not show quote before OnInit', () => { + expect(getQuote()).toBe(''); + }); + + it('should still not show quote after component initialized', () => { + // because the getQuote service is async + fixture.detectChanges(); // trigger data binding + expect(getQuote()).toContain('not initialized'); + }); + + // WIP + // If go this way, add jasmine.clock().uninstall(); to afterEach + // it('should show quote after Angular "settles"', async(() => { + // //jasmine.clock().install(); + // fixture.detectChanges(); // trigger data binding + // fixture.whenStable().then(() => { + // fixture.detectChanges(); // update view with the quote + // expect(getQuote()).toBe(quotes[0]); + // }); + // // jasmine.clock().tick(5000); + // // fixture.whenStable().then(() => { + // // fixture.detectChanges(); // update view with the quote + // // expect(getQuote()).toBe(quotes[1]); + // // }); + // })); + + it('should show quote after getQuote promise returns', fakeAsync(() => { + fixture.detectChanges(); // trigger data binding + tick(); // wait for first async getQuote to return + fixture.detectChanges(); // update view with the quote + expect(getQuote()).toBe(quotes[0]); + + // destroy component to stop the component timer before test ends + // else test errors because still have timer in the queue + fixture.destroy(); + })); + + it('should show 2nd quote after 5 seconds pass', fakeAsync(() => { + fixture.detectChanges(); // trigger data binding + tick(5000); // wait for second async getQuote to return + fixture.detectChanges(); // update view with the quote + expect(getQuote()).toBe(quotes[1]); + + // still have intervalTimer queuing requres + // discardPeriodicTasks() else test errors + discardPeriodicTasks(); + })); + + fit('should show 3rd quote after 10 seconds pass', fakeAsync(() => { + fixture.detectChanges(); // trigger data binding + tick(5000); // wait for second async getQuote to return + fixture.detectChanges(); // update view with the 2nd quote + tick(5000); // wait for third async getQuote to return + fixture.detectChanges(); // update view with the 3rd quote + expect(getQuote()).toBe(quotes[2]); + + // still have intervalTimer queuing requres + // discardPeriodicTasks() else test errors + discardPeriodicTasks(); + })); + // #enddocregion tests +}); diff --git a/public/docs/_examples/testing/ts/app/shared/twain.component.timer.ts.no-work b/public/docs/_examples/testing/ts/app/shared/twain.component.timer.ts.no-work new file mode 100644 index 0000000000..d3dc1f205d --- /dev/null +++ b/public/docs/_examples/testing/ts/app/shared/twain.component.timer.ts.no-work @@ -0,0 +1,27 @@ +// #docregion +import { Component, OnInit, OnDestroy } from '@angular/core'; + +import { TwainService } from './twain.service'; + +@Component({ + selector: 'twain-quote', + template: '

{{quote}}

' +}) +export class TwainComponent implements OnInit, OnDestroy { + intervalId: number; + quote = '-- not initialized yet --'; + constructor(private twainService: TwainService) { } + + getQuote() { + this.twainService.getQuote().then(quote => this.quote = quote); + } + + ngOnInit(): void { + this.getQuote(); + this.intervalId = window.setInterval(() => this.getQuote(), 5000); + } + + ngOnDestroy(): void { + clearInterval(this.intervalId); + } +} diff --git a/public/docs/_examples/testing/ts/app/shared/twain.component.ts b/public/docs/_examples/testing/ts/app/shared/twain.component.ts new file mode 100644 index 0000000000..29f24459ab --- /dev/null +++ b/public/docs/_examples/testing/ts/app/shared/twain.component.ts @@ -0,0 +1,20 @@ +// #docregion +import { Component, OnInit } from '@angular/core'; + +import { TwainService } from './twain.service'; + +// #docregion component +@Component({ + selector: 'twain-quote', + template: '

{{quote}}

' +}) +export class TwainComponent implements OnInit { + intervalId: number; + quote = '...'; + constructor(private twainService: TwainService) { } + + ngOnInit(): void { + this.twainService.getQuote().then(quote => this.quote = quote); + } +} +// #enddocregion component diff --git a/public/docs/_examples/testing/ts/app/shared/twain.service.ts b/public/docs/_examples/testing/ts/app/shared/twain.service.ts new file mode 100644 index 0000000000..9e394df1ee --- /dev/null +++ b/public/docs/_examples/testing/ts/app/shared/twain.service.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@angular/core'; + +const quotes = [ +'Always do right. This will gratify some people and astonish the rest.', +'I have never let my schooling interfere with my education.', +'Don\'t go around saying the world owes you a living. The world owes you nothing. It was here first.', +'Whenever you find yourself on the side of the majority, it is time to pause and reflect.', +'If you tell the truth, you don\'t have to remember anything.', +'Clothes make the man. Naked people have little or no influence on society.', +'It\'s not the size of the dog in the fight, it\'s the size of the fight in the dog.', +'Truth is stranger than fiction, but it is because Fiction is obliged to stick to possibilities; Truth isn\'t.', +'The man who does not read good books has no advantage over the man who cannot read them.', +'Get your facts first, and then you can distort them as much as you please.', +]; + +@Injectable() +export class TwainService { + private next = 0; + + // Imaginary todo: get quotes from a remote quote service + // returns quote after delay simulating server latency + getQuote(): Promise { + return new Promise(resolve => { + setTimeout( () => resolve(this.nextQuote()), 500 ); + }); + } + + private nextQuote() { + if (this.next === quotes.length) { this.next = 0; } + return quotes[ this.next++ ]; + } +} diff --git a/public/docs/_examples/testing/ts/app/welcome.component.spec.ts b/public/docs/_examples/testing/ts/app/welcome.component.spec.ts new file mode 100644 index 0000000000..ec59ef5bc2 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/welcome.component.spec.ts @@ -0,0 +1,83 @@ +// #docplaster +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { DebugElement } from '@angular/core'; + +import { UserService } from './model'; +import { WelcomeComponent } from './welcome.component'; + +describe('WelcomeComponent', () => { + + let comp: WelcomeComponent; + let fixture: ComponentFixture; + let userService: UserService; // the actually injected service + let welcomeEl: DebugElement; // the element with the welcome message + + // #docregion setup + beforeEach(() => { + // fake UserService for test purposes + // #docregion fake-userservice + const fakeUserService = { + isLoggedIn: true, + user: { name: 'Test User'} + }; + // #enddocregion fake-userservice + + // #docregion config-test-module + TestBed.configureTestingModule({ + declarations: [ WelcomeComponent ], + // #enddocregion setup + // providers: [ UserService ] // a real service would be a problem! + // #docregion setup + providers: [ {provide: UserService, useValue: fakeUserService } ] + }); + // #enddocregion config-test-module + + fixture = TestBed.createComponent(WelcomeComponent); + comp = fixture.componentInstance; + + // #enddocregion setup + // #docregion inject-from-testbed + // UserService provided to the TestBed + userService = TestBed.get(UserService); + // #enddocregion inject-from-testbed + // #docregion setup + // #docregion injected-service + // UserService actually injected into the component + userService = fixture.debugElement.injector.get(UserService); + // #enddocregion injected-service + + // get the "welcome" element by CSS selector (e.g., by class name) + welcomeEl = fixture.debugElement.query(By.css('.welcome')); + }); + // #enddocregion setup + + // #docregion tests + it('should welcome the user', () => { + fixture.detectChanges(); // trigger data binding + + let content = welcomeEl.nativeElement.textContent; + expect(content).toContain('Welcome', '"Welcome ..."'); + expect(content).toContain('Test User', 'expected name'); + }); + + it('should welcome "Bubba"', () => { + userService.user.name = 'Bubba'; // welcome message hasn't been shown yet + + fixture.detectChanges(); // trigger data binding + + let content = welcomeEl.nativeElement.textContent; + expect(content).toContain('Bubba'); + }); + + it('should request login if not logged in', () => { + userService.isLoggedIn = false; // welcome message hasn't been shown yet + + fixture.detectChanges(); // trigger data binding + + let content = welcomeEl.nativeElement.textContent; + expect(content).not.toContain('Welcome', 'not welcomed'); + expect(content).toMatch(/log in/i, '"log in"'); + }); + // #enddocregion tests +}); diff --git a/public/docs/_examples/testing/ts/app/welcome.component.ts b/public/docs/_examples/testing/ts/app/welcome.component.ts new file mode 100644 index 0000000000..35958cc5c9 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/welcome.component.ts @@ -0,0 +1,18 @@ +// #docregion +import { Component, OnInit } from '@angular/core'; +import { UserService } from './model'; + +@Component({ + selector: 'app-welcome', + template: '

{{welcome}}

' +}) +export class WelcomeComponent implements OnInit { + welcome = '-- not initialized yet --'; + constructor(private userService: UserService) { } + + ngOnInit(): void { + this.welcome = this.userService.isLoggedIn ? + 'Welcome, ' + this.userService.user.name : + 'Please log in.'; + } +} diff --git a/public/docs/_examples/testing/ts/bag-specs.html b/public/docs/_examples/testing/ts/bag-specs.html new file mode 100644 index 0000000000..792ebc113f --- /dev/null +++ b/public/docs/_examples/testing/ts/bag-specs.html @@ -0,0 +1,41 @@ + + + + + + + Specs Bag + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/docs/_examples/testing/ts/bag-specs.plnkr.json b/public/docs/_examples/testing/ts/bag-specs.plnkr.json new file mode 100644 index 0000000000..89d86da28a --- /dev/null +++ b/public/docs/_examples/testing/ts/bag-specs.plnkr.json @@ -0,0 +1,20 @@ +{ + "description": "Testing - bag.specs", + "files":[ + "browser-test-shim.js", + "systemjs.config.extras.js", + "styles.css", + + "app/bag/**/*.html", + "app/bag/**/*.ts", + "app/bag/**/*.spec.ts", + + "!app/bag/bag-main.ts", + + "testing/*.ts", + + "bag-specs.html" + ], + "main": "bag-specs.html", + "tags": ["testing"] +} diff --git a/public/docs/_examples/testing/ts/bag.html b/public/docs/_examples/testing/ts/bag.html new file mode 100644 index 0000000000..35ff270025 --- /dev/null +++ b/public/docs/_examples/testing/ts/bag.html @@ -0,0 +1,27 @@ + + + + + + Specs Bag + + + + + + + + + + + + + + + + + Loading ... + + diff --git a/public/docs/_examples/testing/ts/bag.plnkr.json b/public/docs/_examples/testing/ts/bag.plnkr.json new file mode 100644 index 0000000000..96e0b79b65 --- /dev/null +++ b/public/docs/_examples/testing/ts/bag.plnkr.json @@ -0,0 +1,13 @@ +{ + "description": "Running the bag", + "files":[ + "styles.css", + + "app/bag/bag.ts", + "app/bag/bag-external-template.html", + "app/bag/bag-main.ts", + "bag.html" + ], + "main": "bag.html", + "tags": ["testing"] +} diff --git a/public/docs/_examples/testing/ts/browser-test-shim.js b/public/docs/_examples/testing/ts/browser-test-shim.js new file mode 100644 index 0000000000..1573c72ebd --- /dev/null +++ b/public/docs/_examples/testing/ts/browser-test-shim.js @@ -0,0 +1,87 @@ +// BROWSER TESTING SHIM +// Keep it in-sync with what karma-test-shim does +// #docregion +/*global jasmine, __karma__, window*/ +(function () { + +Error.stackTraceLimit = 0; // "No stacktrace"" is usually best for app testing. + +// Uncomment to get full stacktrace output. Sometimes helpful, usually not. +// Error.stackTraceLimit = Infinity; // + +jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000; + +var baseURL = document.baseURI; +baseURL = baseURL + baseURL[baseURL.length-1] ? '' : '/'; + +System.config({ + baseURL: baseURL, + // Extend usual application package list with test folder + packages: { 'testing': { main: 'index.js', defaultExtension: 'js' } }, + + // Assume npm: is set in `paths` in systemjs.config + // Map the angular testing umd bundles + map: { + '@angular/core/testing': 'npm:@angular/core/bundles/core-testing.umd.js', + '@angular/common/testing': 'npm:@angular/common/bundles/common-testing.umd.js', + '@angular/compiler/testing': 'npm:@angular/compiler/bundles/compiler-testing.umd.js', + '@angular/platform-browser/testing': 'npm:@angular/platform-browser/bundles/platform-browser-testing.umd.js', + '@angular/platform-browser-dynamic/testing': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic-testing.umd.js', + '@angular/http/testing': 'npm:@angular/http/bundles/http-testing.umd.js', + '@angular/router/testing': 'npm:@angular/router/bundles/router-testing.umd.js', + '@angular/forms/testing': 'npm:@angular/forms/bundles/forms-testing.umd.js', + }, +}); + +System.import('systemjs.config.js') + .then(importSystemJsExtras) + .then(initTestBed) + .then(initTesting); + +/** Optional SystemJS configuration extras. Keep going w/o it */ +function importSystemJsExtras(){ + return System.import('systemjs.config.extras.js') + .catch(function(reason) { + console.log( + 'Warning: System.import could not load the optional "systemjs.config.extras.js". Did you omit it by accident? Continuing without it.' + ); + console.log(reason); + }); +} + +function initTestBed(){ + return Promise.all([ + System.import('@angular/core/testing'), + System.import('@angular/platform-browser-dynamic/testing') + ]) + + .then(function (providers) { + var coreTesting = providers[0]; + var browserTesting = providers[1]; + + coreTesting.TestBed.initTestEnvironment( + browserTesting.BrowserDynamicTestingModule, + browserTesting.platformBrowserDynamicTesting()); + }) +} + +// Import all spec files defined in the html (__spec_files__) +// and start Jasmine testrunner +function initTesting () { + console.log('loading spec files: '+__spec_files__.join(', ')); + return Promise.all( + __spec_files__.map(function(spec) { + return System.import(spec); + }) + ) + // After all imports load, re-execute `window.onload` which + // triggers the Jasmine test-runner start or explain what went wrong + .then(success, console.error.bind(console)); + + function success () { + console.log('Spec files loaded; starting Jasmine testrunner'); + window.onload(); + } +} + +})(); diff --git a/public/docs/_examples/testing/ts/index.html b/public/docs/_examples/testing/ts/index.html index bfde80afe3..b50b69ec18 100644 --- a/public/docs/_examples/testing/ts/index.html +++ b/public/docs/_examples/testing/ts/index.html @@ -1,8 +1,9 @@ + - Testing Tour of Heroes + App Under Test @@ -15,6 +16,7 @@ + diff --git a/public/docs/_examples/testing/ts/karma-test-shim.js b/public/docs/_examples/testing/ts/karma-test-shim.js new file mode 100644 index 0000000000..19fcc89fe9 --- /dev/null +++ b/public/docs/_examples/testing/ts/karma-test-shim.js @@ -0,0 +1,89 @@ +// #docregion +// /*global jasmine, __karma__, window*/ +Error.stackTraceLimit = 0; // "No stacktrace"" is usually best for app testing. + +// Uncomment to get full stacktrace output. Sometimes helpful, usually not. +// Error.stackTraceLimit = Infinity; // + +jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000; + +var builtPath = '/base/app/'; + +__karma__.loaded = function () { }; + +function isJsFile(path) { + return path.slice(-3) == '.js'; +} + +function isSpecFile(path) { + return /\.spec\.(.*\.)?js$/.test(path); +} + +function isBuiltFile(path) { + return isJsFile(path) && (path.substr(0, builtPath.length) == builtPath); +} + +var allSpecFiles = Object.keys(window.__karma__.files) + .filter(isSpecFile) + .filter(isBuiltFile); + +System.config({ + baseURL: '/base', + // Extend usual application package list with test folder + packages: { 'testing': { main: 'index.js', defaultExtension: 'js' } }, + + // Assume npm: is set in `paths` in systemjs.config + // Map the angular testing umd bundles + map: { + '@angular/core/testing': 'npm:@angular/core/bundles/core-testing.umd.js', + '@angular/common/testing': 'npm:@angular/common/bundles/common-testing.umd.js', + '@angular/compiler/testing': 'npm:@angular/compiler/bundles/compiler-testing.umd.js', + '@angular/platform-browser/testing': 'npm:@angular/platform-browser/bundles/platform-browser-testing.umd.js', + '@angular/platform-browser-dynamic/testing': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic-testing.umd.js', + '@angular/http/testing': 'npm:@angular/http/bundles/http-testing.umd.js', + '@angular/router/testing': 'npm:@angular/router/bundles/router-testing.umd.js', + '@angular/forms/testing': 'npm:@angular/forms/bundles/forms-testing.umd.js', + }, +}); + +System.import('systemjs.config.js') + .then(importSystemJsExtras) + .then(initTestBed) + .then(initTesting); + +/** Optional SystemJS configuration extras. Keep going w/o it */ +function importSystemJsExtras(){ + return System.import('systemjs.config.extras.js') + .catch(function(reason) { + console.log( + 'Warning: System.import could not load the optional "systemjs.config.extras.js". Did you omit it by accident? Continuing without it.' + ); + console.log(reason); + }); +} + +function initTestBed(){ + return Promise.all([ + System.import('@angular/core/testing'), + System.import('@angular/platform-browser-dynamic/testing') + ]) + + .then(function (providers) { + var coreTesting = providers[0]; + var browserTesting = providers[1]; + + coreTesting.TestBed.initTestEnvironment( + browserTesting.BrowserDynamicTestingModule, + browserTesting.platformBrowserDynamicTesting()); + }) +} + +// Import all spec files and start karma +function initTesting () { + return Promise.all( + allSpecFiles.map(function (moduleName) { + return System.import(moduleName); + }) + ) + .then(__karma__.start, __karma__.error); +} diff --git a/public/docs/_examples/karma.conf.js b/public/docs/_examples/testing/ts/karma.conf.js similarity index 60% rename from public/docs/_examples/karma.conf.js rename to public/docs/_examples/testing/ts/karma.conf.js index faa52df98e..1e2d293721 100644 --- a/public/docs/_examples/karma.conf.js +++ b/public/docs/_examples/testing/ts/karma.conf.js @@ -1,7 +1,12 @@ +// #docregion module.exports = function(config) { - var appBase = 'app/'; // transpiled app JS files - var appAssets ='/base/app/'; // component assets fetched by Angular's compiler + var appBase = 'app/'; // transpiled app JS and map files + var appSrcBase = 'app/'; // app source TS files + var appAssets = '/base/app/'; // component assets fetched by Angular's compiler + + var testBase = 'testing/'; // transpiled test JS and map files + var testSrcBase = 'testing/'; // test source TS files config.set({ basePath: '', @@ -9,7 +14,8 @@ module.exports = function(config) { plugins: [ require('karma-jasmine'), require('karma-chrome-launcher'), - require('karma-htmlfile-reporter') + require('karma-jasmine-html-reporter'), // click "Debug" in browser to see it + require('karma-htmlfile-reporter') // crashing w/ strange socket error ], customLaunchers: { @@ -26,39 +32,48 @@ module.exports = function(config) { // Polyfills 'node_modules/core-js/client/shim.js', - - // Reflect and Zone.js 'node_modules/reflect-metadata/Reflect.js', + + // zone.js 'node_modules/zone.js/dist/zone.js', + 'node_modules/zone.js/dist/long-stack-trace-zone.js', + 'node_modules/zone.js/dist/proxy.js', + 'node_modules/zone.js/dist/sync-test.js', 'node_modules/zone.js/dist/jasmine-patch.js', 'node_modules/zone.js/dist/async-test.js', 'node_modules/zone.js/dist/fake-async-test.js', - // RxJs. + // RxJs { pattern: 'node_modules/rxjs/**/*.js', included: false, watched: false }, { pattern: 'node_modules/rxjs/**/*.js.map', included: false, watched: false }, - // Angular 2 itself and the testing library + // Paths loaded via module imports: + // Angular itself {pattern: 'node_modules/@angular/**/*.js', included: false, watched: false}, {pattern: 'node_modules/@angular/**/*.js.map', included: false, watched: false}, {pattern: 'systemjs.config.js', included: false, watched: false}, + {pattern: 'systemjs.config.extras.js', included: false, watched: false}, 'karma-test-shim.js', // transpiled application & spec code paths loaded via module imports {pattern: appBase + '**/*.js', included: false, watched: true}, + {pattern: testBase + '**/*.js', included: false, watched: true}, - // asset (HTML & CSS) paths loaded via Angular's component compiler + + // Asset (HTML & CSS) paths loaded via Angular's component compiler // (these paths need to be rewritten, see proxies section) {pattern: appBase + '**/*.html', included: false, watched: true}, {pattern: appBase + '**/*.css', included: false, watched: true}, - // paths for debugging with source maps in dev tools - {pattern: appBase + '**/*.ts', included: false, watched: false}, - {pattern: appBase + '**/*.js.map', included: false, watched: false} + // Paths for debugging with source maps in dev tools + {pattern: appSrcBase + '**/*.ts', included: false, watched: false}, + {pattern: appBase + '**/*.js.map', included: false, watched: false}, + {pattern: testSrcBase + '**/*.ts', included: false, watched: false}, + {pattern: testBase + '**/*.js.map', included: false, watched: false} ], - // proxied base paths for loading assets + // Proxied base paths for loading assets proxies: { // required for component assets fetched by Angular's compiler "/app/": appAssets @@ -66,7 +81,8 @@ module.exports = function(config) { exclude: [], preprocessors: {}, - reporters: ['progress', 'html'], + // disabled HtmlReporter; suddenly crashing w/ strange socket error + reporters: ['progress', 'kjhtml'],//'html'], // HtmlReporter configuration htmlReporter: { diff --git a/public/docs/_examples/testing/ts/liteserver-test-config.json b/public/docs/_examples/testing/ts/liteserver-test-config.json deleted file mode 100644 index 6b1a2b5466..0000000000 --- a/public/docs/_examples/testing/ts/liteserver-test-config.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "startPath": "unit-tests.html" -} \ No newline at end of file diff --git a/public/docs/_examples/testing/ts/plnkr.json b/public/docs/_examples/testing/ts/plnkr.json new file mode 100644 index 0000000000..e3573a6b6d --- /dev/null +++ b/public/docs/_examples/testing/ts/plnkr.json @@ -0,0 +1,16 @@ +{ + "description": "Heroes Test App", + "files":[ + "styles.css", + "systemjs.config.extras.js", + + "app/**/*.css", + "app/**/*.html", + "app/**/*.ts", + + "!app/bag/*.*", + + "index.html" + ], + "tags": ["testing"] +} diff --git a/public/docs/_examples/testing/ts/systemjs.config.extras.js b/public/docs/_examples/testing/ts/systemjs.config.extras.js new file mode 100644 index 0000000000..218e65715c --- /dev/null +++ b/public/docs/_examples/testing/ts/systemjs.config.extras.js @@ -0,0 +1,9 @@ +// #docregion +/** App specific SystemJS configuration */ +System.config({ + packages: { + // barrels + 'app/model': {main:'index.js', defaultExtension:'js'}, + 'app/model/testing': {main:'index.js', defaultExtension:'js'} + } +}); diff --git a/public/docs/_examples/testing/ts/test-helpers/dom-setup.ts.not-yet b/public/docs/_examples/testing/ts/test-helpers/dom-setup.ts.not-yet deleted file mode 100644 index 5e8f6d03a7..0000000000 --- a/public/docs/_examples/testing/ts/test-helpers/dom-setup.ts.not-yet +++ /dev/null @@ -1,18 +0,0 @@ -/////// 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"); -} diff --git a/public/docs/_examples/testing/ts/test-helpers/test-helpers.ts.not-yet b/public/docs/_examples/testing/ts/test-helpers/test-helpers.ts.not-yet deleted file mode 100644 index e39f4ae8ee..0000000000 --- a/public/docs/_examples/testing/ts/test-helpers/test-helpers.ts.not-yet +++ /dev/null @@ -1,103 +0,0 @@ -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; -type PromiseLikeTcbTestFn = (tcb: TestComponentBuilder, ...args:any[]) => PromiseLike; - -/** 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): FunctionWithParamTokens; -export function injectTcb(dependencies: any[], testFn: PromiseLikeTcbTestFn): FunctionWithParamTokens; -export function injectTcb(dependencies: any[] | PromiseLikeTcbTestFn, testFn?: PromiseLikeTcbTestFn) { - - if (typeof dependencies === 'function' ){ - testFn = dependencies; - dependencies = []; - } - - return injectAsync([TestComponentBuilder, ...(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); - }); -} \ No newline at end of file diff --git a/public/docs/_examples/testing/ts/test-shim.js b/public/docs/_examples/testing/ts/test-shim.js deleted file mode 100644 index 31e1998e69..0000000000 --- a/public/docs/_examples/testing/ts/test-shim.js +++ /dev/null @@ -1,48 +0,0 @@ -/*global jasmine, __karma__, window*/ - -// Browser testing shim -(function () { - -// Error.stackTraceLimit = Infinity; - -jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000; - -// Configure systemjs to use the .js extension for imports from the app folder -System.config({ - packages: { - app: { - format: 'register', - defaultExtension: 'js' - } - } -}); - -// Configure Angular for the browser and with test versions of the platform providers -System.import('angular2/testing') - .then(function (testing) { - return System.import('angular2/platform/testing/browser') - .then(function (providers) { - testing.setBaseTestProviders( - providers.TEST_BROWSER_PLATFORM_PROVIDERS, - providers.TEST_BROWSER_APPLICATION_PROVIDERS - ); - }); - }) - - // Load the spec files (__spec_files__) explicitly - .then(function () { - console.log('loading spec files: '+__spec_files__.join(', ')); - return Promise.all(__spec_files__.map(function(spec) { return System.import(spec);} )); - }) - - // After all imports load, re-execute `window.onload` which - // triggers the Jasmine test-runner start or explain what went wrong - .then(success, console.error.bind(console)); - -function success () { - console.log('Spec files loaded; starting Jasmine testrunner'); - window.onload(); -} - - -})(); diff --git a/public/docs/_examples/testing/ts/testing/fake-router.ts b/public/docs/_examples/testing/ts/testing/fake-router.ts new file mode 100644 index 0000000000..d42a3f8ad9 --- /dev/null +++ b/public/docs/_examples/testing/ts/testing/fake-router.ts @@ -0,0 +1,49 @@ + // export for convenience. +export { ActivatedRoute, Router, RouterLink, RouterOutlet} from '@angular/router'; + +import { Component, Directive, Injectable, Input } from '@angular/core'; +import { NavigationExtras } from '@angular/router'; + +@Directive({ + selector: '[routerLink]', + host: { + '(click)': 'onClick()', + '[attr.href]': 'visibleHref', + '[class.router-link-active]': 'isRouteActive' + } +}) +export class FakeRouterLinkDirective { + + isRouteActive = false; + visibleHref: string; // the url displayed on the anchor element. + + @Input('routerLink') linkParams: any; + navigatedTo: any = null; + + onClick() { + this.navigatedTo = this.linkParams; + } +} + +@Component({selector: 'router-outlet', template: ''}) +export class FakeRouterOutletComponent { } + +@Injectable() +export class FakeRouter { + lastCommand: any[]; + navigate(commands: any[], extras?: NavigationExtras) { + this.lastCommand = commands; + return commands; + } +} + +@Injectable() +export class FakeActivatedRoute { + testParams: {} = {}; + + get snapshot() { + return { + params: this.testParams + }; + } +} diff --git a/public/docs/_examples/testing/ts/testing/index.ts b/public/docs/_examples/testing/ts/testing/index.ts new file mode 100644 index 0000000000..f648a212e9 --- /dev/null +++ b/public/docs/_examples/testing/ts/testing/index.ts @@ -0,0 +1,23 @@ +import { tick, ComponentFixture } from '@angular/core/testing'; + +export * from './jasmine-matchers'; +export * from './fake-router'; + +// Short utilities +/** + * Create custom DOM event the old fashioned way + * + * https://developer.mozilla.org/en-US/docs/Web/API/Event/initEvent + * Although officially deprecated, some browsers (phantom) don't accept the preferred "new Event(eventName)" + */ +export function newEvent(eventName: string, bubbles = false, cancelable = false) { + let evt = document.createEvent('CustomEvent'); // MUST be 'CustomEvent' + evt.initCustomEvent(eventName, bubbles, cancelable, null); + return evt; +} + +/** Wait a tick, then detect changes */ +export function advance(f: ComponentFixture): void { + tick(); + f.detectChanges(); +} diff --git a/public/docs/_examples/testing/ts/testing/jasmine-matchers.d.ts b/public/docs/_examples/testing/ts/testing/jasmine-matchers.d.ts new file mode 100644 index 0000000000..f1c5acf77c --- /dev/null +++ b/public/docs/_examples/testing/ts/testing/jasmine-matchers.d.ts @@ -0,0 +1,5 @@ +declare namespace jasmine { + interface Matchers { + toHaveText(actual: any, expectationFailOutput?: any): jasmine.CustomMatcher; + } +} diff --git a/public/docs/_examples/testing/ts/testing/jasmine-matchers.ts b/public/docs/_examples/testing/ts/testing/jasmine-matchers.ts new file mode 100644 index 0000000000..4cab02e148 --- /dev/null +++ b/public/docs/_examples/testing/ts/testing/jasmine-matchers.ts @@ -0,0 +1,45 @@ +/// + +//// Jasmine Custom Matchers //// +// Be sure to extend jasmine-matchers.d.ts when adding matchers + +export function addMatchers(): void { + jasmine.addMatchers({ + toHaveText: toHaveText + }); +} + +function toHaveText(): jasmine.CustomMatcher { + return { + compare: function (actual: any, expectedText: string, expectationFailOutput?: any): jasmine.CustomMatcherResult { + const actualText = elementText(actual); + const pass = actualText.indexOf(expectedText) > -1; + const message = pass ? '' : composeMessage(); + return { pass, message }; + + function composeMessage () { + const a = (actualText.length < 100 ? actualText : actualText.substr(0, 100) + '...'); + const efo = expectationFailOutput ? ` '${expectationFailOutput}'` : ''; + return `Expected element to have text content '${expectedText}' instead of '${a}'${efo}`; + } + } + }; +} + +function elementText(n: any): string { + if (n instanceof Array) { + return n.map(elementText).join(''); + } + + if (n.nodeType === Node.COMMENT_NODE) { + return ''; + } + + if (n.nodeType === Node.ELEMENT_NODE && n.hasChildNodes()) { + return elementText(Array.prototype.slice.call(n.childNodes)); + } + + if (n.nativeElement) { n = n.nativeElement; } + + return n.textContent; +} diff --git a/public/docs/_examples/testing/ts/tsconfig.1.json b/public/docs/_examples/testing/ts/tsconfig.1.json deleted file mode 100644 index 062cf1bcb4..0000000000 --- a/public/docs/_examples/testing/ts/tsconfig.1.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "compilerOptions": { - "target": "es5", - "module": "system", - "moduleResolution": "node", - "sourceMap": true, - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "removeComments": false, - "noImplicitAny": true, - "suppressImplicitAnyIndexErrors": true - } -} diff --git a/public/docs/_examples/testing/ts/unit-tests-0.html b/public/docs/_examples/testing/ts/unit-tests-0.html deleted file mode 100644 index af7d1b9192..0000000000 --- a/public/docs/_examples/testing/ts/unit-tests-0.html +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - Ng App Unit Tests - - - - - - - - - - - - - - - - - - diff --git a/public/docs/_examples/testing/ts/unit-tests-1.html b/public/docs/_examples/testing/ts/unit-tests-1.html deleted file mode 100644 index b370ca053a..0000000000 --- a/public/docs/_examples/testing/ts/unit-tests-1.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - Ng App Unit Tests - - - - - - - - - - - - - - - diff --git a/public/docs/_examples/testing/ts/unit-tests-2.html b/public/docs/_examples/testing/ts/unit-tests-2.html deleted file mode 100644 index d47b4d1f60..0000000000 --- a/public/docs/_examples/testing/ts/unit-tests-2.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - Ng App Unit Tests - - - - - - - - - - - - - - - - - - - diff --git a/public/docs/_examples/testing/ts/unit-tests-3.html b/public/docs/_examples/testing/ts/unit-tests-3.html deleted file mode 100644 index 349606bd6d..0000000000 --- a/public/docs/_examples/testing/ts/unit-tests-3.html +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - Ng App Unit Tests - - - - - - - - - - - - - - - - - diff --git a/public/docs/_examples/testing/ts/unit-tests-4.html b/public/docs/_examples/testing/ts/unit-tests-4.html deleted file mode 100644 index a3e252fdb0..0000000000 --- a/public/docs/_examples/testing/ts/unit-tests-4.html +++ /dev/null @@ -1,43 +0,0 @@ - - - - - Ng App Unit Tests - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/public/docs/_examples/testing/ts/unit-tests-5.html b/public/docs/_examples/testing/ts/unit-tests-5.html deleted file mode 100644 index b95b36760a..0000000000 --- a/public/docs/_examples/testing/ts/unit-tests-5.html +++ /dev/null @@ -1,41 +0,0 @@ - - - - - Ng App Unit Tests - - - - - - - - - - - - - - - - - - - - - - - diff --git a/public/docs/_examples/testing/ts/unit-tests-6.html.not-yet b/public/docs/_examples/testing/ts/unit-tests-6.html.not-yet deleted file mode 100644 index df8e3704ba..0000000000 --- a/public/docs/_examples/testing/ts/unit-tests-6.html.not-yet +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - Ng App Unit Tests - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/public/docs/_examples/testing/ts/unit-tests-7.html.not-yet b/public/docs/_examples/testing/ts/unit-tests-7.html.not-yet deleted file mode 100644 index d5449711ee..0000000000 --- a/public/docs/_examples/testing/ts/unit-tests-7.html.not-yet +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - Ng App Unit Tests - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/public/docs/_examples/testing/ts/unit-tests-bag.html b/public/docs/_examples/testing/ts/unit-tests-bag.html deleted file mode 100644 index f373c387e8..0000000000 --- a/public/docs/_examples/testing/ts/unit-tests-bag.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - Bag of Unit Tests - - - - - - - - - - - - - - - - - - - - - - diff --git a/public/docs/_examples/testing/ts/unit-tests.html.not-yet b/public/docs/_examples/testing/ts/unit-tests.html.not-yet deleted file mode 100644 index f1b8ab444d..0000000000 --- a/public/docs/_examples/testing/ts/unit-tests.html.not-yet +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - Ng App Unit Tests - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/public/docs/_examples/testing/ts/wallaby.js b/public/docs/_examples/testing/ts/wallaby.js new file mode 100644 index 0000000000..acc34d35f5 --- /dev/null +++ b/public/docs/_examples/testing/ts/wallaby.js @@ -0,0 +1,119 @@ +// Configuration for the Wallaby Visual Studio Code testing extension +// https://marketplace.visualstudio.com/items?itemName=WallabyJs.wallaby-vscode +// Note: Wallaby is not open source and costs money + +module.exports = function () { + return { + files: [ + // System.js for module loading + {pattern: 'node_modules/systemjs/dist/system.js', instrument: false}, + {pattern: 'systemjs.config.js', instrument: false}, + {pattern: 'systemjs.config.extras.js', instrument: false}, + + // Polyfills + {pattern: 'node_modules/core-js/client/shim.min.js', instrument: false}, + {pattern: 'node_modules/reflect-metadata/Reflect.js', instrument: false}, + + // zone.js + {pattern: 'node_modules/zone.js/dist/zone.js', instrument: false}, + {pattern: 'node_modules/zone.js/dist/long-stack-trace-zone.js', instrument: false}, + {pattern: 'node_modules/zone.js/dist/proxy.js', instrument: false}, + {pattern: 'node_modules/zone.js/dist/sync-test.js', instrument: false}, + {pattern: 'node_modules/zone.js/dist/jasmine-patch.js', instrument: false}, + {pattern: 'node_modules/zone.js/dist/async-test.js', instrument: false}, + {pattern: 'node_modules/zone.js/dist/fake-async-test.js', instrument: false}, + + // application (but not specs) loaded via module imports + {pattern: 'app/**/*+(ts|html|css)', load: false}, + {pattern: 'app/**/*.spec.ts', ignore: true}, + + {pattern: 'testing/**/*+(ts|html|css)', load: false}, + ], + + tests: [ + {pattern: 'app/**/*.spec.ts', load: false} + ], + + middleware: function (app, express) { + app.use('/node_modules', express.static(require('path').join(__dirname, 'node_modules'))); + }, + + testFramework: 'jasmine', + + debug: true, + + bootstrap: bootstrap + }; +}; + +// Like karma-test-shim.js +function bootstrap (wallaby) { + wallaby.delayStart(); + + System.config({ + // Extend usual application package list with test folder + packages: { 'testing': { main: 'index.js', defaultExtension: 'js' } }, + + // Assume npm: is set in `paths` in systemjs.config + // Map the angular testing umd bundles + map: { + '@angular/core/testing': 'npm:@angular/core/bundles/core-testing.umd.js', + '@angular/common/testing': 'npm:@angular/common/bundles/common-testing.umd.js', + '@angular/compiler/testing': 'npm:@angular/compiler/bundles/compiler-testing.umd.js', + '@angular/platform-browser/testing': 'npm:@angular/platform-browser/bundles/platform-browser-testing.umd.js', + '@angular/platform-browser-dynamic/testing': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic-testing.umd.js', + '@angular/http/testing': 'npm:@angular/http/bundles/http-testing.umd.js', + '@angular/router/testing': 'npm:@angular/router/bundles/router-testing.umd.js', + '@angular/forms/testing': 'npm:@angular/forms/bundles/forms-testing.umd.js', + }, + }); + + System.import('systemjs.config.js') + .then(importSystemJsExtras) + .then(initTestBed) + .then(initTesting); + + /** Optional SystemJS configuration extras. Keep going w/o it */ + function importSystemJsExtras(){ + return System.import('systemjs.config.extras.js') + .catch(function(reason) { + console.log( + 'Warning: System.import could not load the optional "systemjs.config.extras.js". Did you omit it by accident? Continuing without it.' + ); + console.log(reason); + }); + } + + function initTestBed(){ + return Promise.all([ + System.import('@angular/core/testing'), + System.import('@angular/platform-browser-dynamic/testing') + ]) + + .then(function (providers) { + var coreTesting = providers[0]; + var browserTesting = providers[1]; + + coreTesting.TestBed.initTestEnvironment( + browserTesting.BrowserDynamicTestingModule, + browserTesting.platformBrowserDynamicTesting()); + }) + } + + // Load all spec files and start wallaby + function initTesting () { + return Promise.all( + wallaby.tests.map(function (specFile) { + return System.import(specFile); + }) + ) + .then(function () { + wallaby.start(); + }) + .catch(function (e) { + setTimeout(function () { + throw e; + }, 0); + }); + } +} diff --git a/public/docs/_examples/toh-6/ts/app/hero-search.service.ts b/public/docs/_examples/toh-6/ts/app/hero-search.service.ts index ae2e47670a..a7fb3cf520 100644 --- a/public/docs/_examples/toh-6/ts/app/hero-search.service.ts +++ b/public/docs/_examples/toh-6/ts/app/hero-search.service.ts @@ -12,7 +12,7 @@ export class HeroSearchService { search(term: string): Observable { return this.http - .get(`app/heroes/?name=${term}`) + .get('app/heroes/?name=${term}') .map((r: Response) => r.json().data as Hero[]); } } diff --git a/public/docs/_examples/wallaby.js b/public/docs/_examples/wallaby.js deleted file mode 100644 index 28053a11fe..0000000000 --- a/public/docs/_examples/wallaby.js +++ /dev/null @@ -1,77 +0,0 @@ -// Configuration for the Wallaby Visual Studio Code testing extension -// https://marketplace.visualstudio.com/items?itemName=WallabyJs.wallaby-vscode -// Note: Wallaby is not open source and costs money - -module.exports = function () { - - return { - files: [ - // System.js for module loading - {pattern: 'node_modules/systemjs/dist/system.js', instrument: false}, - {pattern: 'systemjs.config.js', instrument: false}, - - // Polyfills - {pattern: 'node_modules/core-js/client/shim.min.js', instrument: false}, - - // Reflect, Zone.js, and test shims - // Rx.js, Angular 2 itself, and the testing library not here because loaded by systemjs - {pattern: 'node_modules/reflect-metadata/Reflect.js', instrument: false}, - {pattern: 'node_modules/zone.js/dist/zone.js', instrument: false}, - {pattern: 'node_modules/zone.js/dist/jasmine-patch.js', instrument: false}, - {pattern: 'node_modules/zone.js/dist/async-test.js', instrument: false}, - {pattern: 'node_modules/zone.js/dist/fake-async-test.js', instrument: false}, - - {pattern: 'app/**/*+(ts|html|css)', load: false}, - {pattern: 'app/**/*.spec.ts', ignore: true} - ], - - tests: [ - {pattern: 'app/**/*.spec.ts', load: false} - ], - - middleware: function (app, express) { - app.use('/node_modules', express.static(require('path').join(__dirname, 'node_modules'))); - }, - - testFramework: 'jasmine', - - debug: true, - - bootstrap: function (wallaby) { - wallaby.delayStart(); - - System.config({ - packageWithIndex: true // sadly, we can't use umd packages (yet?) - }); - - System.import('systemjs.config.js') - .then(function () { - return Promise.all([ - System.import('@angular/core/testing'), - System.import('@angular/platform-browser-dynamic/testing') - ]) - }) - .then(function (providers) { - var testing = providers[0]; - var testingBrowser = providers[1]; - - testing.setBaseTestProviders( - testingBrowser.TEST_BROWSER_DYNAMIC_PLATFORM_PROVIDERS, - testingBrowser.TEST_BROWSER_DYNAMIC_APPLICATION_PROVIDERS); - - // Load all spec files - return Promise.all(wallaby.tests.map(function (specFile) { - return System.import(specFile); - })); - }) - .then(function () { - wallaby.start(); - }) - .catch(function (e) { - setTimeout(function () { - throw e; - }, 0); - }); - } - }; -}; diff --git a/public/docs/ts/latest/guide/testing.jade b/public/docs/ts/latest/guide/testing.jade index 7777219c47..dd8fafa036 100644 --- a/public/docs/ts/latest/guide/testing.jade +++ b/public/docs/ts/latest/guide/testing.jade @@ -1,102 +1,1785 @@ -.alert.is-important - :marked - We are still preparing the testing guide with all the new testing features - introduced in RC5 and will update it very soon. +block includes + include ../_util-fns + - var _JavaScript = 'JavaScript'; + //- Double underscore means don't escape var, use !{__var}. + - var __chaining_op = '; or ,'; + - var __new_op = 'new'; + - var __objectAsMap = 'object'; :marked - We write **unit tests** to explore and confirm the **behavior** of parts of our application. + This chapter offers tips and techniques for testing Angular applications. + Along the way you will learn some general testing principles and techniques but the focus is on + Angular testing. - 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. - -a(id="top") +a#top :marked - # Table of Contents - - 1. [Jasmine Testing 101](#jasmine-101) - - setup to run Jasmine tests in the browser - - basic Jasmine testing skills - - write simple Jasmine tests in TypeScript - - debug a test in the browser - - 1. [The Application Under Test](#aut) - - 1. [First app test](#first-app-tests) - - test a simple application interface outside of Angular - - where to put the test file - - load a test file with systemJS - - 1. [Pipe driven development](#pipe-testing) - - create a test before creating a class - - load multiple test files in our test harness, using system.js - - add the Angular 2 library to our test harness - - watch the new test fail, and fix it - - 1. Test an Asynchronous Service (forthcoming) - - test an asynchronous service class outside of Angular - - write a test plan in code - - fake a dependency - - master the `catch(fail).then(done)` pattern - - move setup to `beforeEach` - - test when a dependency fails - - control async test timeout - - 1. The Angular Test Environment (forthcoming) - - the Angular test environment and why we need help - - add the Angular Test libraries to the test harness - - test the same async service using Angular Dependency Injection - - reduce friction with test helpers - - introducing spies - - 1. Test a Component (forthcoming) - - test the component outside of Angular - - mock the dependent asynchronous service - - simulate interaction with the view (no DOM) - - use a spy-promise to control asynchronous test flow - - 1. Test a Component in the DOM (forthcoming - - test the component inside the Angular test environment - - use the `TestComponentBuilder` - - more test helpers - - interact with the DOM - - bind to a mock dependent asynchronous service - - 1. Run the tests with karma (forthcoming) + # Contents + * [Introduction to Angular Testing](#testing-101) + * [Setup](#setup) + * [The first karma test](#1st-karma-test) + * [The Angular Testing Platform (ATP) ](#atp-intro) + * [The sample application and its tests](#sample-app) + * [A simple component test](#simple-component-test) + * [Test a component with a service dependency](#component-with-dependency) + * [Test a component with an async service](#component-with-async-service) + * [Test a component with an external template](#component-with-external-template) + * [Test a component with inputs and outputs](#component-with-inputs-output) + * [Test a component inside a test host component](#component-inside-test-host) + * [Test a routed component](#routed-component) + * [Isolated tests](#testing-without-atp "Testing without the Angular Testing Platform") + * [_TestBed_ API](#atp-api) + * [FAQ](#faq "Frequently asked questions") +:marked It’s a big agenda. Fortunately, you can learn a little bit at a time and put each lesson to use. + # Live examples + The chapter sample code is available as live examples for inspection, experiment, and download. + + * The sample application + * The first spec + * The complete application specs + * A grab bag of demonstration specs a(href="#top").to-top Back to top .l-hr -a(id="jasmine-101") +a#testing-101 :marked - # Jasmine Testing 101 -!= partial("../testing/jasmine-testing-101") -a(href="#top").to-top Back to top + # Introduction to Angular Testing + + You write tests to explore and confirm the behavior of the application. + + 1. They **guard** against changes that break existing code (“regressions”). + + 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 shine a harsh light on the code from many angles. + When a part of the application seems hard to test, the root cause is often a design flaw, + something to cure now rather than later when it becomes expensive to fix. + + This chapter assumes that you know something about testing. Don't worry if you don't. + There are plenty of books and online resources to get up to speed. + + + + + ## Tools and Technologies + + You can write and run Angular tests with a variety of tools and technologies. + This chapter describes specific choices that are known to work well. + +table(width="100%") + col(width="20%") + col(width="80%") + tr + th Technology + th Purpose + tr(style=top) + td(style="vertical-align: top") Jasmine + td + :marked + The [Jasmine test framework](http://jasmine.github.io/2.4/introduction.html). + provides everything needed to write basic tests. + It ships with an HTML test runner that executes tests in the browser. + tr(style=top) + td(style="vertical-align: top") Angular Testing Platform + td + :marked + The Angular Testing Platform creates a test environment and harness + for the application code under test. + Use it to condition and control parts of the application as they + interact _within_ the Angular environment. + tr(style=top) + td(style="vertical-align: top") Karma + td + :marked + The [karma test runner](https://karma-runner.github.io/1.0/index.html) + is ideal for writing and running tests while developing the application. + It can be an integral part of the application build process. + This chapter describes how to setup and run tests with karma. + tr(style=top) + td(style="vertical-align: top") Protractor + td + :marked + Use protractor to write and run _end-to-end_ (e2e) tests. + End-to-end tests explore the application _as users experience it_. + In e2e testing, one process runs the real application + and a second process runs protractor tests that simulate user behavior + and assert that the application responds in the browser as expected. .l-hr -a(id="aut") +a#setup :marked - # The Application to Test -!= partial("../testing/application-under-test") -a(href="#top").to-top Back to top + # Setup + + Many think writing tests is fun. + Few enjoy setting up the test environment. + To get to the fun as quickly as possible, + the deep details of setup appear later in the chapter (_forthcoming_). + A bare minimum of discussion plus the downloadable source code must suffice for now. + + There are two fast paths to getting started. + 1. Start a new project following the instructions in the + [QuickStart github repository](https://github.com/angular/quickstart/blob/master/README.md). + + 1. Start a new project with the + [Angular CLI](https://github.com/angular/angular-cli/blob/master/README.md). + + Both approaches install **npm packages, files, and scripts** pre-configured for applications + built in their respective modalities. + Their artifacts and procedures differ slightly but their essentials are the same + and there are no differences in the test code. + + In this chapter, the application and its tests are based on the QuickStart repo. + +.alert.is-helpful + :marked + If youur application was based on the QuickStart repository, + you can skip the rest of this section and get on with your first test. + The QuickStart repo provides all necessary setup. + +:marked + Here's brief description of the setup files. + +table(width="100%") + col(width="20%") + col(width="80%") + tr + th File + th Description + tr + td(style="vertical-align: top") karma.conf.js + td + :marked + The karma configuration file that specifies which plug-ins to use, + which application and test files to load, which browser(s) to use, + and how to report test results. + + It loads three other setup files: + * `systemjs.config.js` + * `systemjs.config.extras.js` + * `karma-test-shim.js` + tr + td(style="vertical-align: top") karma-test-shim.js + td + :marked + This shim prepares karma specifically for the Angular test environment + and launches karma itself. + It loads the `systemjs.config.js` file as part of that process. + tr + td(style="vertical-align: top") systemjs.config.js + td + :marked + [SystemJS](https://github.com/systemjs/systemjs/blob/master/README.md) + loads the application and test modules. + This script tells SystemJS where to find the module files and how to load them. + It's the same version of the file used by QuickStart-based applications. + tr + td(style="vertical-align: top") systemjs.config.extras.js + td + :marked + An optional file that supplements the SystemJS configuration in `systemjs.config.js` with + configuration for the specific needs of the application itself. + + A stock `systemjs.config.js` can't anticipate those needs. + You fill the gaps here. + + The sample version for this chapter adds the **model barrel** + to the SystemJs `packages` configuration. + tr + td(colspan="2") + +makeExample('testing/ts/systemjs.config.extras.js', '', 'systemjs.config.extras.js')(format='.') + +:marked + ### npm packages + + The sample tests are written to run in Jasmine and karma. + The two "fast path" setups added the appropriate Jasmine and karma npm packages to the + `devDependencies` section of the `package.json`. + They were installed when you ran `npm install`. .l-hr -a(id="first-app-tests") +a#1st-karma-test :marked - # First app test -!= partial("../testing/first-app-tests") -a(href="#top").to-top Back to top + # The first karma test -.l-hr -a(id="pipe-testing") -:marked - # Pipe driven development -!= partial("../testing/testing-an-angular-pipe") -a(href="#top").to-top Back to top + Start with a simple test to make sure the setup works properly. + + Create a new file called `1st.spec.ts` in the application root folder, `app/` .alert.is-important :marked - The testing chapter is still under development. - Please bear with us as we both update and complete it. + Tests written in Jasmine are called _specs_ . + **The filename extension must be `.spec.ts`**, + the convention adhered to by `karma.conf.js` and other tooling. + +:marked + **Put spec files somewhere within the `app/` folder.** + The `karma.conf.js` tells karma to look for spec files there, + for reasons explained [below](#spec-file-location). + + Add the following code to `app/1st.spec.ts`. ++makeExample('testing/ts/app/1st.spec.ts', '', 'app/1st.spec.ts')(format='.') +:marked + ## Run karma + Compile and run it in karma from the command line. + +.l-sub-section + :marked + The QuickStart repo adds the following command to the `scripts` section in `package.json`. + + code-example(format="." language="bash"). + "test": "tsc && concurrently \"tsc -w\" \"karma start karma.conf.js\"", + :marked + Add that to your `package.json` if it's not there already. + +:marked + Open a terminal or command window and enter +code-example(format="." language="bash"). + npm test +:marked + The command compiles the application and test code a first time. + If the compile fails, the command aborts. + + If it succeeds, the command re-compiles (this time in watch mode) in one process + and starts karma in another. + Both processes watch pertinent files and re-run when they detect changes. + + After a few moments, karma opens a browser ... +figure.image-display + img(src='/resources/images/devguide/testing/karma-browser.png' style="width:400px;" alt="Karma browser") +:marked + ... and starts writing to the console. + + Hide (don't close!) the browser and focus on the console output which should look something like this. + +code-example(format="." language="bash"). + > npm test + > tsc && concurrently "tsc -w" "karma start karma.conf.js" + + [0] 1:37:03 PM - Compilation complete. Watching for file changes. + [1] 24 07 2016 13:37:09.310:WARN [karma]: No captured browser, open http://localhost:9876/ + [1] 24 07 2016 13:37:09.361:INFO [karma]: Karma v0.13.22 server started at http://localhost:9876/ + [1] 24 07 2016 13:37:09.370:INFO [launcher]: Starting browser Chrome + [1] 24 07 2016 13:37:10.974:INFO [Chrome 51.0.2704]: Connected on socket /#Cf6A5PkvMzjbbtn1AAAA with id 24600087 + [1] Chrome 51.0.2704: Executed 0 of 0 SUCCESS + Chrome 51.0.2704: Executed 1 of 1 SUCCESS + SUCCESS (0.005 secs / 0.005 secs) + +:marked + Both the compiler and karma continue to run. The compiler output is preceeded by `[0]`; + the karma output by `[1]`. + + Change the expectation from `true` to `false`. + + The _compiler_ watcher detects the change and recompiles. + +code-example(format="." language="bash"). + [0] 1:49:21 PM - File change detected. Starting incremental compilation... + [0] 1:49:25 PM - Compilation complete. Watching for file changes. + +:marked + The _karma_ watcher detects the change to the compilation output and re-runs the test. +code-example(format="." language="bash"). + [1] Chrome 51.0.2704: Executed 0 of 1 SUCCESS + Chrome 51.0.2704 1st tests true is true FAILED + [1] Expected false to equal true. + [1] Chrome 51.0.2704: Executed 1 of 1 (1 FAILED) (0.005 secs / 0.005 secs) + +:marked + It failed of course. + + Restore the expectation from `false` back to `true`. + Both processes detect the change, re-run, and karma reports complete success. + +.alert.is-helpful + :marked + The console log can be quite long. Keep your eye on the last line. + It says `SUCCESS` when all is well. + + If it says `FAILED`, scroll up to look for the error or, if that's too painful, + pipe the console output to a file and inspect with your favorite editor. + code-example(format="." language="json"). + npm test > spec-output.txt + +:marked + ## Test debugging + + Debug specs in the browser in the same way you debug an application. + + - Reveal the karma browser window (hidden earlier). + - Open the browser's “Developer Tools” (F12 or Ctrl-Shift-I). + - Pick the “sources” section + - Open the `1st.spec.ts` test file (Ctrl-P, then start typing the name of the file). + - Set a breakpoint in the test + - Refresh the browser … and it stops at the breakpoint. + +figure.image-display + img(src='/resources/images/devguide/testing/karma-1st-spec-debug.png' style="width:700px;" alt="Karma debugging") + +a(href="#top").to-top Back to top + +.l-hr +a#atp-intro +:marked + # The Angular Testing Platform (ATP) + + Many tests explore how applications classes interact with Angular and the DOM while under Angular's control. + + Such tests are easy to write with the help of the _Angular Testing Platform_ (ATP) + which consists of the `TestBed` class and some helper functions. + + Tests written with the _Angular Testing Platform_ are the main focus of this chapter. + But they are not the only tests you should write. + + ### Isolated unit tests + + You can and should write [isolated unit tests](#testing-without-atp "Testing without the Angular Testing Platform") + for components, directives, pipes, and services. + Isolated unit tests examine an instance of a class all by itself without + any dependence on Angular or any injected values. + The tester creates a test instance of the class with new, supplying fake constructor parameters as needed, and + then probes the test instance API surface. + + Isolated tests don't reveal how the class interacts with Angular. + In particular, they can't reveal how a component class interacts with its own template or with other components. + + Those tests require the Angular Testing Platform. + + ### Testing with the _ Angular Testing Platform_ + + The _Angular Testing Platform_ consists of the `TestBed` class and some helper functions from `@angular/core/testing`. +.alert.is-important + :marked + The _TestBed_ is officially _experimental_ and thus subject to change. + Consult the [API reference](../api/core/testing/index/TestBed-class.html) for the latest status. +:marked + The `TestBed` creates an Angular test module — an `@NgModule` class — + that you configure to produce the module environment for the class you want to test. + You tell the `TestBed` to create an instance of the test component and probe that instance with tests. + + That's the `TestBed` in a nutshell. + + In practice, you work with the static methods of the `TestBed` class. + These static methods create and update a fresh hidden `TestBed` instance before each Jasmine `it`. +.l-sub-section + :marked + You can access that hidden instance anytime by calling `getTestBed()`; +:marked + This `TestBed` instance comes pre-configured with a baseline of default providers and declarables (components, directives, and pipes) + that almost everyone needs. + This chapter tests a browser application so the default includes the `CommonModule` declarables from `@angular/common` + and the `BrowserModule` providers (some of them mocked) from `@angular/platform-browser`. + + You refine the default test module configuration with application and test specifics + so that it can produce an instance of the test component in the Angular environment suitable for your tests. + + Start by calling `TestBed.configureTestingModule` with an object that looks like `@NgModule` metadata. + This object defines additional imports, declarations, providers and schemas. + + After configuring the `TestBed`, tell it to create an instance of the test component and the test fixture + you'll need to inspect and control the component's immediate environment. + ++makeExample('testing/ts/app/banner.component.spec.ts', 'simple-example-before-each', 'app/banner.component.spec.ts (simplified)')(format='.') +:marked + Angular tests can interact with the HTML in the test DOM, + simulate user activity, tell Angular to perform specific task (such as change detection), + and see the effects of these actions both in the test component and in the test DOM. ++makeExample('testing/ts/app/banner.component.spec.ts', 'simple-example-it', 'app/banner.component.spec.ts (simplified)')(format='.') +:marked + A comprehensive review of the _TestBed_ API appears [later in the chapter](#atp-api). + Let's dive right into Angular testing, starting with with the components of a sample application. + +a(href="#top").to-top Back to top + +.l-hr + +a#sample-app +:marked + # The sample application and its tests + + This chapter tests a cut-down version of the _Tour of Heroes_ [tutorial app](../tutorial). + + The following live example shows how it works and provides the complete source code. + +

+:marked + The following live example runs all the tests of this application + inside the browser, using the Jasmine Test Runner instead of karma. + + It includes the tests discussed in this chapter and additional tests for you to explore. + This live example contains both application and test code. + It is large and can take several minutes to start. Please be patient. + + +a(href="#top").to-top Back to top +.l-hr + +a#simple-component-test +:marked + # Test a component + +:marked + The top of the screen displays application title, presented by the `BannerComponent` in `app/banner.component.ts`. ++makeExample('testing/ts/app/banner.component.ts', '', 'app/banner.component.ts')(format='.') +:marked + `BannerComponent` has an inline template and an interpolation binding, about as simple as it gets. + Probably too simple to be worth testing in real life but perfect for a first encounter with the `TestBed`. + + The corresponding `app/banner-component.spec.ts` sits in the same folder as the component, + for reasons explained [here](#q-spec-file-location); + + Start with ES6 import statements to get access to symbols referenced in the spec. ++makeExample('testing/ts/app/banner.component.spec.ts', 'imports', 'app/banner.component.spec.ts (imports)')(format='.') +:marked + Here's the setup for the tests followed by observations about the `beforeEach`: ++makeExample('testing/ts/app/banner.component.spec.ts', 'setup', 'app/banner.component.spec.ts (imports)')(format='.') +:marked + `TestBed.configureTestingModule` takes an `@NgModule`-like metadata object. + This one simply declares the component to test, `BannerComponent`. + + It lacks `imports` because (a) it extends the default test module configuration which + already has what `BannerComponent` needs + and (b) `BannerComponent` doesn't interact with any other components. + + The configuration could have imported `AppModule` (which declares `BannerComponent`). + But that would lead to tons more configuration in order to support the other components within `AppModule` + that have nothing to do with `BannerComponent`. + + `TestBed.createComponent` creates an instance of `BannerComponent` to test. + The method returns a `ComponentFixture`, a handle on the test environment surrounding the created component. + The fixture provides access to the component instance itself and + to the `DebugElement` which is a handle on the component's DOM element. + + Query the `DebugElement` by CSS selector for the `

` sub-element that holds the actual title. + + + ### _createComponent_ closes configuration + `TestBed.createComponent` closes the current `TestBed` instance to further configuration. + You cannot call any more `TestBed` configuration methods, not `configureTestModule` + nor any of the `override...` methods. The `TestBed` throws an error if you try. + +.alert.is-important + :marked + Do not configure the `TestBed` after calling `createComponent`. +:marked + ### The tests + Jasmine runs this `beforeEach` before each test of which there are two ++makeExample('testing/ts/app/banner.component.spec.ts', 'tests', 'app/banner.component.spec.ts (tests)')(format='.') +:markdown + These tests ask the `DebugElement` for the native HTML element to satisfy their expectations. + +a#fixture-detect-changes +:marked + ### _detectChanges_: Angular change detection under test + + Each test tells Angular when to perform change detection by calling `fixture.detectChanges()`. + The first test does so immediately, triggering data binding and propagation of the `title` property + to the DOM element. + + The second test changes the component's `title` property _and only then_ calls `fixture.detectChanges()`; + the new value appears in the DOM element. + + In production, change detection kicks in automatically + when Angular creates a component or the user enters a keystroke or + an asynchronous activity (e.g., AJAX) completes. + + The `TestBed.createComponent` does _not_ trigger change detection. + The fixture does not automatically push the component's `title` property value into the data bound element, + a fact demonstrated in the following test: ++makeExample('testing/ts/app/banner.component.spec.ts', 'test-w-o-detect-changes', 'app/banner.component.spec.ts (no detectChanges)')(format='.') +:marked + This behavior (or lack of it) is intentional. + It gives the tester an opportunity to investigate the state of + the component _before Angular initiates data binding or calls lifecycle hooks_. + +a#automatic-change-detection +:marked + ### Automatic change detection + Some testers prefer that the Angular test environment run change detection automatically. + That's possible by configuring the `TestBed` with the _AutoDetect_ provider: ++makeExample('testing/ts/app/banner.component.spec.ts', 'auto-detect', 'app/banner.component.spec.ts (AutoDetect)')(format='.') +:marked + Here are three tests that illustrate how _auto-detect_ works. ++makeExample('testing/ts/app/banner.component.spec.ts', 'auto-detect-tests', 'app/banner.component.spec.ts (AutoDetect Tests)')(format='.') +:marked + The first test shows the benefit of automatic change detection. + + The second and third test remind us that Angular does _not_ know about changes to component property + values unless Angular itself (or some asynchronous process) makes the change. + This is as true in production as it is in test. + + In production, external forces rarely change component properties like this, + whereas these kinds of probing changes are typical in unit tests. + The tester will have to call `fixture.detectChanges()` quite often + despite having opted into auto detect. + +.alert.is-helpful + :marked + Rather than wonder when the test fixture will or won't perform change detection, + the samples in this chapter _always call_ `detectChanges()` _explicitly_. + +a(href="#top").to-top Back to top + +.l-hr + +a#component-with-dependency +:marked + # Test a component with a dependency + Components often have service dependencies. + The `WelcomeComponent` displays a welcome message to the logged in user. + It knows who the user is based on a property of the injected `UserService`: ++makeExample('testing/ts/app/welcome.component.ts', '', 'app/welcome.component.ts')(format='.') +:marked + The `WelcomeComponent` has decision logic that interacts with the service; + such logic makes this component worth testing. + Here's the test module configuration for the spec file, `app/welcome.component.spec.ts`: ++makeExample('testing/ts/app/welcome.component.spec.ts', 'config-test-module', 'app/welcome.component.spec.ts')(format='.') +:marked + This time, in addition to declaring the component under test, + the configurations sets the `providers` list with the dependent `UserService`. + + This example configures the test module with a _fake_ `UserService`. + + ## Provide service fakes + + A component under test doesn't have to be injected with real services. + In fact, it is usually better if they are fakes. + The purpose of the spec is to test the component, not the service, + and real services can be trouble. + + Injecting the real `UserService` could be a nightmare. + The real service might try to ask the user for login credentials and + try to reach an authentication server. + These behaviors could be hard to intercept. + It is far easier to create and register a fake `UserService`. + + There are many ways to fake a service. + This test suit supplies a minimal `UserService` that satisfies the needs of the `WelcomeComponent` + and its tests: ++makeExample('testing/ts/app/welcome.component.spec.ts', 'fake-userservice')(format='.') + +a#injected-service-reference +:marked + ## Referencing injected services + The tests need access to the injected (fake) `UserService`. + + You cannot reference the `fakeUserService` object provided to the test module. + **It does not work!** + Surprisingly, the instance actually injected into the component is _not the same_ + as the provided `fakeUserService` object. + +.alert.is-important + :marked + Always use an injector to get a reference to an injected service. +:marked + Where do you get the injector? + Angular has an hierarchical injection system. + In a test there can be injectors at multiple levels. + The current `TestBed` injector creates a top-level injector. + The `WelcomeComponent` injector is a child of that injector created specifically for the component. + + You can get a `UserService` from the current `TestBed` injector by calling `TestBed.get`. ++makeExample('testing/ts/app/welcome.component.spec.ts', 'inject-from-testbed', 'TestBed injector')(format='.') +.l-sub-section + :marked + The [inject](#inject) function is another way to inject one or more services into a test. +:marked + That happens to work for testing the `WelcomeComponent` because the `UserService` instance from the `TestBed` + is the same as the `UserService` instance injected into the component. + + That won't always be the case. + Be absolutely sure to reference the service instance that the component is _actually receiving_, + Call `get` on the component's injector which is `fixture.debugElement.injector`: ++makeExample('testing/ts/app/welcome.component.spec.ts', 'injected-service', 'Component\'s injector')(format='.') +.alert.is-important + :marked + Use the component's own injector to get the component's injected service. +a#welcome-spec-setup +:marked + Here's the complete, preferred `beforeEach`: ++makeExample('testing/ts/app/welcome.component.spec.ts', 'setup', 'app/welcome.component.spec.ts')(format='.') +:marked + And here are some tests: ++makeExample('testing/ts/app/welcome.component.spec.ts', 'tests', 'app/welcome.component.spec.ts')(format='.') +:marked + The first is a sanity test; it confirms that the fake `UserService` is working. + The remaining tests confirm the logic of the component when the service returns different values. + The second test validates the effect of changing the user name. + The third test checks that the component displays the proper message when there is no logged-in user. + +a(href="#top").to-top Back to top + +.l-hr + +a#component-with-async-service +:marked + # Test a component with an async service + Many services return values asynchronously. + Most data services make an HTTP request to a remote server and the response is necessarily asynchronous. + + The "About" view in this sample displays Mark Twain quotes. + The `TwainComponent` handles the display, delegating the server request to the `TwainService`. + Both are in the `app/shared` folder because the author intends to display Twain quotes on other pages someday. + Here is the `TwainComponent`. ++makeExample('testing/ts/app/shared/twain.component.ts', 'component', 'app/shared/twain.component.ts')(format='.') +:marked + The `TwainService` implementation is irrelevant at this point. + It is sufficient to see within `ngOnInit` that `twainService.getQuote` returns a promise which means it is asynchronous. + + In general, tests should not make calls to remote servers. + They should fake such calls. The setup in this `app/shared/twain.component.spec.ts` shows one way to do that: ++makeExample('testing/ts/app/shared/twain.component.spec.ts', 'setup', 'app/shared/twain.component.spec.ts (setup)')(format='.') + +a#service-spy +:marked + ### Spying on the real service + + This setup is similar to the [`welcome.component.spec` setup](#welcome-spec-setup). + But instead of creating a fake service object, it injects the _real_ service (see the test module `providers`) and + replaces the critical `getQuote` method with a Jasmine spy. ++makeExample('testing/ts/app/shared/twain.component.spec.ts', 'spy')(format='.') +:marked + The spy is designed such that any call to `getQuote` receives an immediately resolved promise with a test quote. + The spy bypasses the actual `getQuote` method and therefore will not contact the server. + +.l-sub-section + :marked + Faking a service instance and spying on the real service are _both_ great options. + Pick the one that seems easiest for the current test suite. Don't be afraid to change your mind. +:marked + Here are the tests with commentary to follow: ++makeExample('testing/ts/app/shared/twain.component.spec.ts', 'tests', 'app/shared/twain.component.spec.ts (tests)') +:marked + ### Synchronous tests + The first two tests are synchronous. + Neither test can prove that a value from the service will be displayed. + + Thanks to the spy, the second test verifies that `getQuote` is called. + But the quote itself has not arrived, despite the fact that the spy returns a resolved promise. + + This test must wait at least one full turn of the JavaScript engine, a least one "tick", before the + value becomes available. By that time, the test runner has moved on to the next test in the suite. + + The test must become an "async test" ... like the third test + +a#async-fn-in-it +:marked + ## The _async_ function in _it_ + + Notice the `async` in the third test. ++makeExample('testing/ts/app/shared/twain.component.spec.ts', 'async-test', 'app/shared/twain.component.spec.ts (async test)')(format='.') +:marked + The `async` function is part of the _Angular TestBed_ feature set. + It _takes_ a parameterless function and _returns_ a parameterless function + which becomes the argument to the Jasmine `it` call. + + The body of the `async` argument looks much like the body of a normal `it` argument. + There is nothing obviously asynchronous about it. For example, it doesn't return a promise. + + The `async` function arranges for the tester's code to run in a special _async test zone_ + that almost hides the mechanics of asynchronous execution. + + Almost but not completely. + +a#when-stable +:marked + ## _whenStable_ + The test must wait for the `getQuote` promise to resolve. + + The `getQuote` promise promise resolves in the next turn of the JavaScript engine, thanks to the spy. + But a different test implementation of `getQuote` could take longer. + An integration test might call the _real_ `getQuote`, resulting in an XHR request + that took many seconds to respond. + + This test has no direct access to the promise returned by the call to `testService.getQuote` + which is private and inaccessible inside `TwainComponent`. + + Fortunately, the `getQuote` promise is accessible to the _async test zone_ + which intercepts all promises issued within the _async_ method call. + + The `ComponentFixture.whenStable` method returns its own promise which resolves when the `getQuote` promise completes. + In fact, the _whenStable_ promise resolves when _all pending asynchronous activities_ complete ... the definition of "stable". + + Then the testing continues. + The test kicks off another round of change detection (`fixture.detechChanges`) which tells Angular to update the DOM with the quote. + The `getQuote` helper method extracts the display element text and the expectation confirms that the text matches the test quote. + +a#fakeAsync +a#fake-async +:marked + ## The _fakeAsync_ function + + The fourth test verifies the same component behavior in a different way. ++makeExample('testing/ts/app/shared/twain.component.spec.ts', 'fake-async-test', 'app/shared/twain.component.spec.ts (fakeAsync test)')(format='.') +:marked + Notice that `fakeAsync` replaces `async` as the `it` argument. + The `fakeAsync` function is also part of the _Angular TestBed_ feature set. + Like `async`, it too _takes_ a parameterless function and _returns_ a parameterless function + which becomes the argument to the Jasmine `it` call. + + The `async` function arranges for the tester's code to run in a special _fakeAsync test zone_. + + The key advantage of `fakeAsync` is that the test body looks entirely synchronous. + There are no promises at all. + No `then(...)` chains to disrupt the visible flow of control. + +.l-sub-section + :marked + There are limitations. For example, you cannot make an XHR call from within a `fakeAsync`. +:marked + +a#tick +a#tick-first-look +:marked + ## The _tick_ function + Compare the third and fourth tests. Notice that `fixture.whenStable` is gone, replaced by `tick()`. + + The `tick` function is a part of the _Angular TestBed_ feature set and a companion to `fakeAsync`. + It can only be called within a `fakeAsync` body. + + Calling `tick()` simulates the passage of time until all pending asynchronous activities complete, + including the resolution of the `getQuote` promise in this test case. + + It returns nothing. There is no promise to wait for. + Proceed with the same test code as formerly appeared within the `whenStable.then()` callback. + + Even this simple example is easier to read than the third test. + To more fully appreciate the improvement, imagine a succession of asynchronous operations, + chained in a long sequence of promise callbacks. + +a#jasmine-done +:marked + ## _jasmine.done_ + + While `fakeAsync` and even `async` function greatly simplify Angular asynchronous testing, + you can still fallback to the traditional Jasmine asynchronous testing technique. + + You can still pass `it` a function that takes a + [`done` callback](http://jasmine.github.io/2.0/introduction.html#section-Asynchronous_Support). + Now you are responsible for chaining promises, handling errors, and calling `done` at the appropriate moment. + + Here is a `done` version of the previous two tests: ++makeExample('testing/ts/app/shared/twain.component.spec.ts', 'done-test', 'app/shared/twain.component.spec.ts (done test)')(format='.') +:marked + Although we have no direct access to the `getQuote` promise inside `TwainComponent`, + the spy does and that makes it possible to wait for `getQuote` to finish. + + The `jasmine.done` technique, while discouraged, may become necessary when neither `async` nor `fakeAsync` + can tolerate a particular asynchronous activity. That's rare but it happens. + +a(href="#top").to-top Back to top + +.l-hr + +a#component-with-external-template +:marked + # Test a component with an external template + The `TestBed.createComponent` is a synchronous method. + It assumes that everything it could need is already in memory. + + That has been true so far. + Each tested component's `@Component` metadata has a `template` property specifying an _inline templates_. + Neither component had a `styleUrls` property. + Everything necessary to compile them was in memory at test runtime. + + The `DashboardHeroComponent` is different. + It has an external template and external css file, specified in `templateUrl` and `styleUrls` properties. ++makeExample('testing/ts/app/dashboard/dashboard-hero.component.ts', 'component', 'app/dashboard/dashboard-hero.component.ts (component)')(format='.') +:marked + The compiler must read these files from a file system before it can create a component instance. + + The `TestBed.compileComponents` method asynchronously compiles all the components configured in its + current test module. After it completes, external templates and css files, have been "inlined" + and `TestBed.createComponent` can do its job synchronously. +.l-sub-section + :marked + WebPack developers need not call `compileComponents` because it inlines templates and css + as part of the automated build process that precedes running the test. +:marked + The `app/dashboard/dashboard-hero.component.spec.ts` demonstrates the pre-compilation process: ++makeExample('testing/ts/app/dashboard/dashboard-hero.component.spec.ts', 'compile-components', 'app/dashboard/dashboard-hero.component.spec.ts (compileComponents)')(format='.') + +a#async-fn-in-before-each +:marked + ## The _async_ function in _beforeEach_ + + Notice the `async` call in the `beforeEach`. + + The `async` function is part of the _Angular TestBed_ feature set. + It _takes_ a parameterless function and _returns_ a parameterless function + which becomes the argument to the Jasmine `beforeEach` call. + + The body of the `async` argument looks much like the body of a normal `beforEach` argument. + There is nothing obviously asynchronous about it. For example, it doesn't return a promise. + + The `async` function arranges for the tester's code to run in a special _async test zone_ + that hides the mechanics of asynchronous execution. + +a#compile-components +:marked + ## _compileComponents_ + In this example, `Testbed.compileComponents` compiles one component, the `DashboardComponent`. + It's the only declared component in this test module. + + Tests later in this chapter have more declared components and some of them import application + modules that declare yet more components. + Some or all of these components could have external templates and css files. + `TestBed.compileComponents` compiles them all asynchonously at one time. + + The `compileComponents` method returns a promise so you can perform additional tasks _after_ it finishes. + + ### _compileComponents_ closes configuration + After `compileComponents` runs, the current `TestBed` instance is closed to further configuration. + You cannot call any more `TestBed` configuration methods, not `configureTestModule` + nor any of the `override...` methods. The `TestBed` throws an error if you try. + +.alert.is-important + :marked + Do not configure the `TestBed` after calling `compileComponents`. + Make `compileComponents` the last step + before calling `TestBed.createInstance` to instantiate the test component. +:marked + The `DashboardHeroComponent` spec follows the asynchonous `beforeEach` with a + _synchronous_ `beforeEach` that completes the setup steps and runs tests ... as described in the next section. + +.l-hr + +a#component-with-inputs-outputs +:marked + # Test a component with inputs and outputs + A component with inputs and outputs typically appears inside the view template of a host component. + The host uses a property binding to set the input property and uses an event binding to + listen to events raised by the output property. + + The testing goal is to verify that such bindings work as expected. + The tests should set input values and listen for output events. + + The `DashboardHeroComponent` is tiny example of a component in this role. + It displays an individual heroe provided by the `DashboardComponent`. + Clicking that hero tells the the `DashboardComponent` that the user has selected the hero. + + The `DashboardHeroComponent` is embedded in the `DashboardComponent` template like this: ++makeExample('testing/ts/app/dashboard/dashboard.component.html', 'dashboard-hero', 'app/dashboard/dashboard.component.html (excerpt)')(format='.') +:marked + The `DashboardHeroComponent` appears in an `*ngFor` repeater which sets each component's `hero` input property + to the iteration value and listens for the components `selected` event. + + Here's the component's definition again: ++makeExample('testing/ts/app/dashboard/dashboard-hero.component.ts', 'component', 'app/dashboard/dashboard-hero.component.ts (component)')(format='.') +:marked + While testing a component this simple has little intrinsic value, it's worth knowing how. + Three approaches come to mind: + 1. Test it as used by `DashboardComponent` + 1. Test it as a stand-alone component + 1. Test it as used by a substitute for `DashboardComponent` + + A quick look at the `DashboardComponent` constructor discourages the first approach: ++makeExample('testing/ts/app/dashboard/dashboard.component.ts', 'ctor', 'app/dashboard/dashboard.component.ts (constructor)')(format='.') +:marked + The `DashboardComponent` depends upon the Angular router and the `HeroService`. + You'd probably have to fake them both and that's a lot of work. The router is particularly challenging (see below). + + The immediate goal is to test the `DashboardHeroComponent`, not the `DashboardComponent`, and there's no need + to work hard unnecessarily. Let's try the second and third options. + + ## Test _DashboardHeroComponent_ stand-alone + + Here's the spec file setup. ++makeExample('testing/ts/app/dashboard/dashboard-hero.component.spec.ts', 'setup', 'app/dashboard/dashboard-hero.component.spec.ts (setup)')(format='.') + +:marked + The async `beforeEach` was discussed [above](#component-with-external-template). + Having compiled the components asynchronously with `compileComponents`, the rest of the setup + proceeds _synchronously_ in a _second_ `beforeEach`, using the basic techniques described [earlier](#simple-component-test). + + Note how the setup code assigns a test hero (`expectedHero`) to the component's `hero` property, emulating + the way the `DashboardComponent` would set it via the property binding in its repeater. + + The first test follows: ++makeExample('testing/ts/app/dashboard/dashboard-hero.component.spec.ts', 'name-test', 'app/dashboard/dashboard-hero.component.spec.ts (name test)')(format='.') +:marked + It verifies that the hero name is propagated through to template with a binding. + There's a twist. The template passes the hero name through the Angular `UpperCasePipe` so the + test must match the element value with the uppercased name: ++makeExample('testing/ts/app/dashboard/dashboard-hero.component.html')(format='.') +:marked +.alert.is-helpful + :marked + This small test demonstrates how Angular tests can verify a component's visual representation + — something not possible with [isolated unit tests](#isolated-component-tests) — + at low cost and without resorting to much slower and more complicated end-to-end tests. + +:marked + The second test verifies click behavior. Clicking the hero should rais a `selected` event that the + host component (`DashboardComponent` presumably) can hear: ++makeExample('testing/ts/app/dashboard/dashboard-hero.component.spec.ts', 'click-test', 'app/dashboard/dashboard-hero.component.spec.ts (click test)')(format='.') +:marked + The component exposes an `EventEmitter` property. The test subscribes to it just as the host component would do. + + The Angular `DebugElement.triggerEventHandler` lets the test raise _any data-bound event_. + In this example, the component's template binds to the hero `
`. + + The test has a reference to that `
` in `heroEl` so triggering the `heroEl` click event should cause Angular + to call `DashboardHeroComponent.click`. + + If the component behaves as expected, its `selected` property should emit the `hero` object, + the test detects that emission through its subscription, and the test will pass. + +.l-hr + +a#component-inside-test-host +:marked + # Test a component inside a test host component + + In the previous approach the tests themselves played the role of the host `DashboardComponent`. + A nagging suspicion remains. + Will the `DashboardHeroComponent` work properly when properly data-bound to a host component? + + Testing with the actual `DashboardComponent` host is doable but seems more trouble than its worth. + It's easier to emulate the `DashboardComponent` host with a _test host_ like this one: ++makeExample('testing/ts/app/dashboard/dashboard-hero.component.spec.ts', 'test-host', 'app/dashboard/dashboard-hero.component.spec.ts (test host)')(format='.') +:marked + The test host binds to `DashboardHeroComponent` as the `DashboardComponent` would but without + the distraction of the `Router`, the `HeroService` or even the `*ngFor` repeater. + + The test host sets the component's `hero` input property with its test hero. + It binds the component's `selected` event with its `onSelected` handler that records the emitted hero + in its `selectedHero` property. Later the tests check that property to verify that the + `DashboardHeroComponent.selected` event really did emit the right hero. + + The setup for the test-host tests is similar to the setup for the stand-alone tests: ++makeExample('testing/ts/app/dashboard/dashboard-hero.component.spec.ts', 'test-host-setup', 'app/dashboard/dashboard-hero.component.spec.ts (test host setup)')(format='.') +:marked + This test module configuration shows two important differences: + 1. It _declares_ both the `DashboardHeroComponent` and the `TestHostComponent`. + 1. It _creates_ the `TestHostComponent` instead of the `DashboardHeroComponent`. + + The `fixture` returned by `createComponent` holds an instance of `TestHostComponent` instead of an instance of `DashboardHeroComponent`. + + Of course creating the `TestHostComponent` has the side-effect of creating a `DashboardHeroComponent` + because the latter appears within the template of the former. + The query for the hero element (`heroEl`) still finds it in the test DOM + albeit at greater depth in the element tree than before. + + The tests themselves are almost identical to the stand-alone version ++makeExample('testing/ts/app/dashboard/dashboard-hero.component.spec.ts', 'test-host-tests', 'app/dashboard/dashboard-hero.component.spec.ts (test-host)')(format='.') +:marked + Only the selected event test differs. It confirms that the selected `DashboardHeroComponent` hero + really does find its way up through the event binding to the host component. + +a(href="#top").to-top Back to top + +.l-hr + +a#routed-component +:marked + # Test a routed component + + Testing the actual `DashboardComponent` seemed daunting because it injects the `Router`. ++makeExample('testing/ts/app/dashboard/dashboard.component.ts', 'ctor', 'app/dashboard/dashboard.component.ts (constructor)')(format='.') +:marked + It also injects the `HeroService` but faking that is a [familiar story](#component-with-async-servic). + The `Router` has a complicated API and is entwined with other services and application pre-conditions. + + Fortunately, the `DashboardComponent` isn't doing much with the `Router` ++makeExample('testing/ts/app/dashboard/dashboard.component.ts', 'goto-detail', 'app/dashboard/dashboard.component.ts (goToDetail)')(format='.') +:marked + This is often the case. + As a rule you test the component, not the router, + and care only if the component navigates with the right address under the given conditions. + Faking the router is an easy option. This should do the trick: ++makeExample('testing/ts/app/dashboard/dashboard.component.spec.ts', 'fake-router', 'app/dashboard/dashboard.component.spec.ts (fakeRouter)')(format='.') +:marked + Now we setup the test module with the `fakeRouter` and a fake `HeroService` and + create a test instance of the `DashbaordComponent` for subsequent testing. ++makeExample('testing/ts/app/dashboard/dashboard.component.spec.ts', 'compile-and-create-body', 'app/dashboard/dashboard.component.spec.ts (compile and create)')(format='.') +:marked + The following test clicks the displayed hero and confirms (with the help of a spy) that `Router.navigateByUrl` is called with the expected url. ++makeExample('testing/ts/app/dashboard/dashboard.component.spec.ts', 'navigate-test', 'app/dashboard/dashboard.component.spec.ts (navigate test)')(format='.') + +a#inject +:marked + ## The _inject_ function + + Notice the `inject` function in the second `it` argument. ++makeExample('testing/ts/app/dashboard/dashboard.component.spec.ts', 'inject')(format='.') +:marked + The `inject` function is part of the _Angular TestBed_ feature set. + It injects services into the test function where you can alter, spy on, and manipulate them. + + The `inject` function has two parameters + 1. an array of Angular dependency injection tokens + 1. a test function whose parameters correspond exactly to each item in the injection token array + +.callout.is-important + header inject uses the TestBed Injector + :marked + The `inject` function uses the current `TestBed` injector and can only return services provided at that level. + It does not return services from component providers. + +:marked + This example injects the `Router` from the current `TestBed` injector. + That's fine for this test because the `Router` is (and must be) provided by the application root injector. + + If you need a service provided by the component's _own_ injector, call `fixture.debugElement.injector.get` instead: ++makeExample('testing/ts/app/welcome.component.spec.ts', 'injected-service', 'Component\'s injector')(format='.') +.alert.is-important + :marked + Use the component's own injector to get the service actually injected into the component. + +:marked + The `inject` function closes the current `TestBed` instance to further configuration. + You cannot call any more `TestBed` configuration methods, not `configureTestModule` + nor any of the `override...` methods. The `TestBed` throws an error if you try. + +.alert.is-important + :marked + Do not configure the `TestBed` after calling `inject`. +a(href="#top").to-top Back to top + +.l-hr + +a#isolated-tests +a#testing-without-atp +:marked + # Testing without the Angular Testing Platform + + Testing applications with the help of the Angular Testing Platform (ATP) is the main focus of this chapter. + + However, it's often more productive to explore the inner logic of application classes + with _isolated_ unit tests that don't use the ATP. + Such tests are often smaller, easier to read, + and easier to write and maintain. + + They don't + * import from the Angular test libraries + * configure a module + * prepare dependency injection `providers` + * call `inject` or `async` or `fakeAsync` + + They do + * exhibit standard, Angular-agnostic testing techniques + * create instances directly with `new` + * use stubs, spys, and mocks to fake dependencies. + +.callout.is-important + header Write both kinds of tests + :marked + Good developers write both kinds of tests for the same application part, often in the same spec file. + Write simple _isolated_ unit tests to validate the part in isolation. + Write _Angular_ tests to validate the part as it interacts with Angular, + updates the DOM, and collaborates with the rest of the application. + +:marked + ## Services + Services are good candidates for vanilla unit testing. + Here are some synchronous and asynchronous unit tests of the `FancyService` + written without assistance from Angular Testing Platform. + ++makeExample('testing/ts/app/bag/bag.no-testbed.spec.ts', 'FancyService', 'app/bag/bag.no-testbed.spec.ts') +:marked + A rough line count suggests that these tests are about 25% smaller than equivalent ATP tests. + That's telling but not decisive. + The benefit comes from reduced setup and code complexity. + + Compare these equivalent tests of `FancyService.getTimeoutValue`. ++makeTabs( + `testing/ts/app/bag/bag.no-testbed.spec.ts, testing/ts/app/bag/bag.spec.ts`, + 'getTimeoutValue, getTimeoutValue', + `app/bag/bag.no-testbed.spec.ts, app/bag/bag.spec.ts (with ATP)`) +:marked + They have about the same line-count. + The ATP version has more moving parts, including a couple of helper functions (`async` and `inject`). + Both work and it's not much of an issue if you're using the Angular Testing Platform nearby for other reasons. + On the other hand, why burden simple service tests with ATP complexity? + + Pick the approach that suits you. + + ### Services with dependencies + + Services often depend on other services that Angular injects into the constructor. + You can test these services _without_ the testbed. + In many cases, it's easier to create and _inject_ dependencies by hand. + + The `DependentService` is a simple example ++makeExample('testing/ts/app/bag/bag.ts', 'DependentService', 'app/bag/bag.ts')(format='.') +:marked + It delegates it's only method, `getValue`, to the injected `FancyService`. + + Here are several ways to test it. ++makeExample('testing/ts/app/bag/bag.no-testbed.spec.ts', 'DependentService', 'app/bag/bag.no-testbed.spec.ts') +:marked + The first test creates a `FancyService` with `new` and passes it to the `DependentService` constructor. + + It's rarely that simple. The injected service can be difficult to create or control. + You can mock the dependency, or use a fake value, or stub the pertinent service method + with a substitute method that is easy to control. + + These _isolated_ unit testing techniques are great for exploring the inner logic of a service or its + simple integration with a component class. + Use the Angular Testing Platform when writing tests that validate how a service interacts with components + _within the Angular runtime environment_. + + ## Pipes + Pipes are easy to test without the Angular Testing Platform (ATP). + + A pipe class has one method, `transform`, that turns an input to an output. + The `transform` implementation rarely interacts with the DOM. + Most pipes have no dependence on Angular other than the `@Pipe` + metadata and an interface. + + Consider a `TitleCasePipe` that capitalizes the first letter of each word. + Here's a naive implementation implemented with a regular expression. ++makeExample('testing/ts/app/shared/title-case.pipe.ts', '', 'app/shared/title-case.pipe.ts')(format='.') +:marked + Anything that uses a regular expression is worth testing thoroughly. + Use simple Jasmine to explore the expected cases and the edge cases. ++makeExample('testing/ts/app/shared/title-case.pipe.spec.ts', 'excerpt', 'app/shared/title-case.pipe.spec.ts') +:marked + ## Write ATP tests too + These are tests of the pipe _in isolation_. + They can't tell if the `TitleCasePipe` is working properly + as applied in the application components. + + Consider adding ATP component tests such as this one. ++makeExample('testing/ts/app/hero/hero-detail.component.spec.ts', 'title-case-pipe', 'app/hero/hero-detail.component.spec.ts (pipe test)') + +a#isolated-component-tests +:marked + ## Components + + Component tests typically examine how a component class interacts with its own template or with collaborating components. + The Angular Testing Platform is specifically designed to facilitate such tests. + + Consider this `ButtonComp` component. ++makeExample('testing/ts/app/bag/bag.ts', 'ButtonComp', 'app/bag/bag.ts (ButtonComp)')(format='.') +:marked + The following ATP test demonstrates that clicking a button in the template leads + to an update of the on-screen message. ++makeExample('testing/ts/app/bag/bag.spec.ts', 'ButtonComp', 'app/bag/bag.spec.ts (ButtonComp)')(format='.') +:marked + The assertions verify the data binding flow from one HTML control (the `