docs(testing): testing chapter and samples for RC6 (#2198)

[WIP] docs(testing): new chapter, new samples
This commit is contained in:
Ward Bell 2016-09-13 14:39:39 -07:00 committed by GitHub
parent c5ad38ee86
commit 07cfce795f
143 changed files with 6286 additions and 4869 deletions

View File

@ -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() {

View File

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

View File

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

View File

@ -0,0 +1,40 @@
<!-- Run application specs in a browser -->
<!-- #docregion -->
<!DOCTYPE html>
<html>
<head>
<base href="/">
<title>1st Specs</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
<link rel="stylesheet" href="node_modules/jasmine-core/lib/jasmine-core/jasmine.css">
</head>
<body>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="node_modules/jasmine-core/lib/jasmine-core/jasmine.js"></script>
<script src="node_modules/jasmine-core/lib/jasmine-core/jasmine-html.js"></script>
<script src="node_modules/jasmine-core/lib/jasmine-core/boot.js"></script>
<script src="node_modules/reflect-metadata/Reflect.js"></script>
<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="node_modules/zone.js/dist/long-stack-trace-zone.js"></script>
<script src="node_modules/zone.js/dist/proxy.js"></script>
<script src="node_modules/zone.js/dist/sync-test.js"></script>
<script src="node_modules/zone.js/dist/jasmine-patch.js"></script>
<script src="node_modules/zone.js/dist/async-test.js"></script>
<script src="node_modules/zone.js/dist/fake-async-test.js"></script>
<!-- #docregion files -->
<script>
var __spec_files__ = [
'app/1st.spec'
];
</script>
<!-- #enddocregion files-->
<script src="browser-test-shim.js"></script>
</body>
</html>

View File

@ -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"]
}

View File

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

View File

@ -0,0 +1,52 @@
<!-- Run application specs in a browser -->
<!-- #docregion -->
<!DOCTYPE html>
<html>
<head>
<base href="/">
<title>Sample App Specs</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
<link rel="stylesheet" href="node_modules/jasmine-core/lib/jasmine-core/jasmine.css">
</head>
<body>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="node_modules/jasmine-core/lib/jasmine-core/jasmine.js"></script>
<script src="node_modules/jasmine-core/lib/jasmine-core/jasmine-html.js"></script>
<script src="node_modules/jasmine-core/lib/jasmine-core/boot.js"></script>
<script src="node_modules/reflect-metadata/Reflect.js"></script>
<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="node_modules/zone.js/dist/long-stack-trace-zone.js"></script>
<script src="node_modules/zone.js/dist/proxy.js"></script>
<script src="node_modules/zone.js/dist/sync-test.js"></script>
<script src="node_modules/zone.js/dist/jasmine-patch.js"></script>
<script src="node_modules/zone.js/dist/async-test.js"></script>
<script src="node_modules/zone.js/dist/fake-async-test.js"></script>
<script>
var __spec_files__ = [
'app/app.component.spec',
'app/app.component.router.spec',
'app/banner.component.spec',
'app/dashboard/dashboard.component.spec',
'app/dashboard/dashboard.component.no-testbed.spec',
'app/dashboard/dashboard-hero.component.spec',
'app/hero/hero-list.component.spec',
'app/hero/hero-detail.component.spec',
'app/hero/hero-detail.component.no-testbed.spec',
'app/model/hero.spec',
'app/model/http-hero.service.spec',
'app/shared/title-case.pipe.spec',
'app/twain.component.spec',
'app/welcome.component.spec'
];
</script>
<script src="browser-test-shim.js"></script>
</body>
</html>

View File

@ -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"]
}

View File

@ -0,0 +1,5 @@
// #docplaster
// #docregion
describe('1st tests', () => {
it('true is true', () => expect(true).toBe(true));
});

View File

@ -0,0 +1,12 @@
// #docregion
import { Component } from '@angular/core';
@Component({
template: `
<h2 highlight="skyblue">About</h2>
<twain-quote></twain-quote>
<p>All about this sample</p>
`,
styleUrls: ['app/shared/styles.css']
})
export class AboutComponent { }

View File

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

View File

@ -0,0 +1,10 @@
<app-banner></app-banner>
<app-welcome></app-welcome>
<nav>
<a routerLink="/dashboard">Dashboard</a>
<a routerLink="/heroes">Heroes</a>
<a routerLink="/about">About</a>
</nav>
<router-outlet></router-outlet>

View File

@ -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<AppComponent>;
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<AppComponent>;
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((<any>events[i].constructor).name).toBe(pairs[i][0].name, 'unexpected event name');
expect((<any>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>): any {
const el = fixture.debugElement.query(By.directive(type));
expect(el).toBeTruthy('expected an element for ' + type.name);
return el;
}

View File

@ -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<AppComponent>;
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<AppComponent>;
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 => <MockRouterLink> 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 = <MockRouterLink> 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();
});

View File

@ -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: `
<h1>{{title}}</h1>
<nav>
<a [routerLink]="['Dashboard']">Dashboard</a>
<a [routerLink]="['Heroes']">Heroes</a>
</nav>
<router-outlet></router-outlet>
<hr>
<h1>Bag-a-specs</h1>
<my-if-parent-comp></my-if-parent-comp>
<h3>External Template Comp</h3>
<external-template-comp></external-template-comp>
<h3>Comp With External Template Comp</h3>
<comp-w-ext-comp></comp-w-ext-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 { }

View File

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

View File

@ -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(); };
(<any>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(); };
(<any>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();
});
});
});
});

View File

@ -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 = <ButtonComp> 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 = <InputComp> fixture.componentInstance;
expect(comp.name).toEqual(origName, `At start name should be ${origName} `);
let inputBox = <HTMLInputElement> 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, '<span>Mock</span>')
.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: '<span>Modified {{childBinding}}</span>'})
)
.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<MyIfParentComp>;
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);
})));
});

View File

@ -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: `<button (click)='clicked()'>Click me!</button>`
})
export class ButtonComp {
wasClicked = false;
clicked() { this.wasClicked = true; }
}
@Component({
selector: 'input-comp',
template: `<input [(ngModel)]="name">`
})
export class InputComp {
name = 'John';
}
@Component({
selector: 'child-comp',
template: `<span>Original {{childBinding}}</span>`
})
export class ChildComp {
childBinding = 'Child';
}
@Component({
selector: 'child-comp',
template: `<span>Mock</span>`
})
export class MockChildComp { }
@Component({
selector: 'parent-comp',
template: `Parent(<child-comp></child-comp>)`,
directives: [ChildComp]
})
export class ParentComp { }
@Component({
selector: 'my-if-comp',
template: `MyIf(<span *ngIf="showMore">More</span>)`
})
export class MyIfComp {
showMore = false;
}
@Component({
selector: 'child-child-comp',
template: '<span>ChildChild</span>'
})
export class ChildChildComp { }
@Component({
selector: 'child-comp',
template: `<span>Original {{childBinding}}(<child-child-comp></child-child-comp>)</span>`,
directives: [ChildChildComp]
})
export class ChildWithChildComp {
childBinding = 'Child';
}
@Component({
selector: 'child-child-comp',
template: `<span>ChildChild Mock</span>`
})
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: `
<h3>comp-w-ext-comp</h3>
<external-template-comp></external-template-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: `
<h4>MyIfChildComp</h4>
<div>
<label>Child value: <input [(ngModel)]="childValue"> </label>
</div>
<p><i>Change log:</i></p>
<div *ngFor="let log of changeLog; let i=index">{{i + 1}} - {{log}}</div>`
})
export class MyIfChildComp implements OnInit, OnChanges, OnDestroy {
@Input() value = '';
@Output() valueChange = new EventEmitter<string>();
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: `
<h3>MyIfParentComp</h3>
<label>Parent value:
<input [(ngModel)]="parentValue">
</label>
<button (click)='clicked()'>{{toggleLabel}} Child</button><br>
<div *ngIf="showChild"
style="margin: 4px; padding: 4px; background-color: aliceblue;">
<my-if-child-comp [(value)]="parentValue"></my-if-child-comp>
</div>
`,
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
];

