docs(testing): more testing samples and infrastructure setup

added wallaby config; revised karma-test-shim
This commit is contained in:
Ward Bell 2016-04-14 10:36:38 -07:00
parent 96137efd79
commit e1862887ae
26 changed files with 1019 additions and 175 deletions

View File

@ -76,7 +76,8 @@ var _exampleBoilerplateFiles = [
'styles.css',
'tsconfig.json',
'tslint.json',
'typings.json'
'typings.json',
'wallaby.js'
];
var _exampleDartWebBoilerPlateFiles = ['styles.css'];

View File

@ -1,13 +1,21 @@
.editorconfig
.idea
.vscode
styles.css
typings
typings.json
node_modules
jspm_packages
*.js.map
package.json
karma.conf.js
karma-test-shim.js
tsconfig.json
tslint.json
wallaby.js
npm-debug*.
**/protractor.config.js
protractor.config.js
_test-output
_temp
!**/*e2e-spec.js

View File

@ -1,5 +1,4 @@
describe('Displaying Data Tests', function () {
var _title = "Tour of Heroes";
var _defaultHero = 'Windstorm'
@ -15,7 +14,12 @@ describe('Displaying Data Tests', function () {
expect(element(by.css('h2')).getText()).toContain(_defaultHero);
});
it('should have many heroes', function () {
it('should have heroes', function () {
var heroEls = element.all(by.css('li'));
expect(heroEls.count()).not.toBe(0, 'should have heroes');
});
it('should display "there are many heroes!"', function () {
expect(element(by.css('ul ~ p')).getText()).toContain('There are many heroes!');
});
});

View File

@ -3,7 +3,7 @@
// #docregion imports
import {Component} from 'angular2/core';
// #enddocregion imports
import {Hero} from './hero'
import {Hero} from './hero';
@Component({
selector: 'my-app',

View File

@ -41,30 +41,29 @@ System.config({ packages: packages });
// Configure Angular for the browser and
// with test versions of the platform providers
System.import('angular2/testing')
.then(function (testing) {
return System.import('angular2/platform/testing/browser')
.then(function (providers) {
testing.setBaseTestProviders(
providers.TEST_BROWSER_PLATFORM_PROVIDERS,
providers.TEST_BROWSER_APPLICATION_PROVIDERS
);
});
Promise.all([
System.import('angular2/testing'),
System.import('angular2/platform/testing/browser')
])
.then(function (results) {
var testing = results[0];
var browser = results[1];
testing.setBaseTestProviders(
browser.TEST_BROWSER_PLATFORM_PROVIDERS,
browser.TEST_BROWSER_APPLICATION_PROVIDERS);
// Load all spec files
// (e.g. 'base/app/hero.service.spec.js')
return Promise.all(
Object.keys(window.__karma__.files)
.filter(onlySpecFiles)
.map(function (moduleName) {
moduleNames.push(moduleName);
return System.import(moduleName);
}));
})
// Load all spec files
// (e.g. 'base/app/hero.service.spec.js')
.then(function () {
return Promise.all(
Object.keys(window.__karma__.files)
.filter(onlySpecFiles)
.map(function (moduleName) {
moduleNames.push(moduleName);
return System.import(moduleName);
}));
})
.then(success, fail);
.then(success, fail);
////// Helpers //////

View File

@ -1,7 +1,7 @@
module.exports = function(config) {
var appBase = 'app/'; // transpiled app JS files
var appAssets ='base/app/'; // component assets fetched by Angular's compiler
var appBase = 'app/'; // transpiled app JS files
var appAssets ='/base/app/'; // component assets fetched by Angular's compiler
config.set({
basePath: '',

View File

@ -1,3 +1,4 @@
/* tslint:disable:forin */
// #docregion
import {
Component, Input, ViewChild,
@ -6,7 +7,7 @@ import {
class Hero {
constructor(public name:string){}
constructor(public name: string) {}
}
@Component({
@ -30,13 +31,13 @@ export class OnChangesComponent implements OnChanges {
@Input() power: string;
// #enddocregion inputs
changeLog:string[] = [];
changeLog: string[] = [];
// #docregion ng-on-changes
ngOnChanges(changes: {[propertyName: string]: SimpleChange}) {
for (let propName in changes) {
let prop = changes[propName];
let cur = JSON.stringify(prop.currentValue)
let cur = JSON.stringify(prop.currentValue);
let prev = JSON.stringify(prop.previousValue);
this.changeLog.push(`${propName}: currentValue = ${cur}, previousValue = ${prev}`);
}
@ -50,13 +51,13 @@ export class OnChangesComponent implements OnChanges {
@Component({
selector: 'on-changes-parent',
templateUrl:'app/on-changes-parent.component.html',
templateUrl: 'app/on-changes-parent.component.html',
styles: ['.parent {background: Lavender;}'],
directives: [OnChangesComponent]
})
export class OnChangesParentComponent {
hero:Hero;
power:string;
hero: Hero;
power: string;
title = 'OnChanges';
@ViewChild(OnChangesComponent) childView:OnChangesComponent;
@ -69,6 +70,6 @@ export class OnChangesParentComponent {
this.hero = new Hero('Windstorm');
// setting power only triggers onChanges if this value is different
this.power = 'sing';
this.childView && this.childView.reset();
if (this.childView) { this.childView.reset(); }
}
}

View File

@ -2,19 +2,18 @@
"name": "angular2-examples-master",
"version": "1.0.0",
"description": "Master package.json, the superset of all dependencies for all of the _example package.json files.",
"main": "index.js",
"scripts": {
"start": "tsc && concurrently \"tsc -w\" \"lite-server\" ",
"tsc": "tsc",
"tsc:w": "tsc -w",
"lite": "lite-server",
"live": "live-server",
"test": "tsc && concurrently \"tsc -w\" \"karma start karma.conf.js\"",
"build-and-test": "npm run tsc && npm run test",
"e2e": "tsc && concurrently \"http-server\" \"protractor protractor.config.js\"",
"http-server": "tsc && http-server",
"http-server:e2e": "http-server",
"lite": "lite-server",
"postinstall": "typings install",
"test": "tsc && concurrently \"tsc -w\" \"karma start karma.conf.js\"",
"tsc": "tsc",
"tsc:w": "tsc -w",
"typings": "typings",
"postinstall": "typings install"
"webdriver:update": "webdriver-manager update"
},
"keywords": [],
"author": "",
@ -37,6 +36,7 @@
"typescript": "^1.8.10",
"typings":"^0.7.12",
"canonical-path": "0.0.2",
"http-server": "^0.9.0",
"jasmine-core": "~2.4.1",
"karma": "^0.13.22",
@ -44,7 +44,6 @@
"karma-cli": "^0.1.2",
"karma-htmlfile-reporter": "^0.2.2",
"karma-jasmine": "^0.3.8",
"live-server": "^0.9.2",
"protractor": "^3.2.2",
"rimraf": "^2.5.2"
},

View File

@ -1,9 +1,14 @@
// TO RUN THE TESTS
//
// The first time, run:
// FIRST TIME ONLY- run:
// ./node_modules/.bin/webdriver-manager update
// Make sure the test server is running. Then do.
// ./node_modules/.bin/protractor protractor.config.js
//
// Try: `npm run webdriver:update`
//
// AND THEN EVERYTIME ...
// 1. Compile with `tsc`
// 2. Make sure the test server (e.g., http-server: localhost:8080) is running.
// 3. ./node_modules/.bin/protractor protractor.config.js
//
// To do all steps, try: `npm run e2e`
var fs = require('fs');
var path = require('canonical-path');
@ -93,17 +98,13 @@ function itIf(cond, name, func) {
}
}
// Hack - because of bug with send keys
// Hack - because of bug with protractor send keys
function sendKeys(element, str) {
return str.split('').reduce(function (promise, char) {
return promise.then(function () {
return element.sendKeys(char);
});
return promise.resolve(element.sendKeys(char));
}, element.getAttribute('value'));
// better to create a resolved promise here but ... don't know how with protractor;
}
function Reporter(options) {
var _defaultOutputFile = path.resolve(process.cwd(), "../../", 'protractor-results.txt');
options.outputFile = options.outputFile || _defaultOutputFile;

View File

@ -3,11 +3,11 @@
"version": "1.0.0",
"scripts": {
"start": "tsc && concurrently \"npm run tsc:w\" \"npm run lite\" ",
"lite": "lite-server",
"postinstall": "typings install",
"tsc": "tsc",
"tsc:w": "tsc -w",
"lite": "lite-server",
"typings": "typings",
"postinstall": "typings install"
"typings": "typings"
},
"license": "ISC",
"dependencies": {

View File

@ -0,0 +1,84 @@
/* tslint:disable:no-unused-variable */
import { AppComponent } from './app.component';
import { By } from 'angular2/platform/browser';
import { provide } from 'angular2/core';
import {
beforeEach, beforeEachProviders,
describe, ddescribe, xdescribe,
expect, it, iit, xit,
inject, injectAsync,
ComponentFixture, TestComponentBuilder
} from 'angular2/testing';
import { Hero, HeroService, MockHeroService } from './mock-hero.service';
import { Router, MockRouter,
RouterLink, MockRouterLink,
RouterOutlet, MockRouterOutlet} from './mock-router';
describe('AppComponent', () => {
let fixture: ComponentFixture;
let comp: AppComponent;
beforeEach(injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => {
return tcb
.overrideDirective(AppComponent, RouterLink, MockRouterLink)
.overrideDirective(AppComponent, RouterOutlet, MockRouterOutlet)
.overrideProviders(AppComponent, [
provide(HeroService, {useClass: MockHeroService}),
provide(Router, {useClass: MockRouter}),
])
.createAsync(AppComponent)
.then(fix => {
fixture = fix;
comp = fixture.debugElement.componentInstance;
});
}));
it('can instantiate it', () => {
expect(comp).not.toBeNull();
});
it('can get title from template', () => {
fixture.detectChanges();
let titleEl = fixture.debugElement.query(By.css('h1')).nativeElement;
expect(titleEl).toHaveText(comp.title);
});
it('can get RouterLinks from template', () => {
fixture.detectChanges();
let links = fixture.debugElement
.queryAll(function (de) { return de.componentInstance instanceof MockRouterLink; })
.map(de => <MockRouterLink> de.componentInstance);
expect(links.length).toEqual(2, 'should have 2 links');
expect(links[0].routeParams[0]).toEqual('Dashboard', '1st link should go to Dashboard');
expect(links[1].routeParams[0]).toEqual('Heroes', '1st link should go to Heroes');
let result = links[1].onClick();
expect(result).toEqual(false, 'click should prevent default browser behavior');
});
it('can click Heroes link in template', () => {
fixture.detectChanges();
// Heroes RouterLink DebugElement
let heroesDe = fixture.debugElement
.queryAll(function (de) { return de.componentInstance instanceof MockRouterLink; })[1];
expect(heroesDe).not.toBeNull('should 2nd link');
let link = <MockRouterLink> heroesDe.componentInstance;
expect(link.navigatedTo).toBeNull('link should not have navigate yet');
heroesDe.triggerEventHandler('click', null);
fixture.detectChanges();
expect(link.navigatedTo[0]).toEqual('Heroes');
});
});

View File

@ -1,18 +1,20 @@
// #docplaster
// #docregion
import { Component } from 'angular2/core';
import { RouteConfig, ROUTER_DIRECTIVES, ROUTER_PROVIDERS } from 'angular2/router';
import { HeroService } from './hero.service';
import { DashboardComponent } from './dashboard.component';
import { HeroesComponent } from './heroes.component';
// #docregion hero-detail-import
// Can't test with ROUTER_DIRECTIVES yet
// import { RouteConfig, ROUTER_DIRECTIVES, ROUTER_PROVIDERS } from 'angular2/router';
import { RouteConfig, RouterLink,
RouterOutlet, ROUTER_PROVIDERS } from 'angular2/router';
import { DashboardComponent } from './dashboard.component';
import { HeroesComponent } from './heroes.component';
import { HeroDetailComponent } from './hero-detail.component';
// #enddocregion hero-detail-import
import { HeroService } from './hero.service';
@Component({
selector: 'my-app',
// #docregion template
template: `
<h1>{{title}}</h1>
<nav>
@ -21,39 +23,18 @@ import { HeroDetailComponent } from './hero-detail.component';
</nav>
<router-outlet></router-outlet>
`,
// #enddocregion template
// #docregion style-urls
styleUrls: ['app/app.component.css'],
// #enddocregion style-urls
directives: [ROUTER_DIRECTIVES],
directives: [RouterLink, RouterOutlet],
providers: [
ROUTER_PROVIDERS,
HeroService
]
})
@RouteConfig([
// #docregion dashboard-route
{
path: '/dashboard',
name: 'Dashboard',
component: DashboardComponent,
useAsDefault: true
},
// #enddocregion dashboard-route
// #docregion hero-detail-route
{
path: '/detail/:id',
name: 'HeroDetail',
component: HeroDetailComponent
},
// #enddocregion hero-detail-route
{
path: '/heroes',
name: 'Heroes',
component: HeroesComponent
}
{ 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';
}
// #enddocregion

View File

@ -5,28 +5,22 @@ import {
ChildChildComp, ChildComp, ChildWithChildComp,
ExternalTemplateComp,
FancyService, MockFancyService,
MyIfComp,
InputComp,
MyIfComp, MyIfChildComp, MyIfParentComp,
MockChildComp, MockChildChildComp,
ParentComp,
TestProvidersComp, TestViewProvidersComp
} from './public';
} from './bag';
import { DebugElement } from 'angular2/core';
import { By } from 'angular2/platform/browser';
import {
it,
iit,
xit,
describe,
ddescribe,
xdescribe,
expect,
fakeAsync,
tick,
beforeEach,
inject,
injectAsync,
withProviders,
beforeEachProviders,
TestComponentBuilder
beforeEach, beforeEachProviders, withProviders,
describe, ddescribe, xdescribe,
expect, it, iit, xit,
inject, injectAsync, fakeAsync, tick,
ComponentFixture, TestComponentBuilder
} from 'angular2/testing';
import { provide } from 'angular2/core';
@ -49,13 +43,13 @@ describe('angular2 jasmine matchers', () => {
describe('toHaveCssClass', () => {
it('should assert that the CSS class is present', () => {
let el = document.createElement('div');
el.classList.add('matias');
expect(el).toHaveCssClass('matias');
el.classList.add('bombasto');
expect(el).toHaveCssClass('bombasto');
});
it('should assert that the CSS class is not present', () => {
let el = document.createElement('div');
el.classList.add('matias');
el.classList.add('bombasto');
expect(el).not.toHaveCssClass('fatias');
});
});
@ -187,13 +181,39 @@ describe('test component builder', function() {
let comp = <ButtonComp> fixture.componentInstance;
expect(comp.wasClicked).toEqual(false, 'wasClicked should be false at start');
let btn = fixture.debugElement.query(el => el.name === 'button');
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 works too; which is "better"?
// 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)',
injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => {
let origName = 'John';
let newName = 'Sally';
return 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',
injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => {
@ -220,7 +240,7 @@ describe('test component builder', function() {
});
}));
it('should override component dependencies',
it('should override component directives',
injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => {
return tcb.overrideDirective(ParentComp, ChildComp, MockChildComp)
@ -233,7 +253,7 @@ describe('test component builder', function() {
}));
it('should override child component\'s dependencies',
it('should override child component\'s directives',
injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => {
return tcb.overrideDirective(ParentComp, ChildComp, ChildWithChildComp)
@ -262,7 +282,6 @@ describe('test component builder', function() {
});
}));
it('should override a viewProvider',
injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => {
@ -288,16 +307,158 @@ describe('test component builder', function() {
.toHaveText('from external template\n');
});
}), 10000); // Long timeout because this test makes an actual XHR.
describe('(lifecycle hooks w/ MyIfParentComp)', () => {
let fixture: ComponentFixture;
let parent: MyIfParentComp;
let child: MyIfChildComp;
/**
* Get the MyIfChildComp from parent; fail w/ good message if cannot.
*/
function getChild() {
let childDe: DebugElement; // DebugElement that should hold the MyIfChildComp
// The Hard Way: requires detailed knowledge of the parent template
try {
childDe = fixture.debugElement.children[4].children[0];
} catch (err) { /* we'll report the error */ }
// DebugElement.queryAll: if we wanted all of many instances:
childDe = fixture.debugElement
.queryAll(function (de) { return de.componentInstance instanceof MyIfChildComp; })[0];
// WE'LL USE THIS APPROACH !
// DebugElement.query: find first instance (if any)
childDe = fixture.debugElement
.query(function (de) { return de.componentInstance instanceof MyIfChildComp; });
if (childDe && childDe.componentInstance) {
child = childDe.componentInstance;
} else {
fail('Unable to find MyIfChildComp within MyIfParentComp');
}
return child;
}
// Create MyIfParentComp TCB and component instance before each test (async beforeEach)
beforeEach(injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => {
return 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', injectAsync([], () => {
fixture.detectChanges();
getChild();
child.childValue = 'bar';
let deferred = PromiseWrapper.completer();
let p = deferred.promise.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');
});
// Wait one JS engine turn!
setTimeout(() => deferred.resolve(), 0);
return p;
}));
/* Will soon be able to write it like this:
it('changed child value flows to parent', async(() => {
fixture.detectChanges();
getChild();
child.childValue = 'bar';
// Wait one JS engine turn!
setTimeout(() => {
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');
}, 0);
}));
*/
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);
});
});
});
describe('errors', () => {
describe('inject/async testing errors', () => {
let originalJasmineIt: any;
let originalJasmineBeforeEach: any;
let patchJasmineIt = () => {
let deferred = PromiseWrapper.completer();
originalJasmineIt = jasmine.getEnv().it;
jasmine.getEnv().it = (description: string, fn: Function) => {
jasmine.getEnv().it = (description: string, fn: Function): jasmine.Spec => {
let done = () => { deferred.resolve(); };
(<any>done).fail = (err: any) => { deferred.reject(err); };
fn(done);
@ -311,7 +472,7 @@ describe('errors', () => {
let patchJasmineBeforeEach = () => {
let deferred = PromiseWrapper.completer();
originalJasmineBeforeEach = jasmine.getEnv().beforeEach;
jasmine.getEnv().beforeEach = (fn: any) => {
jasmine.getEnv().beforeEach = (fn: any): void => {
let done = () => { deferred.resolve(); };
(<any>done).fail = (err: any) => { deferred.reject(err); };
fn(done);
@ -456,3 +617,32 @@ describe('errors', () => {
});
});
});
//////// Testing Framework Bugs? /////
import { HeroService } from './hero.service';
import { Component } from 'angular2/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',
injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => {
return tcb.overrideProviders(
AnotherProvidersComp,
[provide(HeroService, {useValue: {}})]
)
.createAsync(AnotherProvidersComp);
}));
});

View File

@ -1,6 +1,7 @@
// Based on https://github.com/angular/angular/blob/master/modules/angular2/test/testing/testing_public_spec.ts
import { Component, Injectable } from 'angular2/core';
import { NgIf } from 'angular2/common';
/* tslint:disable:forin */
import { Component, EventEmitter, Injectable, Input, Output,
OnInit, OnChanges, OnDestroy, SimpleChange } from 'angular2/core';
// Let TypeScript know about the special SystemJS __moduleName variable
declare var __moduleName: string;
@ -39,6 +40,13 @@ export class ButtonComp {
clicked() { this.wasClicked = true; }
}
@Component({
selector: 'input-comp',
template: `<input [(ngModel)]="name">`
})
export class InputComp {
name = 'John';
}
@Component({
selector: 'child-comp',
@ -66,14 +74,12 @@ export class ParentComp { }
@Component({
selector: 'my-if-comp',
template: `MyIf(<span *ngIf="showMore">More</span>)`,
directives: [NgIf]
template: `MyIf(<span *ngIf="showMore">More</span>)`
})
export class MyIfComp {
showMore: boolean = false;
showMore = false;
}
@Component({
selector: 'child-child-comp',
template: '<span>ChildChild</span>'
@ -121,7 +127,7 @@ export class TestViewProvidersComp {
@Component({
moduleId: __moduleName,
selector: 'external-template-comp',
templateUrl: 'public-external-template.html'
templateUrl: 'bag-external-template.html'
})
export class ExternalTemplateComp { }
@ -131,3 +137,88 @@ export class ExternalTemplateComp { }
templateUrl: 'non-existant.html'
})
export class BadTemplateUrl { }
///////// 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="#log of changeLog; #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';
}
}

View File

@ -0,0 +1,171 @@
/* tslint:disable:no-unused-variable */
import { DashboardComponent } from './dashboard.component';
import { By } from 'angular2/platform/browser';
import { provide } from 'angular2/core';
import {
beforeEach, beforeEachProviders,
describe, ddescribe, xdescribe,
expect, it, iit, xit,
inject, injectAsync,
TestComponentBuilder
} from 'angular2/testing';
import { Hero, HeroService, MockHeroService } from './mock-hero.service';
import { Router, MockRouter } from './mock-router';
interface Done {
(): void;
fail: (err: any) => void;
}
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: Done) => {
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;
beforeEachProviders(() => {
mockHeroService = new MockHeroService();
return [
provide(Router, {useClass: MockRouter}),
provide(MockRouter, {useExisting: Router}),
provide(HeroService, {useValue: mockHeroService})
];
});
it('can instantiate it',
injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => {
return tcb.createAsync(DashboardComponent);
}));
it('should NOT have heroes before OnInit',
injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => {
return 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',
injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => {
return 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',
injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => {
return tcb.createAsync(DashboardComponent).then(fixture => {
comp = fixture.debugElement.componentInstance;
fixture.detectChanges(); // runs ngOnInit -> getHeroes
return 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',
injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => {
return tcb.createAsync(DashboardComponent).then(fixture => {
comp = fixture.debugElement.componentInstance;
fixture.detectChanges(); // runs ngOnInit -> getHeroes
return 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)
.toHaveText(mockHeroService.mockHeroes[4].name);
});
});
}));
it('should tell ROUTER to navigate by hero id',
injectAsync([TestComponentBuilder, Router],
(tcb: TestComponentBuilder, router: MockRouter) => {
let spy = spyOn(router, 'navigate').and.callThrough();
return tcb.createAsync(DashboardComponent).then(fixture => {
let hero: Hero = {id: 42, name: 'Abbracadabra' };
comp = fixture.debugElement.componentInstance;
comp.gotoDetail(hero);
let linkParams = spy.calls.mostRecent().args[0];
expect(linkParams[0]).toEqual('HeroDetail', 'should nav to "HeroDetail"');
expect(linkParams[1].id).toEqual(hero.id, 'should nav to fake hero\'s id');
});
}));
});
});

View File

@ -31,7 +31,7 @@ export class DashboardComponent implements OnInit {
ngOnInit() {
this._heroService.getHeroes()
.then(heroes => this.heroes = heroes.slice(1,5));
.then(heroes => this.heroes = heroes.slice(1, 5));
}
// #docregion goto-detail
@ -41,4 +41,4 @@ export class DashboardComponent implements OnInit {
}
// #enddocregion goto-detail
}
// #enddocregion
// #enddocregion

View File

@ -1,19 +1,9 @@
/* tslint:disable:no-unused-variable */
import {
it,
iit,
xit,
describe,
ddescribe,
xdescribe,
expect,
fakeAsync,
tick,
beforeEach,
inject,
injectAsync,
withProviders,
beforeEachProviders
beforeEach, beforeEachProviders, withProviders,
describe, ddescribe, xdescribe,
expect, it, iit, xit,
inject, injectAsync, fakeAsync, TestComponentBuilder, tick
} from 'angular2/testing';
import { provide } from 'angular2/core';
@ -23,17 +13,12 @@ import {
MockConnection } from 'angular2/src/http/backends/mock_backend';
import {
BaseRequestOptions,
ConnectionBackend,
Request,
RequestMethod,
RequestOptions,
Response,
ResponseOptions,
URLSearchParams,
HTTP_PROVIDERS,
XHRBackend,
Http} from 'angular2/http';
Http, HTTP_PROVIDERS,
ConnectionBackend, XHRBackend,
Request, RequestMethod, BaseRequestOptions, RequestOptions,
Response, ResponseOptions,
URLSearchParams
} from 'angular2/http';
// Add all operators to Observable
import 'rxjs/Rx';
@ -45,10 +30,10 @@ import { HeroService } from './http-hero.service';
type HeroData = {id: string, name: string}
const makeHeroData = () => [
{ "id": "1", "name": "Windstorm" },
{ "id": "2", "name": "Bombasto" },
{ "id": "3", "name": "Magneta" },
{ "id": "4", "name": "Tornado" }
{ id: '1', name: 'Windstorm' },
{ id: '2', name: 'Bombasto' },
{ id: '3', name: 'Magneta' },
{ id: '4', name: 'Tornado' }
];
// HeroService expects response data like {data: {the-data}}

View File

@ -1,4 +1,6 @@
import { bootstrap } from 'angular2/platform/browser';
import { AppComponent } from './app.component';
import { bootstrap } from 'angular2/platform/browser';
import { AppComponent } from './app.component';
import { MyIfParentComp } from './bag';
bootstrap(AppComponent);
bootstrap(AppComponent);
bootstrap(MyIfParentComp);

View File

@ -0,0 +1,24 @@
export { Hero } from './hero';
export { HeroService } from './hero.service';
import { HEROES } from './mock-heroes';
import { Hero } from './hero';
import { HeroService } from './hero.service';
import { PromiseWrapper } from 'angular2/src/facade/promise';
export class MockHeroService implements HeroService {
mockHeroes = HEROES.slice();
lastPromise: Promise<any>; // so we can spy on promise calls
getHero(id: number) {
return this.lastPromise = PromiseWrapper.resolve(this.mockHeroes[0]);
}
getHeroes() {
return this.lastPromise = PromiseWrapper.resolve<Hero[]>(this.mockHeroes);
}
getHeroesSlowly() { return this.getHeroes(); }
}

View File

@ -2,15 +2,15 @@
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"}
{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
// #enddocregion

View File

@ -0,0 +1,219 @@
export * from 'angular2/router';
import { Directive, DynamicComponentLoader, ElementRef,
Injectable, Optional, Input } from 'angular2/core';
import { PromiseWrapper } from 'angular2/src/facade/promise';
import { isString} from 'angular2/src/facade/lang';
import { ComponentInstruction, Instruction,
Router, RouterOutlet} from 'angular2/router';
let _resolveToTrue = PromiseWrapper.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 (!isString(this.target) || this.target === '_self') {
this.navigatedTo = this.routeParams;
return false;
}
return true;
}
}
@Directive({selector: 'router-outlet'})
export class MockRouterOutlet extends RouterOutlet {
name: string = null;
constructor(
_elementRef: ElementRef,
@Optional() _loader: DynamicComponentLoader,
_parentRouter: Router,
nameAttr: string) {
super(_elementRef, _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 PromiseWrapper.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 PromiseWrapper.resolve(url);
}
/**
* Navigate via the provided instruction. Returns a promise that resolves when navigation is
* complete.
*/
navigateByInstruction(instruction: Instruction, _skipLocationChange = false): Promise<any> {
return PromiseWrapper.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 PromiseWrapper.resolve(this.mockRecognizedInstruction);
}
registerPrimaryOutlet(outlet: RouterOutlet): Promise<any> {
this.outlet = outlet;
return super.registerPrimaryOutlet(outlet);
}
unregisterPrimaryOutlet(outlet: RouterOutlet) {
super.unregisterPrimaryOutlet(outlet);
this.outlet = null;
}
}
class MockChildRouter extends MockRouter {
constructor(parent: MockRouter, hostComponent: any) {
super();
this.parent = parent;
}
navigateByUrl(url: string, _skipLocationChange = false): Promise<any> {
// Delegate navigation to the root router
return this.parent.navigateByUrl(url, _skipLocationChange);
}
navigateByInstruction(instruction: Instruction, _skipLocationChange = false):
Promise<any> {
// Delegate navigation to the root router
return this.parent.navigateByInstruction(instruction, _skipLocationChange);
}
}

View File

@ -4,7 +4,7 @@
import { MyUppercasePipe } from './my-uppercase.pipe';
describe('MyUppercasePipe', () => {
let pipe : MyUppercasePipe;
let pipe: MyUppercasePipe;
beforeEach(() => {
pipe = new MyUppercasePipe();

View File

@ -15,7 +15,6 @@
<!-- #docregion head -->
<!-- IE required polyfills, in this exact order -->
<script src="node_modules/es6-shim/es6-shim.min.js"></script>
<script src="node_modules/systemjs/dist/system-polyfills.js"></script>
<script src="node_modules/angular2/es6/dev/src/testing/shims_for_IE.js"></script>
<script src="node_modules/angular2/bundles/angular2-polyfills.js"></script>
@ -35,5 +34,7 @@
<body>
<my-app>Loading...</my-app>
<hr>
<my-if-parent-comp>Loading MyIfParentComp ...</my-if-parent-comp>
</body>
</html>

View File

@ -3,7 +3,7 @@
<html>
<head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<title>Angular Public Unit Tests</title>
<title>Bag of Unit Tests</title>
<link rel="stylesheet" href="node_modules/jasmine-core/lib/jasmine-core/jasmine.css">
<script src="node_modules/jasmine-core/lib/jasmine-core/jasmine.js"></script>
@ -24,7 +24,7 @@
<script src="node_modules/angular2/bundles/testing.dev.js"></script>
<script>
var __spec_files__ = ['app/public.spec'];
var __spec_files__ = ['app/bag.spec'];
</script>
<script src="test-shim.js"></script>
</body>

View File

@ -0,0 +1,83 @@
// Configuration for the Wallaby Visual Studio Code testing extension
// https://marketplace.visualstudio.com/items?itemName=WallabyJs.wallaby-vscode
// Note: Wallaby is not open source and costs money
module.exports = function () {
return {
files: [
{pattern: 'node_modules/es6-shim/es6-shim.js', instrument: false},
{pattern: 'node_modules/systemjs/dist/system-polyfills.js', instrument: false},
{pattern: 'node_modules/systemjs/dist/system.js', instrument: false},
{pattern: 'node_modules/reflect-metadata/Reflect.js', instrument: false},
{pattern: 'node_modules/zone.js/dist/zone.js', instrument: false},
{pattern: 'node_modules/zone.js/dist/long-stack-trace-zone.js', instrument: false},
{pattern: 'node_modules/zone.js/dist/jasmine-patch.js', instrument: false},
{pattern: 'app/**/*+(ts|html|css)', load: false},
{pattern: 'app/**/*.spec.ts', ignore: true}
],
tests: [
{pattern: 'app/**/*.spec.ts', load: false}
],
middleware: function (app, express) {
app.use('/node_modules', express.static(require('path').join(__dirname, 'node_modules')));
},
testFramework: 'jasmine',
bootstrap: function (wallaby) {
wallaby.delayStart();
System.config({
defaultJSExtensions: true,
packages: {
app: {
meta: {
'*': {
scriptLoad: true
}
}
}
},
paths: {
'npm:*': 'node_modules/*'
},
map: {
'angular2': 'npm:angular2',
'rxjs': 'npm:rxjs'
}
});
// Configure Angular for the browser and
// with test versions of the platform providers
Promise.all([
System.import('angular2/testing'),
System.import('angular2/platform/testing/browser')
])
.then(function (results) {
var testing = results[0];
var browser = results[1];
testing.setBaseTestProviders(
browser.TEST_BROWSER_PLATFORM_PROVIDERS,
browser.TEST_BROWSER_APPLICATION_PROVIDERS);
// Load all spec files
return Promise.all(wallaby.tests.map(function (specFile) {
return System.import(specFile);
}));
})
.then(function () {
wallaby.start();
})
.catch(function (e) {
setTimeout(function () {
throw e;
}, 0);
});
},
debug: true
};
};