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 = [
|
var _exampleBoilerplateFiles = [
|
||||||
'.editorconfig',
|
'.editorconfig',
|
||||||
'a2docs.css',
|
'a2docs.css',
|
||||||
'karma.conf.js',
|
|
||||||
'karma-test-shim.js',
|
|
||||||
'package.json',
|
'package.json',
|
||||||
'styles.css',
|
'styles.css',
|
||||||
'systemjs.config.js',
|
'systemjs.config.js',
|
||||||
'tsconfig.json',
|
'tsconfig.json',
|
||||||
'tslint.json',
|
'tslint.json',
|
||||||
'typings.json',
|
'typings.json'
|
||||||
'wallaby.js'
|
|
||||||
];
|
];
|
||||||
|
|
||||||
var _exampleDartWebBoilerPlateFiles = ['a2docs.css', 'styles.css'];
|
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)
|
// Using the --build flag will use systemjs.config.plunker.build.js (for preview builds)
|
||||||
gulp.task('build-plunkers', ['_copy-example-boilerplate'], function() {
|
gulp.task('build-plunkers', ['_copy-example-boilerplate'], function() {
|
||||||
regularPlunker.buildPlunkers(EXAMPLES_PATH, LIVE_EXAMPLES_PATH, { errFn: gutil.log, build: argv.build });
|
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() {
|
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-cli": "^1.0.1",
|
||||||
"karma-htmlfile-reporter": "^0.3.4",
|
"karma-htmlfile-reporter": "^0.3.4",
|
||||||
"karma-jasmine": "^1.0.2",
|
"karma-jasmine": "^1.0.2",
|
||||||
|
"karma-jasmine-html-reporter": "^0.2.2",
|
||||||
"karma-phantomjs-launcher": "^1.0.2",
|
"karma-phantomjs-launcher": "^1.0.2",
|
||||||
"karma-sourcemap-loader": "^0.3.7",
|
"karma-sourcemap-loader": "^0.3.7",
|
||||||
"karma-webpack": "^1.8.0",
|
"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 { async, ComponentFixture, TestBed
|
||||||
import { AppComponent } from './app.component';
|
|
||||||
|
|
||||||
import { By } from '@angular/platform-browser';
|
|
||||||
import { DebugElement } from '@angular/core';
|
|
||||||
|
|
||||||
import {
|
|
||||||
async, inject
|
|
||||||
} from '@angular/core/testing';
|
} from '@angular/core/testing';
|
||||||
|
|
||||||
import { ComponentFixture, TestComponentBuilder } from '@angular/core/testing';
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
|
||||||
import { Hero, HeroService, MockHeroService } from './mock-hero.service';
|
import { AppComponent } from './app.component';
|
||||||
|
import { BannerComponent } from './banner.component';
|
||||||
|
import { SharedModule } from './shared/shared.module';
|
||||||
|
|
||||||
import { Router, MockRouter,
|
import { Router, FakeRouter, FakeRouterLinkDirective, FakeRouterOutletComponent
|
||||||
RouterLink, MockRouterLink,
|
} from '../testing';
|
||||||
RouterOutlet, MockRouterOutlet } from './mock-router';
|
|
||||||
|
|
||||||
describe('AppComponent', () => {
|
|
||||||
let fixture: ComponentFixture<AppComponent>;
|
|
||||||
let comp: AppComponent;
|
|
||||||
|
|
||||||
beforeEach(async(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
|
let comp: AppComponent;
|
||||||
tcb
|
let fixture: ComponentFixture<AppComponent>;
|
||||||
.overrideDirective(AppComponent, RouterLink, MockRouterLink)
|
|
||||||
.overrideDirective(AppComponent, RouterOutlet, MockRouterOutlet)
|
describe('AppComponent & TestModule', () => {
|
||||||
.overrideProviders(AppComponent, [
|
beforeEach( async(() => {
|
||||||
{ provide: HeroService, useClass: MockHeroService},
|
TestBed.configureTestingModule({
|
||||||
{ provide: Router, useClass: MockRouter},
|
declarations: [
|
||||||
])
|
AppComponent, BannerComponent,
|
||||||
.createAsync(AppComponent)
|
FakeRouterLinkDirective, FakeRouterOutletComponent
|
||||||
.then(fix => {
|
],
|
||||||
fixture = fix;
|
providers: [{ provide: Router, useClass: FakeRouter }],
|
||||||
comp = fixture.debugElement.componentInstance;
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
});
|
})
|
||||||
})));
|
|
||||||
|
.compileComponents()
|
||||||
|
|
||||||
|
.then(() => {
|
||||||
|
fixture = TestBed.createComponent(AppComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
});
|
||||||
|
|
||||||
|
}));
|
||||||
|
|
||||||
|
tests();
|
||||||
|
});
|
||||||
|
|
||||||
|
function tests() {
|
||||||
|
|
||||||
it('can instantiate it', () => {
|
it('can instantiate it', () => {
|
||||||
expect(comp).not.toBeNull();
|
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', () => {
|
it('can get RouterLinks from template', () => {
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
let links = fixture.debugElement
|
const links = fixture.debugElement
|
||||||
.queryAll(By.directive(MockRouterLink))
|
// find all elements with an attached FakeRouterLink directive
|
||||||
.map(de => <MockRouterLink> de.injector.get(MockRouterLink) );
|
.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.length).toBe(3, 'should have 3 links');
|
||||||
expect(links[0].routeParams[0]).toEqual('Dashboard', '1st link should go to Dashboard');
|
expect(links[0].linkParams).toBe('/dashboard', '1st link should go to Dashboard');
|
||||||
expect(links[1].routeParams[0]).toEqual('Heroes', '1st link should go to Heroes');
|
expect(links[1].linkParams).toBe('/heroes', '1st link should go to Heroes');
|
||||||
|
|
||||||
let result = links[1].onClick();
|
|
||||||
expect(result).toEqual(false, 'click should prevent default browser behavior');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can click Heroes link in template', () => {
|
it('can click Heroes link in template', () => {
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
// Heroes RouterLink DebugElement
|
// Heroes RouterLink DebugElement
|
||||||
let heroesDe = fixture.debugElement
|
const heroesLinkDe = fixture.debugElement
|
||||||
.queryAll(By.directive(MockRouterLink))[1];
|
.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');
|
expect(link.navigatedTo).toBeNull('link should not have navigate yet');
|
||||||
|
|
||||||
heroesDe.triggerEventHandler('click', null);
|
heroesLinkDe.triggerEventHandler('click', null);
|
||||||
|
|
||||||
fixture.detectChanges();
|
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
|
// #docregion
|
||||||
import { Component } from '@angular/core';
|
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({
|
@Component({
|
||||||
selector: 'my-app',
|
selector: 'my-app',
|
||||||
template: `
|
templateUrl: 'app/app.component.html'
|
||||||
<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
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
@RouteConfig([
|
export class AppComponent { }
|
||||||
{ 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';
|
|
||||||
}
|
|
||||||
|
|
|
@ -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-'] {
|
[class*='col-'] {
|
||||||
float: left;
|
float: left;
|
||||||
}
|
}
|
||||||
|
@ -24,40 +22,14 @@ h3 {
|
||||||
.col-1-4 {
|
.col-1-4 {
|
||||||
width: 25%;
|
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 {
|
.grid-pad {
|
||||||
padding: 10px 0;
|
padding: 10px 0;
|
||||||
}
|
}
|
||||||
.grid-pad > [class*='col-']:last-of-type {
|
.grid-pad > [class*='col-']:last-of-type {
|
||||||
padding-right: 20px;
|
padding-right: 20px;
|
||||||
}
|
}
|
||||||
@media (max-width: 600px) {
|
|
||||||
.module {
|
|
||||||
font-size: 10px;
|
|
||||||
max-height: 75px; }
|
|
||||||
}
|
|
||||||
@media (max-width: 1024px) {
|
@media (max-width: 1024px) {
|
||||||
.grid {
|
.grid {
|
||||||
margin: 0;
|
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 {
|
label {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 3em;
|
width: 3em;
|
||||||
|
@ -25,6 +24,6 @@ button:hover {
|
||||||
}
|
}
|
||||||
button:disabled {
|
button:disabled {
|
||||||
background-color: #eee;
|
background-color: #eee;
|
||||||
color: #ccc;
|
color: #ccc;
|
||||||
cursor: auto;
|
cursor: auto;
|
||||||
}
|
}
|
|
@ -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';
|
// main app entry point
|
||||||
import { AppComponent } from './app.component';
|
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||||
|
import { AppModule } from './app.module';
|
||||||
bootstrap(AppComponent);
|
|
||||||
|
|
||||||
|
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 {
|
import {
|
||||||
addProviders,
|
async, inject, TestBed
|
||||||
async, inject, withProviders
|
|
||||||
} from '@angular/core/testing';
|
} from '@angular/core/testing';
|
||||||
|
|
||||||
import { TestComponentBuilder } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
MockBackend,
|
MockBackend,
|
||||||
MockConnection } from '@angular/http/testing';
|
MockConnection
|
||||||
|
} from '@angular/http/testing';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Http, HTTP_PROVIDERS,
|
HttpModule, Http, XHRBackend, Response, ResponseOptions
|
||||||
ConnectionBackend, XHRBackend,
|
|
||||||
Request, RequestMethod, BaseRequestOptions, RequestOptions,
|
|
||||||
Response, ResponseOptions,
|
|
||||||
URLSearchParams
|
|
||||||
} from '@angular/http';
|
} from '@angular/http';
|
||||||
|
|
||||||
// Add all operators to Observable
|
|
||||||
import 'rxjs/Rx';
|
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import 'rxjs/add/observable/of';
|
||||||
|
|
||||||
import { Hero } from './hero';
|
import 'rxjs/add/operator/catch';
|
||||||
import { HeroService } from './http-hero.service';
|
import 'rxjs/add/operator/do';
|
||||||
|
import 'rxjs/add/operator/toPromise';
|
||||||
|
|
||||||
type HeroData = {id: string, name: string}
|
import { Hero } from './hero';
|
||||||
|
import { HttpHeroService as HeroService } from './http-hero.service';
|
||||||
|
|
||||||
const makeHeroData = () => [
|
const makeHeroData = () => [
|
||||||
{ id: '1', name: 'Windstorm' },
|
{ id: 1, name: 'Windstorm' },
|
||||||
{ id: '2', name: 'Bombasto' },
|
{ id: 2, name: 'Bombasto' },
|
||||||
{ id: '3', name: 'Magneta' },
|
{ id: 3, name: 'Magneta' },
|
||||||
{ id: '4', name: 'Tornado' }
|
{ id: 4, name: 'Tornado' }
|
||||||
];
|
] as Hero[];
|
||||||
|
|
||||||
// HeroService expects response data like {data: {the-data}}
|
//////// Tests /////////////
|
||||||
const makeResponseData = (data: {}) => {return { data }; };
|
|
||||||
|
|
||||||
//////// SPECS /////////////
|
|
||||||
describe('Http-HeroService (mockBackend)', () => {
|
describe('Http-HeroService (mockBackend)', () => {
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach( async(() => {
|
||||||
addProviders([
|
TestBed.configureTestingModule({
|
||||||
HTTP_PROVIDERS,
|
imports: [ HttpModule ],
|
||||||
{ provide: XHRBackend, useClass: MockBackend }
|
providers: [
|
||||||
]);
|
HeroService,
|
||||||
});
|
{ provide: XHRBackend, useClass: MockBackend }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
it('can instantiate service when inject service',
|
it('can instantiate service when inject service',
|
||||||
withProviders(() => [HeroService])
|
inject([HeroService], (service: HeroService) => {
|
||||||
.inject([HeroService], (service: HeroService) => {
|
expect(service instanceof HeroService).toBe(true);
|
||||||
expect(service instanceof HeroService).toBe(true);
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
it('can instantiate service with "new"', inject([Http], (http: Http) => {
|
it('can instantiate service with "new"', inject([Http], (http: Http) => {
|
||||||
expect(http).not.toBeNull('http should be provided');
|
expect(http).not.toBeNull('http should be provided');
|
||||||
let service = new HeroService(http);
|
let service = new HeroService(http);
|
||||||
|
@ -69,10 +64,9 @@ describe('Http-HeroService (mockBackend)', () => {
|
||||||
describe('when getHeroes', () => {
|
describe('when getHeroes', () => {
|
||||||
let backend: MockBackend;
|
let backend: MockBackend;
|
||||||
let service: HeroService;
|
let service: HeroService;
|
||||||
let fakeHeroes: HeroData[];
|
let fakeHeroes: Hero[];
|
||||||
let response: Response;
|
let response: Response;
|
||||||
|
|
||||||
|
|
||||||
beforeEach(inject([Http, XHRBackend], (http: Http, be: MockBackend) => {
|
beforeEach(inject([Http, XHRBackend], (http: Http, be: MockBackend) => {
|
||||||
backend = be;
|
backend = be;
|
||||||
service = new HeroService(http);
|
service = new HeroService(http);
|
||||||
|
@ -87,7 +81,7 @@ describe('Http-HeroService (mockBackend)', () => {
|
||||||
service.getHeroes().toPromise()
|
service.getHeroes().toPromise()
|
||||||
// .then(() => Promise.reject('deliberate'))
|
// .then(() => Promise.reject('deliberate'))
|
||||||
.then(heroes => {
|
.then(heroes => {
|
||||||
expect(heroes.length).toEqual(fakeHeroes.length,
|
expect(heroes.length).toBe(fakeHeroes.length,
|
||||||
'should have expected no. of heroes');
|
'should have expected no. of heroes');
|
||||||
});
|
});
|
||||||
})));
|
})));
|
||||||
|
@ -97,7 +91,7 @@ describe('Http-HeroService (mockBackend)', () => {
|
||||||
|
|
||||||
service.getHeroes()
|
service.getHeroes()
|
||||||
.do(heroes => {
|
.do(heroes => {
|
||||||
expect(heroes.length).toEqual(fakeHeroes.length,
|
expect(heroes.length).toBe(fakeHeroes.length,
|
||||||
'should have expected no. of heroes');
|
'should have expected no. of heroes');
|
||||||
})
|
})
|
||||||
.toPromise();
|
.toPromise();
|
||||||
|
@ -110,7 +104,7 @@ describe('Http-HeroService (mockBackend)', () => {
|
||||||
|
|
||||||
service.getHeroes()
|
service.getHeroes()
|
||||||
.do(heroes => {
|
.do(heroes => {
|
||||||
expect(heroes.length).toEqual(0, 'should have no heroes');
|
expect(heroes.length).toBe(0, 'should have no heroes');
|
||||||
})
|
})
|
||||||
.toPromise();
|
.toPromise();
|
||||||
})));
|
})));
|
|
@ -4,10 +4,16 @@ import { Injectable } from '@angular/core';
|
||||||
import { Http, Response } from '@angular/http';
|
import { Http, Response } from '@angular/http';
|
||||||
import { Headers, RequestOptions } from '@angular/http';
|
import { Headers, RequestOptions } from '@angular/http';
|
||||||
import { Hero } from './hero';
|
import { Hero } from './hero';
|
||||||
|
|
||||||
import { Observable } from 'rxjs/Observable';
|
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()
|
@Injectable()
|
||||||
export class HeroService {
|
export class HttpHeroService {
|
||||||
private _heroesUrl = 'app/heroes'; // URL to web api
|
private _heroesUrl = 'app/heroes'; // URL to web api
|
||||||
|
|
||||||
constructor (private http: Http) {}
|
constructor (private http: Http) {}
|
||||||
|
@ -19,6 +25,12 @@ export class HeroService {
|
||||||
.catch(this.handleError);
|
.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> {
|
addHero (name: string): Observable<Hero> {
|
||||||
let body = JSON.stringify({ name });
|
let body = JSON.stringify({ name });
|
||||||
let headers = new Headers({ 'Content-Type': 'application/json' });
|
let headers = new Headers({ 'Content-Type': 'application/json' });
|
||||||
|
@ -29,6 +41,16 @@ export class HeroService {
|
||||||
.catch(this.handleError);
|
.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) {
|
private extractData(res: Response) {
|
||||||
if (res.status < 200 || res.status >= 300) {
|
if (res.status < 200 || res.status >= 300) {
|
||||||
throw new Error('Bad response status: ' + res.status);
|
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