View File

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

View File

@ -0,0 +1,5 @@
// main app entry point
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { BagModule } from './bag';
platformBrowserDynamic().bootstrapModule(BagModule);

View File

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

View File

@ -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 = <HTMLInputElement> 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 = <HTMLInputElement> 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: '<span>Fake</span>' }
})
.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: '<my-service-comp></my-service-comp>' })
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: `
<needs-content #nc>
<child-1 #content text="My"></child-1>
<child-2 #content text="dog"></child-2>
<child-2 text="has"></child-2>
<child-3 #content text="fleas"></child-3>
<div #content>!</div>
</needs-content>
`
}
})
.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<MyIfParentComponent>;
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(<grandchild-1></grandchild-1>)`
})
class FakeChildWithGrandchildComponent { }
@Component({
selector: 'grandchild-1',
template: `Fake Grandchild`
})
class FakeGrandchildComponent { }
@Injectable()
class FakeFancyService extends FancyService {
value: string = 'faked value';
}

View File

@ -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: `
<bank-account
bank="RBC"
account="4747"
[style.width.px]="width"
[style.color]="color"
[class.closed]="isClosed"
[class.open]="!isClosed">
</bank-account>
`
})
export class BankAccountParentComponent {
width = 200;
color = 'red';
isClosed = true;
}
// #docregion ButtonComp
@Component({
selector: 'button-comp',
template: `
<button (click)="clicked()">Click me!</button>
<span>{{message}}</span>`
})
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: `<span>Child-1({{text}})</span>`
})
export class Child1Component {
@Input() text = 'Original';
}
@Component({
selector: 'child-2',
template: '<div>Child-2({{text}})</div>'
})
export class Child2Component {
@Input() text: string;
}
@Component({
selector: 'child-3',
template: '<div>Child-3({{text}})</div>'
})
export class Child3Component {
@Input() text: string;
}
@Component({
selector: 'input-comp',
template: `<input [(ngModel)]="name">`
})
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<any> = new EventEmitter();
// }
// As the style-guide recommends
@Directive({ selector: 'input[value]' })
export class InputValueBinderDirective {
@HostBinding()
@Input()
value: any;
@Output()
valueChange: EventEmitter<any> = new EventEmitter();
@HostListener('input', ['$event.target.value'])
onInput(value: any) { this.valueChange.next(value); }
}
@Component({
selector: 'input-value-comp',
template: `
Name: <input [(value)]="name"> {{name}}
`
})
export class InputValueBinderComponent {
name = 'Sally'; // initial value
}
@Component({
selector: 'parent-comp',
template: `Parent(<child-1></child-1>)`
})
export class ParentComponent { }
@Component({
selector: 'io-comp',
template: `<div class="hero" (click)="click()">Original {{hero.name}}</div>`
})
export class IoComponent {
@Input() hero: Hero;
@Output() selected = new EventEmitter<Hero>();
click() { this.selected.emit(this.hero); }
}
@Component({
selector: 'io-parent-comp',
template: `
<p *ngIf="!selectedHero"><i>Click to select a hero</i></p>
<p *ngIf="selectedHero">The selected hero is {{selectedHero.name}}</p>
<io-comp
*ngFor="let hero of heroes"
[hero]=hero
(selected)="onSelect($event)">
</io-comp>
`
})
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(<span *ngIf="showMore">More</span>)`
})
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: `
<h3>comp-w-ext-comp</h3>
<external-template-comp></external-template-comp>
`
})
export class InnerCompWithExternalTemplateComponent { }
@Component({
selector: 'bad-template-comp',
templateUrl: 'non-existant.html'
})
export class BadTemplateUrlComponent { }
@Component({selector: 'needs-content', template: '<ng-content></ng-content>'})
export class NeedsContentComponent {
// children with #content local variable
@ContentChildren('content') children: any;
}
///////// MyIfChildComp ////////
@Component({
selector: 'my-if-child-1',
template: `
<h4>MyIfChildComp</h4>
<div>
<label>Child value: <input [(ngModel)]="childValue"> </label>
</div>
<p><i>Change log:</i></p>
<div *ngFor="let log of changeLog; let i=index">{{i + 1}} - {{log}}</div>`
})
export class MyIfChildComponent implements OnInit, OnChanges, OnDestroy {
@Input() value = '';
@Output() valueChange = new EventEmitter<string>();
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: `
<h3>MyIfParentComp</h3>
<label>Parent value:
<input [(ngModel)]="parentValue">
</label>
<button (click)='clicked()'>{{toggleLabel}} Child</button><br>
<div *ngIf="showChild"
style="margin: 4px; padding: 4px; background-color: aliceblue;">
<my-if-child-1 [(value)]="parentValue"></my-if-child-1>
</div>
`
})
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: `
<input [(ngModel)]="text">
<span>{{text | reverse}}</span>
`
})
export class ReversePipeComponent {
text = 'my dog has fleas.';
}
@Component({template: '<div>Replace Me</div>'})
export class ShellComponent { }
@Component({
selector: 'bag-comp',
template: `
<h1>Specs Bag</h1>
<my-if-parent-comp></my-if-parent-comp>
<hr>
<h3>Input/Output Component</h3>
<io-parent-comp></io-parent-comp>
<hr>
<h3>External Template Component</h3>
<external-template-comp></external-template-comp>
<hr>
<h3>Component With External Template Component</h3>
<comp-w-ext-comp></comp-w-ext-comp>
<hr>
<h3>Reverse Pipe</h3>
<reverse-pipe-comp></reverse-pipe-comp>
<hr>
<h3>InputValueBinder Directive</h3>
<input-value-comp></input-value-comp>
<hr>
<h3>Button Component</h3>
<button-comp></button-comp>
<hr>
<h3>Needs Content</h3>
<needs-content #nc>
<child-1 #content text="My"></child-1>
<child-2 #content text="dog"></child-2>
<child-2 text="has"></child-2>
<child-3 #content text="fleas"></child-3>
<div #content>!</div>
</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 { }

View File

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

View File

@ -0,0 +1,11 @@
// #docregion
import { Component } from '@angular/core';
@Component({
selector: 'app-banner',
template: '<h1>{{title}}</h1>'
})
export class BannerComponent {
title = 'Test Tour of Heroes';
}

View File

@ -1,11 +0,0 @@
<!-- #docregion -->
<h3>Top Heroes</h3>
<div class="grid grid-pad">
<!-- #docregion click -->
<div *ngFor="let hero of heroes" (click)="gotoDetail(hero)" class="col-1-4">
<!-- #enddocregion click -->
<div class="module hero">
<h4>{{hero.name}}</h4>
</div>
</div>
</div>

View File

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

View File

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

View File

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

View File

@ -0,0 +1,4 @@
<!-- #docregion -->
<div (click)="click()" class="hero">
{{hero.name | uppercase}}
</div>

View File

@ -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<DashboardHeroComponent>;
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<TestHostComponent>;
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: `
<dashboard-hero [hero]="hero" (selected)="onSelected($event)">
</dashboard-hero>`
})
class TestHostComponent {
hero = new Hero(42, 'Test Name');
selectedHero: Hero;
onSelected(hero: Hero) { this.selectedHero = hero; }
}
// #enddocregion test-host

View File

@ -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<Hero>();
click() { this.selected.next(this.hero); }
}
// #enddocregion component

View File

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

View File

@ -0,0 +1,9 @@
<h2 highlight>{{title}}</h2>
<div class="grid grid-pad">
<!-- #docregion dashboard-hero -->
<dashboard-hero *ngFor="let hero of heroes" class="col-1-4"
[hero]=hero (selected)="gotoDetail($event)" >
</dashboard-hero>
<!-- #enddocregion dashboard-hero -->
</div>

View File

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

View File

@ -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<DashboardComponent>;
//////// Deep ////////////////
describe('DashboardComponent (deep)', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ DashboardModule ]
});
});
compileAndCreate();
tests(clickForDeep);
function clickForDeep() {
// get first <div class="hero"> 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 <dashboard-hero> 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 <div class="hero">
// 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
});
}

View File

@ -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`;
}
}

