From 9c806f67b7115a4ef97552ebe1723e69d2a587fd Mon Sep 17 00:00:00 2001 From: Filipe Silva Date: Mon, 12 Sep 2016 17:40:05 +0100 Subject: [PATCH 01/60] fix(harp): temporarily use PR commit for js fix (#2308) --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index f8ece4dacf..4d5061f581 100644 --- a/package.json +++ b/package.json @@ -42,13 +42,13 @@ "globby": "^4.0.0", "gulp": "^3.5.6", "gulp-env": "0.4.0", - "gulp-sass": "^2.3.2", "gulp-less": "^3.1.0", + "gulp-sass": "^2.3.2", "gulp-task-listing": "^1.0.1", "gulp-tslint": "^5.0.0", "gulp-util": "^3.0.6", "gulp-watch": "^4.3.4", - "harp": "0.21.0-pre.0", + "harp": "git://github.com/filipesilva/harp.git#8da8d3497ddbfcbcbadd8be63e0fd731d7310cc4", "html2jade": "^0.8.4", "indent-string": "^2.1.0", "jasmine-core": "^2.3.4", From ed6be8316fa3b41541daf36bca1d807e50672ebd Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Fri, 9 Sep 2016 14:38:06 -0700 Subject: [PATCH 02/60] refactor(api-builder): remove obsolete stuff --- tools/api-builder/angular.io-package/index.js | 29 +++--------- .../processors/filterUnwantedDecorators.js | 20 -------- .../filterUnwantedDecorators.spec.js | 46 ------------------- 3 files changed, 6 insertions(+), 89 deletions(-) delete mode 100644 tools/api-builder/angular.io-package/processors/filterUnwantedDecorators.js delete mode 100644 tools/api-builder/angular.io-package/processors/filterUnwantedDecorators.spec.js diff --git a/tools/api-builder/angular.io-package/index.js b/tools/api-builder/angular.io-package/index.js index f81ee2e69b..39cc16e4be 100644 --- a/tools/api-builder/angular.io-package/index.js +++ b/tools/api-builder/angular.io-package/index.js @@ -15,7 +15,6 @@ module.exports = new Package('angular.io', [basePackage, targetPackage, cheatshe .factory(require('./services/renderMarkdown')) .processor(require('./processors/addJadeDataDocsProcessor')) -.processor(require('./processors/filterUnwantedDecorators')) .processor(require('./processors/matchUpDirectiveDecorators')) .processor(require('./processors/filterMemberDocs')) @@ -47,20 +46,13 @@ module.exports = new Package('angular.io', [basePackage, targetPackage, cheatshe } readTypeScriptModules.basePath = path.resolve(angular_repo_path, 'modules'); readTypeScriptModules.ignoreExportsMatching = [ - '___esModule', - '___core_private_types__', - '___platform_browser_private__', - '___platform_browser_private_types__', - '___platform_browser_dynamic_private__', - '___platform_browser_dynamic_private_types__', - '___platform_server_private__', - '___router_private__' , - '___core_private_testing_types__', - '___compiler_private__', + '__esModule', '__core_private__', - '___core_private__', - '___core_private_testing_placeholder__', - '___core_private_testing__' + '__core_private_testing__', + '__platform_browser_private__', + '__platform_browser_dynamic_private__', + '__platform_server_private__', + '__router_private__', ]; readTypeScriptModules.sourceFiles = [ @@ -81,7 +73,6 @@ module.exports = new Package('angular.io', [basePackage, targetPackage, cheatshe '@angular/platform-webworker-dynamic/index.ts', '@angular/router/index.ts', '@angular/router/testing/index.ts', - '@angular/router-deprecated/index.ts', '@angular/upgrade/index.ts', ]; readTypeScriptModules.hidePrivateMembers = true; @@ -167,12 +158,4 @@ module.exports = new Package('angular.io', [basePackage, targetPackage, cheatshe require('./rendering/toId'), require('./rendering/indentForMarkdown') ])); -}) - -.config(function(filterUnwantedDecorators) { - filterUnwantedDecorators.decoratorsToIgnore = [ - 'CONST', - 'IMPLEMENTS', - 'ABSTRACT' - ]; }); diff --git a/tools/api-builder/angular.io-package/processors/filterUnwantedDecorators.js b/tools/api-builder/angular.io-package/processors/filterUnwantedDecorators.js deleted file mode 100644 index 1f321bd42b..0000000000 --- a/tools/api-builder/angular.io-package/processors/filterUnwantedDecorators.js +++ /dev/null @@ -1,20 +0,0 @@ -var _ = require('lodash'); - -module.exports = function filterUnwantedDecorators() { - return { - decoratorsToIgnore: [], - $runAfter: ['processing-docs'], - $runBefore: ['docs-processed'], - $process: function(docs) { - var decoratorsToIgnore = this.decoratorsToIgnore || []; - _.forEach(docs, function(doc) { - if (doc.decorators) { - doc.decorators = _.filter(doc.decorators, function(decorator) { - return decoratorsToIgnore.indexOf(decorator.name) === -1; - }); - } - }); - return docs; - } - }; -} \ No newline at end of file diff --git a/tools/api-builder/angular.io-package/processors/filterUnwantedDecorators.spec.js b/tools/api-builder/angular.io-package/processors/filterUnwantedDecorators.spec.js deleted file mode 100644 index 47a88f8408..0000000000 --- a/tools/api-builder/angular.io-package/processors/filterUnwantedDecorators.spec.js +++ /dev/null @@ -1,46 +0,0 @@ -var mockPackage = require('../mocks/mockPackage'); -var Dgeni = require('dgeni'); - -describe('filterUnwantedDecorators', function() { - var dgeni, injector, processor; - - beforeEach(function() { - dgeni = new Dgeni([mockPackage()]); - injector = dgeni.configureInjector(); - processor = injector.get('filterUnwantedDecorators'); - }); - - - it('should remove decorators specified by name', function() { - var docs = [ - { id: 'doc1', decorators: [ { name: 'A' }, { name: 'B' } ] }, - { id: 'doc2', decorators: [ { name: 'B' }, { name: 'C' } ] }, - { id: 'doc3', decorators: [ { name: 'A' }, { name: 'C' } ] } - ]; - processor.decoratorsToIgnore = ['D', 'B']; - docs = processor.$process(docs); - - expect(docs).toEqual([ - { id: 'doc1', decorators: [ { name: 'A' } ] }, - { id: 'doc2', decorators: [ { name: 'C' } ] }, - { id: 'doc3', decorators: [ { name: 'A' }, { name: 'C' } ] } - ]); - }); - - - it('should ignore docs that have no decorators', function() { - var docs = [ - { id: 'doc1', decorators: [ { name: 'A' }, { name: 'B' } ] }, - { id: 'doc2' }, - { id: 'doc3', decorators: [ { name: 'A' }, { name: 'C' } ] } - ]; - processor.decoratorsToIgnore = ['D', 'B']; - docs = processor.$process(docs); - - expect(docs).toEqual([ - { id: 'doc1', decorators: [ { name: 'A' } ] }, - { id: 'doc2' }, - { id: 'doc3', decorators: [ { name: 'A' }, { name: 'C' } ] } - ]); - }); -}); \ No newline at end of file From 9b0b1ae3fb5db603d265c88812ce8544459d1e50 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Fri, 9 Sep 2016 14:38:27 -0700 Subject: [PATCH 03/60] fix(api-builder): prefix static members with 'static' --- .../angular.io-package/templates/class.template.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/api-builder/angular.io-package/templates/class.template.html b/tools/api-builder/angular.io-package/templates/class.template.html index bb9afaeb6b..06785cc5fd 100644 --- a/tools/api-builder/angular.io-package/templates/class.template.html +++ b/tools/api-builder/angular.io-package/templates/class.template.html @@ -22,7 +22,7 @@ div(layout="row" layout-xs="column" class="ng-cloak") {% if doc.statics.length %} div(layout="column") {% for member in doc.statics %}{% if not member.internal %} - pre(class="prettyprint no-bg-with-indent") + pre(class="prettyprint no-bg-with-indent") static a(class="code-anchor" href="#{$ member.name $}-anchor") code(class="code-background api-doc-code") {$ member.name | indent(6, false) | trim $} code(class="api-doc-code") {$ params.paramList(member.parameters) | indent(8, false) | trim $}{$ params.returnType(member.returnType) $} From 71b7f102dcd52e5ee50b0bcdf3c396eb37f3912a Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Fri, 9 Sep 2016 15:16:26 -0700 Subject: [PATCH 04/60] fix(ci): bump LATEST_RELEASE to 2.0.0-rc.6 --- .travis.yml | 2 +- scripts/config/bad-code-excerpt-skip-patterns.txt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b30e5d12a1..be3a57ef03 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,7 @@ env: - DBUS_SESSION_BUS_ADDRESS=/dev/null - DISPLAY=:99.0 - CHROME_BIN=chromium-browser - - LATEST_RELEASE=2.0.0-rc.5 + - LATEST_RELEASE=2.0.0-rc.6 - TASK_FLAGS="--dgeni-log=warn" matrix: - TASK=lint diff --git a/scripts/config/bad-code-excerpt-skip-patterns.txt b/scripts/config/bad-code-excerpt-skip-patterns.txt index afe85a03d3..b66e9ce758 100644 --- a/scripts/config/bad-code-excerpt-skip-patterns.txt +++ b/scripts/config/bad-code-excerpt-skip-patterns.txt @@ -4,3 +4,4 @@ /[jt]s/.*/api/router-deprecated/ # Obsolete API entries. No issue open yet. /ts/latest/guide/style-guide.html # https://github.com/angular/angular.io/issues/2123 /ts/latest/guide/upgrade.html # In a transient state until RC6 - @filipe.silva +/[jt]s/.*/api/forms/index/NG_VALIDATORS-let.html # RC6 contains broken example tags From 1646c45d676736785c51166e5f3bffc26759c314 Mon Sep 17 00:00:00 2001 From: Katiuska Gamero Date: Sat, 10 Sep 2016 12:20:34 +0200 Subject: [PATCH 05/60] update conference links - add ng-europe; remove AngularConnect --- public/_includes/_hero-home.jade | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/_includes/_hero-home.jade b/public/_includes/_hero-home.jade index a1d34a334a..d1b8291a76 100644 --- a/public/_includes/_hero-home.jade +++ b/public/_includes/_hero-home.jade @@ -8,6 +8,6 @@ header(class="background-sky") .banner.banner-floaty .banner-ng-annoucement div(class="banner-text" align="center") - p Join us for AngularConnect in London, UK this September! + p Join us for ng-europe in Paris, France this October! div(class="banner-button") - a(href="http://angularconnect.com/?utm_source=angular&utm_medium=banner&utm_campaign=angular-banner" target="_blank" class="button md-button") Register now \ No newline at end of file + a(href="https://ngeurope.org/?utm_source=angular&utm_medium=banner&utm_campaign=angular-banner" target="_blank" class="button md-button") Register now From c3bb033cc18ab7f1ace2e02a4f3eded8a9be92f2 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Tue, 13 Sep 2016 12:09:04 +0100 Subject: [PATCH 06/60] fix(api-builder): escape double-quotes in JSON output --- .../angular.io-package/templates/api-list-audit.template.html | 2 +- .../angular.io-package/templates/api-list-data.template.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/api-builder/angular.io-package/templates/api-list-audit.template.html b/tools/api-builder/angular.io-package/templates/api-list-audit.template.html index 92fe2fcbec..b0610e16f7 100644 --- a/tools/api-builder/angular.io-package/templates/api-list-audit.template.html +++ b/tools/api-builder/angular.io-package/templates/api-list-audit.template.html @@ -6,7 +6,7 @@ "docType": "{$ item.docType $}", "stability": "{$ item.stability $}", "secure": "{$ item.security $}", - "howToUse": "{$ item.howToUse $}", + "howToUse": "{$ item.howToUse | replace('"','\\"') $}", "whatItDoes": {% if item.whatItDoes %}"Exists"{% else %}"Not Done"{% endif %}, "barrel" : "{$ module | replace("/index", "") $}" }{% if not loop.last %},{% endif %} diff --git a/tools/api-builder/angular.io-package/templates/api-list-data.template.html b/tools/api-builder/angular.io-package/templates/api-list-data.template.html index 554091b650..80a424a9a6 100644 --- a/tools/api-builder/angular.io-package/templates/api-list-data.template.html +++ b/tools/api-builder/angular.io-package/templates/api-list-data.template.html @@ -7,7 +7,7 @@ "docType": "{$ item.docType $}", "stability": "{$ item.stability $}", "secure": "{$ item.security $}", - "howToUse": "{$ item.howToUse $}", + "howToUse": "{$ item.howToUse | replace('"','\\"') $}", "whatItDoes": {% if item.whatItDoes %}"Exists"{% else %}"Not Done"{% endif %}, "barrel" : "{$ module | replace("/index", "") $}" }{% if not loop.last %},{% endif %} From 42c16907d82aaa7120333d52ae7eaade8426813d Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Tue, 13 Sep 2016 16:04:04 +0100 Subject: [PATCH 07/60] fix(api-builder): update to a fixed version of dgeni-packages See https://github.com/angular/angular.io/pull/2321#issuecomment-246597648 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4d5061f581..83806a42b3 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "cross-spawn": "^4.0.0", "del": "^2.2.0", "dgeni": "^0.4.0", - "dgeni-packages": "^0.15.2", + "dgeni-packages": "^0.16.0", "diff": "^2.1.3", "fs-extra": "^0.30.0", "globby": "^4.0.0", From 191c18b6cb83b20f22df0eef202ffea7086755fb Mon Sep 17 00:00:00 2001 From: Naomi Black Date: Tue, 13 Sep 2016 08:42:26 -0700 Subject: [PATCH 08/60] firebase(config): update named servers for live and dev --- .firebaserc | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .firebaserc diff --git a/.firebaserc b/.firebaserc new file mode 100644 index 0000000000..2ef123c5bd --- /dev/null +++ b/.firebaserc @@ -0,0 +1,6 @@ +{ + "projects": { + "live": "angular-io", + "ngdocsdev": "ngdocsdev" + } +} \ No newline at end of file From 6180fd51371d5877b5411f6ad5492e17ad15591e Mon Sep 17 00:00:00 2001 From: Naomi Black Date: Tue, 13 Sep 2016 08:47:57 -0700 Subject: [PATCH 09/60] dgeni(api-builder): hide private classes with extra underscores --- tools/api-builder/angular.io-package/index.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tools/api-builder/angular.io-package/index.js b/tools/api-builder/angular.io-package/index.js index 39cc16e4be..a0e2874903 100644 --- a/tools/api-builder/angular.io-package/index.js +++ b/tools/api-builder/angular.io-package/index.js @@ -48,11 +48,18 @@ module.exports = new Package('angular.io', [basePackage, targetPackage, cheatshe readTypeScriptModules.ignoreExportsMatching = [ '__esModule', '__core_private__', + '___core_private__', '__core_private_testing__', + '___core_private_testing__', + '___core_private_testing_placeholder__', '__platform_browser_private__', + '___platform_browser_private__', '__platform_browser_dynamic_private__', + '___platform_browser_dynamic_private__', + '___platform_server_private__', '__platform_server_private__', '__router_private__', + '___router_private__', ]; readTypeScriptModules.sourceFiles = [ From 988694bb12abd974b00314ccba41b3977343ba31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesu=CC=81s=20Rodri=CC=81guez?= Date: Tue, 13 Sep 2016 11:34:44 +0200 Subject: [PATCH 10/60] chore: update docs to rc7 --- .travis.yml | 2 +- .../docs/_examples/cb-ts-to-js/js/index.html | 2 +- .../_examples/forms-deprecated/js/index.html | 2 +- public/docs/_examples/forms/js/index.html | 2 +- .../homepage-hello-world/ts/index.1.html | 2 +- .../_examples/homepage-tabs/ts/index.1.html | 2 +- .../_examples/homepage-todo/ts/index.1.html | 2 +- public/docs/_examples/package.json | 26 ++++++++-------- .../docs/_examples/quickstart/js/index.html | 2 +- .../_examples/quickstart/js/package.1.json | 27 ++++++++--------- .../_examples/quickstart/ts/package.1.json | 26 ++++++++-------- .../_examples/quickstart/ts/typings.1.json | 2 +- .../docs/_examples/styleguide/js/index.html | 2 +- public/docs/_examples/typings.json | 2 +- .../_examples/webpack/ts/package.webpack.json | 30 +++++++++---------- .../docs/_examples/webpack/ts/typings.1.json | 2 +- public/docs/js/latest/_data.json | 2 +- public/docs/ts/latest/_data.json | 2 +- tools/plunker-builder/indexHtmlTranslator.js | 6 ++-- 19 files changed, 71 insertions(+), 72 deletions(-) diff --git a/.travis.yml b/.travis.yml index be3a57ef03..d5da556b4e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,7 @@ env: - DBUS_SESSION_BUS_ADDRESS=/dev/null - DISPLAY=:99.0 - CHROME_BIN=chromium-browser - - LATEST_RELEASE=2.0.0-rc.6 + - LATEST_RELEASE=2.0.0-rc.7 - TASK_FLAGS="--dgeni-log=warn" matrix: - TASK=lint diff --git a/public/docs/_examples/cb-ts-to-js/js/index.html b/public/docs/_examples/cb-ts-to-js/js/index.html index 89e80451a6..448c295609 100644 --- a/public/docs/_examples/cb-ts-to-js/js/index.html +++ b/public/docs/_examples/cb-ts-to-js/js/index.html @@ -10,7 +10,7 @@ - + diff --git a/public/docs/_examples/forms-deprecated/js/index.html b/public/docs/_examples/forms-deprecated/js/index.html index e15f41e4fe..053f5266fa 100644 --- a/public/docs/_examples/forms-deprecated/js/index.html +++ b/public/docs/_examples/forms-deprecated/js/index.html @@ -20,7 +20,7 @@ - + diff --git a/public/docs/_examples/forms/js/index.html b/public/docs/_examples/forms/js/index.html index fe525d5549..e67b53ac26 100644 --- a/public/docs/_examples/forms/js/index.html +++ b/public/docs/_examples/forms/js/index.html @@ -20,7 +20,7 @@ - + diff --git a/public/docs/_examples/homepage-hello-world/ts/index.1.html b/public/docs/_examples/homepage-hello-world/ts/index.1.html index 849560e3e7..8b812feb4a 100644 --- a/public/docs/_examples/homepage-hello-world/ts/index.1.html +++ b/public/docs/_examples/homepage-hello-world/ts/index.1.html @@ -11,7 +11,7 @@ - + diff --git a/public/docs/_examples/homepage-tabs/ts/index.1.html b/public/docs/_examples/homepage-tabs/ts/index.1.html index 650735970f..13e82eaf7c 100644 --- a/public/docs/_examples/homepage-tabs/ts/index.1.html +++ b/public/docs/_examples/homepage-tabs/ts/index.1.html @@ -12,7 +12,7 @@ - + diff --git a/public/docs/_examples/homepage-todo/ts/index.1.html b/public/docs/_examples/homepage-todo/ts/index.1.html index c541fe0f30..ab18c9c9d9 100644 --- a/public/docs/_examples/homepage-todo/ts/index.1.html +++ b/public/docs/_examples/homepage-todo/ts/index.1.html @@ -12,7 +12,7 @@ - + diff --git a/public/docs/_examples/package.json b/public/docs/_examples/package.json index 93215c5d5e..30b6d93c00 100644 --- a/public/docs/_examples/package.json +++ b/public/docs/_examples/package.json @@ -25,23 +25,23 @@ "author": "", "license": "ISC", "dependencies": { - "@angular/common": "2.0.0-rc.6", - "@angular/compiler": "2.0.0-rc.6", - "@angular/compiler-cli": "0.6.0", - "@angular/core": "2.0.0-rc.6", - "@angular/forms": "2.0.0-rc.6", - "@angular/http": "2.0.0-rc.6", - "@angular/platform-browser": "2.0.0-rc.6", - "@angular/platform-browser-dynamic": "2.0.0-rc.6", - "@angular/router": "3.0.0-rc.2", - "@angular/upgrade": "2.0.0-rc.6", - "angular2-in-memory-web-api": "0.0.18", + "@angular/common": "2.0.0-rc.7", + "@angular/compiler": "2.0.0-rc.7", + "@angular/compiler-cli": "0.6.1", + "@angular/core": "2.0.0-rc.7", + "@angular/forms": "2.0.0-rc.7", + "@angular/http": "2.0.0-rc.7", + "@angular/platform-browser": "2.0.0-rc.7", + "@angular/platform-browser-dynamic": "2.0.0-rc.7", + "@angular/router": "3.0.0-rc.3", + "@angular/upgrade": "2.0.0-rc.7", + "angular2-in-memory-web-api": "0.0.19", "bootstrap": "^3.3.6", "core-js": "^2.4.1", "reflect-metadata": "^0.1.3", - "rxjs": "5.0.0-beta.11", + "rxjs": "5.0.0-beta.12", "systemjs": "0.19.27", - "zone.js": "^0.6.17" + "zone.js": "^0.6.21" }, "devDependencies": { "angular-cli": "^1.0.0-beta.5", diff --git a/public/docs/_examples/quickstart/js/index.html b/public/docs/_examples/quickstart/js/index.html index b127bdce13..551e4fb134 100644 --- a/public/docs/_examples/quickstart/js/index.html +++ b/public/docs/_examples/quickstart/js/index.html @@ -16,7 +16,7 @@ - + diff --git a/public/docs/_examples/quickstart/js/package.1.json b/public/docs/_examples/quickstart/js/package.1.json index 971b6b5d2b..5aa35c7de7 100644 --- a/public/docs/_examples/quickstart/js/package.1.json +++ b/public/docs/_examples/quickstart/js/package.1.json @@ -7,23 +7,22 @@ }, "license": "ISC", "dependencies": { - "@angular/common": "2.0.0-rc.6", - "@angular/compiler": "2.0.0-rc.6", - "@angular/core": "2.0.0-rc.6", - "@angular/forms": "2.0.0-rc.6", - "@angular/http": "2.0.0-rc.6", - "@angular/platform-browser": "2.0.0-rc.6", - "@angular/platform-browser-dynamic": "2.0.0-rc.6", - "@angular/router": "3.0.0-rc.2", - "@angular/router-deprecated": "2.0.0-rc.2", - "@angular/upgrade": "2.0.0-rc.6", + "@angular/common": "2.0.0-rc.7", + "@angular/compiler": "2.0.0-rc.7", + "@angular/core": "2.0.0-rc.7", + "@angular/forms": "2.0.0-rc.7", + "@angular/http": "2.0.0-rc.7", + "@angular/platform-browser": "2.0.0-rc.7", + "@angular/platform-browser-dynamic": "2.0.0-rc.7", + "@angular/router": "3.0.0-rc.3", + "@angular/upgrade": "2.0.0-rc.7", "core-js": "^2.4.1", - "reflect-metadata": "0.1.3", - "rxjs": "5.0.0-beta.11", - "zone.js": "0.6.17", + "reflect-metadata": "^0.1.3", + "rxjs": "5.0.0-beta.12", + "zone.js": "0.6.21", - "angular2-in-memory-web-api": "0.0.18", + "angular2-in-memory-web-api": "0.0.19", "bootstrap": "^3.3.6" }, "devDependencies": { diff --git a/public/docs/_examples/quickstart/ts/package.1.json b/public/docs/_examples/quickstart/ts/package.1.json index 5d9d1f6c24..f8dab54569 100644 --- a/public/docs/_examples/quickstart/ts/package.1.json +++ b/public/docs/_examples/quickstart/ts/package.1.json @@ -11,24 +11,24 @@ }, "license": "ISC", "dependencies": { - "@angular/common": "2.0.0-rc.6", - "@angular/compiler": "2.0.0-rc.6", - "@angular/compiler-cli": "0.6.0", - "@angular/core": "2.0.0-rc.6", - "@angular/forms": "2.0.0-rc.6", - "@angular/http": "2.0.0-rc.6", - "@angular/platform-browser": "2.0.0-rc.6", - "@angular/platform-browser-dynamic": "2.0.0-rc.6", - "@angular/router": "3.0.0-rc.2", - "@angular/upgrade": "2.0.0-rc.6", + "@angular/common": "2.0.0-rc.7", + "@angular/compiler": "2.0.0-rc.7", + "@angular/compiler-cli": "0.6.1", + "@angular/core": "2.0.0-rc.7", + "@angular/forms": "2.0.0-rc.7", + "@angular/http": "2.0.0-rc.7", + "@angular/platform-browser": "2.0.0-rc.7", + "@angular/platform-browser-dynamic": "2.0.0-rc.7", + "@angular/router": "3.0.0-rc.3", + "@angular/upgrade": "2.0.0-rc.7", "core-js": "^2.4.1", "reflect-metadata": "^0.1.3", - "rxjs": "5.0.0-beta.11", + "rxjs": "5.0.0-beta.12", "systemjs": "0.19.27", - "zone.js": "^0.6.17", + "zone.js": "^0.6.21", - "angular2-in-memory-web-api": "0.0.18", + "angular2-in-memory-web-api": "0.0.19", "bootstrap": "^3.3.6" }, "devDependencies": { diff --git a/public/docs/_examples/quickstart/ts/typings.1.json b/public/docs/_examples/quickstart/ts/typings.1.json index 72db971273..7da31ca0af 100644 --- a/public/docs/_examples/quickstart/ts/typings.1.json +++ b/public/docs/_examples/quickstart/ts/typings.1.json @@ -2,6 +2,6 @@ "globalDependencies": { "core-js": "registry:dt/core-js#0.0.0+20160725163759", "jasmine": "registry:dt/jasmine#2.2.0+20160621224255", - "node": "registry:dt/node#6.0.0+20160831021119" + "node": "registry:dt/node#6.0.0+20160909174046" } } diff --git a/public/docs/_examples/styleguide/js/index.html b/public/docs/_examples/styleguide/js/index.html index b7da03d83a..d863bfcecb 100644 --- a/public/docs/_examples/styleguide/js/index.html +++ b/public/docs/_examples/styleguide/js/index.html @@ -10,7 +10,7 @@ - + diff --git a/public/docs/_examples/typings.json b/public/docs/_examples/typings.json index 72db971273..7da31ca0af 100644 --- a/public/docs/_examples/typings.json +++ b/public/docs/_examples/typings.json @@ -2,6 +2,6 @@ "globalDependencies": { "core-js": "registry:dt/core-js#0.0.0+20160725163759", "jasmine": "registry:dt/jasmine#2.2.0+20160621224255", - "node": "registry:dt/node#6.0.0+20160831021119" + "node": "registry:dt/node#6.0.0+20160909174046" } } diff --git a/public/docs/_examples/webpack/ts/package.webpack.json b/public/docs/_examples/webpack/ts/package.webpack.json index 4d772b16ee..73ab0765d3 100644 --- a/public/docs/_examples/webpack/ts/package.webpack.json +++ b/public/docs/_examples/webpack/ts/package.webpack.json @@ -10,17 +10,17 @@ }, "license": "MIT", "dependencies": { - "@angular/common": "2.0.0-rc.6", - "@angular/compiler": "2.0.0-rc.6", - "@angular/core": "2.0.0-rc.6", - "@angular/forms": "2.0.0-rc.6", - "@angular/http": "2.0.0-rc.6", - "@angular/platform-browser": "2.0.0-rc.6", - "@angular/platform-browser-dynamic": "2.0.0-rc.6", - "@angular/router": "3.0.0-rc.2", + "@angular/common": "2.0.0-rc.7", + "@angular/compiler": "2.0.0-rc.7", + "@angular/core": "2.0.0-rc.7", + "@angular/forms": "2.0.0-rc.7", + "@angular/http": "2.0.0-rc.7", + "@angular/platform-browser": "2.0.0-rc.7", + "@angular/platform-browser-dynamic": "2.0.0-rc.7", + "@angular/router": "3.0.0-rc.3", "core-js": "^2.4.1", - "rxjs": "5.0.0-beta.11", - "zone.js": "^0.6.17" + "rxjs": "5.0.0-beta.12", + "zone.js": "^0.6.21" }, "devDependencies": { "angular2-template-loader": "^0.4.0", @@ -30,11 +30,11 @@ "html-loader": "^0.4.3", "html-webpack-plugin": "^2.15.0", "jasmine-core": "^2.4.1", - "karma": "^0.13.22", - "karma-jasmine": "^0.3.8", - "karma-phantomjs-launcher": "^1.0.0", + "karma": "^1.2.0", + "karma-jasmine": "^1.0.2", + "karma-phantomjs-launcher": "^1.0.2", "karma-sourcemap-loader": "^0.3.7", - "karma-webpack": "^1.7.0", + "karma-webpack": "^1.8.0", "null-loader": "^0.1.1", "phantomjs-prebuilt": "^2.1.7", "raw-loader": "^0.5.1", @@ -42,7 +42,7 @@ "style-loader": "^0.13.1", "ts-loader": "^0.8.1", "typescript": "^1.8.10", - "typings": "^1.0.4", + "typings": "^1.3.2", "webpack": "^1.13.0", "webpack-dev-server": "^1.14.1", "webpack-merge": "^0.14.0" diff --git a/public/docs/_examples/webpack/ts/typings.1.json b/public/docs/_examples/webpack/ts/typings.1.json index 72db971273..7da31ca0af 100644 --- a/public/docs/_examples/webpack/ts/typings.1.json +++ b/public/docs/_examples/webpack/ts/typings.1.json @@ -2,6 +2,6 @@ "globalDependencies": { "core-js": "registry:dt/core-js#0.0.0+20160725163759", "jasmine": "registry:dt/jasmine#2.2.0+20160621224255", - "node": "registry:dt/node#6.0.0+20160831021119" + "node": "registry:dt/node#6.0.0+20160909174046" } } diff --git a/public/docs/js/latest/_data.json b/public/docs/js/latest/_data.json index a4f0318673..e61260abf7 100644 --- a/public/docs/js/latest/_data.json +++ b/public/docs/js/latest/_data.json @@ -3,7 +3,7 @@ "icon": "home", "title": "Angular Docs", "menuTitle": "Docs Home", - "banner": "Welcome to Angular in JavaScript! The current Angular 2 release is rc.6. Please consult the Change Log about recent enhancements, fixes, and breaking changes." + "banner": "Welcome to Angular in JavaScript! The current Angular 2 release is rc.7. Please consult the Change Log about recent enhancements, fixes, and breaking changes." }, "quickstart": { diff --git a/public/docs/ts/latest/_data.json b/public/docs/ts/latest/_data.json index 122f173344..3d7abd05b0 100644 --- a/public/docs/ts/latest/_data.json +++ b/public/docs/ts/latest/_data.json @@ -3,7 +3,7 @@ "icon": "home", "title": "Angular Docs", "menuTitle": "Docs Home", - "banner": "Welcome to Angular in TypeScript! The current Angular 2 release is rc.6. Please consult the Change Log about recent enhancements, fixes, and breaking changes." + "banner": "Welcome to Angular in TypeScript! The current Angular 2 release is rc.7. Please consult the Change Log about recent enhancements, fixes, and breaking changes." }, "cli-quickstart": { diff --git a/tools/plunker-builder/indexHtmlTranslator.js b/tools/plunker-builder/indexHtmlTranslator.js index add1baf295..f0f7ae080a 100644 --- a/tools/plunker-builder/indexHtmlTranslator.js +++ b/tools/plunker-builder/indexHtmlTranslator.js @@ -45,7 +45,7 @@ var _rxData = [ { pattern: 'script', from: 'node_modules/zone.js/dist/zone.js', - to: 'https://unpkg.com/zone.js@0.6.17?main=browser' + to: 'https://unpkg.com/zone.js@0.6.21?main=browser' }, { pattern: 'script', @@ -54,8 +54,8 @@ var _rxData = [ }, { pattern: 'script', - from: 'node_modules/rxjs/bundles/Rx.umd.js', - to: 'https://unpkg.com/rxjs@5.0.0-beta.11/bundles/Rx.umd.js' + from: 'node_modules/rxjs/bundles/Rx.js', + to: 'https://unpkg.com/rxjs@5.0.0-beta.12/bundles/Rx.js' }, { pattern: 'script', From 87981260e0a80d872c741350230b738ed1ca237b Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Tue, 13 Sep 2016 09:20:58 -0700 Subject: [PATCH 11/60] fix(api-builder): ignore all symbols starting with _ --- tools/api-builder/angular.io-package/index.js | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/tools/api-builder/angular.io-package/index.js b/tools/api-builder/angular.io-package/index.js index a0e2874903..3070d822e3 100644 --- a/tools/api-builder/angular.io-package/index.js +++ b/tools/api-builder/angular.io-package/index.js @@ -45,22 +45,7 @@ module.exports = new Package('angular.io', [basePackage, targetPackage, cheatshe throw new Error('build-api-docs task requires the angular2 repo to be at ' + angular_repo_path); } readTypeScriptModules.basePath = path.resolve(angular_repo_path, 'modules'); - readTypeScriptModules.ignoreExportsMatching = [ - '__esModule', - '__core_private__', - '___core_private__', - '__core_private_testing__', - '___core_private_testing__', - '___core_private_testing_placeholder__', - '__platform_browser_private__', - '___platform_browser_private__', - '__platform_browser_dynamic_private__', - '___platform_browser_dynamic_private__', - '___platform_server_private__', - '__platform_server_private__', - '__router_private__', - '___router_private__', - ]; + readTypeScriptModules.ignoreExportsMatching = [/^_/]; readTypeScriptModules.sourceFiles = [ '@angular/common/index.ts', From d1987b183bf64844ab01c2f25d766c20c941a760 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesu=CC=81s=20Rodri=CC=81guez?= Date: Tue, 13 Sep 2016 18:21:55 +0200 Subject: [PATCH 12/60] docs(di): remove InjectorMetadata mentions --- public/docs/ts/latest/guide/dependency-injection.jade | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/public/docs/ts/latest/guide/dependency-injection.jade b/public/docs/ts/latest/guide/dependency-injection.jade index d8676e1419..3629e40078 100644 --- a/public/docs/ts/latest/guide/dependency-injection.jade +++ b/public/docs/ts/latest/guide/dependency-injection.jade @@ -388,10 +388,10 @@ block ctor-syntax We call that property within our `getHeroes` method when anyone asks for heroes. //- FIXME refer to Dart API when that page becomes available. -- var injMetaUrl = 'https://angular.io/docs/ts/latest/api/core/index/InjectableMetadata-class.html'; +- var injUrl = 'https://angular.io/docs/ts/latest/api/core/index/Injectable-decorator.html'; h3#injectable Why @Injectable()? :marked - **@Injectable()** marks a class as available to an + **@Injectable()** marks a class as available to an injector for instantiation. Generally speaking, an injector will report an error when trying to instantiate a class that is not marked as `@Injectable()`. @@ -423,8 +423,8 @@ block injectable-not-always-needed-in-ts We *can* add it if we really want to. It isn't necessary because the `HeroesComponent` is already marked with `@Component`, and this !{_decorator} class (like `@Directive` and `@Pipe`, which we'll learn about later) - is a subtype of InjectableMetadata. It is in - fact `InjectableMetadata` #{_decorator}s that + is a subtype of Injectable. It is in + fact `Injectable` #{_decorator}s that identify a class as a target for instantiation by an injector. +ifDocsFor('ts') @@ -442,7 +442,7 @@ block injectable-not-always-needed-in-ts for _every class with at least one decorator_. While any decorator will trigger this effect, mark the service class with the - InjectableMetadata #{_decorator} + Injectable #{_decorator} to make the intent clear. .callout.is-critical From dfc671b99fb1a081976b6f4931acc8bf8be738b1 Mon Sep 17 00:00:00 2001 From: Filipe Silva Date: Tue, 13 Sep 2016 18:56:18 +0100 Subject: [PATCH 13/60] fix(build-compile): allow skipping langs in a fresh build (#2307) --- gulpfile.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index c6a47063b8..456d892b5c 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1044,8 +1044,10 @@ function restoreApiHtml() { const relApiDir = path.join('docs', lang, vers, 'api'); const wwwApiSubdir = path.join('www', relApiDir); const backupApiSubdir = path.join('www-backup', relApiDir); - gutil.log(`cp ${backupApiSubdir} ${wwwApiSubdir}`) - fs.copySync(backupApiSubdir, wwwApiSubdir); + if (fs.existsSync(backupApiSubdir) || argv.forceSkipApi !== true) { + gutil.log(`cp ${backupApiSubdir} ${wwwApiSubdir}`) + fs.copySync(backupApiSubdir, wwwApiSubdir); + } }); } From 761c2568813a361924bf0886b0fb9cd2de33cc93 Mon Sep 17 00:00:00 2001 From: Patrice Chalin Date: Tue, 13 Sep 2016 12:21:07 -0700 Subject: [PATCH 14/60] chore(travis): use latest Dart SDK (#2320) Dartdoc has been fixed so we no longer need to be pinned to SDK v1.18.1. --- scripts/install-dart-sdk.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/install-dart-sdk.sh b/scripts/install-dart-sdk.sh index 60ff9710c7..5eb4e7e7e1 100755 --- a/scripts/install-dart-sdk.sh +++ b/scripts/install-dart-sdk.sh @@ -13,8 +13,8 @@ if [[ -z "$(type -t dart)" ]]; then # https://storage.googleapis.com/dart-archive/channels/stable/release/latest/dartium/dartium-macos-x64-release.zip DART_ARCHIVE=https://storage.googleapis.com/dart-archive/channels - # VERS=stable/release/latest - VERS=stable/release/1.18.1 + VERS=stable/release/latest + # VERS=stable/release/1.18.1 # If necessary, pin a specific version like this mkUrl() { local dir=$1 From e1b2f6a2fe2862df20a0df23ee7735994f4dc0fe Mon Sep 17 00:00:00 2001 From: jmcgoldrick Date: Wed, 14 Sep 2016 05:24:59 +1000 Subject: [PATCH 15/60] docs(ngmodule-faq): correct `BrowserModule` import module (#2269) --- public/docs/ts/latest/cookbook/ngmodule-faq.jade | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/docs/ts/latest/cookbook/ngmodule-faq.jade b/public/docs/ts/latest/cookbook/ngmodule-faq.jade index cd4c70eed5..c52f63d137 100644 --- a/public/docs/ts/latest/cookbook/ngmodule-faq.jade +++ b/public/docs/ts/latest/cookbook/ngmodule-faq.jade @@ -166,7 +166,7 @@ a#q-browser-vs-common-module ### Should I import _BrowserModule_ or _CommonModule_? The **root application module** (`AppModule`) of almost every browser application - should import `BrowserModule` from `@angular/core`. + should import `BrowserModule` from `@angular/platform-browser`. `BrowserModule` provides services that are essential to launch and run a browser app. From 98e5bc248e8441efab92e4d71523b67652b81a50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Rodr=C3=ADguez?= Date: Tue, 13 Sep 2016 21:27:13 +0200 Subject: [PATCH 16/60] docs(ngmodule-faq): fix plunker link (#2277) --- public/docs/ts/latest/cookbook/ngmodule-faq.jade | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/docs/ts/latest/cookbook/ngmodule-faq.jade b/public/docs/ts/latest/cookbook/ngmodule-faq.jade index c52f63d137..3841bc3406 100644 --- a/public/docs/ts/latest/cookbook/ngmodule-faq.jade +++ b/public/docs/ts/latest/cookbook/ngmodule-faq.jade @@ -516,7 +516,7 @@ a#q-why-bad .l-sub-section :marked Prove it for yourself. - Run the live example. + Run the live example. Modify the `SharedModule` so that it provides the `UserService` rather than the `CoreModule`. Then toggle between the "Contact" and "Heroes" links a few times. The username goes bonkers as the Angular creates a new `UserService` instance each time. From c5ad38ee86d7f0737ea90cbf58ad8921c5cb15d0 Mon Sep 17 00:00:00 2001 From: Ward Bell Date: Tue, 13 Sep 2016 12:27:27 -0700 Subject: [PATCH 17/60] docs(ngmodule-faq): eager loaded Routed Feature Module must be imported (#2304) fixes #2298 --- public/docs/ts/latest/cookbook/ngmodule-faq.jade | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/public/docs/ts/latest/cookbook/ngmodule-faq.jade b/public/docs/ts/latest/cookbook/ngmodule-faq.jade index 3841bc3406..ce7a675018 100644 --- a/public/docs/ts/latest/cookbook/ngmodule-faq.jade +++ b/public/docs/ts/latest/cookbook/ngmodule-faq.jade @@ -781,14 +781,20 @@ table _Routed Feature Modules_ are _Domain Feature modules_ whose top components are the **targets of router navigation routes**. - All lazy loaded modules are routed feature modules by nature. + All lazy loaded modules are routed feature modules by definition. This chapter's `ContactModule`, `HeroModule` and `CrisisModule` are routed feature modules. Routed Feature Modules _should not export anything_. They don't have to because none of their components ever appear in the template of an external component. - Routed Feature Modules are _never imported_. + A lazy loaded Routed Feature Module should _not be imported_ by any module. + Doing so would trigger an eager load, defeating the purpose of lazy loading. + `HeroModule` and `CrisisModule` are lazy loaded. They aren't mentioned among the `AppModule` imports. + + But an eager loaded Routed Feature Module must be imported by another module + so that the compiler learns about its components. + `ContactModule` is eager loaded and, therefore, is listed among the `AppModule` imports. Routed Feature Modules rarely have _providers_ for reasons [explained earlier](#q-why-bad). When they do, the lifetime of the provided services From 07cfce795fec5fe71f9316ab4f6e17403c6b2c2d Mon Sep 17 00:00:00 2001 From: Ward Bell Date: Tue, 13 Sep 2016 14:39:39 -0700 Subject: [PATCH 18/60] docs(testing): testing chapter and samples for RC6 (#2198) [WIP] docs(testing): new chapter, new samples --- gulpfile.js | 7 +- public/docs/_examples/karma-test-shim.js | 50 - public/docs/_examples/package.json | 1 + .../docs/_examples/testing/ts/1st-specs.html | 40 + .../_examples/testing/ts/1st-specs.plnkr.json | 12 + public/docs/_examples/testing/ts/1st.spec.ts | 20 - .../docs/_examples/testing/ts/app-specs.html | 52 + .../_examples/testing/ts/app-specs.plnkr.json | 23 + .../docs/_examples/testing/ts/app/1st.spec.ts | 5 + .../testing/ts/app/about.component.ts | 12 + .../testing/ts/app/app.component.css | 31 - .../testing/ts/app/app.component.html | 10 + .../ts/app/app.component.router.spec.ts | 201 ++ .../testing/ts/app/app.component.spec.ts | 140 +- .../_examples/testing/ts/app/app.component.ts | 51 +- .../_examples/testing/ts/app/app.module.ts | 33 + .../testing/ts/app/bad-tests.spec.ts | 163 -- .../docs/_examples/testing/ts/app/bag.spec.ts | 460 ---- public/docs/_examples/testing/ts/app/bag.ts | 255 --- .../testing/ts/app/bag/async-helper.spec.ts | 56 + .../app/{ => bag}/bag-external-template.html | 0 .../_examples/testing/ts/app/bag/bag-main.ts | 5 + .../testing/ts/app/bag/bag.no-testbed.spec.ts | 130 ++ .../_examples/testing/ts/app/bag/bag.spec.ts | 674 ++++++ .../docs/_examples/testing/ts/app/bag/bag.ts | 454 ++++ .../testing/ts/app/banner.component.spec.ts | 127 ++ .../testing/ts/app/banner.component.ts | 11 + .../testing/ts/app/dashboard.component.html | 11 - .../ts/app/dashboard.component.spec.ts | 164 -- .../testing/ts/app/dashboard.component.ts | 44 - .../dashboard/dashboard-hero.component.css | 28 + .../dashboard/dashboard-hero.component.html | 4 + .../dashboard-hero.component.spec.ts | 113 + .../app/dashboard/dashboard-hero.component.ts | 17 + .../{ => dashboard}/dashboard.component.css | 28 - .../ts/app/dashboard/dashboard.component.html | 9 + .../dashboard.component.no-testbed.spec.ts | 57 + .../app/dashboard/dashboard.component.spec.ts | 147 ++ .../ts/app/dashboard/dashboard.component.ts | 43 + .../ts/app/dashboard/dashboard.module.ts | 20 + .../testing/ts/app/hero-detail.component.html | 14 - .../testing/ts/app/hero-detail.component.ts | 58 - .../_examples/testing/ts/app/hero.service.ts | 28 - .../_examples/testing/ts/app/hero.spec.ts | 50 - public/docs/_examples/testing/ts/app/hero.ts | 5 - .../app/{ => hero}/hero-detail.component.css | 3 +- .../ts/app/hero/hero-detail.component.html | 11 + .../hero-detail.component.no-testbed.spec.ts | 58 + .../ts/app/hero/hero-detail.component.spec.ts | 196 ++ .../ts/app/hero/hero-detail.component.ts | 52 + .../ts/app/hero/hero-detail.service.ts | 21 + .../hero-list.component.css} | 0 .../ts/app/hero/hero-list.component.html | 8 + .../ts/app/hero/hero-list.component.spec.ts | 139 ++ .../ts/app/hero/hero-list.component.ts | 30 + .../testing/ts/app/hero/hero.module.ts | 9 + .../testing/ts/app/hero/hero.routing.ts | 12 + .../testing/ts/app/heroes.component.html | 21 - .../testing/ts/app/heroes.component.ts | 50 - public/docs/_examples/testing/ts/app/main.ts | 8 +- .../testing/ts/app/mock-hero.service.ts | 22 - .../_examples/testing/ts/app/mock-heroes.ts | 16 - .../_examples/testing/ts/app/mock-router.ts | 217 -- .../testing/ts/app/model/hero.service.ts | 29 + .../testing/ts/app/model/hero.spec.ts | 20 + .../_examples/testing/ts/app/model/hero.ts | 4 + .../app/{ => model}/http-hero.service.spec.ts | 72 +- .../ts/app/{ => model}/http-hero.service.ts | 24 +- .../_examples/testing/ts/app/model/index.ts | 7 + .../testing/ts/app/model/test-heroes.ts | 11 + .../ts/app/model/testing/fake-hero.service.ts | 41 + .../testing/ts/app/model/testing/index.ts | 1 + .../testing/ts/app/model/user.service.ts | 7 + .../testing/ts/app/my-uppercase.pipe.1.ts | 9 - .../testing/ts/app/my-uppercase.pipe.spec.ts | 41 - .../testing/ts/app/my-uppercase.pipe.ts | 13 - .../hero-detail.component.spec.ts.not-yet | 218 -- ...il.component.wrapped-tests.spec.ts.not-yet | 144 -- .../old-specs/hero.service.ng.spec.ts.not-yet | 198 -- .../hero.service.no-ng.1.spec.ts.not-yet | 250 --- .../hero.service.no-ng.spec.ts.not-yet | 150 -- .../heroes.component.ng.spec.ts.not-yet | 276 --- .../heroes.component.no-ng.spec.ts.not-yet | 229 -- .../ts/app/old-specs/user.spec.ts.not-yet | 18 - .../ts/app/shared/highlight.directive.spec.ts | 58 + .../ts/app/shared/highlight.directive.ts | 24 + .../testing/ts/app/shared/shared.module.ts | 15 + .../testing/ts/app/shared/styles.css | 1 + .../ts/app/shared/title-case.pipe.spec.ts | 33 + .../testing/ts/app/shared/title-case.pipe.ts | 11 + .../ts/app/shared/twain.component.spec.ts | 92 + .../twain.component.timer.spec.ts.no-work | 116 ++ .../shared/twain.component.timer.ts.no-work | 27 + .../testing/ts/app/shared/twain.component.ts | 20 + .../testing/ts/app/shared/twain.service.ts | 32 + .../testing/ts/app/welcome.component.spec.ts | 83 + .../testing/ts/app/welcome.component.ts | 18 + .../docs/_examples/testing/ts/bag-specs.html | 41 + .../_examples/testing/ts/bag-specs.plnkr.json | 20 + public/docs/_examples/testing/ts/bag.html | 27 + .../docs/_examples/testing/ts/bag.plnkr.json | 13 + .../_examples/testing/ts/browser-test-shim.js | 87 + public/docs/_examples/testing/ts/index.html | 4 +- .../_examples/testing/ts/karma-test-shim.js | 89 + .../_examples/{ => testing/ts}/karma.conf.js | 42 +- .../testing/ts/liteserver-test-config.json | 3 - public/docs/_examples/testing/ts/plnkr.json | 16 + .../testing/ts/systemjs.config.extras.js | 9 + .../ts/test-helpers/dom-setup.ts.not-yet | 18 - .../ts/test-helpers/test-helpers.ts.not-yet | 103 - public/docs/_examples/testing/ts/test-shim.js | 48 - .../testing/ts/testing/fake-router.ts | 49 + .../_examples/testing/ts/testing/index.ts | 23 + .../testing/ts/testing/jasmine-matchers.d.ts | 5 + .../testing/ts/testing/jasmine-matchers.ts | 45 + .../docs/_examples/testing/ts/tsconfig.1.json | 13 - .../_examples/testing/ts/unit-tests-0.html | 28 - .../_examples/testing/ts/unit-tests-1.html | 21 - .../_examples/testing/ts/unit-tests-2.html | 27 - .../_examples/testing/ts/unit-tests-3.html | 41 - .../_examples/testing/ts/unit-tests-4.html | 43 - .../_examples/testing/ts/unit-tests-5.html | 41 - .../testing/ts/unit-tests-6.html.not-yet | 44 - .../testing/ts/unit-tests-7.html.not-yet | 46 - .../_examples/testing/ts/unit-tests-bag.html | 30 - .../testing/ts/unit-tests.html.not-yet | 57 - public/docs/_examples/testing/ts/wallaby.js | 119 ++ .../toh-6/ts/app/hero-search.service.ts | 2 +- public/docs/_examples/wallaby.js | 77 - public/docs/ts/latest/guide/testing.jade | 1845 ++++++++++++++++- public/docs/ts/latest/testing/_data.json | 28 - .../testing/application-under-test.jade | 10 - .../ts/latest/testing/first-app-tests.jade | 224 -- public/docs/ts/latest/testing/index.jade | 60 - .../latest/testing/jasmine-testing-101.jade | 205 -- .../testing/testing-an-angular-pipe.jade | 199 -- .../images/devguide/testing/app-plunker.png | Bin 0 -> 20815 bytes .../devguide/testing/app-specs-plunker.png | Bin 0 -> 26860 bytes .../devguide/testing/karma-1st-spec-debug.png | Bin 0 -> 82623 bytes .../testing/karma-1st-spec-output.png | Bin 0 -> 36357 bytes .../images/devguide/testing/karma-browser.png | Bin 0 -> 35716 bytes tools/plunker-builder/builder.js | 12 +- tools/plunker-builder/plunkerBuilder.js | 313 +++ 143 files changed, 6286 insertions(+), 4869 deletions(-) delete mode 100644 public/docs/_examples/karma-test-shim.js create mode 100644 public/docs/_examples/testing/ts/1st-specs.html create mode 100644 public/docs/_examples/testing/ts/1st-specs.plnkr.json delete mode 100644 public/docs/_examples/testing/ts/1st.spec.ts create mode 100644 public/docs/_examples/testing/ts/app-specs.html create mode 100644 public/docs/_examples/testing/ts/app-specs.plnkr.json create mode 100644 public/docs/_examples/testing/ts/app/1st.spec.ts create mode 100644 public/docs/_examples/testing/ts/app/about.component.ts delete mode 100644 public/docs/_examples/testing/ts/app/app.component.css create mode 100644 public/docs/_examples/testing/ts/app/app.component.html create mode 100644 public/docs/_examples/testing/ts/app/app.component.router.spec.ts create mode 100644 public/docs/_examples/testing/ts/app/app.module.ts delete mode 100644 public/docs/_examples/testing/ts/app/bad-tests.spec.ts delete mode 100644 public/docs/_examples/testing/ts/app/bag.spec.ts delete mode 100644 public/docs/_examples/testing/ts/app/bag.ts create mode 100644 public/docs/_examples/testing/ts/app/bag/async-helper.spec.ts rename public/docs/_examples/testing/ts/app/{ => bag}/bag-external-template.html (100%) create mode 100644 public/docs/_examples/testing/ts/app/bag/bag-main.ts create mode 100644 public/docs/_examples/testing/ts/app/bag/bag.no-testbed.spec.ts create mode 100644 public/docs/_examples/testing/ts/app/bag/bag.spec.ts create mode 100644 public/docs/_examples/testing/ts/app/bag/bag.ts create mode 100644 public/docs/_examples/testing/ts/app/banner.component.spec.ts create mode 100644 public/docs/_examples/testing/ts/app/banner.component.ts delete mode 100644 public/docs/_examples/testing/ts/app/dashboard.component.html delete mode 100644 public/docs/_examples/testing/ts/app/dashboard.component.spec.ts delete mode 100644 public/docs/_examples/testing/ts/app/dashboard.component.ts create mode 100644 public/docs/_examples/testing/ts/app/dashboard/dashboard-hero.component.css create mode 100644 public/docs/_examples/testing/ts/app/dashboard/dashboard-hero.component.html create mode 100644 public/docs/_examples/testing/ts/app/dashboard/dashboard-hero.component.spec.ts create mode 100644 public/docs/_examples/testing/ts/app/dashboard/dashboard-hero.component.ts rename public/docs/_examples/testing/ts/app/{ => dashboard}/dashboard.component.css (54%) create mode 100644 public/docs/_examples/testing/ts/app/dashboard/dashboard.component.html create mode 100644 public/docs/_examples/testing/ts/app/dashboard/dashboard.component.no-testbed.spec.ts create mode 100644 public/docs/_examples/testing/ts/app/dashboard/dashboard.component.spec.ts create mode 100644 public/docs/_examples/testing/ts/app/dashboard/dashboard.component.ts create mode 100644 public/docs/_examples/testing/ts/app/dashboard/dashboard.module.ts delete mode 100644 public/docs/_examples/testing/ts/app/hero-detail.component.html delete mode 100644 public/docs/_examples/testing/ts/app/hero-detail.component.ts delete mode 100644 public/docs/_examples/testing/ts/app/hero.service.ts delete mode 100644 public/docs/_examples/testing/ts/app/hero.spec.ts delete mode 100644 public/docs/_examples/testing/ts/app/hero.ts rename public/docs/_examples/testing/ts/app/{ => hero}/hero-detail.component.css (93%) create mode 100644 public/docs/_examples/testing/ts/app/hero/hero-detail.component.html create mode 100644 public/docs/_examples/testing/ts/app/hero/hero-detail.component.no-testbed.spec.ts create mode 100644 public/docs/_examples/testing/ts/app/hero/hero-detail.component.spec.ts create mode 100644 public/docs/_examples/testing/ts/app/hero/hero-detail.component.ts create mode 100644 public/docs/_examples/testing/ts/app/hero/hero-detail.service.ts rename public/docs/_examples/testing/ts/app/{heroes.component.css => hero/hero-list.component.css} (100%) create mode 100644 public/docs/_examples/testing/ts/app/hero/hero-list.component.html create mode 100644 public/docs/_examples/testing/ts/app/hero/hero-list.component.spec.ts create mode 100644 public/docs/_examples/testing/ts/app/hero/hero-list.component.ts create mode 100644 public/docs/_examples/testing/ts/app/hero/hero.module.ts create mode 100644 public/docs/_examples/testing/ts/app/hero/hero.routing.ts delete mode 100644 public/docs/_examples/testing/ts/app/heroes.component.html delete mode 100644 public/docs/_examples/testing/ts/app/heroes.component.ts delete mode 100644 public/docs/_examples/testing/ts/app/mock-hero.service.ts delete mode 100644 public/docs/_examples/testing/ts/app/mock-heroes.ts delete mode 100644 public/docs/_examples/testing/ts/app/mock-router.ts create mode 100644 public/docs/_examples/testing/ts/app/model/hero.service.ts create mode 100644 public/docs/_examples/testing/ts/app/model/hero.spec.ts create mode 100644 public/docs/_examples/testing/ts/app/model/hero.ts rename public/docs/_examples/testing/ts/app/{ => model}/http-hero.service.spec.ts (67%) rename public/docs/_examples/testing/ts/app/{ => model}/http-hero.service.ts (68%) create mode 100644 public/docs/_examples/testing/ts/app/model/index.ts create mode 100644 public/docs/_examples/testing/ts/app/model/test-heroes.ts create mode 100644 public/docs/_examples/testing/ts/app/model/testing/fake-hero.service.ts create mode 100644 public/docs/_examples/testing/ts/app/model/testing/index.ts create mode 100644 public/docs/_examples/testing/ts/app/model/user.service.ts delete mode 100644 public/docs/_examples/testing/ts/app/my-uppercase.pipe.1.ts delete mode 100644 public/docs/_examples/testing/ts/app/my-uppercase.pipe.spec.ts delete mode 100644 public/docs/_examples/testing/ts/app/my-uppercase.pipe.ts delete mode 100644 public/docs/_examples/testing/ts/app/old-specs/hero-detail.component.spec.ts.not-yet delete mode 100644 public/docs/_examples/testing/ts/app/old-specs/hero-detail.component.wrapped-tests.spec.ts.not-yet delete mode 100644 public/docs/_examples/testing/ts/app/old-specs/hero.service.ng.spec.ts.not-yet delete mode 100644 public/docs/_examples/testing/ts/app/old-specs/hero.service.no-ng.1.spec.ts.not-yet delete mode 100644 public/docs/_examples/testing/ts/app/old-specs/hero.service.no-ng.spec.ts.not-yet delete mode 100644 public/docs/_examples/testing/ts/app/old-specs/heroes.component.ng.spec.ts.not-yet delete mode 100644 public/docs/_examples/testing/ts/app/old-specs/heroes.component.no-ng.spec.ts.not-yet delete mode 100644 public/docs/_examples/testing/ts/app/old-specs/user.spec.ts.not-yet create mode 100644 public/docs/_examples/testing/ts/app/shared/highlight.directive.spec.ts create mode 100644 public/docs/_examples/testing/ts/app/shared/highlight.directive.ts create mode 100644 public/docs/_examples/testing/ts/app/shared/shared.module.ts create mode 100644 public/docs/_examples/testing/ts/app/shared/styles.css create mode 100644 public/docs/_examples/testing/ts/app/shared/title-case.pipe.spec.ts create mode 100644 public/docs/_examples/testing/ts/app/shared/title-case.pipe.ts create mode 100644 public/docs/_examples/testing/ts/app/shared/twain.component.spec.ts create mode 100644 public/docs/_examples/testing/ts/app/shared/twain.component.timer.spec.ts.no-work create mode 100644 public/docs/_examples/testing/ts/app/shared/twain.component.timer.ts.no-work create mode 100644 public/docs/_examples/testing/ts/app/shared/twain.component.ts create mode 100644 public/docs/_examples/testing/ts/app/shared/twain.service.ts create mode 100644 public/docs/_examples/testing/ts/app/welcome.component.spec.ts create mode 100644 public/docs/_examples/testing/ts/app/welcome.component.ts create mode 100644 public/docs/_examples/testing/ts/bag-specs.html create mode 100644 public/docs/_examples/testing/ts/bag-specs.plnkr.json create mode 100644 public/docs/_examples/testing/ts/bag.html create mode 100644 public/docs/_examples/testing/ts/bag.plnkr.json create mode 100644 public/docs/_examples/testing/ts/browser-test-shim.js create mode 100644 public/docs/_examples/testing/ts/karma-test-shim.js rename public/docs/_examples/{ => testing/ts}/karma.conf.js (60%) delete mode 100644 public/docs/_examples/testing/ts/liteserver-test-config.json create mode 100644 public/docs/_examples/testing/ts/plnkr.json create mode 100644 public/docs/_examples/testing/ts/systemjs.config.extras.js delete mode 100644 public/docs/_examples/testing/ts/test-helpers/dom-setup.ts.not-yet delete mode 100644 public/docs/_examples/testing/ts/test-helpers/test-helpers.ts.not-yet delete mode 100644 public/docs/_examples/testing/ts/test-shim.js create mode 100644 public/docs/_examples/testing/ts/testing/fake-router.ts create mode 100644 public/docs/_examples/testing/ts/testing/index.ts create mode 100644 public/docs/_examples/testing/ts/testing/jasmine-matchers.d.ts create mode 100644 public/docs/_examples/testing/ts/testing/jasmine-matchers.ts delete mode 100644 public/docs/_examples/testing/ts/tsconfig.1.json delete mode 100644 public/docs/_examples/testing/ts/unit-tests-0.html delete mode 100644 public/docs/_examples/testing/ts/unit-tests-1.html delete mode 100644 public/docs/_examples/testing/ts/unit-tests-2.html delete mode 100644 public/docs/_examples/testing/ts/unit-tests-3.html delete mode 100644 public/docs/_examples/testing/ts/unit-tests-4.html delete mode 100644 public/docs/_examples/testing/ts/unit-tests-5.html delete mode 100644 public/docs/_examples/testing/ts/unit-tests-6.html.not-yet delete mode 100644 public/docs/_examples/testing/ts/unit-tests-7.html.not-yet delete mode 100644 public/docs/_examples/testing/ts/unit-tests-bag.html delete mode 100644 public/docs/_examples/testing/ts/unit-tests.html.not-yet create mode 100644 public/docs/_examples/testing/ts/wallaby.js delete mode 100644 public/docs/_examples/wallaby.js delete mode 100644 public/docs/ts/latest/testing/_data.json delete mode 100644 public/docs/ts/latest/testing/application-under-test.jade delete mode 100644 public/docs/ts/latest/testing/first-app-tests.jade delete mode 100644 public/docs/ts/latest/testing/index.jade delete mode 100644 public/docs/ts/latest/testing/jasmine-testing-101.jade delete mode 100644 public/docs/ts/latest/testing/testing-an-angular-pipe.jade create mode 100644 public/resources/images/devguide/testing/app-plunker.png create mode 100644 public/resources/images/devguide/testing/app-specs-plunker.png create mode 100644 public/resources/images/devguide/testing/karma-1st-spec-debug.png create mode 100644 public/resources/images/devguide/testing/karma-1st-spec-output.png create mode 100644 public/resources/images/devguide/testing/karma-browser.png create mode 100644 tools/plunker-builder/plunkerBuilder.js diff --git a/gulpfile.js b/gulpfile.js index 456d892b5c..36185cfb30 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -91,15 +91,12 @@ var _excludeMatchers = _excludePatterns.map(function(excludePattern){ var _exampleBoilerplateFiles = [ '.editorconfig', 'a2docs.css', - 'karma.conf.js', - 'karma-test-shim.js', 'package.json', 'styles.css', 'systemjs.config.js', 'tsconfig.json', 'tslint.json', - 'typings.json', - 'wallaby.js' + 'typings.json' ]; var _exampleDartWebBoilerPlateFiles = ['a2docs.css', 'styles.css']; @@ -636,7 +633,7 @@ gulp.task('build-dart-api-docs', ['_shred-api-examples', 'dartdoc'], function() // Using the --build flag will use systemjs.config.plunker.build.js (for preview builds) gulp.task('build-plunkers', ['_copy-example-boilerplate'], function() { regularPlunker.buildPlunkers(EXAMPLES_PATH, LIVE_EXAMPLES_PATH, { errFn: gutil.log, build: argv.build }); - return embeddedPlunker.buildPlunkers(EXAMPLES_PATH, LIVE_EXAMPLES_PATH, { errFn: gutil.log, build: argv.build }); + return embeddedPlunker.buildPlunkers(EXAMPLES_PATH, LIVE_EXAMPLES_PATH, { errFn: gutil.log, build: argv.build, targetSelf: argv.targetSelf }); }); gulp.task('build-dart-cheatsheet', [], function() { diff --git a/public/docs/_examples/karma-test-shim.js b/public/docs/_examples/karma-test-shim.js deleted file mode 100644 index 5fb73d0301..0000000000 --- a/public/docs/_examples/karma-test-shim.js +++ /dev/null @@ -1,50 +0,0 @@ -// /*global jasmine, __karma__, window*/ -Error.stackTraceLimit = Infinity; -jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000; - -__karma__.loaded = function () { -}; - -function isJsFile(path) { - return path.slice(-3) == '.js'; -} - -function isSpecFile(path) { - return /\.spec\.js$/.test(path); -} - -function isBuiltFile(path) { - var builtPath = '/base/app/'; - return isJsFile(path) && (path.substr(0, builtPath.length) == builtPath); -} - -var allSpecFiles = Object.keys(window.__karma__.files) - .filter(isSpecFile) - .filter(isBuiltFile); - -System.config({ - baseURL: '/base', - packageWithIndex: true // sadly, we can't use umd packages (yet?) -}); - -System.import('systemjs.config.js') - .then(() => Promise.all([ - System.import('@angular/core/testing'), - System.import('@angular/platform-browser-dynamic/testing') - ])) - .then((providers) => { - var coreTesting = providers[0]; - var browserTesting = providers[1]; - coreTesting.TestBed.initTestEnvironment( - browserTesting.BrowserDynamicTestingModule, - browserTesting.platformBrowserDynamicTesting()); - }) - .then(function () { - // Finally, load all spec files. - // This will run the tests directly. - return Promise.all( - allSpecFiles.map(function (moduleName) { - return System.import(moduleName); - })); - }) - .then(__karma__.start, __karma__.error); diff --git a/public/docs/_examples/package.json b/public/docs/_examples/package.json index 30b6d93c00..ee913df8f7 100644 --- a/public/docs/_examples/package.json +++ b/public/docs/_examples/package.json @@ -60,6 +60,7 @@ "karma-cli": "^1.0.1", "karma-htmlfile-reporter": "^0.3.4", "karma-jasmine": "^1.0.2", + "karma-jasmine-html-reporter": "^0.2.2", "karma-phantomjs-launcher": "^1.0.2", "karma-sourcemap-loader": "^0.3.7", "karma-webpack": "^1.8.0", diff --git a/public/docs/_examples/testing/ts/1st-specs.html b/public/docs/_examples/testing/ts/1st-specs.html new file mode 100644 index 0000000000..098b607f81 --- /dev/null +++ b/public/docs/_examples/testing/ts/1st-specs.html @@ -0,0 +1,40 @@ + + + + + + + 1st Specs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/docs/_examples/testing/ts/1st-specs.plnkr.json b/public/docs/_examples/testing/ts/1st-specs.plnkr.json new file mode 100644 index 0000000000..945ec90060 --- /dev/null +++ b/public/docs/_examples/testing/ts/1st-specs.plnkr.json @@ -0,0 +1,12 @@ +{ + "description": "Testing - 1st.specs", + "files":[ + "browser-test-shim.js", + "styles.css", + + "app/1st.spec.ts", + "1st-specs.html" + ], + "main": "1st-specs.html", + "tags": ["testing"] +} diff --git a/public/docs/_examples/testing/ts/1st.spec.ts b/public/docs/_examples/testing/ts/1st.spec.ts deleted file mode 100644 index bcac7a1aa7..0000000000 --- a/public/docs/_examples/testing/ts/1st.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -// #docplaster -// #docregion it - it('true is true', () => expect(true).toEqual(true)); - // #enddocregion it - -// #docregion describe - describe('1st tests', () => { - - it('true is true', () => expect(true).toEqual(true)); - - // #enddocregion describe - // #docregion another-test - it('null is not the same thing as undefined', - () => expect(null).not.toEqual(undefined) - ); - // #enddocregion another-test - - // #docregion describe -}); -// #enddocregion describe diff --git a/public/docs/_examples/testing/ts/app-specs.html b/public/docs/_examples/testing/ts/app-specs.html new file mode 100644 index 0000000000..c51f6cdedc --- /dev/null +++ b/public/docs/_examples/testing/ts/app-specs.html @@ -0,0 +1,52 @@ + + + + + + + Sample App Specs + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/docs/_examples/testing/ts/app-specs.plnkr.json b/public/docs/_examples/testing/ts/app-specs.plnkr.json new file mode 100644 index 0000000000..afd126bb45 --- /dev/null +++ b/public/docs/_examples/testing/ts/app-specs.plnkr.json @@ -0,0 +1,23 @@ +{ + "description": "Testing - app.specs", + "files":[ + "browser-test-shim.js", + "systemjs.config.extras.js", + "styles.css", + + "app/**/*.css", + "app/**/*.html", + "app/**/*.ts", + "app/**/*.spec.ts", + + "testing/*.ts", + + "!app/main.ts", + "!app/bag/*.*", + "!app/1st.spec.ts", + + "app-specs.html" + ], + "main": "app-specs.html", + "tags": ["testing"] +} diff --git a/public/docs/_examples/testing/ts/app/1st.spec.ts b/public/docs/_examples/testing/ts/app/1st.spec.ts new file mode 100644 index 0000000000..63f1ab134c --- /dev/null +++ b/public/docs/_examples/testing/ts/app/1st.spec.ts @@ -0,0 +1,5 @@ +// #docplaster +// #docregion +describe('1st tests', () => { + it('true is true', () => expect(true).toBe(true)); +}); diff --git a/public/docs/_examples/testing/ts/app/about.component.ts b/public/docs/_examples/testing/ts/app/about.component.ts new file mode 100644 index 0000000000..b2690f5a93 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/about.component.ts @@ -0,0 +1,12 @@ +// #docregion +import { Component } from '@angular/core'; + +@Component({ + template: ` +

About

+ +

All about this sample

+ `, + styleUrls: ['app/shared/styles.css'] +}) +export class AboutComponent { } diff --git a/public/docs/_examples/testing/ts/app/app.component.css b/public/docs/_examples/testing/ts/app/app.component.css deleted file mode 100644 index 137e9be7be..0000000000 --- a/public/docs/_examples/testing/ts/app/app.component.css +++ /dev/null @@ -1,31 +0,0 @@ -/* #docplaster */ -/* #docregion css */ -h1 { - font-size: 1.2em; - color: #999; - margin-bottom: 0; -} -h2 { - font-size: 2em; - margin-top: 0; - padding-top: 0; -} -nav a { - padding: 5px 10px; - text-decoration: none; - margin-top: 10px; - display: inline-block; - background-color: #eee; - border-radius: 4px; -} -nav a:visited, a:link { - color: #607D8B; -} -nav a:hover { - color: #039be5; - background-color: #CFD8DC; -} -nav a.router-link-active { - color: #039be5; -} -/* #enddocregion css */ diff --git a/public/docs/_examples/testing/ts/app/app.component.html b/public/docs/_examples/testing/ts/app/app.component.html new file mode 100644 index 0000000000..3690f3cf11 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/app.component.html @@ -0,0 +1,10 @@ + + + + + + diff --git a/public/docs/_examples/testing/ts/app/app.component.router.spec.ts b/public/docs/_examples/testing/ts/app/app.component.router.spec.ts new file mode 100644 index 0000000000..36e34a983e --- /dev/null +++ b/public/docs/_examples/testing/ts/app/app.component.router.spec.ts @@ -0,0 +1,201 @@ +// For more examples: +// https://github.com/angular/angular/blob/master/modules/@angular/router/test/integration.spec.ts + +import { async, ComponentFixture, fakeAsync, TestBed, tick, +} from '@angular/core/testing'; + +import { RouterTestingModule } from '@angular/router/testing'; +import { SpyLocation } from '@angular/common/testing'; + +// tslint:disable:no-unused-variable +import { newEvent } from '../testing'; +// tslint:enable:no-unused-variable + +// r - for relatively obscure router symbols +import * as r from '@angular/router'; +import { Router, RouterLinkWithHref } from '@angular/router'; + +import { By } from '@angular/platform-browser'; +import { DebugElement, Type } from '@angular/core'; +import { Location } from '@angular/common'; + +import { AppModule } from './app.module'; +import { AppComponent } from './app.component'; +import { AboutComponent } from './about.component'; +import { DashboardHeroComponent } from './dashboard/dashboard-hero.component'; +import { TwainService } from './shared/twain.service'; + +let comp: AppComponent; +let fixture: ComponentFixture; +let page: Page; +let router: Router; +let location: SpyLocation; + +describe('AppComponent & RouterTestingModule', () => { + + beforeEach( async(() => { + TestBed.configureTestingModule({ + imports: [ AppModule, RouterTestingModule ] + }) + .compileComponents(); + })); + + it('should navigate to "Dashboard" immediately', fakeAsync(() => { + createComponent(); + expect(location.path()).toEqual('/dashboard', 'after initialNavigation()'); + expectElementOf(DashboardHeroComponent); + })); + + it('should navigate to "About" on click', fakeAsync(() => { + createComponent(); + // page.aboutLinkDe.triggerEventHandler('click', null); // fails + // page.aboutLinkDe.nativeElement.dispatchEvent(newEvent('click')); // fails + page.aboutLinkDe.nativeElement.click(); // fails in phantom + + advance(); + expectPathToBe('/about'); + expectElementOf(AboutComponent); + + page.expectEvents([ + [r.NavigationStart, '/about'], [r.RoutesRecognized, '/about'], + [r.NavigationEnd, '/about'] + ]); + })); + + it('should navigate to "About" w/ browser location URL change', fakeAsync(() => { + createComponent(); + location.simulateHashChange('/about'); + // location.go('/about'); // also works ... except in plunker + advance(); + expectPathToBe('/about'); + expectElementOf(AboutComponent); + })); + + // Can't navigate to lazy loaded modules with this technique + xit('should navigate to "Heroes" on click', fakeAsync(() => { + createComponent(); + page.heroesLinkDe.nativeElement.click(); + advance(); + expectPathToBe('/heroes'); + })); + +}); + + +/////////////// +import { NgModuleFactoryLoader } from '@angular/core'; +import { SpyNgModuleFactoryLoader } from '@angular/router/testing'; + +import { HeroModule } from './hero/hero.module'; // should be lazy loaded +import { HeroListComponent } from './hero/hero-list.component'; + +let loader: SpyNgModuleFactoryLoader; + +///////// Can't get lazy loaded Heroes to work yet +xdescribe('AppComponent & Lazy Loading', () => { + + beforeEach( async(() => { + TestBed.configureTestingModule({ + imports: [ AppModule, RouterTestingModule ] + }) + .compileComponents(); + })); + + beforeEach(fakeAsync(() => { + createComponent(); + loader = TestBed.get(NgModuleFactoryLoader); + loader.stubbedModules = {expected: HeroModule}; + router.resetConfig([{path: 'heroes', loadChildren: 'expected'}]); + })); + + it('dummy', () => expect(true).toBe(true) ); + + + it('should navigate to "Heroes" on click', async(() => { + page.heroesLinkDe.nativeElement.click(); + advance(); + expectPathToBe('/heroes'); + expectElementOf(HeroListComponent); + })); + + xit('can navigate to "Heroes" w/ browser location URL change', fakeAsync(() => { + location.go('/heroes'); + advance(); + expectPathToBe('/heroes'); + expectElementOf(HeroListComponent); + + page.expectEvents([ + [r.NavigationStart, '/heroes'], [r.RoutesRecognized, '/heroes'], + [r.NavigationEnd, '/heroes'] + ]); + })); +}); + +////// Helpers ///////// + +/** Wait a tick, then detect changes */ +function advance(): void { + tick(); + fixture.detectChanges(); +} + +function createComponent() { + fixture = TestBed.createComponent(AppComponent); + comp = fixture.componentInstance; + + const injector = fixture.debugElement.injector; + location = injector.get(Location); + router = injector.get(Router); + router.initialNavigation(); + spyOn(injector.get(TwainService), 'getQuote') + .and.returnValue(Promise.resolve('Test Quote')); // fakes it + + advance(); + + page = new Page(); +} + +class Page { + aboutLinkDe: DebugElement; + dashboardLinkDe: DebugElement; + heroesLinkDe: DebugElement; + recordedEvents: any[] = []; + + // for debugging + comp: AppComponent; + location: SpyLocation; + router: Router; + fixture: ComponentFixture; + + expectEvents(pairs: any[]) { + const events = this.recordedEvents; + expect(events.length).toEqual(pairs.length, 'actual/expected events length mismatch'); + for (let i = 0; i < events.length; ++i) { + expect((events[i].constructor).name).toBe(pairs[i][0].name, 'unexpected event name'); + expect((events[i]).url).toBe(pairs[i][1], 'unexpected event url'); + } + } + + constructor() { + router.events.forEach(e => this.recordedEvents.push(e)); + const links = fixture.debugElement.queryAll(By.directive(RouterLinkWithHref)); + this.aboutLinkDe = links[2]; + this.dashboardLinkDe = links[0]; + this.heroesLinkDe = links[1]; + + // for debugging + this.comp = comp; + this.fixture = fixture; + this.router = router; + } +} + +function expectPathToBe(path: string, expectationFailOutput?: any) { + expect(location.path()).toEqual(path, expectationFailOutput || 'location.path()'); +} + +function expectElementOf(type: Type): any { + const el = fixture.debugElement.query(By.directive(type)); + expect(el).toBeTruthy('expected an element for ' + type.name); + return el; +} diff --git a/public/docs/_examples/testing/ts/app/app.component.spec.ts b/public/docs/_examples/testing/ts/app/app.component.spec.ts index f6c30b0e76..c9fd54535f 100644 --- a/public/docs/_examples/testing/ts/app/app.component.spec.ts +++ b/public/docs/_examples/testing/ts/app/app.component.spec.ts @@ -1,83 +1,119 @@ -/* tslint:disable:no-unused-variable */ -import { AppComponent } from './app.component'; - -import { By } from '@angular/platform-browser'; -import { DebugElement } from '@angular/core'; - -import { - async, inject +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { ComponentFixture, TestComponentBuilder } from '@angular/core/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { By } from '@angular/platform-browser'; -import { Hero, HeroService, MockHeroService } from './mock-hero.service'; +import { AppComponent } from './app.component'; +import { BannerComponent } from './banner.component'; +import { SharedModule } from './shared/shared.module'; -import { Router, MockRouter, - RouterLink, MockRouterLink, - RouterOutlet, MockRouterOutlet } from './mock-router'; +import { Router, FakeRouter, FakeRouterLinkDirective, FakeRouterOutletComponent +} from '../testing'; -describe('AppComponent', () => { - let fixture: ComponentFixture; - let comp: AppComponent; - beforeEach(async(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { - tcb - .overrideDirective(AppComponent, RouterLink, MockRouterLink) - .overrideDirective(AppComponent, RouterOutlet, MockRouterOutlet) - .overrideProviders(AppComponent, [ - { provide: HeroService, useClass: MockHeroService}, - { provide: Router, useClass: MockRouter}, - ]) - .createAsync(AppComponent) - .then(fix => { - fixture = fix; - comp = fixture.debugElement.componentInstance; - }); - }))); +let comp: AppComponent; +let fixture: ComponentFixture; + +describe('AppComponent & TestModule', () => { + beforeEach( async(() => { + TestBed.configureTestingModule({ + declarations: [ + AppComponent, BannerComponent, + FakeRouterLinkDirective, FakeRouterOutletComponent + ], + providers: [{ provide: Router, useClass: FakeRouter }], + schemas: [NO_ERRORS_SCHEMA] + }) + + .compileComponents() + + .then(() => { + fixture = TestBed.createComponent(AppComponent); + comp = fixture.componentInstance; + }); + + })); + + tests(); +}); + +function tests() { it('can instantiate it', () => { expect(comp).not.toBeNull(); }); - it('can get title from template', () => { - fixture.detectChanges(); - let titleEl = fixture.debugElement.query(By.css('h1')).nativeElement; - expect(titleEl.textContent).toContain(comp.title); - }); - it('can get RouterLinks from template', () => { fixture.detectChanges(); - let links = fixture.debugElement - .queryAll(By.directive(MockRouterLink)) - .map(de => de.injector.get(MockRouterLink) ); + const links = fixture.debugElement + // find all elements with an attached FakeRouterLink directive + .queryAll(By.directive(FakeRouterLinkDirective)) + // use injector to get the RouterLink directive instance attached to each element + .map(de => de.injector.get(FakeRouterLinkDirective) as FakeRouterLinkDirective); - expect(links.length).toEqual(2, 'should have 2 links'); - expect(links[0].routeParams[0]).toEqual('Dashboard', '1st link should go to Dashboard'); - expect(links[1].routeParams[0]).toEqual('Heroes', '1st link should go to Heroes'); - - let result = links[1].onClick(); - expect(result).toEqual(false, 'click should prevent default browser behavior'); + expect(links.length).toBe(3, 'should have 3 links'); + expect(links[0].linkParams).toBe('/dashboard', '1st link should go to Dashboard'); + expect(links[1].linkParams).toBe('/heroes', '1st link should go to Heroes'); }); it('can click Heroes link in template', () => { fixture.detectChanges(); // Heroes RouterLink DebugElement - let heroesDe = fixture.debugElement - .queryAll(By.directive(MockRouterLink))[1]; + const heroesLinkDe = fixture.debugElement + .queryAll(By.directive(FakeRouterLinkDirective))[1]; - expect(heroesDe).toBeDefined('should have a 2nd RouterLink'); + expect(heroesLinkDe).toBeDefined('should have a 2nd RouterLink'); - let link = heroesDe.injector.get(MockRouterLink); + const link = heroesLinkDe.injector.get(FakeRouterLinkDirective) as FakeRouterLinkDirective; expect(link.navigatedTo).toBeNull('link should not have navigate yet'); - heroesDe.triggerEventHandler('click', null); + heroesLinkDe.triggerEventHandler('click', null); fixture.detectChanges(); - expect(link.navigatedTo[0]).toEqual('Heroes'); - + expect(link.navigatedTo).toBe('/heroes'); }); +} + +//////// Testing w/ real root module ////// +// Best to avoid +// Tricky because we are disabling the router and its configuration +import { AppModule } from './app.module'; + +describe('AppComponent & AppModule', () => { + + beforeEach( async(() => { + + TestBed.configureTestingModule({ + imports: [ AppModule ], + }) + + .overrideModule(AppModule, { + // Must get rid of `RouterModule.forRoot` to prevent attempt to configure a router + // Can't remove it because it doesn't have a known type (`forRoot` returns an object) + // therefore, must reset the entire `imports` with just the necessary stuff + set: { imports: [ SharedModule ]} + }) + + // Separate override because cannot both `set` and `add/remove` in same override + .overrideModule(AppModule, { + add: { + declarations: [ FakeRouterLinkDirective, FakeRouterOutletComponent ], + providers: [{ provide: Router, useClass: FakeRouter }] + } + }) + + .compileComponents() + + .then(() => { + fixture = TestBed.createComponent(AppComponent); + comp = fixture.componentInstance; + }); + })); + + tests(); }); diff --git a/public/docs/_examples/testing/ts/app/app.component.ts b/public/docs/_examples/testing/ts/app/app.component.ts index f2da2da067..156feee06d 100644 --- a/public/docs/_examples/testing/ts/app/app.component.ts +++ b/public/docs/_examples/testing/ts/app/app.component.ts @@ -1,53 +1,8 @@ -// #docplaster // #docregion -import { Component } from '@angular/core'; - -// Can't test with ROUTER_DIRECTIVES yet -// import { RouteConfig, ROUTER_DIRECTIVES, ROUTER_PROVIDERS } from '@angular/router-deprecated'; - -import { RouteConfig, RouterLink, - RouterOutlet, ROUTER_PROVIDERS } from '@angular/router-deprecated'; - -import { DashboardComponent } from './dashboard.component'; -import { HeroesComponent } from './heroes.component'; -import { HeroDetailComponent } from './hero-detail.component'; -import { HeroService } from './hero.service'; - -import { BAG_DIRECTIVES, BAG_PROVIDERS } from './bag'; +import { Component } from '@angular/core'; @Component({ selector: 'my-app', - template: ` -

{{title}}

- - -
-

Bag-a-specs

- -

External Template Comp

- -

Comp With External Template Comp

- - `, - /* - - */ - styleUrls: ['app/app.component.css'], - directives: [RouterLink, RouterOutlet, BAG_DIRECTIVES], - providers: [ - ROUTER_PROVIDERS, - HeroService, - BAG_PROVIDERS - ] + templateUrl: 'app/app.component.html' }) -@RouteConfig([ - { path: '/dashboard', name: 'Dashboard', component: DashboardComponent, useAsDefault: true }, - { path: '/detail/:id', name: 'HeroDetail', component: HeroDetailComponent }, - { path: '/heroes', name: 'Heroes', component: HeroesComponent } -]) -export class AppComponent { - title = 'Tour of Heroes'; -} +export class AppComponent { } diff --git a/public/docs/_examples/testing/ts/app/app.module.ts b/public/docs/_examples/testing/ts/app/app.module.ts new file mode 100644 index 0000000000..adea748781 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/app.module.ts @@ -0,0 +1,33 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { RouterModule } from '@angular/router'; + + +import { AppComponent } from './app.component'; +import { AboutComponent } from './about.component'; +import { BannerComponent } from './banner.component'; +import { HeroService, + UserService } from './model'; +import { TwainService } from './shared/twain.service'; +import { WelcomeComponent } from './welcome.component'; + + +import { DashboardModule } from './dashboard/dashboard.module'; +import { SharedModule } from './shared/shared.module'; + +@NgModule({ + imports: [ + BrowserModule, + DashboardModule, + RouterModule.forRoot([ + { path: '', redirectTo: 'dashboard', pathMatch: 'full'}, + { path: 'about', component: AboutComponent }, + { path: 'heroes', loadChildren: 'app/hero/hero.module#HeroModule'} + ]), + SharedModule + ], + providers: [ HeroService, TwainService, UserService ], + declarations: [ AppComponent, AboutComponent, BannerComponent, WelcomeComponent ], + bootstrap: [ AppComponent ] +}) +export class AppModule { } diff --git a/public/docs/_examples/testing/ts/app/bad-tests.spec.ts b/public/docs/_examples/testing/ts/app/bad-tests.spec.ts deleted file mode 100644 index d73882372c..0000000000 --- a/public/docs/_examples/testing/ts/app/bad-tests.spec.ts +++ /dev/null @@ -1,163 +0,0 @@ -// Based on https://github.com/angular/angular/blob/master/modules/angular2/test/testing/testing_public_spec.ts -/* tslint:disable:no-unused-variable */ -/** - * Tests that show what goes wrong when the tests are incorrectly written or have a problem - */ -import { - BadTemplateUrlComp, ButtonComp, - ChildChildComp, ChildComp, ChildWithChildComp, - ExternalTemplateComp, - FancyService, MockFancyService, - InputComp, - MyIfComp, MyIfChildComp, MyIfParentComp, - MockChildComp, MockChildChildComp, - ParentComp, - TestProvidersComp, TestViewProvidersComp -} from './bag'; - -import { DebugElement } from '@angular/core'; -import { By } from '@angular/platform-browser'; - -import { - addProviders, - async, inject -} from '@angular/core/testing'; - -import { ComponentFixture, TestComponentBuilder } from '@angular/core/testing'; - -import { ViewMetadata } from '@angular/core'; -import { Observable } from 'rxjs/Rx'; - -//////// SPECS ///////////// - -xdescribe('async & inject testing errors', () => { - let originalJasmineIt: any; - let originalJasmineBeforeEach: any; - - let patchJasmineIt = () => { - return new Promise((resolve, reject) => { - originalJasmineIt = jasmine.getEnv().it; - jasmine.getEnv().it = (description: string, fn: Function): jasmine.Spec => { - let done = () => { resolve(); }; - (done).fail = (err: any) => { reject(err); }; - fn(done); - return null; - }; - }); - }; - - let restoreJasmineIt = () => { jasmine.getEnv().it = originalJasmineIt; }; - - let patchJasmineBeforeEach = () => { - return new Promise((resolve, reject) => { - originalJasmineBeforeEach = jasmine.getEnv().beforeEach; - jasmine.getEnv().beforeEach = (fn: any): void => { - let done = () => { resolve(); }; - (done).fail = (err: any) => { reject(err); }; - fn(done); - return null; - }; - }); - }; - - let restoreJasmineBeforeEach = - () => { jasmine.getEnv().beforeEach = originalJasmineBeforeEach; }; - - const shouldNotSucceed = - (done: DoneFn) => () => done.fail( 'Expected an error, but did not get one.'); - - const shouldFail = - (done: DoneFn, emsg: string) => (err: any) => { expect(err).toEqual(emsg); done(); }; - - it('should fail when an asynchronous error is thrown', (done: DoneFn) => { - let itPromise = patchJasmineIt(); - - it('throws an async error', - async(inject([], () => { setTimeout(() => { throw new Error('bar'); }, 0); }))); - - itPromise.then( - shouldNotSucceed(done), - err => { - expect(err).toEqual('bar'); - done(); - }); - restoreJasmineIt(); - }); - - it('should fail when a returned promise is rejected', (done: DoneFn) => { - let itPromise = patchJasmineIt(); - - it('should fail with an error from a promise', async(() => { - return Promise.reject('baz'); - })); - - itPromise.then( - shouldNotSucceed(done), - err => { - expect(err).toEqual('Uncaught (in promise): baz'); - done(); - }); - restoreJasmineIt(); - }); - - it('should fail when an error occurs inside inject', (done: DoneFn) => { - let itPromise = patchJasmineIt(); - - it('throws an error', inject([], () => { throw new Error('foo'); })); - - itPromise.then( - shouldNotSucceed(done), - shouldFail(done, 'foo') - ); - restoreJasmineIt(); - }); - - // TODO(juliemr): reenable this test when we are using a test zone and can capture this error. - it('should fail when an asynchronous error is thrown', (done: DoneFn) => { - let itPromise = patchJasmineIt(); - - it('throws an async error', - async(inject([], () => { setTimeout(() => { throw new Error('bar'); }, 0); }))); - - itPromise.then( - shouldNotSucceed(done), - shouldFail(done, 'bar') - ); - restoreJasmineIt(); - }); - - it('should fail when XHR loading of a template fails', (done: DoneFn) => { - let itPromise = patchJasmineIt(); - - it('should fail with an error from a promise', - async(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { - tcb.createAsync(BadTemplateUrlComp); - }))); - - itPromise.then( - shouldNotSucceed(done), - shouldFail(done, 'Uncaught (in promise): Failed to load non-existant.html') - ); - restoreJasmineIt(); - }, 10000); - - describe('using addProviders', () => { - addProviders([{ provide: FancyService, useValue: new FancyService() }]); - - beforeEach( - inject([FancyService], (service: FancyService) => { expect(service.value).toEqual('real value'); })); - - describe('nested addProviders', () => { - - it('should fail when the injector has already been used', () => { - patchJasmineBeforeEach(); - expect(() => { - addProviders([{ provide: FancyService, useValue: new FancyService() }]); - }) - .toThrowError('addProviders was called after the injector had been used ' + - 'in a beforeEach or it block. This invalidates the test injector'); - restoreJasmineBeforeEach(); - }); - }); - }); -}); diff --git a/public/docs/_examples/testing/ts/app/bag.spec.ts b/public/docs/_examples/testing/ts/app/bag.spec.ts deleted file mode 100644 index 7218e91f1b..0000000000 --- a/public/docs/_examples/testing/ts/app/bag.spec.ts +++ /dev/null @@ -1,460 +0,0 @@ -// Based on https://github.com/angular/angular/blob/master/modules/angular2/test/testing/testing_public_spec.ts -/* tslint:disable */ -import { - ButtonComp, - ChildChildComp, ChildComp, ChildWithChildComp, - ExternalTemplateComp, - FancyService, MockFancyService, - InputComp, - MyIfComp, MyIfChildComp, MyIfParentComp, - MockChildComp, MockChildChildComp, - ParentComp, - TestProvidersComp, TestViewProvidersComp -} from './bag'; - -import { DebugElement } from '@angular/core'; -import { By } from '@angular/platform-browser'; - -import { - addProviders, - inject, async, - fakeAsync, tick, withProviders -} from '@angular/core/testing'; - -import { ComponentFixture, TestComponentBuilder } from '@angular/core/testing'; - -import { ViewMetadata } from '@angular/core'; - -import { Observable } from 'rxjs/Rx'; - -//////// SPECS ///////////// - -describe('using the async helper', () => { - let actuallyDone = false; - - beforeEach(() => { actuallyDone = false; }); - - afterEach(() => { expect(actuallyDone).toEqual(true); }); - - it('should run normal test', () => { actuallyDone = true; }); - - it('should run normal async test', (done: DoneFn) => { - setTimeout(() => { - actuallyDone = true; - done(); - }, 0); - }); - - it('should run async test with task', - async(() => { setTimeout(() => { actuallyDone = true; }, 0); })); - - it('should run async test with successful promise', async(() => { - let p = new Promise(resolve => { setTimeout(resolve, 10); }); - p.then(() => { actuallyDone = true; }); - })); - - it('should run async test with failed promise', async(() => { - let p = new Promise((resolve, reject) => { setTimeout(reject, 10); }); - p.catch(() => { actuallyDone = true; }); - })); - - xit('should run async test with successful Observable', async(() => { - let source = Observable.of(true).delay(10); - source.subscribe( - val => {}, - err => fail(err), - () => { actuallyDone = true; } // completed - ); - })); -}); - -describe('using the test injector with the inject helper', () => { - - describe('setting up Providers with FancyService', () => { - beforeEach(() => { - addProviders([ - { provide: FancyService, useValue: new FancyService() } - ]); - }); - - it('should use FancyService', - inject([FancyService], (service: FancyService) => { - expect(service.value).toEqual('real value'); - })); - - it('test should wait for FancyService.getAsyncValue', - async(inject([FancyService], (service: FancyService) => { - service.getAsyncValue().then( - value => { expect(value).toEqual('async value'); }); - }))); - - it('test should wait for FancyService.getTimeoutValue', - async(inject([FancyService], (service: FancyService) => { - service.getTimeoutValue().then( - value => { expect(value).toEqual('timeout value'); }); - }))); - - it('test should wait for FancyService.getObservableValue', - async(inject([FancyService], (service: FancyService) => { - service.getObservableValue().subscribe( - value => { expect(value).toEqual('observable value'); } - ); - }))); - - xit('test should wait for FancyService.getObservableDelayValue', - async(inject([FancyService], (service: FancyService) => { - service.getObservableDelayValue().subscribe( - value => { expect(value).toEqual('observable delay value'); } - ); - }))); - - it('should allow the use of fakeAsync (Experimental)', - fakeAsync(inject([FancyService], (service: FancyService) => { - let value: any; - service.getAsyncValue().then((val: any) => value = val); - tick(); // Trigger JS engine cycle until all promises resolve. - expect(value).toEqual('async value'); - }))); - - describe('using inner beforeEach to inject-and-modify FancyService', () => { - beforeEach(inject([FancyService], (service: FancyService) => { - service.value = 'value modified in beforeEach'; - })); - - it('should use modified providers', - inject([FancyService], (service: FancyService) => { - expect(service.value).toEqual('value modified in beforeEach'); - })); - }); - - describe('using async within beforeEach', () => { - beforeEach(async(inject([FancyService], (service: FancyService) => { - service.getAsyncValue().then(value => { service.value = value; }); - }))); - - it('should use asynchronously modified value ... in synchronous test', - inject([FancyService], (service: FancyService) => { - expect(service.value).toEqual('async value'); })); - }); - }); - - describe('using `withProviders` for per-test provision', () => { - it('should inject test-local FancyService for this test', - // `withProviders`: set up providers at individual test level - withProviders(() => [{ provide: FancyService, useValue: {value: 'fake value' }}]) - - // now inject and test - .inject([FancyService], (service: FancyService) => { - expect(service.value).toEqual('fake value'); - })); - }); -}); - -describe('test component builder', function() { - it('should instantiate a component with valid DOM', - async(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { - - tcb.createAsync(ChildComp).then(fixture => { - fixture.detectChanges(); - expect(fixture.nativeElement.textContent).toContain('Original Child'); - }); - }))); - - it('should allow changing members of the component', - async(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { - - tcb.createAsync(MyIfComp).then(fixture => { - fixture.detectChanges(); - expect(fixture.nativeElement.textContent).toContain('MyIf()'); - - fixture.debugElement.componentInstance.showMore = true; - fixture.detectChanges(); - expect(fixture.nativeElement.textContent).toContain('MyIf(More)'); - }); - }))); - - it('should support clicking a button', - async(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { - - tcb.createAsync(ButtonComp).then(fixture => { - - let comp = fixture.componentInstance; - expect(comp.wasClicked).toEqual(false, 'wasClicked should be false at start'); - - let btn = fixture.debugElement.query(By.css('button')); - // let btn = fixture.debugElement.query(el => el.name === 'button'); // the hard way - - btn.triggerEventHandler('click', null); - // btn.nativeElement.click(); // this often works too ... but not all the time! - expect(comp.wasClicked).toEqual(true, 'wasClicked should be true after click'); - }); - }))); - - it('should support entering text in input box (ngModel)', - async(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { - let origName = 'John'; - let newName = 'Sally'; - - tcb.createAsync(InputComp).then(fixture => { - - let comp = 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', - async(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { - - tcb.overrideTemplate(MockChildComp, 'Mock') - .createAsync(MockChildComp) - .then(fixture => { - fixture.detectChanges(); - expect(fixture.nativeElement.textContent).toContain('Mock'); - }); - }))); - - it('should override a view', - async(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { - - tcb.overrideView( - ChildComp, - new ViewMetadata({template: 'Modified {{childBinding}}'}) - ) - .createAsync(ChildComp) - .then(fixture => { - fixture.detectChanges(); - expect(fixture.nativeElement.textContent).toContain('Modified Child'); - - }); - }))); - - it('should override component directives', - async(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { - - tcb.overrideDirective(ParentComp, ChildComp, MockChildComp) - .createAsync(ParentComp) - .then(fixture => { - fixture.detectChanges(); - expect(fixture.nativeElement.textContent).toContain('Parent(Mock)'); - - }); - }))); - - - it('should override child component\'s directives', - async(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { - - tcb.overrideDirective(ParentComp, ChildComp, ChildWithChildComp) - .overrideDirective(ChildWithChildComp, ChildChildComp, MockChildChildComp) - .createAsync(ParentComp) - .then(fixture => { - fixture.detectChanges(); - expect(fixture.nativeElement.textContent) - .toContain('Parent(Original Child(ChildChild Mock))'); - - }); - }))); - - it('should override a provider', - async(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { - - tcb.overrideProviders( - TestProvidersComp, - [{ provide: FancyService, useClass: MockFancyService }] - ) - .createAsync(TestProvidersComp) - .then(fixture => { - fixture.detectChanges(); - expect(fixture.nativeElement.textContent) - .toContain('injected value: mocked out value'); - }); - }))); - - it('should override a viewProvider', - async(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { - - tcb.overrideViewProviders( - TestViewProvidersComp, - [{ provide: FancyService, useClass: MockFancyService }] - ) - .createAsync(TestViewProvidersComp) - .then(fixture => { - fixture.detectChanges(); - expect(fixture.nativeElement.textContent) - .toContain('injected value: mocked out value'); - }); - }))); - - it('should allow an external templateUrl', - async(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { - - tcb.createAsync(ExternalTemplateComp) - .then(fixture => { - fixture.detectChanges(); - expect(fixture.nativeElement.textContent) - .toContain('from external template\n'); - }); - })), 10000); // Long timeout because this test makes an actual XHR. - - describe('(lifecycle hooks w/ MyIfParentComp)', () => { - let fixture: ComponentFixture; - let parent: MyIfParentComp; - let child: MyIfChildComp; - - /** - * Get the MyIfChildComp from parent; fail w/ good message if cannot. - */ - function getChild() { - - let childDe: DebugElement; // DebugElement that should hold the MyIfChildComp - - // The Hard Way: requires detailed knowledge of the parent template - try { - childDe = fixture.debugElement.children[4].children[0]; - } catch (err) { /* we'll report the error */ } - - // DebugElement.queryAll: if we wanted all of many instances: - childDe = fixture.debugElement - .queryAll(function (de) { return de.componentInstance instanceof MyIfChildComp; })[0]; - - // WE'LL USE THIS APPROACH ! - // DebugElement.query: find first instance (if any) - childDe = fixture.debugElement - .query(function (de) { return de.componentInstance instanceof MyIfChildComp; }); - - if (childDe && childDe.componentInstance) { - child = childDe.componentInstance; - } else { - fail('Unable to find MyIfChildComp within MyIfParentComp'); - } - - return child; - } - - // Create MyIfParentComp TCB and component instance before each test (async beforeEach) - beforeEach(async(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { - tcb.createAsync(MyIfParentComp) - .then(fix => { - fixture = fix; - parent = fixture.debugElement.componentInstance; - }); - }))); - - it('should instantiate parent component', () => { - expect(parent).not.toBeNull('parent component should exist'); - }); - - it('parent component OnInit should NOT be called before first detectChanges()', () => { - expect(parent.ngOnInitCalled).toEqual(false); - }); - - it('parent component OnInit should be called after first detectChanges()', () => { - fixture.detectChanges(); - expect(parent.ngOnInitCalled).toEqual(true); - }); - - it('child component should exist after OnInit', () => { - fixture.detectChanges(); - getChild(); - expect(child instanceof MyIfChildComp).toEqual(true, 'should create child'); - }); - - it('should have called child component\'s OnInit ', () => { - fixture.detectChanges(); - getChild(); - expect(child.ngOnInitCalled).toEqual(true); - }); - - it('child component called OnChanges once', () => { - fixture.detectChanges(); - getChild(); - expect(child.ngOnChangesCounter).toEqual(1); - }); - - it('changed parent value flows to child', () => { - fixture.detectChanges(); - getChild(); - - parent.parentValue = 'foo'; - fixture.detectChanges(); - - expect(child.ngOnChangesCounter).toEqual(2, - 'expected 2 changes: initial value and changed value'); - expect(child.childValue).toEqual('foo', - 'childValue should eq changed parent value'); - }); - - it('changed child value flows to parent', async(() => { - fixture.detectChanges(); - getChild(); - - child.childValue = 'bar'; - - return new Promise(resolve => { - // Wait one JS engine turn! - setTimeout(() => resolve(), 0); - }).then(() => { - fixture.detectChanges(); - - expect(child.ngOnChangesCounter).toEqual(2, - 'expected 2 changes: initial value and changed value'); - expect(parent.parentValue).toEqual('bar', - 'parentValue should eq changed parent value'); - }); - - })); - - it('clicking "Close Child" triggers child OnDestroy', () => { - fixture.detectChanges(); - getChild(); - - let btn = fixture.debugElement.query(By.css('button')); - btn.triggerEventHandler('click', null); - - fixture.detectChanges(); - expect(child.ngOnDestroyCalled).toEqual(true); - }); - - }); -}); - - -//////// Testing Framework Bugs? ///// -import { HeroService } from './hero.service'; -import { Component } from '@angular/core'; - -@Component({ - selector: 'another-comp', - template: `AnotherProvidersComp()`, - providers: [FancyService] // <======= BOOM! if we comment out - // Failed: 'undefined' is not an object (evaluating 'dm.providers.concat') -}) -export class AnotherProvidersComp { - constructor( - private _heroService: HeroService - ) { } -} - -describe('tcb.overrideProviders', () => { - it('Component must have at least one provider else crash', - async(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { - - tcb.overrideProviders( - AnotherProvidersComp, - [{ provide: HeroService, useValue: {}} ] - ) - .createAsync(AnotherProvidersComp); - }))); -}); diff --git a/public/docs/_examples/testing/ts/app/bag.ts b/public/docs/_examples/testing/ts/app/bag.ts deleted file mode 100644 index 26f47e8b3d..0000000000 --- a/public/docs/_examples/testing/ts/app/bag.ts +++ /dev/null @@ -1,255 +0,0 @@ -// Based on https://github.com/angular/angular/blob/master/modules/angular2/test/testing/testing_public_spec.ts -/* tslint:disable */ -import { Component, EventEmitter, Injectable, Input, Output, Optional, - OnInit, OnChanges, OnDestroy, SimpleChange } from '@angular/core'; - -import { Observable } from 'rxjs/Rx'; - -////////// The App: Services and Components for the tests. ////////////// - -////////// Services /////////////// - -@Injectable() -export class FancyService { - value: string = 'real value'; - - getValue() { return this.value; } - - getAsyncValue() { return Promise.resolve('async value'); } - - getObservableValue() { return Observable.of('observable value'); } - - getTimeoutValue() { - return new Promise((resolve, reject) => { setTimeout(() => {resolve('timeout value'); }, 10); }); - } - - getObservableDelayValue() { return Observable.of('observable delay value').delay(10); } -} - -@Injectable() -export class MockFancyService extends FancyService { - value: string = 'mocked out value'; -} - -//////////// Components ///////////// - -@Component({ - selector: 'button-comp', - template: `` -}) -export class ButtonComp { - wasClicked = false; - clicked() { this.wasClicked = true; } -} - -@Component({ - selector: 'input-comp', - template: `` -}) -export class InputComp { - name = 'John'; -} - -@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)` -}) -export class MyIfComp { - showMore = 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: module.id, - selector: 'external-template-comp', - templateUrl: 'bag-external-template.html' -}) -export class ExternalTemplateComp { - serviceValue: string; - - constructor(@Optional() private service: FancyService) { } - - ngOnInit() { - if (this.service) { this.serviceValue = this.service.getValue(); } - } -} - -@Component({ - selector: 'comp-w-ext-comp', - template: ` -

comp-w-ext-comp

- - `, - directives: [ExternalTemplateComp] -}) -export class CompWithCompWithExternalTemplate { } - -@Component({ - selector: 'bad-template-comp', - templateUrl: 'non-existant.html' -}) -export class BadTemplateUrlComp { } - - -///////// MyIfChildComp //////// -@Component({ - selector: 'my-if-child-comp', - - template: ` -

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'; - } -} - -export const BAG_PROVIDERS = [FancyService]; - -export const BAG_DIRECTIVES = [ - ButtonComp, - ChildChildComp, ChildComp, ChildWithChildComp, - ExternalTemplateComp, CompWithCompWithExternalTemplate, - InputComp, - MyIfComp, MyIfChildComp, MyIfParentComp, - MockChildComp, MockChildChildComp, - ParentComp, - TestProvidersComp, TestViewProvidersComp -]; diff --git a/public/docs/_examples/testing/ts/app/bag/async-helper.spec.ts b/public/docs/_examples/testing/ts/app/bag/async-helper.spec.ts new file mode 100644 index 0000000000..90ed17e92b --- /dev/null +++ b/public/docs/_examples/testing/ts/app/bag/async-helper.spec.ts @@ -0,0 +1,56 @@ +import { async, fakeAsync, tick } from '@angular/core/testing'; + +import { Observable } from 'rxjs/Observable'; + +describe('Angular async helper', () => { + let actuallyDone = false; + + beforeEach(() => { actuallyDone = false; }); + + afterEach(() => { expect(actuallyDone).toBe(true, 'actuallyDone should be true'); }); + + it('should run normal test', () => { actuallyDone = true; }); + + it('should run normal async test', (done: DoneFn) => { + setTimeout(() => { + actuallyDone = true; + done(); + }, 0); + }); + + it('should run async test with task', + async(() => { setTimeout(() => { actuallyDone = true; }, 0); })); + + it('should run async test with successful promise', async(() => { + const p = new Promise(resolve => { setTimeout(resolve, 10); }); + p.then(() => { actuallyDone = true; }); + })); + + it('should run async test with failed promise', async(() => { + const p = new Promise((resolve, reject) => { setTimeout(reject, 10); }); + p.catch(() => { actuallyDone = true; }); + })); + + // Fail message: Cannot use setInterval from within an async zone test + // See https://github.com/angular/angular/issues/10127 + xit('should run async test with successful delayed Observable', async(() => { + const source = Observable.of(true).delay(10); + source.subscribe( + val => actuallyDone = true, + err => fail(err) + ); + })); + + // Fail message: Error: 1 periodic timer(s) still in the queue + // See https://github.com/angular/angular/issues/10127 + xit('should run async test with successful delayed Observable', fakeAsync(() => { + const source = Observable.of(true).delay(10); + source.subscribe( + val => actuallyDone = true, + err => fail(err) + ); + + tick(); + })); + +}); diff --git a/public/docs/_examples/testing/ts/app/bag-external-template.html b/public/docs/_examples/testing/ts/app/bag/bag-external-template.html similarity index 100% rename from public/docs/_examples/testing/ts/app/bag-external-template.html rename to public/docs/_examples/testing/ts/app/bag/bag-external-template.html diff --git a/public/docs/_examples/testing/ts/app/bag/bag-main.ts b/public/docs/_examples/testing/ts/app/bag/bag-main.ts new file mode 100644 index 0000000000..27b78200ae --- /dev/null +++ b/public/docs/_examples/testing/ts/app/bag/bag-main.ts @@ -0,0 +1,5 @@ +// main app entry point +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; +import { BagModule } from './bag'; + +platformBrowserDynamic().bootstrapModule(BagModule); diff --git a/public/docs/_examples/testing/ts/app/bag/bag.no-testbed.spec.ts b/public/docs/_examples/testing/ts/app/bag/bag.no-testbed.spec.ts new file mode 100644 index 0000000000..6bdbe86cd0 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/bag/bag.no-testbed.spec.ts @@ -0,0 +1,130 @@ +// #docplaster +import { DependentService, FancyService } from './bag'; + +///////// Fakes ///////// +export class FakeFancyService extends FancyService { + value: string = 'faked value'; +} +//////////////////////// +// #docregion FancyService +// Straight Jasmine - no imports from Angular test libraries + +describe('FancyService without the TestBed', () => { + let service: FancyService; + + beforeEach(() => { service = new FancyService(); }); + + it('#getValue should return real value', () => { + expect(service.getValue()).toBe('real value'); + }); + + it('#getAsyncValue should return async value', done => { + service.getAsyncValue().then(value => { + expect(value).toBe('async value'); + done(); + }); + }); + + // #docregion getTimeoutValue + it('#getTimeoutValue should return timeout value', done => { + service = new FancyService(); + service.getTimeoutValue().then(value => { + expect(value).toBe('timeout value'); + done(); + }); + }); + // #enddocregion getTimeoutValue + + it('#getObservableValue should return observable value', done => { + service.getObservableValue().subscribe(value => { + expect(value).toBe('observable value'); + done(); + }); + }); + +}); +// #enddocregion FancyService + +// DependentService requires injection of a FancyService +// #docregion DependentService +describe('DependentService without the TestBed', () => { + let service: DependentService; + + it('#getValue should return real value by way of the real FancyService', () => { + service = new DependentService(new FancyService()); + expect(service.getValue()).toBe('real value'); + }); + + it('#getValue should return faked value by way of a fakeService', () => { + service = new DependentService(new FakeFancyService()); + expect(service.getValue()).toBe('faked value'); + }); + + it('#getValue should return faked value from a fake object', () => { + const fake = { getValue: () => 'fake value' }; + service = new DependentService(fake as FancyService); + expect(service.getValue()).toBe('fake value'); + }); + + it('#getValue should return stubbed value from a FancyService spy', () => { + const fancy = new FancyService(); + const stubValue = 'stub value'; + const spy = spyOn(fancy, 'getValue').and.returnValue(stubValue); + service = new DependentService(fancy); + + expect(service.getValue()).toBe(stubValue, 'service returned stub value'); + expect(spy.calls.count()).toBe(1, 'stubbed method was called once'); + expect(spy.calls.mostRecent().returnValue).toBe(stubValue); + }); +}); +// #enddocregion DependentService + +// #docregion ReversePipe +import { ReversePipe } from './bag'; + +describe('ReversePipe', () => { + let pipe: ReversePipe; + + beforeEach(() => { pipe = new ReversePipe(); }); + + it('transforms "abc" to "cba"', () => { + expect(pipe.transform('abc')).toBe('cba'); + }); + + it('no change to palindrome: "able was I ere I saw elba"', () => { + const palindrome = 'able was I ere I saw elba'; + expect(pipe.transform(palindrome)).toBe(palindrome); + }); + +}); +// #enddocregion ReversePipe + + +import { ButtonComponent } from './bag'; +// #docregion ButtonComp +describe('ButtonComp', () => { + let comp: ButtonComponent; + beforeEach(() => comp = new ButtonComponent()); + + it('#isOn should be false initially', () => { + expect(comp.isOn).toBe(false); + }); + + it('#clicked() should set #isOn to true', () => { + comp.clicked(); + expect(comp.isOn).toBe(true); + }); + + it('#clicked() should set #message to "is on"', () => { + comp.clicked(); + expect(comp.message).toMatch(/is on/i); + }); + + it('#clicked() should toggle #isOn', () => { + comp.clicked(); + expect(comp.isOn).toBe(true); + comp.clicked(); + expect(comp.isOn).toBe(false); + }); +}); +// #enddocregion ButtonComp diff --git a/public/docs/_examples/testing/ts/app/bag/bag.spec.ts b/public/docs/_examples/testing/ts/app/bag/bag.spec.ts new file mode 100644 index 0000000000..1fede16bd7 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/bag/bag.spec.ts @@ -0,0 +1,674 @@ +// #docplaster +import { + BagModule, + BankAccountComponent, BankAccountParentComponent, + ButtonComponent, + Child1Component, Child2Component, Child3Component, + FancyService, + ExternalTemplateComponent, + InputComponent, + IoComponent, IoParentComponent, + MyIfComponent, MyIfChildComponent, MyIfParentComponent, + NeedsContentComponent, ParentComponent, + TestProvidersComponent, TestViewProvidersComponent, + ReversePipeComponent, ShellComponent +} from './bag'; + +import { By } from '@angular/platform-browser'; +import { Component, + DebugElement, + Injectable } from '@angular/core'; +import { FormsModule } from '@angular/forms'; + +// Forms symbols imported only for a specific test below +import { NgModel, NgControl } from '@angular/forms'; + +import { async, ComponentFixture, fakeAsync, inject, TestBed, tick +} from '@angular/core/testing'; + +import { addMatchers, newEvent } from '../../testing'; + +beforeEach( addMatchers ); + +//////// Service Tests ///////////// +// #docregion FancyService +describe('use inject helper in beforeEach', () => { + let service: FancyService; + + beforeEach(() => { + TestBed.configureTestingModule({ providers: [FancyService] }); + + // `TestBed.get` returns the injectable or an + // alternative object (including null) if the service provider is not found. + // Of course it will be found in this case because we're providing it. + // #docregion testbed-get + service = TestBed.get(FancyService, null); + // #enddocregion testbed-get + }); + + it('should use FancyService', () => { + expect(service.getValue()).toBe('real value'); + }); + + it('should use FancyService', () => { + expect(service.getValue()).toBe('real value'); + }); + + it('test should wait for FancyService.getAsyncValue', async(() => { + service.getAsyncValue().then( + value => expect(value).toBe('async value') + ); + })); + + it('test should wait for FancyService.getTimeoutValue', async(() => { + service.getTimeoutValue().then( + value => expect(value).toBe('timeout value') + ); + })); + + it('test should wait for FancyService.getObservableValue', async(() => { + service.getObservableValue().subscribe( + value => expect(value).toBe('observable value') + ); + })); + + // #enddocregion FancyService + // See https://github.com/angular/angular/issues/10127 + xit('test should wait for FancyService.getObservableDelayValue', async(() => { + service.getObservableDelayValue().subscribe( + value => expect(value).toBe('observable delay value') + ); + })); + // #docregion FancyService + it('should allow the use of fakeAsync', fakeAsync(() => { + let value: any; + service.getAsyncValue().then((val: any) => value = val); + tick(); // Trigger JS engine cycle until all promises resolve. + expect(value).toBe('async value'); + })); +}); +// #enddocregion FancyService + +describe('use inject within `it`', () => { + // #docregion getTimeoutValue + beforeEach(() => { + TestBed.configureTestingModule({ providers: [FancyService] }); + }); + + // #enddocregion getTimeoutValue + + it('should use modified providers', + inject([FancyService], (service: FancyService) => { + service.setValue('value modified in beforeEach'); + expect(service.getValue()).toBe('value modified in beforeEach'); + }) + ); + + // #docregion getTimeoutValue + it('test should wait for FancyService.getTimeoutValue', + async(inject([FancyService], (service: FancyService) => { + + service.getTimeoutValue().then( + value => expect(value).toBe('timeout value') + ); + }))); + // #enddocregion getTimeoutValue +}); + +describe('using async(inject) within beforeEach', () => { + let serviceValue: string; + + beforeEach(() => { + TestBed.configureTestingModule({ providers: [FancyService] }); + }); + + beforeEach( async(inject([FancyService], (service: FancyService) => { + service.getAsyncValue().then(value => serviceValue = value); + }))); + + it('should use asynchronously modified value ... in synchronous test', () => { + expect(serviceValue).toBe('async value'); + }); +}); + + +/////////// Component Tests ////////////////// + +describe('TestBed Component Tests', () => { + + beforeEach( async(() => { + TestBed + .configureTestingModule({ + imports: [BagModule], + }) + // Compile everything in BagModule + .compileComponents(); + })); + + it('should create a component with inline template', () => { + const fixture = TestBed.createComponent(Child1Component); + fixture.detectChanges(); + + expect(fixture).toHaveText('Child'); + }); + + it('should create a component with external template', () => { + const fixture = TestBed.createComponent(ExternalTemplateComponent); + fixture.detectChanges(); + + expect(fixture).toHaveText('from external template'); + }); + + it('should allow changing members of the component', () => { + const fixture = TestBed.createComponent(MyIfComponent); + + fixture.detectChanges(); + expect(fixture).toHaveText('MyIf()'); + + fixture.componentInstance.showMore = true; + fixture.detectChanges(); + expect(fixture).toHaveText('MyIf(More)'); + }); + + it('should create a nested component bound to inputs/outputs', () => { + const fixture = TestBed.createComponent(IoParentComponent); + + fixture.detectChanges(); + const heroes = fixture.debugElement.queryAll(By.css('.hero')); + expect(heroes.length).toBeGreaterThan(0, 'has heroes'); + + const comp = fixture.componentInstance; + const hero = comp.heroes[0]; + + heroes[0].triggerEventHandler('click', null); + fixture.detectChanges(); + + const selected = fixture.debugElement.query(By.css('p')); + expect(selected).toHaveText(hero.name); + }); + + it('can access the instance variable of an `*ngFor` row', () => { + const fixture = TestBed.createComponent(IoParentComponent); + const comp = fixture.componentInstance; + + fixture.detectChanges(); + const heroEl = fixture.debugElement.query(By.css('.hero')); // first hero + + const ngForRow = heroEl.parent; // Angular's NgForRow wrapper element + + // jasmine.any is instance-of-type test. + expect(ngForRow.componentInstance).toEqual(jasmine.any(IoComponent), 'component is IoComp'); + + const hero = ngForRow.context['$implicit']; // the hero object + expect(hero.name).toBe(comp.heroes[0].name, '1st hero\'s name'); + }); + + + // #docregion ButtonComp + it('should support clicking a button', () => { + const fixture = TestBed.createComponent(ButtonComponent); + const btn = fixture.debugElement.query(By.css('button')); + const span = fixture.debugElement.query(By.css('span')).nativeElement; + + fixture.detectChanges(); + expect(span.textContent).toMatch(/is off/i, 'before click'); + + btn.triggerEventHandler('click', null); + fixture.detectChanges(); + expect(span.textContent).toMatch(/is on/i, 'after click'); + }); + // #enddocregion ButtonComp + + // ngModel is async so we must wait for it with promise-based `whenStable` + it('should support entering text in input box (ngModel)', async(() => { + const expectedOrigName = 'John'; + const expectedNewName = 'Sally'; + + const fixture = TestBed.createComponent(InputComponent); + fixture.detectChanges(); + + const comp = fixture.componentInstance; + const input = fixture.debugElement.query(By.css('input')).nativeElement; + + expect(comp.name).toBe(expectedOrigName, + `At start name should be ${expectedOrigName} `); + + // wait until ngModel binds comp.name to input box + fixture.whenStable().then(() => { + expect(input.value).toBe(expectedOrigName, + `After ngModel updates input box, input.value should be ${expectedOrigName} `); + + // simulate user entering new name in input + input.value = expectedNewName; + + // that change doesn't flow to the component immediately + expect(comp.name).toBe(expectedOrigName, + `comp.name should still be ${expectedOrigName} after value change, before binding happens`); + + // dispatch a DOM event so that Angular learns of input value change. + // then wait while ngModel pushes input.box value to comp.name + input.dispatchEvent(newEvent('input')); + return fixture.whenStable(); + }) + .then(() => { + expect(comp.name).toBe(expectedNewName, + `After ngModel updates the model, comp.name should be ${expectedNewName} `); + }); + })); + + // fakeAsync version of ngModel input test enables sync test style + // synchronous `tick` replaces asynchronous promise-base `whenStable` + it('should support entering text in input box (ngModel) - fakeAsync', fakeAsync(() => { + const expectedOrigName = 'John'; + const expectedNewName = 'Sally'; + + const fixture = TestBed.createComponent(InputComponent); + fixture.detectChanges(); + + const comp = fixture.componentInstance; + const input = fixture.debugElement.query(By.css('input')).nativeElement; + + expect(comp.name).toBe(expectedOrigName, + `At start name should be ${expectedOrigName} `); + + // wait until ngModel binds comp.name to input box + tick(); + expect(input.value).toBe(expectedOrigName, + `After ngModel updates input box, input.value should be ${expectedOrigName} `); + + // simulate user entering new name in input + input.value = expectedNewName; + + // that change doesn't flow to the component immediately + expect(comp.name).toBe(expectedOrigName, + `comp.name should still be ${expectedOrigName} after value change, before binding happens`); + + // dispatch a DOM event so that Angular learns of input value change. + // then wait a tick while ngModel pushes input.box value to comp.name + input.dispatchEvent(newEvent('input')); + tick(); + expect(comp.name).toBe(expectedNewName, + `After ngModel updates the model, comp.name should be ${expectedNewName} `); + })); + + // #docregion ReversePipeComp + it('ReversePipeComp should reverse the input text', fakeAsync(() => { + const inputText = 'the quick brown fox.'; + const expectedText = '.xof nworb kciuq eht'; + + const fixture = TestBed.createComponent(ReversePipeComponent); + fixture.detectChanges(); + + const comp = fixture.componentInstance; + const input = fixture.debugElement.query(By.css('input')).nativeElement as HTMLInputElement; + const span = fixture.debugElement.query(By.css('span')).nativeElement as HTMLElement; + + // simulate user entering new name in input + input.value = inputText; + + // dispatch a DOM event so that Angular learns of input value change. + // then wait a tick while ngModel pushes input.box value to comp.text + // and Angular updates the output span + input.dispatchEvent(newEvent('input')); + tick(); + fixture.detectChanges(); + expect(span.textContent).toBe(expectedText, 'output span'); + expect(comp.text).toBe(inputText, 'component.text'); + })); + // #enddocregion ReversePipeComp + + // Use this technique to find attached directives of any kind + it('can examine attached directives and listeners', () => { + const fixture = TestBed.createComponent(InputComponent); + fixture.detectChanges(); + + const inputEl = fixture.debugElement.query(By.css('input')); + + expect(inputEl.providerTokens).toContain(NgModel, 'NgModel directive'); + + const ngControl = inputEl.injector.get(NgControl); + expect(ngControl).toEqual(jasmine.any(NgControl), 'NgControl directive'); + + expect(inputEl.listeners.length).toBeGreaterThan(2, 'several listeners attached'); + }); + + // #docregion debug-dom-renderer + it('DebugDomRender should set attributes, styles, classes, and properties', () => { + const fixture = TestBed.createComponent(BankAccountParentComponent); + fixture.detectChanges(); + const comp = fixture.componentInstance; + + // the only child is debugElement of the BankAccount component + const el = fixture.debugElement.children[0]; + const childComp = el.componentInstance as BankAccountComponent; + expect(childComp).toEqual(jasmine.any(BankAccountComponent)); + + expect(el.context).toBe(comp, 'context is the parent component'); + + expect(el.attributes['account']).toBe(childComp.id, 'account attribute'); + expect(el.attributes['bank']).toBe(childComp.bank, 'bank attribute'); + + expect(el.classes['closed']).toBe(true, 'closed class'); + expect(el.classes['open']).toBe(false, 'open class'); + + expect(el.properties['customProperty']).toBe(true, 'customProperty'); + + expect(el.styles['color']).toBe(comp.color, 'color style'); + expect(el.styles['width']).toBe(comp.width + 'px', 'width style'); + }); + // #enddocregion debug-dom-renderer +}); + +describe('TestBed Component Overrides:', () => { + + it('should override ChildComp\'s template', () => { + + const fixture = TestBed.configureTestingModule({ + declarations: [Child1Component], + }) + .overrideComponent(Child1Component, { + set: { template: 'Fake' } + }) + .createComponent(Child1Component); + + fixture.detectChanges(); + expect(fixture).toHaveText('Fake'); + }); + + it('should override TestProvidersComp\'s FancyService provider', () => { + const fixture = TestBed.configureTestingModule({ + declarations: [TestProvidersComponent], + }) + .overrideComponent(TestProvidersComponent, { + remove: { providers: [FancyService]}, + add: { providers: [{ provide: FancyService, useClass: FakeFancyService }] }, + + // Or replace them all (this component has only one provider) + // set: { providers: [{ provide: FancyService, useClass: FakeFancyService }] }, + }) + .createComponent(TestProvidersComponent); + + fixture.detectChanges(); + expect(fixture).toHaveText('injected value: faked value', 'text'); + + // Explore the providerTokens + const tokens = fixture.debugElement.providerTokens; + expect(tokens).toContain(fixture.componentInstance.constructor, 'component ctor'); + expect(tokens).toContain(TestProvidersComponent, 'TestProvidersComp'); + expect(tokens).toContain(FancyService, 'FancyService'); + }); + + it('should override TestViewProvidersComp\'s FancyService viewProvider', () => { + const fixture = TestBed.configureTestingModule({ + declarations: [TestViewProvidersComponent], + }) + .overrideComponent(TestViewProvidersComponent, { + // remove: { viewProviders: [FancyService]}, + // add: { viewProviders: [{ provide: FancyService, useClass: FakeFancyService }] }, + + // Or replace them all (this component has only one viewProvider) + set: { viewProviders: [{ provide: FancyService, useClass: FakeFancyService }] }, + }) + .createComponent(TestViewProvidersComponent); + + fixture.detectChanges(); + expect(fixture).toHaveText('injected value: faked value'); + }); + + it('injected provider should not be same as component\'s provider', () => { + + // TestComponent is parent of TestProvidersComponent + @Component({ template: '' }) + class TestComponent {} + + // 3 levels of FancyService provider: module, TestCompomponent, TestProvidersComponent + const fixture = TestBed.configureTestingModule({ + declarations: [TestComponent, TestProvidersComponent], + providers: [FancyService] + }) + .overrideComponent(TestComponent, { + set: { providers: [{ provide: FancyService, useValue: {} }] } + }) + .overrideComponent(TestProvidersComponent, { + set: { providers: [{ provide: FancyService, useClass: FakeFancyService }] } + }) + .createComponent(TestComponent); + + let testBedProvider: FancyService; + let tcProvider: {}; + let tpcProvider: FakeFancyService; + + // `inject` uses TestBed's injector + inject([FancyService], (s: FancyService) => testBedProvider = s)(); + tcProvider = fixture.debugElement.injector.get(FancyService); + tpcProvider = fixture.debugElement.children[0].injector.get(FancyService); + + expect(testBedProvider).not.toBe(tcProvider, 'testBed/tc not same providers'); + expect(testBedProvider).not.toBe(tpcProvider, 'testBed/tpc not same providers'); + + expect(testBedProvider instanceof FancyService).toBe(true, 'testBedProvider is FancyService'); + expect(tcProvider).toEqual({}, 'tcProvider is {}'); + expect(tpcProvider instanceof FakeFancyService).toBe(true, 'tpcProvider is FakeFancyService'); + }); + + it('can access template local variables as references', () => { + const fixture = TestBed.configureTestingModule({ + declarations: [ShellComponent, NeedsContentComponent, Child1Component, Child2Component, Child3Component], + }) + .overrideComponent(ShellComponent, { + set: { + selector: 'test-shell', + template: ` + + + + + +
!
+
+ ` + } + }) + .createComponent(ShellComponent); + + fixture.detectChanges(); + + // NeedsContentComp is the child of ShellComp + const el = fixture.debugElement.children[0]; + const comp = el.componentInstance; + + expect(comp.children.toArray().length).toBe(4, + 'three different child components and an ElementRef with #content'); + + expect(el.references['nc']).toBe(comp, '#nc reference to component'); + + // #docregion custom-predicate + // Filter for DebugElements with a #content reference + const contentRefs = el.queryAll( de => de.references['content']); + // #enddocregion custom-predicate + expect(contentRefs.length).toBe(4, 'elements w/ a #content reference'); + }); + +}); + +describe('Nested (one-deep) component override', () => { + + beforeEach( async(() => { + TestBed.configureTestingModule({ + declarations: [ParentComponent, FakeChildComponent] + }) + .compileComponents(); + })); + + it('ParentComp should use Fake Child component', () => { + const fixture = TestBed.createComponent(ParentComponent); + fixture.detectChanges(); + expect(fixture).toHaveText('Parent(Fake Child)'); + }); +}); + +describe('Nested (two-deep) component override', () => { + + beforeEach( async(() => { + TestBed.configureTestingModule({ + declarations: [ParentComponent, FakeChildWithGrandchildComponent, FakeGrandchildComponent] + }) + .compileComponents(); + })); + + it('should use Fake Grandchild component', () => { + const fixture = TestBed.createComponent(ParentComponent); + fixture.detectChanges(); + expect(fixture).toHaveText('Parent(Fake Child(Fake Grandchild))'); + }); +}); + +describe('Lifecycle hooks w/ MyIfParentComp', () => { + let fixture: ComponentFixture; + let parent: MyIfParentComponent; + let child: MyIfChildComponent; + + beforeEach( async(() => { + TestBed.configureTestingModule({ + imports: [FormsModule], + declarations: [MyIfChildComponent, MyIfParentComponent] + }) + .compileComponents().then(() => { + fixture = TestBed.createComponent(MyIfParentComponent); + parent = fixture.componentInstance; + }); + })); + + it('should instantiate parent component', () => { + expect(parent).not.toBeNull('parent component should exist'); + }); + + it('parent component OnInit should NOT be called before first detectChanges()', () => { + expect(parent.ngOnInitCalled).toBe(false); + }); + + it('parent component OnInit should be called after first detectChanges()', () => { + fixture.detectChanges(); + expect(parent.ngOnInitCalled).toBe(true); + }); + + it('child component should exist after OnInit', () => { + fixture.detectChanges(); + getChild(); + expect(child instanceof MyIfChildComponent).toBe(true, 'should create child'); + }); + + it('should have called child component\'s OnInit ', () => { + fixture.detectChanges(); + getChild(); + expect(child.ngOnInitCalled).toBe(true); + }); + + it('child component called OnChanges once', () => { + fixture.detectChanges(); + getChild(); + expect(child.ngOnChangesCounter).toBe(1); + }); + + it('changed parent value flows to child', () => { + fixture.detectChanges(); + getChild(); + + parent.parentValue = 'foo'; + fixture.detectChanges(); + + expect(child.ngOnChangesCounter).toBe(2, + 'expected 2 changes: initial value and changed value'); + expect(child.childValue).toBe('foo', + 'childValue should eq changed parent value'); + }); + + // must be async test to see child flow to parent + it('changed child value flows to parent', async(() => { + fixture.detectChanges(); + getChild(); + + child.childValue = 'bar'; + + return new Promise(resolve => { + // Wait one JS engine turn! + setTimeout(() => resolve(), 0); + }) + .then(() => { + fixture.detectChanges(); + + expect(child.ngOnChangesCounter).toBe(2, + 'expected 2 changes: initial value and changed value'); + expect(parent.parentValue).toBe('bar', + 'parentValue should eq changed parent value'); + }); + + })); + + it('clicking "Close Child" triggers child OnDestroy', () => { + fixture.detectChanges(); + getChild(); + + const btn = fixture.debugElement.query(By.css('button')); + btn.triggerEventHandler('click', null); + + fixture.detectChanges(); + expect(child.ngOnDestroyCalled).toBe(true); + }); + + ////// helpers /// + /** + * Get the MyIfChildComp from parent; fail w/ good message if cannot. + */ + function getChild() { + + let childDe: DebugElement; // DebugElement that should hold the MyIfChildComp + + // The Hard Way: requires detailed knowledge of the parent template + try { + childDe = fixture.debugElement.children[4].children[0]; + } catch (err) { /* we'll report the error */ } + + // DebugElement.queryAll: if we wanted all of many instances: + childDe = fixture.debugElement + .queryAll(function (de) { return de.componentInstance instanceof MyIfChildComponent; })[0]; + + // WE'LL USE THIS APPROACH ! + // DebugElement.query: find first instance (if any) + childDe = fixture.debugElement + .query(function (de) { return de.componentInstance instanceof MyIfChildComponent; }); + + if (childDe && childDe.componentInstance) { + child = childDe.componentInstance; + } else { + fail('Unable to find MyIfChildComp within MyIfParentComp'); + } + + return child; + } +}); + +////////// Fakes /////////// + +@Component({ + selector: 'child-1', + template: `Fake Child` +}) +class FakeChildComponent { } + +@Component({ + selector: 'child-1', + template: `Fake Child()` +}) +class FakeChildWithGrandchildComponent { } + +@Component({ + selector: 'grandchild-1', + template: `Fake Grandchild` +}) +class FakeGrandchildComponent { } + +@Injectable() +class FakeFancyService extends FancyService { + value: string = 'faked value'; +} diff --git a/public/docs/_examples/testing/ts/app/bag/bag.ts b/public/docs/_examples/testing/ts/app/bag/bag.ts new file mode 100644 index 0000000000..cbe88f55f5 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/bag/bag.ts @@ -0,0 +1,454 @@ +/* tslint:disable:forin */ +import { Component, ContentChildren, Directive, ElementRef, EventEmitter, + Injectable, Input, Output, Optional, + HostBinding, HostListener, + OnInit, OnChanges, OnDestroy, + Pipe, PipeTransform, + Renderer, SimpleChange } from '@angular/core'; + +import { Observable } from 'rxjs/Observable'; +import 'rxjs/add/observable/of'; +import 'rxjs/add/operator/delay'; + +////////// The App: Services and Components for the tests. ////////////// + +export class Hero { + name: string; +} + +////////// Services /////////////// +// #docregion FancyService +@Injectable() +export class FancyService { + protected value: string = 'real value'; + + getValue() { return this.value; } + setValue(value: string) { this.value = value; } + + getAsyncValue() { return Promise.resolve('async value'); } + + getObservableValue() { return Observable.of('observable value'); } + + getTimeoutValue() { + return new Promise((resolve) => { + setTimeout(() => { resolve('timeout value'); }, 10); + }); + } + + getObservableDelayValue() { + return Observable.of('observable delay value').delay(10); + } +} +// #enddocregion FancyService + +// #docregion DependentService +@Injectable() +export class DependentService { + constructor(private dependentService: FancyService) { } + getValue() { return this.dependentService.getValue(); } +} +// #enddocregion DependentService + +/////////// Pipe //////////////// +/* + * Reverse the input string. +*/ +// #docregion ReversePipe +@Pipe({ name: 'reverse' }) +export class ReversePipe implements PipeTransform { + transform(s: string) { + let r = ''; + for (let i = s.length; i; ) { r += s[--i]; }; + return r; + } +} +// #enddocregion ReversePipe + +//////////// Components ///////////// +@Component({ + selector: 'bank-account', + template: ` + Bank Name: {{bank}} + Account Id: {{id}} + ` +}) +export class BankAccountComponent { + @Input() bank: string; + @Input('account') id: string; + + constructor(private renderer: Renderer, private el: ElementRef ) { + renderer.setElementProperty(el.nativeElement, 'customProperty', true); + } +} + +/** A component with attributes, styles, classes, and property setting */ +@Component({ + selector: 'bank-account-parent', + template: ` + + + ` +}) +export class BankAccountParentComponent { + width = 200; + color = 'red'; + isClosed = true; +} + +// #docregion ButtonComp +@Component({ + selector: 'button-comp', + template: ` + + {{message}}` +}) +export class ButtonComponent { + isOn = false; + clicked() { this.isOn = !this.isOn; } + get message() { return `The light is ${this.isOn ? 'On' : 'Off'}`; } +} +// #enddocregion ButtonComp + +@Component({ + selector: 'child-1', + template: `Child-1({{text}})` +}) +export class Child1Component { + @Input() text = 'Original'; +} + +@Component({ + selector: 'child-2', + template: '
Child-2({{text}})
' +}) +export class Child2Component { + @Input() text: string; +} + +@Component({ + selector: 'child-3', + template: '
Child-3({{text}})
' +}) +export class Child3Component { + @Input() text: string; +} + +@Component({ + selector: 'input-comp', + template: `` +}) +export class InputComponent { + name = 'John'; +} + +/* Prefer this metadata syntax */ +// @Directive({ +// selector: 'input[value]', +// host: { +// '[value]': 'value', +// '(input)': 'valueChange.next($event.target.value)' +// }, +// inputs: ['value'], +// outputs: ['valueChange'] +// }) +// export class InputValueBinderDirective { +// value: any; +// valueChange: EventEmitter = new EventEmitter(); +// } + +// As the style-guide recommends +@Directive({ selector: 'input[value]' }) +export class InputValueBinderDirective { + @HostBinding() + @Input() + value: any; + + @Output() + valueChange: EventEmitter = new EventEmitter(); + + @HostListener('input', ['$event.target.value']) + onInput(value: any) { this.valueChange.next(value); } +} + +@Component({ + selector: 'input-value-comp', + template: ` + Name: {{name}} + ` +}) +export class InputValueBinderComponent { + name = 'Sally'; // initial value +} + +@Component({ + selector: 'parent-comp', + template: `Parent()` +}) +export class ParentComponent { } + +@Component({ + selector: 'io-comp', + template: `
Original {{hero.name}}
` +}) +export class IoComponent { + @Input() hero: Hero; + @Output() selected = new EventEmitter(); + click() { this.selected.emit(this.hero); } +} + +@Component({ + selector: 'io-parent-comp', + template: ` +

Click to select a hero

+

The selected hero is {{selectedHero.name}}

+ + + ` +}) +export class IoParentComponent { + heroes: Hero[] = [ {name: 'Bob'}, {name: 'Carol'}, {name: 'Ted'}, {name: 'Alice'} ]; + selectedHero: Hero; + onSelect(hero: Hero) { this.selectedHero = hero; } +} + +@Component({ + selector: 'my-if-comp', + template: `MyIf(More)` +}) +export class MyIfComponent { + showMore = false; +} + +@Component({ + selector: 'my-service-comp', + template: `injected value: {{fancyService.value}}`, + providers: [FancyService] +}) +export class TestProvidersComponent { + constructor(private fancyService: FancyService) {} +} + + +@Component({ + selector: 'my-service-comp', + template: `injected value: {{fancyService.value}}`, + viewProviders: [FancyService] +}) +export class TestViewProvidersComponent { + constructor(private fancyService: FancyService) {} +} + +@Component({ + moduleId: module.id, + selector: 'external-template-comp', + templateUrl: 'bag-external-template.html' +}) +export class ExternalTemplateComponent implements OnInit { + serviceValue: string; + + constructor(@Optional() private service: FancyService) { } + + ngOnInit() { + if (this.service) { this.serviceValue = this.service.getValue(); } + } +} + +@Component({ + selector: 'comp-w-ext-comp', + template: ` +

comp-w-ext-comp

+ + ` +}) +export class InnerCompWithExternalTemplateComponent { } + +@Component({ + selector: 'bad-template-comp', + templateUrl: 'non-existant.html' +}) +export class BadTemplateUrlComponent { } + + + +@Component({selector: 'needs-content', template: ''}) +export class NeedsContentComponent { + // children with #content local variable + @ContentChildren('content') children: any; +} + +///////// MyIfChildComp //////// +@Component({ + selector: 'my-if-child-1', + + template: ` +

MyIfChildComp

+
+ +
+

Change log:

+
{{i + 1}} - {{log}}
` +}) +export class MyIfChildComponent 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

+ +
+
+ +
+ ` +}) +export class MyIfParentComponent implements OnInit { + ngOnInitCalled = false; + parentValue = 'Hello, World'; + showChild = false; + toggleLabel = 'Unknown'; + + ngOnInit() { + this.ngOnInitCalled = true; + this.clicked(); + } + + clicked() { + this.showChild = !this.showChild; + this.toggleLabel = this.showChild ? 'Close' : 'Show'; + } +} + + +@Component({ + selector: 'reverse-pipe-comp', + template: ` + + {{text | reverse}} + ` +}) +export class ReversePipeComponent { + text = 'my dog has fleas.'; +} + +@Component({template: '
Replace Me
'}) +export class ShellComponent { } + +@Component({ + selector: 'bag-comp', + template: ` +

Specs Bag

+ +
+

Input/Output Component

+ +
+

External Template Component

+ +
+

Component With External Template Component

+ +
+

Reverse Pipe

+ +
+

InputValueBinder Directive

+ +
+

Button Component

+ +
+

Needs Content

+ + + + + +
!
+
+ ` +}) +export class BagComponent { } +//////// Aggregations //////////// + +export const bagDeclarations = [ + BagComponent, + BankAccountComponent, BankAccountParentComponent, + ButtonComponent, + Child1Component, Child2Component, Child3Component, + ExternalTemplateComponent, InnerCompWithExternalTemplateComponent, + InputComponent, + InputValueBinderDirective, InputValueBinderComponent, + IoComponent, IoParentComponent, + MyIfComponent, MyIfChildComponent, MyIfParentComponent, + NeedsContentComponent, ParentComponent, + TestProvidersComponent, TestViewProvidersComponent, + ReversePipe, ReversePipeComponent, ShellComponent +]; + +export const bagProviders = [DependentService, FancyService]; + +//////////////////// +//////////// +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { FormsModule } from '@angular/forms'; + +@NgModule({ + imports: [BrowserModule, FormsModule], + declarations: bagDeclarations, + providers: bagProviders, + entryComponents: [BagComponent], + bootstrap: [BagComponent] +}) +export class BagModule { } + diff --git a/public/docs/_examples/testing/ts/app/banner.component.spec.ts b/public/docs/_examples/testing/ts/app/banner.component.spec.ts new file mode 100644 index 0000000000..c9af53a805 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/banner.component.spec.ts @@ -0,0 +1,127 @@ +// #docplaster +// #docregion +// #docregion imports +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { DebugElement } from '@angular/core'; + +import { BannerComponent } from './banner.component'; +// #enddocregion imports + +// #docregion setup +let comp: BannerComponent; +let fixture: ComponentFixture; +let el: DebugElement; + +describe('BannerComponent', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [ BannerComponent ], // declare the test component + }); + + fixture = TestBed.createComponent(BannerComponent); + + comp = fixture.componentInstance; // BannerComponent test instance + + // get title DebugElement by element name + el = fixture.debugElement.query(By.css('h1')); + }); +// #enddocregion setup + // #docregion tests + it('should display original title', () => { + fixture.detectChanges(); // trigger data binding + expect(el.nativeElement.textContent).toContain(comp.title); + }); + + it('should display a different test title', () => { + comp.title = 'Test Title'; + fixture.detectChanges(); // trigger data binding + expect(el.nativeElement.textContent).toContain('Test Title'); + }); + // #enddocregion tests + // #docregion test-w-o-detect-changes + it('no title in the DOM until manually call `detectChanges`', () => { + expect(el.nativeElement.textContent).toEqual(''); + }); + // #enddocregion test-w-o-detect-changes + +// #docregion setup +}); +// #enddocregion setup + +///////// With AutoChangeDetect ///// +import { ComponentFixtureAutoDetect } from '@angular/core/testing'; + +describe('BannerComponent with AutoChangeDetect', () => { + + beforeEach(() => { + // #docregion auto-detect + fixture = TestBed.configureTestingModule({ + declarations: [ BannerComponent ], + providers: [ + { provide: ComponentFixtureAutoDetect, + useValue: true } + ] + }) + // #enddocregion auto-detect + .createComponent(BannerComponent); + + comp = fixture.componentInstance; // BannerComponent test instance + + // find title DebugElement by element name + el = fixture.debugElement.query(By.css('h1')); + }); + + // #docregion auto-detect-tests + it('should display original title', () => { + // Hooray! No `fixture.detectChanges()` needed + expect(el.nativeElement.textContent).toContain(comp.title); + }); + + it('should still see original title after comp.title change', () => { + const oldTitle = comp.title; + comp.title = 'Test Title'; + // Displayed title is old because Angular didn't hear the change :( + expect(el.nativeElement.textContent).toContain(oldTitle); + }); + + it('should display updated title after detectChanges', () => { + comp.title = 'Test Title'; + fixture.detectChanges(); // detect changes explicitly + expect(el.nativeElement.textContent).toContain(comp.title); + }); + // #enddocregion auto-detect-tests +}); + + +describe('BannerComponent (simpified)', () => { + // #docregion simple-example-before-each + beforeEach(() => { + + // refine the test module by declaring the test component + TestBed.configureTestingModule({ + declarations: [ BannerComponent ], + }); + + // create component and test fixture + fixture = TestBed.createComponent(BannerComponent); + + // get test component from the fixture + comp = fixture.componentInstance; + }); + // #enddocregion simple-example-before-each + + // #docregion simple-example-it + it('should display original title', () => { + + // trigger data binding to update the view + fixture.detectChanges(); + + // find the title element in the DOM using a CSS selector + el = fixture.debugElement.query(By.css('h1')); + + // confirm the element's content + expect(el.nativeElement.textContent).toContain(comp.title); + }); + // #enddocregion simple-example-it +}); diff --git a/public/docs/_examples/testing/ts/app/banner.component.ts b/public/docs/_examples/testing/ts/app/banner.component.ts new file mode 100644 index 0000000000..c220c1482b --- /dev/null +++ b/public/docs/_examples/testing/ts/app/banner.component.ts @@ -0,0 +1,11 @@ +// #docregion +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-banner', + template: '

{{title}}

' +}) +export class BannerComponent { + title = 'Test Tour of Heroes'; +} + diff --git a/public/docs/_examples/testing/ts/app/dashboard.component.html b/public/docs/_examples/testing/ts/app/dashboard.component.html deleted file mode 100644 index 028eab6eb3..0000000000 --- a/public/docs/_examples/testing/ts/app/dashboard.component.html +++ /dev/null @@ -1,11 +0,0 @@ - -

Top Heroes

-
- -
- -
-

{{hero.name}}

-
-
-
diff --git a/public/docs/_examples/testing/ts/app/dashboard.component.spec.ts b/public/docs/_examples/testing/ts/app/dashboard.component.spec.ts deleted file mode 100644 index 1b573c32f3..0000000000 --- a/public/docs/_examples/testing/ts/app/dashboard.component.spec.ts +++ /dev/null @@ -1,164 +0,0 @@ -/* tslint:disable:no-unused-variable */ -import { DashboardComponent } from './dashboard.component'; - -import { By } from '@angular/platform-browser'; - -import { - addProviders, - async, inject -} from '@angular/core/testing'; - -import { ComponentFixture, TestComponentBuilder } from '@angular/core/testing'; - -import { Hero, HeroService, MockHeroService } from './mock-hero.service'; -import { Router, MockRouter } from './mock-router'; - -describe('DashboardComponent', () => { - - //////// WITHOUT ANGULAR INVOLVED /////// - describe('w/o Angular', () => { - let comp: DashboardComponent; - let mockHeroService: MockHeroService; - let router: MockRouter; - - beforeEach(() => { - router = new MockRouter(); - mockHeroService = new MockHeroService(); - comp = new DashboardComponent(router, mockHeroService); - }); - - it('should NOT have heroes before calling OnInit', () => { - expect(comp.heroes.length).toEqual(0, - 'should not have heroes before OnInit'); - }); - - it('should NOT have heroes immediately after OnInit', () => { - comp.ngOnInit(); // ngOnInit -> getHeroes - expect(comp.heroes.length).toEqual(0, - 'should not have heroes until service promise resolves'); - }); - - it('should HAVE heroes after HeroService gets them', (done: DoneFn) => { - comp.ngOnInit(); // ngOnInit -> getHeroes - mockHeroService.lastPromise // the one from getHeroes - .then(() => { - // throw new Error('deliberate error'); // see it fail gracefully - expect(comp.heroes.length).toBeGreaterThan(0, - 'should have heroes after service promise resolves'); - }) - .then(done, done.fail); - }); - - it('should tell ROUTER to navigate by hero id', () => { - let hero: Hero = {id: 42, name: 'Abbracadabra' }; - let spy = spyOn(router, 'navigate').and.callThrough(); - - comp.gotoDetail(hero); - - let linkParams = spy.calls.mostRecent().args[0]; - expect(linkParams[0]).toEqual('HeroDetail', 'should nav to "HeroDetail"'); - expect(linkParams[1].id).toEqual(hero.id, 'should nav to fake hero\'s id'); - }); - - }); - - - ////// WITH ANGULAR TEST INFRASTRUCTURE /////// - describe('using TCB', () => { - let comp: DashboardComponent; - let mockHeroService: MockHeroService; - - beforeEach(() => { - mockHeroService = new MockHeroService(); - addProviders([ - { provide: Router, useClass: MockRouter}, - { provide: MockRouter, useExisting: Router}, - { provide: HeroService, useValue: mockHeroService } - ]); - }); - - it('can instantiate it', - async(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { - tcb.createAsync(DashboardComponent); - }))); - - it('should NOT have heroes before OnInit', - async(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { - tcb.createAsync(DashboardComponent).then(fixture => { - comp = fixture.debugElement.componentInstance; - - expect(comp.heroes.length).toEqual(0, - 'should not have heroes before OnInit'); - }); - }))); - - it('should NOT have heroes immediately after OnInit', - async(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { - tcb.createAsync(DashboardComponent).then(fixture => { - comp = fixture.debugElement.componentInstance; - fixture.detectChanges(); // runs initial lifecycle hooks - - expect(comp.heroes.length).toEqual(0, - 'should not have heroes until service promise resolves'); - }); - }))); - - it('should HAVE heroes after HeroService gets them', - async(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { - - tcb.createAsync(DashboardComponent).then(fixture => { - comp = fixture.debugElement.componentInstance; - fixture.detectChanges(); // runs ngOnInit -> getHeroes - - mockHeroService.lastPromise // the one from getHeroes - .then(() => { - expect(comp.heroes.length).toBeGreaterThan(0, - 'should have heroes after service promise resolves'); - }); - - }); - }))); - - it('should DISPLAY heroes after HeroService gets them', - async(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { - - tcb.createAsync(DashboardComponent).then(fixture => { - comp = fixture.debugElement.componentInstance; - fixture.detectChanges(); // runs ngOnInit -> getHeroes - - mockHeroService.lastPromise // the one from getHeroes - .then(() => { - - // Find and examine the displayed heroes - fixture.detectChanges(); // update bindings - let heroNames = fixture.debugElement.queryAll(By.css('h4')); - - expect(heroNames.length).toEqual(4, 'should display 4 heroes'); - - // the 4th displayed hero should be the 5th mock hero - expect(heroNames[3].nativeElement.textContent) - .toContain(mockHeroService.mockHeroes[4].name); - }); - - }); - }))); - - it('should tell ROUTER to navigate by hero id', - async(inject([TestComponentBuilder, Router], - (tcb: TestComponentBuilder, router: MockRouter) => { - - let spy = spyOn(router, 'navigate').and.callThrough(); - - tcb.createAsync(DashboardComponent).then(fixture => { - let hero: Hero = {id: 42, name: 'Abbracadabra' }; - comp = fixture.debugElement.componentInstance; - comp.gotoDetail(hero); - - let linkParams = spy.calls.mostRecent().args[0]; - expect(linkParams[0]).toEqual('HeroDetail', 'should nav to "HeroDetail"'); - expect(linkParams[1].id).toEqual(hero.id, 'should nav to fake hero\'s id'); - - }); - }))); - }); -}); diff --git a/public/docs/_examples/testing/ts/app/dashboard.component.ts b/public/docs/_examples/testing/ts/app/dashboard.component.ts deleted file mode 100644 index 69a9c5cce6..0000000000 --- a/public/docs/_examples/testing/ts/app/dashboard.component.ts +++ /dev/null @@ -1,44 +0,0 @@ -// #docplaster -// #docregion -import { Component, OnInit } from '@angular/core'; -// #docregion import-router -import { Router } from '@angular/router-deprecated'; -// #enddocregion import-router - -import { Hero } from './hero'; -import { HeroService } from './hero.service'; - -@Component({ - selector: 'my-dashboard', - // #docregion template-url - templateUrl: 'app/dashboard.component.html', - // #enddocregion template-url - // #docregion css - styleUrls: ['app/dashboard.component.css'] - // #enddocregion css -}) -// #docregion component -export class DashboardComponent implements OnInit { - - heroes: Hero[] = []; - -// #docregion ctor - constructor( - private _router: Router, - private _heroService: HeroService) { - } -// #enddocregion ctor - - ngOnInit() { - this._heroService.getHeroes() - .then(heroes => this.heroes = heroes.slice(1, 5)); - } - - // #docregion goto-detail - gotoDetail(hero: Hero) { - let link = ['HeroDetail', { id: hero.id }]; - this._router.navigate(link); - } - // #enddocregion goto-detail -} -// #enddocregion diff --git a/public/docs/_examples/testing/ts/app/dashboard/dashboard-hero.component.css b/public/docs/_examples/testing/ts/app/dashboard/dashboard-hero.component.css new file mode 100644 index 0000000000..eb54d181d8 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/dashboard/dashboard-hero.component.css @@ -0,0 +1,28 @@ +.hero { + padding: 20px; + position: relative; + text-align: center; + color: #eee; + max-height: 120px; + min-width: 120px; + background-color: #607D8B; + border-radius: 2px; +} + +.hero:hover { + background-color: #EEE; + cursor: pointer; + color: #607d8b; +} + +@media (max-width: 600px) { + .hero { + font-size: 10px; + max-height: 75px; } +} + +@media (max-width: 1024px) { + .hero { + min-width: 60px; + } +} diff --git a/public/docs/_examples/testing/ts/app/dashboard/dashboard-hero.component.html b/public/docs/_examples/testing/ts/app/dashboard/dashboard-hero.component.html new file mode 100644 index 0000000000..ff49bd17a5 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/dashboard/dashboard-hero.component.html @@ -0,0 +1,4 @@ + +
+ {{hero.name | uppercase}} +
diff --git a/public/docs/_examples/testing/ts/app/dashboard/dashboard-hero.component.spec.ts b/public/docs/_examples/testing/ts/app/dashboard/dashboard-hero.component.spec.ts new file mode 100644 index 0000000000..86e0a88d2e --- /dev/null +++ b/public/docs/_examples/testing/ts/app/dashboard/dashboard-hero.component.spec.ts @@ -0,0 +1,113 @@ +import { async, ComponentFixture, TestBed +} from '@angular/core/testing'; + +import { By } from '@angular/platform-browser'; +import { DebugElement } from '@angular/core'; + +import { addMatchers } from '../../testing'; + +import { Hero } from '../model/hero'; +import { DashboardHeroComponent } from './dashboard-hero.component'; + +beforeEach( addMatchers ); + +describe('DashboardHeroComponent when tested directly', () => { + + let comp: DashboardHeroComponent; + let expectedHero: Hero; + let fixture: ComponentFixture; + let heroEl: DebugElement; + + // #docregion setup, compile-components + // asynch beforeEach + beforeEach( async(() => { + TestBed.configureTestingModule({ + declarations: [ DashboardHeroComponent ], + }) + .compileComponents(); // compile template and css + })); + // #enddocregion compile-components + + // synchronous beforeEach + beforeEach(() => { + fixture = TestBed.createComponent(DashboardHeroComponent); + comp = fixture.componentInstance; + heroEl = fixture.debugElement.query(By.css('.hero')); // find hero element + + // pretend that it was wired to something that supplied a hero + expectedHero = new Hero(42, 'Test Name'); + comp.hero = expectedHero; + fixture.detectChanges(); // trigger initial data binding + }); + // #enddocregion setup + + // #docregion name-test + it('should display hero name', () => { + const expectedPipedName = expectedHero.name.toUpperCase(); + expect(heroEl.nativeElement.textContent).toContain(expectedPipedName); + }); + // #enddocregion name-test + + // #docregion click-test + it('should raise selected event when clicked', () => { + let selectedHero: Hero; + comp.selected.subscribe((hero: Hero) => selectedHero = hero); + + heroEl.triggerEventHandler('click', null); + expect(selectedHero).toBe(expectedHero); + }); + // #enddocregion click-test +}); + +////////////////// + +describe('DashboardHeroComponent when inside a test host', () => { + let testHost: TestHostComponent; + let fixture: ComponentFixture; + let heroEl: DebugElement; + + // #docregion test-host-setup + beforeEach( async(() => { + TestBed.configureTestingModule({ + declarations: [ DashboardHeroComponent, TestHostComponent ], // declare both + }).compileComponents(); + })); + + beforeEach(() => { + // create TestHosComponent instead of DashboardHeroComponent + fixture = TestBed.createComponent(TestHostComponent); + testHost = fixture.componentInstance; + heroEl = fixture.debugElement.query(By.css('.hero')); // find hero + fixture.detectChanges(); // trigger initial data binding + }); + // #enddocregion test-host-setup + + // #docregion test-host-tests + it('should display hero name', () => { + const expectedPipedName = testHost.hero.name.toUpperCase(); + expect(heroEl.nativeElement.textContent).toContain(expectedPipedName); + }); + + it('should raise selected event when clicked', () => { + heroEl.triggerEventHandler('click', null); + // selected hero should be the same data bound hero + expect(testHost.selectedHero).toBe(testHost.hero); + }); + // #enddocregion test-host-tests +}); + +////// Test Host Component ////// +import { Component } from '@angular/core'; + +// #docregion test-host +@Component({ + template: ` + + ` +}) +class TestHostComponent { + hero = new Hero(42, 'Test Name'); + selectedHero: Hero; + onSelected(hero: Hero) { this.selectedHero = hero; } +} +// #enddocregion test-host diff --git a/public/docs/_examples/testing/ts/app/dashboard/dashboard-hero.component.ts b/public/docs/_examples/testing/ts/app/dashboard/dashboard-hero.component.ts new file mode 100644 index 0000000000..3d8ee8a177 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/dashboard/dashboard-hero.component.ts @@ -0,0 +1,17 @@ +// #docregion +import { Component, EventEmitter, Input, Output } from '@angular/core'; + +import { Hero } from '../model'; + +// #docregion component +@Component({ + selector: 'dashboard-hero', + templateUrl: 'app/dashboard/dashboard-hero.component.html', + styleUrls: ['app/dashboard/dashboard-hero.component.css'] +}) +export class DashboardHeroComponent { + @Input() hero: Hero; + @Output() selected = new EventEmitter(); + click() { this.selected.next(this.hero); } +} +// #enddocregion component diff --git a/public/docs/_examples/testing/ts/app/dashboard.component.css b/public/docs/_examples/testing/ts/app/dashboard/dashboard.component.css similarity index 54% rename from public/docs/_examples/testing/ts/app/dashboard.component.css rename to public/docs/_examples/testing/ts/app/dashboard/dashboard.component.css index ce6e963a5f..98130b61c6 100644 --- a/public/docs/_examples/testing/ts/app/dashboard.component.css +++ b/public/docs/_examples/testing/ts/app/dashboard/dashboard.component.css @@ -1,5 +1,3 @@ -/* #docplaster */ -/* #docregion */ [class*='col-'] { float: left; } @@ -24,40 +22,14 @@ h3 { .col-1-4 { width: 25%; } -.module { - padding: 20px; - text-align: center; - color: #eee; - max-height: 120px; - min-width: 120px; - background-color: #607D8B; - border-radius: 2px; -} -h4 { - position: relative; -} -.module:hover { - background-color: #EEE; - cursor: pointer; - color: #607d8b; -} .grid-pad { padding: 10px 0; } .grid-pad > [class*='col-']:last-of-type { padding-right: 20px; } -@media (max-width: 600px) { - .module { - font-size: 10px; - max-height: 75px; } -} @media (max-width: 1024px) { .grid { margin: 0; } - .module { - min-width: 60px; - } } -/* #enddocregion */ diff --git a/public/docs/_examples/testing/ts/app/dashboard/dashboard.component.html b/public/docs/_examples/testing/ts/app/dashboard/dashboard.component.html new file mode 100644 index 0000000000..b26e16b828 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/dashboard/dashboard.component.html @@ -0,0 +1,9 @@ +

{{title}}

+ +
+ + + + +
diff --git a/public/docs/_examples/testing/ts/app/dashboard/dashboard.component.no-testbed.spec.ts b/public/docs/_examples/testing/ts/app/dashboard/dashboard.component.no-testbed.spec.ts new file mode 100644 index 0000000000..125e5193b7 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/dashboard/dashboard.component.no-testbed.spec.ts @@ -0,0 +1,57 @@ +import { Router } from '@angular/router'; + +import { DashboardComponent } from './dashboard.component'; +import { Hero } from '../model'; + +import { addMatchers } from '../../testing'; +import { FakeHeroService } from '../model/testing'; + +class FakeRouter { + navigateByUrl(url: string) { return url; } +} + +describe('DashboardComponent: w/o Angular TestBed', () => { + let comp: DashboardComponent; + let heroService: FakeHeroService; + let router: Router; + + beforeEach(() => { + addMatchers(); + router = new FakeRouter() as any as Router; + heroService = new FakeHeroService(); + comp = new DashboardComponent(router, heroService); + }); + + it('should NOT have heroes before calling OnInit', () => { + expect(comp.heroes.length).toBe(0, + 'should not have heroes before OnInit'); + }); + + it('should NOT have heroes immediately after OnInit', () => { + comp.ngOnInit(); // ngOnInit -> getHeroes + expect(comp.heroes.length).toBe(0, + 'should not have heroes until service promise resolves'); + }); + + it('should HAVE heroes after HeroService gets them', (done: DoneFn) => { + comp.ngOnInit(); // ngOnInit -> getHeroes + heroService.lastPromise // the one from getHeroes + .then(() => { + // throw new Error('deliberate error'); // see it fail gracefully + expect(comp.heroes.length).toBeGreaterThan(0, + 'should have heroes after service promise resolves'); + }) + .then(done, done.fail); + }); + + it('should tell ROUTER to navigate by hero id', () => { + const hero = new Hero(42, 'Abbracadabra'); + const spy = spyOn(router, 'navigateByUrl'); + + comp.gotoDetail(hero); + + const navArgs = spy.calls.mostRecent().args[0]; + expect(navArgs).toBe('/heroes/42', 'should nav to HeroDetail for Hero 42'); + }); + +}); diff --git a/public/docs/_examples/testing/ts/app/dashboard/dashboard.component.spec.ts b/public/docs/_examples/testing/ts/app/dashboard/dashboard.component.spec.ts new file mode 100644 index 0000000000..981e51db0f --- /dev/null +++ b/public/docs/_examples/testing/ts/app/dashboard/dashboard.component.spec.ts @@ -0,0 +1,147 @@ +// #docplaster +import { async, inject, ComponentFixture, TestBed +} from '@angular/core/testing'; + +import { addMatchers } from '../../testing'; +import { HeroService } from '../model'; +import { FakeHeroService } from '../model/testing'; + +import { By } from '@angular/platform-browser'; +import { Router } from '@angular/router'; + +import { DashboardComponent } from './dashboard.component'; +import { DashboardModule } from './dashboard.module'; + +// #docregion fake-router +class FakeRouter { + navigateByUrl(url: string) { return url; } +} +// #enddocregion fake-router + +beforeEach ( addMatchers ); + +let comp: DashboardComponent; +let fixture: ComponentFixture; + +//////// Deep //////////////// + +describe('DashboardComponent (deep)', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ DashboardModule ] + }); + }); + + compileAndCreate(); + + tests(clickForDeep); + + function clickForDeep() { + // get first
DebugElement + const heroEl = fixture.debugElement.query(By.css('.hero')); + heroEl.triggerEventHandler('click', null); + } +}); + +//////// Shallow //////////////// + +import { NO_ERRORS_SCHEMA } from '@angular/core'; + +describe('DashboardComponent (shallow)', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [ DashboardComponent ], + schemas: [NO_ERRORS_SCHEMA] + }); + }); + + compileAndCreate(); + + tests(clickForShallow); + + function clickForShallow() { + // get first DebugElement + const heroEl = fixture.debugElement.query(By.css('dashboard-hero')); + heroEl.triggerEventHandler('selected', comp.heroes[0]); + } +}); + +/** Add TestBed providers, compile, and create DashboardComponent */ +function compileAndCreate() { + // #docregion compile-and-create-body + beforeEach( async(() => { + TestBed.configureTestingModule({ + providers: [ + { provide: HeroService, useClass: FakeHeroService }, + { provide: Router, useClass: FakeRouter } + ] + }) + .compileComponents().then(() => { + fixture = TestBed.createComponent(DashboardComponent); + comp = fixture.componentInstance; + }); + // #enddocregion compile-and-create-body + })); +} + +/** + * The (almost) same tests for both. + * Only change: the way that the first hero is clicked + */ +function tests(heroClick: Function) { + + it('should NOT have heroes before ngOnInit', () => { + expect(comp.heroes.length).toBe(0, + 'should not have heroes before ngOnInit'); + }); + + it('should NOT have heroes immediately after ngOnInit', () => { + fixture.detectChanges(); // runs initial lifecycle hooks + + expect(comp.heroes.length).toBe(0, + 'should not have heroes until service promise resolves'); + }); + + describe('after get dashboard heroes', () => { + + // Trigger component so it gets heroes and binds to them + beforeEach( async(() => { + fixture.detectChanges(); // runs ngOnInit -> getHeroes + fixture.whenStable() // No need for the `lastPromise` hack! + .then(() => fixture.detectChanges()); // bind to heroes + })); + + it('should HAVE heroes', () => { + expect(comp.heroes.length).toBeGreaterThan(0, + 'should have heroes after service promise resolves'); + }); + + it('should DISPLAY heroes', () => { + // Find and examine the displayed heroes + // Look for them in the DOM by css class + const heroes = fixture.debugElement.queryAll(By.css('dashboard-hero')); + expect(heroes.length).toBe(4, 'should display 4 heroes'); + }); + + // #docregion navigate-test, inject + it('should tell ROUTER to navigate when hero clicked', + inject([Router], (router: Router) => { // ... + // #enddocregion inject + + const spy = spyOn(router, 'navigateByUrl'); + + heroClick(); // trigger click on first inner
+ + // args passed to router.navigateByUrl() + const navArgs = spy.calls.first().args[0]; + + // expecting to navigate to id of the component's first hero + const id = comp.heroes[0].id; + expect(navArgs).toBe('/heroes/' + id, + 'should nav to HeroDetail for first hero'); + // #docregion inject + })); + // #enddocregion navigate-test, inject + }); +} + diff --git a/public/docs/_examples/testing/ts/app/dashboard/dashboard.component.ts b/public/docs/_examples/testing/ts/app/dashboard/dashboard.component.ts new file mode 100644 index 0000000000..7c7f4cc077 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/dashboard/dashboard.component.ts @@ -0,0 +1,43 @@ +// #docregion +import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; + +import { Hero, HeroService } from '../model'; + +@Component({ + selector: 'app-dashboard', + templateUrl: 'app/dashboard/dashboard.component.html', + styleUrls: [ + 'app/shared/styles.css', + 'app/dashboard/dashboard.component.css' + ] +}) +export class DashboardComponent implements OnInit { + + heroes: Hero[] = []; + + // #docregion ctor + constructor( + private router: Router, + private heroService: HeroService) { + } + // #enddocregion ctor + + ngOnInit() { + this.heroService.getHeroes() + .then(heroes => this.heroes = heroes.slice(1, 5)); + } + + // #docregion goto-detail + gotoDetail(hero: Hero) { + let url = `/heroes/${hero.id}`; + this.router.navigateByUrl(url); + } + // #enddocregion goto-detail + + get title() { + let cnt = this.heroes.length; + return cnt === 0 ? 'No Heroes' : + cnt === 1 ? 'Top Hero' : `Top ${cnt} Heroes`; + } +} diff --git a/public/docs/_examples/testing/ts/app/dashboard/dashboard.module.ts b/public/docs/_examples/testing/ts/app/dashboard/dashboard.module.ts new file mode 100644 index 0000000000..8a70c851a0 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/dashboard/dashboard.module.ts @@ -0,0 +1,20 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { SharedModule } from '../shared/shared.module'; + +import { DashboardComponent } from './dashboard.component'; +import { DashboardHeroComponent } from './dashboard-hero.component'; + +const routes: Routes = [ + { path: 'dashboard', component: DashboardComponent }, +]; + +@NgModule({ + imports: [ + SharedModule, + RouterModule.forChild(routes) + ], + declarations: [ DashboardComponent, DashboardHeroComponent ] +}) +export class DashboardModule { } diff --git a/public/docs/_examples/testing/ts/app/hero-detail.component.html b/public/docs/_examples/testing/ts/app/hero-detail.component.html deleted file mode 100644 index cf96fc2169..0000000000 --- a/public/docs/_examples/testing/ts/app/hero-detail.component.html +++ /dev/null @@ -1,14 +0,0 @@ - - -
-

{{hero.name}} details!

-
- {{hero.id}}
-
- - -
- - - -
\ No newline at end of file diff --git a/public/docs/_examples/testing/ts/app/hero-detail.component.ts b/public/docs/_examples/testing/ts/app/hero-detail.component.ts deleted file mode 100644 index 3fcbf071e0..0000000000 --- a/public/docs/_examples/testing/ts/app/hero-detail.component.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* tslint:disable */ -// #docplaster -// #docregion -// #docregion v2 -// #docregion import-oninit -import { Component, OnInit } from '@angular/core'; -// #enddocregion import-oninit -// #docregion import-route-params -import { RouteParams } from '@angular/router-deprecated'; -// #enddocregion import-route-params - -import { Hero } from './hero'; -// #docregion import-hero-service -import { HeroService } from './hero.service'; -// #enddocregion import-hero-service - -// #docregion extract-template -@Component({ - selector: 'my-hero-detail', - // #docregion template-url - templateUrl: 'app/hero-detail.component.html', - // #enddocregion template-url -// #enddocregion v2 - styleUrls: ['app/hero-detail.component.css'], - inputs: ['hero'] -// #docregion v2 -}) -// #enddocregion extract-template -// #docregion implement -export class HeroDetailComponent implements OnInit { -// #enddocregion implement - hero: Hero; - -// #docregion ctor - constructor( - private _heroService: HeroService, - private _routeParams: RouteParams) { - } -// #enddocregion ctor - -// #docregion ng-oninit - ngOnInit() { - // #docregion get-id - let id = +this._routeParams.get('id'); - // #enddocregion get-id - this._heroService.getHero(id) - .then(hero => this.hero = hero); - } -// #enddocregion ng-oninit - -// #docregion go-back - goBack() { - window.history.back(); - } -// #enddocregion go-back -} -// #enddocregion v2 -// #enddocregion diff --git a/public/docs/_examples/testing/ts/app/hero.service.ts b/public/docs/_examples/testing/ts/app/hero.service.ts deleted file mode 100644 index e9473e4038..0000000000 --- a/public/docs/_examples/testing/ts/app/hero.service.ts +++ /dev/null @@ -1,28 +0,0 @@ -// #docplaster -// #docregion -import { Hero } from './hero'; -import { HEROES } from './mock-heroes'; -import { Injectable } from '@angular/core'; - -@Injectable() -export class HeroService { - getHeroes() { - return Promise.resolve(HEROES); - } - - // See the "Take it slow" appendix - getHeroesSlowly() { - return new Promise(resolve => - setTimeout(() => resolve(HEROES), 2000) // 2 seconds - ); - } - - // #docregion get-hero - getHero(id: number) { - return Promise.resolve(HEROES).then( - heroes => heroes.find(hero => hero.id === id) - ); - } - // #enddocregion get-hero -} -// #enddocregion diff --git a/public/docs/_examples/testing/ts/app/hero.spec.ts b/public/docs/_examples/testing/ts/app/hero.spec.ts deleted file mode 100644 index 78a73ad6b0..0000000000 --- a/public/docs/_examples/testing/ts/app/hero.spec.ts +++ /dev/null @@ -1,50 +0,0 @@ -// #docregion -// #docplaster -// #docregion base-hero-spec -import { Hero } from './hero'; - -describe('Hero', () => { - - it('has name', () => { - let hero: Hero = {id: 1, name: 'Super Cat'}; - expect(hero.name).toEqual('Super Cat'); - }); - - it('has id', () => { - let hero: Hero = {id: 1, name: 'Super Cat'}; - expect(hero.id).toEqual(1); - }); - // #enddocregion base-hero-spec - - - /* more tests we could run - - it('can clone itself', () => { - let hero = new Hero(1, 'Super Cat'); - let clone = hero.clone(); - expect(hero).toEqual(clone); - }); - - it('has expected generated id when id not given in the constructor', () => { - Hero.setNextId(100); // reset the `nextId` seed - let hero = new Hero(null, 'Cool Kitty'); - expect(hero.id).toEqual(100); - }); - - it('has expected generated id when id=0 in the constructor', () => { - Hero.setNextId(100); - let hero = new Hero(0, 'Cool Kitty'); - expect(hero.id).toEqual(100); - }) - - it('increments generated id for each new Hero w/o an id', () => { - Hero.setNextId(100); - let hero1 = new Hero(0, 'Cool Kitty'); - let hero2 = new Hero(null, 'Hip Cat'); - expect(hero2.id).toEqual(101); - }); - - */ - // #docregion base-hero-spec -}); -// #enddocregion base-hero-spec diff --git a/public/docs/_examples/testing/ts/app/hero.ts b/public/docs/_examples/testing/ts/app/hero.ts deleted file mode 100644 index 8f7cc205c8..0000000000 --- a/public/docs/_examples/testing/ts/app/hero.ts +++ /dev/null @@ -1,5 +0,0 @@ -// #docregion -export class Hero { - id: number; - name: string; -} diff --git a/public/docs/_examples/testing/ts/app/hero-detail.component.css b/public/docs/_examples/testing/ts/app/hero/hero-detail.component.css similarity index 93% rename from public/docs/_examples/testing/ts/app/hero-detail.component.css rename to public/docs/_examples/testing/ts/app/hero/hero-detail.component.css index ab2437efd8..f6139ba274 100644 --- a/public/docs/_examples/testing/ts/app/hero-detail.component.css +++ b/public/docs/_examples/testing/ts/app/hero/hero-detail.component.css @@ -1,4 +1,3 @@ -/* #docregion */ label { display: inline-block; width: 3em; @@ -25,6 +24,6 @@ button:hover { } button:disabled { background-color: #eee; - color: #ccc; + color: #ccc; cursor: auto; } diff --git a/public/docs/_examples/testing/ts/app/hero/hero-detail.component.html b/public/docs/_examples/testing/ts/app/hero/hero-detail.component.html new file mode 100644 index 0000000000..6927fc83ad --- /dev/null +++ b/public/docs/_examples/testing/ts/app/hero/hero-detail.component.html @@ -0,0 +1,11 @@ +
+

{{hero.name | titlecase}} Details

+
+ {{hero.id}}
+
+ + +
+ + +
diff --git a/public/docs/_examples/testing/ts/app/hero/hero-detail.component.no-testbed.spec.ts b/public/docs/_examples/testing/ts/app/hero/hero-detail.component.no-testbed.spec.ts new file mode 100644 index 0000000000..73c22f29e7 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/hero/hero-detail.component.no-testbed.spec.ts @@ -0,0 +1,58 @@ +import { HeroDetailComponent } from './hero-detail.component'; +import { Hero } from '../model'; + +import { FakeActivatedRoute } from '../../testing'; + +////////// Tests //////////////////// + +describe('HeroDetailComponent - no TestBed', () => { + let activatedRoute: FakeActivatedRoute; + let comp: HeroDetailComponent; + let expectedHero: Hero; + let hds: any; + let router: any; + + beforeEach( done => { + expectedHero = new Hero(42, 'Bubba'); + activatedRoute = new FakeActivatedRoute(); + activatedRoute.testParams = { id: expectedHero.id }; + + router = jasmine.createSpyObj('router', ['navigate']); + + hds = jasmine.createSpyObj('HeroDetailService', ['getHero', 'saveHero']); + hds.getHero.and.returnValue(Promise.resolve(expectedHero)); + hds.saveHero.and.returnValue(Promise.resolve(expectedHero)); + + comp = new HeroDetailComponent(hds, activatedRoute, router); + comp.ngOnInit(); + + // OnInit calls HDS.getHero; wait for it to get the fake hero + hds.getHero.calls.first().returnValue.then(done); + }); + + it('should expose the hero retrieved from the service', () => { + expect(comp.hero).toBe(expectedHero); + }); + + it('should navigate when click cancel', () => { + comp.cancel(); + expect(router.navigate.calls.any()).toBe(true, 'router.navigate called'); + }); + + it('should save when click save', () => { + comp.save(); + expect(hds.saveHero.calls.any()).toBe(true, 'HeroDetailService.save called'); + expect(router.navigate.calls.any()).toBe(false, 'router.navigate not called yet'); + }); + + it('should navigate when click save resolves', done => { + comp.save(); + // waits for async save to complete before navigating + hds.saveHero.calls.first().returnValue + .then(() => { + expect(router.navigate.calls.any()).toBe(true, 'router.navigate called'); + done(); + }); + }); + +}); diff --git a/public/docs/_examples/testing/ts/app/hero/hero-detail.component.spec.ts b/public/docs/_examples/testing/ts/app/hero/hero-detail.component.spec.ts new file mode 100644 index 0000000000..0dd52ed54e --- /dev/null +++ b/public/docs/_examples/testing/ts/app/hero/hero-detail.component.spec.ts @@ -0,0 +1,196 @@ +import { + async, ComponentFixture, fakeAsync, inject, TestBed, tick +} from '@angular/core/testing'; + +import { By } from '@angular/platform-browser'; +import { DebugElement } from '@angular/core'; + +import { + addMatchers, newEvent, + ActivatedRoute, FakeActivatedRoute, Router, FakeRouter +} from '../../testing'; + +import { HEROES, FakeHeroService } from '../model/testing'; + +import { HeroModule } from './hero.module'; +import { HeroDetailComponent } from './hero-detail.component'; +import { HeroDetailService } from './hero-detail.service'; +import { Hero, HeroService } from '../model'; + +////// Testing Vars ////// +let activatedRoute: FakeActivatedRoute; +let comp: HeroDetailComponent; +let fixture: ComponentFixture; +let page: Page; + +////////// Tests //////////////////// + +describe('HeroDetailComponent', () => { + + beforeEach( async(() => { + addMatchers(); + activatedRoute = new FakeActivatedRoute(); + + TestBed.configureTestingModule({ + imports: [ HeroModule ], + + // DON'T RE-DECLARE because already declared in HeroModule + // declarations: [HeroDetailComponent, TitleCasePipe], // No! + + providers: [ + { provide: ActivatedRoute, useValue: activatedRoute }, + { provide: HeroService, useClass: FakeHeroService }, + { provide: Router, useClass: FakeRouter}, + ] + }) + .compileComponents(); + })); + + describe('when navigate to hero id=' + HEROES[0].id, () => { + let expectedHero: Hero; + + beforeEach( async(() => { + expectedHero = HEROES[0]; + activatedRoute.testParams = { id: expectedHero.id }; + createComponent(); + })); + + it('should display that hero\'s name', () => { + expect(page.nameDisplay.textContent).toBe(expectedHero.name); + }); + + it('should navigate when click cancel', () => { + page.cancelBtn.triggerEventHandler('click', null); + expect(page.navSpy.calls.any()).toBe(true, 'router.navigate called'); + }); + + it('should save when click save', () => { + page.saveBtn.triggerEventHandler('click', null); + expect(page.saveSpy.calls.any()).toBe(true, 'HeroDetailService.save called'); + }); + + it('should navigate when click click save resolves', fakeAsync(() => { + page.saveBtn.triggerEventHandler('click', null); + tick(); // waits for async save to "complete" before navigating + expect(page.navSpy.calls.any()).toBe(true, 'router.navigate called'); + })); + + + // #docregion title-case-pipe + it('should convert original hero name to Title Case', () => { + expect(page.nameDisplay.textContent).toBe(comp.hero.name); + }); + // #enddocregion title-case-pipe + + it('should convert hero name to Title Case', fakeAsync(() => { + const inputName = 'quick BROWN fox'; + const expectedName = 'Quick Brown Fox'; + + // simulate user entering new name in input + page.nameInput.value = inputName; + + // dispatch a DOM event so that Angular learns of input value change. + // detectChanges() makes ngModel push input value to component property + // and Angular updates the output span + page.nameInput.dispatchEvent(newEvent('input')); + fixture.detectChanges(); + expect(page.nameDisplay.textContent).toBe(expectedName, 'hero name display'); + expect(comp.hero.name).toBe(inputName, 'comp.hero.name'); + })); + + }); + + describe('when navigate with no hero id', () => { + beforeEach( async( createComponent )); + + it('should have hero.id === 0', () => { + expect(comp.hero.id).toBe(0); + }); + + it('should display empty hero name', () => { + expect(page.nameDisplay.textContent).toBe(''); + }); + }); + + describe('when navigate to non-existant hero id', () => { + beforeEach( async(() => { + activatedRoute.testParams = { id: 99999 }; + createComponent(); + })); + + it('should try to navigate back to hero list', () => { + expect(page.gotoSpy.calls.any()).toBe(true, 'comp.gotoList called'); + expect(page.navSpy.calls.any()).toBe(true, 'router.navigate called'); + }); + }); + + /////////////////////////// + + // Why we must use `fixture.debugElement.injector` in `Page()` + it('cannot use `inject` to get component\'s provided service', () => { + let service: HeroDetailService; + fixture = TestBed.createComponent(HeroDetailComponent); + expect( + // Throws because `inject` only has access to TestBed's injector + // which is an ancestor of the component's injector + inject([HeroDetailService], (hds: HeroDetailService) => service = hds ) + ) + .toThrowError(/No provider for HeroDetailService/); + + // get `HeroDetailService` with component's own injector + service = fixture.debugElement.injector.get(HeroDetailService); + expect(service).toBeDefined('debugElement.injector'); + }); +}); + +/////////// Helpers ///// + +/** Create the HeroDetailComponent, initialize it, set test variables */ +function createComponent() { + fixture = TestBed.createComponent(HeroDetailComponent); + comp = fixture.componentInstance; + page = new Page(); + + // change detection triggers ngOnInit which gets a hero + fixture.detectChanges(); + return fixture.whenStable().then(() => { + // got the hero and updated component + // change detection updates the view + fixture.detectChanges(); + page.addPageElements(); + }); +} + +class Page { + gotoSpy: jasmine.Spy; + navSpy: jasmine.Spy; + saveSpy: jasmine.Spy; + + saveBtn: DebugElement; + cancelBtn: DebugElement; + nameDisplay: HTMLElement; + nameInput: HTMLInputElement; + + constructor() { + // Use component's injector to see the services it injected. + let compInjector = fixture.debugElement.injector; + let hds = compInjector.get(HeroDetailService); + let router = compInjector.get(Router); + this.gotoSpy = spyOn(comp, 'gotoList').and.callThrough(); + this.saveSpy = spyOn(hds, 'saveHero').and.callThrough(); + this.navSpy = spyOn(router, 'navigate').and.callThrough(); + } + + /** Add page elements after page initializes */ + addPageElements() { + if (comp.hero) { + // have a hero so these DOM elements can be reached + let buttons = fixture.debugElement.queryAll(By.css('button')); + this.saveBtn = buttons[0]; + this.cancelBtn = buttons[1]; + this.nameDisplay = fixture.debugElement.query(By.css('span')).nativeElement; + this.nameInput = fixture.debugElement.query(By.css('input')).nativeElement; + } + } +} + diff --git a/public/docs/_examples/testing/ts/app/hero/hero-detail.component.ts b/public/docs/_examples/testing/ts/app/hero/hero-detail.component.ts new file mode 100644 index 0000000000..9350c369af --- /dev/null +++ b/public/docs/_examples/testing/ts/app/hero/hero-detail.component.ts @@ -0,0 +1,52 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; + +import { Hero } from '../model'; +import { HeroDetailService } from './hero-detail.service'; + +@Component({ + selector: 'app-hero-detail', + templateUrl: 'app/hero/hero-detail.component.html', + styleUrls: [ + 'app/shared/styles.css', + 'app/hero/hero-detail.component.css' + ], + providers: [ HeroDetailService ] +}) +export class HeroDetailComponent implements OnInit { + @Input() hero: Hero; + + constructor( + private heroDetailService: HeroDetailService, + private route: ActivatedRoute, + private router: Router) { + } + + ngOnInit() { + let id = this.route.snapshot.params['id']; + + // tslint:disable-next-line:triple-equals + if (id == undefined) { + // no id; act as if is new + this.hero = new Hero(); + } else { + this.heroDetailService.getHero(id).then(hero => { + if (hero) { + this.hero = hero; + } else { + this.gotoList(); // id not found; navigate to list + } + }); + } + } + + save() { + this.heroDetailService.saveHero(this.hero).then(() => this.gotoList()); + } + + cancel() { this.gotoList(); } + + gotoList() { + this.router.navigate(['../'], {relativeTo: this.route}); + } +} diff --git a/public/docs/_examples/testing/ts/app/hero/hero-detail.service.ts b/public/docs/_examples/testing/ts/app/hero/hero-detail.service.ts new file mode 100644 index 0000000000..970cb1b98b --- /dev/null +++ b/public/docs/_examples/testing/ts/app/hero/hero-detail.service.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@angular/core'; + +import { Hero, HeroService } from '../model'; + +@Injectable() +export class HeroDetailService { + constructor(private heroService: HeroService) { } + + getHero(id: number | string): Promise { + if (typeof id === 'string') { + id = parseInt(id as string, 10); + } + return this.heroService.getHero(id).then(hero => { + return hero ? Object.assign({}, hero) : null; // clone or null + }); + } + + saveHero(hero: Hero) { + return this.heroService.updateHero(hero); + } +} diff --git a/public/docs/_examples/testing/ts/app/heroes.component.css b/public/docs/_examples/testing/ts/app/hero/hero-list.component.css similarity index 100% rename from public/docs/_examples/testing/ts/app/heroes.component.css rename to public/docs/_examples/testing/ts/app/hero/hero-list.component.css diff --git a/public/docs/_examples/testing/ts/app/hero/hero-list.component.html b/public/docs/_examples/testing/ts/app/hero/hero-list.component.html new file mode 100644 index 0000000000..cd37301fd6 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/hero/hero-list.component.html @@ -0,0 +1,8 @@ +

My Heroes

+
    +
  • + {{hero.id}} {{hero.name}} +
  • +
diff --git a/public/docs/_examples/testing/ts/app/hero/hero-list.component.spec.ts b/public/docs/_examples/testing/ts/app/hero/hero-list.component.spec.ts new file mode 100644 index 0000000000..f997cf787e --- /dev/null +++ b/public/docs/_examples/testing/ts/app/hero/hero-list.component.spec.ts @@ -0,0 +1,139 @@ +import { async, ComponentFixture, fakeAsync, TestBed, tick +} from '@angular/core/testing'; + +import { By } from '@angular/platform-browser'; +import { DebugElement } from '@angular/core'; + +import { addMatchers, newEvent, Router, FakeRouter +} from '../../testing'; + +import { HEROES, FakeHeroService } from '../model/testing'; + +import { HeroModule } from './hero.module'; +import { HeroListComponent } from './hero-list.component'; +import { HighlightDirective } from '../shared/highlight.directive'; +import { HeroService } from '../model'; + +let comp: HeroListComponent; +let fixture: ComponentFixture; +let page: Page; + +/////// Tests ////// + +describe('HeroListComponent', () => { + + beforeEach( async(() => { + addMatchers(); + TestBed.configureTestingModule({ + imports: [HeroModule], + providers: [ + { provide: HeroService, useClass: FakeHeroService }, + { provide: Router, useClass: FakeRouter} + ] + }) + .compileComponents() + .then(createComponent); + })); + + it('should display heroes', () => { + expect(page.heroRows.length).toBeGreaterThan(0); + }); + + it('1st hero should match 1st test hero', () => { + const expectedHero = HEROES[0]; + const actualHero = page.heroRows[0].textContent; + expect(actualHero).toContain(expectedHero.id, 'hero.id'); + expect(actualHero).toContain(expectedHero.name, 'hero.name'); + }); + + it('should select hero on click', fakeAsync(() => { + const expectedHero = HEROES[1]; + const li = page.heroRows[1]; + li.dispatchEvent(newEvent('click')); + tick(); + // `.toEqual` because selectedHero is clone of expectedHero; see FakeHeroService + expect(comp.selectedHero).toEqual(expectedHero); + })); + + it('should navigate to selected hero detail on click', fakeAsync(() => { + const expectedHero = HEROES[1]; + const li = page.heroRows[1]; + li.dispatchEvent(newEvent('click')); + tick(); + + // should have navigated + expect(page.navSpy.calls.any()).toBe(true, 'navigate called'); + + // composed hero detail will be URL like 'heroes/42' + // expect link array with the route path and hero id + // first argument to router.navigate is link array + const navArgs = page.navSpy.calls.first().args[0]; + expect(navArgs[0]).toContain('heroes', 'nav to heroes detail URL'); + expect(navArgs[1]).toBe(expectedHero.id, 'expected hero.id'); + + })); + + it('should find `HighlightDirective` with `By.directive', () => { + // #docregion by + // Can find DebugElement either by css selector or by directive + const h2 = fixture.debugElement.query(By.css('h2')); + const directive = fixture.debugElement.query(By.directive(HighlightDirective)); + // #enddocregion by + expect(h2).toBe(directive); + }); + + it('should color header with `HighlightDirective`', () => { + const h2 = page.highlightDe.nativeElement as HTMLElement; + const bgColor = h2.style.backgroundColor; + + // different browsers report color values differently + const isExpectedColor = bgColor === 'gold' || bgColor === 'rgb(255, 215, 0)'; + expect(isExpectedColor).toBe(true, 'backgroundColor'); + }); + + it('the `HighlightDirective` is among the element\'s providers', () => { + expect(page.highlightDe.providerTokens).toContain(HighlightDirective, 'HighlightDirective'); + }); +}); + +/////////// Helpers ///// + +/** Create the component and set the `page` test variables */ +function createComponent() { + fixture = TestBed.createComponent(HeroListComponent); + comp = fixture.componentInstance; + + // change detection triggers ngOnInit which gets a hero + fixture.detectChanges(); + + return fixture.whenStable().then(() => { + // got the heroes and updated component + // change detection updates the view + fixture.detectChanges(); + page = new Page(); + }); +} + +class Page { + /** Hero line elements */ + heroRows: HTMLLIElement[]; + + /** Highlighted element */ + highlightDe: DebugElement; + + /** Spy on router navigate method */ + navSpy: jasmine.Spy; + + constructor() { + this.heroRows = fixture.debugElement.queryAll(By.css('li')).map(de => de.nativeElement); + + // Find the first element with an attached HighlightDirective + this.highlightDe = fixture.debugElement.query(By.directive(HighlightDirective)); + + // Get the component's injected router and spy on it + const router = fixture.debugElement.injector.get(Router); + this.navSpy = spyOn(router, 'navigate').and.callThrough(); + }; +} + + diff --git a/public/docs/_examples/testing/ts/app/hero/hero-list.component.ts b/public/docs/_examples/testing/ts/app/hero/hero-list.component.ts new file mode 100644 index 0000000000..d4ad30b019 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/hero/hero-list.component.ts @@ -0,0 +1,30 @@ +import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; + +import { Hero, HeroService } from '../model'; + +@Component({ + selector: 'app-heroes', + templateUrl: 'app/hero/hero-list.component.html', + styleUrls: [ + 'app/shared/styles.css', + 'app/hero/hero-list.component.css' + ] +}) +export class HeroListComponent implements OnInit { + heroes: Promise; + selectedHero: Hero; + + constructor( + private router: Router, + private heroService: HeroService) { } + + ngOnInit() { + this.heroes = this.heroService.getHeroes(); + } + + onSelect(hero: Hero) { + this.selectedHero = hero; + this.router.navigate(['../heroes', this.selectedHero.id ]); + } +} diff --git a/public/docs/_examples/testing/ts/app/hero/hero.module.ts b/public/docs/_examples/testing/ts/app/hero/hero.module.ts new file mode 100644 index 0000000000..541d49103f --- /dev/null +++ b/public/docs/_examples/testing/ts/app/hero/hero.module.ts @@ -0,0 +1,9 @@ +import { NgModule } from '@angular/core'; +import { SharedModule } from '../shared/shared.module'; +import { routedComponents, routing } from './hero.routing'; + +@NgModule({ + imports: [ SharedModule, routing ], + declarations: [ routedComponents ] +}) +export class HeroModule { } diff --git a/public/docs/_examples/testing/ts/app/hero/hero.routing.ts b/public/docs/_examples/testing/ts/app/hero/hero.routing.ts new file mode 100644 index 0000000000..9530bc3953 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/hero/hero.routing.ts @@ -0,0 +1,12 @@ +import { RouterModule, Routes } from '@angular/router'; + +import { HeroListComponent } from './hero-list.component'; +import { HeroDetailComponent } from './hero-detail.component'; + +const routes: Routes = [ + { path: '', component: HeroListComponent }, + { path: ':id', component: HeroDetailComponent } +]; + +export const routedComponents = [HeroDetailComponent, HeroListComponent]; +export const routing = RouterModule.forChild(routes); diff --git a/public/docs/_examples/testing/ts/app/heroes.component.html b/public/docs/_examples/testing/ts/app/heroes.component.html deleted file mode 100644 index cce1853d30..0000000000 --- a/public/docs/_examples/testing/ts/app/heroes.component.html +++ /dev/null @@ -1,21 +0,0 @@ - - -

My Heroes

-
    -
  • - {{hero.id}} {{hero.name}} -
  • -
- -
-

- - {{selectedHero.name | uppercase}} is my hero - -

- -
- - diff --git a/public/docs/_examples/testing/ts/app/heroes.component.ts b/public/docs/_examples/testing/ts/app/heroes.component.ts deleted file mode 100644 index 1e2651f256..0000000000 --- a/public/docs/_examples/testing/ts/app/heroes.component.ts +++ /dev/null @@ -1,50 +0,0 @@ -// #docplaster -// #docregion -import { Component, OnInit } from '@angular/core'; -import { Router } from '@angular/router-deprecated'; - -import { Hero } from './hero'; -import { HeroDetailComponent } from './hero-detail.component'; -import { HeroService } from './hero.service'; - -// #docregion metadata -// #docregion heroes-component-renaming -@Component({ - selector: 'my-heroes', -// #enddocregion heroes-component-renaming - templateUrl: 'app/heroes.component.html', - styleUrls: ['app/heroes.component.css'], - directives: [HeroDetailComponent] -// #docregion heroes-component-renaming -}) -// #enddocregion heroes-component-renaming -// #enddocregion metadata -// #docregion class -// #docregion heroes-component-renaming -export class HeroesComponent implements OnInit { -// #enddocregion heroes-component-renaming - heroes: Hero[]; - selectedHero: Hero; - - constructor( - private _router: Router, - private _heroService: HeroService) { } - - getHeroes() { - this._heroService.getHeroes().then(heroes => this.heroes = heroes); - } - - ngOnInit() { - this.getHeroes(); - } - - onSelect(hero: Hero) { this.selectedHero = hero; } - - gotoDetail() { - this._router.navigate(['HeroDetail', { id: this.selectedHero.id }]); - } -// #docregion heroes-component-renaming -} -// #enddocregion heroes-component-renaming -// #enddocregion class -// #enddocregion diff --git a/public/docs/_examples/testing/ts/app/main.ts b/public/docs/_examples/testing/ts/app/main.ts index 5f185788a3..2c89d35a81 100644 --- a/public/docs/_examples/testing/ts/app/main.ts +++ b/public/docs/_examples/testing/ts/app/main.ts @@ -1,5 +1,5 @@ -import { bootstrap } from '@angular/platform-browser-dynamic'; -import { AppComponent } from './app.component'; - -bootstrap(AppComponent); +// main app entry point +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; +import { AppModule } from './app.module'; +platformBrowserDynamic().bootstrapModule(AppModule); diff --git a/public/docs/_examples/testing/ts/app/mock-hero.service.ts b/public/docs/_examples/testing/ts/app/mock-hero.service.ts deleted file mode 100644 index b1538be366..0000000000 --- a/public/docs/_examples/testing/ts/app/mock-hero.service.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { HEROES } from './mock-heroes'; -import { Hero } from './hero'; -import { HeroService } from './hero.service'; - -export { Hero } from './hero'; -export { HeroService } from './hero.service'; - -export class MockHeroService implements HeroService { - - mockHeroes = HEROES.slice(); - lastPromise: Promise; // so we can spy on promise calls - - getHero(id: number) { - return this.lastPromise = Promise.resolve(this.mockHeroes[0]); - } - - getHeroes() { - return this.lastPromise = Promise.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 deleted file mode 100644 index ddd36d7868..0000000000 --- a/public/docs/_examples/testing/ts/app/mock-heroes.ts +++ /dev/null @@ -1,16 +0,0 @@ -// #docregion -import { Hero } from './hero'; - -export var HEROES: Hero[] = [ - {id: 11, name: 'Mr. Nice'}, - {id: 12, name: 'Narco'}, - {id: 13, name: 'Bombasto'}, - {id: 14, name: 'Celeritas'}, - {id: 15, name: 'Magneta'}, - {id: 16, name: 'RubberMan'}, - {id: 17, name: 'Dynama'}, - {id: 18, name: 'Dr IQ'}, - {id: 19, name: 'Magma'}, - {id: 20, name: 'Tornado'} -]; -// #enddocregion diff --git a/public/docs/_examples/testing/ts/app/mock-router.ts b/public/docs/_examples/testing/ts/app/mock-router.ts deleted file mode 100644 index a49763f7cf..0000000000 --- a/public/docs/_examples/testing/ts/app/mock-router.ts +++ /dev/null @@ -1,217 +0,0 @@ -/* tslint:disable */ -export * from '@angular/router-deprecated'; - -import { Directive, DynamicComponentLoader, ViewContainerRef, - Injectable, Optional, Input } from '@angular/core'; - -import { ComponentInstruction, Instruction, - Router, RouterOutlet} from '@angular/router-deprecated'; - -let _resolveToTrue = Promise.resolve(true); - -const NOT_IMPLEMENTED = (what: string) => { - throw new Error (`"${what}" is not implemented`); -}; - - -@Directive({ - selector: '[routerLink]', - host: { - '(click)': 'onClick()', - '[attr.href]': 'visibleHref', - '[class.router-link-active]': 'isRouteActive' - } -}) -export class MockRouterLink { - - isRouteActive = false; - visibleHref: string; // the url displayed on the anchor element. - - @Input('routerLink') routeParams: any[]; - @Input() target: string; - navigatedTo: any[] = null; - - constructor(public router: Router) { } - - onClick() { - this.navigatedTo = null; - - // If no target, or if target is _self, prevent default browser behavior - if (!this.target || typeof this.target !== 'string' || this.target === '_self') { - this.navigatedTo = this.routeParams; - return false; - } - return true; - } -} - -@Directive({selector: 'router-outlet'}) -export class MockRouterOutlet extends RouterOutlet { - name: string = null; - - constructor( - _viewContainerRef: ViewContainerRef, - @Optional() _loader: DynamicComponentLoader, - _parentRouter: Router, - nameAttr: string) { - super(_viewContainerRef, _loader, _parentRouter, nameAttr); - if (nameAttr) { - this.name = nameAttr; - } - } - - /** - * Called by the Router to instantiate a new component during the commit phase of a navigation. - * This method in turn is responsible for calling the `routerOnActivate` hook of its child. - */ - activate(nextInstruction: ComponentInstruction): Promise { 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 Promise.resolve(linkParams); - } - - /** - * Navigate to a URL. Returns a promise that resolves when navigation is complete. - * It's preferred to navigate with `navigate` instead of this method, since URLs are more brittle. - * - * If the given URL begins with a `/`, router will navigate absolutely. - * If the given URL does not begin with `/`, the router will navigate relative to this component. - */ - navigateByUrl(url: string, _skipLocationChange = false): Promise { - return Promise.resolve(url); - } - - - /** - * Navigate via the provided instruction. Returns a promise that resolves when navigation is - * complete. - */ - navigateByInstruction(instruction: Instruction, _skipLocationChange = false): Promise { - return Promise.resolve(instruction); - } - - /** - * Subscribe to URL updates from the router - */ - subscribe(onNext: (v: any) => void, onError?: (v: any) => void) { - return {onNext, onError}; - } - - /** - * Given a URL, returns an instruction representing the component graph - */ - recognize(url: string): Promise { - return Promise.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/model/hero.service.ts b/public/docs/_examples/testing/ts/app/model/hero.service.ts new file mode 100644 index 0000000000..7f2931a7f6 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/model/hero.service.ts @@ -0,0 +1,29 @@ +import { Injectable } from '@angular/core'; + +import { Hero } from './hero'; +import { HEROES } from './test-heroes'; + +@Injectable() +/** Dummy HeroService that pretends to be real */ +export class HeroService { + getHeroes() { + return Promise.resolve(HEROES); + } + + getHero(id: number | string): Promise { + if (typeof id === 'string') { + id = parseInt(id as string, 10); + } + return this.getHeroes().then( + heroes => heroes.find(hero => hero.id === id) + ); + } + + updateHero(hero: Hero): Promise { + return this.getHero(hero.id).then(h => { + return h ? + Object.assign(h, hero) : + Promise.reject(`Hero ${hero.id} not found`) as any as Promise; + }); + } +} diff --git a/public/docs/_examples/testing/ts/app/model/hero.spec.ts b/public/docs/_examples/testing/ts/app/model/hero.spec.ts new file mode 100644 index 0000000000..e8acf913f2 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/model/hero.spec.ts @@ -0,0 +1,20 @@ +// #docregion +import { Hero } from './hero'; + +describe('Hero', () => { + it('has name', () => { + const hero = new Hero(1, 'Super Cat'); + expect(hero.name).toBe('Super Cat'); + }); + + it('has id', () => { + const hero = new Hero(1, 'Super Cat'); + expect(hero.id).toBe(1); + }); + + it('can clone itself', () => { + const hero = new Hero(1, 'Super Cat'); + const clone = hero.clone(); + expect(hero).toEqual(clone); + }); +}); diff --git a/public/docs/_examples/testing/ts/app/model/hero.ts b/public/docs/_examples/testing/ts/app/model/hero.ts new file mode 100644 index 0000000000..6a98f0dfdc --- /dev/null +++ b/public/docs/_examples/testing/ts/app/model/hero.ts @@ -0,0 +1,4 @@ +export class Hero { + constructor(public id = 0, public name = '') { } + clone() { return new Hero(this.id, this.name); } +} diff --git a/public/docs/_examples/testing/ts/app/http-hero.service.spec.ts b/public/docs/_examples/testing/ts/app/model/http-hero.service.spec.ts similarity index 67% rename from public/docs/_examples/testing/ts/app/http-hero.service.spec.ts rename to public/docs/_examples/testing/ts/app/model/http-hero.service.spec.ts index 375efde560..c16b421274 100644 --- a/public/docs/_examples/testing/ts/app/http-hero.service.spec.ts +++ b/public/docs/_examples/testing/ts/app/model/http-hero.service.spec.ts @@ -1,59 +1,54 @@ -/* tslint:disable:no-unused-variable */ import { - addProviders, - async, inject, withProviders + async, inject, TestBed } from '@angular/core/testing'; -import { TestComponentBuilder } from '@angular/core/testing'; - import { MockBackend, - MockConnection } from '@angular/http/testing'; + MockConnection +} from '@angular/http/testing'; import { - Http, HTTP_PROVIDERS, - ConnectionBackend, XHRBackend, - Request, RequestMethod, BaseRequestOptions, RequestOptions, - Response, ResponseOptions, - URLSearchParams + HttpModule, Http, XHRBackend, Response, ResponseOptions } from '@angular/http'; -// Add all operators to Observable -import 'rxjs/Rx'; import { Observable } from 'rxjs/Observable'; +import 'rxjs/add/observable/of'; -import { Hero } from './hero'; -import { HeroService } from './http-hero.service'; +import 'rxjs/add/operator/catch'; +import 'rxjs/add/operator/do'; +import 'rxjs/add/operator/toPromise'; -type HeroData = {id: string, name: string} +import { Hero } from './hero'; +import { HttpHeroService as HeroService } from './http-hero.service'; const makeHeroData = () => [ - { id: '1', name: 'Windstorm' }, - { id: '2', name: 'Bombasto' }, - { id: '3', name: 'Magneta' }, - { id: '4', name: 'Tornado' } -]; + { id: 1, name: 'Windstorm' }, + { id: 2, name: 'Bombasto' }, + { id: 3, name: 'Magneta' }, + { id: 4, name: 'Tornado' } +] as Hero[]; -// HeroService expects response data like {data: {the-data}} -const makeResponseData = (data: {}) => {return { data }; }; - -//////// SPECS ///////////// +//////// Tests ///////////// describe('Http-HeroService (mockBackend)', () => { - beforeEach(() => { - addProviders([ - HTTP_PROVIDERS, - { provide: XHRBackend, useClass: MockBackend } - ]); - }); + beforeEach( async(() => { + TestBed.configureTestingModule({ + imports: [ HttpModule ], + providers: [ + HeroService, + { provide: XHRBackend, useClass: MockBackend } + ] + }) + .compileComponents(); + })); it('can instantiate service when inject service', - withProviders(() => [HeroService]) - .inject([HeroService], (service: HeroService) => { - expect(service instanceof HeroService).toBe(true); + inject([HeroService], (service: HeroService) => { + expect(service instanceof HeroService).toBe(true); })); + it('can instantiate service with "new"', inject([Http], (http: Http) => { expect(http).not.toBeNull('http should be provided'); let service = new HeroService(http); @@ -69,10 +64,9 @@ describe('Http-HeroService (mockBackend)', () => { describe('when getHeroes', () => { let backend: MockBackend; let service: HeroService; - let fakeHeroes: HeroData[]; + let fakeHeroes: Hero[]; let response: Response; - beforeEach(inject([Http, XHRBackend], (http: Http, be: MockBackend) => { backend = be; service = new HeroService(http); @@ -87,7 +81,7 @@ describe('Http-HeroService (mockBackend)', () => { service.getHeroes().toPromise() // .then(() => Promise.reject('deliberate')) .then(heroes => { - expect(heroes.length).toEqual(fakeHeroes.length, + expect(heroes.length).toBe(fakeHeroes.length, 'should have expected no. of heroes'); }); }))); @@ -97,7 +91,7 @@ describe('Http-HeroService (mockBackend)', () => { service.getHeroes() .do(heroes => { - expect(heroes.length).toEqual(fakeHeroes.length, + expect(heroes.length).toBe(fakeHeroes.length, 'should have expected no. of heroes'); }) .toPromise(); @@ -110,7 +104,7 @@ describe('Http-HeroService (mockBackend)', () => { service.getHeroes() .do(heroes => { - expect(heroes.length).toEqual(0, 'should have no heroes'); + expect(heroes.length).toBe(0, 'should have no heroes'); }) .toPromise(); }))); diff --git a/public/docs/_examples/testing/ts/app/http-hero.service.ts b/public/docs/_examples/testing/ts/app/model/http-hero.service.ts similarity index 68% rename from public/docs/_examples/testing/ts/app/http-hero.service.ts rename to public/docs/_examples/testing/ts/app/model/http-hero.service.ts index bfde5bfdc8..a5fe46b801 100644 --- a/public/docs/_examples/testing/ts/app/http-hero.service.ts +++ b/public/docs/_examples/testing/ts/app/model/http-hero.service.ts @@ -4,10 +4,16 @@ import { Injectable } from '@angular/core'; import { Http, Response } from '@angular/http'; import { Headers, RequestOptions } from '@angular/http'; import { Hero } from './hero'; + import { Observable } from 'rxjs/Observable'; +import 'rxjs/add/observable/throw'; + +import 'rxjs/add/operator/do'; +import 'rxjs/add/operator/catch'; +import 'rxjs/add/operator/map'; @Injectable() -export class HeroService { +export class HttpHeroService { private _heroesUrl = 'app/heroes'; // URL to web api constructor (private http: Http) {} @@ -19,6 +25,12 @@ export class HeroService { .catch(this.handleError); } + getHero(id: number | string) { + return this.http + .get('app/heroes/?id=${id}') + .map((r: Response) => r.json().data as Hero[]); + } + addHero (name: string): Observable { let body = JSON.stringify({ name }); let headers = new Headers({ 'Content-Type': 'application/json' }); @@ -29,6 +41,16 @@ export class HeroService { .catch(this.handleError); } + updateHero (hero: Hero): Observable { + let body = JSON.stringify(hero); + let headers = new Headers({ 'Content-Type': 'application/json' }); + let options = new RequestOptions({ headers: headers }); + + return this.http.put(this._heroesUrl, body, options) + .map(this.extractData) + .catch(this.handleError); + } + private extractData(res: Response) { if (res.status < 200 || res.status >= 300) { throw new Error('Bad response status: ' + res.status); diff --git a/public/docs/_examples/testing/ts/app/model/index.ts b/public/docs/_examples/testing/ts/app/model/index.ts new file mode 100644 index 0000000000..227004d5be --- /dev/null +++ b/public/docs/_examples/testing/ts/app/model/index.ts @@ -0,0 +1,7 @@ +// Model barrel +export * from './hero'; +export * from './hero.service'; +export * from './http-hero.service'; +export * from './test-heroes'; + +export * from './user.service'; diff --git a/public/docs/_examples/testing/ts/app/model/test-heroes.ts b/public/docs/_examples/testing/ts/app/model/test-heroes.ts new file mode 100644 index 0000000000..d40ce5d564 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/model/test-heroes.ts @@ -0,0 +1,11 @@ +// #docregion +import { Hero } from './hero'; + +export var HEROES: Hero[] = [ + new Hero(11, 'Mr. Nice'), + new Hero(12, 'Narco'), + new Hero(13, 'Bombasto'), + new Hero(14, 'Celeritas'), + new Hero(15, 'Magneta'), + new Hero(16, 'RubberMan') +]; diff --git a/public/docs/_examples/testing/ts/app/model/testing/fake-hero.service.ts b/public/docs/_examples/testing/ts/app/model/testing/fake-hero.service.ts new file mode 100644 index 0000000000..79a865cc44 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/model/testing/fake-hero.service.ts @@ -0,0 +1,41 @@ +// re-export for tester convenience +export { Hero } from '../hero'; +export { HeroService } from '../hero.service'; + +import { Hero } from '../hero'; +import { HeroService } from '../hero.service'; + +export var HEROES: Hero[] = [ + new Hero(41, 'Bob'), + new Hero(42, 'Carol'), + new Hero(43, 'Ted'), + new Hero(44, 'Alice'), + new Hero(45, 'Speedy'), + new Hero(46, 'Stealthy') +]; + +export class FakeHeroService implements HeroService { + + heroes = HEROES.map(h => h.clone()); + lastPromise: Promise; // remember so we can spy on promise calls + + getHero(id: number | string) { + if (typeof id === 'string') { + id = parseInt(id as string, 10); + } + let hero = this.heroes.find(h => h.id === id); + return this.lastPromise = Promise.resolve(hero); + } + + getHeroes() { + return this.lastPromise = Promise.resolve(this.heroes); + } + + updateHero(hero: Hero): Promise { + return this.lastPromise = this.getHero(hero.id).then(h => { + return h ? + Object.assign(h, hero) : + Promise.reject(`Hero ${hero.id} not found`) as any as Promise; + }); + } +} diff --git a/public/docs/_examples/testing/ts/app/model/testing/index.ts b/public/docs/_examples/testing/ts/app/model/testing/index.ts new file mode 100644 index 0000000000..6da76e67db --- /dev/null +++ b/public/docs/_examples/testing/ts/app/model/testing/index.ts @@ -0,0 +1 @@ +export * from './fake-hero.service'; diff --git a/public/docs/_examples/testing/ts/app/model/user.service.ts b/public/docs/_examples/testing/ts/app/model/user.service.ts new file mode 100644 index 0000000000..280efefeec --- /dev/null +++ b/public/docs/_examples/testing/ts/app/model/user.service.ts @@ -0,0 +1,7 @@ +import { Injectable } from '@angular/core'; + +@Injectable() +export class UserService { + isLoggedIn = true; + user = {name: 'Sam Spade'}; +} diff --git a/public/docs/_examples/testing/ts/app/my-uppercase.pipe.1.ts b/public/docs/_examples/testing/ts/app/my-uppercase.pipe.1.ts deleted file mode 100644 index 94b5bc45ce..0000000000 --- a/public/docs/_examples/testing/ts/app/my-uppercase.pipe.1.ts +++ /dev/null @@ -1,9 +0,0 @@ -// #docregion -import { Pipe, PipeTransform } from '@angular/core'; - -@Pipe({ name: 'my-uppercase' }) -export class MyUppercasePipe implements PipeTransform { - transform(value: string) { - return value; - } -} 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 deleted file mode 100644 index 731b2ed965..0000000000 --- a/public/docs/_examples/testing/ts/app/my-uppercase.pipe.spec.ts +++ /dev/null @@ -1,41 +0,0 @@ -// #docregion -// #docplaster -// #docregion base-pipe-spec -import { MyUppercasePipe } from './my-uppercase.pipe'; - -describe('MyUppercasePipe', () => { - let pipe: MyUppercasePipe; - - beforeEach(() => { - pipe = new MyUppercasePipe(); - }); - - // #docregion expectations - it('transforms "abc" to "ABC"', () => { - expect(pipe.transform('abc')).toEqual('ABC'); - }); - - it('transforms "abc def" to "ABC DEF"', () => { - expect(pipe.transform('abc def')).toEqual('ABC DEF'); - }); - - it('leaves "ABC DEF" unchanged', () => { - expect(pipe.transform('ABC DEF')).toEqual('ABC DEF'); - }); - // #enddocregion expectations - // #enddocregion base-pipe-spec - - /* more tests we could run - - it('transforms "abc-def" to "Abc-def"', () => { - expect(pipe.transform('abc-def')).toEqual('Abc-def'); - }); - - it('transforms " abc def" to " Abc Def" (preserves spaces) ', () => { - expect(pipe.transform(' abc def')).toEqual(' Abc Def'); - }); - - */ - // #docregion base-pipe-spec -}); -// #enddocregion base-pipe-spec diff --git a/public/docs/_examples/testing/ts/app/my-uppercase.pipe.ts b/public/docs/_examples/testing/ts/app/my-uppercase.pipe.ts deleted file mode 100644 index 6584f92ef6..0000000000 --- a/public/docs/_examples/testing/ts/app/my-uppercase.pipe.ts +++ /dev/null @@ -1,13 +0,0 @@ -// #docregion -// #docregion depends-on-angular -import { Pipe, PipeTransform } from '@angular/core'; -// #enddocregion depends-on-angular - -@Pipe({ name: 'my-uppercase' }) -export class MyUppercasePipe implements PipeTransform { - // #docregion uppercase - transform(value: string) { - return value.toUpperCase(); - } - // #enddocregion uppercase -} diff --git a/public/docs/_examples/testing/ts/app/old-specs/hero-detail.component.spec.ts.not-yet b/public/docs/_examples/testing/ts/app/old-specs/hero-detail.component.spec.ts.not-yet deleted file mode 100644 index 80c210be5d..0000000000 --- a/public/docs/_examples/testing/ts/app/old-specs/hero-detail.component.spec.ts.not-yet +++ /dev/null @@ -1,218 +0,0 @@ -///// Boiler Plate //// -import {bind, By, Component, Directive, EventEmitter, FORM_DIRECTIVES} from 'angular2/angular2'; - -// Angular 2 Test Bed -import { -beforeEachProviders, inject, injectAsync, RootTestComponent as RTC, -beforeEach, ddescribe, xdescribe, describe, expect, iit, it, xit // Jasmine wrappers -} from 'angular2/testing'; - -import {dispatchEvent, DoneFn, injectTcb, tick} from '../test-helpers/test-helpers'; - -///// Testing this component //// -import {HeroDetailComponent} from './hero-detail.component'; -import {Hero} from './hero'; - -describe('HeroDetailComponent', () => { - - /////////// Component Tests without DOM interaction ///////////// - describe('(No DOM)', () => { - it('can be created', () => { - let hdc = new HeroDetailComponent(); - expect(hdc instanceof HeroDetailComponent).toEqual(true); // proof of life - }); - - it('onDelete method should raise delete event', (done: DoneFn) => { - let hdc = new HeroDetailComponent(); - - // Listen for the HeroComponent.delete EventEmitter's event - hdc.delete.toRx().subscribe(() => { - console.log('HeroComponent.delete event raised'); - done(); // it must have worked - }, (error: any) => { fail(error); done() }); - - hdc.onDelete(); - }); - - // Disable until toPromise() works again - xit('onDelete method should raise delete event (w/ promise)', (done: DoneFn) => { - - let hdc = new HeroDetailComponent(); - - // Listen for the HeroComponent.delete EventEmitter's event - let p = hdc.delete.toRx() - .toPromise() - .then(() => { - console.log('HeroComponent.delete event raised in promise'); - }) - .then(done, done.fail); - - hdc.delete.toRx() - .subscribe(() => { - console.log('HeroComponent.delete event raised in subscription') - }); - - hdc.onDelete(); - - // toPromise() does not fulfill until emitter is completed by `return()` - hdc.delete.return(); - }); - - it('onUpdate method should modify hero', () => { - let hdc = new HeroDetailComponent(); - hdc.hero = new Hero(42, 'Cat Woman'); - let origNameLength = hdc.hero.name.length; - - hdc.onUpdate(); - expect(hdc.hero.name.length).toBeGreaterThan(origNameLength); - }); - }); - - - /////////// Component tests that check the DOM ///////////// - describe('(DOM)', () => { - // Disable until toPromise() works again - xit('Delete button should raise delete event', injectTcb(tcb => { - - // We only care about the button - let template = ''; - - return tcb - .overrideTemplate(HeroDetailComponent, template) - .createAsync(HeroDetailComponent) - .then((rootTC: RTC) => { - let hdc: HeroDetailComponent = rootTC.debugElement.componentInstance; - - // // USE PROMISE WRAPPING AN OBSERVABLE UNTIL can get `toPromise` working again - // let p = new Promise((resolve) => { - // // Listen for the HeroComponent.delete EventEmitter's event with observable - // hdc.delete.toRx().subscribe((hero: Hero) => { - // console.log('Observable heard HeroComponent.delete event raised'); - // resolve(hero); - // }); - // }) - - //Listen for the HeroComponent.delete EventEmitter's event with promise - let p = > hdc.delete.toRx().toPromise() - .then((hero:Hero) => { - console.log('Promise heard HeroComponent.delete event raised'); - }); - - // trigger the 'click' event on the HeroDetailComponent delete button - let el = rootTC.debugElement.query(By.css('button')); - el.triggerEventHandler('click', null); - - // toPromise() does not fulfill until emitter is completed by `return()` - hdc.delete.return(); - - return p; - }); - - })); - - it('Update button should modify hero', injectTcb(tcb => { - - let template = - `
- - -
` - - return tcb - .overrideTemplate(HeroDetailComponent, template) - .createAsync(HeroDetailComponent) - .then((rootTC: RTC) => { - - let hdc: HeroDetailComponent = rootTC.debugElement.componentInstance; - hdc.hero = new Hero(42, 'Cat Woman'); - let origNameLength = hdc.hero.name.length; - - // trigger the 'click' event on the HeroDetailComponent update button - rootTC.debugElement.query(By.css('#update')) - .triggerEventHandler('click', null); - - expect(hdc.hero.name.length).toBeGreaterThan(origNameLength); - }); - })); - - it('Entering hero name in textbox changes hero', injectTcb(tcb => { - - let hdc: HeroDetailComponent - let template = `` - - return tcb - .overrideTemplate(HeroDetailComponent, template) - .createAsync(HeroDetailComponent) - .then((rootTC: RTC) => { - - hdc = rootTC.debugElement.componentInstance; - - hdc.hero = new Hero(42, 'Cat Woman'); - rootTC.detectChanges(); - - // get the HTML element and change its value in the DOM - var input = rootTC.debugElement.query(By.css('input')).nativeElement; - input.value = "Dog Man" - dispatchEvent(input, 'change'); // event triggers Ng to update model - - rootTC.detectChanges(); - // model update hasn't happened yet, despite `detectChanges` - expect(hdc.hero.name).toEqual('Cat Woman'); - - }) - .then(tick) // must wait a tick for the model update - .then(() => { - expect(hdc.hero.name).toEqual('Dog Man'); - }); - })); - - // Simulates ... - // 1. change a hero - // 2. select a different hero - // 3 re-select the first hero - // 4. confirm that the change is preserved in HTML - // Reveals 2-way binding bug in alpha-36, fixed in pull #3715 for alpha-37 - - it('toggling heroes after modifying name preserves the change on screen', injectTcb(tcb => { - - let hdc: HeroDetailComponent; - let hero1 = new Hero(1, 'Cat Woman'); - let hero2 = new Hero(2, 'Goat Boy'); - let input: HTMLInputElement; - let rootTC: RTC; - let template = `{{hero.id}} - ` - - return tcb - .overrideTemplate(HeroDetailComponent, template) - .createAsync(HeroDetailComponent) - .then((rtc: RTC) => { - rootTC = rtc; - hdc = rootTC.debugElement.componentInstance; - - hdc.hero = hero1; // start with hero1 - rootTC.detectChanges(); - - // get the HTML element and change its value in the DOM - input = rootTC.debugElement.query(By.css('input')).nativeElement; - input.value = "Dog Man" - dispatchEvent(input, 'change'); // event triggers Ng to update model - }) - .then(tick) // must wait a tick for the model update - .then(() => { - expect(hdc.hero.name).toEqual('Dog Man'); - - hdc.hero = hero2 // switch to hero2 - rootTC.detectChanges(); - - hdc.hero = hero1 // switch back to hero1 - rootTC.detectChanges(); - - // model value will be the same changed value (of course) - expect(hdc.hero.name).toEqual('Dog Man'); - - // the view should reflect the same changed value - expect(input.value).toEqual('Dog Man'); - }); - })); - }); -}); diff --git a/public/docs/_examples/testing/ts/app/old-specs/hero-detail.component.wrapped-tests.spec.ts.not-yet b/public/docs/_examples/testing/ts/app/old-specs/hero-detail.component.wrapped-tests.spec.ts.not-yet deleted file mode 100644 index 319ac93ebf..0000000000 --- a/public/docs/_examples/testing/ts/app/old-specs/hero-detail.component.wrapped-tests.spec.ts.not-yet +++ /dev/null @@ -1,144 +0,0 @@ -///// Boiler Plate //// -import {bind, Component, Directive, EventEmitter, FORM_DIRECTIVES, View} from 'angular2/angular2'; - -// Angular 2 Test Bed -import { - beforeEachProviders, By, DebugElement, RootTestComponent as RTC, - beforeEach, ddescribe, xdescribe, describe, expect, iit, it, xit // Jasmine wrappers -} from 'angular2/testing'; - -import {injectAsync, injectTcb} from '../test-helpers/test-helpers'; - -///// Testing this component //// -import {HeroDetailComponent} from './hero-detail.component'; -import {Hero} from './hero'; - -describe('HeroDetailComponent', () => { - - it('can be created', () => { - let hc = new HeroDetailComponent() - expect(hc instanceof HeroDetailComponent).toEqual(true); // proof of life - }); - - it('parent "currentHero" flows down to HeroDetailComponent', injectTcb( tcb => { - return tcb - .createAsync(TestWrapper) - .then((rootTC:RTC) => { - let hc:HeroDetailComponent = rootTC.componentViewChildren[0].componentInstance; - let hw:TestWrapper = rootTC.componentInstance; - - rootTC.detectChanges(); // trigger view binding - - expect(hw.currentHero).toBe(hc.hero); - }); - })); - - it('delete button should raise delete event for parent component', injectTcb( tcb => { - - return tcb - //.overrideTemplate(HeroDetailComponent, '') - .overrideDirective(TestWrapper, HeroDetailComponent, mockHDC) - .createAsync(TestWrapper) - .then((rootTC:RTC) => { - - let hw:TestWrapper = rootTC.componentInstance; - let hdcElement = rootTC.componentViewChildren[0]; - let hdc:HeroDetailComponent = hdcElement.componentInstance; - - rootTC.detectChanges(); // trigger view binding - - // We can watch the HeroComponent.delete EventEmitter's event - let subscription = hdc.delete.toRx().subscribe(() => { - console.log('HeroComponent.delete event raised'); - subscription.dispose(); - }); - - // We can EITHER invoke HeroComponent delete button handler OR - // trigger the 'click' event on the delete HeroComponent button - // BUT DON'T DO BOTH - - // Trigger event - // FRAGILE because assumes precise knowledge of HeroComponent template - hdcElement - .query(By.css('#delete')) - .triggerEventHandler('click', {}); - - hw.testCallback = () => { - // if wrapper.onDelete is called, HeroComponent.delete event must have been raised - //console.log('HeroWrapper.onDelete called'); - expect(true).toEqual(true); - } - // hc.onDelete(); - }); - }), 500); // needs some time for event to complete; 100ms is not long enough - - it('update button should modify hero', injectTcb( tcb => { - - return tcb - .createAsync(TestWrapper) - .then((rootTC:RTC) => { - - let hc:HeroDetailComponent = rootTC.componentViewChildren[0].componentInstance; - let hw:TestWrapper = rootTC.componentInstance; - let origNameLength = hw.currentHero.name.length; - - rootTC.detectChanges(); // trigger view binding - - // We can EITHER invoke HeroComponent update button handler OR - // trigger the 'click' event on the HeroComponent update button - // BUT DON'T DO BOTH - - // Trigger event - // FRAGILE because assumes precise knowledge of HeroComponent template - rootTC.componentViewChildren[0] - .componentViewChildren[2] - .triggerEventHandler('click', {}); - - // hc.onUpdate(); // Invoke button handler - expect(hw.currentHero.name.length).toBeGreaterThan(origNameLength); - }); - })); - -}); - -///// Test Components //////// - -// TestWrapper is a convenient way to communicate w/ HeroDetailComponent in a test -@Component({selector: 'hero-wrapper'}) -@View({ - template: ``, - directives: [HeroDetailComponent] -}) -class TestWrapper { - currentHero = new Hero(42, 'Cat Woman'); - userName = 'Sally'; - testCallback() {} // monkey-punched in a test - onDelete() { this.testCallback(); } -} - -@View({ - template: ` -
-

{{hero.name}} | {{userName}}

- - -
{{hero.id}}
- -
`, - directives: [FORM_DIRECTIVES] -}) -class mockHDC //extends HeroDetailComponent { } -{ - hero: Hero; - - delete = new EventEmitter(); - - onDelete() { this.delete.next(this.hero) } - - onUpdate() { - if (this.hero) { - this.hero.name += 'x'; - } - } - userName: string; -} \ No newline at end of file diff --git a/public/docs/_examples/testing/ts/app/old-specs/hero.service.ng.spec.ts.not-yet b/public/docs/_examples/testing/ts/app/old-specs/hero.service.ng.spec.ts.not-yet deleted file mode 100644 index a3e4c0c6a4..0000000000 --- a/public/docs/_examples/testing/ts/app/old-specs/hero.service.ng.spec.ts.not-yet +++ /dev/null @@ -1,198 +0,0 @@ -// Test a service when Angular DI is in play - -// Angular 2 Test Bed -import { - beforeEach, xdescribe, describe, it, xit, // Jasmine wrappers - beforeEachProviders, inject, injectAsync, -} from 'angular2/testing'; - -import {bind} from 'angular2/core'; - -// Service related imports -import {HeroService} from './hero.service'; -import {BackendService} from './backend.service'; -import {Hero} from './hero'; - -////// tests //////////// - -describe('HeroService (with angular DI)', () => { - - beforeEachProviders(() => [HeroService]); - - describe('creation', () => { - - beforeEachProviders( () => [bind(BackendService).toValue(null)] ); - - it('can instantiate the service', - inject([HeroService], (service: HeroService) => { - expect(service).toBeDefined(); - })); - - it('service.heroes is empty', - inject([HeroService], (service: HeroService) => { - expect(service.heroes.length).toEqual(0); - })); - }); - - describe('#refresh', () => { - - describe('when backend provides data', () => { - - beforeEach(() => { - heroData = [new Hero(1, 'Foo'), new Hero(2, 'Bar'), new Hero(3,'Baz')]; - }); - - beforeEachProviders(() => - [bind(BackendService).toClass(HappyBackendService)] - ); - - it('refresh promise returns expected # of heroes when fulfilled', - injectAsync([HeroService], (service: HeroService) => { - - return service.refresh().then(heroes => - expect(heroes.length).toEqual(heroData.length) - ); - })); - - it('service.heroes has expected # of heroes when fulfilled', - injectAsync([HeroService], (service: HeroService) => { - - return service.refresh().then(() => - expect(service.heroes.length).toEqual(heroData.length) - ); - })); - - it('service.heroes remains empty until fulfilled', - inject([HeroService], (service: HeroService) => { - - service.refresh(); - - // executed before refresh completes - expect(service.heroes.length).toEqual(0); - })); - - it('service.heroes remains empty when the server returns no data', - injectAsync([HeroService], (service: HeroService) => { - - heroData = []; // simulate no heroes from the backend - - return service.refresh().then(() => - expect(service.heroes.length).toEqual(0) - ); - })); - - it('resets service.heroes w/ original data after re-refresh', - injectAsync([HeroService], (service: HeroService) => { - - let firstHeroes: Hero[]; - let changedName = 'Gerry Mander'; - - return service.refresh().then(heroes => { - firstHeroes = heroes; // remember array reference - - // Changes to cache! Should disappear after refresh - service.heroes[0].name = changedName; - service.heroes.push(new Hero(33, 'Hercules')); - return service.refresh() - }) - .then(() => { - expect(firstHeroes).toBe(service.heroes); // same object - expect(service.heroes.length).toEqual(heroData.length); // no Hercules - expect(service.heroes[0].name).not.toEqual(changedName); // reverted name change - }); - })); - - it('clears service.heroes while waiting for re-refresh', - injectAsync([HeroService], (service: HeroService) => { - - return service.refresh().then(() => { - service.refresh(); - expect(service.heroes.length).toEqual(0); - }); - })); - // the paranoid will verify not only that the array lengths are the same - // but also that the contents are the same. - it('service.heroes has expected heroes when fulfilled (paranoia)', - injectAsync([HeroService], (service: HeroService) => { - - return service.refresh().then(() => { - expect(service.heroes.length).toEqual(heroData.length); - service.heroes.forEach(h => - expect(heroData.some( - // hero instances are not the same objects but - // each hero in result matches an original hero by value - hd => hd.name === h.name && hd.id === h.id) - ) - ); - }); - })); - - }); - - describe('when backend throws an error', () => { - - beforeEachProviders(() => - [bind(BackendService).toClass(FailingBackendService)] - ); - - it('returns failed promise with the server error', - injectAsync([HeroService], (service: HeroService) => { - - return service.refresh() - .then(() => fail('refresh should have failed')) - .catch(err => expect(err).toBe(testError)); - })); - - it('resets heroes array to empty', - injectAsync([HeroService], (service: HeroService) => { - - return service.refresh() - .then(() => fail('refresh should have failed')) - .catch(err => expect(service.heroes.length).toEqual(0)) - })); - }); - - describe('when backend throws an error (spy version)', () => { - - beforeEachProviders(() => [BackendService]); - - beforeEach(inject([BackendService], (backend: BackendService) => - spyOn(backend, 'fetchAllHeroesAsync').and.callFake(() => Promise.reject(testError) - ))); - - it('returns failed promise with the server error', - injectAsync([HeroService], (service: HeroService) => { - - return service.refresh() - .then(() => fail('refresh should have failed')) - .catch(err => expect(err).toBe(testError)); - })); - - it('resets heroes array to empty', - injectAsync([HeroService], (service: HeroService) => { - - return service.refresh() - .then(() => fail('refresh should have failed')) - .catch(err => expect(service.heroes.length).toEqual(0)) - })); - }); - - }); -}); -///////// test helpers ///////// -var service: HeroService; -var heroData: Hero[]; - -class HappyBackendService { - // return a promise for fake heroes that resolves as quickly as possible - fetchAllHeroesAsync = () => - Promise.resolve(heroData.map(h => h.clone())); -} - -var testError = 'BackendService.fetchAllHeroesAsync failed on purpose'; - -class FailingBackendService { - // return a promise that fails as quickly as possible - fetchAllHeroesAsync = () => - Promise.reject(testError); -} diff --git a/public/docs/_examples/testing/ts/app/old-specs/hero.service.no-ng.1.spec.ts.not-yet b/public/docs/_examples/testing/ts/app/old-specs/hero.service.no-ng.1.spec.ts.not-yet deleted file mode 100644 index 89cbc597f8..0000000000 --- a/public/docs/_examples/testing/ts/app/old-specs/hero.service.no-ng.1.spec.ts.not-yet +++ /dev/null @@ -1,250 +0,0 @@ -/* - * Dev Guide steps to hero.service.no-ng.spec - * Try it with unit-tests-4.html - */ - -// The phase of hero-service-spec -// when we're outlining what we want to test -describe('HeroService (test plan)', () => { - - describe('creation', () => { - xit('can instantiate the service'); - xit('service.heroes is empty'); - }); - - describe('#refresh', () => { - - describe('when server provides heroes', () => { - xit('refresh promise returns expected # of heroes when fulfilled'); - xit('service.heroes has expected # of heroes when fulfilled'); - xit('service.heroes remains empty until fulfilled'); - xit('service.heroes remains empty when the server returns no data'); - xit('resets service.heroes w/ original data after re-refresh'); - xit('clears service.heroes while waiting for re-refresh'); - }); - - describe('when the server fails', () => { - xit('returns failed promise with the server error'); - xit('clears service.heroes'); - }); - - }); - -}); - -import {HeroService} from './hero.service'; - -describe('HeroService (beginning tests - 1)', () => { - - describe('creation', () => { - it('can instantiate the service', () => { - let service = new HeroService(null); - expect(service).toBeDefined(); - }); - - it('heroes is empty', () => { - let service = new HeroService(null); - expect(service.heroes.length).toEqual(0); - }); - - }); - -}); - -import {BackendService} from './backend.service'; -import {Hero} from './hero'; - -xdescribe('HeroService (beginning tests - 2 [dont run])', () => { - let heroData:Hero[]; - - // No good! - it('refresh promise returns expected # of heroes when fulfilled', () => { - let service = new HeroService(null); - service.refresh().then(heroes => { - expect(heroes.length).toBeGreaterThan(0); // don’t know how many to expect yet - }); - }); - - // better ... but not async! - it('refresh promise returns expected # of heroes when fulfilled', () => { - - heroData = [new Hero(1, 'Foo'), new Hero(2, 'Bar'), new Hero(3, 'Baz')]; - - let backend = { - // return a promise for fake heroes that resolves as quickly as possible - fetchAllHeroesAsync: () => Promise.resolve(heroData) - }; - - let service = new HeroService(backend); - service.refresh().then(heroes => { - expect(heroes.length).toEqual(heroData.length); // is it? - expect(heroes.length).not.toEqual(heroData.length); // or is it not? - console.log('** inside callback **'); - }); - - console.log('** end of test **'); - }); - - // better ... but forgot to call done! - it('refresh promise returns expected # of heroes when fulfilled', done => { - - heroData = [new Hero(1, 'Foo'), new Hero(2, 'Bar'), new Hero(3, 'Baz')]; - - let backend = { - // return a promise for fake heroes that resolves as quickly as possible - fetchAllHeroesAsync: () => Promise.resolve(heroData) - }; - - let service = new HeroService(backend); - service.refresh().then(heroes => { - expect(heroes.length).toEqual(heroData.length); // is it? - expect(heroes.length).not.toEqual(heroData.length); // or is it not? - console.log('** inside callback **'); - }); - - console.log('** end of test **'); - }); -}); - -describe('HeroService (beginning tests - 3 [async])', () => { - - let heroData:Hero[]; - // Now it's proper async! - it('refresh promise returns expected # of heroes when fulfilled', done => { - - heroData = [new Hero(1, 'Foo'), new Hero(2, 'Bar'), new Hero(3, 'Baz')]; - - let backend = { - // return a promise for fake heroes that resolves as quickly as possible - fetchAllHeroesAsync: () => Promise.resolve(heroData) - }; - - let service = new HeroService(backend); - service.refresh().then(heroes => { - expect(heroes.length).toEqual(heroData.length); // is it? - //expect(heroes.length).not.toEqual(heroData.length); // or is it not? - console.log('** inside callback **'); - done(); - }); - - console.log('** end of test **'); - }); - - // Final before catch - it('refresh promise returns expected # of heroes when fulfilled', done => { - - heroData = [new Hero(1, 'Foo'), new Hero(2, 'Bar'), new Hero(3, 'Baz')]; - - let backend = { - // return a promise for fake heroes that resolves as quickly as possible - fetchAllHeroesAsync: () => Promise.resolve(heroData) - }; - - let service = new HeroService(backend); - service.refresh().then(heroes => { - expect(heroes.length).toEqual(heroData.length); - }) - .then(done); - }); - - // Final before beforeEach refactoring - it('refresh promise returns expected # of heroes when fulfilled', done => { - - heroData = [new Hero(1, 'Foo'), new Hero(2, 'Bar'), new Hero(3, 'Baz')]; - - let backend = { - // return a promise for fake heroes that resolves as quickly as possible - fetchAllHeroesAsync: () => Promise.resolve(heroData) - }; - - let service = new HeroService(backend); - service.refresh().then(heroes => { - expect(heroes.length).toEqual(heroData.length); - }) - .then(done, done.fail); - }); - - it('service.heroes remains empty until fulfilled', () => { - heroData = [new Hero(1, 'Foo'), new Hero(2, 'Bar'), new Hero(3, 'Baz')]; - - let backend = { - // return a promise for fake heroes that resolves as quickly as possible - fetchAllHeroesAsync: () => Promise.resolve(heroData) - }; - - let service = new HeroService(backend); - service.refresh(); - - // executed before refresh completes - expect(service.heroes.length).toEqual(0); - }); -}); - - -describe('HeroService (beginning tests - 4 [beforeEach])', () => { - let heroData:Hero[]; - let service:HeroService; // local to describe so tests can see it - - // before beforEach refactoring - beforeEach(() => { - heroData = [new Hero(1, 'Foo'), new Hero(2, 'Bar'), new Hero(3,'Baz')]; - - let backend = { - // return a promise for fake heroes that resolves as quickly as possible - fetchAllHeroesAsync: () => Promise.resolve(heroData) - }; - - service = new HeroService(backend); - }); - - it('refresh promise returns expected # of heroes when fulfilled', done => { - service.refresh().then(heroes => - expect(heroes.length).toEqual(heroData.length) - ) - .then(done, done.fail); - }); - - it('service.heroes remains empty until fulfilled', () => { - service.refresh(); - - // executed before refresh completes - expect(service.heroes.length).toEqual(0); - }); - -}); - -describe('HeroService (beginning tests - 5 [refactored beforeEach])', () => { - - describe('when backend provides data', () => { - beforeEach(() => { - heroData = [new Hero(1, 'Foo'), new Hero(2, 'Bar'), new Hero(3,'Baz')]; - service = new HeroService(new HappyBackendService()); - }); - - it('refresh promise returns expected # of heroes when fulfilled', done => { - service.refresh().then(() => - expect(service.heroes.length).toEqual(heroData.length) - ) - .then(done, done.fail); - }); - - it('service.heroes remains empty until fulfilled', () => { - service.refresh(); - - // executed before refresh completes - expect(service.heroes.length).toEqual(0); - }); - }); - -}); - - -///////// test helpers ///////// -var service: HeroService; -var heroData: Hero[]; - -class HappyBackendService { - // return a promise for fake heroes that resolves as quickly as possible - fetchAllHeroesAsync = () => - Promise.resolve(heroData.map(h => h.clone())); -} diff --git a/public/docs/_examples/testing/ts/app/old-specs/hero.service.no-ng.spec.ts.not-yet b/public/docs/_examples/testing/ts/app/old-specs/hero.service.no-ng.spec.ts.not-yet deleted file mode 100644 index c2deb56c7c..0000000000 --- a/public/docs/_examples/testing/ts/app/old-specs/hero.service.no-ng.spec.ts.not-yet +++ /dev/null @@ -1,150 +0,0 @@ -// Test a service without referencing Angular (no Angular DI) -import {HeroService} from './hero.service'; -import {BackendService} from './backend.service'; -import {Hero} from './hero'; - -////// tests //////////// - -describe('HeroService (no-angular)', () => { - - describe('creation', () => { - it('can instantiate the service', () => { - let service = new HeroService(null); - expect(service).toBeDefined(); - }); - - it('service.heroes is empty', () => { - let service = new HeroService(null); - expect(service.heroes.length).toEqual(0); - }); - }); - - describe('#refresh', () => { - - describe('when backend provides data', () => { - - beforeEach(() => { - heroData = [new Hero(1, 'Foo'), new Hero(2, 'Bar'), new Hero(3, 'Baz')]; - service = new HeroService(new HappyBackendService()); - }); - - - it('refresh promise returns expected # of heroes when fulfilled', done => { - service.refresh().then(heroes => - expect(heroes.length).toEqual(heroData.length) - ) - .then(done, done.fail); - }); - - it('service.heroes has expected # of heroes when fulfilled', done => { - service.refresh().then(() => - expect(service.heroes.length).toEqual(heroData.length) - ) - .then(done, done.fail); - }); - - it('service.heroes remains empty until fulfilled', () => { - service.refresh(); - - // executed before refresh completes - expect(service.heroes.length).toEqual(0); - }); - - it('service.heroes remains empty when the server returns no data', done => { - heroData = []; // simulate no heroes from the backend - - service.refresh().then(() => - expect(service.heroes.length).toEqual(0) - ) - .then(done, done.fail); - }); - - it('resets service.heroes w/ original data after re-refresh', done => { - let firstHeroes: Hero[]; - let changedName = 'Gerry Mander'; - - service.refresh().then(() => { - firstHeroes = service.heroes; // remember array reference - - // Changes to cache! Should disappear after refresh - service.heroes[0].name = changedName; - service.heroes.push(new Hero(33, 'Hercules')); - return service.refresh() - }) - .then(() => { - expect(firstHeroes).toBe(service.heroes); // same array - expect(service.heroes.length).toEqual(heroData.length); // no Hercules - expect(service.heroes[0].name).not.toEqual(changedName); // reverted name change - }) - .then(done, done.fail); - }); - - it('clears service.heroes while waiting for re-refresh', done => { - service.refresh().then(() => { - service.refresh(); - expect(service.heroes.length).toEqual(0); - }) - .then(done, done.fail); - }); - - // the paranoid will verify not only that the array lengths are the same - // but also that the contents are the same. - it('service.heroes has expected heroes when fulfilled (paranoia)', done => { - service.refresh().then(() => { - expect(service.heroes.length).toEqual(heroData.length); - service.heroes.forEach(h => - expect(heroData.some( - // hero instances are not the same objects but - // each hero in result matches an original hero by value - hd => hd.name === h.name && hd.id === h.id) - ) - ); - }) - .then(done, done.fail); - }); - - }); - - describe('when backend throws an error', () => { - - beforeEach(() => { - service = new HeroService(new FailingBackendService()); - }); - - it('returns failed promise with the server error', done => { - service.refresh() - .then(() => fail('refresh should have failed')) - .catch(err => expect(err).toEqual(testError)) - .then(done, done.fail); - }); - - it('clears service.heroes', done => { - service.refresh() - .then(() => fail('refresh should have failed')) - .catch(err => expect(service.heroes.length).toEqual(0)) - .then(done, done.fail); - }); - - }); - }); -}); - -///////// test helpers ///////// - -var service: HeroService; -var heroData: Hero[]; - -class HappyBackendService { - // return a promise for fake heroes that resolves as quickly as possible - fetchAllHeroesAsync = () => - Promise.resolve(heroData.map(h => h.clone())); -} - -var testError = 'BackendService.fetchAllHeroesAsync failed on purpose'; - -class FailingBackendService { - // return a promise that fails as quickly as possible - // force-cast it to because of TS typing bug. - fetchAllHeroesAsync = () => - >Promise.reject(testError); -} diff --git a/public/docs/_examples/testing/ts/app/old-specs/heroes.component.ng.spec.ts.not-yet b/public/docs/_examples/testing/ts/app/old-specs/heroes.component.ng.spec.ts.not-yet deleted file mode 100644 index 489745ca2b..0000000000 --- a/public/docs/_examples/testing/ts/app/old-specs/heroes.component.ng.spec.ts.not-yet +++ /dev/null @@ -1,276 +0,0 @@ -///// Angular 2 Test Bed //// -import {bind, By} from 'angular2/angular2'; - -import { - beforeEach, xdescribe, describe, it, xit, // Jasmine wrappers - beforeEachProviders, - injectAsync, - RootTestComponent as RTC, - TestComponentBuilder as TCB -} from 'angular2/testing'; - -import { - expectSelectedHtml, - expectViewChildHtml, - expectViewChildClass, - injectTcb, tick} from '../test-helpers/test-helpers'; - -///// Testing this component //// -import {HeroesComponent} from './heroes.component'; -import {Hero} from './hero'; -import {HeroService} from './hero.service'; -import {User} from './user'; - -let hc: HeroesComponent; -let heroData: Hero[]; // fresh heroes for each test -let mockUser: User; -let service: HeroService; - -// get the promise from the refresh spy; -// casting required because of inadequate d.ts for Jasmine -let refreshPromise = () => (service.refresh).calls.mostRecent().returnValue; - -describe('HeroesComponent (with Angular)', () => { - - beforeEach(() => { - heroData = [new Hero(1, 'Foo'), new Hero(2, 'Bar'), new Hero(3, 'Baz')]; - mockUser = new User(); - }); - - // Set up DI bindings required by component (and its nested components?) - // else hangs silently forever - beforeEachProviders(() => [ - bind(HeroService).toClass(HappyHeroService), - bind(User).toValue(mockUser) - ]); - - // test-lib bug? first test fails unless this no-op test runs first - it('ignore this test', () => expect(true).toEqual(true)); // hack - - it('can be created and has userName', injectTcb((tcb:TCB) => { - let template = ''; - return tcb - .overrideTemplate(HeroesComponent, template) - .createAsync(HeroesComponent) - .then((rootTC: RTC) => { - hc = rootTC.debugElement.componentInstance; - expect(hc).toBeDefined();// proof of life - expect(hc.userName).toEqual(mockUser.name); - }); - })); - - it('binds view to userName', injectTcb((tcb:TCB) => { - let template = `

{{userName}}'s Heroes

`; - return tcb - .overrideTemplate(HeroesComponent, template) - .createAsync(HeroesComponent) - .then((rootTC: RTC) => { - hc = rootTC.debugElement.componentInstance; - - rootTC.detectChanges(); // trigger component property binding - expectSelectedHtml(rootTC, 'h1').toMatch(hc.userName); - expectViewChildHtml(rootTC).toMatch(hc.userName); - }); - })); - - describe('#onInit', () => { - let template = ''; - - it('HeroService.refresh not called immediately', - injectTcb([HeroService], (tcb:TCB, heroService:HeroService) => { - - return tcb - .overrideTemplate(HeroesComponent, template) - .createAsync(HeroesComponent) - .then(() => { - let spy = heroService.refresh; - expect(spy.calls.count()).toEqual(0); - }); - })); - - it('onInit calls HeroService.refresh', - injectTcb([HeroService], (tcb:TCB, heroService:HeroService) => { - - return tcb - .overrideTemplate(HeroesComponent, template) - .createAsync(HeroesComponent) - .then((rootTC: RTC) => { - hc = rootTC.debugElement.componentInstance; - let spy = heroService.refresh; - hc.ngOnInit(); // Angular framework calls when it creates the component - expect(spy.calls.count()).toEqual(1); - }); - })); - - it('onInit is called after the test calls detectChanges', injectTcb((tcb:TCB) => { - - return tcb - .overrideTemplate(HeroesComponent, template) - .createAsync(HeroesComponent) - .then((rootTC: RTC) => { - hc = rootTC.debugElement.componentInstance; - let spy = spyOn(hc, 'onInit').and.callThrough(); - - expect(spy.calls.count()).toEqual(0); - rootTC.detectChanges(); - expect(spy.calls.count()).toEqual(1); - }); - })); - }) - - describe('#heroes', () => { - // focus on the part of the template that displays heroe names - let template = - '
  • {{h.name}}
'; - - it('binds view to heroes', injectTcb((tcb:TCB) => { - return tcb - .overrideTemplate(HeroesComponent, template) - .createAsync(HeroesComponent) - .then((rootTC: RTC) => { - // trigger {{heroes}} binding - rootTC.detectChanges(); - - // hc.heroes is still empty; need a JS cycle to get the data - return rootTC; - }) - .then((rootTC: RTC) => { - hc = rootTC.debugElement.componentInstance; - // now heroes are available for binding - expect(hc.heroes.length).toEqual(heroData.length); - - rootTC.detectChanges(); // trigger component property binding - - // confirm hero list is displayed by looking for a known hero - expect(rootTC.debugElement.nativeElement.innerHTML).toMatch(heroData[0].name); - }); - })); - - // ... add more tests of component behavior affecting the heroes list - - }); - - describe('#onSelected', () => { - - it('no hero is selected by default', injectHC(hc => { - expect(hc.currentHero).not.toBeDefined(); - })); - - it('sets the "currentHero"', injectHC(hc => { - hc.onSelect(heroData[1]); // select the second hero - expect(hc.currentHero).toEqual(heroData[1]); - })); - - it('no hero is selected after onRefresh() called', injectHC(hc => { - hc.onSelect(heroData[1]); // select the second hero - hc.onRefresh(); - expect(hc.currentHero).not.toBeDefined(); - })); - - // TODO: Remove `withNgClass=true` ONCE BUG IS FIXED - xit('the view of the "currentHero" has the "selected" class (NG2 BUG)', injectHC((hc, rootTC) => { - hc.onSelect(heroData[1]); // select the second hero - - rootTC.detectChanges(); - - // The 3rd ViewChild is 2nd hero; the 1st is for the template - expectViewChildClass(rootTC, 2).toMatch('selected'); - }, true /* true == include ngClass */)); - - it('the view of a non-selected hero does NOT have the "selected" class', injectHC((hc, rootTC) => { - hc.onSelect(heroData[1]); // select the second hero - rootTC.detectChanges(); - // The 4th ViewChild is 3rd hero; the 1st is for the template - expectViewChildClass(rootTC, 4).not.toMatch('selected'); - })); - - }); - - // Most #onDelete tests not re-implemented because - // writing those tests w/in Angular adds little value and - // is far more painful than writing them to run outside Angular - // Only bother with the one test that checks the DOM - describe('#onDeleted', () => { - let template = - '
  • {{h.name}}
'; - - it('the list view does not contain the "deleted" currentHero', injectTcb((tcb:TCB) => { - return tcb - .overrideTemplate(HeroesComponent, template) - .createAsync(HeroesComponent) - .then((rootTC: RTC) => { - hc = rootTC.debugElement.componentInstance; - // trigger {{heroes}} binding - rootTC.detectChanges(); - return rootTC; // wait for heroes to arrive - }) - .then((rootTC: RTC) => { - hc.currentHero = heroData[1]; - hc.onDelete() - rootTC.detectChanges(); // trigger component property binding - - // confirm hero list is not displayed by looking for removed hero - expect(rootTC.debugElement.nativeElement.innerHTML).not.toMatch(heroData[1].name); - }); - })); - }); -}); - -////// Helpers ////// - -class HappyHeroService { - - constructor() { - spyOn(this, 'refresh').and.callThrough(); - } - - heroes: Hero[]; - - refresh() { - this.heroes = []; - // updates cached heroes after one JavaScript cycle - return new Promise((resolve, reject) => { - this.heroes.push(...heroData); - resolve(this.heroes); - }); - } -} - - -// The same setup for every test in the #onSelected suite -// TODO: Remove `withNgClass` and always include in template ONCE BUG IS FIXED -function injectHC(testFn: (hc: HeroesComponent, rootTC?: RTC) => void, withNgClass:boolean = false) { - - // This is the bad boy: [ngClass]="getSelectedClass(hero)" - let ngClass = withNgClass ? '[ngClass]="getSelectedClass(hero)"' : ''; - - // focus on the part of the template that displays heroes - let template = - `
  • - ({{hero.id}}) {{hero.name}} -
`; - - return injectTcb((tcb:TCB) => { - let hc: HeroesComponent; - - return tcb - .overrideTemplate(HeroesComponent, template) - .createAsync(HeroesComponent) - .then((rootTC:RTC) => { - hc = rootTC.debugElement.componentInstance; - rootTC.detectChanges();// trigger {{heroes}} binding - return rootTC; - }) - .then((rootTC:RTC) => { // wait a tick until heroes are fetched -console.error("WAS THIS FIXED??"); - // CRASHING HERE IF TEMPLATE HAS '[ngClass]="getSelectedClass(hero)"' - // WITH EXCEPTION: - // "Expression 'getSelectedClass(hero) in null' has changed after it was checked." - - rootTC.detectChanges(); // show the list - testFn(hc, rootTC); - }); - }) -} diff --git a/public/docs/_examples/testing/ts/app/old-specs/heroes.component.no-ng.spec.ts.not-yet b/public/docs/_examples/testing/ts/app/old-specs/heroes.component.no-ng.spec.ts.not-yet deleted file mode 100644 index b1c1f2cff8..0000000000 --- a/public/docs/_examples/testing/ts/app/old-specs/heroes.component.no-ng.spec.ts.not-yet +++ /dev/null @@ -1,229 +0,0 @@ -import {HeroesComponent} from './heroes.component'; -import {Hero} from './hero'; -import {HeroService} from './hero.service'; -import {User} from './user'; - -describe('HeroesComponent (Test Plan)', () => { - xit('can be created'); - xit('has expected userName'); - - describe('#onInit', () => { - xit('HeroService.refresh not called immediately'); - xit('onInit calls HeroService.refresh'); - }); - - describe('#heroes', () => { - xit('lacks heroes when created'); - xit('has heroes after cache loaded'); - xit('restores heroes after refresh called again'); - - xit('binds view to heroes'); - }); - - describe('#onSelected', () => { - xit('no hero is selected by default'); - xit('sets the "currentHero"'); - xit('no hero is selected after onRefresh() called'); - - xit('the view of the "currentHero" has the "selected" class (NG2 BUG)'); - xit('the view of a non-selected hero does NOT have the "selected" class'); - }); - - describe('#onDelete', () => { - xit('removes the supplied hero (only) from the list'); - xit('removes the currentHero from the list if no hero argument'); - xit('is harmless if no supplied or current hero'); - xit('is harmless if hero not in list'); - xit('is harmless if the list is empty'); - xit('the new currentHero is the one after the removed hero'); - xit('the new currentHero is the one before the removed hero if none after'); - - xit('the list view does not contain the "deleted" currentHero'); - }); -}); - -let hc:HeroesComponent; -let heroData: Hero[]; // fresh heroes for each test -let mockUser: User; -let service: HeroService; - -// get the promise from the refresh spy; -// casting required because of inadequate d.ts for Jasmine -let refreshPromise = () => (service.refresh).calls.mostRecent().returnValue; - -describe('HeroesComponent (no Angular)', () => { - - beforeEach(()=> { - heroData = [new Hero(1, 'Foo'), new Hero(2, 'Bar'), new Hero(3, 'Baz')]; - mockUser = new User(); - }); - - beforeEach(()=> { - service = new HappyHeroService(); - hc = new HeroesComponent(service, mockUser) - }); - - it('can be created', () => { - expect(hc instanceof HeroesComponent).toEqual(true); // proof of life - }); - - it('has expected userName', () => { - expect(hc.userName).toEqual(mockUser.name); - }); - - describe('#onInit', () => { - it('HeroService.refresh not called immediately', () => { - let spy = service.refresh; - expect(spy.calls.count()).toEqual(0); - }); - - it('onInit calls HeroService.refresh', () => { - let spy = service.refresh; - hc.ngOnInit(); // Angular framework calls when it creates the component - expect(spy.calls.count()).toEqual(1); - }); - }) - - describe('#heroes', () => { - - it('lacks heroes when created', () => { - let heroes = hc.heroes; - expect(heroes.length).toEqual(0); // not filled yet - }); - - it('has heroes after cache loaded', done => { - hc.ngOnInit(); // Angular framework calls when it creates the component - - refreshPromise().then(() => { - let heroes = hc.heroes; // now the component has heroes to show - expect(heroes.length).toEqual(heroData.length); - }) - .then(done, done.fail); - }); - - it('restores heroes after refresh called again', done => { - hc.ngOnInit(); // component initialization triggers service - let heroes: Hero[]; - - refreshPromise().then(() => { - heroes = hc.heroes; // now the component has heroes to show - heroes[0].name = 'Wotan'; - heroes.push(new Hero(33, 'Thor')); - hc.onRefresh(); - }) - .then(() => { - heroes = hc.heroes; // get it again (don't reuse old array!) - expect(heroes[0]).not.toEqual('Wotan'); // change reversed - expect(heroes.length).toEqual(heroData.length); // orig num of heroes - }) - .then(done, done.fail); - }); - }); - - describe('#onSelected', () => { - - it('no hero is selected by default', () => { - expect(hc.currentHero).not.toBeDefined(); - }); - - it('sets the "currentHero"', () => { - hc.onSelect(heroData[1]); // select the second hero - expect(hc.currentHero).toEqual(heroData[1]); - }); - - it('no hero is selected after onRefresh() called', () => { - hc.onSelect(heroData[1]); // select the second hero - hc.onRefresh(); - expect(hc.currentHero).not.toBeDefined(); - }); - }); - - - describe('#onDelete', () => { - - // Load the heroes asynchronously before each test - // Getting the async out of the way in the beforeEach - // means tests can be synchronous - // Note: could have cheated and simply plugged hc.heroes with fake data - // that trick would fail if we reimplemented hc.heroes as a readonly property - beforeEach(done => { - hc.ngOnInit(); // Angular framework calls when it creates the component - refreshPromise().then(done, done.fail); - }); - - it('removes the supplied hero (only) from the list', () => { - hc.currentHero = heroData[1]; - let hero = heroData[2]; - hc.onDelete(hero); - - expect(hc.heroes).not.toContain(hero); - expect(hc.heroes).toContain(heroData[1]); // left current in place - expect(hc.heroes.length).toEqual(heroData.length - 1); - }); - - it('removes the currentHero from the list if no hero argument', () => { - hc.currentHero = heroData[1]; - hc.onDelete(); - expect(hc.heroes).not.toContain(heroData[1]); - }); - - it('is harmless if no supplied or current hero', () => { - hc.currentHero = null; - hc.onDelete(); - expect(hc.heroes.length).toEqual(heroData.length); - }); - - it('is harmless if hero not in list', () => { - let hero = heroData[1].clone(); // object reference matters, not id - hc.onDelete(hero); - expect(hc.heroes.length).toEqual(heroData.length); - }); - - // must go async to get hc to clear its heroes list - it('is harmless if the list is empty', done => { - let hero = heroData[1]; - heroData = []; - hc.onRefresh(); - refreshPromise().then(() => { - hc.onDelete(hero); // shouldn't fail - }) - .then(done, done.fail); - }); - - it('the new currentHero is the one after the removed hero', () => { - hc.currentHero = heroData[1]; - let expectedCurrent = heroData[2]; - hc.onDelete(); - expect(hc.currentHero).toBe(expectedCurrent); - }); - - it('the new currentHero is the one before the removed hero if none after', () => { - hc.currentHero = heroData[heroData.length - 1]; // last hero - let expectedCurrent = heroData[heroData.length - 2]; // penultimate hero - hc.onDelete(); - expect(hc.currentHero).toBe(expectedCurrent); - }); - }); - -}); - - -////// Helpers ////// - -class HappyHeroService { - - constructor() { - spyOn(this, 'refresh').and.callThrough(); - } - - heroes: Hero[]; - - refresh() { - this.heroes = []; - // updates cached heroes after one JavaScript cycle - return new Promise((resolve, reject) => { - this.heroes.push(...heroData); - resolve(this.heroes); - }); - } -} diff --git a/public/docs/_examples/testing/ts/app/old-specs/user.spec.ts.not-yet b/public/docs/_examples/testing/ts/app/old-specs/user.spec.ts.not-yet deleted file mode 100644 index f65ac89a5e..0000000000 --- a/public/docs/_examples/testing/ts/app/old-specs/user.spec.ts.not-yet +++ /dev/null @@ -1,18 +0,0 @@ -import {User} from './user'; - -describe('User', () => { - let user:User; - - beforeEach(() => { - user = new User(); - }); - - it('has id === 42', () => { - expect(user.id).toEqual(42); - }); - - it('has an email address', () => { - expect(user.email.length).toBeGreaterThan(0); - }); - -}); \ No newline at end of file diff --git a/public/docs/_examples/testing/ts/app/shared/highlight.directive.spec.ts b/public/docs/_examples/testing/ts/app/shared/highlight.directive.spec.ts new file mode 100644 index 0000000000..c5f13934b9 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/shared/highlight.directive.spec.ts @@ -0,0 +1,58 @@ +import { Component, DebugElement } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; + +import { HighlightDirective } from './highlight.directive'; + +// Component to test directive +@Component({ + template: ` +

Something Yellow

+

Something Gray

+

Something White

+ ` + +}) +class TestComponent { } + +////// Tests ////////// +describe('HighlightDirective', () => { + + let fixture: ComponentFixture; + let h2Des: DebugElement[]; + + beforeEach(() => { + fixture = TestBed.configureTestingModule({ + declarations: [ HighlightDirective, TestComponent ] + }) + .createComponent(TestComponent); + + h2Des = fixture.debugElement.queryAll(By.css('h2')); + }); + + it('should have `HighlightDirective`', () => { + // The HighlightDirective listed in

tokens means it is attached + expect(h2Des[0].providerTokens).toContain(HighlightDirective, 'HighlightDirective'); + }); + + it('should color first

background "yellow"', () => { + fixture.detectChanges(); + const h2 = h2Des[0].nativeElement as HTMLElement; + expect(h2.style.backgroundColor).toBe('yellow'); + }); + + it('should color second

background w/ default color', () => { + fixture.detectChanges(); + const h2 = h2Des[1].nativeElement as HTMLElement; + expect(h2.style.backgroundColor).toBe(HighlightDirective.defaultColor); + }); + + it('should NOT color third

(no directive)', () => { + // no directive + expect(h2Des[2].providerTokens).not.toContain(HighlightDirective, 'HighlightDirective'); + fixture.detectChanges(); + + const h2 = h2Des[2].nativeElement as HTMLElement; + expect(h2.style.backgroundColor).toBe('', 'backgroundColor'); + }); +}); diff --git a/public/docs/_examples/testing/ts/app/shared/highlight.directive.ts b/public/docs/_examples/testing/ts/app/shared/highlight.directive.ts new file mode 100644 index 0000000000..9c091b3638 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/shared/highlight.directive.ts @@ -0,0 +1,24 @@ +import { Directive, ElementRef, Input, OnChanges, Renderer } from '@angular/core'; + +@Directive({ selector: '[highlight]' }) +/** + * Set backgroundColor for the attached element ton highlight color and + * set element `customProperty` = true + */ +export class HighlightDirective implements OnChanges { + + static defaultColor = 'rgb(211, 211, 211)'; // lightgray + + @Input('highlight') bgColor: string; + + constructor(private renderer: Renderer, private el: ElementRef) { + renderer.setElementProperty(el.nativeElement, 'customProperty', true); + } + + ngOnChanges() { + this.renderer.setElementStyle( + this.el.nativeElement, 'backgroundColor', + this.bgColor || HighlightDirective.defaultColor ); + } +} + diff --git a/public/docs/_examples/testing/ts/app/shared/shared.module.ts b/public/docs/_examples/testing/ts/app/shared/shared.module.ts new file mode 100644 index 0000000000..17c41c0410 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/shared/shared.module.ts @@ -0,0 +1,15 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; + +import { HighlightDirective } from './highlight.directive'; +import { TitleCasePipe } from './title-case.pipe'; +import { TwainComponent } from './twain.component'; + +@NgModule({ + imports: [ CommonModule ], + exports: [ CommonModule, FormsModule, + HighlightDirective, TitleCasePipe, TwainComponent ], + declarations: [ HighlightDirective, TitleCasePipe, TwainComponent ] +}) +export class SharedModule { } diff --git a/public/docs/_examples/testing/ts/app/shared/styles.css b/public/docs/_examples/testing/ts/app/shared/styles.css new file mode 100644 index 0000000000..b26317fa5e --- /dev/null +++ b/public/docs/_examples/testing/ts/app/shared/styles.css @@ -0,0 +1 @@ +/* MISSING */ diff --git a/public/docs/_examples/testing/ts/app/shared/title-case.pipe.spec.ts b/public/docs/_examples/testing/ts/app/shared/title-case.pipe.spec.ts new file mode 100644 index 0000000000..7481537c10 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/shared/title-case.pipe.spec.ts @@ -0,0 +1,33 @@ +// #docplaster +// #docregion +import { TitleCasePipe } from './title-case.pipe'; + +// #docregion excerpt +describe('TitleCasePipe', () => { + // This pipe is a pure function so no need for BeforeEach + let pipe = new TitleCasePipe(); + + it('transforms "abc" to "Abc"', () => { + expect(pipe.transform('abc')).toBe('Abc'); + }); + + it('transforms "abc def" to "Abc Def"', () => { + expect(pipe.transform('abc def')).toBe('Abc Def'); + }); + + // ... more tests ... +// #enddocregion excerpt + it('leaves "Abc Def" unchanged', () => { + expect(pipe.transform('Abc Def')).toBe('Abc Def'); + }); + + it('transforms "abc-def" to "Abc-def"', () => { + expect(pipe.transform('abc-def')).toBe('Abc-def'); + }); + + it('transforms " abc def" to " Abc Def" (preserves spaces) ', () => { + expect(pipe.transform(' abc def')).toBe(' Abc Def'); + }); +// #docregion excerpt +}); +// #enddocregion excerpt diff --git a/public/docs/_examples/testing/ts/app/shared/title-case.pipe.ts b/public/docs/_examples/testing/ts/app/shared/title-case.pipe.ts new file mode 100644 index 0000000000..df2567778d --- /dev/null +++ b/public/docs/_examples/testing/ts/app/shared/title-case.pipe.ts @@ -0,0 +1,11 @@ +// #docregion +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({name: 'titlecase', pure: false}) +/** Transform to Title Case: uppercase the first letter of the words in a string.*/ +export class TitleCasePipe implements PipeTransform { + transform(input: string): string { + return input.length === 0 ? '' : + input.replace(/\w\S*/g, (txt => txt[0].toUpperCase() + txt.substr(1).toLowerCase() )); + } +} diff --git a/public/docs/_examples/testing/ts/app/shared/twain.component.spec.ts b/public/docs/_examples/testing/ts/app/shared/twain.component.spec.ts new file mode 100644 index 0000000000..767e1ec2ca --- /dev/null +++ b/public/docs/_examples/testing/ts/app/shared/twain.component.spec.ts @@ -0,0 +1,92 @@ +// #docplaster +import { async, fakeAsync, ComponentFixture, TestBed, tick } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { DebugElement } from '@angular/core'; + +import { TwainService } from './twain.service'; +import { TwainComponent } from './twain.component'; + +describe('TwainComponent', () => { + + let comp: TwainComponent; + let fixture: ComponentFixture; + + let spy: jasmine.Spy; + let twainEl: DebugElement; // the element with the Twain quote + let twainService: TwainService; // the actually injected service + + const testQuote = 'Test Quote'; + + // #docregion setup + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [ TwainComponent ], + providers: [ TwainService ], + }); + + fixture = TestBed.createComponent(TwainComponent); + comp = fixture.componentInstance; + + // TwainService actually injected into the component + twainService = fixture.debugElement.injector.get(TwainService); + + // Setup spy on the `getQuote` method + // #docregion spy + spy = spyOn(twainService, 'getQuote') + .and.returnValue(Promise.resolve(testQuote)); + // #enddocregion spy + + // Get the Twain quote element by CSS selector (e.g., by class name) + twainEl = fixture.debugElement.query(By.css('.twain')); + }); + // #enddocregion setup + + // #docregion tests + function getQuote() { return twainEl.nativeElement.textContent; } + + it('should not show quote before OnInit', () => { + expect(getQuote()).toBe('', 'nothing displayed'); + expect(spy.calls.any()).toBe(false, 'getQuote not yet called'); + }); + + it('should still not show quote after component initialized', () => { + fixture.detectChanges(); // trigger data binding + // getQuote service is async => still has not returned with quote + expect(getQuote()).toBe('...', 'no quote yet'); + expect(spy.calls.any()).toBe(true, 'getQuote called'); + }); + + // #docregion async-test + it('should show quote after getQuote promise (async)', async(() => { + fixture.detectChanges(); // trigger data binding + + fixture.whenStable().then(() => { // wait for async getQuote + fixture.detectChanges(); // update view with quote + expect(getQuote()).toBe(testQuote); + }); + })); + // #enddocregion async-test + + // #docregion fake-async-test + it('should show quote after getQuote promise (fakeAsync)', fakeAsync(() => { + fixture.detectChanges(); // trigger data binding + tick(); // wait for async getQuote + fixture.detectChanges(); // update view with quote + expect(getQuote()).toBe(testQuote); + })); + // #enddocregion fake-async-test + // #enddocregion tests + + // #docregion done-test + it('should show quote after getQuote promise (done)', done => { + fixture.detectChanges(); // trigger data binding + + // get the spy promise and wait for it to resolve + spy.calls.mostRecent().returnValue.then(() => { + fixture.detectChanges(); // update view with quote + expect(getQuote()).toBe(testQuote); + done(); + }); + }); + // #enddocregion done-test +}); diff --git a/public/docs/_examples/testing/ts/app/shared/twain.component.timer.spec.ts.no-work b/public/docs/_examples/testing/ts/app/shared/twain.component.timer.spec.ts.no-work new file mode 100644 index 0000000000..74dec3e766 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/shared/twain.component.timer.spec.ts.no-work @@ -0,0 +1,116 @@ +// #docplaster +// When AppComponent learns to present quote with intervalTimer +import { async, discardPeriodicTasks, fakeAsync, ComponentFixture, TestBed, tick } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { DebugElement } from '@angular/core'; + +import { TwainService } from './model'; +import { TwainComponent } from './twain.component'; + +xdescribe('TwainComponent', () => { + + let comp: TwainComponent; + let fixture: ComponentFixture; + + const quotes = [ + 'Test Quote 1', + 'Test Quote 2', + 'Test Quote 3' + ]; + + let spy: jasmine.Spy; + let twainEl: DebugElement; // the element with the Twain quote + let twainService: TwainService; // the actually injected service + + function getQuote() { return twainEl.nativeElement.textContent; } + + // #docregion setup + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [ TwainComponent ], + providers: [ TwainService ], + }); + + fixture = TestBed.createComponent(TwainComponent); + comp = fixture.componentInstance; + + // TwainService actually injected into the component + twainService = fixture.debugElement.injector.get(TwainService); + + // Setup spy on the `getQuote` method + spy = spyOn(twainService, 'getQuote') + .and.returnValues(...quotes.map(q => Promise.resolve(q))); + + // Get the Twain quote element by CSS selector (e.g., by class name) + twainEl = fixture.debugElement.query(By.css('.twain')); + }); + + afterEach(() => { + // destroy component to stop the component timer + fixture.destroy(); + }); + // #enddocregion setup + + // #docregion tests + it('should not show quote before OnInit', () => { + expect(getQuote()).toBe(''); + }); + + it('should still not show quote after component initialized', () => { + // because the getQuote service is async + fixture.detectChanges(); // trigger data binding + expect(getQuote()).toContain('not initialized'); + }); + + // WIP + // If go this way, add jasmine.clock().uninstall(); to afterEach + // it('should show quote after Angular "settles"', async(() => { + // //jasmine.clock().install(); + // fixture.detectChanges(); // trigger data binding + // fixture.whenStable().then(() => { + // fixture.detectChanges(); // update view with the quote + // expect(getQuote()).toBe(quotes[0]); + // }); + // // jasmine.clock().tick(5000); + // // fixture.whenStable().then(() => { + // // fixture.detectChanges(); // update view with the quote + // // expect(getQuote()).toBe(quotes[1]); + // // }); + // })); + + it('should show quote after getQuote promise returns', fakeAsync(() => { + fixture.detectChanges(); // trigger data binding + tick(); // wait for first async getQuote to return + fixture.detectChanges(); // update view with the quote + expect(getQuote()).toBe(quotes[0]); + + // destroy component to stop the component timer before test ends + // else test errors because still have timer in the queue + fixture.destroy(); + })); + + it('should show 2nd quote after 5 seconds pass', fakeAsync(() => { + fixture.detectChanges(); // trigger data binding + tick(5000); // wait for second async getQuote to return + fixture.detectChanges(); // update view with the quote + expect(getQuote()).toBe(quotes[1]); + + // still have intervalTimer queuing requres + // discardPeriodicTasks() else test errors + discardPeriodicTasks(); + })); + + fit('should show 3rd quote after 10 seconds pass', fakeAsync(() => { + fixture.detectChanges(); // trigger data binding + tick(5000); // wait for second async getQuote to return + fixture.detectChanges(); // update view with the 2nd quote + tick(5000); // wait for third async getQuote to return + fixture.detectChanges(); // update view with the 3rd quote + expect(getQuote()).toBe(quotes[2]); + + // still have intervalTimer queuing requres + // discardPeriodicTasks() else test errors + discardPeriodicTasks(); + })); + // #enddocregion tests +}); diff --git a/public/docs/_examples/testing/ts/app/shared/twain.component.timer.ts.no-work b/public/docs/_examples/testing/ts/app/shared/twain.component.timer.ts.no-work new file mode 100644 index 0000000000..d3dc1f205d --- /dev/null +++ b/public/docs/_examples/testing/ts/app/shared/twain.component.timer.ts.no-work @@ -0,0 +1,27 @@ +// #docregion +import { Component, OnInit, OnDestroy } from '@angular/core'; + +import { TwainService } from './twain.service'; + +@Component({ + selector: 'twain-quote', + template: '

{{quote}}

' +}) +export class TwainComponent implements OnInit, OnDestroy { + intervalId: number; + quote = '-- not initialized yet --'; + constructor(private twainService: TwainService) { } + + getQuote() { + this.twainService.getQuote().then(quote => this.quote = quote); + } + + ngOnInit(): void { + this.getQuote(); + this.intervalId = window.setInterval(() => this.getQuote(), 5000); + } + + ngOnDestroy(): void { + clearInterval(this.intervalId); + } +} diff --git a/public/docs/_examples/testing/ts/app/shared/twain.component.ts b/public/docs/_examples/testing/ts/app/shared/twain.component.ts new file mode 100644 index 0000000000..29f24459ab --- /dev/null +++ b/public/docs/_examples/testing/ts/app/shared/twain.component.ts @@ -0,0 +1,20 @@ +// #docregion +import { Component, OnInit } from '@angular/core'; + +import { TwainService } from './twain.service'; + +// #docregion component +@Component({ + selector: 'twain-quote', + template: '

{{quote}}

' +}) +export class TwainComponent implements OnInit { + intervalId: number; + quote = '...'; + constructor(private twainService: TwainService) { } + + ngOnInit(): void { + this.twainService.getQuote().then(quote => this.quote = quote); + } +} +// #enddocregion component diff --git a/public/docs/_examples/testing/ts/app/shared/twain.service.ts b/public/docs/_examples/testing/ts/app/shared/twain.service.ts new file mode 100644 index 0000000000..9e394df1ee --- /dev/null +++ b/public/docs/_examples/testing/ts/app/shared/twain.service.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@angular/core'; + +const quotes = [ +'Always do right. This will gratify some people and astonish the rest.', +'I have never let my schooling interfere with my education.', +'Don\'t go around saying the world owes you a living. The world owes you nothing. It was here first.', +'Whenever you find yourself on the side of the majority, it is time to pause and reflect.', +'If you tell the truth, you don\'t have to remember anything.', +'Clothes make the man. Naked people have little or no influence on society.', +'It\'s not the size of the dog in the fight, it\'s the size of the fight in the dog.', +'Truth is stranger than fiction, but it is because Fiction is obliged to stick to possibilities; Truth isn\'t.', +'The man who does not read good books has no advantage over the man who cannot read them.', +'Get your facts first, and then you can distort them as much as you please.', +]; + +@Injectable() +export class TwainService { + private next = 0; + + // Imaginary todo: get quotes from a remote quote service + // returns quote after delay simulating server latency + getQuote(): Promise { + return new Promise(resolve => { + setTimeout( () => resolve(this.nextQuote()), 500 ); + }); + } + + private nextQuote() { + if (this.next === quotes.length) { this.next = 0; } + return quotes[ this.next++ ]; + } +} diff --git a/public/docs/_examples/testing/ts/app/welcome.component.spec.ts b/public/docs/_examples/testing/ts/app/welcome.component.spec.ts new file mode 100644 index 0000000000..ec59ef5bc2 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/welcome.component.spec.ts @@ -0,0 +1,83 @@ +// #docplaster +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { DebugElement } from '@angular/core'; + +import { UserService } from './model'; +import { WelcomeComponent } from './welcome.component'; + +describe('WelcomeComponent', () => { + + let comp: WelcomeComponent; + let fixture: ComponentFixture; + let userService: UserService; // the actually injected service + let welcomeEl: DebugElement; // the element with the welcome message + + // #docregion setup + beforeEach(() => { + // fake UserService for test purposes + // #docregion fake-userservice + const fakeUserService = { + isLoggedIn: true, + user: { name: 'Test User'} + }; + // #enddocregion fake-userservice + + // #docregion config-test-module + TestBed.configureTestingModule({ + declarations: [ WelcomeComponent ], + // #enddocregion setup + // providers: [ UserService ] // a real service would be a problem! + // #docregion setup + providers: [ {provide: UserService, useValue: fakeUserService } ] + }); + // #enddocregion config-test-module + + fixture = TestBed.createComponent(WelcomeComponent); + comp = fixture.componentInstance; + + // #enddocregion setup + // #docregion inject-from-testbed + // UserService provided to the TestBed + userService = TestBed.get(UserService); + // #enddocregion inject-from-testbed + // #docregion setup + // #docregion injected-service + // UserService actually injected into the component + userService = fixture.debugElement.injector.get(UserService); + // #enddocregion injected-service + + // get the "welcome" element by CSS selector (e.g., by class name) + welcomeEl = fixture.debugElement.query(By.css('.welcome')); + }); + // #enddocregion setup + + // #docregion tests + it('should welcome the user', () => { + fixture.detectChanges(); // trigger data binding + + let content = welcomeEl.nativeElement.textContent; + expect(content).toContain('Welcome', '"Welcome ..."'); + expect(content).toContain('Test User', 'expected name'); + }); + + it('should welcome "Bubba"', () => { + userService.user.name = 'Bubba'; // welcome message hasn't been shown yet + + fixture.detectChanges(); // trigger data binding + + let content = welcomeEl.nativeElement.textContent; + expect(content).toContain('Bubba'); + }); + + it('should request login if not logged in', () => { + userService.isLoggedIn = false; // welcome message hasn't been shown yet + + fixture.detectChanges(); // trigger data binding + + let content = welcomeEl.nativeElement.textContent; + expect(content).not.toContain('Welcome', 'not welcomed'); + expect(content).toMatch(/log in/i, '"log in"'); + }); + // #enddocregion tests +}); diff --git a/public/docs/_examples/testing/ts/app/welcome.component.ts b/public/docs/_examples/testing/ts/app/welcome.component.ts new file mode 100644 index 0000000000..35958cc5c9 --- /dev/null +++ b/public/docs/_examples/testing/ts/app/welcome.component.ts @@ -0,0 +1,18 @@ +// #docregion +import { Component, OnInit } from '@angular/core'; +import { UserService } from './model'; + +@Component({ + selector: 'app-welcome', + template: '

{{welcome}}

' +}) +export class WelcomeComponent implements OnInit { + welcome = '-- not initialized yet --'; + constructor(private userService: UserService) { } + + ngOnInit(): void { + this.welcome = this.userService.isLoggedIn ? + 'Welcome, ' + this.userService.user.name : + 'Please log in.'; + } +} diff --git a/public/docs/_examples/testing/ts/bag-specs.html b/public/docs/_examples/testing/ts/bag-specs.html new file mode 100644 index 0000000000..792ebc113f --- /dev/null +++ b/public/docs/_examples/testing/ts/bag-specs.html @@ -0,0 +1,41 @@ + + + + + + + Specs Bag + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/docs/_examples/testing/ts/bag-specs.plnkr.json b/public/docs/_examples/testing/ts/bag-specs.plnkr.json new file mode 100644 index 0000000000..89d86da28a --- /dev/null +++ b/public/docs/_examples/testing/ts/bag-specs.plnkr.json @@ -0,0 +1,20 @@ +{ + "description": "Testing - bag.specs", + "files":[ + "browser-test-shim.js", + "systemjs.config.extras.js", + "styles.css", + + "app/bag/**/*.html", + "app/bag/**/*.ts", + "app/bag/**/*.spec.ts", + + "!app/bag/bag-main.ts", + + "testing/*.ts", + + "bag-specs.html" + ], + "main": "bag-specs.html", + "tags": ["testing"] +} diff --git a/public/docs/_examples/testing/ts/bag.html b/public/docs/_examples/testing/ts/bag.html new file mode 100644 index 0000000000..35ff270025 --- /dev/null +++ b/public/docs/_examples/testing/ts/bag.html @@ -0,0 +1,27 @@ + + + + + + Specs Bag + + + + + + + + + + + + + + + + + Loading ... + + diff --git a/public/docs/_examples/testing/ts/bag.plnkr.json b/public/docs/_examples/testing/ts/bag.plnkr.json new file mode 100644 index 0000000000..96e0b79b65 --- /dev/null +++ b/public/docs/_examples/testing/ts/bag.plnkr.json @@ -0,0 +1,13 @@ +{ + "description": "Running the bag", + "files":[ + "styles.css", + + "app/bag/bag.ts", + "app/bag/bag-external-template.html", + "app/bag/bag-main.ts", + "bag.html" + ], + "main": "bag.html", + "tags": ["testing"] +} diff --git a/public/docs/_examples/testing/ts/browser-test-shim.js b/public/docs/_examples/testing/ts/browser-test-shim.js new file mode 100644 index 0000000000..1573c72ebd --- /dev/null +++ b/public/docs/_examples/testing/ts/browser-test-shim.js @@ -0,0 +1,87 @@ +// BROWSER TESTING SHIM +// Keep it in-sync with what karma-test-shim does +// #docregion +/*global jasmine, __karma__, window*/ +(function () { + +Error.stackTraceLimit = 0; // "No stacktrace"" is usually best for app testing. + +// Uncomment to get full stacktrace output. Sometimes helpful, usually not. +// Error.stackTraceLimit = Infinity; // + +jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000; + +var baseURL = document.baseURI; +baseURL = baseURL + baseURL[baseURL.length-1] ? '' : '/'; + +System.config({ + baseURL: baseURL, + // Extend usual application package list with test folder + packages: { 'testing': { main: 'index.js', defaultExtension: 'js' } }, + + // Assume npm: is set in `paths` in systemjs.config + // Map the angular testing umd bundles + map: { + '@angular/core/testing': 'npm:@angular/core/bundles/core-testing.umd.js', + '@angular/common/testing': 'npm:@angular/common/bundles/common-testing.umd.js', + '@angular/compiler/testing': 'npm:@angular/compiler/bundles/compiler-testing.umd.js', + '@angular/platform-browser/testing': 'npm:@angular/platform-browser/bundles/platform-browser-testing.umd.js', + '@angular/platform-browser-dynamic/testing': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic-testing.umd.js', + '@angular/http/testing': 'npm:@angular/http/bundles/http-testing.umd.js', + '@angular/router/testing': 'npm:@angular/router/bundles/router-testing.umd.js', + '@angular/forms/testing': 'npm:@angular/forms/bundles/forms-testing.umd.js', + }, +}); + +System.import('systemjs.config.js') + .then(importSystemJsExtras) + .then(initTestBed) + .then(initTesting); + +/** Optional SystemJS configuration extras. Keep going w/o it */ +function importSystemJsExtras(){ + return System.import('systemjs.config.extras.js') + .catch(function(reason) { + console.log( + 'Warning: System.import could not load the optional "systemjs.config.extras.js". Did you omit it by accident? Continuing without it.' + ); + console.log(reason); + }); +} + +function initTestBed(){ + return Promise.all([ + System.import('@angular/core/testing'), + System.import('@angular/platform-browser-dynamic/testing') + ]) + + .then(function (providers) { + var coreTesting = providers[0]; + var browserTesting = providers[1]; + + coreTesting.TestBed.initTestEnvironment( + browserTesting.BrowserDynamicTestingModule, + browserTesting.platformBrowserDynamicTesting()); + }) +} + +// Import all spec files defined in the html (__spec_files__) +// and start Jasmine testrunner +function initTesting () { + 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/index.html b/public/docs/_examples/testing/ts/index.html index bfde80afe3..b50b69ec18 100644 --- a/public/docs/_examples/testing/ts/index.html +++ b/public/docs/_examples/testing/ts/index.html @@ -1,8 +1,9 @@ + - Testing Tour of Heroes + App Under Test @@ -15,6 +16,7 @@ + diff --git a/public/docs/_examples/testing/ts/karma-test-shim.js b/public/docs/_examples/testing/ts/karma-test-shim.js new file mode 100644 index 0000000000..19fcc89fe9 --- /dev/null +++ b/public/docs/_examples/testing/ts/karma-test-shim.js @@ -0,0 +1,89 @@ +// #docregion +// /*global jasmine, __karma__, window*/ +Error.stackTraceLimit = 0; // "No stacktrace"" is usually best for app testing. + +// Uncomment to get full stacktrace output. Sometimes helpful, usually not. +// Error.stackTraceLimit = Infinity; // + +jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000; + +var builtPath = '/base/app/'; + +__karma__.loaded = function () { }; + +function isJsFile(path) { + return path.slice(-3) == '.js'; +} + +function isSpecFile(path) { + return /\.spec\.(.*\.)?js$/.test(path); +} + +function isBuiltFile(path) { + return isJsFile(path) && (path.substr(0, builtPath.length) == builtPath); +} + +var allSpecFiles = Object.keys(window.__karma__.files) + .filter(isSpecFile) + .filter(isBuiltFile); + +System.config({ + baseURL: '/base', + // Extend usual application package list with test folder + packages: { 'testing': { main: 'index.js', defaultExtension: 'js' } }, + + // Assume npm: is set in `paths` in systemjs.config + // Map the angular testing umd bundles + map: { + '@angular/core/testing': 'npm:@angular/core/bundles/core-testing.umd.js', + '@angular/common/testing': 'npm:@angular/common/bundles/common-testing.umd.js', + '@angular/compiler/testing': 'npm:@angular/compiler/bundles/compiler-testing.umd.js', + '@angular/platform-browser/testing': 'npm:@angular/platform-browser/bundles/platform-browser-testing.umd.js', + '@angular/platform-browser-dynamic/testing': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic-testing.umd.js', + '@angular/http/testing': 'npm:@angular/http/bundles/http-testing.umd.js', + '@angular/router/testing': 'npm:@angular/router/bundles/router-testing.umd.js', + '@angular/forms/testing': 'npm:@angular/forms/bundles/forms-testing.umd.js', + }, +}); + +System.import('systemjs.config.js') + .then(importSystemJsExtras) + .then(initTestBed) + .then(initTesting); + +/** Optional SystemJS configuration extras. Keep going w/o it */ +function importSystemJsExtras(){ + return System.import('systemjs.config.extras.js') + .catch(function(reason) { + console.log( + 'Warning: System.import could not load the optional "systemjs.config.extras.js". Did you omit it by accident? Continuing without it.' + ); + console.log(reason); + }); +} + +function initTestBed(){ + return Promise.all([ + System.import('@angular/core/testing'), + System.import('@angular/platform-browser-dynamic/testing') + ]) + + .then(function (providers) { + var coreTesting = providers[0]; + var browserTesting = providers[1]; + + coreTesting.TestBed.initTestEnvironment( + browserTesting.BrowserDynamicTestingModule, + browserTesting.platformBrowserDynamicTesting()); + }) +} + +// Import all spec files and start karma +function initTesting () { + return Promise.all( + allSpecFiles.map(function (moduleName) { + return System.import(moduleName); + }) + ) + .then(__karma__.start, __karma__.error); +} diff --git a/public/docs/_examples/karma.conf.js b/public/docs/_examples/testing/ts/karma.conf.js similarity index 60% rename from public/docs/_examples/karma.conf.js rename to public/docs/_examples/testing/ts/karma.conf.js index faa52df98e..1e2d293721 100644 --- a/public/docs/_examples/karma.conf.js +++ b/public/docs/_examples/testing/ts/karma.conf.js @@ -1,7 +1,12 @@ +// #docregion 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 and map files + var appSrcBase = 'app/'; // app source TS files + var appAssets = '/base/app/'; // component assets fetched by Angular's compiler + + var testBase = 'testing/'; // transpiled test JS and map files + var testSrcBase = 'testing/'; // test source TS files config.set({ basePath: '', @@ -9,7 +14,8 @@ module.exports = function(config) { plugins: [ require('karma-jasmine'), require('karma-chrome-launcher'), - require('karma-htmlfile-reporter') + require('karma-jasmine-html-reporter'), // click "Debug" in browser to see it + require('karma-htmlfile-reporter') // crashing w/ strange socket error ], customLaunchers: { @@ -26,39 +32,48 @@ module.exports = function(config) { // Polyfills 'node_modules/core-js/client/shim.js', - - // Reflect and Zone.js 'node_modules/reflect-metadata/Reflect.js', + + // zone.js 'node_modules/zone.js/dist/zone.js', + 'node_modules/zone.js/dist/long-stack-trace-zone.js', + 'node_modules/zone.js/dist/proxy.js', + 'node_modules/zone.js/dist/sync-test.js', 'node_modules/zone.js/dist/jasmine-patch.js', 'node_modules/zone.js/dist/async-test.js', 'node_modules/zone.js/dist/fake-async-test.js', - // RxJs. + // RxJs { pattern: 'node_modules/rxjs/**/*.js', included: false, watched: false }, { pattern: 'node_modules/rxjs/**/*.js.map', included: false, watched: false }, - // Angular 2 itself and the testing library + // Paths loaded via module imports: + // Angular itself {pattern: 'node_modules/@angular/**/*.js', included: false, watched: false}, {pattern: 'node_modules/@angular/**/*.js.map', included: false, watched: false}, {pattern: 'systemjs.config.js', included: false, watched: false}, + {pattern: 'systemjs.config.extras.js', included: false, watched: false}, 'karma-test-shim.js', // transpiled application & spec code paths loaded via module imports {pattern: appBase + '**/*.js', included: false, watched: true}, + {pattern: testBase + '**/*.js', included: false, watched: true}, - // asset (HTML & CSS) paths loaded via Angular's component compiler + + // 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} + // Paths for debugging with source maps in dev tools + {pattern: appSrcBase + '**/*.ts', included: false, watched: false}, + {pattern: appBase + '**/*.js.map', included: false, watched: false}, + {pattern: testSrcBase + '**/*.ts', included: false, watched: false}, + {pattern: testBase + '**/*.js.map', included: false, watched: false} ], - // proxied base paths for loading assets + // Proxied base paths for loading assets proxies: { // required for component assets fetched by Angular's compiler "/app/": appAssets @@ -66,7 +81,8 @@ module.exports = function(config) { exclude: [], preprocessors: {}, - reporters: ['progress', 'html'], + // disabled HtmlReporter; suddenly crashing w/ strange socket error + reporters: ['progress', 'kjhtml'],//'html'], // HtmlReporter configuration htmlReporter: { diff --git a/public/docs/_examples/testing/ts/liteserver-test-config.json b/public/docs/_examples/testing/ts/liteserver-test-config.json deleted file mode 100644 index 6b1a2b5466..0000000000 --- a/public/docs/_examples/testing/ts/liteserver-test-config.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "startPath": "unit-tests.html" -} \ No newline at end of file diff --git a/public/docs/_examples/testing/ts/plnkr.json b/public/docs/_examples/testing/ts/plnkr.json new file mode 100644 index 0000000000..e3573a6b6d --- /dev/null +++ b/public/docs/_examples/testing/ts/plnkr.json @@ -0,0 +1,16 @@ +{ + "description": "Heroes Test App", + "files":[ + "styles.css", + "systemjs.config.extras.js", + + "app/**/*.css", + "app/**/*.html", + "app/**/*.ts", + + "!app/bag/*.*", + + "index.html" + ], + "tags": ["testing"] +} diff --git a/public/docs/_examples/testing/ts/systemjs.config.extras.js b/public/docs/_examples/testing/ts/systemjs.config.extras.js new file mode 100644 index 0000000000..218e65715c --- /dev/null +++ b/public/docs/_examples/testing/ts/systemjs.config.extras.js @@ -0,0 +1,9 @@ +// #docregion +/** App specific SystemJS configuration */ +System.config({ + packages: { + // barrels + 'app/model': {main:'index.js', defaultExtension:'js'}, + 'app/model/testing': {main:'index.js', defaultExtension:'js'} + } +}); diff --git a/public/docs/_examples/testing/ts/test-helpers/dom-setup.ts.not-yet b/public/docs/_examples/testing/ts/test-helpers/dom-setup.ts.not-yet deleted file mode 100644 index 5e8f6d03a7..0000000000 --- a/public/docs/_examples/testing/ts/test-helpers/dom-setup.ts.not-yet +++ /dev/null @@ -1,18 +0,0 @@ -/////// MUST IMPORT AND EXECUTE BEFORE TestComponentBuilder TESTS //////////// - -// CRAZY BUG WORKAROUND: -// Must FIRST import and mention something (anything?) from angular -// else this file hangs systemjs for almost a minute -import { bind } from 'angular2/angular2'; -function noop() { return bind; } - -/////// THIS SECTION REALLY SHOULD BE EXECUTED FOR US BY ANGULAR //////////// -// should be in `angular2/test` or `angular2/angular2` but it isn't yet -import {BrowserDomAdapter} from 'angular2/src/core/dom/browser_adapter'; - -if (BrowserDomAdapter) { - // MUST be called before any specs involving the TestComponentBuilder - BrowserDomAdapter.makeCurrent(); -} else { - console.log("BrowserDomAdapter not found; TestComponentBuilder tests will fail"); -} diff --git a/public/docs/_examples/testing/ts/test-helpers/test-helpers.ts.not-yet b/public/docs/_examples/testing/ts/test-helpers/test-helpers.ts.not-yet deleted file mode 100644 index e39f4ae8ee..0000000000 --- a/public/docs/_examples/testing/ts/test-helpers/test-helpers.ts.not-yet +++ /dev/null @@ -1,103 +0,0 @@ -import {FunctionWithParamTokens, injectAsync,RootTestComponent, TestComponentBuilder} from 'angular2/testing'; -import {By} from 'angular2/angular2' - -///////// Should be in testing ///////// - -export type DoneFn = { - fail: (err?:any) => void, - (done?:any): () => void -} - -///////// injectAsync extensions /// - -type PromiseLikeTestFn = (...args:any[]) => PromiseLike; -type PromiseLikeTcbTestFn = (tcb: TestComponentBuilder, ...args:any[]) => PromiseLike; - -/** Run an async component test within Angular test bed using TestComponentBuilder -// Example -// it('async Component test', tcb => { -// // your test here -// // your test here -// // your test here -// return aPromise; -// }); -// -// May precede the test fn with some injectables which will be passed as args AFTER the TestComponentBuilder -// Example: -// it('async Component test w/ injectables', [HeroService], (tcb, service:HeroService) => { -// // your test here -// return aPromise; -// }); -*/ -export function injectTcb(testFn: (tcb: TestComponentBuilder) => PromiseLike): FunctionWithParamTokens; -export function injectTcb(dependencies: any[], testFn: PromiseLikeTcbTestFn): FunctionWithParamTokens; -export function injectTcb(dependencies: any[] | PromiseLikeTcbTestFn, testFn?: PromiseLikeTcbTestFn) { - - if (typeof dependencies === 'function' ){ - testFn = dependencies; - dependencies = []; - } - - return injectAsync([TestComponentBuilder, ...(dependencies)], testFn); -} -///////// inspectors and expectations ///////// - -export function getSelectedHtml(rootTC: RootTestComponent, selector: string) { - var debugElement = rootTC.debugElement.query(By.css(selector)); - return debugElement && debugElement.nativeElement && debugElement.nativeElement.innerHTML; -} - -export function expectSelectedHtml(rootTC: RootTestComponent, selector: string) { - return expect(getSelectedHtml(rootTC, selector)); -} - -export function getSelectedClassName(rootTC: RootTestComponent, selector: string) { - var debugElement = rootTC.debugElement.query(By.css(selector)); - return debugElement && debugElement.nativeElement && debugElement.nativeElement.className; -} - -export function expectSelectedClassName(rootTC: RootTestComponent, selector: string) { - return expect(getSelectedClassName(rootTC, selector)); -} - -export function getViewChildHtml(rootTC: RootTestComponent, elIndex: number = 0) { - let child = rootTC.debugElement.componentViewChildren[elIndex]; - return child && child.nativeElement && child.nativeElement.innerHTML -} - -export function expectViewChildHtml(rootTC: RootTestComponent, elIndex: number = 0) { - return expect(getViewChildHtml(rootTC, elIndex)); -} - -export function expectViewChildClass(rootTC: RootTestComponent, elIndex: number = 0) { - let child = rootTC.debugElement.componentViewChildren[elIndex]; - return expect(child && child.nativeElement && child.nativeElement.className); -} - -export function dispatchEvent(element: Element, eventType: string) { - element.dispatchEvent(new Event(eventType)); -} - -/** Let time pass so that DOM or Ng can react -// returns a promise that returns ("passes through") -// the value resolved in the previous `then` (if any) -// after delaying for [millis] which is zero by default. -// Example (passing along the rootTC w/ no delay): -// ... -// return rootTC; // optional -// }) -// .then(tick) -// .then(rootTC:RTC => { .. do something ..}); -// -// Example (passing along nothing in particular w/ 10ms delay): -// ... -// // don't care if it returns something or not -// }) -// .then(_ => tick(_, 10)) // ten milliseconds pass -// .then(() => { .. do something ..}); -*/ -export function tick(passThru?: any, millis: number = 0){ - return new Promise((resolve, reject) =>{ - setTimeout(() => resolve(passThru), millis); - }); -} \ No newline at end of file diff --git a/public/docs/_examples/testing/ts/test-shim.js b/public/docs/_examples/testing/ts/test-shim.js deleted file mode 100644 index 31e1998e69..0000000000 --- a/public/docs/_examples/testing/ts/test-shim.js +++ /dev/null @@ -1,48 +0,0 @@ -/*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/testing/fake-router.ts b/public/docs/_examples/testing/ts/testing/fake-router.ts new file mode 100644 index 0000000000..d42a3f8ad9 --- /dev/null +++ b/public/docs/_examples/testing/ts/testing/fake-router.ts @@ -0,0 +1,49 @@ + // export for convenience. +export { ActivatedRoute, Router, RouterLink, RouterOutlet} from '@angular/router'; + +import { Component, Directive, Injectable, Input } from '@angular/core'; +import { NavigationExtras } from '@angular/router'; + +@Directive({ + selector: '[routerLink]', + host: { + '(click)': 'onClick()', + '[attr.href]': 'visibleHref', + '[class.router-link-active]': 'isRouteActive' + } +}) +export class FakeRouterLinkDirective { + + isRouteActive = false; + visibleHref: string; // the url displayed on the anchor element. + + @Input('routerLink') linkParams: any; + navigatedTo: any = null; + + onClick() { + this.navigatedTo = this.linkParams; + } +} + +@Component({selector: 'router-outlet', template: ''}) +export class FakeRouterOutletComponent { } + +@Injectable() +export class FakeRouter { + lastCommand: any[]; + navigate(commands: any[], extras?: NavigationExtras) { + this.lastCommand = commands; + return commands; + } +} + +@Injectable() +export class FakeActivatedRoute { + testParams: {} = {}; + + get snapshot() { + return { + params: this.testParams + }; + } +} diff --git a/public/docs/_examples/testing/ts/testing/index.ts b/public/docs/_examples/testing/ts/testing/index.ts new file mode 100644 index 0000000000..f648a212e9 --- /dev/null +++ b/public/docs/_examples/testing/ts/testing/index.ts @@ -0,0 +1,23 @@ +import { tick, ComponentFixture } from '@angular/core/testing'; + +export * from './jasmine-matchers'; +export * from './fake-router'; + +// Short utilities +/** + * Create custom DOM event the old fashioned way + * + * https://developer.mozilla.org/en-US/docs/Web/API/Event/initEvent + * Although officially deprecated, some browsers (phantom) don't accept the preferred "new Event(eventName)" + */ +export function newEvent(eventName: string, bubbles = false, cancelable = false) { + let evt = document.createEvent('CustomEvent'); // MUST be 'CustomEvent' + evt.initCustomEvent(eventName, bubbles, cancelable, null); + return evt; +} + +/** Wait a tick, then detect changes */ +export function advance(f: ComponentFixture): void { + tick(); + f.detectChanges(); +} diff --git a/public/docs/_examples/testing/ts/testing/jasmine-matchers.d.ts b/public/docs/_examples/testing/ts/testing/jasmine-matchers.d.ts new file mode 100644 index 0000000000..f1c5acf77c --- /dev/null +++ b/public/docs/_examples/testing/ts/testing/jasmine-matchers.d.ts @@ -0,0 +1,5 @@ +declare namespace jasmine { + interface Matchers { + toHaveText(actual: any, expectationFailOutput?: any): jasmine.CustomMatcher; + } +} diff --git a/public/docs/_examples/testing/ts/testing/jasmine-matchers.ts b/public/docs/_examples/testing/ts/testing/jasmine-matchers.ts new file mode 100644 index 0000000000..4cab02e148 --- /dev/null +++ b/public/docs/_examples/testing/ts/testing/jasmine-matchers.ts @@ -0,0 +1,45 @@ +/// + +//// Jasmine Custom Matchers //// +// Be sure to extend jasmine-matchers.d.ts when adding matchers + +export function addMatchers(): void { + jasmine.addMatchers({ + toHaveText: toHaveText + }); +} + +function toHaveText(): jasmine.CustomMatcher { + return { + compare: function (actual: any, expectedText: string, expectationFailOutput?: any): jasmine.CustomMatcherResult { + const actualText = elementText(actual); + const pass = actualText.indexOf(expectedText) > -1; + const message = pass ? '' : composeMessage(); + return { pass, message }; + + function composeMessage () { + const a = (actualText.length < 100 ? actualText : actualText.substr(0, 100) + '...'); + const efo = expectationFailOutput ? ` '${expectationFailOutput}'` : ''; + return `Expected element to have text content '${expectedText}' instead of '${a}'${efo}`; + } + } + }; +} + +function elementText(n: any): string { + if (n instanceof Array) { + return n.map(elementText).join(''); + } + + if (n.nodeType === Node.COMMENT_NODE) { + return ''; + } + + if (n.nodeType === Node.ELEMENT_NODE && n.hasChildNodes()) { + return elementText(Array.prototype.slice.call(n.childNodes)); + } + + if (n.nativeElement) { n = n.nativeElement; } + + return n.textContent; +} diff --git a/public/docs/_examples/testing/ts/tsconfig.1.json b/public/docs/_examples/testing/ts/tsconfig.1.json deleted file mode 100644 index 062cf1bcb4..0000000000 --- a/public/docs/_examples/testing/ts/tsconfig.1.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "compilerOptions": { - "target": "es5", - "module": "system", - "moduleResolution": "node", - "sourceMap": true, - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "removeComments": false, - "noImplicitAny": true, - "suppressImplicitAnyIndexErrors": true - } -} diff --git a/public/docs/_examples/testing/ts/unit-tests-0.html b/public/docs/_examples/testing/ts/unit-tests-0.html deleted file mode 100644 index af7d1b9192..0000000000 --- a/public/docs/_examples/testing/ts/unit-tests-0.html +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - Ng App Unit Tests - - - - - - - - - - - - - - - - - - diff --git a/public/docs/_examples/testing/ts/unit-tests-1.html b/public/docs/_examples/testing/ts/unit-tests-1.html deleted file mode 100644 index b370ca053a..0000000000 --- a/public/docs/_examples/testing/ts/unit-tests-1.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - Ng App Unit Tests - - - - - - - - - - - - - - - diff --git a/public/docs/_examples/testing/ts/unit-tests-2.html b/public/docs/_examples/testing/ts/unit-tests-2.html deleted file mode 100644 index d47b4d1f60..0000000000 --- a/public/docs/_examples/testing/ts/unit-tests-2.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - Ng App Unit Tests - - - - - - - - - - - - - - - - - - - diff --git a/public/docs/_examples/testing/ts/unit-tests-3.html b/public/docs/_examples/testing/ts/unit-tests-3.html deleted file mode 100644 index 349606bd6d..0000000000 --- a/public/docs/_examples/testing/ts/unit-tests-3.html +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - Ng App Unit Tests - - - - - - - - - - - - - - - - - diff --git a/public/docs/_examples/testing/ts/unit-tests-4.html b/public/docs/_examples/testing/ts/unit-tests-4.html deleted file mode 100644 index a3e252fdb0..0000000000 --- a/public/docs/_examples/testing/ts/unit-tests-4.html +++ /dev/null @@ -1,43 +0,0 @@ - - - - - Ng App Unit Tests - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/public/docs/_examples/testing/ts/unit-tests-5.html b/public/docs/_examples/testing/ts/unit-tests-5.html deleted file mode 100644 index b95b36760a..0000000000 --- a/public/docs/_examples/testing/ts/unit-tests-5.html +++ /dev/null @@ -1,41 +0,0 @@ - - - - - Ng App Unit Tests - - - - - - - - - - - - - - - - - - - - - - - diff --git a/public/docs/_examples/testing/ts/unit-tests-6.html.not-yet b/public/docs/_examples/testing/ts/unit-tests-6.html.not-yet deleted file mode 100644 index df8e3704ba..0000000000 --- a/public/docs/_examples/testing/ts/unit-tests-6.html.not-yet +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - Ng App Unit Tests - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/public/docs/_examples/testing/ts/unit-tests-7.html.not-yet b/public/docs/_examples/testing/ts/unit-tests-7.html.not-yet deleted file mode 100644 index d5449711ee..0000000000 --- a/public/docs/_examples/testing/ts/unit-tests-7.html.not-yet +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - Ng App Unit Tests - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/public/docs/_examples/testing/ts/unit-tests-bag.html b/public/docs/_examples/testing/ts/unit-tests-bag.html deleted file mode 100644 index f373c387e8..0000000000 --- a/public/docs/_examples/testing/ts/unit-tests-bag.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - Bag of Unit Tests - - - - - - - - - - - - - - - - - - - - - - diff --git a/public/docs/_examples/testing/ts/unit-tests.html.not-yet b/public/docs/_examples/testing/ts/unit-tests.html.not-yet deleted file mode 100644 index f1b8ab444d..0000000000 --- a/public/docs/_examples/testing/ts/unit-tests.html.not-yet +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - Ng App Unit Tests - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/public/docs/_examples/testing/ts/wallaby.js b/public/docs/_examples/testing/ts/wallaby.js new file mode 100644 index 0000000000..acc34d35f5 --- /dev/null +++ b/public/docs/_examples/testing/ts/wallaby.js @@ -0,0 +1,119 @@ +// 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: [ + // System.js for module loading + {pattern: 'node_modules/systemjs/dist/system.js', instrument: false}, + {pattern: 'systemjs.config.js', instrument: false}, + {pattern: 'systemjs.config.extras.js', instrument: false}, + + // Polyfills + {pattern: 'node_modules/core-js/client/shim.min.js', instrument: false}, + {pattern: 'node_modules/reflect-metadata/Reflect.js', instrument: false}, + + // zone.js + {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/proxy.js', instrument: false}, + {pattern: 'node_modules/zone.js/dist/sync-test.js', instrument: false}, + {pattern: 'node_modules/zone.js/dist/jasmine-patch.js', instrument: false}, + {pattern: 'node_modules/zone.js/dist/async-test.js', instrument: false}, + {pattern: 'node_modules/zone.js/dist/fake-async-test.js', instrument: false}, + + // application (but not specs) loaded via module imports + {pattern: 'app/**/*+(ts|html|css)', load: false}, + {pattern: 'app/**/*.spec.ts', ignore: true}, + + {pattern: 'testing/**/*+(ts|html|css)', load: false}, + ], + + 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', + + debug: true, + + bootstrap: bootstrap + }; +}; + +// Like karma-test-shim.js +function bootstrap (wallaby) { + wallaby.delayStart(); + + System.config({ + // Extend usual application package list with test folder + packages: { 'testing': { main: 'index.js', defaultExtension: 'js' } }, + + // Assume npm: is set in `paths` in systemjs.config + // Map the angular testing umd bundles + map: { + '@angular/core/testing': 'npm:@angular/core/bundles/core-testing.umd.js', + '@angular/common/testing': 'npm:@angular/common/bundles/common-testing.umd.js', + '@angular/compiler/testing': 'npm:@angular/compiler/bundles/compiler-testing.umd.js', + '@angular/platform-browser/testing': 'npm:@angular/platform-browser/bundles/platform-browser-testing.umd.js', + '@angular/platform-browser-dynamic/testing': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic-testing.umd.js', + '@angular/http/testing': 'npm:@angular/http/bundles/http-testing.umd.js', + '@angular/router/testing': 'npm:@angular/router/bundles/router-testing.umd.js', + '@angular/forms/testing': 'npm:@angular/forms/bundles/forms-testing.umd.js', + }, + }); + + System.import('systemjs.config.js') + .then(importSystemJsExtras) + .then(initTestBed) + .then(initTesting); + + /** Optional SystemJS configuration extras. Keep going w/o it */ + function importSystemJsExtras(){ + return System.import('systemjs.config.extras.js') + .catch(function(reason) { + console.log( + 'Warning: System.import could not load the optional "systemjs.config.extras.js". Did you omit it by accident? Continuing without it.' + ); + console.log(reason); + }); + } + + function initTestBed(){ + return Promise.all([ + System.import('@angular/core/testing'), + System.import('@angular/platform-browser-dynamic/testing') + ]) + + .then(function (providers) { + var coreTesting = providers[0]; + var browserTesting = providers[1]; + + coreTesting.TestBed.initTestEnvironment( + browserTesting.BrowserDynamicTestingModule, + browserTesting.platformBrowserDynamicTesting()); + }) + } + + // Load all spec files and start wallaby + function initTesting () { + return Promise.all( + wallaby.tests.map(function (specFile) { + return System.import(specFile); + }) + ) + .then(function () { + wallaby.start(); + }) + .catch(function (e) { + setTimeout(function () { + throw e; + }, 0); + }); + } +} diff --git a/public/docs/_examples/toh-6/ts/app/hero-search.service.ts b/public/docs/_examples/toh-6/ts/app/hero-search.service.ts index ae2e47670a..a7fb3cf520 100644 --- a/public/docs/_examples/toh-6/ts/app/hero-search.service.ts +++ b/public/docs/_examples/toh-6/ts/app/hero-search.service.ts @@ -12,7 +12,7 @@ export class HeroSearchService { search(term: string): Observable { return this.http - .get(`app/heroes/?name=${term}`) + .get('app/heroes/?name=${term}') .map((r: Response) => r.json().data as Hero[]); } } diff --git a/public/docs/_examples/wallaby.js b/public/docs/_examples/wallaby.js deleted file mode 100644 index 28053a11fe..0000000000 --- a/public/docs/_examples/wallaby.js +++ /dev/null @@ -1,77 +0,0 @@ -// 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: [ - // System.js for module loading - {pattern: 'node_modules/systemjs/dist/system.js', instrument: false}, - {pattern: 'systemjs.config.js', instrument: false}, - - // Polyfills - {pattern: 'node_modules/core-js/client/shim.min.js', instrument: false}, - - // Reflect, Zone.js, and test shims - // Rx.js, Angular 2 itself, and the testing library not here because loaded by systemjs - {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/jasmine-patch.js', instrument: false}, - {pattern: 'node_modules/zone.js/dist/async-test.js', instrument: false}, - {pattern: 'node_modules/zone.js/dist/fake-async-test.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', - - debug: true, - - bootstrap: function (wallaby) { - wallaby.delayStart(); - - System.config({ - packageWithIndex: true // sadly, we can't use umd packages (yet?) - }); - - System.import('systemjs.config.js') - .then(function () { - return Promise.all([ - System.import('@angular/core/testing'), - System.import('@angular/platform-browser-dynamic/testing') - ]) - }) - .then(function (providers) { - var testing = providers[0]; - var testingBrowser = providers[1]; - - testing.setBaseTestProviders( - testingBrowser.TEST_BROWSER_DYNAMIC_PLATFORM_PROVIDERS, - testingBrowser.TEST_BROWSER_DYNAMIC_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); - }); - } - }; -}; diff --git a/public/docs/ts/latest/guide/testing.jade b/public/docs/ts/latest/guide/testing.jade index 7777219c47..dd8fafa036 100644 --- a/public/docs/ts/latest/guide/testing.jade +++ b/public/docs/ts/latest/guide/testing.jade @@ -1,102 +1,1785 @@ -.alert.is-important - :marked - We are still preparing the testing guide with all the new testing features - introduced in RC5 and will update it very soon. +block includes + include ../_util-fns + - var _JavaScript = 'JavaScript'; + //- Double underscore means don't escape var, use !{__var}. + - var __chaining_op = '; or ,'; + - var __new_op = 'new'; + - var __objectAsMap = 'object'; :marked - We write **unit tests** to explore and confirm the **behavior** of parts of our application. + This chapter offers tips and techniques for testing Angular applications. + Along the way you will learn some general testing principles and techniques but the focus is on + Angular testing. - 1. They **guard** against breaking existing code (“regressions”) when we make changes. - 1. They **clarify** what the code does both when used as intended and when faced with deviant conditions. - 1. They **reveal** mistakes in design and implementation. Tests force us to look at our code from many angles. When a part of our application seems hard to test, we may have discovered a design flaw, something we can cure now rather than later when it becomes expensive to fix. - -a(id="top") +a#top :marked - # Table of Contents - - 1. [Jasmine Testing 101](#jasmine-101) - - setup to run Jasmine tests in the browser - - basic Jasmine testing skills - - write simple Jasmine tests in TypeScript - - debug a test in the browser - - 1. [The Application Under Test](#aut) - - 1. [First app test](#first-app-tests) - - test a simple application interface outside of Angular - - where to put the test file - - load a test file with systemJS - - 1. [Pipe driven development](#pipe-testing) - - create a test before creating a class - - load multiple test files in our test harness, using system.js - - add the Angular 2 library to our test harness - - watch the new test fail, and fix it - - 1. Test an Asynchronous Service (forthcoming) - - test an asynchronous service class outside of Angular - - write a test plan in code - - fake a dependency - - master the `catch(fail).then(done)` pattern - - move setup to `beforeEach` - - test when a dependency fails - - control async test timeout - - 1. The Angular Test Environment (forthcoming) - - the Angular test environment and why we need help - - add the Angular Test libraries to the test harness - - test the same async service using Angular Dependency Injection - - reduce friction with test helpers - - introducing spies - - 1. Test a Component (forthcoming) - - test the component outside of Angular - - mock the dependent asynchronous service - - simulate interaction with the view (no DOM) - - use a spy-promise to control asynchronous test flow - - 1. Test a Component in the DOM (forthcoming - - test the component inside the Angular test environment - - use the `TestComponentBuilder` - - more test helpers - - interact with the DOM - - bind to a mock dependent asynchronous service - - 1. Run the tests with karma (forthcoming) + # Contents + * [Introduction to Angular Testing](#testing-101) + * [Setup](#setup) + * [The first karma test](#1st-karma-test) + * [The Angular Testing Platform (ATP) ](#atp-intro) + * [The sample application and its tests](#sample-app) + * [A simple component test](#simple-component-test) + * [Test a component with a service dependency](#component-with-dependency) + * [Test a component with an async service](#component-with-async-service) + * [Test a component with an external template](#component-with-external-template) + * [Test a component with inputs and outputs](#component-with-inputs-output) + * [Test a component inside a test host component](#component-inside-test-host) + * [Test a routed component](#routed-component) + * [Isolated tests](#testing-without-atp "Testing without the Angular Testing Platform") + * [_TestBed_ API](#atp-api) + * [FAQ](#faq "Frequently asked questions") +:marked It’s a big agenda. Fortunately, you can learn a little bit at a time and put each lesson to use. + # Live examples + The chapter sample code is available as live examples for inspection, experiment, and download. + + * The sample application + * The first spec + * The complete application specs + * A grab bag of demonstration specs a(href="#top").to-top Back to top .l-hr -a(id="jasmine-101") +a#testing-101 :marked - # Jasmine Testing 101 -!= partial("../testing/jasmine-testing-101") -a(href="#top").to-top Back to top + # Introduction to Angular Testing + + You write tests to explore and confirm the behavior of the application. + + 1. They **guard** against changes that break existing code (“regressions”). + + 1. They **clarify** what the code does both when used as intended and when faced with deviant conditions. + + 1. They **reveal** mistakes in design and implementation. + Tests shine a harsh light on the code from many angles. + When a part of the application seems hard to test, the root cause is often a design flaw, + something to cure now rather than later when it becomes expensive to fix. + + This chapter assumes that you know something about testing. Don't worry if you don't. + There are plenty of books and online resources to get up to speed. + + + + + ## Tools and Technologies + + You can write and run Angular tests with a variety of tools and technologies. + This chapter describes specific choices that are known to work well. + +table(width="100%") + col(width="20%") + col(width="80%") + tr + th Technology + th Purpose + tr(style=top) + td(style="vertical-align: top") Jasmine + td + :marked + The [Jasmine test framework](http://jasmine.github.io/2.4/introduction.html). + provides everything needed to write basic tests. + It ships with an HTML test runner that executes tests in the browser. + tr(style=top) + td(style="vertical-align: top") Angular Testing Platform + td + :marked + The Angular Testing Platform creates a test environment and harness + for the application code under test. + Use it to condition and control parts of the application as they + interact _within_ the Angular environment. + tr(style=top) + td(style="vertical-align: top") Karma + td + :marked + The [karma test runner](https://karma-runner.github.io/1.0/index.html) + is ideal for writing and running tests while developing the application. + It can be an integral part of the application build process. + This chapter describes how to setup and run tests with karma. + tr(style=top) + td(style="vertical-align: top") Protractor + td + :marked + Use protractor to write and run _end-to-end_ (e2e) tests. + End-to-end tests explore the application _as users experience it_. + In e2e testing, one process runs the real application + and a second process runs protractor tests that simulate user behavior + and assert that the application responds in the browser as expected. .l-hr -a(id="aut") +a#setup :marked - # The Application to Test -!= partial("../testing/application-under-test") -a(href="#top").to-top Back to top + # Setup + + Many think writing tests is fun. + Few enjoy setting up the test environment. + To get to the fun as quickly as possible, + the deep details of setup appear later in the chapter (_forthcoming_). + A bare minimum of discussion plus the downloadable source code must suffice for now. + + There are two fast paths to getting started. + 1. Start a new project following the instructions in the + [QuickStart github repository](https://github.com/angular/quickstart/blob/master/README.md). + + 1. Start a new project with the + [Angular CLI](https://github.com/angular/angular-cli/blob/master/README.md). + + Both approaches install **npm packages, files, and scripts** pre-configured for applications + built in their respective modalities. + Their artifacts and procedures differ slightly but their essentials are the same + and there are no differences in the test code. + + In this chapter, the application and its tests are based on the QuickStart repo. + +.alert.is-helpful + :marked + If youur application was based on the QuickStart repository, + you can skip the rest of this section and get on with your first test. + The QuickStart repo provides all necessary setup. + +:marked + Here's brief description of the setup files. + +table(width="100%") + col(width="20%") + col(width="80%") + tr + th File + th Description + tr + td(style="vertical-align: top") karma.conf.js + td + :marked + The karma configuration file that specifies which plug-ins to use, + which application and test files to load, which browser(s) to use, + and how to report test results. + + It loads three other setup files: + * `systemjs.config.js` + * `systemjs.config.extras.js` + * `karma-test-shim.js` + tr + td(style="vertical-align: top") karma-test-shim.js + td + :marked + This shim prepares karma specifically for the Angular test environment + and launches karma itself. + It loads the `systemjs.config.js` file as part of that process. + tr + td(style="vertical-align: top") systemjs.config.js + td + :marked + [SystemJS](https://github.com/systemjs/systemjs/blob/master/README.md) + loads the application and test modules. + This script tells SystemJS where to find the module files and how to load them. + It's the same version of the file used by QuickStart-based applications. + tr + td(style="vertical-align: top") systemjs.config.extras.js + td + :marked + An optional file that supplements the SystemJS configuration in `systemjs.config.js` with + configuration for the specific needs of the application itself. + + A stock `systemjs.config.js` can't anticipate those needs. + You fill the gaps here. + + The sample version for this chapter adds the **model barrel** + to the SystemJs `packages` configuration. + tr + td(colspan="2") + +makeExample('testing/ts/systemjs.config.extras.js', '', 'systemjs.config.extras.js')(format='.') + +:marked + ### npm packages + + The sample tests are written to run in Jasmine and karma. + The two "fast path" setups added the appropriate Jasmine and karma npm packages to the + `devDependencies` section of the `package.json`. + They were installed when you ran `npm install`. .l-hr -a(id="first-app-tests") +a#1st-karma-test :marked - # First app test -!= partial("../testing/first-app-tests") -a(href="#top").to-top Back to top + # The first karma test -.l-hr -a(id="pipe-testing") -:marked - # Pipe driven development -!= partial("../testing/testing-an-angular-pipe") -a(href="#top").to-top Back to top + Start with a simple test to make sure the setup works properly. + + Create a new file called `1st.spec.ts` in the application root folder, `app/` .alert.is-important :marked - The testing chapter is still under development. - Please bear with us as we both update and complete it. + Tests written in Jasmine are called _specs_ . + **The filename extension must be `.spec.ts`**, + the convention adhered to by `karma.conf.js` and other tooling. + +:marked + **Put spec files somewhere within the `app/` folder.** + The `karma.conf.js` tells karma to look for spec files there, + for reasons explained [below](#spec-file-location). + + Add the following code to `app/1st.spec.ts`. ++makeExample('testing/ts/app/1st.spec.ts', '', 'app/1st.spec.ts')(format='.') +:marked + ## Run karma + Compile and run it in karma from the command line. + +.l-sub-section + :marked + The QuickStart repo adds the following command to the `scripts` section in `package.json`. + + code-example(format="." language="bash"). + "test": "tsc && concurrently \"tsc -w\" \"karma start karma.conf.js\"", + :marked + Add that to your `package.json` if it's not there already. + +:marked + Open a terminal or command window and enter +code-example(format="." language="bash"). + npm test +:marked + The command compiles the application and test code a first time. + If the compile fails, the command aborts. + + If it succeeds, the command re-compiles (this time in watch mode) in one process + and starts karma in another. + Both processes watch pertinent files and re-run when they detect changes. + + After a few moments, karma opens a browser ... +figure.image-display + img(src='/resources/images/devguide/testing/karma-browser.png' style="width:400px;" alt="Karma browser") +:marked + ... and starts writing to the console. + + Hide (don't close!) the browser and focus on the console output which should look something like this. + +code-example(format="." language="bash"). + > npm test + > tsc && concurrently "tsc -w" "karma start karma.conf.js" + + [0] 1:37:03 PM - Compilation complete. Watching for file changes. + [1] 24 07 2016 13:37:09.310:WARN [karma]: No captured browser, open http://localhost:9876/ + [1] 24 07 2016 13:37:09.361:INFO [karma]: Karma v0.13.22 server started at http://localhost:9876/ + [1] 24 07 2016 13:37:09.370:INFO [launcher]: Starting browser Chrome + [1] 24 07 2016 13:37:10.974:INFO [Chrome 51.0.2704]: Connected on socket /#Cf6A5PkvMzjbbtn1AAAA with id 24600087 + [1] Chrome 51.0.2704: Executed 0 of 0 SUCCESS + Chrome 51.0.2704: Executed 1 of 1 SUCCESS + SUCCESS (0.005 secs / 0.005 secs) + +:marked + Both the compiler and karma continue to run. The compiler output is preceeded by `[0]`; + the karma output by `[1]`. + + Change the expectation from `true` to `false`. + + The _compiler_ watcher detects the change and recompiles. + +code-example(format="." language="bash"). + [0] 1:49:21 PM - File change detected. Starting incremental compilation... + [0] 1:49:25 PM - Compilation complete. Watching for file changes. + +:marked + The _karma_ watcher detects the change to the compilation output and re-runs the test. +code-example(format="." language="bash"). + [1] Chrome 51.0.2704: Executed 0 of 1 SUCCESS + Chrome 51.0.2704 1st tests true is true FAILED + [1] Expected false to equal true. + [1] Chrome 51.0.2704: Executed 1 of 1 (1 FAILED) (0.005 secs / 0.005 secs) + +:marked + It failed of course. + + Restore the expectation from `false` back to `true`. + Both processes detect the change, re-run, and karma reports complete success. + +.alert.is-helpful + :marked + The console log can be quite long. Keep your eye on the last line. + It says `SUCCESS` when all is well. + + If it says `FAILED`, scroll up to look for the error or, if that's too painful, + pipe the console output to a file and inspect with your favorite editor. + code-example(format="." language="json"). + npm test > spec-output.txt + +:marked + ## Test debugging + + Debug specs in the browser in the same way you debug an application. + + - Reveal the karma browser window (hidden earlier). + - Open the browser's “Developer Tools” (F12 or Ctrl-Shift-I). + - Pick the “sources” section + - Open the `1st.spec.ts` test file (Ctrl-P, then start typing the name of the file). + - Set a breakpoint in the test + - Refresh the browser … and it stops at the breakpoint. + +figure.image-display + img(src='/resources/images/devguide/testing/karma-1st-spec-debug.png' style="width:700px;" alt="Karma debugging") + +a(href="#top").to-top Back to top + +.l-hr +a#atp-intro +:marked + # The Angular Testing Platform (ATP) + + Many tests explore how applications classes interact with Angular and the DOM while under Angular's control. + + Such tests are easy to write with the help of the _Angular Testing Platform_ (ATP) + which consists of the `TestBed` class and some helper functions. + + Tests written with the _Angular Testing Platform_ are the main focus of this chapter. + But they are not the only tests you should write. + + ### Isolated unit tests + + You can and should write [isolated unit tests](#testing-without-atp "Testing without the Angular Testing Platform") + for components, directives, pipes, and services. + Isolated unit tests examine an instance of a class all by itself without + any dependence on Angular or any injected values. + The tester creates a test instance of the class with new, supplying fake constructor parameters as needed, and + then probes the test instance API surface. + + Isolated tests don't reveal how the class interacts with Angular. + In particular, they can't reveal how a component class interacts with its own template or with other components. + + Those tests require the Angular Testing Platform. + + ### Testing with the _ Angular Testing Platform_ + + The _Angular Testing Platform_ consists of the `TestBed` class and some helper functions from `@angular/core/testing`. +.alert.is-important + :marked + The _TestBed_ is officially _experimental_ and thus subject to change. + Consult the [API reference](../api/core/testing/index/TestBed-class.html) for the latest status. +:marked + The `TestBed` creates an Angular test module — an `@NgModule` class — + that you configure to produce the module environment for the class you want to test. + You tell the `TestBed` to create an instance of the test component and probe that instance with tests. + + That's the `TestBed` in a nutshell. + + In practice, you work with the static methods of the `TestBed` class. + These static methods create and update a fresh hidden `TestBed` instance before each Jasmine `it`. +.l-sub-section + :marked + You can access that hidden instance anytime by calling `getTestBed()`; +:marked + This `TestBed` instance comes pre-configured with a baseline of default providers and declarables (components, directives, and pipes) + that almost everyone needs. + This chapter tests a browser application so the default includes the `CommonModule` declarables from `@angular/common` + and the `BrowserModule` providers (some of them mocked) from `@angular/platform-browser`. + + You refine the default test module configuration with application and test specifics + so that it can produce an instance of the test component in the Angular environment suitable for your tests. + + Start by calling `TestBed.configureTestingModule` with an object that looks like `@NgModule` metadata. + This object defines additional imports, declarations, providers and schemas. + + After configuring the `TestBed`, tell it to create an instance of the test component and the test fixture + you'll need to inspect and control the component's immediate environment. + ++makeExample('testing/ts/app/banner.component.spec.ts', 'simple-example-before-each', 'app/banner.component.spec.ts (simplified)')(format='.') +:marked + Angular tests can interact with the HTML in the test DOM, + simulate user activity, tell Angular to perform specific task (such as change detection), + and see the effects of these actions both in the test component and in the test DOM. ++makeExample('testing/ts/app/banner.component.spec.ts', 'simple-example-it', 'app/banner.component.spec.ts (simplified)')(format='.') +:marked + A comprehensive review of the _TestBed_ API appears [later in the chapter](#atp-api). + Let's dive right into Angular testing, starting with with the components of a sample application. + +a(href="#top").to-top Back to top + +.l-hr + +a#sample-app +:marked + # The sample application and its tests + + This chapter tests a cut-down version of the _Tour of Heroes_ [tutorial app](../tutorial). + + The following live example shows how it works and provides the complete source code. + +

+:marked + The following live example runs all the tests of this application + inside the browser, using the Jasmine Test Runner instead of karma. + + It includes the tests discussed in this chapter and additional tests for you to explore. + This live example contains both application and test code. + It is large and can take several minutes to start. Please be patient. + + +a(href="#top").to-top Back to top +.l-hr + +a#simple-component-test +:marked + # Test a component + +:marked + The top of the screen displays application title, presented by the `BannerComponent` in `app/banner.component.ts`. ++makeExample('testing/ts/app/banner.component.ts', '', 'app/banner.component.ts')(format='.') +:marked + `BannerComponent` has an inline template and an interpolation binding, about as simple as it gets. + Probably too simple to be worth testing in real life but perfect for a first encounter with the `TestBed`. + + The corresponding `app/banner-component.spec.ts` sits in the same folder as the component, + for reasons explained [here](#q-spec-file-location); + + Start with ES6 import statements to get access to symbols referenced in the spec. ++makeExample('testing/ts/app/banner.component.spec.ts', 'imports', 'app/banner.component.spec.ts (imports)')(format='.') +:marked + Here's the setup for the tests followed by observations about the `beforeEach`: ++makeExample('testing/ts/app/banner.component.spec.ts', 'setup', 'app/banner.component.spec.ts (imports)')(format='.') +:marked + `TestBed.configureTestingModule` takes an `@NgModule`-like metadata object. + This one simply declares the component to test, `BannerComponent`. + + It lacks `imports` because (a) it extends the default test module configuration which + already has what `BannerComponent` needs + and (b) `BannerComponent` doesn't interact with any other components. + + The configuration could have imported `AppModule` (which declares `BannerComponent`). + But that would lead to tons more configuration in order to support the other components within `AppModule` + that have nothing to do with `BannerComponent`. + + `TestBed.createComponent` creates an instance of `BannerComponent` to test. + The method returns a `ComponentFixture`, a handle on the test environment surrounding the created component. + The fixture provides access to the component instance itself and + to the `DebugElement` which is a handle on the component's DOM element. + + Query the `DebugElement` by CSS selector for the `

` sub-element that holds the actual title. + + + ### _createComponent_ closes configuration + `TestBed.createComponent` closes the current `TestBed` instance to further configuration. + You cannot call any more `TestBed` configuration methods, not `configureTestModule` + nor any of the `override...` methods. The `TestBed` throws an error if you try. + +.alert.is-important + :marked + Do not configure the `TestBed` after calling `createComponent`. +:marked + ### The tests + Jasmine runs this `beforeEach` before each test of which there are two ++makeExample('testing/ts/app/banner.component.spec.ts', 'tests', 'app/banner.component.spec.ts (tests)')(format='.') +:markdown + These tests ask the `DebugElement` for the native HTML element to satisfy their expectations. + +a#fixture-detect-changes +:marked + ### _detectChanges_: Angular change detection under test + + Each test tells Angular when to perform change detection by calling `fixture.detectChanges()`. + The first test does so immediately, triggering data binding and propagation of the `title` property + to the DOM element. + + The second test changes the component's `title` property _and only then_ calls `fixture.detectChanges()`; + the new value appears in the DOM element. + + In production, change detection kicks in automatically + when Angular creates a component or the user enters a keystroke or + an asynchronous activity (e.g., AJAX) completes. + + The `TestBed.createComponent` does _not_ trigger change detection. + The fixture does not automatically push the component's `title` property value into the data bound element, + a fact demonstrated in the following test: ++makeExample('testing/ts/app/banner.component.spec.ts', 'test-w-o-detect-changes', 'app/banner.component.spec.ts (no detectChanges)')(format='.') +:marked + This behavior (or lack of it) is intentional. + It gives the tester an opportunity to investigate the state of + the component _before Angular initiates data binding or calls lifecycle hooks_. + +a#automatic-change-detection +:marked + ### Automatic change detection + Some testers prefer that the Angular test environment run change detection automatically. + That's possible by configuring the `TestBed` with the _AutoDetect_ provider: ++makeExample('testing/ts/app/banner.component.spec.ts', 'auto-detect', 'app/banner.component.spec.ts (AutoDetect)')(format='.') +:marked + Here are three tests that illustrate how _auto-detect_ works. ++makeExample('testing/ts/app/banner.component.spec.ts', 'auto-detect-tests', 'app/banner.component.spec.ts (AutoDetect Tests)')(format='.') +:marked + The first test shows the benefit of automatic change detection. + + The second and third test remind us that Angular does _not_ know about changes to component property + values unless Angular itself (or some asynchronous process) makes the change. + This is as true in production as it is in test. + + In production, external forces rarely change component properties like this, + whereas these kinds of probing changes are typical in unit tests. + The tester will have to call `fixture.detectChanges()` quite often + despite having opted into auto detect. + +.alert.is-helpful + :marked + Rather than wonder when the test fixture will or won't perform change detection, + the samples in this chapter _always call_ `detectChanges()` _explicitly_. + +a(href="#top").to-top Back to top + +.l-hr + +a#component-with-dependency +:marked + # Test a component with a dependency + Components often have service dependencies. + The `WelcomeComponent` displays a welcome message to the logged in user. + It knows who the user is based on a property of the injected `UserService`: ++makeExample('testing/ts/app/welcome.component.ts', '', 'app/welcome.component.ts')(format='.') +:marked + The `WelcomeComponent` has decision logic that interacts with the service; + such logic makes this component worth testing. + Here's the test module configuration for the spec file, `app/welcome.component.spec.ts`: ++makeExample('testing/ts/app/welcome.component.spec.ts', 'config-test-module', 'app/welcome.component.spec.ts')(format='.') +:marked + This time, in addition to declaring the component under test, + the configurations sets the `providers` list with the dependent `UserService`. + + This example configures the test module with a _fake_ `UserService`. + + ## Provide service fakes + + A component under test doesn't have to be injected with real services. + In fact, it is usually better if they are fakes. + The purpose of the spec is to test the component, not the service, + and real services can be trouble. + + Injecting the real `UserService` could be a nightmare. + The real service might try to ask the user for login credentials and + try to reach an authentication server. + These behaviors could be hard to intercept. + It is far easier to create and register a fake `UserService`. + + There are many ways to fake a service. + This test suit supplies a minimal `UserService` that satisfies the needs of the `WelcomeComponent` + and its tests: ++makeExample('testing/ts/app/welcome.component.spec.ts', 'fake-userservice')(format='.') + +a#injected-service-reference +:marked + ## Referencing injected services + The tests need access to the injected (fake) `UserService`. + + You cannot reference the `fakeUserService` object provided to the test module. + **It does not work!** + Surprisingly, the instance actually injected into the component is _not the same_ + as the provided `fakeUserService` object. + +.alert.is-important + :marked + Always use an injector to get a reference to an injected service. +:marked + Where do you get the injector? + Angular has an hierarchical injection system. + In a test there can be injectors at multiple levels. + The current `TestBed` injector creates a top-level injector. + The `WelcomeComponent` injector is a child of that injector created specifically for the component. + + You can get a `UserService` from the current `TestBed` injector by calling `TestBed.get`. ++makeExample('testing/ts/app/welcome.component.spec.ts', 'inject-from-testbed', 'TestBed injector')(format='.') +.l-sub-section + :marked + The [inject](#inject) function is another way to inject one or more services into a test. +:marked + That happens to work for testing the `WelcomeComponent` because the `UserService` instance from the `TestBed` + is the same as the `UserService` instance injected into the component. + + That won't always be the case. + Be absolutely sure to reference the service instance that the component is _actually receiving_, + Call `get` on the component's injector which is `fixture.debugElement.injector`: ++makeExample('testing/ts/app/welcome.component.spec.ts', 'injected-service', 'Component\'s injector')(format='.') +.alert.is-important + :marked + Use the component's own injector to get the component's injected service. +a#welcome-spec-setup +:marked + Here's the complete, preferred `beforeEach`: ++makeExample('testing/ts/app/welcome.component.spec.ts', 'setup', 'app/welcome.component.spec.ts')(format='.') +:marked + And here are some tests: ++makeExample('testing/ts/app/welcome.component.spec.ts', 'tests', 'app/welcome.component.spec.ts')(format='.') +:marked + The first is a sanity test; it confirms that the fake `UserService` is working. + The remaining tests confirm the logic of the component when the service returns different values. + The second test validates the effect of changing the user name. + The third test checks that the component displays the proper message when there is no logged-in user. + +a(href="#top").to-top Back to top + +.l-hr + +a#component-with-async-service +:marked + # Test a component with an async service + Many services return values asynchronously. + Most data services make an HTTP request to a remote server and the response is necessarily asynchronous. + + The "About" view in this sample displays Mark Twain quotes. + The `TwainComponent` handles the display, delegating the server request to the `TwainService`. + Both are in the `app/shared` folder because the author intends to display Twain quotes on other pages someday. + Here is the `TwainComponent`. ++makeExample('testing/ts/app/shared/twain.component.ts', 'component', 'app/shared/twain.component.ts')(format='.') +:marked + The `TwainService` implementation is irrelevant at this point. + It is sufficient to see within `ngOnInit` that `twainService.getQuote` returns a promise which means it is asynchronous. + + In general, tests should not make calls to remote servers. + They should fake such calls. The setup in this `app/shared/twain.component.spec.ts` shows one way to do that: ++makeExample('testing/ts/app/shared/twain.component.spec.ts', 'setup', 'app/shared/twain.component.spec.ts (setup)')(format='.') + +a#service-spy +:marked + ### Spying on the real service + + This setup is similar to the [`welcome.component.spec` setup](#welcome-spec-setup). + But instead of creating a fake service object, it injects the _real_ service (see the test module `providers`) and + replaces the critical `getQuote` method with a Jasmine spy. ++makeExample('testing/ts/app/shared/twain.component.spec.ts', 'spy')(format='.') +:marked + The spy is designed such that any call to `getQuote` receives an immediately resolved promise with a test quote. + The spy bypasses the actual `getQuote` method and therefore will not contact the server. + +.l-sub-section + :marked + Faking a service instance and spying on the real service are _both_ great options. + Pick the one that seems easiest for the current test suite. Don't be afraid to change your mind. +:marked + Here are the tests with commentary to follow: ++makeExample('testing/ts/app/shared/twain.component.spec.ts', 'tests', 'app/shared/twain.component.spec.ts (tests)') +:marked + ### Synchronous tests + The first two tests are synchronous. + Neither test can prove that a value from the service will be displayed. + + Thanks to the spy, the second test verifies that `getQuote` is called. + But the quote itself has not arrived, despite the fact that the spy returns a resolved promise. + + This test must wait at least one full turn of the JavaScript engine, a least one "tick", before the + value becomes available. By that time, the test runner has moved on to the next test in the suite. + + The test must become an "async test" ... like the third test + +a#async-fn-in-it +:marked + ## The _async_ function in _it_ + + Notice the `async` in the third test. ++makeExample('testing/ts/app/shared/twain.component.spec.ts', 'async-test', 'app/shared/twain.component.spec.ts (async test)')(format='.') +:marked + The `async` function is part of the _Angular TestBed_ feature set. + It _takes_ a parameterless function and _returns_ a parameterless function + which becomes the argument to the Jasmine `it` call. + + The body of the `async` argument looks much like the body of a normal `it` argument. + There is nothing obviously asynchronous about it. For example, it doesn't return a promise. + + The `async` function arranges for the tester's code to run in a special _async test zone_ + that almost hides the mechanics of asynchronous execution. + + Almost but not completely. + +a#when-stable +:marked + ## _whenStable_ + The test must wait for the `getQuote` promise to resolve. + + The `getQuote` promise promise resolves in the next turn of the JavaScript engine, thanks to the spy. + But a different test implementation of `getQuote` could take longer. + An integration test might call the _real_ `getQuote`, resulting in an XHR request + that took many seconds to respond. + + This test has no direct access to the promise returned by the call to `testService.getQuote` + which is private and inaccessible inside `TwainComponent`. + + Fortunately, the `getQuote` promise is accessible to the _async test zone_ + which intercepts all promises issued within the _async_ method call. + + The `ComponentFixture.whenStable` method returns its own promise which resolves when the `getQuote` promise completes. + In fact, the _whenStable_ promise resolves when _all pending asynchronous activities_ complete ... the definition of "stable". + + Then the testing continues. + The test kicks off another round of change detection (`fixture.detechChanges`) which tells Angular to update the DOM with the quote. + The `getQuote` helper method extracts the display element text and the expectation confirms that the text matches the test quote. + +a#fakeAsync +a#fake-async +:marked + ## The _fakeAsync_ function + + The fourth test verifies the same component behavior in a different way. ++makeExample('testing/ts/app/shared/twain.component.spec.ts', 'fake-async-test', 'app/shared/twain.component.spec.ts (fakeAsync test)')(format='.') +:marked + Notice that `fakeAsync` replaces `async` as the `it` argument. + The `fakeAsync` function is also part of the _Angular TestBed_ feature set. + Like `async`, it too _takes_ a parameterless function and _returns_ a parameterless function + which becomes the argument to the Jasmine `it` call. + + The `async` function arranges for the tester's code to run in a special _fakeAsync test zone_. + + The key advantage of `fakeAsync` is that the test body looks entirely synchronous. + There are no promises at all. + No `then(...)` chains to disrupt the visible flow of control. + +.l-sub-section + :marked + There are limitations. For example, you cannot make an XHR call from within a `fakeAsync`. +:marked + +a#tick +a#tick-first-look +:marked + ## The _tick_ function + Compare the third and fourth tests. Notice that `fixture.whenStable` is gone, replaced by `tick()`. + + The `tick` function is a part of the _Angular TestBed_ feature set and a companion to `fakeAsync`. + It can only be called within a `fakeAsync` body. + + Calling `tick()` simulates the passage of time until all pending asynchronous activities complete, + including the resolution of the `getQuote` promise in this test case. + + It returns nothing. There is no promise to wait for. + Proceed with the same test code as formerly appeared within the `whenStable.then()` callback. + + Even this simple example is easier to read than the third test. + To more fully appreciate the improvement, imagine a succession of asynchronous operations, + chained in a long sequence of promise callbacks. + +a#jasmine-done +:marked + ## _jasmine.done_ + + While `fakeAsync` and even `async` function greatly simplify Angular asynchronous testing, + you can still fallback to the traditional Jasmine asynchronous testing technique. + + You can still pass `it` a function that takes a + [`done` callback](http://jasmine.github.io/2.0/introduction.html#section-Asynchronous_Support). + Now you are responsible for chaining promises, handling errors, and calling `done` at the appropriate moment. + + Here is a `done` version of the previous two tests: ++makeExample('testing/ts/app/shared/twain.component.spec.ts', 'done-test', 'app/shared/twain.component.spec.ts (done test)')(format='.') +:marked + Although we have no direct access to the `getQuote` promise inside `TwainComponent`, + the spy does and that makes it possible to wait for `getQuote` to finish. + + The `jasmine.done` technique, while discouraged, may become necessary when neither `async` nor `fakeAsync` + can tolerate a particular asynchronous activity. That's rare but it happens. + +a(href="#top").to-top Back to top + +.l-hr + +a#component-with-external-template +:marked + # Test a component with an external template + The `TestBed.createComponent` is a synchronous method. + It assumes that everything it could need is already in memory. + + That has been true so far. + Each tested component's `@Component` metadata has a `template` property specifying an _inline templates_. + Neither component had a `styleUrls` property. + Everything necessary to compile them was in memory at test runtime. + + The `DashboardHeroComponent` is different. + It has an external template and external css file, specified in `templateUrl` and `styleUrls` properties. ++makeExample('testing/ts/app/dashboard/dashboard-hero.component.ts', 'component', 'app/dashboard/dashboard-hero.component.ts (component)')(format='.') +:marked + The compiler must read these files from a file system before it can create a component instance. + + The `TestBed.compileComponents` method asynchronously compiles all the components configured in its + current test module. After it completes, external templates and css files, have been "inlined" + and `TestBed.createComponent` can do its job synchronously. +.l-sub-section + :marked + WebPack developers need not call `compileComponents` because it inlines templates and css + as part of the automated build process that precedes running the test. +:marked + The `app/dashboard/dashboard-hero.component.spec.ts` demonstrates the pre-compilation process: ++makeExample('testing/ts/app/dashboard/dashboard-hero.component.spec.ts', 'compile-components', 'app/dashboard/dashboard-hero.component.spec.ts (compileComponents)')(format='.') + +a#async-fn-in-before-each +:marked + ## The _async_ function in _beforeEach_ + + Notice the `async` call in the `beforeEach`. + + The `async` function is part of the _Angular TestBed_ feature set. + It _takes_ a parameterless function and _returns_ a parameterless function + which becomes the argument to the Jasmine `beforeEach` call. + + The body of the `async` argument looks much like the body of a normal `beforEach` argument. + There is nothing obviously asynchronous about it. For example, it doesn't return a promise. + + The `async` function arranges for the tester's code to run in a special _async test zone_ + that hides the mechanics of asynchronous execution. + +a#compile-components +:marked + ## _compileComponents_ + In this example, `Testbed.compileComponents` compiles one component, the `DashboardComponent`. + It's the only declared component in this test module. + + Tests later in this chapter have more declared components and some of them import application + modules that declare yet more components. + Some or all of these components could have external templates and css files. + `TestBed.compileComponents` compiles them all asynchonously at one time. + + The `compileComponents` method returns a promise so you can perform additional tasks _after_ it finishes. + + ### _compileComponents_ closes configuration + After `compileComponents` runs, the current `TestBed` instance is closed to further configuration. + You cannot call any more `TestBed` configuration methods, not `configureTestModule` + nor any of the `override...` methods. The `TestBed` throws an error if you try. + +.alert.is-important + :marked + Do not configure the `TestBed` after calling `compileComponents`. + Make `compileComponents` the last step + before calling `TestBed.createInstance` to instantiate the test component. +:marked + The `DashboardHeroComponent` spec follows the asynchonous `beforeEach` with a + _synchronous_ `beforeEach` that completes the setup steps and runs tests ... as described in the next section. + +.l-hr + +a#component-with-inputs-outputs +:marked + # Test a component with inputs and outputs + A component with inputs and outputs typically appears inside the view template of a host component. + The host uses a property binding to set the input property and uses an event binding to + listen to events raised by the output property. + + The testing goal is to verify that such bindings work as expected. + The tests should set input values and listen for output events. + + The `DashboardHeroComponent` is tiny example of a component in this role. + It displays an individual heroe provided by the `DashboardComponent`. + Clicking that hero tells the the `DashboardComponent` that the user has selected the hero. + + The `DashboardHeroComponent` is embedded in the `DashboardComponent` template like this: ++makeExample('testing/ts/app/dashboard/dashboard.component.html', 'dashboard-hero', 'app/dashboard/dashboard.component.html (excerpt)')(format='.') +:marked + The `DashboardHeroComponent` appears in an `*ngFor` repeater which sets each component's `hero` input property + to the iteration value and listens for the components `selected` event. + + Here's the component's definition again: ++makeExample('testing/ts/app/dashboard/dashboard-hero.component.ts', 'component', 'app/dashboard/dashboard-hero.component.ts (component)')(format='.') +:marked + While testing a component this simple has little intrinsic value, it's worth knowing how. + Three approaches come to mind: + 1. Test it as used by `DashboardComponent` + 1. Test it as a stand-alone component + 1. Test it as used by a substitute for `DashboardComponent` + + A quick look at the `DashboardComponent` constructor discourages the first approach: ++makeExample('testing/ts/app/dashboard/dashboard.component.ts', 'ctor', 'app/dashboard/dashboard.component.ts (constructor)')(format='.') +:marked + The `DashboardComponent` depends upon the Angular router and the `HeroService`. + You'd probably have to fake them both and that's a lot of work. The router is particularly challenging (see below). + + The immediate goal is to test the `DashboardHeroComponent`, not the `DashboardComponent`, and there's no need + to work hard unnecessarily. Let's try the second and third options. + + ## Test _DashboardHeroComponent_ stand-alone + + Here's the spec file setup. ++makeExample('testing/ts/app/dashboard/dashboard-hero.component.spec.ts', 'setup', 'app/dashboard/dashboard-hero.component.spec.ts (setup)')(format='.') + +:marked + The async `beforeEach` was discussed [above](#component-with-external-template). + Having compiled the components asynchronously with `compileComponents`, the rest of the setup + proceeds _synchronously_ in a _second_ `beforeEach`, using the basic techniques described [earlier](#simple-component-test). + + Note how the setup code assigns a test hero (`expectedHero`) to the component's `hero` property, emulating + the way the `DashboardComponent` would set it via the property binding in its repeater. + + The first test follows: ++makeExample('testing/ts/app/dashboard/dashboard-hero.component.spec.ts', 'name-test', 'app/dashboard/dashboard-hero.component.spec.ts (name test)')(format='.') +:marked + It verifies that the hero name is propagated through to template with a binding. + There's a twist. The template passes the hero name through the Angular `UpperCasePipe` so the + test must match the element value with the uppercased name: ++makeExample('testing/ts/app/dashboard/dashboard-hero.component.html')(format='.') +:marked +.alert.is-helpful + :marked + This small test demonstrates how Angular tests can verify a component's visual representation + — something not possible with [isolated unit tests](#isolated-component-tests) — + at low cost and without resorting to much slower and more complicated end-to-end tests. + +:marked + The second test verifies click behavior. Clicking the hero should rais a `selected` event that the + host component (`DashboardComponent` presumably) can hear: ++makeExample('testing/ts/app/dashboard/dashboard-hero.component.spec.ts', 'click-test', 'app/dashboard/dashboard-hero.component.spec.ts (click test)')(format='.') +:marked + The component exposes an `EventEmitter` property. The test subscribes to it just as the host component would do. + + The Angular `DebugElement.triggerEventHandler` lets the test raise _any data-bound event_. + In this example, the component's template binds to the hero `
`. + + The test has a reference to that `
` in `heroEl` so triggering the `heroEl` click event should cause Angular + to call `DashboardHeroComponent.click`. + + If the component behaves as expected, its `selected` property should emit the `hero` object, + the test detects that emission through its subscription, and the test will pass. + +.l-hr + +a#component-inside-test-host +:marked + # Test a component inside a test host component + + In the previous approach the tests themselves played the role of the host `DashboardComponent`. + A nagging suspicion remains. + Will the `DashboardHeroComponent` work properly when properly data-bound to a host component? + + Testing with the actual `DashboardComponent` host is doable but seems more trouble than its worth. + It's easier to emulate the `DashboardComponent` host with a _test host_ like this one: ++makeExample('testing/ts/app/dashboard/dashboard-hero.component.spec.ts', 'test-host', 'app/dashboard/dashboard-hero.component.spec.ts (test host)')(format='.') +:marked + The test host binds to `DashboardHeroComponent` as the `DashboardComponent` would but without + the distraction of the `Router`, the `HeroService` or even the `*ngFor` repeater. + + The test host sets the component's `hero` input property with its test hero. + It binds the component's `selected` event with its `onSelected` handler that records the emitted hero + in its `selectedHero` property. Later the tests check that property to verify that the + `DashboardHeroComponent.selected` event really did emit the right hero. + + The setup for the test-host tests is similar to the setup for the stand-alone tests: ++makeExample('testing/ts/app/dashboard/dashboard-hero.component.spec.ts', 'test-host-setup', 'app/dashboard/dashboard-hero.component.spec.ts (test host setup)')(format='.') +:marked + This test module configuration shows two important differences: + 1. It _declares_ both the `DashboardHeroComponent` and the `TestHostComponent`. + 1. It _creates_ the `TestHostComponent` instead of the `DashboardHeroComponent`. + + The `fixture` returned by `createComponent` holds an instance of `TestHostComponent` instead of an instance of `DashboardHeroComponent`. + + Of course creating the `TestHostComponent` has the side-effect of creating a `DashboardHeroComponent` + because the latter appears within the template of the former. + The query for the hero element (`heroEl`) still finds it in the test DOM + albeit at greater depth in the element tree than before. + + The tests themselves are almost identical to the stand-alone version ++makeExample('testing/ts/app/dashboard/dashboard-hero.component.spec.ts', 'test-host-tests', 'app/dashboard/dashboard-hero.component.spec.ts (test-host)')(format='.') +:marked + Only the selected event test differs. It confirms that the selected `DashboardHeroComponent` hero + really does find its way up through the event binding to the host component. + +a(href="#top").to-top Back to top + +.l-hr + +a#routed-component +:marked + # Test a routed component + + Testing the actual `DashboardComponent` seemed daunting because it injects the `Router`. ++makeExample('testing/ts/app/dashboard/dashboard.component.ts', 'ctor', 'app/dashboard/dashboard.component.ts (constructor)')(format='.') +:marked + It also injects the `HeroService` but faking that is a [familiar story](#component-with-async-servic). + The `Router` has a complicated API and is entwined with other services and application pre-conditions. + + Fortunately, the `DashboardComponent` isn't doing much with the `Router` ++makeExample('testing/ts/app/dashboard/dashboard.component.ts', 'goto-detail', 'app/dashboard/dashboard.component.ts (goToDetail)')(format='.') +:marked + This is often the case. + As a rule you test the component, not the router, + and care only if the component navigates with the right address under the given conditions. + Faking the router is an easy option. This should do the trick: ++makeExample('testing/ts/app/dashboard/dashboard.component.spec.ts', 'fake-router', 'app/dashboard/dashboard.component.spec.ts (fakeRouter)')(format='.') +:marked + Now we setup the test module with the `fakeRouter` and a fake `HeroService` and + create a test instance of the `DashbaordComponent` for subsequent testing. ++makeExample('testing/ts/app/dashboard/dashboard.component.spec.ts', 'compile-and-create-body', 'app/dashboard/dashboard.component.spec.ts (compile and create)')(format='.') +:marked + The following test clicks the displayed hero and confirms (with the help of a spy) that `Router.navigateByUrl` is called with the expected url. ++makeExample('testing/ts/app/dashboard/dashboard.component.spec.ts', 'navigate-test', 'app/dashboard/dashboard.component.spec.ts (navigate test)')(format='.') + +a#inject +:marked + ## The _inject_ function + + Notice the `inject` function in the second `it` argument. ++makeExample('testing/ts/app/dashboard/dashboard.component.spec.ts', 'inject')(format='.') +:marked + The `inject` function is part of the _Angular TestBed_ feature set. + It injects services into the test function where you can alter, spy on, and manipulate them. + + The `inject` function has two parameters + 1. an array of Angular dependency injection tokens + 1. a test function whose parameters correspond exactly to each item in the injection token array + +.callout.is-important + header inject uses the TestBed Injector + :marked + The `inject` function uses the current `TestBed` injector and can only return services provided at that level. + It does not return services from component providers. + +:marked + This example injects the `Router` from the current `TestBed` injector. + That's fine for this test because the `Router` is (and must be) provided by the application root injector. + + If you need a service provided by the component's _own_ injector, call `fixture.debugElement.injector.get` instead: ++makeExample('testing/ts/app/welcome.component.spec.ts', 'injected-service', 'Component\'s injector')(format='.') +.alert.is-important + :marked + Use the component's own injector to get the service actually injected into the component. + +:marked + The `inject` function closes the current `TestBed` instance to further configuration. + You cannot call any more `TestBed` configuration methods, not `configureTestModule` + nor any of the `override...` methods. The `TestBed` throws an error if you try. + +.alert.is-important + :marked + Do not configure the `TestBed` after calling `inject`. +a(href="#top").to-top Back to top + +.l-hr + +a#isolated-tests +a#testing-without-atp +:marked + # Testing without the Angular Testing Platform + + Testing applications with the help of the Angular Testing Platform (ATP) is the main focus of this chapter. + + However, it's often more productive to explore the inner logic of application classes + with _isolated_ unit tests that don't use the ATP. + Such tests are often smaller, easier to read, + and easier to write and maintain. + + They don't + * import from the Angular test libraries + * configure a module + * prepare dependency injection `providers` + * call `inject` or `async` or `fakeAsync` + + They do + * exhibit standard, Angular-agnostic testing techniques + * create instances directly with `new` + * use stubs, spys, and mocks to fake dependencies. + +.callout.is-important + header Write both kinds of tests + :marked + Good developers write both kinds of tests for the same application part, often in the same spec file. + Write simple _isolated_ unit tests to validate the part in isolation. + Write _Angular_ tests to validate the part as it interacts with Angular, + updates the DOM, and collaborates with the rest of the application. + +:marked + ## Services + Services are good candidates for vanilla unit testing. + Here are some synchronous and asynchronous unit tests of the `FancyService` + written without assistance from Angular Testing Platform. + ++makeExample('testing/ts/app/bag/bag.no-testbed.spec.ts', 'FancyService', 'app/bag/bag.no-testbed.spec.ts') +:marked + A rough line count suggests that these tests are about 25% smaller than equivalent ATP tests. + That's telling but not decisive. + The benefit comes from reduced setup and code complexity. + + Compare these equivalent tests of `FancyService.getTimeoutValue`. ++makeTabs( + `testing/ts/app/bag/bag.no-testbed.spec.ts, testing/ts/app/bag/bag.spec.ts`, + 'getTimeoutValue, getTimeoutValue', + `app/bag/bag.no-testbed.spec.ts, app/bag/bag.spec.ts (with ATP)`) +:marked + They have about the same line-count. + The ATP version has more moving parts, including a couple of helper functions (`async` and `inject`). + Both work and it's not much of an issue if you're using the Angular Testing Platform nearby for other reasons. + On the other hand, why burden simple service tests with ATP complexity? + + Pick the approach that suits you. + + ### Services with dependencies + + Services often depend on other services that Angular injects into the constructor. + You can test these services _without_ the testbed. + In many cases, it's easier to create and _inject_ dependencies by hand. + + The `DependentService` is a simple example ++makeExample('testing/ts/app/bag/bag.ts', 'DependentService', 'app/bag/bag.ts')(format='.') +:marked + It delegates it's only method, `getValue`, to the injected `FancyService`. + + Here are several ways to test it. ++makeExample('testing/ts/app/bag/bag.no-testbed.spec.ts', 'DependentService', 'app/bag/bag.no-testbed.spec.ts') +:marked + The first test creates a `FancyService` with `new` and passes it to the `DependentService` constructor. + + It's rarely that simple. The injected service can be difficult to create or control. + You can mock the dependency, or use a fake value, or stub the pertinent service method + with a substitute method that is easy to control. + + These _isolated_ unit testing techniques are great for exploring the inner logic of a service or its + simple integration with a component class. + Use the Angular Testing Platform when writing tests that validate how a service interacts with components + _within the Angular runtime environment_. + + ## Pipes + Pipes are easy to test without the Angular Testing Platform (ATP). + + A pipe class has one method, `transform`, that turns an input to an output. + The `transform` implementation rarely interacts with the DOM. + Most pipes have no dependence on Angular other than the `@Pipe` + metadata and an interface. + + Consider a `TitleCasePipe` that capitalizes the first letter of each word. + Here's a naive implementation implemented with a regular expression. ++makeExample('testing/ts/app/shared/title-case.pipe.ts', '', 'app/shared/title-case.pipe.ts')(format='.') +:marked + Anything that uses a regular expression is worth testing thoroughly. + Use simple Jasmine to explore the expected cases and the edge cases. ++makeExample('testing/ts/app/shared/title-case.pipe.spec.ts', 'excerpt', 'app/shared/title-case.pipe.spec.ts') +:marked + ## Write ATP tests too + These are tests of the pipe _in isolation_. + They can't tell if the `TitleCasePipe` is working properly + as applied in the application components. + + Consider adding ATP component tests such as this one. ++makeExample('testing/ts/app/hero/hero-detail.component.spec.ts', 'title-case-pipe', 'app/hero/hero-detail.component.spec.ts (pipe test)') + +a#isolated-component-tests +:marked + ## Components + + Component tests typically examine how a component class interacts with its own template or with collaborating components. + The Angular Testing Platform is specifically designed to facilitate such tests. + + Consider this `ButtonComp` component. ++makeExample('testing/ts/app/bag/bag.ts', 'ButtonComp', 'app/bag/bag.ts (ButtonComp)')(format='.') +:marked + The following ATP test demonstrates that clicking a button in the template leads + to an update of the on-screen message. ++makeExample('testing/ts/app/bag/bag.spec.ts', 'ButtonComp', 'app/bag/bag.spec.ts (ButtonComp)')(format='.') +:marked + The assertions verify the data binding flow from one HTML control (the ` - - - - - - - - -
-
- Name via form.controls = {{showFormControls(heroForm)}} -
- - - -
- - -
-

You submitted the following:

-
-
Name
-
{{ model.name }}
-
-
-
Alter Ego
-
{{ model.alterEgo }}
-
-
-
Power
-
{{ model.power }}
-
-
- -
- -
- - - -
-
- - - - -
-
- - - -
- -
- -
-

Hero Form

-
-
- - -
- -
- - -
- - - -
- - -
- - - - - -
-
- - - - -
- -
-

Hero Form

-
- - {{diagnostic}} -
- - -
- -
- - -
- -
- - -
- - - - -
-
- - - -
- - - TODO: remove this: {{model.name}} - -
- - - TODO: remove this: {{model.name}} - -
- -
- - - - - -
- - -
TODO: remove this: {{spy.className}} - -
- -
diff --git a/public/docs/_examples/forms-deprecated/ts/app/hero-form.component.ts b/public/docs/_examples/forms-deprecated/ts/app/hero-form.component.ts deleted file mode 100644 index 7ea7c44738..0000000000 --- a/public/docs/_examples/forms-deprecated/ts/app/hero-form.component.ts +++ /dev/null @@ -1,66 +0,0 @@ -// #docplaster -// #docregion -// #docregion first, final -import { Component } from '@angular/core'; -import { NgForm } from '@angular/common'; - -import { Hero } from './hero'; - -@Component({ - selector: 'hero-form', - templateUrl: 'app/hero-form.component.html' -}) -export class HeroFormComponent { - - powers = ['Really Smart', 'Super Flexible', - 'Super Hot', 'Weather Changer']; - - model = new Hero(18, 'Dr IQ', this.powers[0], 'Chuck Overstreet'); - - // #docregion submitted - submitted = false; - - onSubmit() { this.submitted = true; } - // #enddocregion submitted - - // #enddocregion final - // TODO: Remove this when we're done - get diagnostic() { return JSON.stringify(this.model); } - // #enddocregion first - - // #docregion final - // Reset the form with a new hero AND restore 'pristine' class state - // by toggling 'active' flag which causes the form - // to be removed/re-added in a tick via NgIf - // TODO: Workaround until NgForm has a reset method (#6822) - // #docregion new-hero - active = true; - - // #docregion new-hero-v1 - newHero() { - this.model = new Hero(42, '', ''); - // #enddocregion new-hero-v1 - this.active = false; - setTimeout(() => this.active = true, 0); - // #docregion new-hero-v1 - } - // #enddocregion new-hero-v1 - // #enddocregion new-hero - // #enddocregion final - //////// NOT SHOWN IN DOCS //////// - - // Reveal in html: - // Name via form.controls = {{showFormControls(heroForm)}} - showFormControls(form: NgForm) { - - return form && form.controls['name'] && - // #docregion form-controls - form.controls['name'].value; // Dr. IQ - // #enddocregion form-controls - } - - ///////////////////////////// - - // #docregion first, final -} -// #enddocregion first, final diff --git a/public/docs/_examples/forms-deprecated/ts/app/hero.ts b/public/docs/_examples/forms-deprecated/ts/app/hero.ts deleted file mode 100644 index c128626452..0000000000 --- a/public/docs/_examples/forms-deprecated/ts/app/hero.ts +++ /dev/null @@ -1,11 +0,0 @@ -// #docregion -export class Hero { - - constructor( - public id: number, - public name: string, - public power: string, - public alterEgo?: string - ) { } - -} diff --git a/public/docs/_examples/forms-deprecated/ts/app/main.ts b/public/docs/_examples/forms-deprecated/ts/app/main.ts deleted file mode 100644 index 5338161d66..0000000000 --- a/public/docs/_examples/forms-deprecated/ts/app/main.ts +++ /dev/null @@ -1,6 +0,0 @@ -// #docregion -import { bootstrap } from '@angular/platform-browser-dynamic'; - -import { AppComponent } from './app.component'; - -bootstrap(AppComponent); diff --git a/public/docs/_examples/forms-deprecated/ts/example-config.json b/public/docs/_examples/forms-deprecated/ts/example-config.json deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/public/docs/_examples/forms-deprecated/ts/forms.css b/public/docs/_examples/forms-deprecated/ts/forms.css deleted file mode 100644 index d7e11405b1..0000000000 --- a/public/docs/_examples/forms-deprecated/ts/forms.css +++ /dev/null @@ -1,9 +0,0 @@ -/* #docregion */ -.ng-valid[required] { - border-left: 5px solid #42A948; /* green */ -} - -.ng-invalid { - border-left: 5px solid #a94442; /* red */ -} -/* #enddocregion */ \ No newline at end of file diff --git a/public/docs/_examples/forms-deprecated/ts/index.html b/public/docs/_examples/forms-deprecated/ts/index.html deleted file mode 100644 index 4df2d32d46..0000000000 --- a/public/docs/_examples/forms-deprecated/ts/index.html +++ /dev/null @@ -1,34 +0,0 @@ - - - - - Hero Form - - - - - - - - - - - - - - - - - - - - - - - - Loading... - - - diff --git a/public/docs/_examples/forms-deprecated/ts/plnkr.json b/public/docs/_examples/forms-deprecated/ts/plnkr.json deleted file mode 100644 index c813933f8e..0000000000 --- a/public/docs/_examples/forms-deprecated/ts/plnkr.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "description": "Forms-Deprecated", - "files":[ - "!**/*.d.ts", - "!**/*.js" - ] -} \ No newline at end of file diff --git a/public/docs/_examples/router-deprecated/e2e-spec.ts.disabled b/public/docs/_examples/router-deprecated/e2e-spec.ts.disabled deleted file mode 100644 index 3892285845..0000000000 --- a/public/docs/_examples/router-deprecated/e2e-spec.ts.disabled +++ /dev/null @@ -1,127 +0,0 @@ -/// -'use strict'; -describe('Router', function () { - - beforeAll(function () { - browser.get(''); - }); - - function getPageStruct() { - let hrefEles = element.all(by.css('my-app a')); - - return { - hrefs: hrefEles, - routerParent: element(by.css('my-app > undefined')), - routerTitle: element(by.css('my-app > undefined > h2')), - - crisisHref: hrefEles.get(0), - crisisList: element.all(by.css('my-app > undefined > undefined li')), - crisisDetail: element(by.css('my-app > undefined > undefined > div')), - crisisDetailTitle: element(by.css('my-app > undefined > undefined > div > h3')), - - heroesHref: hrefEles.get(1), - heroesList: element.all(by.css('my-app > undefined li')), - heroDetail: element(by.css('my-app > undefined > div')), - heroDetailTitle: element(by.css('my-app > undefined > div > h3')), - - }; - } - - it('should be able to see the start screen', function () { - let page = getPageStruct(); - expect(page.hrefs.count()).toEqual(2, 'should be two dashboard choices'); - expect(page.crisisHref.getText()).toEqual('Crisis Center'); - expect(page.heroesHref.getText()).toEqual('Heroes'); - }); - - it('should be able to see crises center items', function () { - let page = getPageStruct(); - expect(page.crisisList.count()).toBe(4, 'should be 4 crisis center entries at start'); - }); - - it('should be able to see hero items', function () { - let page = getPageStruct(); - page.heroesHref.click().then(function() { - expect(page.routerTitle.getText()).toContain('HEROES'); - expect(page.heroesList.count()).toBe(6, 'should be 6 heroes'); - }); - }); - - it('should be able to toggle the views', function () { - let page = getPageStruct(); - page.crisisHref.click().then(function() { - expect(page.crisisList.count()).toBe(4, 'should be 4 crisis center entries'); - return page.heroesHref.click(); - }).then(function() { - expect(page.heroesList.count()).toBe(6, 'should be 6 heroes'); - }); - }); - - it('should be able to edit and save details from the crisis center view', function () { - crisisCenterEdit(2, true); - }); - - it('should be able to edit and cancel details from the crisis center view', function () { - crisisCenterEdit(3, false); - }); - - it('should be able to edit and save details from the heroes view', function () { - let page = getPageStruct(); - let heroEle: protractor.ElementFinder; - let heroText: string; - page.heroesHref.click().then(function() { - heroEle = page.heroesList.get(4); - return heroEle.getText(); - }).then(function(text) { - expect(text.length).toBeGreaterThan(0, 'should have some text'); - // remove leading id from text - heroText = text.substr(text.indexOf(' ')).trim(); - return heroEle.click(); - }).then(function() { - expect(page.heroesList.count()).toBe(0, 'should no longer see crisis center entries'); - expect(page.heroDetail.isPresent()).toBe(true, 'should be able to see crisis detail'); - expect(page.heroDetailTitle.getText()).toContain(heroText); - let inputEle = page.heroDetail.element(by.css('input')); - return sendKeys(inputEle, '-foo'); - }).then(function() { - expect(page.heroDetailTitle.getText()).toContain(heroText + '-foo'); - let buttonEle = page.heroDetail.element(by.css('button')); - return buttonEle.click(); - }).then(function() { - expect(heroEle.getText()).toContain(heroText + '-foo'); - }); - }); - - function crisisCenterEdit(index: number, shouldSave: boolean) { - let page = getPageStruct(); - let crisisEle: protractor.ElementFinder; - let crisisText: string; - page.crisisHref.click() - .then(function () { - crisisEle = page.crisisList.get(index); - return crisisEle.getText(); - }).then(function (text) { - expect(text.length).toBeGreaterThan(0, 'should have some text'); - // remove leading id from text - crisisText = text.substr(text.indexOf(' ')).trim(); - return crisisEle.click(); - }).then(function () { - expect(page.crisisList.count()).toBe(0, 'should no longer see crisis center entries'); - expect(page.crisisDetail.isPresent()).toBe(true, 'should be able to see crisis detail'); - expect(page.crisisDetailTitle.getText()).toContain(crisisText); - let inputEle = page.crisisDetail.element(by.css('input')); - return sendKeys(inputEle, '-foo'); - }).then(function () { - expect(page.crisisDetailTitle.getText()).toContain(crisisText + '-foo'); - let buttonEle = page.crisisDetail.element(by.cssContainingText('button', shouldSave ? 'Save' : 'Cancel')); - return buttonEle.click(); - }).then(function () { - if (shouldSave) { - expect(crisisEle.getText()).toContain(crisisText + '-foo'); - } else { - expect(crisisEle.getText()).not.toContain(crisisText + '-foo'); - } - }); - } - -}); diff --git a/public/docs/_examples/router-deprecated/ts/app/app.component.1.ts b/public/docs/_examples/router-deprecated/ts/app/app.component.1.ts deleted file mode 100644 index 0e20623fd3..0000000000 --- a/public/docs/_examples/router-deprecated/ts/app/app.component.1.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* First version */ -// #docplaster - -// #docregion -import { Component } from '@angular/core'; -// #docregion import-router -import { RouteConfig, ROUTER_DIRECTIVES } from '@angular/router-deprecated'; -// #enddocregion import-router - -import { CrisisListComponent } from './crisis-list.component'; -import { HeroListComponent } from './hero-list.component'; - -@Component({ - selector: 'my-app', -// #docregion template - template: ` -

Component Router (Deprecated)

-
- - `, -// #enddocregion template - directives: [ROUTER_DIRECTIVES] -}) -// #enddocregion -/* -// #docregion route-config -@Component({ ... }) -// #enddocregion route-config -*/ -// #docregion -// #docregion route-config -@RouteConfig([ -// #docregion route-defs - {path: '/crisis-center', name: 'CrisisCenter', component: CrisisListComponent}, - {path: '/heroes', name: 'Heroes', component: HeroListComponent} -// #enddocregion route-defs -]) -export class AppComponent { } -// #enddocregion route-config -// #enddocregion diff --git a/public/docs/_examples/router-deprecated/ts/app/app.component.2.ts b/public/docs/_examples/router-deprecated/ts/app/app.component.2.ts deleted file mode 100644 index e4685ff418..0000000000 --- a/public/docs/_examples/router-deprecated/ts/app/app.component.2.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* Second Heroes version */ -// #docplaster - -// #docregion -import { Component } from '@angular/core'; -import { RouteConfig, ROUTER_DIRECTIVES } from '@angular/router-deprecated'; - -import { CrisisListComponent } from './crisis-list.component'; -// #enddocregion -/* -// Apparent Milestone 2 imports -// #docregion -// #docregion hero-import -import { HeroListComponent } from './heroes/hero-list.component'; -import { HeroDetailComponent } from './heroes/hero-detail.component'; -import { HeroService } from './heroes/hero.service'; -// #enddocregion hero-import -// #enddocregion -*/ -// Actual Milestone 2 imports -import { HeroListComponent } from './heroes/hero-list.component.1'; -import { HeroDetailComponent } from './heroes/hero-detail.component.1'; -import { HeroService } from './heroes/hero.service'; -// #docregion - -@Component({ - selector: 'my-app', - template: ` -

Component Router (Deprecated)

- - - `, - providers: [HeroService], - directives: [ROUTER_DIRECTIVES] -}) -// #enddocregion -/* -// #docregion route-config -@Component({ ... }) -// #enddocregion route-config -*/ -// #docregion -// #docregion route-config -@RouteConfig([ -// #docregion route-defs - {path: '/crisis-center', name: 'CrisisCenter', component: CrisisListComponent}, - {path: '/heroes', name: 'Heroes', component: HeroListComponent}, - // #docregion hero-detail-route - {path: '/hero/:id', name: 'HeroDetail', component: HeroDetailComponent} - // #enddocregion hero-detail-route -// #enddocregion route-defs -]) -export class AppComponent { } -// #enddocregion route-config -// #enddocregion diff --git a/public/docs/_examples/router-deprecated/ts/app/app.component.3.ts b/public/docs/_examples/router-deprecated/ts/app/app.component.3.ts deleted file mode 100644 index 68635e8aad..0000000000 --- a/public/docs/_examples/router-deprecated/ts/app/app.component.3.ts +++ /dev/null @@ -1,52 +0,0 @@ -// #docplaster -import { Component } from '@angular/core'; -import { RouteConfig, ROUTER_DIRECTIVES } from '@angular/router-deprecated'; - -import { CrisisCenterComponent } from './crisis-center/crisis-center.component.1'; -import { DialogService } from './dialog.service'; -import { HeroService } from './heroes/hero.service'; - -@Component({ - selector: 'my-app', -// #enddocregion - /* Typical link - // #docregion h-anchor - Heroes - // #enddocregion h-anchor - */ - /* Incomplete Crisis Center link when CC lacks a default - // #docregion cc-anchor-fail - // The link now fails with a "non-terminal link" error - // #docregion cc-anchor-w-default - Crisis Center - // #enddocregion cc-anchor-w-default - // #enddocregion cc-anchor-fail - */ - /* Crisis Center link when CC lacks a default - // #docregion cc-anchor-no-default - Crisis Center - // #enddocregion cc-anchor-no-default - */ - /* Crisis Center Detail link - // #docregion Dragon-anchor - Dragon Crisis - // #enddocregion Dragon-anchor - */ -// #docregion template - template: ` -

Component Router (Deprecated)

- - - `, -// #enddocregion template - providers: [DialogService, HeroService], - directives: [ROUTER_DIRECTIVES] -}) -@RouteConfig([ - {path: '/crisis-center/...', name: 'CrisisCenter', component: CrisisCenterComponent}, -]) -export class AppComponent { } diff --git a/public/docs/_examples/router-deprecated/ts/app/app.component.ts b/public/docs/_examples/router-deprecated/ts/app/app.component.ts deleted file mode 100644 index a6f784cda9..0000000000 --- a/public/docs/_examples/router-deprecated/ts/app/app.component.ts +++ /dev/null @@ -1,44 +0,0 @@ -// #docplaster -// #docregion -import { Component } from '@angular/core'; -import { RouteConfig, ROUTER_DIRECTIVES } from '@angular/router-deprecated'; - -import { CrisisCenterComponent } from './crisis-center/crisis-center.component'; -import { HeroListComponent } from './heroes/hero-list.component'; -import { HeroDetailComponent } from './heroes/hero-detail.component'; - -import { DialogService } from './dialog.service'; -import { HeroService } from './heroes/hero.service'; - -@Component({ - selector: 'my-app', -// #docregion template - template: ` -

Component Router (Deprecated)

- - - `, -// #enddocregion template - providers: [DialogService, HeroService], - directives: [ROUTER_DIRECTIVES] -}) -// #docregion route-config -@RouteConfig([ - - // #docregion route-config-cc - { // Crisis Center child route - path: '/crisis-center/...', - name: 'CrisisCenter', - component: CrisisCenterComponent, - useAsDefault: true - }, - // #enddocregion route-config-cc - - {path: '/heroes', name: 'Heroes', component: HeroListComponent}, - {path: '/hero/:id', name: 'HeroDetail', component: HeroDetailComponent}, -]) -// #enddocregion route-config -export class AppComponent { } diff --git a/public/docs/_examples/router-deprecated/ts/app/crisis-center/crisis-center.component.1.ts b/public/docs/_examples/router-deprecated/ts/app/crisis-center/crisis-center.component.1.ts deleted file mode 100644 index 6925fb8008..0000000000 --- a/public/docs/_examples/router-deprecated/ts/app/crisis-center/crisis-center.component.1.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Component } from '@angular/core'; -import { RouteConfig, RouterOutlet } from '@angular/router-deprecated'; - -import { CrisisListComponent } from './crisis-list.component.1'; -import { CrisisDetailComponent } from './crisis-detail.component.1'; -import { CrisisService } from './crisis.service'; - -// #docregion minus-imports -@Component({ - template: ` -

CRISIS CENTER

- - `, - directives: [RouterOutlet], -// #docregion providers - providers: [CrisisService] -// #enddocregion providers -}) -// #docregion route-config -@RouteConfig([ - // #docregion default-route - {path: '/', name: 'CrisisList', component: CrisisListComponent, useAsDefault: true}, - // #enddocregion default-route - {path: '/:id', name: 'CrisisDetail', component: CrisisDetailComponent} -]) -// #enddocregion route-config -export class CrisisCenterComponent { } -// #enddocregion minus-imports diff --git a/public/docs/_examples/router-deprecated/ts/app/crisis-center/crisis-center.component.ts b/public/docs/_examples/router-deprecated/ts/app/crisis-center/crisis-center.component.ts deleted file mode 100644 index 3c735ae6ae..0000000000 --- a/public/docs/_examples/router-deprecated/ts/app/crisis-center/crisis-center.component.ts +++ /dev/null @@ -1,22 +0,0 @@ -// #docregion -import { Component } from '@angular/core'; -import { RouteConfig, RouterOutlet } from '@angular/router-deprecated'; - -import { CrisisListComponent } from './crisis-list.component'; -import { CrisisDetailComponent } from './crisis-detail.component'; -import { CrisisService } from './crisis.service'; - -@Component({ - template: ` -

CRISIS CENTER

- - `, - directives: [RouterOutlet], - providers: [CrisisService] -}) -@RouteConfig([ - {path: '/', name: 'CrisisList', component: CrisisListComponent, useAsDefault: true}, - {path: '/:id', name: 'CrisisDetail', component: CrisisDetailComponent} -]) -export class CrisisCenterComponent { } -// #enddocregion diff --git a/public/docs/_examples/router-deprecated/ts/app/crisis-center/crisis-detail.component.1.ts b/public/docs/_examples/router-deprecated/ts/app/crisis-center/crisis-detail.component.1.ts deleted file mode 100644 index 0c683853c9..0000000000 --- a/public/docs/_examples/router-deprecated/ts/app/crisis-center/crisis-detail.component.1.ts +++ /dev/null @@ -1,95 +0,0 @@ -// #docplaster - -// #docregion -import { Component, OnInit } from '@angular/core'; -import { RouteParams, Router } from '@angular/router-deprecated'; -// #docregion routerCanDeactivate -import { CanDeactivate, ComponentInstruction } from '@angular/router-deprecated'; - -import { DialogService } from '../dialog.service'; - -// #enddocregion routerCanDeactivate -import { Crisis, CrisisService } from './crisis.service'; - -@Component({ - // #docregion template - template: ` -
-

"{{editName}}"

-
- {{crisis.id}}
-
- - -
-

- - -

-
- `, - // #enddocregion template - styles: ['input {width: 20em}'] -}) -// #docregion routerCanDeactivate, cancel-save -export class CrisisDetailComponent implements OnInit, CanDeactivate { - - crisis: Crisis; - editName: string; - -// #enddocregion routerCanDeactivate, cancel-save - constructor( - private service: CrisisService, - private router: Router, - private routeParams: RouteParams, - private dialog: DialogService - ) { } - - // #docregion ngOnInit - ngOnInit() { - let id = +this.routeParams.get('id'); - this.service.getCrisis(id).then(crisis => { - if (crisis) { - this.editName = crisis.name; - this.crisis = crisis; - } else { // id not found - this.gotoCrises(); - } - }); - } - // #enddocregion ngOnInit - - // #docregion routerCanDeactivate - routerCanDeactivate(next: ComponentInstruction, prev: ComponentInstruction): any { - // Allow synchronous navigation (`true`) if no crisis or the crisis is unchanged. - if (!this.crisis || this.crisis.name === this.editName) { - return true; - } - // Otherwise ask the user with the dialog service and return its - // promise which resolves to true or false when the user decides - return this.dialog.confirm('Discard changes?'); - } - // #enddocregion routerCanDeactivate - - // #docregion cancel-save - cancel() { - this.editName = this.crisis.name; - this.gotoCrises(); - } - - save() { - this.crisis.name = this.editName; - this.gotoCrises(); - } - // #enddocregion cancel-save - - // #docregion gotoCrises - gotoCrises() { - // Like Crisis Center -

"{{editName}}"

-
- {{crisis.id}}
-
- - -
-

- - -

-

- `, - styles: ['input {width: 20em}'] -}) - -export class CrisisDetailComponent implements OnInit, CanDeactivate { - - crisis: Crisis; - editName: string; - - constructor( - private service: CrisisService, - private router: Router, - private routeParams: RouteParams, - private _dialog: DialogService - ) { } - - ngOnInit() { - let id = +this.routeParams.get('id'); - this.service.getCrisis(id).then(crisis => { - if (crisis) { - this.editName = crisis.name; - this.crisis = crisis; - } else { // id not found - this.gotoCrises(); - } - }); - } - - routerCanDeactivate(next: ComponentInstruction, prev: ComponentInstruction): any { - // Allow synchronous navigation (`true`) if no crisis or the crisis is unchanged. - if (!this.crisis || this.crisis.name === this.editName) { - return true; - } - // Otherwise ask the user with the dialog service and return its - // promise which resolves to true or false when the user decides - return this._dialog.confirm('Discard changes?'); - } - - cancel() { - this.editName = this.crisis.name; - this.gotoCrises(); - } - - save() { - this.crisis.name = this.editName; - this.gotoCrises(); - } - - // #docregion gotoCrises - gotoCrises() { - let crisisId = this.crisis ? this.crisis.id : null; - // Pass along the hero id if available - // so that the CrisisListComponent can select that hero. - // Add a totally useless `foo` parameter for kicks. - // #docregion gotoCrises-navigate - this.router.navigate(['CrisisList', {id: crisisId, foo: 'foo'} ]); - // #enddocregion gotoCrises-navigate - } - // #enddocregion gotoCrises -} diff --git a/public/docs/_examples/router-deprecated/ts/app/crisis-center/crisis-list.component.1.ts b/public/docs/_examples/router-deprecated/ts/app/crisis-center/crisis-list.component.1.ts deleted file mode 100644 index 45121da69e..0000000000 --- a/public/docs/_examples/router-deprecated/ts/app/crisis-center/crisis-list.component.1.ts +++ /dev/null @@ -1,37 +0,0 @@ -// #docplaster - -// #docregion -import { Component, OnInit } from '@angular/core'; -import { Router } from '@angular/router-deprecated'; - -import { Crisis, CrisisService } from './crisis.service'; - -@Component({ - // #docregion template - template: ` -
    -
  • - {{crisis.id}} {{crisis.name}} -
  • -
- `, - // #enddocregion template -}) -export class CrisisListComponent implements OnInit { - crises: Crisis[]; - - constructor( - private service: CrisisService, - private router: Router) {} - - ngOnInit() { - this.service.getCrises().then(crises => this.crises = crises); - } - - // #docregion select - onSelect(crisis: Crisis) { - this.router.navigate(['CrisisDetail', { id: crisis.id }] ); - } - // #enddocregion select -} diff --git a/public/docs/_examples/router-deprecated/ts/app/crisis-center/crisis-list.component.ts b/public/docs/_examples/router-deprecated/ts/app/crisis-center/crisis-list.component.ts deleted file mode 100644 index a5770d256f..0000000000 --- a/public/docs/_examples/router-deprecated/ts/app/crisis-center/crisis-list.component.ts +++ /dev/null @@ -1,41 +0,0 @@ -// #docplaster - -// #docregion -import { Component, OnInit } from '@angular/core'; -import { RouteParams, Router } from '@angular/router-deprecated'; - -import { Crisis, CrisisService } from './crisis.service'; - -@Component({ - template: ` -
    -
  • - {{crisis.id}} {{crisis.name}} -
  • -
- `, -}) -export class CrisisListComponent implements OnInit { - crises: Crisis[]; - - private selectedId: number; - - constructor( - private service: CrisisService, - private router: Router, - routeParams: RouteParams) { - this.selectedId = +routeParams.get('id'); - } - - isSelected(crisis: Crisis) { return crisis.id === this.selectedId; } - - ngOnInit() { - this.service.getCrises().then(crises => this.crises = crises); - } - - onSelect(crisis: Crisis) { - this.router.navigate( ['CrisisDetail', { id: crisis.id }] ); - } -} diff --git a/public/docs/_examples/router-deprecated/ts/app/crisis-center/crisis.service.ts b/public/docs/_examples/router-deprecated/ts/app/crisis-center/crisis.service.ts deleted file mode 100644 index a847a15217..0000000000 --- a/public/docs/_examples/router-deprecated/ts/app/crisis-center/crisis.service.ts +++ /dev/null @@ -1,40 +0,0 @@ -// #docplaster -// #docregion -import { Injectable } from '@angular/core'; - -export class Crisis { - constructor(public id: number, public name: string) { } -} - -let crises = [ - new Crisis(1, 'Dragon Burning Cities'), - new Crisis(2, 'Sky Rains Great White Sharks'), - new Crisis(3, 'Giant Asteroid Heading For Earth'), - new Crisis(4, 'Procrastinators Meeting Delayed Again'), -]; - -let crisesPromise = Promise.resolve(crises); - -@Injectable() -export class CrisisService { - getCrises() { return crisesPromise; } - - getCrisis(id: number | string) { - return crisesPromise - .then(crises => crises.find(c => c.id === +id)); - } - -// #enddocregion - - static nextCrisisId = 100; - - addCrisis(name: string) { - name = name.trim(); - if (name) { - let crisis = new Crisis(CrisisService.nextCrisisId++, name); - crisesPromise.then(crises => crises.push(crisis)); - } - } -// #docregion -} -// #enddocregion diff --git a/public/docs/_examples/router-deprecated/ts/app/crisis-list.component.ts b/public/docs/_examples/router-deprecated/ts/app/crisis-list.component.ts deleted file mode 100644 index 6caa3653b5..0000000000 --- a/public/docs/_examples/router-deprecated/ts/app/crisis-list.component.ts +++ /dev/null @@ -1,10 +0,0 @@ -// Initial empty version -// #docregion -import { Component } from '@angular/core'; - -@Component({ - template: ` -

CRISIS CENTER

-

Get your crisis here

` -}) -export class CrisisListComponent { } diff --git a/public/docs/_examples/router-deprecated/ts/app/dialog.service.ts b/public/docs/_examples/router-deprecated/ts/app/dialog.service.ts deleted file mode 100644 index 71a342cbe8..0000000000 --- a/public/docs/_examples/router-deprecated/ts/app/dialog.service.ts +++ /dev/null @@ -1,18 +0,0 @@ -// #docregion -import { Injectable } from '@angular/core'; -/** - * Async modal dialog service - * DialogService makes this app easier to test by faking this service. - * TODO: better modal implementation that doesn't use window.confirm - */ -@Injectable() -export class DialogService { - /** - * Ask user to confirm an action. `message` explains the action and choices. - * Returns promise resolving to `true`=confirm or `false`=cancel - */ - confirm(message?: string) { - return new Promise((resolve, reject) => - resolve(window.confirm(message || 'Is it OK?'))); - }; -} diff --git a/public/docs/_examples/router-deprecated/ts/app/hero-list.component.ts b/public/docs/_examples/router-deprecated/ts/app/hero-list.component.ts deleted file mode 100644 index 5dbbe17d8e..0000000000 --- a/public/docs/_examples/router-deprecated/ts/app/hero-list.component.ts +++ /dev/null @@ -1,10 +0,0 @@ -/// Initial empty version -// #docregion -import { Component } from '@angular/core'; - -@Component({ - template: ` -

HEROES

-

Get your heroes here

` -}) -export class HeroListComponent { } diff --git a/public/docs/_examples/router-deprecated/ts/app/heroes/hero-detail.component.1.ts b/public/docs/_examples/router-deprecated/ts/app/heroes/hero-detail.component.1.ts deleted file mode 100644 index ebfa0bf21d..0000000000 --- a/public/docs/_examples/router-deprecated/ts/app/heroes/hero-detail.component.1.ts +++ /dev/null @@ -1,47 +0,0 @@ -// #docregion -import { Component, OnInit } from '@angular/core'; -import { RouteParams, Router } from '@angular/router-deprecated'; - -import { Hero, HeroService } from './hero.service'; - -@Component({ - template: ` -

HEROES

-
-

"{{hero.name}}"

-
- {{hero.id}}
-
- - -
-

- -

-
- `, -}) -export class HeroDetailComponent implements OnInit { - hero: Hero; - - // #docregion ctor - constructor( - private router: Router, - private routeParams: RouteParams, - private service: HeroService) {} - // #enddocregion ctor - - // #docregion ngOnInit - ngOnInit() { - let id = this.routeParams.get('id'); - this.service.getHero(id).then(hero => this.hero = hero); - } - // #enddocregion ngOnInit - - // #docregion gotoHeroes - gotoHeroes() { - // Like Heroes - this.router.navigate(['Heroes']); - } - // #enddocregion gotoHeroes -} diff --git a/public/docs/_examples/router-deprecated/ts/app/heroes/hero-detail.component.ts b/public/docs/_examples/router-deprecated/ts/app/heroes/hero-detail.component.ts deleted file mode 100644 index 10e73fd27b..0000000000 --- a/public/docs/_examples/router-deprecated/ts/app/heroes/hero-detail.component.ts +++ /dev/null @@ -1,52 +0,0 @@ -// #docregion -import { Component, OnInit } from '@angular/core'; -import { RouteParams, Router } from '@angular/router-deprecated'; - -import { Hero, HeroService } from './hero.service'; - -@Component({ - template: ` -

HEROES

-
-

"{{hero.name}}"

-
- {{hero.id}}
-
- - -
-

- -

-
- `, -}) -export class HeroDetailComponent implements OnInit { - hero: Hero; - - // #docregion ctor - constructor( - private router: Router, - private routeParams: RouteParams, - private service: HeroService) {} - // #enddocregion ctor - - // #docregion ngOnInit - ngOnInit() { - let id = this.routeParams.get('id'); - this.service.getHero(id).then(hero => this.hero = hero); - } - // #enddocregion ngOnInit - - // #docregion gotoHeroes - gotoHeroes() { - let heroId = this.hero ? this.hero.id : null; - // Pass along the hero id if available - // so that the HeroList component can select that hero. - // Add a totally useless `foo` parameter for kicks. - // #docregion gotoHeroes-navigate - this.router.navigate(['Heroes', {id: heroId, foo: 'foo'} ]); - // #enddocregion gotoHeroes-navigate - } - // #enddocregion gotoHeroes -} diff --git a/public/docs/_examples/router-deprecated/ts/app/heroes/hero-list.component.1.ts b/public/docs/_examples/router-deprecated/ts/app/heroes/hero-list.component.1.ts deleted file mode 100644 index cb1d20327c..0000000000 --- a/public/docs/_examples/router-deprecated/ts/app/heroes/hero-list.component.1.ts +++ /dev/null @@ -1,50 +0,0 @@ -// #docplaster - -// #docregion -// TODO SOMEDAY: Feature Componetized like HeroCenter -import { Component, OnInit } from '@angular/core'; -import { Router } from '@angular/router-deprecated'; - -import { Hero, HeroService } from './hero.service'; - -@Component({ - // #docregion template - template: ` -

HEROES

-
    -
  • - {{hero.id}} {{hero.name}} -
  • -
- ` - // #enddocregion template -}) -export class HeroListComponent implements OnInit { - heroes: Hero[]; - - // #docregion ctor - constructor( - private router: Router, - private service: HeroService) { } - // #enddocregion ctor - - ngOnInit() { - this.service.getHeroes().then(heroes => this.heroes = heroes); - } - - // #docregion select - onSelect(hero: Hero) { - // #docregion nav-to-detail - this.router.navigate( ['HeroDetail', { id: hero.id }] ); - // #enddocregion nav-to-detail - } - // #enddocregion select -} -// #enddocregion - -/* A link parameters array -// #docregion link-parameters-array -['HeroDetail', { id: hero.id }] // {id: 15} -// #enddocregion link-parameters-array -*/ diff --git a/public/docs/_examples/router-deprecated/ts/app/heroes/hero-list.component.ts b/public/docs/_examples/router-deprecated/ts/app/heroes/hero-list.component.ts deleted file mode 100644 index 1ca787592f..0000000000 --- a/public/docs/_examples/router-deprecated/ts/app/heroes/hero-list.component.ts +++ /dev/null @@ -1,56 +0,0 @@ -// #docplaster - -// TODO SOMEDAY: Feature Componetized like CrisisCenter -// #docregion -import { Component, OnInit } from '@angular/core'; -// #docregion import-route-params -import { RouteParams, Router } from '@angular/router-deprecated'; -// #enddocregion import-route-params - -import { Hero, HeroService } from './hero.service'; - -@Component({ - // #docregion template - template: ` -

HEROES

-
    -
  • - {{hero.id}} {{hero.name}} -
  • -
- ` - // #enddocregion template -}) -export class HeroListComponent implements OnInit { - heroes: Hero[]; - - // #docregion ctor - private selectedId: number; - - constructor( - private service: HeroService, - private router: Router, - routeParams: RouteParams) { - this.selectedId = +routeParams.get('id'); - } - // #enddocregion ctor - - // #docregion isSelected - isSelected(hero: Hero) { return hero.id === this.selectedId; } - // #enddocregion isSelected - - // #docregion select - onSelect(hero: Hero) { - this.router.navigate( ['HeroDetail', { id: hero.id }] ); - } - // #enddocregion select - - ngOnInit() { - - - this.service.getHeroes().then(heroes => this.heroes = heroes); - } -} -// #enddocregion diff --git a/public/docs/_examples/router-deprecated/ts/app/heroes/hero.service.ts b/public/docs/_examples/router-deprecated/ts/app/heroes/hero.service.ts deleted file mode 100644 index c819bd2632..0000000000 --- a/public/docs/_examples/router-deprecated/ts/app/heroes/hero.service.ts +++ /dev/null @@ -1,27 +0,0 @@ -// #docregion -import { Injectable } from '@angular/core'; - -export class Hero { - constructor(public id: number, public name: string) { } -} - -let HEROES = [ - new Hero(11, 'Mr. Nice'), - new Hero(12, 'Narco'), - new Hero(13, 'Bombasto'), - new Hero(14, 'Celeritas'), - new Hero(15, 'Magneta'), - new Hero(16, 'RubberMan') -]; - -let heroesPromise = Promise.resolve(HEROES); - -@Injectable() -export class HeroService { - getHeroes() { return heroesPromise; } - - getHero(id: number | string) { - return heroesPromise - .then(heroes => heroes.find(h => h.id === +id)); - } -} diff --git a/public/docs/_examples/router-deprecated/ts/app/main.1.ts b/public/docs/_examples/router-deprecated/ts/app/main.1.ts deleted file mode 100644 index ce110455d4..0000000000 --- a/public/docs/_examples/router-deprecated/ts/app/main.1.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* First version */ -// #docplaster - -// #docregion all -import { bootstrap } from '@angular/platform-browser-dynamic'; -import { ROUTER_PROVIDERS } from '@angular/router-deprecated'; - -import { AppComponent } from './app.component'; - -// #enddocregion all - -/* Can't use AppComponent ... but display as if we can -// #docregion all -bootstrap(AppComponent, [ -// #enddocregion all -*/ - -// Actually use the v.1 component -import { AppComponent as ac } from './app.component.1'; -bootstrap(ac, [ -// #docregion all - ROUTER_PROVIDERS -]); -// #enddocregion all diff --git a/public/docs/_examples/router-deprecated/ts/app/main.2.ts b/public/docs/_examples/router-deprecated/ts/app/main.2.ts deleted file mode 100644 index 74862cdd0a..0000000000 --- a/public/docs/_examples/router-deprecated/ts/app/main.2.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* Second version */ -// For Milestone #2 -// Also includes digression on HashPathStrategy (not used in the final app) -// #docplaster - -// #docregion -import { bootstrap } from '@angular/platform-browser-dynamic'; -import { ROUTER_PROVIDERS } from '@angular/router-deprecated'; - -// Add these symbols to override the `LocationStrategy` -import { LocationStrategy, - HashLocationStrategy } from '@angular/common'; - -import { AppComponent } from './app.component'; -// #enddocregion -/* Can't use AppComponent ... but display as if we can -// #docregion - -bootstrap(AppComponent, [ -// #enddocregion -*/ - -// Actually use the v.2 component -import { AppComponent as ac } from './app.component.2'; - -bootstrap(ac, [ -// #docregion - ROUTER_PROVIDERS, - { provide: LocationStrategy, useClass: HashLocationStrategy } // .../#/crisis-center/ -]); -// #enddocregion diff --git a/public/docs/_examples/router-deprecated/ts/app/main.3.ts b/public/docs/_examples/router-deprecated/ts/app/main.3.ts deleted file mode 100644 index 9e9eb04721..0000000000 --- a/public/docs/_examples/router-deprecated/ts/app/main.3.ts +++ /dev/null @@ -1,7 +0,0 @@ -// #docregion -import { bootstrap } from '@angular/platform-browser-dynamic'; -import { ROUTER_PROVIDERS } from '@angular/router-deprecated'; - -import { AppComponent } from './app.component.3'; - -bootstrap(AppComponent, [ROUTER_PROVIDERS]); diff --git a/public/docs/_examples/router-deprecated/ts/app/main.ts b/public/docs/_examples/router-deprecated/ts/app/main.ts deleted file mode 100644 index 08bbbef6e8..0000000000 --- a/public/docs/_examples/router-deprecated/ts/app/main.ts +++ /dev/null @@ -1,7 +0,0 @@ -// #docregion -import { bootstrap } from '@angular/platform-browser-dynamic'; -import { ROUTER_PROVIDERS } from '@angular/router-deprecated'; - -import { AppComponent } from './app.component'; - -bootstrap(AppComponent, [ROUTER_PROVIDERS]); diff --git a/public/docs/_examples/router-deprecated/ts/example-config.json b/public/docs/_examples/router-deprecated/ts/example-config.json deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/public/docs/_examples/router-deprecated/ts/index.1.html b/public/docs/_examples/router-deprecated/ts/index.1.html deleted file mode 100644 index 53a6d4832f..0000000000 --- a/public/docs/_examples/router-deprecated/ts/index.1.html +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - Router (Deprecated) Sample v.1 - - - - - - - - - - - - - - - - -

Milestone 1

- loading... - - - - diff --git a/public/docs/_examples/router-deprecated/ts/index.2.html b/public/docs/_examples/router-deprecated/ts/index.2.html deleted file mode 100644 index d71bd929cb..0000000000 --- a/public/docs/_examples/router-deprecated/ts/index.2.html +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - Router (Deprecated) Sample v.2 - - - - - - - - - - - - - - - - -

Milestone 2

- loading... - - - - diff --git a/public/docs/_examples/router-deprecated/ts/index.3.html b/public/docs/_examples/router-deprecated/ts/index.3.html deleted file mode 100644 index abbb771beb..0000000000 --- a/public/docs/_examples/router-deprecated/ts/index.3.html +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - Router (Deprecated) Sample v.3 - - - - - - - - - - - - - - - - -

Milestone 3

- loading... - - - - diff --git a/public/docs/_examples/router-deprecated/ts/index.html b/public/docs/_examples/router-deprecated/ts/index.html deleted file mode 100644 index 35a6ddfc73..0000000000 --- a/public/docs/_examples/router-deprecated/ts/index.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - Router (Deprecated) Sample - - - - - - - - - - - - - - - - - loading... - - - - diff --git a/public/docs/_examples/router-deprecated/ts/plnkr.json b/public/docs/_examples/router-deprecated/ts/plnkr.json deleted file mode 100644 index 91eff6fdb9..0000000000 --- a/public/docs/_examples/router-deprecated/ts/plnkr.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "description": "Router (Deprecated Beta)", - "files":[ - "!**/*.d.ts", - "!**/*.js", - "!**/*.[1,2,3].*", - "!app/crisis-list.component.ts", - "!app/hero-list.component.ts", - "!app/crisis-center/add-crisis.component.ts" - ], - "tags": ["router", "deprecated"] -} diff --git a/public/docs/dart/latest/guide/_data.json b/public/docs/dart/latest/guide/_data.json index 8b9f230bd6..f13c3c42c8 100644 --- a/public/docs/dart/latest/guide/_data.json +++ b/public/docs/dart/latest/guide/_data.json @@ -36,13 +36,6 @@ "basics": true }, - "forms-deprecated": { - "title": "Forms", - "intro": "A form creates a cohesive, effective, and compelling data entry experience. An Angular form coordinates a set of data-bound user controls, tracks changes, validates input, and presents errors.", - "basics": true, - "hide": true - }, - "dependency-injection": { "title": "Dependency Injection", "intro": "Angular's dependency injection system creates and delivers dependent services \"just-in-time\".", @@ -130,12 +123,6 @@ "intro": "Pipes transform displayed values within a template." }, - "router-deprecated": { - "title": "Routing & Navigation", - "intro": "Discover the basics of screen navigation with the Angular 2 Component Router.", - "hide": true - }, - "router": { "title": "Routing & Navigation", "intro": "Discover the basics of screen navigation with the Angular 2 Component Router." diff --git a/public/docs/dart/latest/guide/forms-deprecated.jade b/public/docs/dart/latest/guide/forms-deprecated.jade deleted file mode 100644 index f5711f49ee..0000000000 --- a/public/docs/dart/latest/guide/forms-deprecated.jade +++ /dev/null @@ -1,4 +0,0 @@ -include ../_util-fns - -:marked - This page has no Dart equivalent. Instead, see the [forms guide](forms.html). diff --git a/public/docs/dart/latest/guide/router-deprecated.jade b/public/docs/dart/latest/guide/router-deprecated.jade deleted file mode 100644 index d76267fce1..0000000000 --- a/public/docs/dart/latest/guide/router-deprecated.jade +++ /dev/null @@ -1,4 +0,0 @@ -include ../_util-fns - -:marked - This page has no Dart equivalent. Instead, see the [router guide](router.html). diff --git a/public/docs/js/latest/guide/_data.json b/public/docs/js/latest/guide/_data.json index d687f38e3a..724711f666 100644 --- a/public/docs/js/latest/guide/_data.json +++ b/public/docs/js/latest/guide/_data.json @@ -116,12 +116,6 @@ "intro": "Pipes transform displayed values within a template." }, - "router-deprecated": { - "title": "Router (Deprecated Beta)", - "intro": "The deprecated Beta Router.", - "hide": true - }, - "router": { "title": "Routing & Navigation", "intro": "Discover the basics of screen navigation with the Angular 2 router." diff --git a/public/docs/js/latest/guide/forms-deprecated.jade b/public/docs/js/latest/guide/forms-deprecated.jade deleted file mode 100644 index 69634ab56a..0000000000 --- a/public/docs/js/latest/guide/forms-deprecated.jade +++ /dev/null @@ -1,649 +0,0 @@ -include ../_util-fns - -.alert.is-important - :marked - This guide is using the deprecated forms API, which is disabled as of RC5, thus this sample only works up to RC4. - - We have created a new version using the new API here. - -:marked - We’ve all used a form to login, submit a help request, place an order, book a flight, - schedule a meeting and perform countless other data entry tasks. - Forms are the mainstay of business applications. - - Any seasoned web developer can slap together an HTML form with all the right tags. - It's more challenging to create a cohesive data entry experience that guides the - user efficiently and effectively through the workflow behind the form. - - *That* takes design skills that are, to be frank, well out of scope for this chapter. - - It also takes framework support for - **two-way data binding, change tracking, validation, and error handling** - ... which we shall cover in this chapter on Angular forms. - - We will build a simple form from scratch, one step at a time. Along the way we'll learn - - - How to build an Angular form with a component and template - - - The `ngModel` two-way data binding syntax for reading and writing values to input controls - - - The `ngControl` directive to track the change state and validity of form controls - - - The special CSS classes that `ngControl` adds to form controls and how we can use them to provide strong visual feedback - - - How to display validation errors to users and enable/disable form controls - - - How to share information across controls with template local variables - - Live Example - -.l-main-section -:marked - ## Template-Driven Forms - - Many of us will build forms by writing templates in the Angular [template syntax](./template-syntax.html) with - the form-specific directives and techniques described in this chapter. -.l-sub-section - :marked - That's not the only way to create a form but it's the way we'll cover in this chapter. -:marked - We can build almost any form we need with an Angular template — login forms, contact forms ... pretty much any business forms. - We can lay out the controls creatively, bind them to data, specify validation rules and display validation errors, - conditionally enable or disable specific controls, trigger built-in visual feedback, and much more. - - It will be pretty easy because Angular handles many of the repetitive, boiler plate tasks we'd - otherwise wrestle with ourselves. - - We'll discuss and learn to build the following template-driven form: - -figure.image-display - img(src="/resources/images/devguide/forms/hero-form-1.png" width="400px" alt="Clean Form") - -:marked - Here at the *Hero Employment Agency* we use this form to maintain personal information about the - heroes in our stable. Every hero needs a job. It's our company mission to match the right hero with the right crisis! - - Two of the three fields on this form are required. Required fields have a green bar on the left to make them easy to spot. - - If we delete the hero name, the form displays a validation error in an attention grabbing style: - -figure.image-display - img(src="/resources/images/devguide/forms/hero-form-2.png" width="400px" alt="Invalid, Name Required") - -:marked - Note that the submit button is disabled and the "required" bar to the left of the input control changed from green to red. - -.l-sub-section - p We'll' customize the colors and location of the "required" bar with standard CSS. - -:marked - We will build this form in the following sequence of small steps - - 1. Create the `Hero` model class - 1. Create the component that controls the form - 1. Create a template with the initial form layout - 1. Add the **ngModel** directive to each form input control - 1. Add the **ngControl** directive to each form input control - 1. Add custom CSS to provide visual feedback - 1. Show and hide validation error messages - 1. Handle form submission with **ngSubmit** - 1. Disable the form’s submit button until the form is valid - -:marked - ## Setup - Create a new project folder (`angular2-forms`) and follow the steps in the [QuickStart](../quickstart.html). - - ## Create the Hero Model Class - - As users enter form data, we capture their changes and update an instance of a model. - We can't layout the form until we know what the model looks like. - - A model can be as simple as a "property bag" that holds facts about a thing of application importance. - That describes well our `Hero` class with its three required fields (`id`, `name`, `power`) - and one optional field (`alterEgo`). - - Create a new file in the app folder called `hero.js` and give it the following constructor: - -+makeExample('forms-deprecated/js/app/hero.js', null, 'app/hero.js') - -:marked - It's an anemic model with few requirements and no behavior. Perfect for our demo. - - The `alterEgo` is optional and the constructor lets us omit it by being the last argument. - - We can create a new hero like this: -code-example(format=""). - var myHero = new Hero(42, 'SkyDog', - 'Fetch any object at any distance', 'Leslie Rollover'); - console.log('My hero is called ' + myHero.name); // "My hero is called SkyDog" -:marked - We update the `` of the `index.html` to include this javascript file. - -+makeExample('forms-deprecated/js/index.html', 'scripts-hero', 'index.html (excerpt)')(format=".") - -.l-main-section -:marked - ## Create a Form component - - An Angular form has two parts: an HTML-based template and a code-based Component to handle data and user interactions. - - We begin with the Component because it states, in brief, what the Hero editor can do. - - Create a new file called `hero-form.component.js` and give it the following definition: - -+makeExample('forms-deprecated/js/app/hero-form.component.js', 'first', 'app/hero-form.component.js') - -:marked - There’s nothing special about this component, nothing form-specific, nothing to distinguish it from any component we've written before. - - Understanding this component requires only the Angular 2 concepts we’ve learned in previous chapters - - 1. We use the `ng.core` object from the Angular library as we usually do. - - 1. The `Component()` selector value of "hero-form" means we can drop this form in a parent template with a `` tag. - - 1. The `templateUrl` property points to a separate file for template HTML called `hero-form.component.html`. - - 1. We defined dummy data for `model` and `powers` as befits a demo. - Down the road, we can inject a data service to get and save real data - or perhaps expose these properties as [inputs and outputs](./template-syntax.html#inputs-outputs) for binding to a - parent component. None of this concerns us now and these future changes won't affect our form. - - 1. We threw in a `diagnostic` method at the end to return a JSON representation of our model. - It'll help us see what we're doing during our development; we've left ourselves a cleanup note to discard it later. - - Why don't we write the template inline in the component file as we often do - elsewhere in the Developer Guide? - - There is no “right” answer for all occasions. We like inline templates when they are short. - Most form templates won't be short. TypeScript and JavaScript files generally aren't the best place to - write (or read) large stretches of HTML and few editors are much help with files that have a mix of HTML and code. - We also like short files with a clear and obvious purpose like this one. - - We made a good choice to put the HTML template elsewhere. - We'll write that template in a moment. Before we do, we'll take a step back - and revise the `app.component.js` to make use of our new `HeroFormComponent`. - -:marked - Again we update the `` of the `index.html` to include the new javascript file. - -+makeExample('forms-deprecated/js/index.html', 'scripts-hero-form', 'index.html (excerpt)')(format=".") - -.l-main-section -:marked - ## Revise the *app.component.js* - - `app.component.js` is the application's root component. It will host our new `HeroFormComponent`. - - Replace the contents of the "QuickStart" version with the following: -+makeExample('forms-deprecated/js/app/app.component.js', null, 'app/app.component.js') - -:marked -.l-sub-section - :marked - There are only two changes: - - 1. The `template` is simply the new element tag identified by the component's `select` property. - - 1. The `directives` array tells Angular that our template depends upon the `HeroFormComponent` - which is itself a Directive (as are all Components). - -.l-main-section -:marked - ## Create an initial HTML Form Template - - Create a new template file called `hero-form.component.html` and give it the following definition: - -+makeExample('forms-deprecated/js/app/hero-form.component.html', 'start', 'app/hero-form.component.html') - -:marked - That is plain old HTML 5. We're presenting two of the `Hero` fields, `name` and `alterEgo`, and - opening them up for user input in input boxes. - - The *Name* `` control has the HTML5 `required` attribute; - the *Alter Ego* `` control does not because `alterEgo` is optional. - - We've got a *Submit* button at the bottom with some classes on it. - - **We are not using Angular yet**. There are no bindings. No extra directives. Just layout. - - The `container`,`form-group`, `form-control`, and `btn` classes - come from [Twitter Bootstrap](http://getbootstrap.com/css/). Purely cosmetic. - We're using Bootstrap to gussy up our form. - Hey, what's a form without a little style! - -.callout.is-important - header Angular Forms Do Not Require A Style Library - :marked - Angular makes no use of the `container`, `form-group`, `form-control`, and `btn` classes or - the styles of any external library. Angular apps can use any CSS library - ... or none at all. - -:marked - Let's add the stylesheet. - -ol - li Open a terminal window in the application root folder and enter the command: - code-example(language="html" escape="html"). - npm install bootstrap --save - li Open index.html and add the following link to the <head>. - +makeExample('forms-deprecated/js/index.html', 'bootstrap')(format=".") -:marked -.l-main-section -:marked - ## Add Powers with ***ngFor** - Our hero may choose one super power from a fixed list of Agency-approved powers. - We maintain that list internally (in `HeroFormComponent`). - - We'll add a `select` to our - form and bind the options to the `powers` list using `NgFor`, - a technique we might have seen before in the [Displaying Data](./displaying-data.html) chapter. - - Add the following HTML *immediately below* the *Alter Ego* group. -+makeExample('forms-deprecated/js/app/hero-form.component.html', 'powers', 'app/hero-form.component.html (excerpt)')(format=".") - -:marked - We are repeating the `` tag for each power in the list of Powers. - The `#p` local template variable is a different power in each iteration; - we display its name using the interpolation syntax with the double-curly-braces. - -.l-main-section -:marked - ## Two-way data binding with ***ngModel** - Running the app right now would be disappointing. - -figure.image-display - img(src="/resources/images/devguide/forms/hero-form-3.png" width="400px" alt="Early form with no binding") -:marked - We don't see hero data because we are not binding to the `Hero` yet. - We know how to do that from earlier chapters. - [Displaying Data](./displaying-data.html) taught us Property Binding. - [User Input](./user-input.html) showed us how to listen for DOM events with an - Event Binding and how to update a component property with the displayed value. - - Now we need to display, listen, and extract at the same time. - - We could use those techniques again in our form. - Instead we'll introduce something new, the `NgModel` directive, that - makes binding our form to the model super-easy. - - Find the `` tag for the "Name" and update it like this - -+makeExample('forms-deprecated/js/app/hero-form.component.html', 'ngModel-1','app/hero-form.component.html (excerpt)')(format=".") - -.l-sub-section - :marked - We appended a diagnostic interpolation after the input tag - so we can see what we're doing. - We left ourselves a note to throw it way when we're done. - -:marked - Focus on the binding syntax: `[(ngModel)]="..."`. - - If we ran the app right now and started typing in the *Name* input box, - adding and deleting characters, we'd see them appearing and disappearing - from the interpolated text. - At some point it might look like this. -figure.image-display - img(src="/resources/images/devguide/forms/ng-model-in-action.png" width="400px" alt="ngModel in action") -:marked - The diagnostic is evidence that we really are flowing values from the input box to the model and - back again. **That's two-way data binding!** - - Let's add similar `[(ngModel)]` bindings to *Alter Ego* and *Hero Power*. - We'll ditch the input box binding message - and add a new binding at the top to the component's `diagnostic` method. - Then we can confirm that two-way data binding works *for the entire Hero model*. - - After revision the core of our form should have three `[(ngModel)]` bindings that - look much like this: - -+makeExample('forms-deprecated/js/app/hero-form.component.html', 'ngModel-2', 'app/hero-form.component.html (excerpt)') - -:marked - If we ran the app right now and changed every Hero model property, the form might display like this: -figure.image-display - img(src="/resources/images/devguide/forms/ng-model-in-action-2.png" width="400px" alt="ngModel in super action") -:marked - The diagnostic near the top of the form - confirms that all of our changes are reflected in the model. - - **Delete** the `{{diagnostic()}}` binding at the top as it has served its purpose. - -.l-sub-section - :marked - ### Inside [(ngModel)] - *This section is an optional deep dive into [(ngModel)]. Not interested? Skip ahead!* - - The punctuation in the binding syntax, [()], is a good clue to what's going on. - - In a Property Binding, a value flows from the model to a target property on screen. - We identify that target property by surrounding its name in brackets, []. - This is a one-way data binding **from the model to the view**. - - In an Event Binding, we flow the value from the target property on screen to the model. - We identify that target property by surrounding its name in parentheses, (). - This is a one-way data binding in the opposite direction **from the view to the model**. - - No wonder Angular chose to combine the punctuation as [()] - to signify a two-way data binding and a **flow of data in both directions**. - - In fact, we can break the `NgModel` binding into its two separate modes - as we do in this re-write of the "Name" `` binding: - +makeExample('forms-deprecated/js/app/hero-form.component.html', 'ngModel-3','app/hero-form.component.html (excerpt)')(format=".") - - :marked -
The Property Binding should feel familiar. The Event Binding might seem strange. - - The `ngModelChange` is not an `` element event. - It is actually an event property of the `NgModel` directive. - When Angular sees a binding target in the form [(x)], - it expects the `x` directive to have an `x` input property and an `xChange` output property. - - The other oddity is the template expression, `model.name = $event`. - We're used to seeing an `$event` object coming from a DOM event. - The `ngModelChange` property doesn't produce a DOM event; it's an Angular `EventEmitter` - property that returns the input box value when it fires — which is precisely what - we should assign to the model's `name' property. - - Nice to know but is it practical? We almost always prefer `[(ngModel)]`. - We might split the binding if we had to do something special in - the event handling such as debounce or throttle the key strokes. - - Learn more about `NgModel` and other template syntax in the - [Template Syntax](./template-syntax.html) chapter. - -.l-main-section -:marked - ## Track change-state and validity with **ngControl** - - A form isn't just about data binding. We'd also like to know the state of the controls on our form. - The `NgControl` directive keeps track of control state for us. - -.callout.is-helpful - header NgControl requires Form - :marked - The `NgControl` is one of a family of `NgForm` directives that can only be applied to - a control within a ` tag. -:marked - Our application can ask an `NgControl` if the user touched the control, - if the value changed, or if the value became invalid. - - `NgControl` doesn't just track state; it updates the control with special - Angular CSS classes from the set we listed above. - We can leverage those class names to change the appearance of the - control and make messages appear or disappear. - - We'll explore those effects soon. Right now - we should **add `ngControl`to all three form controls**, - starting with the *Name* input box -+makeExample('forms-deprecated/js/app/hero-form.component.html', 'ngControl-1', 'app/hero-form.component.html (excerpt)')(format=".") -:marked - Be sure to assign a unique name to each `ngControl` directive. - -.l-sub-section - :marked - Angular registers controls under their `ngControl` names - with the `NgForm`. - We didn't add the `NgForm` directive explicitly but it's here - and we'll talk about it [later in this chapter](#ngForm). - -.l-main-section -:marked - ## Add Custom CSS for Visual Feedback - - `NgControl` doesn't just track state. - It updates the control with three classes that reflect the state. - -table - tr - th State - th Class if true - th Class if false - tr - td Control has been visited - td ng-touched - td ng-untouched - tr - td Control's value has changed - td ng-dirty - td ng-pristine - tr - td Control's value is valid - td ng-valid - td ng-invalid -:marked - Let's add a temporary [local template variable](./template-syntax.html#local-vars) named **spy** - to the "Name" `` tag and use the spy to display those classes. - -+makeExample('forms-deprecated/js/app/hero-form.component.html', 'ngControl-2','app/hero-form.component.html (excerpt)')(format=".") - -:marked - Now run the app and focus on the *Name* input box. - Follow the next four steps *precisely* - - 1. Look but don't touched - 1. Click in the input box, then click outside the text input box - 1. Add slashes to the end of the name - 1. Erase the name - - The actions and effects are as follows: -figure.image-display - img(src="/resources/images/devguide/forms/control-state-transitions-anim.gif" alt="Control State Transition") -:marked - We should be able to see the following four sets of class names and their transitions: -figure.image-display - img(src="/resources/images/devguide/forms/ng-control-class-changes.png" width="400px" alt="Control State Transitions") - -:marked - The (`ng-valid` | `ng-invalid`) pair are most interesting to us. We want to send a - strong visual signal when the data are invalid and we want to mark required fields. - - We realize we can do both at the same time with a colored bar on the left of the input box: - -figure.image-display - img(src="/resources/images/devguide/forms/validity-required-indicator.png" width="400px" alt="Invalid Form") - -:marked - We achieve this effect by adding two styles to a new `forms.css` file - that we add to our project as a sibling to `index.html`. - -+makeExample('forms-deprecated/js/forms.css',null,'forms.css')(format=".") -:marked - These styles select for the two Angular validity classes and the HTML 5 "required" attribute. - - We update the `` of the `index.html` to include this style sheet. -+makeExample('forms-deprecated/js/index.html', 'styles', 'index.html (excerpt)')(format=".") -:marked - ## Show and Hide Validation Error messages - - We can do better. - - The "Name" input box is required. Clearing it turns the bar red. That says *something* is wrong but we - don't know *what* is wrong or what to do about it. - We can leverage the `ng-invalid` class to reveal a helpful message. - - Here's the way it should look when the user deletes the name: -figure.image-display - img(src="/resources/images/devguide/forms/name-required-error.png" width="400px" alt="Name required") - -:marked - To achieve this effect we extend the `` tag with - 1. a [local template variable](./template-syntax.html#local-vars) - 1. the "*is required*" message in a nearby `
` which we'll display only if the control is invalid. - - Here's how we do it for the *name* input box: --var stylePattern = { otl: /(#name="form")|(.*div.*$)|(Name is required)/gm }; -+makeExample('forms-deprecated/js/app/hero-form.component.html', - 'name-with-error-msg', - 'app/hero-form.component.html (excerpt)', - stylePattern) -:marked - When we added the `ngControl` directive, we bound it to the model's `name` property. - Here we initialize a template local variable (`name`) with the value "ngForm" (`#name="ngForm"`). - Angular recognizes that syntax and re-sets the `name` local template variable to the - `ngControl` directive instance. - In other words, the `name` local template variable becomes a handle on the `ngControl` object - for this input box. - - Now we can control visibility of the "name" error message by binding the message `
` element's `hidden` property - to the `ngControl` object's `valid` property. The message is hidden while the control is valid; - the message is revealed when the control becomes invalid. - -.l-sub-section - :marked - ### The NgForm directive - We just set a template local variable with the value of an `NgForm` directive. - Why did that work? We didn't add the **[`NgForm`](../api/common/index/NgForm-directive.html) directive** explicitly. - - Angular added it surreptitiously, wrapping it around the `
` element - - The `NgForm` directive supplements the `form` element with additional features. - It collects `Controls` (elements identified by an `ngControl` directive) - and monitors their properties including their validity. - It also has its own `valid` property which is true only if every contained - control is valid. -:marked - The Hero *Alter Ego* is optional so we can leave that be. - - Hero *Power* selection is required. - We can add the same kind of error handling to the `` control has the HTML5 `required` attribute; - the *Alter Ego* `` control does not because `alterEgo` is optional. - - We've got a *Submit* button at the bottom with some classes on it. - - **We are not using Angular yet**. There are no bindings. No extra directives. Just layout. - - The `container`,`form-group`, `form-control`, and `btn` classes - come from [Twitter Bootstrap](http://getbootstrap.com/css/). Purely cosmetic. - We're using Bootstrap to gussy up our form. - Hey, what's a form without a little style! - -.callout.is-important - header Angular Forms Do Not Require A Style Library - :marked - Angular makes no use of the `container`, `form-group`, `form-control`, and `btn` classes or - the styles of any external library. Angular apps can use any CSS library - ... or none at all. - -:marked - Let's add the stylesheet. - -ol - li Open a terminal window in the application root folder and enter the command: - code-example(language="html" escape="html"). - npm install bootstrap --save - li Open index.html and add the following link to the <head>. - +makeExample('forms-deprecated/ts/index.html', 'bootstrap')(format=".") -:marked -.l-main-section -:marked - ## Add Powers with ***ngFor** - Our hero may choose one super power from a fixed list of Agency-approved powers. - We maintain that list internally (in `HeroFormComponent`). - - We'll add a `select` to our - form and bind the options to the `powers` list using `ngFor`, - a technique we might have seen before in the [Displaying Data](./displaying-data.html) chapter. - - Add the following HTML *immediately below* the *Alter Ego* group. -+makeExample('forms-deprecated/ts/app/hero-form.component.html', 'powers', 'app/hero-form.component.html (excerpt)')(format=".") - -:marked - We are repeating the `` tag for each power in the list of Powers. - The `p` template input variable is a different power in each iteration; - we display its name using the interpolation syntax with the double-curly-braces. - - -.l-main-section -:marked - ## Two-way data binding with **ngModel** - Running the app right now would be disappointing. - -figure.image-display - img(src="/resources/images/devguide/forms/hero-form-3.png" width="400px" alt="Early form with no binding") -:marked - We don't see hero data because we are not binding to the `Hero` yet. - We know how to do that from earlier chapters. - [Displaying Data](./displaying-data.html) taught us Property Binding. - [User Input](./user-input.html) showed us how to listen for DOM events with an - Event Binding and how to update a component property with the displayed value. - - Now we need to display, listen, and extract at the same time. - - We could use those techniques again in our form. - Instead we'll introduce something new, the `[(ngModel)]` syntax, that - makes binding our form to the model super-easy. - - Find the `` tag for the "Name" and update it like this - -+makeExample('forms-deprecated/ts/app/hero-form.component.html', 'ngModel-1','app/hero-form.component.html (excerpt)')(format=".") - -.l-sub-section - :marked - We appended a diagnostic interpolation after the input tag - so we can see what we're doing. - We left ourselves a note to throw it away when we're done. - -:marked - Focus on the binding syntax: `[(ngModel)]="..."`. - - If we ran the app right now and started typing in the *Name* input box, - adding and deleting characters, we'd see them appearing and disappearing - from the interpolated text. - At some point it might look like this. -figure.image-display - img(src="/resources/images/devguide/forms/ng-model-in-action.png" width="400px" alt="ngModel in action") -:marked - The diagnostic is evidence that we really are flowing values from the input box to the model and - back again. **That's two-way data binding!** - - Let's add similar `[(ngModel)]` bindings to *Alter Ego* and *Hero Power*. - We'll ditch the input box binding message - and add a new binding at the top to the component's `diagnostic` property. - Then we can confirm that two-way data binding works *for the entire Hero model*. - - After revision the core of our form should have three `[(ngModel)]` bindings that - look much like this: - -+makeExample('forms-deprecated/ts/app/hero-form.component.html', 'ngModel-2', 'app/hero-form.component.html (excerpt)') - -:marked - If we ran the app right now and changed every Hero model property, the form might display like this: -figure.image-display - img(src="/resources/images/devguide/forms/ng-model-in-action-2.png" width="400px" alt="ngModel in super action") -:marked - The diagnostic near the top of the form - confirms that all of our changes are reflected in the model. - - **Delete** the `{{diagnostic}}` binding at the top as it has served its purpose. - -.l-sub-section - :marked - ### Inside [(ngModel)] - *This section is an optional deep dive into [(ngModel)]. Not interested? Skip ahead!* - - The punctuation in the binding syntax, [()], is a good clue to what's going on. - - In a Property Binding, a value flows from the model to a target property on screen. - We identify that target property by surrounding its name in brackets, []. - This is a one-way data binding **from the model to the view**. - - In an Event Binding, we flow the value from the target property on screen to the model. - We identify that target property by surrounding its name in parentheses, (). - This is a one-way data binding in the opposite direction **from the view to the model**. - - No wonder Angular chose to combine the punctuation as [()] - to signify a two-way data binding and a **flow of data in both directions**. - - In fact, we can break the `NgModel` binding into its two separate modes - as we do in this re-write of the "Name" `` binding: - +makeExample('forms-deprecated/ts/app/hero-form.component.html', 'ngModel-3','app/hero-form.component.html (excerpt)')(format=".") - - :marked -
The Property Binding should feel familiar. The Event Binding might seem strange. - - The `ngModelChange` is not an `` element event. - It is actually an event property of the `NgModel` directive. - When Angular sees a binding target in the form [(x)], - it expects the `x` directive to have an `x` input property and an `xChange` output property. - - The other oddity is the template expression, `model.name = $event`. - We're used to seeing an `$event` object coming from a DOM event. - The `ngModelChange` property doesn't produce a DOM event; it's an Angular `EventEmitter` - property that returns the input box value when it fires — which is precisely what - we should assign to the model's `name` property. - - Nice to know but is it practical? We almost always prefer `[(ngModel)]`. - We might split the binding if we had to do something special in - the event handling such as debounce or throttle the key strokes. - - Learn more about `NgModel` and other template syntax in the - [Template Syntax](./template-syntax.html) chapter. - -.l-main-section -:marked - ## Track change-state and validity with **ngControl** - - A form isn't just about data binding. We'd also like to know the state of the controls on our form. - - By setting `ngControl` we create a directive that can tell if the user touched the control, - if the value changed, or if the value became invalid. - - This directive doesn't just track state; it updates the control with special - Angular CSS classes from the set we listed above. - We can leverage those class names to change the appearance of the - control and make messages appear or disappear. - - We'll explore those effects soon. Right now - we should **add `ngControl` to all three form controls**, - starting with the *Name* input box -+makeExample('forms-deprecated/ts/app/hero-form.component.html', 'ngControl-1', 'app/hero-form.component.html (excerpt)')(format=".") -:marked - We set this particular `ngControl` to "name" which makes sense for our app. Any unique value will do. - -.l-sub-section - :marked - Internally Angular creates `Controls` and registers them under their `ngControl` names - with an `NgForm` directive that Angular attached to the `` tag. - We'll talk about `NgForm` [later in the chapter](#ngForm). - - The `ngControl` *attribute* in our template actually maps to the - [NgControlName](../api/common/index/NgControlName-directive.html) directive. - There is also a `NgControl` *abstract* directive which is *not the same thing*. - We often ignore this technical distinction and refer to `NgControlName` more conveniently (albeit incorrectly) as the *NgControl* directive. - - While we're under the hood, we might as well note that the `ngModel` in the - two-way binding syntax is now a property of the `NgControlName` directive. - The `NgModel` directive is no longer involved. We only need one directive to manage the DOM element - and there is no practical difference in the way either directive handles data binding. - -.l-main-section -:marked - ## Add Custom CSS for Visual Feedback - - The *NgControl* directive doesn't just track state. - It updates the control with three classes that reflect the state. - -table - tr - th State - th Class if true - th Class if false - tr - td Control has been visited - td ng-touched - td ng-untouched - tr - td Control's value has changed - td ng-dirty - td ng-pristine - tr - td Control's value is valid - td ng-valid - td ng-invalid -:marked - Let's add a temporary [template reference variable](./template-syntax.html#ref-vars) named **spy** - to the "Name" `` tag and use the spy to display those classes. - -+makeExample('forms-deprecated/ts/app/hero-form.component.html', 'ngControl-2','app/hero-form.component.html (excerpt)')(format=".") - -:marked - Now run the app and focus on the *Name* input box. - Follow the next four steps *precisely* - - 1. Look but don't touch - 1. Click in the input box, then click outside the text input box - 1. Add slashes to the end of the name - 1. Erase the name - - The actions and effects are as follows: -figure.image-display - img(src="/resources/images/devguide/forms/control-state-transitions-anim.gif" alt="Control State Transition") -:marked - We should be able to see the following four sets of class names and their transitions: -figure.image-display - img(src="/resources/images/devguide/forms/ng-control-class-changes.png" width="400px" alt="Control State Transitions") - -:marked - The (`ng-valid` | `ng-invalid`) pair are most interesting to us. We want to send a - strong visual signal when the data are invalid and we want to mark required fields. - - We realize we can do both at the same time with a colored bar on the left of the input box: - -figure.image-display - img(src="/resources/images/devguide/forms/validity-required-indicator.png" width="400px" alt="Invalid Form") - -:marked - We achieve this effect by adding two styles to a new `forms.css` file - that we add to our project as a sibling to `index.html`. - -+makeExample('forms-deprecated/ts/forms.css',null,'forms.css')(format=".") -:marked - These styles select for the two Angular validity classes and the HTML 5 "required" attribute. - - We update the `` of the `index.html` to include this style sheet. -+makeExample('forms-deprecated/ts/index.html', 'styles', 'index.html (excerpt)')(format=".") -:marked - ## Show and Hide Validation Error messages - - We can do better. - - The "Name" input box is required. Clearing it turns the bar red. That says *something* is wrong but we - don't know *what* is wrong or what to do about it. - We can leverage the `ng-invalid` class to reveal a helpful message. - - Here's the way it should look when the user deletes the name: -figure.image-display - img(src="/resources/images/devguide/forms/name-required-error.png" width="400px" alt="Name required") - -:marked - To achieve this effect we extend the `` tag with - 1. a [template reference variable](./template-syntax.html#ref-vars) - 1. the "*is required*" message in a nearby `
` which we'll display only if the control is invalid. - - Here's how we do it for the *name* input box: -+makeExample('forms-deprecated/ts/app/hero-form.component.html', - 'name-with-error-msg', - 'app/hero-form.component.html (excerpt)')(format=".") -:marked - We need a template reference variable to access the input box's Angular control from within the template. - Here we created a variable called `name` and gave it the value "ngForm". -.l-sub-section - :marked - Why "ngForm"? - A directive's [exportAs](../api/core/index/DirectiveMetadata-class.html#!#exportAs) property - tells Angular how to link the reference variable to the directive. - We set `name` to `ngForm` because the `NgControlName` directive's `exportAs` property happens to be "ngForm". - - This seems unintuitive at first until we realize that *all* control directives in the - Angular form family — including `NgForm`, `NgModel`, `NgControlName` and `NgControlGroup` — *exportAs* "ngForm" - and we only ever apply *one* of these directives to an element tag. - Consistency rules! - - Now we can control visibility of the "name" error message by binding properties of the `name` control to the message `
` element's `hidden` property. -+makeExample('forms-deprecated/ts/app/hero-form.component.html', - 'hidden-error-msg', - 'app/hero-form.component.html (excerpt)') -:marked - In this example, we hide the message when the control is valid or pristine; - pristine means the user hasn't changed the value since it was displayed in this form. - - This user experience is the developer's choice. Some folks want to see the message at all times. - If we ignore the `pristine` state, we would hide the message only when the value is valid. - If we arrive in this component with a new (blank) hero or an invalid hero, - we'll see the error message immediately, before we've done anything. - - Some folks find that behavior disconcerting. They only want to see the message when the user makes an invalid change. - Hiding the message while the control is "pristine" achieves that goal. - We'll see the significance of this choice when we [add a new hero](#new-hero) to the form. - - The Hero *Alter Ego* is optional so we can leave that be. - - Hero *Power* selection is required. - We can add the same kind of error handling to the `' + + '' + '
' + '
' + - '

{{ section.title }}

' + + '

{{ section.title }}

' + '