From e1862887ae853422dc5631d1520f7990ff4fbd13 Mon Sep 17 00:00:00 2001 From: Ward Bell Date: Thu, 14 Apr 2016 10:36:38 -0700 Subject: [PATCH] docs(testing): more testing samples and infrastructure setup added wallaby config; revised karma-test-shim --- gulpfile.js | 3 +- public/docs/_examples/.gitignore | 10 +- .../_examples/displaying-data/e2e-spec.js | 8 +- .../displaying-data/ts/app/app.component.ts | 2 +- public/docs/_examples/karma-test-shim.js | 43 ++- public/docs/_examples/karma.conf.js | 4 +- .../ts/app/on-changes.component.ts | 15 +- public/docs/_examples/package.json | 17 +- public/docs/_examples/protractor.config.js | 23 +- .../_examples/quickstart/ts/package.1.json | 6 +- .../testing/ts/app/app.component.spec.ts | 84 ++++++ .../_examples/testing/ts/app/app.component.ts | 45 +--- ...mplate.html => bag-external-template.html} | 0 .../ts/app/{public.spec.ts => bag.spec.ts} | 246 ++++++++++++++++-- .../testing/ts/app/{public.ts => bag.ts} | 105 +++++++- .../ts/app/dashboard.component.spec.ts | 171 ++++++++++++ .../testing/ts/app/dashboard.component.ts | 4 +- .../testing/ts/app/http-hero.service.spec.ts | 43 +-- public/docs/_examples/testing/ts/app/main.ts | 8 +- .../testing/ts/app/mock-hero.service.ts | 24 ++ .../_examples/testing/ts/app/mock-heroes.ts | 22 +- .../_examples/testing/ts/app/mock-router.ts | 219 ++++++++++++++++ .../testing/ts/app/my-uppercase.pipe.spec.ts | 2 +- public/docs/_examples/testing/ts/index.html | 3 +- ...-tests-public.html => unit-tests-bag.html} | 4 +- public/docs/_examples/wallaby.js | 83 ++++++ 26 files changed, 1019 insertions(+), 175 deletions(-) create mode 100644 public/docs/_examples/testing/ts/app/app.component.spec.ts rename public/docs/_examples/testing/ts/app/{public-external-template.html => bag-external-template.html} (100%) rename public/docs/_examples/testing/ts/app/{public.spec.ts => bag.spec.ts} (66%) rename public/docs/_examples/testing/ts/app/{public.ts => bag.ts} (50%) create mode 100644 public/docs/_examples/testing/ts/app/dashboard.component.spec.ts create mode 100644 public/docs/_examples/testing/ts/app/mock-hero.service.ts create mode 100644 public/docs/_examples/testing/ts/app/mock-router.ts rename public/docs/_examples/testing/ts/{unit-tests-public.html => unit-tests-bag.html} (92%) create mode 100644 public/docs/_examples/wallaby.js diff --git a/gulpfile.js b/gulpfile.js index 44baa44792..50ca975a39 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -76,7 +76,8 @@ var _exampleBoilerplateFiles = [ 'styles.css', 'tsconfig.json', 'tslint.json', - 'typings.json' + 'typings.json', + 'wallaby.js' ]; var _exampleDartWebBoilerPlateFiles = ['styles.css']; diff --git a/public/docs/_examples/.gitignore b/public/docs/_examples/.gitignore index a574567c52..07614a46ec 100644 --- a/public/docs/_examples/.gitignore +++ b/public/docs/_examples/.gitignore @@ -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 diff --git a/public/docs/_examples/displaying-data/e2e-spec.js b/public/docs/_examples/displaying-data/e2e-spec.js index 4768050501..0f01977bb1 100644 --- a/public/docs/_examples/displaying-data/e2e-spec.js +++ b/public/docs/_examples/displaying-data/e2e-spec.js @@ -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!'); }); }); diff --git a/public/docs/_examples/displaying-data/ts/app/app.component.ts b/public/docs/_examples/displaying-data/ts/app/app.component.ts index a85a09081e..ace957d9b4 100644 --- a/public/docs/_examples/displaying-data/ts/app/app.component.ts +++ b/public/docs/_examples/displaying-data/ts/app/app.component.ts @@ -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', diff --git a/public/docs/_examples/karma-test-shim.js b/public/docs/_examples/karma-test-shim.js index 932744bc69..e2aff07852 100644 --- a/public/docs/_examples/karma-test-shim.js +++ b/public/docs/_examples/karma-test-shim.js @@ -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 ////// diff --git a/public/docs/_examples/karma.conf.js b/public/docs/_examples/karma.conf.js index e128ca2fb3..0b36ed93c5 100644 --- a/public/docs/_examples/karma.conf.js +++ b/public/docs/_examples/karma.conf.js @@ -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: '', diff --git a/public/docs/_examples/lifecycle-hooks/ts/app/on-changes.component.ts b/public/docs/_examples/lifecycle-hooks/ts/app/on-changes.component.ts index dd6c7bddbe..6fdae1269b 100644 --- a/public/docs/_examples/lifecycle-hooks/ts/app/on-changes.component.ts +++ b/public/docs/_examples/lifecycle-hooks/ts/app/on-changes.component.ts @@ -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(); } } } diff --git a/public/docs/_examples/package.json b/public/docs/_examples/package.json index 4a8db4dcb8..fd16231de6 100644 --- a/public/docs/_examples/package.json +++ b/public/docs/_examples/package.json @@ -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" }, diff --git a/public/docs/_examples/protractor.config.js b/public/docs/_examples/protractor.config.js index 18406b3682..a48114444a 100644 --- a/public/docs/_examples/protractor.config.js +++ b/public/docs/_examples/protractor.config.js @@ -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; diff --git a/public/docs/_examples/quickstart/ts/package.1.json b/public/docs/_examples/quickstart/ts/package.1.json index bff084cb9b..cd26500603 100644 --- a/public/docs/_examples/quickstart/ts/package.1.json +++ b/public/docs/_examples/quickstart/ts/package.1.json @@ -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": { diff --git a/public/docs/_examples/testing/ts/app/app.component.spec.ts b/public/docs/_examples/testing/ts/app/app.component.spec.ts new file mode 100644 index 0000000000..caa77ad66e --- /dev/null +++ b/public/docs/_examples/testing/ts/app/app.component.spec.ts @@ -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 => 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 = heroesDe.componentInstance; + expect(link.navigatedTo).toBeNull('link should not have navigate yet'); + + heroesDe.triggerEventHandler('click', null); + + fixture.detectChanges(); + expect(link.navigatedTo[0]).toEqual('Heroes'); + + }); +}); + diff --git a/public/docs/_examples/testing/ts/app/app.component.ts b/public/docs/_examples/testing/ts/app/app.component.ts index df9985b5ee..9210f80650 100644 --- a/public/docs/_examples/testing/ts/app/app.component.ts +++ b/public/docs/_examples/testing/ts/app/app.component.ts @@ -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: `