View File

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

View File

@ -1,14 +0,0 @@
<!-- #docplaster -->
<!-- #docregion -->
<div *ngIf="hero">
<h2>{{hero.name}} details!</h2>
<div>
<label>id: </label>{{hero.id}}</div>
<div>
<label>name: </label>
<input [(ngModel)]="hero.name" placeholder="name" />
</div>
<!-- #docregion back-button -->
<button (click)="goBack()">Back</button>
<!-- #enddocregion back-button -->
</div>

View File

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

View File

@ -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<Hero[]>(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

View File

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

View File

@ -1,5 +0,0 @@
// #docregion
export class Hero {
id: number;
name: string;
}

View File

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

View File

@ -0,0 +1,11 @@
<div *ngIf="hero">
<h2><span>{{hero.name | titlecase}}</span> Details</h2>
<div>
<label>id: </label>{{hero.id}}</div>
<div>
<label>name: </label>
<input [(ngModel)]="hero.name" placeholder="name" />
</div>
<button (click)="save()">Save</button>
<button (click)="cancel()">Cancel</button>
</div>

View File

@ -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, <any> 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();
});
});
});

View File

@ -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<HeroDetailComponent>;
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;
}
}
}

View File

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

View File

@ -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<Hero> {
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);
}
}

View File

@ -0,0 +1,8 @@
<h2 highlight="gold">My Heroes</h2>
<ul class="heroes">
<li *ngFor="let hero of heroes | async "
[class.selected]="hero === selectedHero"
(click)="onSelect(hero)">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</li>
</ul>

View File

@ -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<HeroListComponent>;
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();
};
}

View File

@ -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<Hero[]>;
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 ]);
}
}

View File

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

View File

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

View File

@ -1,21 +0,0 @@
<!-- #docplaster -->
<!-- #docregion -->
<h2>My Heroes</h2>
<ul class="heroes">
<li *ngFor="let hero of heroes"
[class.selected]="hero === selectedHero"
(click)="onSelect(hero)">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</li>
</ul>
<!-- #docregion mini-detail -->
<div *ngIf="selectedHero">
<h2>
<!-- #docregion pipe -->
{{selectedHero.name | uppercase}} is my hero
<!-- #enddocregion pipe -->
</h2>
<button (click)="gotoDetail()">View Details</button>
</div>
<!-- #enddocregion mini-detail -->
<!-- #enddocregion -->

View File

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

View File

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

View File

@ -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<any>; // so we can spy on promise calls
getHero(id: number) {
return this.lastPromise = Promise.resolve(this.mockHeroes[0]);
}
getHeroes() {
return this.lastPromise = Promise.resolve<Hero[]>(this.mockHeroes);
}
getHeroesSlowly() { return this.getHeroes(); }
}

View File

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

View File

