diff --git a/public/docs/_examples/.gitignore b/public/docs/_examples/.gitignore index d821c82bea..a574567c52 100644 --- a/public/docs/_examples/.gitignore +++ b/public/docs/_examples/.gitignore @@ -10,3 +10,4 @@ tsconfig.json tslint.json npm-debug*. **/protractor.config.js +_test-output diff --git a/public/docs/_examples/karma-test-shim.js b/public/docs/_examples/karma-test-shim.js index 8ccb27a727..932744bc69 100644 --- a/public/docs/_examples/karma-test-shim.js +++ b/public/docs/_examples/karma-test-shim.js @@ -1,68 +1,91 @@ -// Tun on full stack traces in errors to help debugging -Error.stackTraceLimit=Infinity; +/*global jasmine, __karma__, window*/ +(function () { +// Error.stackTraceLimit = Infinity; jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000; -// // Cancel Karma's synchronous start, -// // we will call `__karma__.start()` later, once all the specs are loaded. -__karma__.loaded = function() {}; +// Cancel Karma's synchronous start, +// we call `__karma__.start()` later, once all the specs are loaded. +__karma__.loaded = function () { }; +// SET THE RUNTIME APPLICATION ROOT HERE +var appRoot ='app'; // no trailing slash! -System.config({ - packages: { - 'base/app': { - defaultExtension: false, - // removed because of issues with raw .js files not being found. - // format: 'register', - map: Object.keys(window.__karma__.files). - filter(onlyAppFiles). - reduce(function createPathRecords(pathsMapping, appPath) { - // creates local module name mapping to global path with karma's fingerprint in path, e.g.: - // './hero.service': '/base/src/app/hero.service.js?f4523daf879cfb7310ef6242682ccf10b2041b3e' - var moduleName = appPath.replace(/^\/base\/app\//, './').replace(/\.js$/, ''); - pathsMapping[moduleName] = appPath + '?' + window.__karma__.files[appPath] - return pathsMapping; - }, {}) +// RegExp for client application base path within karma (which always starts 'base\') +var karmaBase = '^\/base\/'; // RegEx string for base of karma folders +var appPackage = 'base/' + appRoot; //e.g., base/app +var appRootRe = new RegExp(karmaBase + appRoot + '\/'); +var onlyAppFilesRe = new RegExp(karmaBase + appRoot + '\/(?!.*\.spec\.js$)([a-z0-9-_\.\/]+)\.js$'); - } - } -}); +var moduleNames = []; -// old code from angular 44 -// System.import('angular2/src/core/dom/browser_adapter').then(function(browser_adapter) { -// new path for angular 51 -System.import('angular2/src/platform/browser/browser_adapter').then(function(browser_adapter) { - browser_adapter.BrowserDomAdapter.makeCurrent(); -}).then(function() { +// Configure systemjs packages to use the .js extension for imports from the app folder +var packages = {}; +packages[appPackage] = { + defaultExtension: false, + format: 'register', + map: Object.keys(window.__karma__.files) + .filter(onlyAppFiles) + // Create local module name mapping to karma file path for app files + // with karma's fingerprint in query string, e.g.: + // './hero.service': '/base/app/hero.service.js?f4523daf879cfb7310ef6242682ccf10b2041b3e' + .reduce(function (pathsMapping, appPath) { + var moduleName = appPath.replace(appRootRe, './').replace(/\.js$/, ''); + pathsMapping[moduleName] = appPath + '?' + window.__karma__.files[appPath]; + return pathsMapping; + }, {}) + } + +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 + ); + }); + }) + +// Load all spec files +// (e.g. 'base/app/hero.service.spec.js') +.then(function () { return Promise.all( - Object.keys(window.__karma__.files) // All files served by Karma. - .filter(onlySpecFiles) - // .map(filePath2moduleName) // Normalize paths to module names. - .map(function(moduleName) { - // loads all spec files via their global module names (e.g. 'base/src/app/hero.service.spec') - return System.import(moduleName); - })); + Object.keys(window.__karma__.files) + .filter(onlySpecFiles) + .map(function (moduleName) { + moduleNames.push(moduleName); + return System.import(moduleName); + })); }) -.then(function() { - __karma__.start(); -}, function(error) { - __karma__.error(error.stack || error); -}); +.then(success, fail); -function filePath2moduleName(filePath) { - return filePath. - replace(/^\//, ''). // remove / prefix - replace(/\.\w+$/, ''); // remove suffix -} - +////// Helpers ////// function onlyAppFiles(filePath) { - return /^\/base\/app\/.*\.js$/.test(filePath) && !onlySpecFiles(filePath); + return onlyAppFilesRe.test(filePath); } - function onlySpecFiles(filePath) { return /\.spec\.js$/.test(filePath); } + +function success () { + console.log( + 'Spec files loaded:\n ' + + moduleNames.join('\n ') + + '\nStarting Jasmine testrunner'); + __karma__.start(); +} + +function fail(error) { + __karma__.error(error.stack || error); +} + +})(); diff --git a/public/docs/_examples/karma.conf.js b/public/docs/_examples/karma.conf.js index 5b07bee380..e128ca2fb3 100644 --- a/public/docs/_examples/karma.conf.js +++ b/public/docs/_examples/karma.conf.js @@ -1,43 +1,82 @@ module.exports = function(config) { + + var appBase = 'app/'; // transpiled app JS files + var appAssets ='base/app/'; // component assets fetched by Angular's compiler + config.set({ - basePath: '', - frameworks: ['jasmine'], - - files: [ - // paths loaded by Karma - {pattern: 'node_modules/systemjs/dist/system.src.js', included: true, watched: true}, - {pattern: 'node_modules/angular2/bundles/angular2.js', included: true, watched: true}, - {pattern: 'node_modules/angular2/bundles/testing.js', included: true, watched: true}, - {pattern: 'karma-test-shim.js', included: true, watched: true}, - {pattern: 'app/test/*.js', included: true, watched: true}, - - // paths loaded via module imports - {pattern: 'app/**/*.js', included: false, watched: true}, - - // paths loaded via Angular's component compiler - // (these paths need to be rewritten, see proxies section) - {pattern: 'app/**/*.html', included: false, watched: true}, - {pattern: 'app/**/*.css', included: false, watched: true}, - - // paths to support debugging with source maps in dev tools - {pattern: 'app/**/*.ts', included: false, watched: false}, - {pattern: 'app/**/*.js.map', included: false, watched: false} + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-htmlfile-reporter') ], - // proxied base paths - proxies: { - // required for component assests fetched by Angular's compiler - "/app/": "/base/app/" + customLaunchers: { + // From the CLI. Not used here but interesting + // chrome setup for travis CI using chromium + Chrome_travis_ci: { + base: 'Chrome', + flags: ['--no-sandbox'] + } }, - reporters: ['progress'], - port: 9877, + files: [ + // Angular and shim libraries loaded by Karma + { pattern: 'node_modules/systemjs/dist/system-polyfills.js', included: true, watched: true }, + { pattern: 'node_modules/systemjs/dist/system.src.js', included: true, watched: true }, + { pattern: 'node_modules/es6-shim/es6-shim.js', included: true, watched: true }, + { pattern: 'node_modules/angular2/bundles/angular2-polyfills.js', included: true, watched: true }, + { pattern: 'node_modules/rxjs/bundles/Rx.js', included: true, watched: true }, + { pattern: 'node_modules/angular2/bundles/angular2.js', included: true, watched: true }, + { pattern: 'node_modules/angular2/bundles/testing.dev.js', included: true, watched: true }, + + // External libraries loaded by Karma + { pattern: 'node_modules/angular2/bundles/http.dev.js', included: true, watched: true }, + { pattern: 'node_modules/angular2/bundles/router.dev.js', included: true, watched: true }, + { pattern: 'node_modules/a2-in-memory-web-api/web-api.js', included: true, watched: true }, + + // Configures module loader w/ app and specs, then launch karma + { pattern: 'karma-test-shim.js', included: true, watched: true }, + + // transpiled application & spec code paths loaded via module imports + {pattern: appBase + '**/*.js', included: false, watched: true}, + + // asset (HTML & CSS) paths loaded via Angular's component compiler + // (these paths need to be rewritten, see proxies section) + {pattern: appBase + '**/*.html', included: false, watched: true}, + {pattern: appBase + '**/*.css', included: false, watched: true}, + + // paths for debugging with source maps in dev tools + {pattern: appBase + '**/*.ts', included: false, watched: false}, + {pattern: appBase + '**/*.js.map', included: false, watched: false} + ], + + // proxied base paths for loading assets + proxies: { + // required for component assets fetched by Angular's compiler + "/app/": appAssets + }, + + exclude: [], + preprocessors: {}, + reporters: ['progress', 'html'], + + // HtmlReporter configuration + htmlReporter: { + // Open this file to see results in browser + outputFile: '_test-output/tests.html', + + // Optional + pageTitle: 'Unit Tests', + subPageTitle: __dirname + }, + + port: 9876, colors: true, logLevel: config.LOG_INFO, autoWatch: true, browsers: ['Chrome'], - singleRun: true + singleRun: false }) } diff --git a/public/docs/_examples/karma.js.conf.js b/public/docs/_examples/karma.js.conf.js deleted file mode 100644 index e55127f0f0..0000000000 --- a/public/docs/_examples/karma.js.conf.js +++ /dev/null @@ -1,68 +0,0 @@ -// Karma configuration -// Generated on Mon Aug 10 2015 11:36:40 GMT-0700 (Pacific Daylight Time) - -module.exports = function(config) { - config.set({ - - // base path that will be used to resolve all patterns (eg. files, exclude) - basePath: '', - - - // frameworks to use - // available frameworks: https://npmjs.org/browse/keyword/karma-adapter - frameworks: ['jasmine'], - - - // list of files / patterns to load in the browser - files: [ - { pattern: 'https://code.angularjs.org/2.0.0-alpha.34/angular2.sfx.dev.js', watched: false }, - - '**/js/*.js', - ], - - - // list of files to exclude - exclude: [ - '**/*.e2e-spec.js' - ], - - - // preprocess matching files before serving them to the browser - // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor - preprocessors: { - }, - - - // test results reporter to use - // possible values: 'dots', 'progress' - // available reporters: https://npmjs.org/browse/keyword/karma-reporter - reporters: ['progress'], - - - // web server port - port: 9876, - - - // enable / disable colors in the output (reporters and logs) - colors: true, - - - // level of logging - // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG - logLevel: config.LOG_INFO, - - - // enable / disable watching file and executing tests whenever any file changes - autoWatch: true, - - - // start these browsers - // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher - browsers: ['Chrome'], - - - // Continuous Integration mode - // if true, Karma captures browsers, runs the tests and exits - singleRun: false - }) -} diff --git a/public/docs/_examples/karma.ts.conf.js b/public/docs/_examples/karma.ts.conf.js deleted file mode 100644 index 5b2d176b56..0000000000 --- a/public/docs/_examples/karma.ts.conf.js +++ /dev/null @@ -1,70 +0,0 @@ -// Karma configuration -// Generated on Mon Aug 10 2015 11:36:40 GMT-0700 (Pacific Daylight Time) - -module.exports = function(config) { - config.set({ - - // base path that will be used to resolve all patterns (eg. files, exclude) - basePath: '', - - - // frameworks to use - // available frameworks: https://npmjs.org/browse/keyword/karma-adapter - frameworks: ['jasmine'], - - - // list of files / patterns to load in the browser - files: [ - { pattern: 'https://github.jspm.io/jmcriffey/bower-traceur-runtime@0.0.87/traceur-runtime.js', watched: false }, - { pattern: 'https://jspm.io/system@0.16.js', watched: false }, - { pattern: 'https://code.angularjs.org/2.0.0-alpha.34/angular2.dev.js', watched: false }, - - '**/ts/**/*.spec.js' - ], - - - // list of files to exclude - exclude: [ - '**/*.e2e-spec.js' - ], - - - // preprocess matching files before serving them to the browser - // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor - preprocessors: { - }, - - - // test results reporter to use - // possible values: 'dots', 'progress' - // available reporters: https://npmjs.org/browse/keyword/karma-reporter - reporters: ['progress'], - - - // web server port - port: 9876, - - - // enable / disable colors in the output (reporters and logs) - colors: true, - - - // level of logging - // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG - logLevel: config.LOG_INFO, - - - // enable / disable watching file and executing tests whenever any file changes - autoWatch: true, - - - // start these browsers - // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher - browsers: ['Chrome'], - - - // Continuous Integration mode - // if true, Karma captures browsers, runs the tests and exits - singleRun: false - }) -} diff --git a/public/docs/_examples/package.json b/public/docs/_examples/package.json index 92908d0c30..a5e20d4c6f 100644 --- a/public/docs/_examples/package.json +++ b/public/docs/_examples/package.json @@ -4,12 +4,12 @@ "description": "Master package.json, the superset of all dependencies for all of the _example package.json files.", "main": "index.js", "scripts": { - "start": "tsc && concurrently \"npm run tsc:w\" \"npm run lite\" ", + "start": "tsc && concurrently \"tsc -w\" \"lite-server\" ", "tsc": "tsc", "tsc:w": "tsc -w", "lite": "lite-server", "live": "live-server", - "test": "karma start karma.conf.js", + "test": "tsc && concurrently \"tsc -w\" \"karma start karma.conf.js\"", "build-and-test": "npm run tsc && npm run test", "http-server": "tsc && http-server", "http-server:e2e": "http-server", @@ -42,6 +42,7 @@ "karma": "^0.13.22", "karma-chrome-launcher": "^0.2.3", "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", diff --git a/public/docs/_examples/protractor-conf.old.js b/public/docs/_examples/protractor-conf.old.js deleted file mode 100644 index 12dd89ab9f..0000000000 --- a/public/docs/_examples/protractor-conf.old.js +++ /dev/null @@ -1,24 +0,0 @@ -exports.config = { - onPrepare: function() { - patchProtractorWait(browser); - }, - seleniumAddress: 'http://localhost:4444/wd/hub', - baseUrl: 'http://localhost:8080/', - specs: [ - '**/*e2e-spec.js' - ] -}; - -// Disable waiting for Angular as we don't have an integration layer yet... -// TODO(tbosch): Implement a proper debugging API for Ng2.0, remove this here -// and the sleeps in all tests. -function patchProtractorWait(browser) { - browser.ignoreSynchronization = true; - var _get = browser.get; - var sleepInterval = process.env.TRAVIS || process.env.JENKINS_URL ? 14000 : 8000; - browser.get = function() { - var result = _get.apply(this, arguments); - browser.sleep(sleepInterval); - return result; - } -} diff --git a/public/docs/_examples/server-communication/ts/app/toh/hero.service.1.ts b/public/docs/_examples/server-communication/ts/app/toh/hero.service.1.ts index d5734fffc5..05b97fa75d 100644 --- a/public/docs/_examples/server-communication/ts/app/toh/hero.service.1.ts +++ b/public/docs/_examples/server-communication/ts/app/toh/hero.service.1.ts @@ -3,7 +3,7 @@ // #docregion import {Injectable} from 'angular2/core'; -import {Http, Response} from 'angular2/http'; +import {Http} from 'angular2/http'; import {Headers, RequestOptions} from 'angular2/http'; import {Hero} from './hero'; @@ -32,13 +32,13 @@ export class HeroService { .then(res => res.json().data) .catch(this.handleError); } - private handleError (error: any) { // in a real world app, we may send the error to some remote logging infrastructure - // instead of just logging it to the console - console.error(error); - return Promise.reject(error.message || error.json().error || 'Server error'); + console.error(error); // log to console instead + let errMsg = error.message || 'Server error'; + return Promise.reject(errMsg); } + // #enddocregion methods } // #enddocregion diff --git a/public/docs/_examples/server-communication/ts/app/toh/hero.service.ts b/public/docs/_examples/server-communication/ts/app/toh/hero.service.ts index 7ff9805ac0..4e9a0d9070 100644 --- a/public/docs/_examples/server-communication/ts/app/toh/hero.service.ts +++ b/public/docs/_examples/server-communication/ts/app/toh/hero.service.ts @@ -3,7 +3,7 @@ // #docregion // #docregion v1 import {Injectable} from 'angular2/core'; -import {Http, Response} from 'angular2/http'; +import {Http} from 'angular2/http'; // #enddocregion v1 // #docregion import-request-options import {Headers, RequestOptions} from 'angular2/http'; @@ -32,10 +32,10 @@ export class HeroService { // #docregion methods // #docregion error-handling - getHeroes () { + getHeroes (): Observable { // #docregion http-get, http-get-v1 return this.http.get(this._heroesUrl) - .map(res => res.json().data) + .map(this.extractData) // #enddocregion v1, http-get-v1, error-handling .do(data => console.log(data)) // eyeball results in the console // #docregion v1, http-get-v1, error-handling @@ -46,27 +46,36 @@ export class HeroService { // #enddocregion v1 // #docregion addhero - addHero (name: string) : Observable { + addHero (name: string): Observable { let body = JSON.stringify({ name }); - //#docregion headers + // #docregion headers let headers = new Headers({ 'Content-Type': 'application/json' }); let options = new RequestOptions({ headers: headers }); return this.http.post(this._heroesUrl, body, options) - //#enddocregion headers - .map(res => res.json().data) - .catch(this.handleError) + // #enddocregion headers + .map(this.extractData) + .catch(this.handleError); } // #enddocregion addhero // #docregion v1 + + private extractData(res: Response) { + if (res.status < 200 || res.status >= 300) { + throw new Error('Bad response status: ' + res.status); + } + let body = res.json(); + return body.data || { }; + } + // #docregion error-handling - private handleError (error: Response) { + private handleError (error: any) { // in a real world app, we may send the error to some remote logging infrastructure - // instead of just logging it to the console - console.error(error); - return Observable.throw(error.json().error || 'Server error'); + console.error(error); // log to console instead + let errMsg = error.message || 'Server error'; + return Observable.throw(errMsg); } // #enddocregion error-handling // #enddocregion methods diff --git a/public/docs/_examples/testing/ts/app/hero.ts b/public/docs/_examples/testing/ts/app/hero.ts index 2b89781da5..8f7cc205c8 100644 --- a/public/docs/_examples/testing/ts/app/hero.ts +++ b/public/docs/_examples/testing/ts/app/hero.ts @@ -1,5 +1,5 @@ // #docregion -export interface Hero { +export class Hero { id: number; name: string; } 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 new file mode 100644 index 0000000000..f5d61b4955 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/http-hero.service.spec.ts @@ -0,0 +1,148 @@ +/* tslint:disable:no-unused-variable */ +import { + it, + iit, + xit, + describe, + ddescribe, + xdescribe, + expect, + fakeAsync, + tick, + beforeEach, + inject, + injectAsync, + withProviders, + beforeEachProviders +} from 'angular2/testing'; + +import { provide } from 'angular2/core'; + +import { + MockBackend, + MockConnection } from 'angular2/src/http/backends/mock_backend'; + +import { + BaseRequestOptions, + ConnectionBackend, + Request, + RequestMethod, + RequestOptions, + Response, + ResponseOptions, + URLSearchParams, + HTTP_PROVIDERS, + XHRBackend, + Http} from 'angular2/http'; + +// Add all operators to Observable +import 'rxjs/Rx'; +import { Observable } from 'rxjs/Observable'; + +import { Hero } from './hero'; +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" } +]; + +// HeroService expects response data like {data: {the-data}} +const makeResponseData = (data: {}) => {return { data }; }; + +//////// SPECS ///////////// +describe('Http-HeroService (mockBackend)', () => { + + beforeEachProviders(() => [ + HTTP_PROVIDERS, + provide(XHRBackend, {useClass: MockBackend}) + ]); + + it('can instantiate service when inject service', + withProviders(() => [HeroService]) + .inject([HeroService], (service: HeroService) => { + expect(service instanceof HeroService).toBe(true); + })); + + + it('can instantiate service with "new"', inject([Http], (http: Http) => { + expect(http).not.toBeNull('http should be provided'); + let service = new HeroService(http); + expect(service instanceof HeroService).toBe(true, 'new service should be ok'); + })); + + + it('can provide the mockBackend as XHRBackend', + inject([XHRBackend], (backend: MockBackend) => { + expect(backend).not.toBeNull('backend should be provided'); + })); + + describe('when getHeroes', () => { + let backend: MockBackend; + let service: HeroService; + let fakeHeroes: HeroData[]; + let response: Response; + + + beforeEach(inject([Http, XHRBackend], (http: Http, be: MockBackend) => { + backend = be; + service = new HeroService(http); + fakeHeroes = makeHeroData(); + let options = new ResponseOptions({status: 200, body: {data: fakeHeroes}}); + response = new Response(options); + })); + + it('should have expected fake heroes (then)', injectAsync([], () => { + backend.connections.subscribe((c: MockConnection) => c.mockRespond(response)); + + return service.getHeroes().toPromise() + // .then(() => Promise.reject('deliberate')) + .then(heroes => { + expect(heroes.length).toEqual(fakeHeroes.length, + 'should have expected no. of heroes'); + }); + })); + + it('should have expected fake heroes (Observable.do)', injectAsync([], () => { + backend.connections.subscribe((c: MockConnection) => c.mockRespond(response)); + + return service.getHeroes() + .do(heroes => { + expect(heroes.length).toEqual(fakeHeroes.length, + 'should have expected no. of heroes'); + }) + .toPromise(); + })); + + + it('should be OK returning no heroes', injectAsync([], () => { + let resp = new Response(new ResponseOptions({status: 200, body: {data: []}})); + backend.connections.subscribe((c: MockConnection) => c.mockRespond(resp)); + + return service.getHeroes() + .do(heroes => { + expect(heroes.length).toEqual(0, 'should have no heroes'); + }) + .toPromise(); + })); + + it('should treat 404 as an Observable error', injectAsync([], () => { + let resp = new Response(new ResponseOptions({status: 404})); + backend.connections.subscribe((c: MockConnection) => c.mockRespond(resp)); + + return service.getHeroes() + .do(heroes => { + fail('should not respond with heroes'); + }) + .catch(err => { + expect(err).toMatch(/Bad response status/, 'should catch bad response status code'); + return Observable.of(null); // failure is the expected test result + }) + .toPromise(); + })); + }); +}); diff --git a/public/docs/_examples/testing/ts/app/http-hero.service.ts b/public/docs/_examples/testing/ts/app/http-hero.service.ts new file mode 100644 index 0000000000..40c2f37866 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/http-hero.service.ts @@ -0,0 +1,44 @@ +// #docplaster +// #docregion +import {Injectable} from 'angular2/core'; +import {Http, Response} from 'angular2/http'; +import {Headers, RequestOptions} from 'angular2/http'; +import {Hero} from './hero'; +import {Observable} from 'rxjs/Observable'; + +@Injectable() +export class HeroService { + constructor (private http: Http) {} + private _heroesUrl = 'app/heroes'; // URL to web api + getHeroes (): Observable { + return this.http.get(this._heroesUrl) + .map(this.extractData) + // .do(data => console.log(data)) // eyeball results in the console + .catch(this.handleError); + } + + addHero (name: string): Observable { + let body = JSON.stringify({ name }); + let headers = new Headers({ 'Content-Type': 'application/json' }); + let options = new RequestOptions({ headers: headers }); + + return this.http.post(this._heroesUrl, body, options) + .map(this.extractData) + .catch(this.handleError); + } + + private extractData(res: Response) { + if (res.status < 200 || res.status >= 300) { + throw new Error('Bad response status: ' + res.status); + } + let body = res.json(); + return body.data || { }; + } + + private handleError (error: any) { + // in a real world app, we may send the error to some remote logging infrastructure + let errMsg = error.message || 'Server error'; + console.error(errMsg); // log to console instead + return Observable.throw(errMsg); + } +} diff --git a/public/docs/_examples/testing/ts/app/public-external-template.html b/public/docs/_examples/testing/ts/app/public-external-template.html new file mode 100644 index 0000000000..4c2b23755f --- /dev/null +++ b/public/docs/_examples/testing/ts/app/public-external-template.html @@ -0,0 +1 @@ +from external template diff --git a/public/docs/_examples/testing/ts/app/public.spec.ts b/public/docs/_examples/testing/ts/app/public.spec.ts new file mode 100644 index 0000000000..1ccd8c89f4 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/public.spec.ts @@ -0,0 +1,458 @@ +// Based on https://github.com/angular/angular/blob/master/modules/angular2/test/testing/testing_public_spec.ts +/* tslint:disable:no-unused-variable */ +import { + BadTemplateUrl, ButtonComp, + ChildChildComp, ChildComp, ChildWithChildComp, + ExternalTemplateComp, + FancyService, MockFancyService, + MyIfComp, + MockChildComp, MockChildChildComp, + ParentComp, + TestProvidersComp, TestViewProvidersComp +} from './public'; + +import { + it, + iit, + xit, + describe, + ddescribe, + xdescribe, + expect, + fakeAsync, + tick, + beforeEach, + inject, + injectAsync, + withProviders, + beforeEachProviders, + TestComponentBuilder +} from 'angular2/testing'; + +import { provide } from 'angular2/core'; +import { ViewMetadata } from 'angular2/core'; +import { PromiseWrapper } from 'angular2/src/facade/promise'; +import { XHR } from 'angular2/src/compiler/xhr'; +import { XHRImpl } from 'angular2/src/platform/browser/xhr_impl'; + +/////////// Module Preparation /////////////////////// +interface Done { + (): void; + fail: (err: any) => void; +} + +//////// SPECS ///////////// + +/// Verify can use Angular testing's DOM abstraction to access DOM + +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'); + }); + + it('should assert that the CSS class is not present', () => { + let el = document.createElement('div'); + el.classList.add('matias'); + expect(el).not.toHaveCssClass('fatias'); + }); + }); + + describe('toHaveCssStyle', () => { + it('should assert that the CSS style is present', () => { + let el = document.createElement('div'); + expect(el).not.toHaveCssStyle('width'); + + el.style.setProperty('width', '100px'); + expect(el).toHaveCssStyle('width'); + }); + + it('should assert that the styles are matched against the element', () => { + let el = document.createElement('div'); + expect(el).not.toHaveCssStyle({width: '100px', height: '555px'}); + + el.style.setProperty('width', '100px'); + expect(el).toHaveCssStyle({width: '100px'}); + expect(el).not.toHaveCssStyle({width: '100px', height: '555px'}); + + el.style.setProperty('height', '555px'); + expect(el).toHaveCssStyle({height: '555px'}); + expect(el).toHaveCssStyle({width: '100px', height: '555px'}); + }); + }); +}); + +describe('using the test injector with the inject helper', () => { + it('should run normal tests', () => { expect(true).toEqual(true); }); + + it('should run normal async tests', (done: Done) => { + setTimeout(() => { + expect(true).toEqual(true); + done(); + }, 0); + }); + + it('provides a real XHR instance', + inject([XHR], (xhr: any) => { expect(xhr).toBeAnInstanceOf(XHRImpl); })); + + describe('setting up Providers with FancyService', () => { + beforeEachProviders(() => [ + provide(FancyService, {useValue: new FancyService()}) + ]); + + it('should use FancyService', + inject([FancyService], (service: FancyService) => { + expect(service.value).toEqual('real value'); + })); + + it('test should wait for FancyService.getAsyncValue', + injectAsync([FancyService], (service: FancyService) => { + return service.getAsyncValue().then( + (value) => { expect(value).toEqual('async value'); }); + })); + + // Experimental: write async tests synchonously by faking async processing + it('should allow the use of fakeAsync (Experimental)', + inject([FancyService], fakeAsync((service: FancyService) => { + let value: any; + service.getAsyncValue().then((val: any) => value = val); + tick(); // Trigger JS engine cycle until all promises resolve. + expect(value).toEqual('async value'); + }))); + + describe('using inner beforeEach to inject-and-modify FancyService', () => { + beforeEach(inject([FancyService], (service: FancyService) => { + service.value = 'value modified in beforeEach'; + })); + + it('should use modified providers', + inject([FancyService], (service: FancyService) => { + expect(service.value).toEqual('value modified in beforeEach'); + })); + }); + + describe('using async within beforeEach', () => { + beforeEach(injectAsync([FancyService], (service: FancyService) => { + return service.getAsyncValue().then(value => { service.value = value; }); + })); + + it('should use asynchronously modified value ... in synchronous test', + inject([FancyService], (service: FancyService) => { + expect(service.value).toEqual('async value'); })); + }); + }); + + describe('using `withProviders` for per-test provision', () => { + it('should inject test-local FancyService for this test', + // `withProviders`: set up providers at individual test level + withProviders(() => [provide(FancyService, {useValue: {value: 'fake value'}})]) + + // now inject and test + .inject([FancyService], (service: FancyService) => { + expect(service.value).toEqual('fake value'); + })); + }); +}); + +describe('test component builder', function() { + it('should instantiate a component with valid DOM', + injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => { + + return tcb.createAsync(ChildComp).then(fixture => { + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveText('Original Child'); + }); + })); + + it('should allow changing members of the component', + injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => { + + return tcb.createAsync(MyIfComp).then(fixture => { + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveText('MyIf()'); + + fixture.debugElement.componentInstance.showMore = true; + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveText('MyIf(More)'); + }); + })); + + it('should support clicking a button', + injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => { + + return tcb.createAsync(ButtonComp).then(fixture => { + + let comp = fixture.componentInstance; + expect(comp.wasClicked).toEqual(false, 'wasClicked should be false at start'); + + let btn = fixture.debugElement.query(el => el.name === 'button'); + btn.triggerEventHandler('click', null); + // btn.nativeElement.click(); // this works too; which is "better"? + expect(comp.wasClicked).toEqual(true, 'wasClicked should be true after click'); + }); + })); + + it('should override a template', + injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => { + + return tcb.overrideTemplate(MockChildComp, 'Mock') + .createAsync(MockChildComp) + .then(fixture => { + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveText('Mock'); + }); + })); + + it('should override a view', + injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => { + + return tcb.overrideView( + ChildComp, + new ViewMetadata({template: 'Modified {{childBinding}}'}) + ) + .createAsync(ChildComp) + .then(fixture => { + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveText('Modified Child'); + + }); + })); + + it('should override component dependencies', + injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => { + + return tcb.overrideDirective(ParentComp, ChildComp, MockChildComp) + .createAsync(ParentComp) + .then(fixture => { + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveText('Parent(Mock)'); + + }); + })); + + + it('should override child component\'s dependencies', + injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => { + + return tcb.overrideDirective(ParentComp, ChildComp, ChildWithChildComp) + .overrideDirective(ChildWithChildComp, ChildChildComp, MockChildChildComp) + .createAsync(ParentComp) + .then(fixture => { + fixture.detectChanges(); + expect(fixture.nativeElement) + .toHaveText('Parent(Original Child(ChildChild Mock))'); + + }); + })); + + it('should override a provider', + injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => { + + return tcb.overrideProviders( + TestProvidersComp, + [provide(FancyService, {useClass: MockFancyService})] + ) + .createAsync(TestProvidersComp) + .then(fixture => { + fixture.detectChanges(); + expect(fixture.nativeElement) + .toHaveText('injected value: mocked out value'); + }); + })); + + + it('should override a viewProvider', + injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => { + + return tcb.overrideViewProviders( + TestViewProvidersComp, + [provide(FancyService, {useClass: MockFancyService})] + ) + .createAsync(TestViewProvidersComp) + .then(fixture => { + fixture.detectChanges(); + expect(fixture.nativeElement) + .toHaveText('injected value: mocked out value'); + }); + })); + + it('should allow an external templateUrl', + injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => { + + return tcb.createAsync(ExternalTemplateComp) + .then(fixture => { + fixture.detectChanges(); + expect(fixture.nativeElement) + .toHaveText('from external template\n'); + }); + }), 10000); // Long timeout because this test makes an actual XHR. +}); + +describe('errors', () => { + let originalJasmineIt: any; + let originalJasmineBeforeEach: any; + + let patchJasmineIt = () => { + let deferred = PromiseWrapper.completer(); + originalJasmineIt = jasmine.getEnv().it; + jasmine.getEnv().it = (description: string, fn: Function) => { + let done = () => { deferred.resolve(); }; + (done).fail = (err: any) => { deferred.reject(err); }; + fn(done); + return null; + }; + return deferred.promise; + }; + + let restoreJasmineIt = () => { jasmine.getEnv().it = originalJasmineIt; }; + + let patchJasmineBeforeEach = () => { + let deferred = PromiseWrapper.completer(); + originalJasmineBeforeEach = jasmine.getEnv().beforeEach; + jasmine.getEnv().beforeEach = (fn: any) => { + let done = () => { deferred.resolve(); }; + (done).fail = (err: any) => { deferred.reject(err); }; + fn(done); + return null; + }; + return deferred.promise; + }; + + let restoreJasmineBeforeEach = + () => { jasmine.getEnv().beforeEach = originalJasmineBeforeEach; }; + + const shouldNotSucceed = + (done: Done) => () => done.fail( 'Expected function to throw, but it did not'); + + const shouldFail = + (done: Done, emsg: string) => (err: any) => { expect(err).toEqual(emsg); done(); }; + + it('injectAsync should fail when return was forgotten in it', (done: Done) => { + let itPromise = patchJasmineIt(); + it('forgets to return a proimse', injectAsync([], () => { return true; })); + + itPromise.then( + shouldNotSucceed(done), + shouldFail(done, + 'Error: injectAsync was expected to return a promise, but the returned value was: true') + ); + restoreJasmineIt(); + }); + + it('inject should fail if a value was returned', (done: Done) => { + let itPromise = patchJasmineIt(); + it('returns a value', inject([], () => { return true; })); + + itPromise.then( + shouldNotSucceed(done), + shouldFail(done, + 'Error: inject returned a value. Did you mean to use injectAsync? Returned value was: true') + ); + restoreJasmineIt(); + }); + + it('injectAsync should fail when return was forgotten in beforeEach', (done: Done) => { + let beforeEachPromise = patchJasmineBeforeEach(); + beforeEach(injectAsync([], () => { return true; })); + + beforeEachPromise.then( + shouldNotSucceed(done), + shouldFail(done, + 'Error: injectAsync was expected to return a promise, but the returned value was: true') + ); + restoreJasmineBeforeEach(); + }); + + it('inject should fail if a value was returned in beforeEach', (done: Done) => { + let beforeEachPromise = patchJasmineBeforeEach(); + beforeEach(inject([], () => { return true; })); + + beforeEachPromise.then( + shouldNotSucceed(done), + shouldFail(done, + 'Error: inject returned a value. Did you mean to use injectAsync? Returned value was: true') + ); + restoreJasmineBeforeEach(); + }); + + it('should fail when an error occurs inside inject', (done: Done) => { + let itPromise = patchJasmineIt(); + + it('throws an error', inject([], () => { throw new Error('foo'); })); + + itPromise.then( + shouldNotSucceed(done), + err => { expect(err.message).toEqual('foo'); done(); } + ); + restoreJasmineIt(); + }); + + // TODO(juliemr): reenable this test when we are using a test zone and can capture this error. + xit('should fail when an asynchronous error is thrown', (done: Done) => { + let itPromise = patchJasmineIt(); + + it('throws an async error', + injectAsync([], () => { setTimeout(() => { throw new Error('bar'); }, 0); })); + + itPromise.then( + shouldNotSucceed(done), + err => { expect(err.message).toEqual('bar'); done(); } + ); + restoreJasmineIt(); + }); + + it('should fail when a returned promise is rejected', (done: Done) => { + let itPromise = patchJasmineIt(); + + it('should fail with an error from a promise', injectAsync([], () => { + let deferred = PromiseWrapper.completer(); + let p = deferred.promise.then(() => { expect(1).toEqual(2); }); + + deferred.reject('baz'); + return p; + })); + + itPromise.then( + shouldNotSucceed(done), + shouldFail(done, 'baz') + ); + restoreJasmineIt(); + }); + + it('should fail when an XHR fails', (done: Done) => { + let itPromise = patchJasmineIt(); + + it('should fail with an error from a promise', + injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => { + return tcb.createAsync(BadTemplateUrl); + })); + + itPromise.then( + shouldNotSucceed(done), + shouldFail(done, 'Failed to load non-existant.html') + ); + restoreJasmineIt(); + }, 10000); + + describe('using beforeEachProviders', () => { + beforeEachProviders(() => [provide(FancyService, {useValue: new FancyService()})]); + + beforeEach( + inject([FancyService], (service: FancyService) => { expect(service.value).toEqual('real value'); })); + + describe('nested beforeEachProviders', () => { + + it('should fail when the injector has already been used', () => { + patchJasmineBeforeEach(); + expect(() => { + beforeEachProviders(() => [provide(FancyService, {useValue: new FancyService()})]); + }) + .toThrowError('beforeEachProviders was called after the injector had been used ' + + 'in a beforeEach or it block. This invalidates the test injector'); + restoreJasmineBeforeEach(); + }); + }); + }); +}); diff --git a/public/docs/_examples/testing/ts/app/public.ts b/public/docs/_examples/testing/ts/app/public.ts new file mode 100644 index 0000000000..382c2c700e --- /dev/null +++ b/public/docs/_examples/testing/ts/app/public.ts @@ -0,0 +1,133 @@ +// 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'; + +// Let TypeScript know about the special SystemJS __moduleName variable +declare var __moduleName: string; + +// moduleName is not set in some module loaders; set it explicitly +if (!__moduleName) { + __moduleName = `http://${location.host}/${location.pathname}/app/`; +} +// console.log(`The __moduleName is ${__moduleName} `); + + + +////////// The App: Services and Components for the tests. ////////////// + +////////// Services /////////////// + +@Injectable() +export class FancyService { + value: string = 'real value'; + getAsyncValue() { return Promise.resolve('async value'); } +} + +@Injectable() +export class MockFancyService extends FancyService { + value: string = 'mocked out value'; +} + +//////////// Components ///////////// + +@Component({ + selector: 'button-comp', + template: `` +}) +export class ButtonComp { + wasClicked = false; + clicked() { this.wasClicked = true; } +} + + +@Component({ + selector: 'child-comp', + template: `Original {{childBinding}}` +}) +export class ChildComp { + childBinding = 'Child'; +} + + +@Component({ + selector: 'child-comp', + template: `Mock` +}) +export class MockChildComp { } + + +@Component({ + selector: 'parent-comp', + template: `Parent()`, + directives: [ChildComp] +}) +export class ParentComp { } + + +@Component({ + selector: 'my-if-comp', + template: `MyIf(More)`, + directives: [NgIf] +}) +export class MyIfComp { + showMore: boolean = false; +} + + +@Component({ + selector: 'child-child-comp', + template: 'ChildChild' +}) +export class ChildChildComp { } + + +@Component({ + selector: 'child-comp', + template: `Original {{childBinding}}()`, + directives: [ChildChildComp] +}) +export class ChildWithChildComp { + childBinding = 'Child'; +} + + +@Component({ + selector: 'child-child-comp', + template: `ChildChild Mock` +}) +export class MockChildChildComp { } + + +@Component({ + selector: 'my-service-comp', + template: `injected value: {{fancyService.value}}`, + providers: [FancyService] +}) +export class TestProvidersComp { + constructor(private fancyService: FancyService) {} +} + + +@Component({ + selector: 'my-service-comp', + template: `injected value: {{fancyService.value}}`, + viewProviders: [FancyService] +}) +export class TestViewProvidersComp { + constructor(private fancyService: FancyService) {} +} + + +@Component({ + moduleId: __moduleName, + selector: 'external-template-comp', + templateUrl: 'public-external-template.html' +}) +export class ExternalTemplateComp { } + + +@Component({ + selector: 'bad-template-comp', + templateUrl: 'non-existant.html' +}) +export class BadTemplateUrl { } diff --git a/public/docs/_examples/testing/ts/test-shim.js b/public/docs/_examples/testing/ts/test-shim.js new file mode 100644 index 0000000000..31e1998e69 --- /dev/null +++ b/public/docs/_examples/testing/ts/test-shim.js @@ -0,0 +1,48 @@ +/*global jasmine, __karma__, window*/ + +// Browser testing shim +(function () { + +// Error.stackTraceLimit = Infinity; + +jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000; + +// Configure systemjs to use the .js extension for imports from the app folder +System.config({ + packages: { + app: { + format: 'register', + defaultExtension: 'js' + } + } +}); + +// 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 + ); + }); + }) + + // Load the spec files (__spec_files__) explicitly + .then(function () { + console.log('loading spec files: '+__spec_files__.join(', ')); + return Promise.all(__spec_files__.map(function(spec) { return System.import(spec);} )); + }) + + // After all imports load, re-execute `window.onload` which + // triggers the Jasmine test-runner start or explain what went wrong + .then(success, console.error.bind(console)); + +function success () { + console.log('Spec files loaded; starting Jasmine testrunner'); + window.onload(); +} + + +})(); diff --git a/public/docs/_examples/testing/ts/unit-tests-public.html b/public/docs/_examples/testing/ts/unit-tests-public.html new file mode 100644 index 0000000000..cbb3f7f859 --- /dev/null +++ b/public/docs/_examples/testing/ts/unit-tests-public.html @@ -0,0 +1,32 @@ + + + + + + Angular Public Unit Tests + + + + + + + + + + + + + + + + + + + + + + + +