{{title}}

`, -// #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 diff --git a/public/docs/_examples/testing/ts/app/public-external-template.html b/public/docs/_examples/testing/ts/app/bag-external-template.html similarity index 100% rename from public/docs/_examples/testing/ts/app/public-external-template.html rename to public/docs/_examples/testing/ts/app/bag-external-template.html diff --git a/public/docs/_examples/testing/ts/app/public.spec.ts b/public/docs/_examples/testing/ts/app/bag.spec.ts similarity index 66% rename from public/docs/_examples/testing/ts/app/public.spec.ts rename to public/docs/_examples/testing/ts/app/bag.spec.ts index 1ccd8c89f4..f77e2c9703 100644 --- a/public/docs/_examples/testing/ts/app/public.spec.ts +++ b/public/docs/_examples/testing/ts/app/bag.spec.ts @@ -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 = 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 = fixture.componentInstance; + expect(comp.name).toEqual(origName, `At start name should be ${origName} `); + + let inputBox = fixture.debugElement.query(By.css('input')).nativeElement; + fixture.detectChanges(); + expect(inputBox.value).toEqual(origName, `At start input box value should be ${origName} `); + + inputBox.value = newName; + expect(comp.name).toEqual(origName, + `Name should still be ${origName} after value change, before detectChanges`); + + fixture.detectChanges(); + expect(inputBox.value).toEqual(newName, + `After value change and detectChanges, name should now be ${newName} `); + }); + })); + it('should override a template', 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(); }; (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(); }; (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); + })); +}); diff --git a/public/docs/_examples/testing/ts/app/public.ts b/public/docs/_examples/testing/ts/app/bag.ts similarity index 50% rename from public/docs/_examples/testing/ts/app/public.ts rename to public/docs/_examples/testing/ts/app/bag.ts index 382c2c700e..1b861d1223 100644 --- a/public/docs/_examples/testing/ts/app/public.ts +++ b/public/docs/_examples/testing/ts/app/bag.ts @@ -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: `` +}) +export class InputComp { + name = 'John'; +} @Component({ selector: 'child-comp', @@ -66,14 +74,12 @@ export class ParentComp { } @Component({ selector: 'my-if-comp', - template: `MyIf(More)`, - directives: [NgIf] + template: `MyIf(More)` }) export class MyIfComp { - showMore: boolean = false; + showMore = false; } - @Component({ selector: 'child-child-comp', template: 'ChildChild' @@ -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: ` +

MyIfChildComp

+
+ +
+

Change log:

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

MyIfParentComp