@ -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<any> { 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<any> { 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<any> { 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<any> {
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<any> { 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<any> {
NOT_IMPLEMENTED('commit'); return _resolveToTrue;
}
deactivate(instruction: Instruction, _skipLocationChange = false): Promise<any> {
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<any> {
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<any> {
return Promise.resolve(url);
}
/**
* Navigate via the provided instruction. Returns a promise that resolves when navigation is
* complete.
*/
navigateByInstruction(instruction: Instruction, _skipLocationChange = false): Promise<any> {
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<Instruction> {
return Promise.resolve(this.mockRecognizedInstruction);
}
registerPrimaryOutlet(outlet: RouterOutlet): Promise<any> {
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<any> {
// Delegate navigation to the root router
return this.parent.navigateByUrl(url, _skipLocationChange);
}
navigateByInstruction(instruction: Instruction, _skipLocationChange = false):
Promise<any> {
// Delegate navigation to the root router
return this.parent.navigateByInstruction(instruction, _skipLocationChange);
}
}

View File

@ -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<Hero> {
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<Hero> {
return this.getHero(hero.id).then(h => {
return h ?
Object.assign(h, hero) :
Promise.reject(`Hero ${hero.id} not found`) as any as Promise<Hero>;
});
}
}

View File

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

View File

@ -0,0 +1,4 @@
export class Hero {
constructor(public id = 0, public name = '') { }
clone() { return new Hero(this.id, this.name); }
}

View File

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

View File

@ -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<Hero> {
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<Hero> {
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);

View File

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

View File

@ -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')
];

View File

@ -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<any>; // 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<Hero[]>(this.heroes);
}
updateHero(hero: Hero): Promise<Hero> {
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<Hero>;
});
}
}

View File

@ -0,0 +1 @@
export * from './fake-hero.service';

View File

@ -0,0 +1,7 @@
import { Injectable } from '@angular/core';
@Injectable()
export class UserService {
isLoggedIn = true;
user = {name: 'Sam Spade'};
}

View File

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

View File

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

View File

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

View File

@ -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 = '<button (click)="onDelete()">Delete</button>';
return tcb
.overrideTemplate(HeroDetailComponent, template)
.createAsync(HeroDetailComponent)
.then((rootTC: RTC) => {
let hdc: HeroDetailComponent = rootTC.debugElement.componentInstance;
// // USE PROMISE WRAPPING AN OBSERVABLE UNTIL can get `toPromise` working again
// let p = new Promise<Hero>((resolve) => {
// // Listen for the HeroComponent.delete EventEmitter's event with observable
// hdc.delete.toRx().subscribe((hero: Hero) => {
// console.log('Observable heard HeroComponent.delete event raised');
// resolve(hero);
// });
// })
//Listen for the HeroComponent.delete EventEmitter's event with promise
let p = <Promise<Hero>> hdc.delete.toRx().toPromise()
.then((hero:Hero) => {
console.log('Promise heard HeroComponent.delete event raised');
});
// trigger the 'click' event on the HeroDetailComponent delete button
let el = rootTC.debugElement.query(By.css('button'));
el.triggerEventHandler('click', null);
// toPromise() does not fulfill until emitter is completed by `return()`
hdc.delete.return();
return p;
});
}));
it('Update button should modify hero', injectTcb(tcb => {
let template =
`<div>
<button id="update" (click)="onUpdate()" [disabled]="!hero">Update</button>
<input [(ngModel)]="hero.name"/>
</div>`
return tcb
.overrideTemplate(HeroDetailComponent, template)
.createAsync(HeroDetailComponent)
.then((rootTC: RTC) => {
let hdc: HeroDetailComponent = rootTC.debugElement.componentInstance;
hdc.hero = new Hero(42, 'Cat Woman');
let origNameLength = hdc.hero.name.length;
// trigger the 'click' event on the HeroDetailComponent update button
rootTC.debugElement.query(By.css('#update'))
.triggerEventHandler('click', null);
expect(hdc.hero.name.length).toBeGreaterThan(origNameLength);
});
}));
it('Entering hero name in textbox changes hero', injectTcb(tcb => {
let hdc: HeroDetailComponent
let template = `<input [(ngModel)]="hero.name"/>`
return tcb
.overrideTemplate(HeroDetailComponent, template)
.createAsync(HeroDetailComponent)
.then((rootTC: RTC) => {
hdc = rootTC.debugElement.componentInstance;
hdc.hero = new Hero(42, 'Cat Woman');
rootTC.detectChanges();
// get the HTML element and change its value in the DOM
var input = rootTC.debugElement.query(By.css('input')).nativeElement;
input.value = "Dog Man"
dispatchEvent(input, 'change'); // event triggers Ng to update model
rootTC.detectChanges();
// model update hasn't happened yet, despite `detectChanges`
expect(hdc.hero.name).toEqual('Cat Woman');
})
.then(tick) // must wait a tick for the model update
.then(() => {
expect(hdc.hero.name).toEqual('Dog Man');
});
}));
// Simulates ...
// 1. change a hero
// 2. select a different hero
// 3 re-select the first hero
// 4. confirm that the change is preserved in HTML
// Reveals 2-way binding bug in alpha-36, fixed in pull #3715 for alpha-37
it('toggling heroes after modifying name preserves the change on screen', injectTcb(tcb => {
let hdc: HeroDetailComponent;
let hero1 = new Hero(1, 'Cat Woman');
let hero2 = new Hero(2, 'Goat Boy');
let input: HTMLInputElement;
let rootTC: RTC;
let template = `{{hero.id}} - <input [(ngModel)]="hero.name"/>`
return tcb
.overrideTemplate(HeroDetailComponent, template)
.createAsync(HeroDetailComponent)
.then((rtc: RTC) => {
rootTC = rtc;
hdc = rootTC.debugElement.componentInstance;
hdc.hero = hero1; // start with hero1
rootTC.detectChanges();
// get the HTML element and change its value in the DOM
input = rootTC.debugElement.query(By.css('input')).nativeElement;
input.value = "Dog Man"
dispatchEvent(input, 'change'); // event triggers Ng to update model
})
.then(tick) // must wait a tick for the model update
.then(() => {
expect(hdc.hero.name).toEqual('Dog Man');
hdc.hero = hero2 // switch to hero2
rootTC.detectChanges();
hdc.hero = hero1 // switch back to hero1
rootTC.detectChanges();
// model value will be the same changed value (of course)
expect(hdc.hero.name).toEqual('Dog Man');
// the view should reflect the same changed value
expect(input.value).toEqual('Dog Man');
});
}));
});
});

View File

