docs(testing): testing chapter and samples for RC6 (#2198)
[WIP] docs(testing): new chapter, new samples
This commit is contained in:
parent
c5ad38ee86
commit
07cfce795f
|
@ -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() {
|
||||
|
|
|
@ -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);
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
|
@ -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"]
|
||||
}
|
|
@ -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
|
|
@ -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>
|
|
@ -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"]
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
// #docplaster
|
||||
// #docregion
|
||||
describe('1st tests', () => {
|
||||
it('true is true', () => expect(true).toBe(true));
|
||||
});
|
|
@ -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 { }
|
|
@ -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 */
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
|
@ -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, FakeRouter, FakeRouterLinkDirective, FakeRouterOutletComponent
|
||||
} from '../testing';
|
||||
|
||||
import { Router, MockRouter,
|
||||
RouterLink, MockRouterLink,
|
||||
RouterOutlet, MockRouterOutlet } from './mock-router';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
let fixture: ComponentFixture<AppComponent>;
|
||||
let comp: AppComponent;
|
||||
let fixture: ComponentFixture<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;
|
||||
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();
|
||||
});
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
||||
@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 { }
|
||||
|
|
|
@ -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 { }
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
})));
|
||||
});
|
|
@ -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
|
||||
];
|
|
@ -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();
|
||||
}));
|
||||
|
||||
});
|
|
@ -0,0 +1,5 @@
|
|||
// main app entry point
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||
import { BagModule } from './bag';
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(BagModule);
|
|
@ -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
|
|
@ -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';
|
||||
}
|
|
@ -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 { }
|
||||
|
|
@ -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
|
||||
});
|
|
@ -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';
|
||||
}
|
||||
|
|
@ -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>
|
|
@ -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');
|
||||
|
||||
});
|
||||
})));
|
||||
});
|
||||
});
|
|
@ -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
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
<!-- #docregion -->
|
||||
<div (click)="click()" class="hero">
|
||||
{{hero.name | uppercase}}
|
||||
</div>
|
|
@ -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
|
|
@ -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
|
|
@ -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 */
|
|
@ -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>
|
|
@ -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');
|
||||
});
|
||||
|
||||
});
|
|
@ -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
|
||||
});
|
||||
}
|
||||
|
|
@ -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`;
|
||||
}
|
||||
}
|
|
@ -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 { }
|
|
@ -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>
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -1,5 +0,0 @@
|
|||
// #docregion
|
||||
export class Hero {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
|
@ -1,4 +1,3 @@
|
|||
/* #docregion */
|
||||
label {
|
||||
display: inline-block;
|
||||
width: 3em;
|
|
@ -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>
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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});
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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();
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -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 ]);
|
||||
}
|
||||
}
|
|
@ -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 { }
|
|
@ -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);
|
|
@ -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 -->
|
|
@ -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
|
|
@ -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);
|
||||
|
|
|
@ -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(); }
|
||||
}
|
|
@ -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
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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>;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,4 @@
|
|||
export class Hero {
|
||||
constructor(public id = 0, public name = '') { }
|
||||
clone() { return new Hero(this.id, this.name); }
|
||||
}
|
|
@ -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 'rxjs/add/operator/catch';
|
||||
import 'rxjs/add/operator/do';
|
||||
import 'rxjs/add/operator/toPromise';
|
||||
|
||||
import { Hero } from './hero';
|
||||
import { HeroService } from './http-hero.service';
|
||||
|
||||
type HeroData = {id: string, name: string}
|
||||
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,
|
||||
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) => {
|
||||
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();
|
||||
})));
|
|
@ -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);
|
|
@ -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';
|
|
@ -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')
|
||||
];
|
|
@ -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>;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export * from './fake-hero.service';
|
|
@ -0,0 +1,7 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
isLoggedIn = true;
|
||||
user = {name: 'Sam Spade'};
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -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');
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -1,250 +0,0 @@
|
|||
/*
|
||||
* Dev Guide steps to hero.service.no-ng.spec
|
||||
* Try it with unit-tests-4.html
|
||||
*/
|
||||
|
||||
// The phase of hero-service-spec
|
||||
// when we're outlining what we want to test
|
||||
describe('HeroService (test plan)', () => {
|
||||
|
||||
describe('creation', () => {
|
||||
xit('can instantiate the service');
|
||||
xit('service.heroes is empty');
|
||||
});
|
||||
|
||||
describe('#refresh', () => {
|
||||
|
||||
describe('when server provides heroes', () => {
|
||||
xit('refresh promise returns expected # of heroes when fulfilled');
|
||||
xit('service.heroes has expected # of heroes when fulfilled');
|
||||
xit('service.heroes remains empty until fulfilled');
|
||||
xit('service.heroes remains empty when the server returns no data');
|
||||
xit('resets service.heroes w/ original data after re-refresh');
|
||||
xit('clears service.heroes while waiting for re-refresh');
|
||||
});
|
||||
|
||||
describe('when the server fails', () => {
|
||||
xit('returns failed promise with the server error');
|
||||
xit('clears service.heroes');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
import {HeroService} from './hero.service';
|
||||
|
||||
describe('HeroService (beginning tests - 1)', () => {
|
||||
|
||||
describe('creation', () => {
|
||||
it('can instantiate the service', () => {
|
||||
let service = new HeroService(null);
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
it('heroes is empty', () => {
|
||||
let service = new HeroService(null);
|
||||
expect(service.heroes.length).toEqual(0);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
import {BackendService} from './backend.service';
|
||||
import {Hero} from './hero';
|
||||
|
||||
xdescribe('HeroService (beginning tests - 2 [dont run])', () => {
|
||||
let heroData:Hero[];
|
||||
|
||||
// No good!
|
||||
it('refresh promise returns expected # of heroes when fulfilled', () => {
|
||||
let service = new HeroService(null);
|
||||
service.refresh().then(heroes => {
|
||||
expect(heroes.length).toBeGreaterThan(0); // don’t know how many to expect yet
|
||||
});
|
||||
});
|
||||
|
||||
// better ... but not async!
|
||||
it('refresh promise returns expected # of heroes when fulfilled', () => {
|
||||
|
||||
heroData = [new Hero(1, 'Foo'), new Hero(2, 'Bar'), new Hero(3, 'Baz')];
|
||||
|
||||
let backend = <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()));
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
})
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
|
||||
});
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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 );
|
||||
}
|
||||
}
|
||||
|
|
@ -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 { }
|
|
@ -0,0 +1 @@
|
|||
/* MISSING */
|
|
@ -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
|
|
@ -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() ));
|
||||
}
|
||||
}
|
|
@ -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
|
||||
});
|
|
@ -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
|
||||
});
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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++ ];
|
||||
}
|
||||
}
|
|
@ -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
|
||||
});
|
|
@ -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.';
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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"]
|
||||
}
|
|
@ -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
Loading…
Reference in New Issue