+ +
+
+ +
+ `, + directives: [MyIfChildComp] +}) +export class MyIfParentComp implements OnInit { + ngOnInitCalled = false; + parentValue = 'Hello, World'; + showChild = false; + toggleLabel = 'Unknown'; + + ngOnInit() { + this.ngOnInitCalled = true; + this.clicked(); + } + + clicked() { + this.showChild = !this.showChild; + this.toggleLabel = this.showChild ? 'Close' : 'Show'; + } +} diff --git a/public/docs/_examples/testing/ts/app/dashboard.component.spec.ts b/public/docs/_examples/testing/ts/app/dashboard.component.spec.ts new file mode 100644 index 0000000000..134c2ae406 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/dashboard.component.spec.ts @@ -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'); + + }); + })); + }); +}); diff --git a/public/docs/_examples/testing/ts/app/dashboard.component.ts b/public/docs/_examples/testing/ts/app/dashboard.component.ts index c4c516a864..95b7be50d2 100644 --- a/public/docs/_examples/testing/ts/app/dashboard.component.ts +++ b/public/docs/_examples/testing/ts/app/dashboard.component.ts @@ -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 \ No newline at end of file +// #enddocregion diff --git a/public/docs/_examples/testing/ts/app/http-hero.service.spec.ts b/public/docs/_examples/testing/ts/app/http-hero.service.spec.ts index f5d61b4955..7a80c5022a 100644 --- a/public/docs/_examples/testing/ts/app/http-hero.service.spec.ts +++ b/public/docs/_examples/testing/ts/app/http-hero.service.spec.ts @@ -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}} diff --git a/public/docs/_examples/testing/ts/app/main.ts b/public/docs/_examples/testing/ts/app/main.ts index c469e18fd0..0b7adf17c7 100644 --- a/public/docs/_examples/testing/ts/app/main.ts +++ b/public/docs/_examples/testing/ts/app/main.ts @@ -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); \ No newline at end of file +bootstrap(AppComponent); +bootstrap(MyIfParentComp); diff --git a/public/docs/_examples/testing/ts/app/mock-hero.service.ts b/public/docs/_examples/testing/ts/app/mock-hero.service.ts new file mode 100644 index 0000000000..4fcd42118a --- /dev/null +++ b/public/docs/_examples/testing/ts/app/mock-hero.service.ts @@ -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; // so we can spy on promise calls + + getHero(id: number) { + return this.lastPromise = PromiseWrapper.resolve(this.mockHeroes[0]); + } + + getHeroes() { + return this.lastPromise = PromiseWrapper.resolve(this.mockHeroes); + } + + getHeroesSlowly() { return this.getHeroes(); } +} diff --git a/public/docs/_examples/testing/ts/app/mock-heroes.ts b/public/docs/_examples/testing/ts/app/mock-heroes.ts index cdcba35097..ddd36d7868 100644 --- a/public/docs/_examples/testing/ts/app/mock-heroes.ts +++ b/public/docs/_examples/testing/ts/app/mock-heroes.ts @@ -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 \ No newline at end of file +// #enddocregion diff --git a/public/docs/_examples/testing/ts/app/mock-router.ts b/public/docs/_examples/testing/ts/app/mock-router.ts new file mode 100644 index 0000000000..3370ab0b4f --- /dev/null +++ b/public/docs/_examples/testing/ts/app/mock-router.ts @@ -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 { NOT_IMPLEMENTED('activate'); return _resolveToTrue; } + + /** + * Called by the {@link Router} during the commit phase of a navigation when an outlet + * reuses a component between different routes. + * This method in turn is responsible for calling the `routerOnReuse` hook of its child. + */ + reuse(nextInstruction: ComponentInstruction): Promise { NOT_IMPLEMENTED('reuse'); return _resolveToTrue; } + + /** + * Called by the {@link Router} when an outlet disposes of a component's contents. + * This method in turn is responsible for calling the `routerOnDeactivate` hook of its child. + */ + deactivate(nextInstruction: ComponentInstruction): Promise { NOT_IMPLEMENTED('deactivate'); return _resolveToTrue; } + + /** + * Called by the {@link Router} during recognition phase of a navigation. + * + * If this resolves to `false`, the given navigation is cancelled. + * + * This method delegates to the child component's `routerCanDeactivate` hook if it exists, + * and otherwise resolves to true. + */ + routerCanDeactivate(nextInstruction: ComponentInstruction): Promise { + NOT_IMPLEMENTED('routerCanDeactivate'); return _resolveToTrue; + } + + /** + * Called by the {@link Router} during recognition phase of a navigation. + * + * If the new child component has a different Type than the existing child component, + * this will resolve to `false`. You can't reuse an old component when the new component + * is of a different Type. + * + * Otherwise, this method delegates to the child component's `routerCanReuse` hook if it exists, + * or resolves to true if the hook is not present. + */ + routerCanReuse(nextInstruction: ComponentInstruction): Promise { NOT_IMPLEMENTED('routerCanReuse'); return _resolveToTrue; } + +} + +@Injectable() +export class MockRouter extends Router { + + mockIsRouteActive = false; + mockRecognizedInstruction: Instruction; + outlet: RouterOutlet = null; + + constructor() { + super(null, null, null, null); + } + + auxRouter(hostComponent: any): Router { return new MockChildRouter(this, hostComponent); } + childRouter(hostComponent: any): Router { return new MockChildRouter(this, hostComponent); } + + commit(instruction: Instruction, _skipLocationChange = false): Promise { + NOT_IMPLEMENTED('commit'); return _resolveToTrue; + } + + deactivate(instruction: Instruction, _skipLocationChange = false): Promise { + NOT_IMPLEMENTED('deactivate'); return _resolveToTrue; + } + + /** + * Generate an `Instruction` based on the provided Route Link DSL. + */ + generate(linkParams: any[]): Instruction { + NOT_IMPLEMENTED('generate'); return null; + } + + isRouteActive(instruction: Instruction): boolean { return this.mockIsRouteActive; } + + /** + * Navigate based on the provided Route Link DSL. It's preferred to navigate with this method + * over `navigateByUrl`. + * + * ### Usage + * + * This method takes an array representing the Route Link DSL: + * ``` + * ['./MyCmp', {param: 3}] + * ``` + * See the {@link RouterLink} directive for more. + */ + navigate(linkParams: any[]): Promise { + return 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 { + return PromiseWrapper.resolve(url); + } + + + /** + * Navigate via the provided instruction. Returns a promise that resolves when navigation is + * complete. + */ + navigateByInstruction(instruction: Instruction, _skipLocationChange = false): Promise { + 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 { + return PromiseWrapper.resolve(this.mockRecognizedInstruction); + } + + registerPrimaryOutlet(outlet: RouterOutlet): Promise { + this.outlet = outlet; + return super.registerPrimaryOutlet(outlet); + } + + unregisterPrimaryOutlet(outlet: RouterOutlet) { + super.unregisterPrimaryOutlet(outlet); + this.outlet = null; + } +} + +class MockChildRouter extends MockRouter { + constructor(parent: MockRouter, hostComponent: any) { + super(); + this.parent = parent; + } + + + navigateByUrl(url: string, _skipLocationChange = false): Promise { + // Delegate navigation to the root router + return this.parent.navigateByUrl(url, _skipLocationChange); + } + + navigateByInstruction(instruction: Instruction, _skipLocationChange = false): + Promise { + // Delegate navigation to the root router + return this.parent.navigateByInstruction(instruction, _skipLocationChange); + } +} diff --git a/public/docs/_examples/testing/ts/app/my-uppercase.pipe.spec.ts b/public/docs/_examples/testing/ts/app/my-uppercase.pipe.spec.ts index fc9952f6b2..731b2ed965 100644 --- a/public/docs/_examples/testing/ts/app/my-uppercase.pipe.spec.ts +++ b/public/docs/_examples/testing/ts/app/my-uppercase.pipe.spec.ts @@ -4,7 +4,7 @@ import { MyUppercasePipe } from './my-uppercase.pipe'; describe('MyUppercasePipe', () => { - let pipe : MyUppercasePipe; + let pipe: MyUppercasePipe; beforeEach(() => { pipe = new MyUppercasePipe(); diff --git a/public/docs/_examples/testing/ts/index.html b/public/docs/_examples/testing/ts/index.html index 0e91f05760..0f78ff7173 100644 --- a/public/docs/_examples/testing/ts/index.html +++ b/public/docs/_examples/testing/ts/index.html @@ -15,7 +15,6 @@ - @@ -35,5 +34,7 @@ Loading... +
+ Loading MyIfParentComp ... diff --git a/public/docs/_examples/testing/ts/unit-tests-public.html b/public/docs/_examples/testing/ts/unit-tests-bag.html similarity index 92% rename from public/docs/_examples/testing/ts/unit-tests-public.html rename to public/docs/_examples/testing/ts/unit-tests-bag.html index cbb3f7f859..5fea6bcbb1 100644 --- a/public/docs/_examples/testing/ts/unit-tests-public.html +++ b/public/docs/_examples/testing/ts/unit-tests-bag.html @@ -3,7 +3,7 @@ - Angular Public Unit Tests + Bag of Unit Tests @@ -24,7 +24,7 @@ diff --git a/public/docs/_examples/wallaby.js b/public/docs/_examples/wallaby.js new file mode 100644 index 0000000000..6ec91bf5bf --- /dev/null +++ b/public/docs/_examples/wallaby.js @@ -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 + }; +};