@ -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, '<button (click)="onDelete()" [disabled]="!hero">Delete</button>')
.overrideDirective(TestWrapper, HeroDetailComponent, mockHDC)
.createAsync(TestWrapper)
.then((rootTC:RTC) => {
let hw:TestWrapper = rootTC.componentInstance;
let hdcElement = rootTC.componentViewChildren[0];
let hdc:HeroDetailComponent = hdcElement.componentInstance;
rootTC.detectChanges(); // trigger view binding
// We can watch the HeroComponent.delete EventEmitter's event
let subscription = hdc.delete.toRx().subscribe(() => {
console.log('HeroComponent.delete event raised');
subscription.dispose();
});
// We can EITHER invoke HeroComponent delete button handler OR
// trigger the 'click' event on the delete HeroComponent button
// BUT DON'T DO BOTH
// Trigger event
// FRAGILE because assumes precise knowledge of HeroComponent template
hdcElement
.query(By.css('#delete'))
.triggerEventHandler('click', {});
hw.testCallback = () => {
// if wrapper.onDelete is called, HeroComponent.delete event must have been raised
//console.log('HeroWrapper.onDelete called');
expect(true).toEqual(true);
}
// hc.onDelete();
});
}), 500); // needs some time for event to complete; 100ms is not long enough
it('update button should modify hero', injectTcb( tcb => {
return tcb
.createAsync(TestWrapper)
.then((rootTC:RTC) => {
let hc:HeroDetailComponent = rootTC.componentViewChildren[0].componentInstance;
let hw:TestWrapper = rootTC.componentInstance;
let origNameLength = hw.currentHero.name.length;
rootTC.detectChanges(); // trigger view binding
// We can EITHER invoke HeroComponent update button handler OR
// trigger the 'click' event on the HeroComponent update button
// BUT DON'T DO BOTH
// Trigger event
// FRAGILE because assumes precise knowledge of HeroComponent template
rootTC.componentViewChildren[0]
.componentViewChildren[2]
.triggerEventHandler('click', {});
// hc.onUpdate(); // Invoke button handler
expect(hw.currentHero.name.length).toBeGreaterThan(origNameLength);
});
}));
});
///// Test Components ////////
// TestWrapper is a convenient way to communicate w/ HeroDetailComponent in a test
@Component({selector: 'hero-wrapper'})
@View({
template: `<my-hero-detail [hero]="currentHero" [user-name]="userName" (delete)="onDelete()"></my-hero-detail>`,
directives: [HeroDetailComponent]
})
class TestWrapper {
currentHero = new Hero(42, 'Cat Woman');
userName = 'Sally';
testCallback() {} // monkey-punched in a test
onDelete() { this.testCallback(); }
}
@View({
template: `
<div>
<h2>{{hero.name}} | {{userName}}</h2>
<button id="delete" (click)="onDelete()" [disabled]="!hero">Delete</button>
<button id="update" (click)="onUpdate()" [disabled]="!hero">Update</button>
<div id="id">{{hero.id}}</div>
<input [(ngModel)]="hero.name"/>
</div>`,
directives: [FORM_DIRECTIVES]
})
class mockHDC //extends HeroDetailComponent { }
{
hero: Hero;
delete = new EventEmitter();
onDelete() { this.delete.next(this.hero) }
onUpdate() {
if (this.hero) {
this.hero.name += 'x';
}
}
userName: string;
}

View File

@ -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<Hero[]>(heroData.map(h => h.clone()));
}
var testError = 'BackendService.fetchAllHeroesAsync failed on purpose';
class FailingBackendService {
// return a promise that fails as quickly as possible
fetchAllHeroesAsync = () =>
Promise.reject(testError);
}

View File

@ -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); // dont know how many to expect yet
});
});
// better ... but not async!
it('refresh promise returns expected # of heroes when fulfilled', () => {
heroData = [new Hero(1, 'Foo'), new Hero(2, 'Bar'), new Hero(3, 'Baz')];
let backend = <BackendService>{
// return a promise for fake heroes that resolves as quickly as possible
fetchAllHeroesAsync: () => Promise.resolve<Hero[]>(heroData)
};
let service = new HeroService(backend);
service.refresh().then(heroes => {
expect(heroes.length).toEqual(heroData.length); // is it?
expect(heroes.length).not.toEqual(heroData.length); // or is it not?
console.log('** inside callback **');
});
console.log('** end of test **');
});
// better ... but forgot to call done!
it('refresh promise returns expected # of heroes when fulfilled', done => {
heroData = [new Hero(1, 'Foo'), new Hero(2, 'Bar'), new Hero(3, 'Baz')];
let backend = <BackendService>{
// return a promise for fake heroes that resolves as quickly as possible
fetchAllHeroesAsync: () => Promise.resolve<Hero[]>(heroData)
};
let service = new HeroService(backend);
service.refresh().then(heroes => {
expect(heroes.length).toEqual(heroData.length); // is it?
expect(heroes.length).not.toEqual(heroData.length); // or is it not?
console.log('** inside callback **');
});
console.log('** end of test **');
});
});
describe('HeroService (beginning tests - 3 [async])', () => {
let heroData:Hero[];
// Now it's proper async!
it('refresh promise returns expected # of heroes when fulfilled', done => {
heroData = [new Hero(1, 'Foo'), new Hero(2, 'Bar'), new Hero(3, 'Baz')];
let backend = <BackendService>{
// return a promise for fake heroes that resolves as quickly as possible
fetchAllHeroesAsync: () => Promise.resolve<Hero[]>(heroData)
};
let service = new HeroService(backend);
service.refresh().then(heroes => {
expect(heroes.length).toEqual(heroData.length); // is it?
//expect(heroes.length).not.toEqual(heroData.length); // or is it not?
console.log('** inside callback **');
done();
});
console.log('** end of test **');
});
// Final before catch
it('refresh promise returns expected # of heroes when fulfilled', done => {
heroData = [new Hero(1, 'Foo'), new Hero(2, 'Bar'), new Hero(3, 'Baz')];
let backend = <BackendService>{
// return a promise for fake heroes that resolves as quickly as possible
fetchAllHeroesAsync: () => Promise.resolve<Hero[]>(heroData)
};
let service = new HeroService(backend);
service.refresh().then(heroes => {
expect(heroes.length).toEqual(heroData.length);
})
.then(done);
});
// Final before beforeEach refactoring
it('refresh promise returns expected # of heroes when fulfilled', done => {
heroData = [new Hero(1, 'Foo'), new Hero(2, 'Bar'), new Hero(3, 'Baz')];
let backend = <BackendService>{
// return a promise for fake heroes that resolves as quickly as possible
fetchAllHeroesAsync: () => Promise.resolve<Hero[]>(heroData)
};
let service = new HeroService(backend);
service.refresh().then(heroes => {
expect(heroes.length).toEqual(heroData.length);
})
.then(done, done.fail);
});
it('service.heroes remains empty until fulfilled', () => {
heroData = [new Hero(1, 'Foo'), new Hero(2, 'Bar'), new Hero(3, 'Baz')];
let backend = <BackendService>{
// return a promise for fake heroes that resolves as quickly as possible
fetchAllHeroesAsync: () => Promise.resolve<Hero[]>(heroData)
};
let service = new HeroService(backend);
service.refresh();
// executed before refresh completes
expect(service.heroes.length).toEqual(0);
});
});
describe('HeroService (beginning tests - 4 [beforeEach])', () => {
let heroData:Hero[];
let service:HeroService; // local to describe so tests can see it
// before beforEach refactoring
beforeEach(() => {
heroData = [new Hero(1, 'Foo'), new Hero(2, 'Bar'), new Hero(3,'Baz')];
let backend = <BackendService> {
// return a promise for fake heroes that resolves as quickly as possible
fetchAllHeroesAsync: () => Promise.resolve<Hero[]>(heroData)
};
service = new HeroService(backend);
});
it('refresh promise returns expected # of heroes when fulfilled', done => {
service.refresh().then(heroes =>
expect(heroes.length).toEqual(heroData.length)
)
.then(done, done.fail);
});
it('service.heroes remains empty until fulfilled', () => {
service.refresh();
// executed before refresh completes
expect(service.heroes.length).toEqual(0);
});
});
describe('HeroService (beginning tests - 5 [refactored beforeEach])', () => {
describe('when backend provides data', () => {
beforeEach(() => {
heroData = [new Hero(1, 'Foo'), new Hero(2, 'Bar'), new Hero(3,'Baz')];
service = new HeroService(new HappyBackendService());
});
it('refresh promise returns expected # of heroes when fulfilled', done => {
service.refresh().then(() =>
expect(service.heroes.length).toEqual(heroData.length)
)
.then(done, done.fail);
});
it('service.heroes remains empty until fulfilled', () => {
service.refresh();
// executed before refresh completes
expect(service.heroes.length).toEqual(0);
});
});
});
///////// test helpers /////////
var service: HeroService;
var heroData: Hero[];
class HappyBackendService {
// return a promise for fake heroes that resolves as quickly as possible
fetchAllHeroesAsync = () =>
Promise.resolve<Hero[]>(heroData.map(h => h.clone()));
}

View File

@ -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<Hero[]>(heroData.map(h => h.clone()));
}
var testError = 'BackendService.fetchAllHeroesAsync failed on purpose';
class FailingBackendService {
// return a promise that fails as quickly as possible
// force-cast it to <Promise<Hero[]> because of TS typing bug.
fetchAllHeroesAsync = () =>
<Promise<Hero[]>><any>Promise.reject(testError);
}

View File

@ -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 = () => (<any>service.refresh).calls.mostRecent().returnValue;
describe('HeroesComponent (with Angular)', () => {
beforeEach(() => {
heroData = [new Hero(1, 'Foo'), new Hero(2, 'Bar'), new Hero(3, 'Baz')];
mockUser = new User();
});
// Set up DI bindings required by component (and its nested components?)
// else hangs silently forever
beforeEachProviders(() => [
bind(HeroService).toClass(HappyHeroService),
bind(User).toValue(mockUser)
]);
// test-lib bug? first test fails unless this no-op test runs first
it('ignore this test', () => expect(true).toEqual(true)); // hack
it('can be created and has userName', injectTcb((tcb:TCB) => {
let template = '';
return tcb
.overrideTemplate(HeroesComponent, template)
.createAsync(HeroesComponent)
.then((rootTC: RTC) => {
hc = rootTC.debugElement.componentInstance;
expect(hc).toBeDefined();// proof of life
expect(hc.userName).toEqual(mockUser.name);
});
}));
it('binds view to userName', injectTcb((tcb:TCB) => {
let template = `<h1>{{userName}}'s Heroes</h1>`;
return tcb
.overrideTemplate(HeroesComponent, template)
.createAsync(HeroesComponent)
.then((rootTC: RTC) => {
hc = rootTC.debugElement.componentInstance;
rootTC.detectChanges(); // trigger component property binding
expectSelectedHtml(rootTC, 'h1').toMatch(hc.userName);
expectViewChildHtml(rootTC).toMatch(hc.userName);
});
}));
describe('#onInit', () => {
let template = '';
it('HeroService.refresh not called immediately',
injectTcb([HeroService], (tcb:TCB, heroService:HeroService) => {
return tcb
.overrideTemplate(HeroesComponent, template)
.createAsync(HeroesComponent)
.then(() => {
let spy = <jasmine.Spy><any> heroService.refresh;
expect(spy.calls.count()).toEqual(0);
});
}));
it('onInit calls HeroService.refresh',
injectTcb([HeroService], (tcb:TCB, heroService:HeroService) => {
return tcb
.overrideTemplate(HeroesComponent, template)
.createAsync(HeroesComponent)
.then((rootTC: RTC) => {
hc = rootTC.debugElement.componentInstance;
let spy = <jasmine.Spy><any> heroService.refresh;
hc.ngOnInit(); // Angular framework calls when it creates the component
expect(spy.calls.count()).toEqual(1);
});
}));
it('onInit is called after the test calls detectChanges', injectTcb((tcb:TCB) => {
return tcb
.overrideTemplate(HeroesComponent, template)
.createAsync(HeroesComponent)
.then((rootTC: RTC) => {
hc = rootTC.debugElement.componentInstance;
let spy = spyOn(hc, 'onInit').and.callThrough();
expect(spy.calls.count()).toEqual(0);
rootTC.detectChanges();
expect(spy.calls.count()).toEqual(1);
});
}));
})
describe('#heroes', () => {
// focus on the part of the template that displays heroe names
let template =
'<ul><li *ngFor="#h of heroes">{{h.name}}</li></ul>';
it('binds view to heroes', injectTcb((tcb:TCB) => {
return tcb
.overrideTemplate(HeroesComponent, template)
.createAsync(HeroesComponent)
.then((rootTC: RTC) => {
// trigger {{heroes}} binding
rootTC.detectChanges();
// hc.heroes is still empty; need a JS cycle to get the data
return rootTC;
})
.then((rootTC: RTC) => {
hc = rootTC.debugElement.componentInstance;
// now heroes are available for binding
expect(hc.heroes.length).toEqual(heroData.length);
rootTC.detectChanges(); // trigger component property binding
// confirm hero list is displayed by looking for a known hero
expect(rootTC.debugElement.nativeElement.innerHTML).toMatch(heroData[0].name);
});
}));
// ... add more tests of component behavior affecting the heroes list
});
describe('#onSelected', () => {
it('no hero is selected by default', injectHC(hc => {
expect(hc.currentHero).not.toBeDefined();
}));
it('sets the "currentHero"', injectHC(hc => {
hc.onSelect(heroData[1]); // select the second hero
expect(hc.currentHero).toEqual(heroData[1]);
}));
it('no hero is selected after onRefresh() called', injectHC(hc => {
hc.onSelect(heroData[1]); // select the second hero
hc.onRefresh();
expect(hc.currentHero).not.toBeDefined();
}));
// TODO: Remove `withNgClass=true` ONCE BUG IS FIXED
xit('the view of the "currentHero" has the "selected" class (NG2 BUG)', injectHC((hc, rootTC) => {
hc.onSelect(heroData[1]); // select the second hero
rootTC.detectChanges();
// The 3rd ViewChild is 2nd hero; the 1st is for the template
expectViewChildClass(rootTC, 2).toMatch('selected');
}, true /* true == include ngClass */));
it('the view of a non-selected hero does NOT have the "selected" class', injectHC((hc, rootTC) => {
hc.onSelect(heroData[1]); // select the second hero
rootTC.detectChanges();
// The 4th ViewChild is 3rd hero; the 1st is for the template
expectViewChildClass(rootTC, 4).not.toMatch('selected');
}));
});
// Most #onDelete tests not re-implemented because
// writing those tests w/in Angular adds little value and
// is far more painful than writing them to run outside Angular
// Only bother with the one test that checks the DOM
describe('#onDeleted', () => {
let template =
'<ul><li *ngFor="#h of heroes">{{h.name}}</li></ul>';
it('the list view does not contain the "deleted" currentHero', injectTcb((tcb:TCB) => {
return tcb
.overrideTemplate(HeroesComponent, template)
.createAsync(HeroesComponent)
.then((rootTC: RTC) => {
hc = rootTC.debugElement.componentInstance;
// trigger {{heroes}} binding
rootTC.detectChanges();
return rootTC; // wait for heroes to arrive
})
.then((rootTC: RTC) => {
hc.currentHero = heroData[1];
hc.onDelete()
rootTC.detectChanges(); // trigger component property binding
// confirm hero list is not displayed by looking for removed hero
expect(rootTC.debugElement.nativeElement.innerHTML).not.toMatch(heroData[1].name);
});
}));
});
});
////// Helpers //////
class HappyHeroService {
constructor() {
spyOn(this, 'refresh').and.callThrough();
}
heroes: Hero[];
refresh() {
this.heroes = [];
// updates cached heroes after one JavaScript cycle
return new Promise((resolve, reject) => {
this.heroes.push(...heroData);
resolve(this.heroes);
});
}
}
// The same setup for every test in the #onSelected suite
// TODO: Remove `withNgClass` and always include in template ONCE BUG IS FIXED
function injectHC(testFn: (hc: HeroesComponent, rootTC?: RTC) => void, withNgClass:boolean = false) {
// This is the bad boy: [ngClass]="getSelectedClass(hero)"
let ngClass = withNgClass ? '[ngClass]="getSelectedClass(hero)"' : '';
// focus on the part of the template that displays heroes
let template =
`<ul><li *ngFor="#hero of heroes"
${ngClass}
(click)="onSelect(hero)">
({{hero.id}}) {{hero.name}}
</li></ul>`;
return injectTcb((tcb:TCB) => {
let hc: HeroesComponent;
return tcb
.overrideTemplate(HeroesComponent, template)
.createAsync(HeroesComponent)
.then((rootTC:RTC) => {
hc = rootTC.debugElement.componentInstance;
rootTC.detectChanges();// trigger {{heroes}} binding
return rootTC;
})
.then((rootTC:RTC) => { // wait a tick until heroes are fetched
console.error("WAS THIS FIXED??");
// CRASHING HERE IF TEMPLATE HAS '[ngClass]="getSelectedClass(hero)"'
// WITH EXCEPTION:
// "Expression 'getSelectedClass(hero) in null' has changed after it was checked."
rootTC.detectChanges(); // show the list
testFn(hc, rootTC);
});
})
}

View File

@ -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 = () => (<any>service.refresh).calls.mostRecent().returnValue;
describe('HeroesComponent (no Angular)', () => {
beforeEach(()=> {
heroData = [new Hero(1, 'Foo'), new Hero(2, 'Bar'), new Hero(3, 'Baz')];
mockUser = new User();
});
beforeEach(()=> {
service = <any> new HappyHeroService();
hc = new HeroesComponent(service, mockUser)
});
it('can be created', () => {
expect(hc instanceof HeroesComponent).toEqual(true); // proof of life
});
it('has expected userName', () => {
expect(hc.userName).toEqual(mockUser.name);
});
describe('#onInit', () => {
it('HeroService.refresh not called immediately', () => {
let spy = <jasmine.Spy><any> service.refresh;
expect(spy.calls.count()).toEqual(0);
});
it('onInit calls HeroService.refresh', () => {
let spy = <jasmine.Spy><any> service.refresh;
hc.ngOnInit(); // Angular framework calls when it creates the component
expect(spy.calls.count()).toEqual(1);
});
})
describe('#heroes', () => {
it('lacks heroes when created', () => {
let heroes = hc.heroes;
expect(heroes.length).toEqual(0); // not filled yet
});
it('has heroes after cache loaded', done => {
hc.ngOnInit(); // Angular framework calls when it creates the component
refreshPromise().then(() => {
let heroes = hc.heroes; // now the component has heroes to show
expect(heroes.length).toEqual(heroData.length);
})
.then(done, done.fail);
});
it('restores heroes after refresh called again', done => {
hc.ngOnInit(); // component initialization triggers service
let heroes: Hero[];
refreshPromise().then(() => {
heroes = hc.heroes; // now the component has heroes to show
heroes[0].name = 'Wotan';
heroes.push(new Hero(33, 'Thor'));
hc.onRefresh();
})
.then(() => {
heroes = hc.heroes; // get it again (don't reuse old array!)
expect(heroes[0]).not.toEqual('Wotan'); // change reversed
expect(heroes.length).toEqual(heroData.length); // orig num of heroes
})
.then(done, done.fail);
});
});
describe('#onSelected', () => {
it('no hero is selected by default', () => {
expect(hc.currentHero).not.toBeDefined();
});
it('sets the "currentHero"', () => {
hc.onSelect(heroData[1]); // select the second hero
expect(hc.currentHero).toEqual(heroData[1]);
});
it('no hero is selected after onRefresh() called', () => {
hc.onSelect(heroData[1]); // select the second hero
hc.onRefresh();
expect(hc.currentHero).not.toBeDefined();
});
});
describe('#onDelete', () => {
// Load the heroes asynchronously before each test
// Getting the async out of the way in the beforeEach
// means tests can be synchronous
// Note: could have cheated and simply plugged hc.heroes with fake data
// that trick would fail if we reimplemented hc.heroes as a readonly property
beforeEach(done => {
hc.ngOnInit(); // Angular framework calls when it creates the component
refreshPromise().then(done, done.fail);
});
it('removes the supplied hero (only) from the list', () => {
hc.currentHero = heroData[1];
let hero = heroData[2];
hc.onDelete(hero);
expect(hc.heroes).not.toContain(hero);
expect(hc.heroes).toContain(heroData[1]); // left current in place
expect(hc.heroes.length).toEqual(heroData.length - 1);
});
it('removes the currentHero from the list if no hero argument', () => {
hc.currentHero = heroData[1];
hc.onDelete();
expect(hc.heroes).not.toContain(heroData[1]);
});
it('is harmless if no supplied or current hero', () => {
hc.currentHero = null;
hc.onDelete();
expect(hc.heroes.length).toEqual(heroData.length);
});
it('is harmless if hero not in list', () => {
let hero = heroData[1].clone(); // object reference matters, not id
hc.onDelete(hero);
expect(hc.heroes.length).toEqual(heroData.length);
});
// must go async to get hc to clear its heroes list
it('is harmless if the list is empty', done => {
let hero = heroData[1];
heroData = [];
hc.onRefresh();
refreshPromise().then(() => {
hc.onDelete(hero); // shouldn't fail
})
.then(done, done.fail);
});
it('the new currentHero is the one after the removed hero', () => {
hc.currentHero = heroData[1];
let expectedCurrent = heroData[2];
hc.onDelete();
expect(hc.currentHero).toBe(expectedCurrent);
});
it('the new currentHero is the one before the removed hero if none after', () => {
hc.currentHero = heroData[heroData.length - 1]; // last hero
let expectedCurrent = heroData[heroData.length - 2]; // penultimate hero
hc.onDelete();
expect(hc.currentHero).toBe(expectedCurrent);
});
});
});
////// Helpers //////
class HappyHeroService {
constructor() {
spyOn(this, 'refresh').and.callThrough();
}
heroes: Hero[];
refresh() {
this.heroes = [];
// updates cached heroes after one JavaScript cycle
return new Promise((resolve, reject) => {
this.heroes.push(...heroData);
resolve(this.heroes);
});
}
}

View File

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

View File

@ -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: `
<h2 highlight="yellow">Something Yellow</h2>
<h2 highlight>Something Gray</h2>
<h2>Something White</h2>
`
})
class TestComponent { }
////// Tests //////////
describe('HighlightDirective', () => {
let fixture: ComponentFixture<TestComponent>;
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 <h2> tokens means it is attached
expect(h2Des[0].providerTokens).toContain(HighlightDirective, 'HighlightDirective');
});
it('should color first <h2> background "yellow"', () => {
fixture.detectChanges();
const h2 = h2Des[0].nativeElement as HTMLElement;
expect(h2.style.backgroundColor).toBe('yellow');
});
it('should color second <h2> 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 <h2> (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');
});
});

View File

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

View File

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

View File

@ -0,0 +1 @@
/* MISSING */

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,27 @@
// #docregion
import { Component, OnInit, OnDestroy } from '@angular/core';
import { TwainService } from './twain.service';
@Component({
selector: 'twain-quote',
template: '<p class="twain"><i>{{quote}}</i></p>'
})
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);
}
}

View File

@ -0,0 +1,20 @@
// #docregion
import { Component, OnInit } from '@angular/core';
import { TwainService } from './twain.service';
// #docregion component
@Component({
selector: 'twain-quote',
template: '<p class="twain"><i>{{quote}}</i></p>'
})
export class TwainComponent implements OnInit {
intervalId: number;
quote = '...';
constructor(private twainService: TwainService) { }
ngOnInit(): void {
this.twainService.getQuote().then(quote => this.quote = quote);
}
}
// #enddocregion component

View File

@ -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<string> {
return new Promise(resolve => {
setTimeout( () => resolve(this.nextQuote()), 500 );
});
}
private nextQuote() {
if (this.next === quotes.length) { this.next = 0; }
return quotes[ this.next++ ];
}
}

View File

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

View File

@ -0,0 +1,18 @@
// #docregion
import { Component, OnInit } from '@angular/core';
import { UserService } from './model';
@Component({
selector: 'app-welcome',
template: '<h3 class="welcome" ><i>{{welcome}}</i></h3>'
})
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.';
}
}

View File

@ -0,0 +1,41 @@
<!-- Run the "bag" specs in a browser -->
<!-- #docregion -->
<!DOCTYPE html>
<html>
<head>
<base href="/">
<title>Specs Bag</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
<link rel="stylesheet" href="node_modules/jasmine-core/lib/jasmine-core/jasmine.css">
</head>
<body>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="node_modules/jasmine-core/lib/jasmine-core/jasmine.js"></script>
<script src="node_modules/jasmine-core/lib/jasmine-core/jasmine-html.js"></script>
<script src="node_modules/jasmine-core/lib/jasmine-core/boot.js"></script>
<script src="node_modules/reflect-metadata/Reflect.js"></script>
<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="node_modules/zone.js/dist/long-stack-trace-zone.js"></script>
<script src="node_modules/zone.js/dist/proxy.js"></script>
<script src="node_modules/zone.js/dist/sync-test.js"></script>
<script src="node_modules/zone.js/dist/jasmine-patch.js"></script>
<script src="node_modules/zone.js/dist/async-test.js"></script>
<script src="node_modules/zone.js/dist/fake-async-test.js"></script>
<script>
var __spec_files__ = [
'app/bag/bag.spec',
'app/bag/bag.no-testbed.spec',
'app/bag/async-helper.spec'
];
</script>
<script src="browser-test-shim.js"></script>
</body>
</html>

View File

@ -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"]
}

View File

@ -0,0 +1,27 @@
<!-- Run the bag source as an app -->
<!DOCTYPE html>
<html>
<head>
<base href="/">
<title>Specs Bag</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
<!-- Polyfill(s) for older browsers -->
<script src="node_modules/core-js/client/shim.min.js"></script>
<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="node_modules/reflect-metadata/Reflect.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="systemjs.config.js"></script>
<script>
System.import('app/bag/bag-main').catch(function(err){ console.error(err); });
</script>
</head>
<body>
<bag-comp>Loading ...</bag-comp>
</body>
</html>

Some files were not shown because too many files have changed in this diff Show More