Compare commits
260 Commits
6.0.0-beta
...
merge_pr_m
Author | SHA1 | Date | |
---|---|---|---|
aabe16c08c | |||
b6c941053e | |||
c82cef8bc6 | |||
f8749bfb70 | |||
7d65356ae3 | |||
11f30fc351 | |||
b107131f8a | |||
11264e2174 | |||
b924ce3a62 | |||
e75f0cee18 | |||
8c358844dd | |||
e454c5a98e | |||
930ecacd86 | |||
5170ffe844 | |||
e95b61d42a | |||
8a85888773 | |||
83b32a0a0a | |||
8d34364ff5 | |||
142117f6bb | |||
79656e7f96 | |||
d100f1b187 | |||
4bd3e5f92f | |||
3f3be429c9 | |||
7e4c13f2de | |||
82a791223c | |||
ef99126aea | |||
c10c831b8e | |||
40ba009e25 | |||
d3827a0017 | |||
a7e1f236ff | |||
1f599818bd | |||
5a32d7e36f | |||
1ea41d48d3 | |||
25a43041d2 | |||
c593d69ce7 | |||
13ab91e05d | |||
f089bf5333 | |||
8e1e040f72 | |||
28240625e6 | |||
0d248079ba | |||
a4032296cc | |||
4180912538 | |||
094666da17 | |||
3a809cb431 | |||
b43b164a61 | |||
1dcbc12fd3 | |||
ae2e85e8ef | |||
aad431642a | |||
a81d599bfc | |||
7effb0016c | |||
f791862e52 | |||
b2f366b3b7 | |||
9eecb0b27f | |||
45eff4cc65 | |||
b3ffeaa22b | |||
f194d00366 | |||
b7544cccc6 | |||
894b098eb3 | |||
022ad4a420 | |||
a4f9e8180b | |||
e86b64b620 | |||
90e9c59e23 | |||
ca06af40f4 | |||
6091a954cc | |||
d27fca9301 | |||
5c89d6bffa | |||
3e6a86fb0a | |||
a7ebf5aadd | |||
b42921bbd2 | |||
722dec11b0 | |||
9e6268ba59 | |||
435f6eecd2 | |||
1c1cbba04b | |||
3b692a55a7 | |||
69a0578e00 | |||
b5ca275590 | |||
519f022b02 | |||
236a9320df | |||
28ac24444f | |||
99909bbf2c | |||
ee60bb5b36 | |||
f6120c09e7 | |||
e2bdef4cf6 | |||
8115edc82f | |||
a8b5465e24 | |||
9ce495b3d8 | |||
d40263447d | |||
31c5c1060a | |||
c9ebd60435 | |||
5a14e2238f | |||
3ceee99e22 | |||
28b23f954c | |||
ad17e5e791 | |||
c30d329faa | |||
991300b86c | |||
7c45db3a19 | |||
67cf11d071 | |||
49082d7ab2 | |||
6b627f67db | |||
5c320b4c2a | |||
ac2b04a5ab | |||
a63b764b54 | |||
2654357c72 | |||
4ec40c6ab2 | |||
80d424798e | |||
7fa2d4b503 | |||
f4845fae12 | |||
f693be3996 | |||
a73d5308e0 | |||
e1bf067090 | |||
884de18cba | |||
dfa2fb95d5 | |||
2639b4bffb | |||
978f97cc59 | |||
f1a063298e | |||
d241532488 | |||
f755db78dc | |||
5dd2b5135d | |||
7ac34e42a0 | |||
029dbf0e18 | |||
bba65e0f41 | |||
a069e08354 | |||
03d93c96a3 | |||
020338230f | |||
a1d86daa71 | |||
7078fbffb4 | |||
0aa9b46b79 | |||
831592c381 | |||
f628797d91 | |||
47f51c2ead | |||
ba9cd5bbc4 | |||
b54ad053f9 | |||
5b8eb9c5c7 | |||
0b683123d2 | |||
363498b6b4 | |||
a1bb56f739 | |||
5bb9fcad3e | |||
f4697f351e | |||
1d571b299d | |||
3a0b5a928c | |||
265ac8a106 | |||
fa7d8907d0 | |||
0220ce7002 | |||
3bd0b2ab28 | |||
a589ca0adb | |||
72f8abd7b3 | |||
20a900b648 | |||
6435ecd3c6 | |||
16d1700a8e | |||
b75cf3f70b | |||
4f19491fec | |||
8f36fd1374 | |||
2de0d4c1db | |||
5e4af7c550 | |||
8ec21fc325 | |||
eb48750705 | |||
be59c3a98c | |||
b333919722 | |||
235a235fab | |||
2d5e7d1b52 | |||
647b8595d0 | |||
0a1a397cd7 | |||
7f9b1b78f6 | |||
1e9484673d | |||
88bec238ac | |||
62e7b9da1e | |||
61341b2791 | |||
92a5876f51 | |||
a57df4ee20 | |||
92d7060cb0 | |||
7e9b120452 | |||
b081dfe705 | |||
4a4d749710 | |||
c878d55397 | |||
263a2eca88 | |||
44154e71fd | |||
0b2f7d13d0 | |||
420cc7afc6 | |||
5fc77c90cb | |||
c3484450b8 | |||
fbef94a8ee | |||
aa456edafc | |||
7007f51c35 | |||
bc1e22922a | |||
cf8d512e43 | |||
0b1f5d2127 | |||
dcf64a0d01 | |||
a9545aba4d | |||
d9ae70c699 | |||
a751649c8d | |||
3f5a3d6ea1 | |||
10a014d89e | |||
8feb8e5408 | |||
16dada28f5 | |||
67cf7128ae | |||
16e5b866d2 | |||
83d43ac850 | |||
cd25939be9 | |||
b58c3527e9 | |||
efc67ee5ef | |||
7a406a3896 | |||
98001a065d | |||
e442881ead | |||
b37cee36f9 | |||
e56de1025a | |||
64ae6d206e | |||
54a14312d1 | |||
7e95802cc1 | |||
e3e7044d06 | |||
eb3bfc25be | |||
94d769de71 | |||
66191e8a37 | |||
bec188506c | |||
4f869ff755 | |||
8f6047340e | |||
9744a1c966 | |||
0bcfae7cac | |||
140e7c00d1 | |||
941e88ff79 | |||
71ea931df5 | |||
545fdf10e2 | |||
7e928db204 | |||
cd4c0eab94 | |||
5b06069fd9 | |||
d0f3162e84 | |||
81537cb161 | |||
370ab66c4f | |||
2707012181 | |||
4d62be69c5 | |||
7e51e52f55 | |||
e81606c97a | |||
f791e9f081 | |||
3aa7e0228a | |||
9d3326caa7 | |||
1940b18124 | |||
0846784b98 | |||
0d10b9002e | |||
0c9ec37e26 | |||
9a0700f5bd | |||
ae7bc2238d | |||
5df626bbe1 | |||
5a624fa1be | |||
3a86940ca5 | |||
7b120b5f73 | |||
de25d1886e | |||
d77444b88a | |||
240aed29e0 | |||
bf29936af9 | |||
339ca83f9d | |||
447783e575 | |||
743d8bc845 | |||
f816666ede | |||
d3c2aa5f95 | |||
3cc1d76ee7 | |||
124283982b | |||
65cf1add97 | |||
8b14488827 | |||
f9fa157a09 | |||
eb8ddd2983 | |||
1aa2947f70 |
25
.circleci/bazel.rc
Normal file
25
.circleci/bazel.rc
Normal file
@ -0,0 +1,25 @@
|
||||
# These options are enabled when running on CI
|
||||
# We do this by copying this file to /etc/bazel.bazelrc at the start of the build.
|
||||
# See remote cache documentation in /docs/BAZEL.md
|
||||
|
||||
# Don't be spammy in the logs
|
||||
build --noshow_progress
|
||||
|
||||
# Don't run manual tests
|
||||
test --test_tag_filters=-manual
|
||||
|
||||
# Enable experimental CircleCI bazel remote cache proxy
|
||||
# See remote cache documentation in /docs/BAZEL.md
|
||||
build --experimental_remote_spawn_cache --remote_rest_cache=http://localhost:7643
|
||||
|
||||
# Prevent unstable environment variables from tainting cache keys
|
||||
build --experimental_strict_action_env
|
||||
|
||||
# Workaround https://github.com/bazelbuild/bazel/issues/3645
|
||||
# Bazel doesn't calculate the memory ceiling correctly when running under Docker.
|
||||
# Limit Bazel to consuming resources that fit in CircleCI "medium" class which is the default:
|
||||
# https://circleci.com/docs/2.0/configuration-reference/#resource_class
|
||||
build --local_resources=3072,2.0,1.0
|
||||
|
||||
# Retry in the event of flakes, eg. https://circleci.com/gh/angular/angular/31309
|
||||
test --flaky_test_attempts=2
|
@ -15,6 +15,13 @@
|
||||
var_1: &docker_image angular/ngcontainer:0.1.0
|
||||
var_2: &cache_key angular-{{ .Branch }}-{{ checksum "yarn.lock" }}-0.1.0
|
||||
|
||||
# See remote cache documentation in /docs/BAZEL.md
|
||||
var_3: &setup-bazel-remote-cache
|
||||
run:
|
||||
name: Start up bazel remote cache proxy
|
||||
command: ~/bazel-remote-proxy -backend circleci://
|
||||
background: true
|
||||
|
||||
# Settings common to each job
|
||||
anchor_1: &job_defaults
|
||||
working_directory: ~/ng
|
||||
@ -34,14 +41,16 @@ jobs:
|
||||
steps:
|
||||
- checkout:
|
||||
<<: *post_checkout
|
||||
# Check BUILD.bazel formatting before we have a node_modules directory
|
||||
# Then we don't need any exclude pattern to avoid checking those files
|
||||
- run: 'buildifier -mode=check $(find . -type f \( -name BUILD.bazel -or -name BUILD \)) ||
|
||||
(echo "BUILD files not formatted. Please run ''yarn buildifier''" ; exit 1)'
|
||||
# Run the skylark linter to check our Bazel rules
|
||||
- run: 'find . -type f -name "*.bzl" |
|
||||
xargs java -jar /usr/local/bin/Skylint_deploy.jar ||
|
||||
# See remote cache documentation in /docs/BAZEL.md
|
||||
- run: .circleci/setup_cache.sh
|
||||
- run: sudo cp .circleci/bazel.rc /etc/bazel.bazelrc
|
||||
- *setup-bazel-remote-cache
|
||||
|
||||
- run: 'yarn buildifier -mode=check ||
|
||||
(echo -e "\nBUILD files not formatted. Please run ''yarn buildifier''" ; exit 1)'
|
||||
- run: 'yarn skylint ||
|
||||
(echo -e "\n.bzl files have lint errors. Please run ''yarn skylint''"; exit 1)'
|
||||
|
||||
- restore_cache:
|
||||
key: *cache_key
|
||||
|
||||
@ -54,6 +63,11 @@ jobs:
|
||||
steps:
|
||||
- checkout:
|
||||
<<: *post_checkout
|
||||
# See remote cache documentation in /docs/BAZEL.md
|
||||
- run: .circleci/setup_cache.sh
|
||||
- run: sudo cp .circleci/bazel.rc /etc/bazel.bazelrc
|
||||
- *setup-bazel-remote-cache
|
||||
|
||||
- restore_cache:
|
||||
key: *cache_key
|
||||
|
||||
@ -62,7 +76,16 @@ jobs:
|
||||
# Use bazel query so that we explicitly ask for all buildable targets to be built as well
|
||||
# This avoids waiting for a build command to finish before running the first test
|
||||
# See https://github.com/bazelbuild/bazel/issues/4257
|
||||
- run: bazel query --output=label '//modules/... union //packages/... union //tools/...' | xargs bazel test --config=ci
|
||||
- run: bazel query --output=label '//modules/... union //packages/... union //tools/...' | xargs bazel test
|
||||
|
||||
# CircleCI will allow us to go back and view/download these artifacts from past builds.
|
||||
# Also we can use a service like https://buildsize.org/ to automatically track binary size of these artifacts.
|
||||
- store_artifacts:
|
||||
path: dist/bin/packages/core/test/bundling/hello_world/bundle.min.js
|
||||
destination: packages/core/test/bundling/hello_world/bundle.min.js
|
||||
- store_artifacts:
|
||||
path: dist/bin/packages/core/test/bundling/hello_world/bundle.min.js.brotli
|
||||
destination: packages/core/test/bundling/hello_world/bundle.min.js.brotli
|
||||
|
||||
- save_cache:
|
||||
key: *cache_key
|
||||
|
11
.circleci/setup_cache.sh
Executable file
11
.circleci/setup_cache.sh
Executable file
@ -0,0 +1,11 @@
|
||||
#!/bin/sh
|
||||
# Install bazel remote cache proxy
|
||||
# This is temporary until the feature is no longer experimental on CircleCI.
|
||||
# See remote cache documentation in /docs/BAZEL.md
|
||||
|
||||
set -u -e
|
||||
|
||||
readonly DOWNLOAD_URL="https://5-116431813-gh.circle-artifacts.com/0/pkg/bazel-remote-proxy-$(uname -s)_$(uname -m)"
|
||||
|
||||
curl --fail -o ~/bazel-remote-proxy "$DOWNLOAD_URL"
|
||||
chmod +x ~/bazel-remote-proxy
|
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@ -25,7 +25,7 @@ ISSUES MISSING IMPORTANT INFORMATION MAY BE CLOSED WITHOUT INVESTIGATION.
|
||||
## Minimal reproduction of the problem with instructions
|
||||
<!--
|
||||
For bug reports please provide the *STEPS TO REPRODUCE* and if possible a *MINIMAL DEMO* of the problem via
|
||||
https://plnkr.co or similar (you can use this template as a starting point: http://plnkr.co/edit/tpl:AvJOMERrnz94ekVua0u5).
|
||||
https://stackblitz.com or similar (you can use this template as a starting point: https://stackblitz.com/fork/angular-gitter).
|
||||
-->
|
||||
|
||||
## What is the motivation / use case for changing the behavior?
|
||||
|
46
.github/angular-robot.yml
vendored
46
.github/angular-robot.yml
vendored
@ -13,6 +13,30 @@ merge:
|
||||
# text to show when some checks are failing
|
||||
failureText: "The following checks are failing:"
|
||||
|
||||
# the g3 status will be added to your pull requests if they include files that match the patterns
|
||||
g3Status:
|
||||
# set to true to disable
|
||||
disabled: false
|
||||
# the name of the status
|
||||
context: "google3"
|
||||
# text to show when the status is pending
|
||||
pendingDesc: "Googler: test this change in google3 http://go/angular-g3sync"
|
||||
# text to show when the status is success
|
||||
successDesc: "Does not affect google3"
|
||||
# list of patterns to check for the files changed by the PR
|
||||
# this list must be manually kept in sync with google3/third_party/javascript/angular2/copy.bara.sky
|
||||
include:
|
||||
- "BUILD.bazel"
|
||||
- "LICENSE"
|
||||
- "WORKSPACE"
|
||||
- "modules/**"
|
||||
- "packages/**"
|
||||
# list of patterns to ignore for the files changed by the PR
|
||||
exclude:
|
||||
- "packages/language-service/**"
|
||||
- "**/.gitignore"
|
||||
- "**/.gitkeep"
|
||||
|
||||
# comment that will be added to a PR when there is a conflict, leave empty or set to false to disable
|
||||
mergeConflictComment: "Hi @{{PRAuthor}}! This PR has merge conflicts due to recent upstream merges.
|
||||
\nPlease help to unblock it by resolving these conflicts. Thanks!"
|
||||
@ -26,13 +50,15 @@ merge:
|
||||
noConflict: true
|
||||
# list of labels that a PR needs to have, checked with a regexp (e.g. "PR target:" will work for the label "PR target: master")
|
||||
requiredLabels:
|
||||
- "PR target:"
|
||||
- "PR target: *"
|
||||
- "cla: yes"
|
||||
|
||||
# list of labels that a PR shouldn't have, checked after the required labels with a regexp
|
||||
forbiddenLabels:
|
||||
- "PR target: TBD"
|
||||
- "PR action: cleanup"
|
||||
- "PR action: review"
|
||||
- "PR state: blocked"
|
||||
- "cla: no"
|
||||
|
||||
# list of PR statuses that need to be successful
|
||||
@ -54,15 +80,23 @@ merge:
|
||||
|
||||
# options for the triage plugin
|
||||
triage:
|
||||
# number of the milestone to apply when the issue has not been triaged yet
|
||||
needsTriageMilestone: 83,
|
||||
# number of the milestone to apply when the issue is triaged
|
||||
defaultMilestone: 82,
|
||||
# arrays of labels that determine if an issue is triaged
|
||||
triagedLabels:
|
||||
-
|
||||
- "type: bug"
|
||||
- "severity"
|
||||
- "freq"
|
||||
- "comp:"
|
||||
- "type: bug/fix"
|
||||
- "severity*"
|
||||
- "freq*"
|
||||
- "comp: *"
|
||||
-
|
||||
- "type: feature"
|
||||
- "comp:"
|
||||
- "comp: *"
|
||||
-
|
||||
- "type: refactor"
|
||||
- "comp: *"
|
||||
-
|
||||
- "type: RFC / Discussion / question"
|
||||
- "comp: *"
|
||||
|
@ -44,6 +44,7 @@ groups:
|
||||
all:
|
||||
users: all
|
||||
required: 1
|
||||
rejection_value: -999
|
||||
# In this group, your self-approval does not count
|
||||
author_approval:
|
||||
auto: false
|
||||
|
@ -56,7 +56,6 @@ env:
|
||||
- CI_MODE=aio
|
||||
- CI_MODE=aio_e2e AIO_SHARD=0
|
||||
- CI_MODE=aio_e2e AIO_SHARD=1
|
||||
- CI_MODE=bazel
|
||||
|
||||
matrix:
|
||||
fast_finish: true
|
||||
|
@ -32,6 +32,7 @@ filegroup(
|
||||
"reflect-metadata",
|
||||
"source-map-support",
|
||||
"minimist",
|
||||
"tslib",
|
||||
] for ext in [
|
||||
"*.js",
|
||||
"*.json",
|
||||
|
167
CHANGELOG.md
167
CHANGELOG.md
@ -1,3 +1,170 @@
|
||||
<a name="6.0.0-beta.5"></a>
|
||||
# [6.0.0-beta.5](https://github.com/angular/angular/compare/6.0.0-beta.4...6.0.0-beta.5) (2018-02-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **animations:** report correct totalTime value even during noOp animations ([#22225](https://github.com/angular/angular/issues/22225)) ([e1bf067](https://github.com/angular/angular/commit/e1bf067))
|
||||
* **common:** correct mapping of Observable methods ([#20518](https://github.com/angular/angular/issues/20518)) ([2639b4b](https://github.com/angular/angular/commit/2639b4b)), closes [#20516](https://github.com/angular/angular/issues/20516)
|
||||
* **common:** then and else template might be set to null ([#22298](https://github.com/angular/angular/issues/22298)) ([8115edc](https://github.com/angular/angular/commit/8115edc))
|
||||
* **compiler-cli:** add missing entry point to package, update tsickle ([#22295](https://github.com/angular/angular/issues/22295)) ([28ac244](https://github.com/angular/angular/commit/28ac244))
|
||||
* **core:** properly handle function without prototype in reflector ([#22284](https://github.com/angular/angular/issues/22284)) ([a7ebf5a](https://github.com/angular/angular/commit/a7ebf5a)), closes [#19978](https://github.com/angular/angular/issues/19978)
|
||||
* **core:** require factory to be provided for shakeable InjectionToken ([#22207](https://github.com/angular/angular/issues/22207)) ([f755db7](https://github.com/angular/angular/commit/f755db7)), closes [#22205](https://github.com/angular/angular/issues/22205)
|
||||
* **forms:** set state before emitting a value from ngModelChange ([#21514](https://github.com/angular/angular/issues/21514)) ([3e6a86f](https://github.com/angular/angular/commit/3e6a86f)), closes [#21513](https://github.com/angular/angular/issues/21513)
|
||||
* **core:** set `preserveWhitespaces` to false by default ([#22046](https://github.com/angular/angular/issues/22046)) ([f1a0632](https://github.com/angular/angular/commit/f1a0632)), closes [#22027](https://github.com/angular/angular/issues/22027)
|
||||
|
||||
### Features
|
||||
|
||||
* **common:** better error message when non-template element used in NgIf ([#22274](https://github.com/angular/angular/issues/22274)) ([67cf11d](https://github.com/angular/angular/commit/67cf11d)), closes [#16410](https://github.com/angular/angular/issues/16410)
|
||||
* **compiler-cli:** Check unvalidated combination of ngc and TypeScript ([#22293](https://github.com/angular/angular/issues/22293)) ([3ceee99](https://github.com/angular/angular/commit/3ceee99)), closes [#20669](https://github.com/angular/angular/issues/20669)
|
||||
* **core:** support metadata reflection for native class types ([#22356](https://github.com/angular/angular/issues/22356)) ([5c89d6b](https://github.com/angular/angular/commit/5c89d6b)), closes [#21731](https://github.com/angular/angular/issues/21731)
|
||||
* **platform-browser:** do not throw error when Hammer.js not loaded ([#22257](https://github.com/angular/angular/issues/22257)) ([991300b](https://github.com/angular/angular/commit/991300b)), closes [#16992](https://github.com/angular/angular/issues/16992)
|
||||
* **platform-browser:** fix [#19604](https://github.com/angular/angular/issues/19604), can config hammerOptions ([#21979](https://github.com/angular/angular/issues/21979)) ([1d571b2](https://github.com/angular/angular/commit/1d571b2))
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* **animations:** When animation is triggered within a disabled zone, the
|
||||
associated event (which an instance of AnimationEvent) will no longer
|
||||
report the totalTime as 0 (it will emit the actual time of the
|
||||
animation). To detect if an animation event is reporting a disabled
|
||||
animation then the `event.disabled` property can be used instead.
|
||||
|
||||
* **forms:** ngModelChange is now emitted after the value/validity is updated on its control.
|
||||
|
||||
Previously, ngModelChange was emitted before its underlying control was updated.
|
||||
This was fine if you passed through the value directly through the $event keyword, e.g.
|
||||
|
||||
```
|
||||
<input [(ngModel)]="name" (ngModelChange)="onChange($event)">
|
||||
|
||||
onChange(value) {
|
||||
console.log(value); // would log updated value
|
||||
}
|
||||
```
|
||||
|
||||
However, if you had a handler for the ngModelChange event that checked the value through the control,
|
||||
you would get the old value rather than the updated value. e.g:
|
||||
|
||||
```
|
||||
<input #modelDir="ngModel" [(ngModel)]="name" (ngModelChange)="onChange(modelDir)">
|
||||
|
||||
onChange(ngModel: NgModel) {
|
||||
console.log(ngModel.value); // would log old value, not updated value
|
||||
}
|
||||
```
|
||||
|
||||
Now the value and validity will be updated before the ngModelChange event is emitted,
|
||||
so the same setup will log the updated value.
|
||||
|
||||
```
|
||||
onChange(ngModel: NgModel) {
|
||||
console.log(ngModel.value); // will log updated value
|
||||
}
|
||||
```
|
||||
|
||||
We think this order will be less confusing when the control is checked directly.
|
||||
You will only need to update your app if it has relied on this bug to keep track of the old control value.
|
||||
If that is the case, you should be able to track the old value directly by saving it on your component.
|
||||
|
||||
<a name="5.2.6"></a>
|
||||
## [5.2.6](https://github.com/angular/angular/compare/5.2.5...5.2.6) (2018-02-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **common:** correct mapping of Observable methods ([#20518](https://github.com/angular/angular/issues/20518)) ([ce5e8fa](https://github.com/angular/angular/commit/ce5e8fa)), closes [#20516](https://github.com/angular/angular/issues/20516)
|
||||
* **common:** then and else template might be set to null ([#22298](https://github.com/angular/angular/issues/22298)) ([af6a056](https://github.com/angular/angular/commit/af6a056))
|
||||
* **compiler-cli:** add missing entry point to package, update tsickle ([#22295](https://github.com/angular/angular/issues/22295)) ([c5418c7](https://github.com/angular/angular/commit/c5418c7))
|
||||
* **core:** properly handle function without prototype in reflector ([#22284](https://github.com/angular/angular/issues/22284)) ([5ec38f2](https://github.com/angular/angular/commit/5ec38f2)), closes [#19978](https://github.com/angular/angular/issues/19978)
|
||||
* **core:** support metadata reflection for native class types ([#22356](https://github.com/angular/angular/issues/22356)) ([ee91de9](https://github.com/angular/angular/commit/ee91de9)), closes [#21731](https://github.com/angular/angular/issues/21731)
|
||||
|
||||
<a name="6.0.0-beta.4"></a>
|
||||
# [6.0.0-beta.4](https://github.com/angular/angular/compare/6.0.0-beta.3...6.0.0-beta.4) (2018-02-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **bazel:** allow TS to read ambient typings ([#21876](https://github.com/angular/angular/issues/21876)) ([b081dfe](https://github.com/angular/angular/commit/b081dfe)), closes [#21872](https://github.com/angular/angular/issues/21872)
|
||||
* **bazel:** improve error message for missing assets ([#22096](https://github.com/angular/angular/issues/22096)) ([dcf64a0](https://github.com/angular/angular/commit/dcf64a0)), closes [#22095](https://github.com/angular/angular/issues/22095)
|
||||
* **common:** add locale currency values ([#21783](https://github.com/angular/angular/issues/21783)) ([420cc7a](https://github.com/angular/angular/commit/420cc7a)), closes [#20385](https://github.com/angular/angular/issues/20385)
|
||||
* **common:** round currencies based on decimal digits in `CurrencyPipe` ([#21783](https://github.com/angular/angular/issues/21783)) ([44154e7](https://github.com/angular/angular/commit/44154e7)), closes [#10189](https://github.com/angular/angular/issues/10189)
|
||||
* **common:** weaken AsyncPipe transform signature ([#22169](https://github.com/angular/angular/issues/22169)) ([be59c3a](https://github.com/angular/angular/commit/be59c3a))
|
||||
* **compiler:** make unary plus operator consistent to JavaScript ([#22154](https://github.com/angular/angular/issues/22154)) ([72f8abd](https://github.com/angular/angular/commit/72f8abd)), closes [#22089](https://github.com/angular/angular/issues/22089)
|
||||
* **core:** add stacktrace in log when error during cleanup component in TestBed ([#22162](https://github.com/angular/angular/issues/22162)) ([16d1700](https://github.com/angular/angular/commit/16d1700))
|
||||
* **core:** ensure initial value of QueryList length ([#21980](https://github.com/angular/angular/issues/21980)) ([#21982](https://github.com/angular/angular/issues/21982)) ([e56de10](https://github.com/angular/angular/commit/e56de10)), closes [#21980](https://github.com/angular/angular/issues/21980)
|
||||
* **core:** use appropriate inert document strategy for Firefox & Safari ([#17019](https://github.com/angular/angular/issues/17019)) ([a751649](https://github.com/angular/angular/commit/a751649))
|
||||
* **forms:** make Validators.email support optional controls ([#20869](https://github.com/angular/angular/issues/20869)) ([140e7c0](https://github.com/angular/angular/commit/140e7c0))
|
||||
* **forms:** prevent event emission on enable/disable when emitEvent is false ([#12366](https://github.com/angular/angular/issues/12366)) ([#21018](https://github.com/angular/angular/issues/21018)) ([0bcfae7](https://github.com/angular/angular/commit/0bcfae7))
|
||||
* **forms:** set state before emitting a value from ngModelChange ([#21514](https://github.com/angular/angular/issues/21514)) ([9744a1c](https://github.com/angular/angular/commit/9744a1c)), closes [#21513](https://github.com/angular/angular/issues/21513)
|
||||
* **language-service:** correct instructions to install the language service ([#22000](https://github.com/angular/angular/issues/22000)) ([b37cee3](https://github.com/angular/angular/commit/b37cee3))
|
||||
* **platform-browser:** add @Injectable where it was missing ([#22005](https://github.com/angular/angular/issues/22005)) ([0a1a397](https://github.com/angular/angular/commit/0a1a397))
|
||||
* **platform-browser:** support 0/false/null values in transfer_state ([#22179](https://github.com/angular/angular/issues/22179)) ([6435ecd](https://github.com/angular/angular/commit/6435ecd))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **bazel:** allow explicit specification of factories ([#22003](https://github.com/angular/angular/issues/22003)) ([e442881](https://github.com/angular/angular/commit/e442881))
|
||||
* **compiler:** mark @NgModules in provider lists for identification at runtime ([#22005](https://github.com/angular/angular/issues/22005)) ([2d5e7d1](https://github.com/angular/angular/commit/2d5e7d1))
|
||||
* **forms:** multiple validators for array method ([#20766](https://github.com/angular/angular/issues/20766)) ([941e88f](https://github.com/angular/angular/commit/941e88f)), closes [#20665](https://github.com/angular/angular/issues/20665)
|
||||
* change @Injectable() to support tree-shakeable tokens ([#22005](https://github.com/angular/angular/issues/22005)) ([235a235](https://github.com/angular/angular/commit/235a235))
|
||||
|
||||
<a name="5.2.5"></a>
|
||||
## [5.2.5](https://github.com/angular/angular/compare/5.2.4...5.2.5) (2018-02-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **aio:** update Firebase redirects and SW routes ([#21763](https://github.com/angular/angular/pull/21763)) ([#22104](https://github.com/angular/angular/pull/22104)) ([15ff7ba](https://github.com/angular/angular/commit/15ff7ba)), closes [#21377](https://github.com/angular/angular/issues/21377)
|
||||
* **bazel:** allow TS to read ambient typings ([#21876](https://github.com/angular/angular/issues/21876)) ([d57fd0b](https://github.com/angular/angular/commit/d57fd0b)), closes [#21872](https://github.com/angular/angular/issues/21872)
|
||||
* **bazel:** improve error message for missing assets ([#22096](https://github.com/angular/angular/issues/22096)) ([c5ec8d9](https://github.com/angular/angular/commit/c5ec8d9)), closes [#22095](https://github.com/angular/angular/issues/22095)
|
||||
* **common:** weaken AsyncPipe transform signature ([#22169](https://github.com/angular/angular/issues/22169)) ([c6bdc83](https://github.com/angular/angular/commit/c6bdc83))
|
||||
* **compiler:** make unary plus operator consistent to JavaScript ([#22154](https://github.com/angular/angular/issues/22154)) ([1b8ea10](https://github.com/angular/angular/commit/1b8ea10)), closes [#22089](https://github.com/angular/angular/issues/22089)
|
||||
* **core:** add stacktrace in log when error during cleanup component in TestBed ([#22162](https://github.com/angular/angular/issues/22162)) ([c4f841f](https://github.com/angular/angular/commit/c4f841f))
|
||||
* **core:** ensure initial value of QueryList length ([#21980](https://github.com/angular/angular/issues/21980)) ([#21982](https://github.com/angular/angular/issues/21982)) ([47b73fd](https://github.com/angular/angular/commit/47b73fd)), closes [#21980](https://github.com/angular/angular/issues/21980)
|
||||
* **core:** use appropriate inert document strategy for Firefox & Safari ([#17019](https://github.com/angular/angular/issues/17019)) ([47b71d9](https://github.com/angular/angular/commit/47b71d9))
|
||||
* **forms:** prevent event emission on enable/disable when emitEvent is false ([#12366](https://github.com/angular/angular/issues/12366)) ([#21018](https://github.com/angular/angular/issues/21018)) ([56b9591](https://github.com/angular/angular/commit/56b9591))
|
||||
* **language-service:** correct instructions to install the language service ([#22000](https://github.com/angular/angular/issues/22000)) ([0b23573](https://github.com/angular/angular/commit/0b23573))
|
||||
* **platform-browser:** support 0/false/null values in transfer_state ([#22179](https://github.com/angular/angular/issues/22179)) ([da6ab91](https://github.com/angular/angular/commit/da6ab91))
|
||||
|
||||
<a name="6.0.0-beta.3"></a>
|
||||
# [6.0.0-beta.3](https://github.com/angular/angular/compare/6.0.0-beta.2...6.0.0-beta.3) (2018-02-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **common:** don't convert null to a string when flushing a mock request ([#21417](https://github.com/angular/angular/issues/21417)) ([8b14488](https://github.com/angular/angular/commit/8b14488)), closes [#20744](https://github.com/angular/angular/issues/20744)
|
||||
* **core:** fix [#20582](https://github.com/angular/angular/issues/20582), don't need to wrap zone in location change listener ([#20640](https://github.com/angular/angular/issues/20640)) ([f791e9f](https://github.com/angular/angular/commit/f791e9f))
|
||||
* **core:** fix proper propagation of subscriptions in EventEmitter ([#22016](https://github.com/angular/angular/issues/22016)) ([e81606c](https://github.com/angular/angular/commit/e81606c)), closes [#21999](https://github.com/angular/angular/issues/21999)
|
||||
* **core:** should check Zone existance when scheduleMicroTask ([#20656](https://github.com/angular/angular/issues/20656)) ([3a86940](https://github.com/angular/angular/commit/3a86940))
|
||||
* **forms:** publish missing types ([#19941](https://github.com/angular/angular/issues/19941)) ([2707012](https://github.com/angular/angular/commit/2707012))
|
||||
* **ivy:** generate correct interpolations ([#21946](https://github.com/angular/angular/issues/21946)) ([3cc1d76](https://github.com/angular/angular/commit/3cc1d76))
|
||||
* **ivy:** generate lifecycle pattern ([#21865](https://github.com/angular/angular/issues/21865)) ([f816666](https://github.com/angular/angular/commit/f816666))
|
||||
* **ivy:** improve `bindV` perf and memory usage ([#21881](https://github.com/angular/angular/issues/21881)) ([0846784](https://github.com/angular/angular/commit/0846784))
|
||||
* **ivy:** remove unnecessary parameter of NgOnChangesFeature ([#21879](https://github.com/angular/angular/issues/21879)) ([65cf1ad](https://github.com/angular/angular/commit/65cf1ad))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **compiler-cli:** reflect static methods added to classes in metadata ([#21926](https://github.com/angular/angular/issues/21926)) ([eb8ddd2](https://github.com/angular/angular/commit/eb8ddd2))
|
||||
* **ivy:** add canonical example of a pipe. ([#21834](https://github.com/angular/angular/issues/21834)) ([743d8bc](https://github.com/angular/angular/commit/743d8bc))
|
||||
* **ivy:** add support for attributes on ng-content nodes ([#21935](https://github.com/angular/angular/issues/21935)) ([1aa2947](https://github.com/angular/angular/commit/1aa2947))
|
||||
* **ivy:** memoize array literals in render3 ([#21973](https://github.com/angular/angular/issues/21973)) ([4d62be6](https://github.com/angular/angular/commit/4d62be6))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **ivy:** improve Uglify configuration in hello world integration test ([#21985](https://github.com/angular/angular/issues/21985)) ([7e51e52](https://github.com/angular/angular/commit/7e51e52))
|
||||
|
||||
|
||||
|
||||
<a name="5.2.4"></a>
|
||||
## [5.2.4](https://github.com/angular/angular/compare/5.2.3...5.2.4) (2018-02-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **common:** don't convert null to a string when flushing a mock request ([#21417](https://github.com/angular/angular/issues/21417)) ([c4fb696](https://github.com/angular/angular/commit/c4fb696)), closes [#20744](https://github.com/angular/angular/issues/20744)
|
||||
* **core:** fix [#20582](https://github.com/angular/angular/issues/20582), don't need to wrap zone in location change listener ([#22007](https://github.com/angular/angular/issues/22007)) ([ce51ea9](https://github.com/angular/angular/commit/ce51ea9))
|
||||
* **core:** fix proper propagation of subscriptions in EventEmitter ([#22016](https://github.com/angular/angular/issues/22016)) ([c6645e7](https://github.com/angular/angular/commit/c6645e7)), closes [#21999](https://github.com/angular/angular/issues/21999)
|
||||
* **core:** should check Zone existance when scheduleMicroTask ([#20656](https://github.com/angular/angular/issues/20656)) ([aa9ba7f](https://github.com/angular/angular/commit/aa9ba7f))
|
||||
|
||||
|
||||
|
||||
<a name="6.0.0-beta.2"></a>
|
||||
# [6.0.0-beta.2](https://github.com/angular/angular/compare/6.0.0-beta.1...6.0.0-beta.2) (2018-01-31)
|
||||
|
||||
|
12
CODE_OF_CONDUCT.md
Normal file
12
CODE_OF_CONDUCT.md
Normal file
@ -0,0 +1,12 @@
|
||||
# Contributor Code of Conduct
|
||||
## Version 0.3b-angular
|
||||
|
||||
As contributors and maintainers of the Angular project, we pledge to respect everyone who contributes by posting issues, updating documentation, submitting pull requests, providing feedback in comments, and any other activities.
|
||||
|
||||
Communication through any of Angular's channels (GitHub, Gitter, IRC, mailing lists, Google+, Twitter, etc.) must be constructive and never resort to personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
|
||||
|
||||
We promise to extend courtesy and respect to everyone involved in this project regardless of gender, gender identity, sexual orientation, disability, age, race, ethnicity, religion, or level of experience. We expect anyone contributing to the Angular project to do the same.
|
||||
|
||||
If any member of the community violates this code of conduct, the maintainers of the Angular project may take action, removing issues, comments, and PRs or blocking accounts as deemed appropriate.
|
||||
|
||||
If you are subject to or witness unacceptable behavior, or have any other concerns, please email us at [conduct@angular.io](mailto:conduct@angular.io).
|
@ -51,7 +51,7 @@ and help you to craft the change so that it is successfully accepted into the pr
|
||||
|
||||
Before you submit an issue, please search the issue tracker, maybe an issue for your problem already exists and the discussion might inform you of workarounds readily available.
|
||||
|
||||
We want to fix all the issues as soon as possible, but before fixing a bug we need to reproduce and confirm it. In order to reproduce bugs we will systematically ask you to provide a minimal reproduction scenario using http://plnkr.co. Having a live, reproducible scenario gives us wealth of important information without going back & forth to you with additional questions like:
|
||||
We want to fix all the issues as soon as possible, but before fixing a bug we need to reproduce and confirm it. In order to reproduce bugs, we will systematically ask you to provide a minimal reproduction scenario using http://plnkr.co. Having a live, reproducible scenario gives us a wealth of important information without going back & forth to you with additional questions like:
|
||||
|
||||
- version of Angular used
|
||||
- 3rd-party libraries and their versions
|
||||
@ -61,7 +61,7 @@ A minimal reproduce scenario using http://plnkr.co/ allows us to quickly confirm
|
||||
|
||||
We will be insisting on a minimal reproduce scenario in order to save maintainers time and ultimately be able to fix more bugs. Interestingly, from our experience users often find coding problems themselves while preparing a minimal plunk. We understand that sometimes it might be hard to extract essentials bits of code from a larger code-base but we really need to isolate the problem before we can fix it.
|
||||
|
||||
Unfortunately we are not able to investigate / fix bugs without a minimal reproduction, so if we don't hear back from you we are going to close an issue that don't have enough info to be reproduced.
|
||||
Unfortunately, we are not able to investigate / fix bugs without a minimal reproduction, so if we don't hear back from you we are going to close an issue that doesn't have enough info to be reproduced.
|
||||
|
||||
You can file new issues by filling out our [new issue form](https://github.com/angular/angular/issues/new).
|
||||
|
||||
@ -173,12 +173,12 @@ The **header** is mandatory and the **scope** of the header is optional.
|
||||
Any line of the commit message cannot be longer 100 characters! This allows the message to be easier
|
||||
to read on GitHub as well as in various git tools.
|
||||
|
||||
Footer should contain a [closing reference to an issue](https://help.github.com/articles/closing-issues-via-commit-messages/) if any.
|
||||
The footer should contain a [closing reference to an issue](https://help.github.com/articles/closing-issues-via-commit-messages/) if any.
|
||||
|
||||
Samples: (even more [samples](https://github.com/angular/angular/commits/master))
|
||||
|
||||
```
|
||||
docs(changelog): update change log to beta.5
|
||||
docs(changelog): update changelog to beta.5
|
||||
```
|
||||
```
|
||||
fix(release): need to depend on latest rxjs and zone.js
|
||||
@ -203,7 +203,7 @@ Must be one of the following:
|
||||
* **test**: Adding missing tests or correcting existing tests
|
||||
|
||||
### Scope
|
||||
The scope should be the name of the npm package affected (as perceived by person reading changelog generated from commit messages.
|
||||
The scope should be the name of the npm package affected (as perceived by the person reading the changelog generated from commit messages.
|
||||
|
||||
The following is the list of supported scopes:
|
||||
|
||||
@ -232,10 +232,10 @@ There are currently a few exceptions to the "use package name" rule:
|
||||
* none/empty string: useful for `style`, `test` and `refactor` changes that are done across all packages (e.g. `style: add missing semicolons`)
|
||||
|
||||
### Subject
|
||||
The subject contains succinct description of the change:
|
||||
The subject contains a succinct description of the change:
|
||||
|
||||
* use the imperative, present tense: "change" not "changed" nor "changes"
|
||||
* don't capitalize first letter
|
||||
* don't capitalize the first letter
|
||||
* no dot (.) at the end
|
||||
|
||||
### Body
|
||||
@ -266,7 +266,7 @@ changes to be accepted, the CLA must be signed. It's a quick process, we promise
|
||||
* https://help.github.com/articles/setting-your-commit-email-address-in-git/
|
||||
* https://stackoverflow.com/questions/37245303/what-does-usera-committed-with-userb-13-days-ago-on-github-mean
|
||||
* https://help.github.com/articles/about-commit-email-addresses/
|
||||
* https://help.github.com/articles/blocking-command-line-pushes-that-expose-your-personal-email-address/
|
||||
* https://help.github.com/articles/blocking-command-line-pushes-that-expose-your-personal-email-address/
|
||||
|
||||
Note that if you have more than one Git identity, it is important to verify that you are logged in with the same ID with which you signed the CLA, before you commit changes. If not, your PR will fail the CLA check.
|
||||
|
||||
|
55
WORKSPACE
55
WORKSPACE
@ -1,11 +1,14 @@
|
||||
workspace(name = "angular")
|
||||
|
||||
load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")
|
||||
# Using a pre-release snapshot to pick up a commit that makes all nodejs_binary
|
||||
# programs produce source-mapped stack traces and uglify sourcemaps.
|
||||
RULES_NODEJS_VERSION = "4303cbef12e5e252ad66cc35cff1123e3a44ee83"
|
||||
|
||||
git_repository(
|
||||
http_archive(
|
||||
name = "build_bazel_rules_nodejs",
|
||||
remote = "https://github.com/bazelbuild/rules_nodejs.git",
|
||||
commit = "230d39a391226f51c03448f91eb61370e2e58c42",
|
||||
url = "https://github.com/bazelbuild/rules_nodejs/archive/%s.zip" % RULES_NODEJS_VERSION,
|
||||
strip_prefix = "rules_nodejs-%s" % RULES_NODEJS_VERSION,
|
||||
sha256 = "fccb9a7122f339d89c9994dc0fea33c737dd76e66281d0da0cb841da5f1edec7",
|
||||
)
|
||||
|
||||
load("@build_bazel_rules_nodejs//:defs.bzl", "check_bazel_version", "node_repositories")
|
||||
@ -13,10 +16,13 @@ load("@build_bazel_rules_nodejs//:defs.bzl", "check_bazel_version", "node_reposi
|
||||
check_bazel_version("0.9.0")
|
||||
node_repositories(package_json = ["//:package.json"])
|
||||
|
||||
git_repository(
|
||||
RULES_TYPESCRIPT_VERSION = "d3cc5cd72d89aee0e4c2553ae1b99c707ecbef4e"
|
||||
|
||||
http_archive(
|
||||
name = "build_bazel_rules_typescript",
|
||||
remote = "https://github.com/bazelbuild/rules_typescript.git",
|
||||
commit = "eb3244363e1cb265c84e723b347926f28c29aa35"
|
||||
url = "https://github.com/bazelbuild/rules_typescript/archive/%s.zip" % RULES_TYPESCRIPT_VERSION,
|
||||
strip_prefix = "rules_typescript-%s" % RULES_TYPESCRIPT_VERSION,
|
||||
sha256 = "a233fcca41c3e59f639ac71c396edb30e9e9716cf8ed5fb20b51ff8910d5d895",
|
||||
)
|
||||
|
||||
load("@build_bazel_rules_typescript//:defs.bzl", "ts_setup_workspace")
|
||||
@ -28,13 +34,16 @@ local_repository(
|
||||
path = "node_modules/rxjs/src",
|
||||
)
|
||||
|
||||
git_repository(
|
||||
# This commit matches the version of buildifier in angular/ngcontainer
|
||||
# If you change this, also check if it matches the version in the angular/ngcontainer
|
||||
# version in /.circleci/config.yml
|
||||
BAZEL_BUILDTOOLS_VERSION = "b3b620e8bcff18ed3378cd3f35ebeb7016d71f71"
|
||||
|
||||
http_archive(
|
||||
name = "com_github_bazelbuild_buildtools",
|
||||
remote = "https://github.com/bazelbuild/buildtools.git",
|
||||
# Note, this commit matches the version of buildifier in angular/ngcontainer
|
||||
# If you change this, also check if it matches the version in the angular/ngcontainer
|
||||
# version in /.circleci/config.yml
|
||||
commit = "b3b620e8bcff18ed3378cd3f35ebeb7016d71f71",
|
||||
url = "https://github.com/bazelbuild/buildtools/archive/%s.zip" % BAZEL_BUILDTOOLS_VERSION,
|
||||
strip_prefix = "buildtools-%s" % BAZEL_BUILDTOOLS_VERSION,
|
||||
sha256 = "dad19224258ed67cbdbae9b7befb785c3b966e5a33b04b3ce58ddb7824b97d73",
|
||||
)
|
||||
|
||||
http_archive(
|
||||
@ -56,3 +65,23 @@ http_archive(
|
||||
strip_prefix = "bazel-9755c72b48866ed034bd28aa033e9abd27431b1e",
|
||||
sha256 = "5b8443fc3481b5fcd9e7f348e1dd93c1397f78b223623c39eb56494c55f41962",
|
||||
)
|
||||
|
||||
# We have a source dependency on the Devkit repository, because it's built with
|
||||
# Bazel.
|
||||
# This allows us to edit sources and have the effect appear immediately without
|
||||
# re-packaging or "npm link"ing.
|
||||
# Even better, things like aspects will visit the entire graph including
|
||||
# ts_library rules in the devkit repository.
|
||||
http_archive(
|
||||
name = "angular_devkit",
|
||||
url = "https://github.com/angular/devkit/archive/v0.3.1.zip",
|
||||
strip_prefix = "devkit-0.3.1",
|
||||
sha256 = "31d4b597fe9336650acf13df053c1c84dcbe9c29c6a833bcac3819cd3fd8cad3",
|
||||
)
|
||||
|
||||
http_archive(
|
||||
name = "org_brotli",
|
||||
url = "https://github.com/google/brotli/archive/v1.0.2.zip",
|
||||
strip_prefix = "brotli-1.0.2",
|
||||
sha256 = "b43d5d6bc40f2fa6c785b738d86c6bbe022732fe25196ebbe43b9653a025920d",
|
||||
)
|
||||
|
@ -39,7 +39,7 @@
|
||||
],
|
||||
"e2e": {
|
||||
"protractor": {
|
||||
"config": "./protractor.conf.js"
|
||||
"config": "tests/e2e/protractor.conf.js"
|
||||
}
|
||||
},
|
||||
"lint": [
|
||||
@ -50,12 +50,12 @@
|
||||
"project": "src/tsconfig.spec.json"
|
||||
},
|
||||
{
|
||||
"project": "e2e/tsconfig.e2e.json"
|
||||
"project": "tests/e2e/tsconfig.e2e.json"
|
||||
}
|
||||
],
|
||||
"test": {
|
||||
"karma": {
|
||||
"config": "./karma.conf.js"
|
||||
"config": "src/karma.conf.js"
|
||||
}
|
||||
},
|
||||
"defaults": {
|
||||
|
@ -105,8 +105,7 @@ The general setup is as follows:
|
||||
* Open a terminal, ensure the dependencies are installed; run an initial doc generation; then start the doc-viewer:
|
||||
|
||||
```bash
|
||||
yarn
|
||||
yarn docs
|
||||
yarn setup
|
||||
yarn start
|
||||
```
|
||||
|
||||
|
@ -1,12 +1,12 @@
|
||||
// #docregion
|
||||
import { Component, Input, AfterViewInit, ViewChild, ComponentFactoryResolver, OnDestroy } from '@angular/core';
|
||||
import { Component, Input, OnInit, ViewChild, ComponentFactoryResolver, OnDestroy } from '@angular/core';
|
||||
|
||||
import { AdDirective } from './ad.directive';
|
||||
import { AdItem } from './ad-item';
|
||||
import { AdComponent } from './ad.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-add-banner',
|
||||
selector: 'app-ad-banner',
|
||||
// #docregion ad-host
|
||||
template: `
|
||||
<div class="ad-banner">
|
||||
@ -17,16 +17,15 @@ import { AdComponent } from './ad.component';
|
||||
// #enddocregion ad-host
|
||||
})
|
||||
// #docregion class
|
||||
export class AdBannerComponent implements AfterViewInit, OnDestroy {
|
||||
export class AdBannerComponent implements OnInit, OnDestroy {
|
||||
@Input() ads: AdItem[];
|
||||
currentAddIndex: number = -1;
|
||||
currentAdIndex: number = -1;
|
||||
@ViewChild(AdDirective) adHost: AdDirective;
|
||||
subscription: any;
|
||||
interval: any;
|
||||
|
||||
constructor(private componentFactoryResolver: ComponentFactoryResolver) { }
|
||||
|
||||
ngAfterViewInit() {
|
||||
ngOnInit() {
|
||||
this.loadComponent();
|
||||
this.getAds();
|
||||
}
|
||||
@ -36,8 +35,8 @@ export class AdBannerComponent implements AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
loadComponent() {
|
||||
this.currentAddIndex = (this.currentAddIndex + 1) % this.ads.length;
|
||||
let adItem = this.ads[this.currentAddIndex];
|
||||
this.currentAdIndex = (this.currentAdIndex + 1) % this.ads.length;
|
||||
let adItem = this.ads[this.currentAdIndex];
|
||||
|
||||
let componentFactory = this.componentFactoryResolver.resolveComponentFactory(adItem.component);
|
||||
|
||||
|
@ -8,7 +8,7 @@ import { AdItem } from './ad-item';
|
||||
selector: 'app-root',
|
||||
template: `
|
||||
<div>
|
||||
<app-add-banner [ads]="ads"></app-add-banner>
|
||||
<app-ad-banner [ads]="ads"></app-ad-banner>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
|
@ -15,6 +15,7 @@ describe('Form Validation Tests', function () {
|
||||
});
|
||||
|
||||
tests('Template-Driven Form');
|
||||
bobTests();
|
||||
});
|
||||
|
||||
describe('Reactive form', () => {
|
||||
|
@ -20,7 +20,7 @@ export function forbiddenNameValidator(nameRe: RegExp): ValidatorFn {
|
||||
// #enddocregion directive-providers
|
||||
})
|
||||
export class ForbiddenValidatorDirective implements Validator {
|
||||
@Input() forbiddenName: string;
|
||||
@Input('appForbiddenName') forbiddenName: string;
|
||||
|
||||
validate(control: AbstractControl): {[key: string]: any} {
|
||||
return this.forbiddenName ? forbiddenNameValidator(new RegExp(this.forbiddenName, 'i'))(control)
|
||||
|
@ -12,7 +12,7 @@
|
||||
<!-- #docregion name-with-error-msg -->
|
||||
<!-- #docregion name-input -->
|
||||
<input id="name" name="name" class="form-control"
|
||||
required minlength="4" forbiddenName="bob"
|
||||
required minlength="4" appForbiddenName="bob"
|
||||
[(ngModel)]="hero.name" #name="ngModel" >
|
||||
<!-- #enddocregion name-input -->
|
||||
|
||||
|
131
aio/content/examples/observables-in-angular/src/main.ts
Normal file
131
aio/content/examples/observables-in-angular/src/main.ts
Normal file
@ -0,0 +1,131 @@
|
||||
|
||||
import { Component, Output, OnInit, EventEmitter, NgModule } from '@angular/core';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
// #docregion eventemitter
|
||||
|
||||
@Component({
|
||||
selector: 'zippy',
|
||||
template: `
|
||||
<div class="zippy">
|
||||
<div (click)="toggle()">Toggle</div>
|
||||
<div [hidden]="!visible">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
</div>`})
|
||||
|
||||
export class ZippyComponent {
|
||||
visible = true;
|
||||
@Output() open = new EventEmitter<any>();
|
||||
@Output() close = new EventEmitter<any>();
|
||||
|
||||
toggle() {
|
||||
this.visible = !this.visible;
|
||||
if (this.visible) {
|
||||
this.open.emit(null);
|
||||
} else {
|
||||
this.close.emit(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// #enddocregion eventemitter
|
||||
|
||||
// #docregion pipe
|
||||
|
||||
@Component({
|
||||
selector: 'async-observable-pipe',
|
||||
template: `<div><code>observable|async</code>:
|
||||
Time: {{ time | async }}</div>`
|
||||
})
|
||||
export class AsyncObservablePipeComponent {
|
||||
time = new Observable(observer =>
|
||||
setInterval(() => observer.next(new Date().toString()), 1000)
|
||||
);
|
||||
}
|
||||
|
||||
// #enddocregion pipe
|
||||
|
||||
// #docregion router
|
||||
|
||||
import { Router, NavigationStart } from '@angular/router';
|
||||
import { filter } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'app-routable',
|
||||
templateUrl: './routable.component.html',
|
||||
styleUrls: ['./routable.component.css']
|
||||
})
|
||||
export class Routable1Component implements OnInit {
|
||||
|
||||
navStart: Observable<NavigationStart>;
|
||||
|
||||
constructor(private router: Router) {
|
||||
// Create a new Observable the publishes only the NavigationStart event
|
||||
this.navStart = router.events.pipe(
|
||||
filter(evt => evt instanceof NavigationStart)
|
||||
) as Observable<NavigationStart>;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.navStart.subscribe(evt => console.log('Navigation Started!'));
|
||||
}
|
||||
}
|
||||
|
||||
// #enddocregion router
|
||||
|
||||
|
||||
// #docregion activated_route
|
||||
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-routable',
|
||||
templateUrl: './routable.component.html',
|
||||
styleUrls: ['./routable.component.css']
|
||||
})
|
||||
export class Routable2Component implements OnInit {
|
||||
constructor(private activatedRoute: ActivatedRoute) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.activatedRoute.url
|
||||
.subscribe(url => console.log('The URL changed to: ' + url));
|
||||
}
|
||||
}
|
||||
|
||||
// #enddocregion activated_route
|
||||
|
||||
|
||||
// #docregion forms
|
||||
|
||||
import { FormGroup } from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'my-component',
|
||||
template: 'MyComponent Template'
|
||||
})
|
||||
export class MyComponent implements OnInit {
|
||||
nameChangeLog: string[] = [];
|
||||
heroForm: FormGroup;
|
||||
|
||||
ngOnInit() {
|
||||
this.logNameChange();
|
||||
}
|
||||
logNameChange() {
|
||||
const nameControl = this.heroForm.get('name');
|
||||
nameControl.valueChanges.forEach(
|
||||
(value: string) => this.nameChangeLog.push(value)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// #enddocregion forms
|
||||
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations:
|
||||
[ZippyComponent, AsyncObservablePipeComponent, Routable1Component, Routable2Component, MyComponent]
|
||||
})
|
||||
export class AppModule {
|
||||
}
|
66
aio/content/examples/observables/src/creating.ts
Normal file
66
aio/content/examples/observables/src/creating.ts
Normal file
@ -0,0 +1,66 @@
|
||||
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
// #docregion subscriber
|
||||
|
||||
// This function runs when subscribe() is called
|
||||
function sequenceSubscriber(observer) {
|
||||
// synchronously deliver 1, 2, and 3, then complete
|
||||
observer.next(1);
|
||||
observer.next(2);
|
||||
observer.next(3);
|
||||
observer.complete();
|
||||
|
||||
// unsubscribe function doesn't need to do anything in this
|
||||
// because values are delivered synchronously
|
||||
return {unsubscribe() {}};
|
||||
}
|
||||
|
||||
// Create a new Observable that will deliver the above sequence
|
||||
const sequence = new Observable(sequenceSubscriber);
|
||||
|
||||
// execute the Observable and print the result of each notification
|
||||
sequence.subscribe({
|
||||
next(num) { console.log(num); },
|
||||
complete() { console.log('Finished sequence'); }
|
||||
});
|
||||
|
||||
// Logs:
|
||||
// 1
|
||||
// 2
|
||||
// 3
|
||||
// Finished sequence
|
||||
|
||||
// #enddocregion subscriber
|
||||
|
||||
// #docregion fromevent
|
||||
|
||||
function fromEvent(target, eventName) {
|
||||
return new Observable((observer) => {
|
||||
const handler = (e) => observer.next(e);
|
||||
|
||||
// Add the event handler to the target
|
||||
target.addEventListener(eventName, handler);
|
||||
|
||||
return () => {
|
||||
// Detach the event handler from the target
|
||||
target.removeEventListener(eventName, handler);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// #enddocregion fromevent
|
||||
|
||||
// #docregion fromevent_use
|
||||
|
||||
const ESC_KEY = 27;
|
||||
const nameInput = document.getElementById('name') as HTMLInputElement;
|
||||
|
||||
const subscription = fromEvent(nameInput, 'keydown')
|
||||
.subscribe((e: KeyboardEvent) => {
|
||||
if (e.keyCode === ESC_KEY) {
|
||||
nameInput.value = '';
|
||||
}
|
||||
});
|
||||
|
||||
// #enddocregion fromevent_use
|
32
aio/content/examples/observables/src/geolocation.ts
Normal file
32
aio/content/examples/observables/src/geolocation.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
// #docregion
|
||||
|
||||
// Create an Observable that will start listening to geolocation updates
|
||||
// when a consumer subscribes.
|
||||
const locations = new Observable((observer) => {
|
||||
// Get the next and error callbacks. These will be passed in when
|
||||
// the consumer subscribes.
|
||||
const {next, error} = observer;
|
||||
let watchId;
|
||||
|
||||
// Simple geolocation API check provides values to publish
|
||||
if ('geolocation' in navigator) {
|
||||
watchId = navigator.geolocation.watchPosition(next, error);
|
||||
} else {
|
||||
error('Geolocation not available');
|
||||
}
|
||||
|
||||
// When the consumer unsubscribes, clean up data ready for next subscription.
|
||||
return {unsubscribe() { navigator.geolocation.clearWatch(watchId); }};
|
||||
});
|
||||
|
||||
// Call subscribe() to start listening for updates.
|
||||
const locationsSubscription = locations.subscribe({
|
||||
next(position) { console.log('Current Position: ', position); },
|
||||
error(msg) { console.log('Error Getting Location: ', msg); }
|
||||
});
|
||||
|
||||
// Stop listening for location after 10 seconds
|
||||
setTimeout(() => { locationsSubscription.unsubscribe(); }, 10000);
|
||||
// #enddocregion
|
5
aio/content/examples/observables/src/main.ts
Normal file
5
aio/content/examples/observables/src/main.ts
Normal file
@ -0,0 +1,5 @@
|
||||
|
||||
import './geolocation';
|
||||
import './subscribing';
|
||||
import './creating';
|
||||
import './multicasting';
|
155
aio/content/examples/observables/src/multicasting.ts
Normal file
155
aio/content/examples/observables/src/multicasting.ts
Normal file
@ -0,0 +1,155 @@
|
||||
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
// #docregion delay_sequence
|
||||
|
||||
function sequenceSubscriber(observer) {
|
||||
const seq = [1, 2, 3];
|
||||
let timeoutId;
|
||||
|
||||
// Will run through an array of numbers, emitting one value
|
||||
// per second until it gets to the end of the array.
|
||||
function doSequence(arr, idx) {
|
||||
timeoutId = setTimeout(() => {
|
||||
observer.next(arr[idx]);
|
||||
if (idx === arr.length - 1) {
|
||||
observer.complete();
|
||||
} else {
|
||||
doSequence(arr, idx++);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
doSequence(seq, 0);
|
||||
|
||||
// Unsubscribe should clear the timeout to stop execution
|
||||
return {unsubscribe() {
|
||||
clearTimeout(timeoutId);
|
||||
}};
|
||||
}
|
||||
|
||||
// Create a new Observable that will deliver the above sequence
|
||||
const sequence = new Observable(sequenceSubscriber);
|
||||
|
||||
sequence.subscribe({
|
||||
next(num) { console.log(num); },
|
||||
complete() { console.log('Finished sequence'); }
|
||||
});
|
||||
|
||||
// Logs:
|
||||
// (at 1 second): 1
|
||||
// (at 2 seconds): 2
|
||||
// (at 3 seconds): 3
|
||||
// (at 3 seconds): Finished sequence
|
||||
|
||||
// #enddocregion delay_sequence
|
||||
|
||||
// #docregion subscribe_twice
|
||||
|
||||
// Subscribe starts the clock, and will emit after 1 second
|
||||
sequence.subscribe({
|
||||
next(num) { console.log('1st subscribe: ' + num); },
|
||||
complete() { console.log('1st sequence finished.'); }
|
||||
});
|
||||
|
||||
// After 1/2 second, subscribe again.
|
||||
setTimeout(() => {
|
||||
sequence.subscribe({
|
||||
next(num) { console.log('2nd subscribe: ' + num); },
|
||||
complete() { console.log('2nd sequence finished.'); }
|
||||
});
|
||||
}, 500);
|
||||
|
||||
// Logs:
|
||||
// (at 1 second): 1st subscribe: 1
|
||||
// (at 1.5 seconds): 2nd subscribe: 1
|
||||
// (at 2 seconds): 1st subscribe: 2
|
||||
// (at 2.5 seconds): 2nd subscribe: 2
|
||||
// (at 3 seconds): 1st subscribe: 3
|
||||
// (at 3 seconds): 1st sequence finished
|
||||
// (at 3.5 seconds): 2nd subscribe: 3
|
||||
// (at 3.5 seconds): 2nd sequence finished
|
||||
|
||||
// #enddocregion subscribe_twice
|
||||
|
||||
// #docregion multicast_sequence
|
||||
|
||||
function multicastSequenceSubscriber() {
|
||||
const seq = [1, 2, 3];
|
||||
// Keep track of each observer (one for every active subscription)
|
||||
const observers = [];
|
||||
// Still a single timeoutId because there will only ever be one
|
||||
// set of values being generated, multicasted to each subscriber
|
||||
let timeoutId;
|
||||
|
||||
// Return the subscriber function (runs when subscribe()
|
||||
// function is invoked)
|
||||
return (observer) => {
|
||||
observers.push(observer);
|
||||
// When this is the first subscription, start the sequence
|
||||
if (observers.length === 1) {
|
||||
timeoutId = doSequence({
|
||||
next(val) {
|
||||
// Iterate through observers and notify all subscriptions
|
||||
observers.forEach(obs => obs.next(val));
|
||||
},
|
||||
complete() {
|
||||
// Notify all complete callbacks
|
||||
observers.forEach(obs => obs.complete());
|
||||
}
|
||||
}, seq, 0);
|
||||
}
|
||||
|
||||
return {
|
||||
unsubscribe() {
|
||||
// Remove from the observers array so it's no longer notified
|
||||
observers.splice(observers.indexOf(observer), 1);
|
||||
// If there's no more listeners, do cleanup
|
||||
if (observers.length === 0) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// Run through an array of numbers, emitting one value
|
||||
// per second until it gets to the end of the array.
|
||||
function doSequence(observer, arr, idx) {
|
||||
return setTimeout(() => {
|
||||
observer.next(arr[idx]);
|
||||
if (idx === arr.length - 1) {
|
||||
observer.complete();
|
||||
} else {
|
||||
doSequence(observer, arr, idx++);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// Create a new Observable that will deliver the above sequence
|
||||
const multicastSequence = new Observable(multicastSequenceSubscriber);
|
||||
|
||||
// Subscribe starts the clock, and begins to emit after 1 second
|
||||
multicastSequence.subscribe({
|
||||
next(num) { console.log('1st subscribe: ' + num); },
|
||||
complete() { console.log('1st sequence finished.'); }
|
||||
});
|
||||
|
||||
// After 1 1/2 seconds, subscribe again (should "miss" the first value).
|
||||
setTimeout(() => {
|
||||
multicastSequence.subscribe({
|
||||
next(num) { console.log('2nd subscribe: ' + num); },
|
||||
complete() { console.log('2nd sequence finished.'); }
|
||||
});
|
||||
}, 1500);
|
||||
|
||||
// Logs:
|
||||
// (at 1 second): 1st subscribe: 1
|
||||
// (at 2 seconds): 1st subscribe: 2
|
||||
// (at 2 seconds): 2nd subscribe: 2
|
||||
// (at 3 seconds): 1st subscribe: 3
|
||||
// (at 3 seconds): 1st sequence finished
|
||||
// (at 3 seconds): 2nd subscribe: 3
|
||||
// (at 3 seconds): 2nd sequence finished
|
||||
|
||||
// #enddocregion multicast_sequence
|
33
aio/content/examples/observables/src/subscribing.ts
Normal file
33
aio/content/examples/observables/src/subscribing.ts
Normal file
@ -0,0 +1,33 @@
|
||||
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import 'rxjs/add/observable/of';
|
||||
|
||||
// #docregion observer
|
||||
|
||||
// Create simple observable that emits three values
|
||||
const myObservable = Observable.of(1, 2, 3);
|
||||
|
||||
// Create observer object
|
||||
const myObserver = {
|
||||
next: x => console.log('Observer got a next value: ' + x),
|
||||
error: err => console.error('Observer got an error: ' + err),
|
||||
complete: () => console.log('Observer got a complete notification'),
|
||||
};
|
||||
|
||||
// Execute with the observer object
|
||||
myObservable.subscribe(myObserver);
|
||||
// Logs:
|
||||
// Observer got a next value: 1
|
||||
// Observer got a next value: 2
|
||||
// Observer got a next value: 3
|
||||
// Observer got a complete notification
|
||||
|
||||
// #enddocregion observer
|
||||
|
||||
// #docregion sub_fn
|
||||
myObservable.subscribe(
|
||||
x => console.log('Observer got a next value: ' + x),
|
||||
err => console.error('Observer got an error: ' + err),
|
||||
() => console.log('Observer got a complete notification')
|
||||
);
|
||||
// #enddocregion sub_fn
|
@ -0,0 +1,26 @@
|
||||
|
||||
import { ajax } from 'rxjs/observable/dom/ajax';
|
||||
import { range } from 'rxjs/observable/range';
|
||||
import { timer } from 'rxjs/observable/timer';
|
||||
import { pipe } from 'rxjs/util/pipe';
|
||||
import { retryWhen, zip, map, mergeMap } from 'rxjs/operators';
|
||||
|
||||
function backoff(maxTries, ms) {
|
||||
return pipe(
|
||||
retryWhen(attempts => range(1, maxTries)
|
||||
.pipe(
|
||||
zip(attempts, (i) => i),
|
||||
map(i => i * i),
|
||||
mergeMap(i => timer(i * ms))
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
ajax('/api/endpoint')
|
||||
.pipe(backoff(3, 250))
|
||||
.subscribe(data => handleData(data));
|
||||
|
||||
function handleData(data) {
|
||||
// ...
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
|
||||
import { fromEvent } from 'rxjs/observable/fromEvent';
|
||||
import { ajax } from 'rxjs/observable/dom/ajax';
|
||||
import { map, filter, debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
|
||||
|
||||
const searchBox = document.getElementById('search-box');
|
||||
|
||||
const typeahead = fromEvent(searchBox, 'input').pipe(
|
||||
map((e: KeyboardEvent) => e.target.value),
|
||||
filter(text => text.length > 2),
|
||||
debounceTime(10),
|
||||
distinctUntilChanged(),
|
||||
switchMap(() => ajax('/api/endpoint'))
|
||||
);
|
||||
|
||||
typeahead.subscribe(data => {
|
||||
// Handle the data from the API
|
||||
});
|
0
aio/content/examples/rx-library/example-config.json
Normal file
0
aio/content/examples/rx-library/example-config.json
Normal file
26
aio/content/examples/rx-library/src/error-handling.ts
Normal file
26
aio/content/examples/rx-library/src/error-handling.ts
Normal file
@ -0,0 +1,26 @@
|
||||
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import 'rxjs/add/observable/of';
|
||||
|
||||
// #docregion
|
||||
|
||||
import { ajax } from 'rxjs/observable/dom/ajax';
|
||||
import { map, catchError } from 'rxjs/operators';
|
||||
// Return "response" from the API. If an error happens,
|
||||
// return an empty array.
|
||||
const apiData = ajax('/api/data').pipe(
|
||||
map(res => {
|
||||
if (!res.response) {
|
||||
throw new Error('Value expected!');
|
||||
}
|
||||
return res.response;
|
||||
}),
|
||||
catchError(err => Observable.of([]))
|
||||
);
|
||||
|
||||
apiData.subscribe({
|
||||
next(x) { console.log('data: ', x); },
|
||||
error(err) { console.log('errors already caught... will not run'); }
|
||||
});
|
||||
|
||||
// #enddocregion
|
0
aio/content/examples/rx-library/src/main.ts
Normal file
0
aio/content/examples/rx-library/src/main.ts
Normal file
20
aio/content/examples/rx-library/src/naming-convention.ts
Normal file
20
aio/content/examples/rx-library/src/naming-convention.ts
Normal file
@ -0,0 +1,20 @@
|
||||
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
@Component({
|
||||
selector: 'app-stopwatch',
|
||||
templateUrl: './stopwatch.component.html'
|
||||
})
|
||||
export class StopwatchComponent {
|
||||
|
||||
stopwatchValue: number;
|
||||
stopwatchValue$: Observable<number>;
|
||||
|
||||
start() {
|
||||
this.stopwatchValue$.subscribe(num =>
|
||||
this.stopwatchValue = num
|
||||
);
|
||||
}
|
||||
}
|
25
aio/content/examples/rx-library/src/operators.1.ts
Normal file
25
aio/content/examples/rx-library/src/operators.1.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import 'rxjs/add/observable/of';
|
||||
|
||||
// #docregion
|
||||
|
||||
import { pipe } from 'rxjs/util/pipe';
|
||||
import { filter, map } from 'rxjs/operators';
|
||||
|
||||
const nums = Observable.of(1, 2, 3, 4, 5);
|
||||
|
||||
// Create a function that accepts an Observable.
|
||||
const squareOddVals = pipe(
|
||||
filter(n => n % 2),
|
||||
map(n => n * n)
|
||||
);
|
||||
|
||||
// Create an Observable that will run the filter and map functions
|
||||
const squareOdd = squareOddVals(nums);
|
||||
|
||||
// Suscribe to run the combined functions
|
||||
squareOdd.subscribe(x => console.log(x));
|
||||
|
||||
// #enddocregion
|
||||
|
||||
|
18
aio/content/examples/rx-library/src/operators.2.ts
Normal file
18
aio/content/examples/rx-library/src/operators.2.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import 'rxjs/add/observable/of';
|
||||
|
||||
// #docregion
|
||||
|
||||
import { filter } from 'rxjs/operators/filter';
|
||||
import { map } from 'rxjs/operators/map';
|
||||
|
||||
const squareOdd = Observable.of(1, 2, 3, 4, 5)
|
||||
.pipe(
|
||||
filter(n => n % 2),
|
||||
map(n => n * n)
|
||||
);
|
||||
|
||||
// Subscribe to get values
|
||||
squareOdd.subscribe(x => console.log(x));
|
||||
|
||||
// #enddocregion
|
21
aio/content/examples/rx-library/src/operators.ts
Normal file
21
aio/content/examples/rx-library/src/operators.ts
Normal file
@ -0,0 +1,21 @@
|
||||
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import 'rxjs/add/observable/of';
|
||||
|
||||
// #docregion
|
||||
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
const nums = Observable.of(1, 2, 3);
|
||||
|
||||
const squareValues = map((val: number) => val * val);
|
||||
const squaredNums = squareValues(nums);
|
||||
|
||||
squaredNums.subscribe(x => console.log(x));
|
||||
|
||||
// Logs
|
||||
// 1
|
||||
// 4
|
||||
// 9
|
||||
|
||||
// #enddocregion
|
27
aio/content/examples/rx-library/src/retry-on-error.ts
Normal file
27
aio/content/examples/rx-library/src/retry-on-error.ts
Normal file
@ -0,0 +1,27 @@
|
||||
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import 'rxjs/add/observable/of';
|
||||
|
||||
|
||||
// #docregion
|
||||
|
||||
import { ajax } from 'rxjs/observable/dom/ajax';
|
||||
import { map, retry, catchError } from 'rxjs/operators';
|
||||
|
||||
const apiData = ajax('/api/data').pipe(
|
||||
retry(3), // Retry up to 3 times before failing
|
||||
map(res => {
|
||||
if (!res.response) {
|
||||
throw new Error('Value expected!');
|
||||
}
|
||||
return res.response;
|
||||
}),
|
||||
catchError(err => Observable.of([]))
|
||||
);
|
||||
|
||||
apiData.subscribe({
|
||||
next(x) { console.log('data: ', x); },
|
||||
error(err) { console.log('errors already caught... will not run'); }
|
||||
});
|
||||
|
||||
// #enddocregion
|
65
aio/content/examples/rx-library/src/simple-creation.ts
Normal file
65
aio/content/examples/rx-library/src/simple-creation.ts
Normal file
@ -0,0 +1,65 @@
|
||||
|
||||
// #docregion promise
|
||||
|
||||
import { fromPromise } from 'rxjs/observable/fromPromise';
|
||||
|
||||
// Create an Observable out of a promise
|
||||
const data = fromPromise(fetch('/api/endpoint'));
|
||||
// Subscribe to begin listening for async result
|
||||
data.subscribe({
|
||||
next(response) { console.log(response); },
|
||||
error(err) { console.error('Error: ' + err); },
|
||||
complete() { console.log('Completed'); }
|
||||
});
|
||||
|
||||
// #enddocregion promise
|
||||
|
||||
// #docregion interval
|
||||
|
||||
import { interval } from 'rxjs/observable/interval';
|
||||
|
||||
// Create an Observable that will publish a value on an interval
|
||||
const secondsCounter = interval(1000);
|
||||
// Subscribe to begin publishing values
|
||||
secondsCounter.subscribe(n =>
|
||||
console.log(`It's been ${n} seconds since subscribing!`));
|
||||
|
||||
// #enddocregion interval
|
||||
|
||||
|
||||
// #docregion event
|
||||
|
||||
import { fromEvent } from 'rxjs/observable/fromEvent';
|
||||
|
||||
const el = document.getElementById('my-element');
|
||||
|
||||
// Create an Observable that will publish mouse movements
|
||||
const mouseMoves = fromEvent(el, 'mousemove');
|
||||
|
||||
// Subscribe to start listening for mouse-move events
|
||||
const subscription = mouseMoves.subscribe((evt: MouseEvent) => {
|
||||
// Log coords of mouse movements
|
||||
console.log(`Coords: ${evt.clientX} X ${evt.clientY}`);
|
||||
|
||||
// When the mouse is over the upper-left of the screen,
|
||||
// unsubscribe to stop listening for mouse movements
|
||||
if (evt.clientX < 40 && evt.clientY < 40) {
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
});
|
||||
|
||||
// #enddocregion event
|
||||
|
||||
|
||||
// #docregion ajax
|
||||
|
||||
import { ajax } from 'rxjs/observable/dom/ajax';
|
||||
|
||||
// Create an Observable that will create an AJAX request
|
||||
const apiData = ajax('/api/data');
|
||||
// Subscribe to create the request
|
||||
apiData.subscribe(res => console.log(res.status, res.response));
|
||||
|
||||
// #enddocregion ajax
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"description": "Testing - app.specs",
|
||||
"description": "Testing - specs",
|
||||
"files":[
|
||||
"src/styles.css",
|
||||
|
||||
|
@ -1,5 +0,0 @@
|
||||
// #docplaster
|
||||
// #docregion
|
||||
describe('1st tests', () => {
|
||||
it('true is true', () => expect(true).toBe(true));
|
||||
});
|
@ -1,9 +1,8 @@
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
|
||||
import { AboutComponent } from './about.component';
|
||||
import { HighlightDirective } from './shared/highlight.directive';
|
||||
import { HighlightDirective } from '../shared/highlight.directive';
|
||||
|
||||
let fixture: ComponentFixture<AboutComponent>;
|
||||
|
||||
@ -19,8 +18,8 @@ describe('AboutComponent (highlightDirective)', () => {
|
||||
});
|
||||
|
||||
it('should have skyblue <h2>', () => {
|
||||
const de = fixture.debugElement.query(By.css('h2'));
|
||||
const bgColor = de.nativeElement.style.backgroundColor;
|
||||
const h2: HTMLElement = fixture.nativeElement.querySelector('h2');
|
||||
const bgColor = h2.style.backgroundColor;
|
||||
expect(bgColor).toBe('skyblue');
|
||||
});
|
||||
// #enddocregion tests
|
@ -3,7 +3,8 @@ import { Component } from '@angular/core';
|
||||
@Component({
|
||||
template: `
|
||||
<h2 highlight="skyblue">About</h2>
|
||||
<h3>Quote of the day:</h3>
|
||||
<twain-quote></twain-quote>
|
||||
<p>All about this sample</p>`
|
||||
`
|
||||
})
|
||||
export class AboutComponent { }
|
@ -0,0 +1,76 @@
|
||||
// #docplaster
|
||||
// #docregion
|
||||
import { TestBed, async } from '@angular/core/testing';
|
||||
// #enddocregion
|
||||
import { AppComponent } from './app-initial.component';
|
||||
/*
|
||||
// #docregion
|
||||
import { AppComponent } from './app.component';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
// #enddocregion
|
||||
*/
|
||||
describe('AppComponent (initial CLI version)', () => {
|
||||
// #docregion
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
AppComponent
|
||||
],
|
||||
}).compileComponents();
|
||||
}));
|
||||
it('should create the app', async(() => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.debugElement.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
}));
|
||||
it(`should have as title 'app'`, async(() => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.debugElement.componentInstance;
|
||||
expect(app.title).toEqual('app');
|
||||
}));
|
||||
it('should render title in a h1 tag', async(() => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
fixture.detectChanges();
|
||||
const compiled = fixture.nativeElement;
|
||||
expect(compiled.querySelector('h1').textContent).toContain('Welcome to app!');
|
||||
}));
|
||||
});
|
||||
// #enddocregion
|
||||
|
||||
/// As it should be
|
||||
import { DebugElement } from '@angular/core';
|
||||
import { ComponentFixture } from '@angular/core/testing';
|
||||
|
||||
describe('AppComponent (initial CLI version - as it should be)', () => {
|
||||
|
||||
let app: AppComponent;
|
||||
let de: DebugElement;
|
||||
let fixture: ComponentFixture<AppComponent>;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
AppComponent
|
||||
],
|
||||
});
|
||||
|
||||
fixture = TestBed.createComponent(AppComponent);
|
||||
app = fixture.componentInstance;
|
||||
de = fixture.debugElement;
|
||||
});
|
||||
|
||||
it('should create the app', () => {
|
||||
expect(app).toBeDefined();
|
||||
});
|
||||
|
||||
it(`should have as title 'app'`, () => {
|
||||
expect(app.title).toEqual('app');
|
||||
});
|
||||
|
||||
it('should render title in an h1 tag', () => {
|
||||
fixture.detectChanges();
|
||||
expect(de.nativeElement.querySelector('h1').textContent)
|
||||
.toContain('Welcome to app!');
|
||||
});
|
||||
});
|
@ -0,0 +1,11 @@
|
||||
// #docregion
|
||||
// Reduced version of the initial AppComponent generated by CLI
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
template: '<h1>Welcome to {{title}}!</h1>'
|
||||
})
|
||||
export class AppComponent {
|
||||
title = 'app';
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { AboutComponent } from './about.component';
|
||||
import { AboutComponent } from './about/about.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
|
@ -1,11 +1,11 @@
|
||||
<!-- #docregion -->
|
||||
<app-banner></app-banner>
|
||||
<app-welcome></app-welcome>
|
||||
|
||||
<!-- #docregion links -->
|
||||
<nav>
|
||||
<a routerLink="/dashboard">Dashboard</a>
|
||||
<a routerLink="/heroes">Heroes</a>
|
||||
<a routerLink="/about">About</a>
|
||||
</nav>
|
||||
|
||||
<!-- #enddocregion links -->
|
||||
<router-outlet></router-outlet>
|
||||
|
@ -4,11 +4,11 @@
|
||||
import { async, ComponentFixture, fakeAsync, TestBed, tick,
|
||||
} from '@angular/core/testing';
|
||||
|
||||
import { asyncData } from '../testing';
|
||||
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { SpyLocation } from '@angular/common/testing';
|
||||
|
||||
import { click } from '../testing';
|
||||
|
||||
// r - for relatively obscure router symbols
|
||||
import * as r from '@angular/router';
|
||||
import { Router, RouterLinkWithHref } from '@angular/router';
|
||||
@ -17,11 +17,15 @@ import { By } from '@angular/platform-browser';
|
||||
import { DebugElement, Type } from '@angular/core';
|
||||
import { Location } from '@angular/common';
|
||||
|
||||
import { click } from '../testing';
|
||||
|
||||
import { AppModule } from './app.module';
|
||||
import { AppComponent } from './app.component';
|
||||
import { AboutComponent } from './about.component';
|
||||
import { AboutComponent } from './about/about.component';
|
||||
import { DashboardComponent } from './dashboard/dashboard.component';
|
||||
import { TwainService } from './shared/twain.service';
|
||||
import { TwainService } from './twain/twain.service';
|
||||
|
||||
import { HeroService, TestHeroService } from './model/testing/test-hero.service';
|
||||
|
||||
let comp: AppComponent;
|
||||
let fixture: ComponentFixture<AppComponent>;
|
||||
@ -31,15 +35,19 @@ let location: SpyLocation;
|
||||
|
||||
describe('AppComponent & RouterTestingModule', () => {
|
||||
|
||||
beforeEach( async(() => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [ AppModule, RouterTestingModule ]
|
||||
imports: [ AppModule, RouterTestingModule ],
|
||||
providers: [
|
||||
{ provide: HeroService, useClass: TestHeroService }
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
it('should navigate to "Dashboard" immediately', fakeAsync(() => {
|
||||
createComponent();
|
||||
tick(); // wait for async data to arrive
|
||||
expect(location.path()).toEqual('/dashboard', 'after initialNavigation()');
|
||||
expectElementOf(DashboardComponent);
|
||||
}));
|
||||
@ -64,7 +72,7 @@ describe('AppComponent & RouterTestingModule', () => {
|
||||
}));
|
||||
|
||||
// Can't navigate to lazy loaded modules with this technique
|
||||
xit('should navigate to "Heroes" on click', fakeAsync(() => {
|
||||
xit('should navigate to "Heroes" on click (not working yet)', fakeAsync(() => {
|
||||
createComponent();
|
||||
page.heroesLinkDe.nativeElement.click();
|
||||
advance();
|
||||
@ -84,9 +92,9 @@ import { HeroListComponent } from './hero/hero-list.component';
|
||||
let loader: SpyNgModuleFactoryLoader;
|
||||
|
||||
///////// Can't get lazy loaded Heroes to work yet
|
||||
xdescribe('AppComponent & Lazy Loading', () => {
|
||||
xdescribe('AppComponent & Lazy Loading (not working yet)', () => {
|
||||
|
||||
beforeEach( async(() => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [ AppModule, RouterTestingModule ]
|
||||
})
|
||||
@ -95,14 +103,11 @@ xdescribe('AppComponent & Lazy Loading', () => {
|
||||
|
||||
beforeEach(fakeAsync(() => {
|
||||
createComponent();
|
||||
loader = TestBed.get(NgModuleFactoryLoader);
|
||||
loader.stubbedModules = {expected: HeroModule};
|
||||
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();
|
||||
@ -110,25 +115,24 @@ xdescribe('AppComponent & Lazy Loading', () => {
|
||||
expectElementOf(HeroListComponent);
|
||||
}));
|
||||
|
||||
xit('can navigate to "Heroes" w/ browser location URL change', fakeAsync(() => {
|
||||
it('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 */
|
||||
/**
|
||||
* Advance to the routed page
|
||||
* Wait a tick, then detect changes, and tick again
|
||||
*/
|
||||
function advance(): void {
|
||||
tick();
|
||||
fixture.detectChanges();
|
||||
tick(); // wait while navigating
|
||||
fixture.detectChanges(); // update view
|
||||
tick(); // wait for async data to arrive
|
||||
}
|
||||
|
||||
function createComponent() {
|
||||
@ -140,8 +144,8 @@ function createComponent() {
|
||||
router = injector.get(Router);
|
||||
router.initialNavigation();
|
||||
spyOn(injector.get(TwainService), 'getQuote')
|
||||
.and.returnValue(Promise.resolve('Test Quote')); // fakes it
|
||||
|
||||
// fake fast async observable
|
||||
.and.returnValue(asyncData('Test Quote'));
|
||||
advance();
|
||||
|
||||
page = new Page();
|
||||
@ -151,7 +155,6 @@ class Page {
|
||||
aboutLinkDe: DebugElement;
|
||||
dashboardLinkDe: DebugElement;
|
||||
heroesLinkDe: DebugElement;
|
||||
recordedEvents: any[] = [];
|
||||
|
||||
// for debugging
|
||||
comp: AppComponent;
|
||||
@ -159,17 +162,7 @@ class Page {
|
||||
router: Router;
|
||||
fixture: ComponentFixture<AppComponent>;
|
||||
|
||||
expectEvents(pairs: any[]) {
|
||||
const events = this.recordedEvents;
|
||||
expect(events.length).toEqual(pairs.length, 'actual/expected events length mismatch');
|
||||
for (let i = 0; i < events.length; ++i) {
|
||||
expect((<any>events[i].constructor).name).toBe(pairs[i][0].name, 'unexpected event name');
|
||||
expect((<any>events[i]).url).toBe(pairs[i][1], 'unexpected event url');
|
||||
}
|
||||
}
|
||||
|
||||
constructor() {
|
||||
router.events.subscribe(e => this.recordedEvents.push(e));
|
||||
const links = fixture.debugElement.queryAll(By.directive(RouterLinkWithHref));
|
||||
this.aboutLinkDe = links[2];
|
||||
this.dashboardLinkDe = links[0];
|
||||
|
@ -1,69 +1,67 @@
|
||||
// #docplaster
|
||||
import { async, ComponentFixture, TestBed
|
||||
} from '@angular/core/testing';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DebugElement } from '@angular/core';
|
||||
import { Component, DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { By } from '@angular/platform-browser';
|
||||
|
||||
// #docregion setup-schemas
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
// #enddocregion setup-schemas
|
||||
// #docregion setup-stubs-w-imports
|
||||
import { Component } from '@angular/core';
|
||||
// #docregion setup-schemas
|
||||
import { AppComponent } from './app.component';
|
||||
// #enddocregion setup-schemas
|
||||
import { BannerComponent } from './banner.component';
|
||||
import { RouterLinkStubDirective } from '../testing';
|
||||
// #docregion setup-schemas
|
||||
import { RouterOutletStubComponent } from '../testing';
|
||||
import { AppComponent } from './app.component';
|
||||
import { RouterLinkDirectiveStub } from '../testing';
|
||||
|
||||
// #enddocregion setup-schemas
|
||||
@Component({selector: 'app-welcome', template: ''})
|
||||
class WelcomeStubComponent {}
|
||||
// #docregion component-stubs
|
||||
@Component({selector: 'app-banner', template: ''})
|
||||
class BannerStubComponent {}
|
||||
|
||||
// #enddocregion setup-stubs-w-imports
|
||||
@Component({selector: 'router-outlet', template: ''})
|
||||
class RouterOutletStubComponent { }
|
||||
|
||||
@Component({selector: 'app-welcome', template: ''})
|
||||
class WelcomeStubComponent {}
|
||||
// #enddocregion component-stubs
|
||||
|
||||
let comp: AppComponent;
|
||||
let fixture: ComponentFixture<AppComponent>;
|
||||
|
||||
describe('AppComponent & TestModule', () => {
|
||||
// #docregion setup-stubs, setup-stubs-w-imports
|
||||
beforeEach( async(() => {
|
||||
beforeEach(async(() => {
|
||||
// #docregion testbed-stubs
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
BannerComponent, WelcomeStubComponent,
|
||||
RouterLinkStubDirective, RouterOutletStubComponent
|
||||
RouterLinkDirectiveStub,
|
||||
BannerStubComponent,
|
||||
RouterOutletStubComponent,
|
||||
WelcomeStubComponent
|
||||
]
|
||||
})
|
||||
|
||||
.compileComponents()
|
||||
.then(() => {
|
||||
// #enddocregion testbed-stubs
|
||||
.compileComponents().then(() => {
|
||||
fixture = TestBed.createComponent(AppComponent);
|
||||
comp = fixture.componentInstance;
|
||||
});
|
||||
}));
|
||||
// #enddocregion setup-stubs, setup-stubs-w-imports
|
||||
tests();
|
||||
});
|
||||
|
||||
//////// Testing w/ NO_ERRORS_SCHEMA //////
|
||||
describe('AppComponent & NO_ERRORS_SCHEMA', () => {
|
||||
// #docregion setup-schemas
|
||||
beforeEach( async(() => {
|
||||
beforeEach(async(() => {
|
||||
// #docregion no-errors-schema, mixed-setup
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ AppComponent, RouterLinkStubDirective ],
|
||||
schemas: [ NO_ERRORS_SCHEMA ]
|
||||
declarations: [
|
||||
AppComponent,
|
||||
// #enddocregion no-errors-schema
|
||||
BannerStubComponent,
|
||||
// #docregion no-errors-schema
|
||||
RouterLinkDirectiveStub
|
||||
],
|
||||
schemas: [ NO_ERRORS_SCHEMA ]
|
||||
})
|
||||
|
||||
.compileComponents()
|
||||
.then(() => {
|
||||
// #enddocregion no-errors-schema, mixed-setup
|
||||
.compileComponents().then(() => {
|
||||
fixture = TestBed.createComponent(AppComponent);
|
||||
comp = fixture.componentInstance;
|
||||
});
|
||||
}));
|
||||
// #enddocregion setup-schemas
|
||||
tests();
|
||||
});
|
||||
|
||||
@ -75,7 +73,7 @@ import { AppRoutingModule } from './app-routing.module';
|
||||
|
||||
describe('AppComponent & AppModule', () => {
|
||||
|
||||
beforeEach( async(() => {
|
||||
beforeEach(async(() => {
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [ AppModule ]
|
||||
@ -88,7 +86,7 @@ describe('AppComponent & AppModule', () => {
|
||||
imports: [ AppRoutingModule ]
|
||||
},
|
||||
add: {
|
||||
declarations: [ RouterLinkStubDirective, RouterOutletStubComponent ]
|
||||
declarations: [ RouterLinkDirectiveStub, RouterOutletStubComponent ]
|
||||
}
|
||||
})
|
||||
|
||||
@ -104,40 +102,40 @@ describe('AppComponent & AppModule', () => {
|
||||
});
|
||||
|
||||
function tests() {
|
||||
let links: RouterLinkStubDirective[];
|
||||
let routerLinks: RouterLinkDirectiveStub[];
|
||||
let linkDes: DebugElement[];
|
||||
|
||||
// #docregion test-setup
|
||||
beforeEach(() => {
|
||||
// trigger initial data binding
|
||||
fixture.detectChanges();
|
||||
fixture.detectChanges(); // trigger initial data binding
|
||||
|
||||
// find DebugElements with an attached RouterLinkStubDirective
|
||||
linkDes = fixture.debugElement
|
||||
.queryAll(By.directive(RouterLinkStubDirective));
|
||||
.queryAll(By.directive(RouterLinkDirectiveStub));
|
||||
|
||||
// get the attached link directive instances using the DebugElement injectors
|
||||
links = linkDes
|
||||
.map(de => de.injector.get(RouterLinkStubDirective) as RouterLinkStubDirective);
|
||||
// get attached link directive instances
|
||||
// using each DebugElement's injector
|
||||
routerLinks = linkDes.map(de => de.injector.get(RouterLinkDirectiveStub));
|
||||
});
|
||||
// #enddocregion test-setup
|
||||
|
||||
it('can instantiate it', () => {
|
||||
it('can instantiate the component', () => {
|
||||
expect(comp).not.toBeNull();
|
||||
});
|
||||
|
||||
// #docregion tests
|
||||
it('can get RouterLinks from template', () => {
|
||||
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', '2nd link should go to Heroes');
|
||||
expect(routerLinks.length).toBe(3, 'should have 3 routerLinks');
|
||||
expect(routerLinks[0].linkParams).toBe('/dashboard');
|
||||
expect(routerLinks[1].linkParams).toBe('/heroes');
|
||||
expect(routerLinks[2].linkParams).toBe('/about');
|
||||
});
|
||||
|
||||
it('can click Heroes link in template', () => {
|
||||
const heroesLinkDe = linkDes[1];
|
||||
const heroesLink = links[1];
|
||||
const heroesLinkDe = linkDes[1]; // heroes link DebugElement
|
||||
const heroesLink = routerLinks[1]; // heroes link directive
|
||||
|
||||
expect(heroesLink.navigatedTo).toBeNull('link should not have navigated yet');
|
||||
expect(heroesLink.navigatedTo).toBeNull('should not have navigated yet');
|
||||
|
||||
heroesLinkDe.triggerEventHandler('click', null);
|
||||
fixture.detectChanges();
|
||||
|
@ -1,29 +1,50 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
|
||||
import { AppComponent } from './app.component';
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
|
||||
import { AboutComponent } from './about.component';
|
||||
import { BannerComponent } from './banner.component';
|
||||
import { AboutComponent } from './about/about.component';
|
||||
import { BannerComponent } from './banner/banner.component';
|
||||
import { HeroService } from './model/hero.service';
|
||||
import { UserService } from './model/user.service';
|
||||
import { HeroService } from './model/hero.service';
|
||||
import { TwainService } from './shared/twain.service';
|
||||
import { WelcomeComponent } from './welcome.component';
|
||||
|
||||
import { TwainComponent } from './twain/twain.component';
|
||||
import { TwainService } from './twain/twain.service';
|
||||
import { WelcomeComponent } from './welcome/welcome.component';
|
||||
|
||||
import { DashboardModule } from './dashboard/dashboard.module';
|
||||
import { SharedModule } from './shared/shared.module';
|
||||
|
||||
import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
|
||||
import { InMemoryDataService } from './in-memory-data.service';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
BrowserModule,
|
||||
DashboardModule,
|
||||
AppRoutingModule,
|
||||
SharedModule
|
||||
SharedModule,
|
||||
HttpClientModule,
|
||||
|
||||
// The HttpClientInMemoryWebApiModule module intercepts HTTP requests
|
||||
// and returns simulated server responses.
|
||||
// Remove it when a real server is ready to receive requests.
|
||||
HttpClientInMemoryWebApiModule.forRoot(
|
||||
InMemoryDataService, { dataEncapsulation: false }
|
||||
)
|
||||
],
|
||||
providers: [ HeroService, TwainService, UserService ],
|
||||
declarations: [ AppComponent, AboutComponent, BannerComponent, WelcomeComponent ],
|
||||
bootstrap: [ AppComponent ]
|
||||
providers: [
|
||||
HeroService,
|
||||
TwainService,
|
||||
UserService
|
||||
],
|
||||
declarations: [
|
||||
AppComponent,
|
||||
AboutComponent,
|
||||
BannerComponent,
|
||||
TwainComponent,
|
||||
WelcomeComponent ],
|
||||
bootstrap: [ AppComponent ]
|
||||
})
|
||||
export class AppModule { }
|
||||
|
@ -1,130 +0,0 @@
|
||||
// #docplaster
|
||||
import { DependentService, FancyService } from './bag';
|
||||
|
||||
///////// Fakes /////////
|
||||
export class FakeFancyService extends FancyService {
|
||||
value = '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: DoneFn) => {
|
||||
service.getAsyncValue().then(value => {
|
||||
expect(value).toBe('async value');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
// #docregion getTimeoutValue
|
||||
it('#getTimeoutValue should return timeout value', (done: DoneFn) => {
|
||||
service = new FancyService();
|
||||
service.getTimeoutValue().then(value => {
|
||||
expect(value).toBe('timeout value');
|
||||
done();
|
||||
});
|
||||
});
|
||||
// #enddocregion getTimeoutValue
|
||||
|
||||
it('#getObservableValue should return observable value', (done: DoneFn) => {
|
||||
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
|
@ -1,681 +0,0 @@
|
||||
// #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, click } 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')
|
||||
);
|
||||
}));
|
||||
|
||||
// Must use done. See https://github.com/angular/angular/issues/10127
|
||||
it('test should wait for FancyService.getObservableDelayValue', (done: DoneFn) => {
|
||||
service.getObservableDelayValue().subscribe(value => {
|
||||
expect(value).toBe('observable delay value');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
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];
|
||||
|
||||
click(heroes[0]);
|
||||
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 component', () => {
|
||||
const fixture = TestBed.createComponent(IoParentComponent);
|
||||
const comp = fixture.componentInstance;
|
||||
const heroName = comp.heroes[0].name; // first hero's name
|
||||
|
||||
fixture.detectChanges();
|
||||
const ngForRow = fixture.debugElement.query(By.directive(IoComponent)); // first hero ngForRow
|
||||
|
||||
const hero = ngForRow.context['hero']; // the hero object passed into the row
|
||||
expect(hero.name).toBe(heroName, 'ngRow.context.hero');
|
||||
|
||||
const rowComp = ngForRow.componentInstance;
|
||||
// jasmine.any is an "instance-of-type" test.
|
||||
expect(rowComp).toEqual(jasmine.any(IoComponent), 'component is IoComp');
|
||||
expect(rowComp.hero.name).toBe(heroName, 'component.hero');
|
||||
});
|
||||
|
||||
|
||||
// #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');
|
||||
|
||||
click(btn);
|
||||
fixture.detectChanges();
|
||||
expect(span.textContent).toMatch(/is on/i, 'after click');
|
||||
});
|
||||
// #enddocregion ButtonComp
|
||||
|
||||
// ngModel is async so we must wait for it with promise-based `whenStable`
|
||||
it('should support entering text in input box (ngModel)', async(() => {
|
||||
const expectedOrigName = 'John';
|
||||
const expectedNewName = 'Sally';
|
||||
|
||||
const fixture = TestBed.createComponent(InputComponent);
|
||||
fixture.detectChanges();
|
||||
|
||||
const comp = fixture.componentInstance;
|
||||
const input = <HTMLInputElement> fixture.debugElement.query(By.css('input')).nativeElement;
|
||||
|
||||
expect(comp.name).toBe(expectedOrigName,
|
||||
`At start name should be ${expectedOrigName} `);
|
||||
|
||||
// wait until ngModel binds comp.name to input box
|
||||
fixture.whenStable().then(() => {
|
||||
expect(input.value).toBe(expectedOrigName,
|
||||
`After ngModel updates input box, input.value should be ${expectedOrigName} `);
|
||||
|
||||
// simulate user entering new name in input
|
||||
input.value = expectedNewName;
|
||||
|
||||
// that change doesn't flow to the component immediately
|
||||
expect(comp.name).toBe(expectedOrigName,
|
||||
`comp.name should still be ${expectedOrigName} after value change, before binding happens`);
|
||||
|
||||
// dispatch a DOM event so that Angular learns of input value change.
|
||||
// then wait while ngModel pushes input.box value to comp.name
|
||||
input.dispatchEvent(newEvent('input'));
|
||||
return fixture.whenStable();
|
||||
})
|
||||
.then(() => {
|
||||
expect(comp.name).toBe(expectedNewName,
|
||||
`After ngModel updates the model, comp.name should be ${expectedNewName} `);
|
||||
});
|
||||
}));
|
||||
|
||||
// fakeAsync version of ngModel input test enables sync test style
|
||||
// synchronous `tick` replaces asynchronous promise-base `whenStable`
|
||||
it('should support entering text in input box (ngModel) - fakeAsync', fakeAsync(() => {
|
||||
const expectedOrigName = 'John';
|
||||
const expectedNewName = 'Sally';
|
||||
|
||||
const fixture = TestBed.createComponent(InputComponent);
|
||||
fixture.detectChanges();
|
||||
|
||||
const comp = fixture.componentInstance;
|
||||
const input = <HTMLInputElement> fixture.debugElement.query(By.css('input')).nativeElement;
|
||||
|
||||
expect(comp.name).toBe(expectedOrigName,
|
||||
`At start name should be ${expectedOrigName} `);
|
||||
|
||||
// wait until ngModel binds comp.name to input box
|
||||
tick();
|
||||
expect(input.value).toBe(expectedOrigName,
|
||||
`After ngModel updates input box, input.value should be ${expectedOrigName} `);
|
||||
|
||||
// simulate user entering new name in input
|
||||
input.value = expectedNewName;
|
||||
|
||||
// that change doesn't flow to the component immediately
|
||||
expect(comp.name).toBe(expectedOrigName,
|
||||
`comp.name should still be ${expectedOrigName} after value change, before binding happens`);
|
||||
|
||||
// dispatch a DOM event so that Angular learns of input value change.
|
||||
// then wait a tick while ngModel pushes input.box value to comp.name
|
||||
input.dispatchEvent(newEvent('input'));
|
||||
tick();
|
||||
expect(comp.name).toBe(expectedNewName,
|
||||
`After ngModel updates the model, comp.name should be ${expectedNewName} `);
|
||||
}));
|
||||
|
||||
// #docregion ReversePipeComp
|
||||
it('ReversePipeComp should reverse the input text', fakeAsync(() => {
|
||||
const inputText = 'the quick brown fox.';
|
||||
const expectedText = '.xof nworb kciuq eht';
|
||||
|
||||
const fixture = TestBed.createComponent(ReversePipeComponent);
|
||||
fixture.detectChanges();
|
||||
|
||||
const comp = fixture.componentInstance;
|
||||
const input = fixture.debugElement.query(By.css('input')).nativeElement as HTMLInputElement;
|
||||
const span = fixture.debugElement.query(By.css('span')).nativeElement as HTMLElement;
|
||||
|
||||
// simulate user entering new name in input
|
||||
input.value = inputText;
|
||||
|
||||
// dispatch a DOM event so that Angular learns of input value change.
|
||||
// then wait a tick while ngModel pushes input.box value to comp.text
|
||||
// and Angular updates the output span
|
||||
input.dispatchEvent(newEvent('input'));
|
||||
tick();
|
||||
fixture.detectChanges();
|
||||
expect(span.textContent).toBe(expectedText, 'output span');
|
||||
expect(comp.text).toBe(inputText, 'component.text');
|
||||
}));
|
||||
// #enddocregion ReversePipeComp
|
||||
|
||||
// Use this technique to find attached directives of any kind
|
||||
it('can examine attached directives and listeners', () => {
|
||||
const fixture = TestBed.createComponent(InputComponent);
|
||||
fixture.detectChanges();
|
||||
|
||||
const inputEl = fixture.debugElement.query(By.css('input'));
|
||||
|
||||
expect(inputEl.providerTokens).toContain(NgModel, 'NgModel directive');
|
||||
|
||||
const ngControl = inputEl.injector.get(NgControl);
|
||||
expect(ngControl).toEqual(jasmine.any(NgControl), 'NgControl directive');
|
||||
|
||||
expect(inputEl.listeners.length).toBeGreaterThan(2, 'several listeners attached');
|
||||
});
|
||||
|
||||
// #docregion dom-attributes
|
||||
it('BankAccountComponent 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(childComp, 'context is the child 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.styles['color']).toBe(comp.color, 'color style');
|
||||
expect(el.styles['width']).toBe(comp.width + 'px', 'width style');
|
||||
// #enddocregion dom-attributes
|
||||
|
||||
// Removed on 12/02/2016 when ceased public discussion of the `Renderer`. Revive in future?
|
||||
// expect(el.properties['customProperty']).toBe(true, 'customProperty');
|
||||
|
||||
// #docregion dom-attributes
|
||||
});
|
||||
// #enddocregion dom-attributes
|
||||
|
||||
|
||||
});
|
||||
|
||||
describe('TestBed Component Overrides:', () => {
|
||||
|
||||
it('should override ChildComp\'s template', () => {
|
||||
|
||||
const fixture = TestBed.configureTestingModule({
|
||||
declarations: [Child1Component],
|
||||
})
|
||||
.overrideComponent(Child1Component, {
|
||||
set: { template: '<span>Fake</span>' }
|
||||
})
|
||||
.createComponent(Child1Component);
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(fixture).toHaveText('Fake');
|
||||
});
|
||||
|
||||
it('should override TestProvidersComp\'s FancyService provider', () => {
|
||||
const fixture = TestBed.configureTestingModule({
|
||||
declarations: [TestProvidersComponent],
|
||||
})
|
||||
.overrideComponent(TestProvidersComponent, {
|
||||
remove: { providers: [FancyService]},
|
||||
add: { providers: [{ provide: FancyService, useClass: FakeFancyService }] },
|
||||
|
||||
// Or replace them all (this component has only one provider)
|
||||
// set: { providers: [{ provide: FancyService, useClass: FakeFancyService }] },
|
||||
})
|
||||
.createComponent(TestProvidersComponent);
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(fixture).toHaveText('injected value: faked value', 'text');
|
||||
|
||||
// Explore the providerTokens
|
||||
const tokens = fixture.debugElement.providerTokens;
|
||||
expect(tokens).toContain(fixture.componentInstance.constructor, 'component ctor');
|
||||
expect(tokens).toContain(TestProvidersComponent, 'TestProvidersComp');
|
||||
expect(tokens).toContain(FancyService, 'FancyService');
|
||||
});
|
||||
|
||||
it('should override TestViewProvidersComp\'s FancyService viewProvider', () => {
|
||||
const fixture = TestBed.configureTestingModule({
|
||||
declarations: [TestViewProvidersComponent],
|
||||
})
|
||||
.overrideComponent(TestViewProvidersComponent, {
|
||||
// remove: { viewProviders: [FancyService]},
|
||||
// add: { viewProviders: [{ provide: FancyService, useClass: FakeFancyService }] },
|
||||
|
||||
// Or replace them all (this component has only one viewProvider)
|
||||
set: { viewProviders: [{ provide: FancyService, useClass: FakeFancyService }] },
|
||||
})
|
||||
.createComponent(TestViewProvidersComponent);
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(fixture).toHaveText('injected value: faked value');
|
||||
});
|
||||
|
||||
it('injected provider should not be same as component\'s provider', () => {
|
||||
|
||||
// TestComponent is parent of TestProvidersComponent
|
||||
@Component({ template: '<my-service-comp></my-service-comp>' })
|
||||
class TestComponent {}
|
||||
|
||||
// 3 levels of FancyService provider: module, TestCompomponent, TestProvidersComponent
|
||||
const fixture = TestBed.configureTestingModule({
|
||||
declarations: [TestComponent, TestProvidersComponent],
|
||||
providers: [FancyService]
|
||||
})
|
||||
.overrideComponent(TestComponent, {
|
||||
set: { providers: [{ provide: FancyService, useValue: {} }] }
|
||||
})
|
||||
.overrideComponent(TestProvidersComponent, {
|
||||
set: { providers: [{ provide: FancyService, useClass: FakeFancyService }] }
|
||||
})
|
||||
.createComponent(TestComponent);
|
||||
|
||||
let testBedProvider: FancyService;
|
||||
let tcProvider: {};
|
||||
let tpcProvider: FakeFancyService;
|
||||
|
||||
// `inject` uses TestBed's injector
|
||||
inject([FancyService], (s: FancyService) => testBedProvider = s)();
|
||||
tcProvider = fixture.debugElement.injector.get(FancyService);
|
||||
tpcProvider = fixture.debugElement.children[0].injector.get(FancyService) as FakeFancyService;
|
||||
|
||||
expect(testBedProvider).not.toBe(<any> tcProvider, 'testBed/tc not same providers');
|
||||
expect(testBedProvider).not.toBe(tpcProvider, 'testBed/tpc not same providers');
|
||||
|
||||
expect(testBedProvider instanceof FancyService).toBe(true, 'testBedProvider is FancyService');
|
||||
expect(tcProvider).toEqual({}, 'tcProvider is {}');
|
||||
expect(tpcProvider instanceof FakeFancyService).toBe(true, 'tpcProvider is FakeFancyService');
|
||||
});
|
||||
|
||||
it('can access template local variables as references', () => {
|
||||
const fixture = TestBed.configureTestingModule({
|
||||
declarations: [ShellComponent, NeedsContentComponent, Child1Component, Child2Component, Child3Component],
|
||||
})
|
||||
.overrideComponent(ShellComponent, {
|
||||
set: {
|
||||
selector: 'test-shell',
|
||||
template: `
|
||||
<needs-content #nc>
|
||||
<child-1 #content text="My"></child-1>
|
||||
<child-2 #content text="dog"></child-2>
|
||||
<child-2 text="has"></child-2>
|
||||
<child-3 #content text="fleas"></child-3>
|
||||
<div #content>!</div>
|
||||
</needs-content>
|
||||
`
|
||||
}
|
||||
})
|
||||
.createComponent(ShellComponent);
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
// NeedsContentComp is the child of ShellComp
|
||||
const el = fixture.debugElement.children[0];
|
||||
const comp = el.componentInstance;
|
||||
|
||||
expect(comp.children.toArray().length).toBe(4,
|
||||
'three different child components and an ElementRef with #content');
|
||||
|
||||
expect(el.references['nc']).toBe(comp, '#nc reference to component');
|
||||
|
||||
// #docregion custom-predicate
|
||||
// Filter for DebugElements with a #content reference
|
||||
const contentRefs = el.queryAll( de => de.references['content']);
|
||||
// #enddocregion custom-predicate
|
||||
expect(contentRefs.length).toBe(4, 'elements w/ a #content reference');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('Nested (one-deep) component override', () => {
|
||||
|
||||
beforeEach( async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ParentComponent, FakeChildComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
it('ParentComp should use Fake Child component', () => {
|
||||
const fixture = TestBed.createComponent(ParentComponent);
|
||||
fixture.detectChanges();
|
||||
expect(fixture).toHaveText('Parent(Fake Child)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Nested (two-deep) component override', () => {
|
||||
|
||||
beforeEach( async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ParentComponent, FakeChildWithGrandchildComponent, FakeGrandchildComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
it('should use Fake Grandchild component', () => {
|
||||
const fixture = TestBed.createComponent(ParentComponent);
|
||||
fixture.detectChanges();
|
||||
expect(fixture).toHaveText('Parent(Fake Child(Fake Grandchild))');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Lifecycle hooks w/ MyIfParentComp', () => {
|
||||
let fixture: ComponentFixture<MyIfParentComponent>;
|
||||
let parent: MyIfParentComponent;
|
||||
let child: MyIfChildComponent;
|
||||
|
||||
beforeEach( async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [FormsModule],
|
||||
declarations: [MyIfChildComponent, MyIfParentComponent]
|
||||
})
|
||||
.compileComponents().then(() => {
|
||||
fixture = TestBed.createComponent(MyIfParentComponent);
|
||||
parent = fixture.componentInstance;
|
||||
});
|
||||
}));
|
||||
|
||||
it('should instantiate parent component', () => {
|
||||
expect(parent).not.toBeNull('parent component should exist');
|
||||
});
|
||||
|
||||
it('parent component OnInit should NOT be called before first detectChanges()', () => {
|
||||
expect(parent.ngOnInitCalled).toBe(false);
|
||||
});
|
||||
|
||||
it('parent component OnInit should be called after first detectChanges()', () => {
|
||||
fixture.detectChanges();
|
||||
expect(parent.ngOnInitCalled).toBe(true);
|
||||
});
|
||||
|
||||
it('child component should exist after OnInit', () => {
|
||||
fixture.detectChanges();
|
||||
getChild();
|
||||
expect(child instanceof MyIfChildComponent).toBe(true, 'should create child');
|
||||
});
|
||||
|
||||
it('should have called child component\'s OnInit ', () => {
|
||||
fixture.detectChanges();
|
||||
getChild();
|
||||
expect(child.ngOnInitCalled).toBe(true);
|
||||
});
|
||||
|
||||
it('child component called OnChanges once', () => {
|
||||
fixture.detectChanges();
|
||||
getChild();
|
||||
expect(child.ngOnChangesCounter).toBe(1);
|
||||
});
|
||||
|
||||
it('changed parent value flows to child', () => {
|
||||
fixture.detectChanges();
|
||||
getChild();
|
||||
|
||||
parent.parentValue = 'foo';
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(child.ngOnChangesCounter).toBe(2,
|
||||
'expected 2 changes: initial value and changed value');
|
||||
expect(child.childValue).toBe('foo',
|
||||
'childValue should eq changed parent value');
|
||||
});
|
||||
|
||||
// must be async test to see child flow to parent
|
||||
it('changed child value flows to parent', async(() => {
|
||||
fixture.detectChanges();
|
||||
getChild();
|
||||
|
||||
child.childValue = 'bar';
|
||||
|
||||
return new Promise(resolve => {
|
||||
// Wait one JS engine turn!
|
||||
setTimeout(() => resolve(), 0);
|
||||
})
|
||||
.then(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(child.ngOnChangesCounter).toBe(2,
|
||||
'expected 2 changes: initial value and changed value');
|
||||
expect(parent.parentValue).toBe('bar',
|
||||
'parentValue should eq changed parent value');
|
||||
});
|
||||
|
||||
}));
|
||||
|
||||
it('clicking "Close Child" triggers child OnDestroy', () => {
|
||||
fixture.detectChanges();
|
||||
getChild();
|
||||
|
||||
const btn = fixture.debugElement.query(By.css('button'));
|
||||
click(btn);
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(child.ngOnDestroyCalled).toBe(true);
|
||||
});
|
||||
|
||||
////// helpers ///
|
||||
/**
|
||||
* Get the MyIfChildComp from parent; fail w/ good message if cannot.
|
||||
*/
|
||||
function getChild() {
|
||||
|
||||
let childDe: DebugElement; // DebugElement that should hold the MyIfChildComp
|
||||
|
||||
// The Hard Way: requires detailed knowledge of the parent template
|
||||
try {
|
||||
childDe = fixture.debugElement.children[4].children[0];
|
||||
} catch (err) { /* we'll report the error */ }
|
||||
|
||||
// DebugElement.queryAll: if we wanted all of many instances:
|
||||
childDe = fixture.debugElement
|
||||
.queryAll(function (de) { return de.componentInstance instanceof MyIfChildComponent; })[0];
|
||||
|
||||
// WE'LL USE THIS APPROACH !
|
||||
// DebugElement.query: find first instance (if any)
|
||||
childDe = fixture.debugElement
|
||||
.query(function (de) { return de.componentInstance instanceof MyIfChildComponent; });
|
||||
|
||||
if (childDe && childDe.componentInstance) {
|
||||
child = childDe.componentInstance;
|
||||
} else {
|
||||
fail('Unable to find MyIfChildComp within MyIfParentComp');
|
||||
}
|
||||
|
||||
return child;
|
||||
}
|
||||
});
|
||||
|
||||
////////// Fakes ///////////
|
||||
|
||||
@Component({
|
||||
selector: 'child-1',
|
||||
template: `Fake Child`
|
||||
})
|
||||
class FakeChildComponent { }
|
||||
|
||||
@Component({
|
||||
selector: 'child-1',
|
||||
template: `Fake Child(<grandchild-1></grandchild-1>)`
|
||||
})
|
||||
class FakeChildWithGrandchildComponent { }
|
||||
|
||||
@Component({
|
||||
selector: 'grandchild-1',
|
||||
template: `Fake Grandchild`
|
||||
})
|
||||
class FakeGrandchildComponent { }
|
||||
|
||||
@Injectable()
|
||||
class FakeFancyService extends FancyService {
|
||||
value = 'faked value';
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
// #docplaster
|
||||
// #docregion imports
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { DebugElement } from '@angular/core';
|
||||
|
||||
import { BannerComponent } from './banner-inline.component';
|
||||
// #enddocregion imports
|
||||
|
||||
// #docregion setup
|
||||
describe('BannerComponent (inline template)', () => {
|
||||
|
||||
let comp: BannerComponent;
|
||||
let fixture: ComponentFixture<BannerComponent>;
|
||||
let de: DebugElement;
|
||||
let el: HTMLElement;
|
||||
|
||||
// #docregion before-each
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ BannerComponent ], // declare the test component
|
||||
});
|
||||
|
||||
fixture = TestBed.createComponent(BannerComponent);
|
||||
|
||||
comp = fixture.componentInstance; // BannerComponent test instance
|
||||
|
||||
// query for the title <h1> by CSS element selector
|
||||
de = fixture.debugElement.query(By.css('h1'));
|
||||
el = de.nativeElement;
|
||||
});
|
||||
// #enddocregion before-each
|
||||
// #enddocregion setup
|
||||
|
||||
// #docregion test-w-o-detect-changes
|
||||
it('no title in the DOM until manually call `detectChanges`', () => {
|
||||
expect(el.textContent).toEqual('');
|
||||
});
|
||||
// #enddocregion test-w-o-detect-changes
|
||||
|
||||
// #docregion tests
|
||||
it('should display original title', () => {
|
||||
fixture.detectChanges();
|
||||
expect(el.textContent).toContain(comp.title);
|
||||
});
|
||||
|
||||
it('should display a different test title', () => {
|
||||
comp.title = 'Test Title';
|
||||
fixture.detectChanges();
|
||||
expect(el.textContent).toContain('Test Title');
|
||||
});
|
||||
// #enddocregion tests
|
||||
// #docregion setup
|
||||
});
|
||||
// #enddocregion setup
|
@ -1,53 +0,0 @@
|
||||
// #docplaster
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { DebugElement } from '@angular/core';
|
||||
|
||||
import { BannerComponent } from './banner.component';
|
||||
|
||||
describe('BannerComponent (templateUrl)', () => {
|
||||
|
||||
let comp: BannerComponent;
|
||||
let fixture: ComponentFixture<BannerComponent>;
|
||||
let de: DebugElement;
|
||||
let el: HTMLElement;
|
||||
|
||||
// #docregion async-before-each
|
||||
// async beforeEach
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ BannerComponent ], // declare the test component
|
||||
})
|
||||
.compileComponents(); // compile template and css
|
||||
}));
|
||||
// #enddocregion async-before-each
|
||||
|
||||
// #docregion sync-before-each
|
||||
// synchronous beforeEach
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(BannerComponent);
|
||||
|
||||
comp = fixture.componentInstance; // BannerComponent test instance
|
||||
|
||||
// query for the title <h1> by CSS element selector
|
||||
de = fixture.debugElement.query(By.css('h1'));
|
||||
el = de.nativeElement;
|
||||
});
|
||||
// #enddocregion sync-before-each
|
||||
|
||||
it('no title in the DOM until manually call `detectChanges`', () => {
|
||||
expect(el.textContent).toEqual('');
|
||||
});
|
||||
|
||||
it('should display original title', () => {
|
||||
fixture.detectChanges();
|
||||
expect(el.textContent).toContain(comp.title);
|
||||
});
|
||||
|
||||
it('should display a different test title', () => {
|
||||
comp.title = 'Test Title';
|
||||
fixture.detectChanges();
|
||||
expect(el.textContent).toContain('Test Title');
|
||||
});
|
||||
|
||||
});
|
@ -0,0 +1,72 @@
|
||||
// #docplaster
|
||||
// #docregion import-async
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
// #enddocregion import-async
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { DebugElement } from '@angular/core';
|
||||
|
||||
import { BannerComponent } from './banner-external.component';
|
||||
|
||||
describe('BannerComponent (external files)', () => {
|
||||
let component: BannerComponent;
|
||||
let fixture: ComponentFixture<BannerComponent>;
|
||||
let h1: HTMLElement;
|
||||
|
||||
describe('Two beforeEach', () => {
|
||||
// #docregion async-before-each
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ BannerComponent ],
|
||||
})
|
||||
.compileComponents(); // compile template and css
|
||||
}));
|
||||
// #enddocregion async-before-each
|
||||
|
||||
// synchronous beforeEach
|
||||
// #docregion sync-before-each
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(BannerComponent);
|
||||
component = fixture.componentInstance; // BannerComponent test instance
|
||||
h1 = fixture.nativeElement.querySelector('h1');
|
||||
});
|
||||
// #enddocregion sync-before-each
|
||||
|
||||
tests();
|
||||
});
|
||||
|
||||
describe('One beforeEach', () => {
|
||||
// #docregion one-before-each
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ BannerComponent ],
|
||||
})
|
||||
.compileComponents()
|
||||
.then(() => {
|
||||
fixture = TestBed.createComponent(BannerComponent);
|
||||
component = fixture.componentInstance;
|
||||
h1 = fixture.nativeElement.querySelector('h1');
|
||||
});
|
||||
}));
|
||||
// #enddocregion one-before-each
|
||||
|
||||
tests();
|
||||
});
|
||||
|
||||
function tests() {
|
||||
it('no title in the DOM until manually call `detectChanges`', () => {
|
||||
expect(h1.textContent).toEqual('');
|
||||
});
|
||||
|
||||
it('should display original title', () => {
|
||||
fixture.detectChanges();
|
||||
expect(h1.textContent).toContain(component.title);
|
||||
});
|
||||
|
||||
it('should display a different test title', () => {
|
||||
component.title = 'Test Title';
|
||||
fixture.detectChanges();
|
||||
expect(h1.textContent).toContain('Test Title');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -1,11 +1,14 @@
|
||||
// #docplaster
|
||||
// #docregion
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
// #docregion metadata
|
||||
@Component({
|
||||
selector: 'app-banner',
|
||||
template: '<h1>{{title}}</h1>'
|
||||
templateUrl: './banner-external.component.html',
|
||||
styleUrls: ['./banner-external.component.css']
|
||||
})
|
||||
// #enddocregion metadata
|
||||
export class BannerComponent {
|
||||
title = 'Test Tour of Heroes';
|
||||
}
|
||||
|
@ -0,0 +1,119 @@
|
||||
// #docplaster
|
||||
// #docregion import-by
|
||||
import { By } from '@angular/platform-browser';
|
||||
// #enddocregion import-by
|
||||
// #docregion import-debug-element
|
||||
import { DebugElement } from '@angular/core';
|
||||
// #enddocregion import-debug-element
|
||||
// #docregion v1
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
// #enddocregion v1
|
||||
import { BannerComponent } from './banner-initial.component';
|
||||
/*
|
||||
// #docregion v1
|
||||
import { BannerComponent } from './banner.component';
|
||||
|
||||
describe('BannerComponent', () => {
|
||||
// #enddocregion v1
|
||||
*/
|
||||
describe('BannerComponent (initial CLI generated)', () => {
|
||||
// #docregion v1
|
||||
let component: BannerComponent;
|
||||
let fixture: ComponentFixture<BannerComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ BannerComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(BannerComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeDefined();
|
||||
});
|
||||
});
|
||||
// #enddocregion v1
|
||||
|
||||
// #docregion v2
|
||||
describe('BannerComponent (minimal)', () => {
|
||||
it('should create', () => {
|
||||
// #docregion configureTestingModule
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ BannerComponent ]
|
||||
});
|
||||
// #enddocregion configureTestingModule
|
||||
// #docregion createComponent
|
||||
const fixture = TestBed.createComponent(BannerComponent);
|
||||
// #enddocregion createComponent
|
||||
// #docregion componentInstance
|
||||
const component = fixture.componentInstance;
|
||||
expect(component).toBeDefined();
|
||||
// #enddocregion componentInstance
|
||||
});
|
||||
});
|
||||
// #enddocregion v2
|
||||
|
||||
// #docregion v3, v4
|
||||
describe('BannerComponent (with beforeEach)', () => {
|
||||
let component: BannerComponent;
|
||||
let fixture: ComponentFixture<BannerComponent>;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ BannerComponent ]
|
||||
});
|
||||
fixture = TestBed.createComponent(BannerComponent);
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeDefined();
|
||||
});
|
||||
// #enddocregion v3
|
||||
|
||||
// #docregion v4-test-2
|
||||
it('should contain "banner works!"', () => {
|
||||
const bannerElement: HTMLElement = fixture.nativeElement;
|
||||
expect(bannerElement.textContent).toContain('banner works!');
|
||||
});
|
||||
// #enddocregion v4-test-2
|
||||
|
||||
// #docregion v4-test-3
|
||||
it('should have <p> with "banner works!"', () => {
|
||||
// #docregion nativeElement
|
||||
const bannerElement: HTMLElement = fixture.nativeElement;
|
||||
// #enddocregion nativeElement
|
||||
const p = bannerElement.querySelector('p');
|
||||
expect(p.textContent).toEqual('banner works!');
|
||||
});
|
||||
// #enddocregion v4-test-3
|
||||
|
||||
|
||||
// #docregion v4-test-4
|
||||
it('should find the <p> with fixture.debugElement.nativeElement)', () => {
|
||||
// #docregion debugElement-nativeElement
|
||||
const bannerDe: DebugElement = fixture.debugElement;
|
||||
const bannerEl: HTMLElement = bannerDe.nativeElement;
|
||||
// #enddocregion debugElement-nativeElement
|
||||
const p = bannerEl.querySelector('p');
|
||||
expect(p.textContent).toEqual('banner works!');
|
||||
});
|
||||
// #enddocregion v4-test-4
|
||||
|
||||
// #docregion v4-test-5
|
||||
it('should find the <p> with fixture.debugElement.query(By.css)', () => {
|
||||
const bannerDe: DebugElement = fixture.debugElement;
|
||||
const paragraphDe = bannerDe.query(By.css('p'));
|
||||
const p: HTMLElement = paragraphDe.nativeElement;
|
||||
expect(p.textContent).toEqual('banner works!');
|
||||
});
|
||||
// #enddocregion v4-test-5
|
||||
// #docregion v3
|
||||
});
|
||||
// #enddocregion v3, v4
|
@ -0,0 +1,10 @@
|
||||
// BannerComponent as initially generated by the CLI
|
||||
// #docregion
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-banner',
|
||||
template: `<p>banner works!</p>`,
|
||||
styles: []
|
||||
})
|
||||
export class BannerComponent { }
|
@ -7,53 +7,45 @@ import { async } from '@angular/core/testing';
|
||||
import { ComponentFixtureAutoDetect } from '@angular/core/testing';
|
||||
// #enddocregion import-ComponentFixtureAutoDetect
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { DebugElement } from '@angular/core';
|
||||
|
||||
import { BannerComponent } from './banner.component';
|
||||
|
||||
describe('BannerComponent (AutoChangeDetect)', () => {
|
||||
let comp: BannerComponent;
|
||||
let fixture: ComponentFixture<BannerComponent>;
|
||||
let de: DebugElement;
|
||||
let el: HTMLElement;
|
||||
let h1: HTMLElement;
|
||||
|
||||
beforeEach(async(() => {
|
||||
beforeEach(() => {
|
||||
// #docregion auto-detect
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ BannerComponent ],
|
||||
providers: [
|
||||
{ provide: ComponentFixtureAutoDetect, useValue: true }
|
||||
]
|
||||
})
|
||||
});
|
||||
// #enddocregion auto-detect
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(BannerComponent);
|
||||
comp = fixture.componentInstance;
|
||||
de = fixture.debugElement.query(By.css('h1'));
|
||||
el = de.nativeElement;
|
||||
h1 = fixture.nativeElement.querySelector('h1');
|
||||
});
|
||||
|
||||
// #docregion auto-detect-tests
|
||||
it('should display original title', () => {
|
||||
// Hooray! No `fixture.detectChanges()` needed
|
||||
expect(el.textContent).toContain(comp.title);
|
||||
expect(h1.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.textContent).toContain(oldTitle);
|
||||
expect(h1.textContent).toContain(oldTitle);
|
||||
});
|
||||
|
||||
it('should display updated title after detectChanges', () => {
|
||||
comp.title = 'Test Title';
|
||||
fixture.detectChanges(); // detect changes explicitly
|
||||
expect(el.textContent).toContain(comp.title);
|
||||
expect(h1.textContent).toContain(comp.title);
|
||||
});
|
||||
// #enddocregion auto-detect-tests
|
||||
});
|
@ -0,0 +1,56 @@
|
||||
// #docplaster
|
||||
// #docregion
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { DebugElement } from '@angular/core';
|
||||
|
||||
import { BannerComponent } from './banner.component';
|
||||
|
||||
describe('BannerComponent (inline template)', () => {
|
||||
// #docregion setup
|
||||
let component: BannerComponent;
|
||||
let fixture: ComponentFixture<BannerComponent>;
|
||||
let h1: HTMLElement;
|
||||
|
||||
// #docregion configure-and-create
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ BannerComponent ],
|
||||
});
|
||||
fixture = TestBed.createComponent(BannerComponent);
|
||||
// #enddocregion configure-and-create
|
||||
component = fixture.componentInstance; // BannerComponent test instance
|
||||
h1 = fixture.nativeElement.querySelector('h1');
|
||||
// #docregion configure-and-create
|
||||
});
|
||||
// #enddocregion setup, configure-and-create
|
||||
|
||||
// #docregion test-w-o-detect-changes
|
||||
it('no title in the DOM after createComponent()', () => {
|
||||
expect(h1.textContent).toEqual('');
|
||||
});
|
||||
// #enddocregion test-w-o-detect-changes
|
||||
|
||||
// #docregion expect-h1-default-v1
|
||||
it('should display original title', () => {
|
||||
// #enddocregion expect-h1-default-v1
|
||||
fixture.detectChanges();
|
||||
// #docregion expect-h1-default-v1
|
||||
expect(h1.textContent).toContain(component.title);
|
||||
});
|
||||
// #enddocregion expect-h1-default-v1
|
||||
|
||||
// #docregion expect-h1-default
|
||||
it('should display original title after detectChanges()', () => {
|
||||
fixture.detectChanges();
|
||||
expect(h1.textContent).toContain(component.title);
|
||||
});
|
||||
// #enddocregion expect-h1-default
|
||||
|
||||
// #docregion after-change
|
||||
it('should display a different test title', () => {
|
||||
component.title = 'Test Title';
|
||||
fixture.detectChanges();
|
||||
expect(h1.textContent).toContain('Test Title');
|
||||
});
|
||||
// #enddocregion after-change
|
||||
});
|
@ -1,12 +1,12 @@
|
||||
// #docregion
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
// #docregion component
|
||||
@Component({
|
||||
selector: 'app-banner',
|
||||
templateUrl: './banner.component.html',
|
||||
styleUrls: ['./banner.component.css']
|
||||
template: '<h1>{{title}}</h1>',
|
||||
styles: ['h1 { color: green; font-size: 350%}']
|
||||
})
|
||||
export class BannerComponent {
|
||||
title = 'Test Tour of Heroes';
|
||||
}
|
||||
|
||||
// #enddocregion component
|
@ -1,4 +0,0 @@
|
||||
<!-- #docregion -->
|
||||
<div (click)="click()" class="hero">
|
||||
{{hero.name | uppercase}}
|
||||
</div>
|
@ -1,7 +1,9 @@
|
||||
|
||||
// #docplaster
|
||||
import { async, ComponentFixture, TestBed
|
||||
} from '@angular/core/testing';
|
||||
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { DebugElement } from '@angular/core';
|
||||
|
||||
import { addMatchers, click } from '../../testing';
|
||||
@ -11,64 +13,96 @@ import { DashboardHeroComponent } from './dashboard-hero.component';
|
||||
|
||||
beforeEach( addMatchers );
|
||||
|
||||
describe('DashboardHeroComponent class only', () => {
|
||||
// #docregion class-only
|
||||
it('raises the selected event when clicked', () => {
|
||||
const comp = new DashboardHeroComponent();
|
||||
const hero: Hero = { id: 42, name: 'Test' };
|
||||
comp.hero = hero;
|
||||
|
||||
comp.selected.subscribe(selectedHero => expect(selectedHero).toBe(hero));
|
||||
comp.click();
|
||||
});
|
||||
// #enddocregion class-only
|
||||
});
|
||||
|
||||
describe('DashboardHeroComponent when tested directly', () => {
|
||||
|
||||
let comp: DashboardHeroComponent;
|
||||
let expectedHero: Hero;
|
||||
let fixture: ComponentFixture<DashboardHeroComponent>;
|
||||
let heroEl: DebugElement;
|
||||
let heroDe: DebugElement;
|
||||
let heroEl: HTMLElement;
|
||||
|
||||
// #docregion setup, compile-components
|
||||
// async beforeEach
|
||||
beforeEach( async(() => {
|
||||
beforeEach(async(() => {
|
||||
// #docregion setup, config-testbed
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ DashboardHeroComponent ],
|
||||
declarations: [ DashboardHeroComponent ]
|
||||
})
|
||||
.compileComponents(); // compile template and css
|
||||
// #enddocregion setup, config-testbed
|
||||
.compileComponents();
|
||||
}));
|
||||
// #enddocregion compile-components
|
||||
|
||||
// synchronous beforeEach
|
||||
beforeEach(() => {
|
||||
// #docregion setup
|
||||
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');
|
||||
// find the hero's DebugElement and element
|
||||
heroDe = fixture.debugElement.query(By.css('.hero'));
|
||||
heroEl = heroDe.nativeElement;
|
||||
|
||||
// mock the hero supplied by the parent component
|
||||
expectedHero = { id: 42, name: 'Test Name' };
|
||||
|
||||
// simulate the parent setting the input property with that hero
|
||||
comp.hero = expectedHero;
|
||||
fixture.detectChanges(); // trigger initial data binding
|
||||
|
||||
// trigger initial data binding
|
||||
fixture.detectChanges();
|
||||
// #enddocregion setup
|
||||
});
|
||||
// #enddocregion setup
|
||||
|
||||
// #docregion name-test
|
||||
it('should display hero name', () => {
|
||||
it('should display hero name in uppercase', () => {
|
||||
const expectedPipedName = expectedHero.name.toUpperCase();
|
||||
expect(heroEl.nativeElement.textContent).toContain(expectedPipedName);
|
||||
expect(heroEl.textContent).toContain(expectedPipedName);
|
||||
});
|
||||
// #enddocregion name-test
|
||||
|
||||
// #docregion click-test
|
||||
it('should raise selected event when clicked', () => {
|
||||
it('should raise selected event when clicked (triggerEventHandler)', () => {
|
||||
let selectedHero: Hero;
|
||||
comp.selected.subscribe((hero: Hero) => selectedHero = hero);
|
||||
|
||||
// #docregion trigger-event-handler
|
||||
heroEl.triggerEventHandler('click', null);
|
||||
heroDe.triggerEventHandler('click', null);
|
||||
// #enddocregion trigger-event-handler
|
||||
expect(selectedHero).toBe(expectedHero);
|
||||
});
|
||||
// #enddocregion click-test
|
||||
|
||||
// #docregion click-test-2
|
||||
it('should raise selected event when clicked', () => {
|
||||
let selectedHero: Hero;
|
||||
comp.selected.subscribe((hero: Hero) => selectedHero = hero);
|
||||
// #docregion click-test-2
|
||||
it('should raise selected event when clicked (element.click)', () => {
|
||||
let selectedHero: Hero;
|
||||
comp.selected.subscribe((hero: Hero) => selectedHero = hero);
|
||||
|
||||
heroEl.click();
|
||||
expect(selectedHero).toBe(expectedHero);
|
||||
});
|
||||
// #enddocregion click-test-2
|
||||
|
||||
// #docregion click-test-3
|
||||
it('should raise selected event when clicked (click helper)', () => {
|
||||
let selectedHero: Hero;
|
||||
comp.selected.subscribe(hero => selectedHero = hero);
|
||||
|
||||
click(heroDe); // click helper with DebugElement
|
||||
click(heroEl); // click helper with native element
|
||||
|
||||
click(heroEl); // triggerEventHandler helper
|
||||
expect(selectedHero).toBe(expectedHero);
|
||||
});
|
||||
// #enddocregion click-test-2
|
||||
// #enddocregion click-test-3
|
||||
});
|
||||
|
||||
//////////////////
|
||||
@ -76,28 +110,31 @@ describe('DashboardHeroComponent when tested directly', () => {
|
||||
describe('DashboardHeroComponent when inside a test host', () => {
|
||||
let testHost: TestHostComponent;
|
||||
let fixture: ComponentFixture<TestHostComponent>;
|
||||
let heroEl: DebugElement;
|
||||
let heroEl: HTMLElement;
|
||||
|
||||
// #docregion test-host-setup
|
||||
beforeEach( async(() => {
|
||||
beforeEach(async(() => {
|
||||
// #docregion test-host-setup
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ DashboardHeroComponent, TestHostComponent ], // declare both
|
||||
}).compileComponents();
|
||||
declarations: [ DashboardHeroComponent, TestHostComponent ]
|
||||
})
|
||||
// #enddocregion test-host-setup
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
// #docregion test-host-setup
|
||||
// create TestHostComponent instead of DashboardHeroComponent
|
||||
fixture = TestBed.createComponent(TestHostComponent);
|
||||
testHost = fixture.componentInstance;
|
||||
heroEl = fixture.debugElement.query(By.css('.hero')); // find hero
|
||||
heroEl = fixture.nativeElement.querySelector('.hero');
|
||||
fixture.detectChanges(); // trigger initial data binding
|
||||
// #enddocregion test-host-setup
|
||||
});
|
||||
// #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);
|
||||
expect(heroEl.textContent).toContain(expectedPipedName);
|
||||
});
|
||||
|
||||
it('should raise selected event when clicked', () => {
|
||||
@ -114,10 +151,12 @@ import { Component } from '@angular/core';
|
||||
// #docregion test-host
|
||||
@Component({
|
||||
template: `
|
||||
<dashboard-hero [hero]="hero" (selected)="onSelected($event)"></dashboard-hero>`
|
||||
<dashboard-hero
|
||||
[hero]="hero" (selected)="onSelected($event)">
|
||||
</dashboard-hero>`
|
||||
})
|
||||
class TestHostComponent {
|
||||
hero = new Hero(42, 'Test Name');
|
||||
hero: Hero = {id: 42, name: 'Test Name' };
|
||||
selectedHero: Hero;
|
||||
onSelected(hero: Hero) { this.selectedHero = hero; }
|
||||
}
|
||||
|
@ -5,13 +5,17 @@ import { Hero } from '../model/hero';
|
||||
|
||||
// #docregion component
|
||||
@Component({
|
||||
selector: 'dashboard-hero',
|
||||
templateUrl: './dashboard-hero.component.html',
|
||||
selector: 'dashboard-hero',
|
||||
template: `
|
||||
<div (click)="click()" class="hero">
|
||||
{{hero.name | uppercase}}
|
||||
</div>`,
|
||||
styleUrls: [ './dashboard-hero.component.css' ]
|
||||
})
|
||||
// #docregion class
|
||||
export class DashboardHeroComponent {
|
||||
@Input() hero: Hero;
|
||||
@Output() selected = new EventEmitter<Hero>();
|
||||
click() { this.selected.emit(this.hero); }
|
||||
}
|
||||
// #enddocregion component
|
||||
// #enddocregion component, class
|
||||
|
@ -1,24 +1,24 @@
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { DashboardComponent } from './dashboard.component';
|
||||
import { Hero } from '../model';
|
||||
import { Hero } from '../model/hero';
|
||||
|
||||
import { addMatchers } from '../../testing';
|
||||
import { FakeHeroService } from '../model/testing';
|
||||
import { TestHeroService, HeroService } from '../model/testing/test-hero.service';
|
||||
|
||||
class FakeRouter {
|
||||
navigateByUrl(url: string) { return url; }
|
||||
}
|
||||
|
||||
describe('DashboardComponent: w/o Angular TestBed', () => {
|
||||
describe('DashboardComponent class only', () => {
|
||||
let comp: DashboardComponent;
|
||||
let heroService: FakeHeroService;
|
||||
let heroService: TestHeroService;
|
||||
let router: Router;
|
||||
|
||||
beforeEach(() => {
|
||||
addMatchers();
|
||||
router = new FakeRouter() as any as Router;
|
||||
heroService = new FakeHeroService();
|
||||
heroService = new TestHeroService();
|
||||
comp = new DashboardComponent(router, heroService);
|
||||
});
|
||||
|
||||
@ -35,17 +35,19 @@ describe('DashboardComponent: w/o Angular TestBed', () => {
|
||||
|
||||
it('should HAVE heroes after HeroService gets them', (done: DoneFn) => {
|
||||
comp.ngOnInit(); // ngOnInit -> getHeroes
|
||||
heroService.lastPromise // the one from getHeroes
|
||||
.then(() => {
|
||||
heroService.lastResult // the one from getHeroes
|
||||
.subscribe(
|
||||
() => {
|
||||
// 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);
|
||||
done();
|
||||
},
|
||||
done.fail);
|
||||
});
|
||||
|
||||
it('should tell ROUTER to navigate by hero id', () => {
|
||||
const hero = new Hero(42, 'Abbracadabra');
|
||||
const hero: Hero = {id: 42, name: 'Abbracadabra' };
|
||||
const spy = spyOn(router, 'navigateByUrl');
|
||||
|
||||
comp.gotoDetail(hero);
|
||||
|
@ -2,9 +2,9 @@
|
||||
import { async, inject, ComponentFixture, TestBed
|
||||
} from '@angular/core/testing';
|
||||
|
||||
import { addMatchers, click } from '../../testing';
|
||||
import { HeroService } from '../model';
|
||||
import { FakeHeroService } from '../model/testing';
|
||||
import { addMatchers, asyncData, click } from '../../testing';
|
||||
import { HeroService } from '../model/hero.service';
|
||||
import { getTestHeroes } from '../model/testing/test-heroes';
|
||||
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { Router } from '@angular/router';
|
||||
@ -12,12 +12,6 @@ import { Router } from '@angular/router';
|
||||
import { DashboardComponent } from './dashboard.component';
|
||||
import { DashboardModule } from './dashboard.module';
|
||||
|
||||
// #docregion router-stub
|
||||
class RouterStub {
|
||||
navigateByUrl(url: string) { return url; }
|
||||
}
|
||||
// #enddocregion router-stub
|
||||
|
||||
beforeEach ( addMatchers );
|
||||
|
||||
let comp: DashboardComponent;
|
||||
@ -37,8 +31,8 @@ describe('DashboardComponent (deep)', () => {
|
||||
tests(clickForDeep);
|
||||
|
||||
function clickForDeep() {
|
||||
// get first <div class="hero"> DebugElement
|
||||
const heroEl = fixture.debugElement.query(By.css('.hero'));
|
||||
// get first <div class="hero">
|
||||
const heroEl: HTMLElement = fixture.nativeElement.querySelector('.hero');
|
||||
click(heroEl);
|
||||
}
|
||||
});
|
||||
@ -61,24 +55,32 @@ describe('DashboardComponent (shallow)', () => {
|
||||
|
||||
function clickForShallow() {
|
||||
// get first <dashboard-hero> DebugElement
|
||||
const heroEl = fixture.debugElement.query(By.css('dashboard-hero'));
|
||||
heroEl.triggerEventHandler('selected', comp.heroes[0]);
|
||||
const heroDe = fixture.debugElement.query(By.css('dashboard-hero'));
|
||||
heroDe.triggerEventHandler('selected', comp.heroes[0]);
|
||||
}
|
||||
});
|
||||
|
||||
/** Add TestBed providers, compile, and create DashboardComponent */
|
||||
function compileAndCreate() {
|
||||
// #docregion compile-and-create-body
|
||||
beforeEach( async(() => {
|
||||
beforeEach(async(() => {
|
||||
// #docregion router-spy
|
||||
const routerSpy = jasmine.createSpyObj('Router', ['navigateByUrl']);
|
||||
const heroServiceSpy = jasmine.createSpyObj('HeroService', ['getHeroes']);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{ provide: HeroService, useClass: FakeHeroService },
|
||||
{ provide: Router, useClass: RouterStub }
|
||||
{ provide: HeroService, useValue: heroServiceSpy },
|
||||
{ provide: Router, useValue: routerSpy }
|
||||
]
|
||||
})
|
||||
// #enddocregion router-spy
|
||||
.compileComponents().then(() => {
|
||||
fixture = TestBed.createComponent(DashboardComponent);
|
||||
comp = fixture.componentInstance;
|
||||
|
||||
// getHeroes spy returns observable of test heroes
|
||||
heroServiceSpy.getHeroes.and.returnValue(asyncData(getTestHeroes()));
|
||||
});
|
||||
// #enddocregion compile-and-create-body
|
||||
}));
|
||||
@ -104,8 +106,11 @@ function tests(heroClick: Function) {
|
||||
|
||||
describe('after get dashboard heroes', () => {
|
||||
|
||||
let router: Router;
|
||||
|
||||
// Trigger component so it gets heroes and binds to them
|
||||
beforeEach( async(() => {
|
||||
beforeEach(async(() => {
|
||||
router = fixture.debugElement.injector.get(Router);
|
||||
fixture.detectChanges(); // runs ngOnInit -> getHeroes
|
||||
fixture.whenStable() // No need for the `lastPromise` hack!
|
||||
.then(() => fixture.detectChanges()); // bind to heroes
|
||||
@ -119,29 +124,25 @@ function tests(heroClick: Function) {
|
||||
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'));
|
||||
const heroes = fixture.nativeElement.querySelectorAll('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');
|
||||
// #docregion navigate-test
|
||||
it('should tell ROUTER to navigate when hero clicked', () => {
|
||||
|
||||
heroClick(); // trigger click on first inner <div class="hero">
|
||||
|
||||
// args passed to router.navigateByUrl()
|
||||
// args passed to router.navigateByUrl() spy
|
||||
const spy = router.navigateByUrl as jasmine.Spy;
|
||||
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
|
||||
});
|
||||
// #enddocregion navigate-test
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { Hero } from '../model/hero';
|
||||
import { Hero } from '../model/hero';
|
||||
import { HeroService } from '../model/hero.service';
|
||||
|
||||
@Component({
|
||||
@ -23,7 +23,7 @@ export class DashboardComponent implements OnInit {
|
||||
|
||||
ngOnInit() {
|
||||
this.heroService.getHeroes()
|
||||
.then(heroes => this.heroes = heroes.slice(1, 5));
|
||||
.subscribe(heroes => this.heroes = heroes.slice(1, 5));
|
||||
}
|
||||
|
||||
// #docregion goto-detail
|
||||
|
@ -1,7 +1,8 @@
|
||||
// tslint:disable-next-line:no-unused-variable
|
||||
import { async, fakeAsync, tick } from '@angular/core/testing';
|
||||
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { of } from 'rxjs/observable/of';
|
||||
import { delay } from 'rxjs/operators';
|
||||
|
||||
describe('Angular async helper', () => {
|
||||
let actuallyDone = false;
|
||||
@ -34,8 +35,8 @@ describe('Angular async helper', () => {
|
||||
|
||||
// Use done. Cannot use setInterval with async or fakeAsync
|
||||
// See https://github.com/angular/angular/issues/10127
|
||||
it('should run async test with successful delayed Observable', (done: any) => {
|
||||
const source = Observable.of(true).delay(10);
|
||||
it('should run async test with successful delayed Observable', (done: DoneFn) => {
|
||||
const source = of(true).pipe(delay(10));
|
||||
source.subscribe(
|
||||
val => actuallyDone = true,
|
||||
err => fail(err),
|
||||
@ -46,7 +47,7 @@ describe('Angular async helper', () => {
|
||||
// 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);
|
||||
// const source = of(true).pipe(delay(10));
|
||||
// source.subscribe(
|
||||
// val => actuallyDone = true,
|
||||
// err => fail(err)
|
||||
@ -56,7 +57,7 @@ describe('Angular async helper', () => {
|
||||
// // 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);
|
||||
// const source = of(true).pipe(delay(10));
|
||||
// source.subscribe(
|
||||
// val => actuallyDone = true,
|
||||
// err => fail(err)
|
@ -1,5 +1,5 @@
|
||||
// main app entry point
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||
import { BagModule } from './bag';
|
||||
import { DemoModule } from './demo';
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(BagModule);
|
||||
platformBrowserDynamic().bootstrapModule(DemoModule);
|
153
aio/content/examples/testing/src/app/demo/demo.spec.ts
Normal file
153
aio/content/examples/testing/src/app/demo/demo.spec.ts
Normal file
@ -0,0 +1,153 @@
|
||||
// #docplaster
|
||||
import {
|
||||
LightswitchComponent,
|
||||
MasterService,
|
||||
ValueService,
|
||||
ReversePipe
|
||||
} from './demo';
|
||||
|
||||
///////// Fakes /////////
|
||||
export class FakeValueService extends ValueService {
|
||||
value = 'faked service value';
|
||||
}
|
||||
////////////////////////
|
||||
describe('demo (no TestBed):', () => {
|
||||
|
||||
// #docregion ValueService
|
||||
// Straight Jasmine testing without Angular's testing support
|
||||
describe('ValueService', () => {
|
||||
let service: ValueService;
|
||||
beforeEach(() => { service = new ValueService(); });
|
||||
|
||||
it('#getValue should return real value', () => {
|
||||
expect(service.getValue()).toBe('real value');
|
||||
});
|
||||
|
||||
it('#getObservableValue should return value from observable',
|
||||
(done: DoneFn) => {
|
||||
service.getObservableValue().subscribe(value => {
|
||||
expect(value).toBe('observable value');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('#getPromiseValue should return value from a promise',
|
||||
(done: DoneFn) => {
|
||||
service.getPromiseValue().then(value => {
|
||||
expect(value).toBe('promise value');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
// #enddocregion ValueService
|
||||
|
||||
// MasterService requires injection of a ValueService
|
||||
// #docregion MasterService
|
||||
describe('MasterService without Angular testing support', () => {
|
||||
let masterService: MasterService;
|
||||
|
||||
it('#getValue should return real value from the real service', () => {
|
||||
masterService = new MasterService(new ValueService());
|
||||
expect(masterService.getValue()).toBe('real value');
|
||||
});
|
||||
|
||||
it('#getValue should return faked value from a fakeService', () => {
|
||||
masterService = new MasterService(new FakeValueService());
|
||||
expect(masterService.getValue()).toBe('faked service value');
|
||||
});
|
||||
|
||||
it('#getValue should return faked value from a fake object', () => {
|
||||
const fake = { getValue: () => 'fake value' };
|
||||
masterService = new MasterService(fake as ValueService);
|
||||
expect(masterService.getValue()).toBe('fake value');
|
||||
});
|
||||
|
||||
it('#getValue should return stubbed value from a spy', () => {
|
||||
// create `getValue` spy on an object representing the ValueService
|
||||
const valueServiceSpy =
|
||||
jasmine.createSpyObj('ValueService', ['getValue']);
|
||||
|
||||
// set the value to return when the `getValue` spy is called.
|
||||
const stubValue = 'stub value';
|
||||
valueServiceSpy.getValue.and.returnValue(stubValue);
|
||||
|
||||
masterService = new MasterService(valueServiceSpy);
|
||||
|
||||
expect(masterService.getValue())
|
||||
.toBe(stubValue, 'service returned stub value');
|
||||
expect(valueServiceSpy.getValue.calls.count())
|
||||
.toBe(1, 'spy method was called once');
|
||||
expect(valueServiceSpy.getValue.calls.mostRecent().returnValue)
|
||||
.toBe(stubValue);
|
||||
});
|
||||
});
|
||||
// #enddocregion MasterService
|
||||
|
||||
describe('MasterService (no beforeEach)', () => {
|
||||
// #docregion no-before-each-test
|
||||
it('#getValue should return stubbed value from a spy', () => {
|
||||
// #docregion no-before-each-setup-call
|
||||
const { masterService, stubValue, valueServiceSpy } = setup();
|
||||
// #enddocregion no-before-each-setup-call
|
||||
expect(masterService.getValue())
|
||||
.toBe(stubValue, 'service returned stub value');
|
||||
expect(valueServiceSpy.getValue.calls.count())
|
||||
.toBe(1, 'spy method was called once');
|
||||
expect(valueServiceSpy.getValue.calls.mostRecent().returnValue)
|
||||
.toBe(stubValue);
|
||||
});
|
||||
// #enddocregion no-before-each-test
|
||||
|
||||
// #docregion no-before-each-setup
|
||||
function setup() {
|
||||
const valueServiceSpy =
|
||||
jasmine.createSpyObj('ValueService', ['getValue']);
|
||||
const stubValue = 'stub value';
|
||||
const masterService = new MasterService(valueServiceSpy);
|
||||
|
||||
valueServiceSpy.getValue.and.returnValue(stubValue);
|
||||
return { masterService, stubValue, valueServiceSpy };
|
||||
}
|
||||
// #enddocregion no-before-each-setup
|
||||
});
|
||||
|
||||
// #docregion ReversePipe
|
||||
|
||||
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
|
||||
|
||||
// #docregion Lightswitch
|
||||
describe('LightswitchComp', () => {
|
||||
it('#clicked() should toggle #isOn', () => {
|
||||
const comp = new LightswitchComponent();
|
||||
expect(comp.isOn).toBe(false, 'off at first');
|
||||
comp.clicked();
|
||||
expect(comp.isOn).toBe(true, 'on after click');
|
||||
comp.clicked();
|
||||
expect(comp.isOn).toBe(false, 'off after second click');
|
||||
});
|
||||
|
||||
it('#clicked() should set #message to "is on"', () => {
|
||||
const comp = new LightswitchComponent();
|
||||
expect(comp.message).toMatch(/is off/i, 'off at first');
|
||||
comp.clicked();
|
||||
expect(comp.message).toMatch(/is on/i, 'on after clicked');
|
||||
});
|
||||
});
|
||||
// #enddocregion Lightswitch
|
||||
|
||||
});
|
706
aio/content/examples/testing/src/app/demo/demo.testbed.spec.ts
Normal file
706
aio/content/examples/testing/src/app/demo/demo.testbed.spec.ts
Normal file
@ -0,0 +1,706 @@
|
||||
// #docplaster
|
||||
import {
|
||||
DemoModule,
|
||||
BankAccountComponent, BankAccountParentComponent,
|
||||
LightswitchComponent,
|
||||
Child1Component, Child2Component, Child3Component,
|
||||
MasterService,
|
||||
ValueService,
|
||||
ExternalTemplateComponent,
|
||||
InputComponent,
|
||||
IoComponent, IoParentComponent,
|
||||
MyIfComponent, MyIfChildComponent, MyIfParentComponent,
|
||||
NeedsContentComponent, ParentComponent,
|
||||
TestProvidersComponent, TestViewProvidersComponent,
|
||||
ReversePipeComponent, ShellComponent
|
||||
} from './demo';
|
||||
|
||||
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, click } from '../../testing';
|
||||
|
||||
export class NotProvided extends ValueService { /* example below */}
|
||||
beforeEach( addMatchers );
|
||||
|
||||
describe('demo (with TestBed):', () => {
|
||||
|
||||
//////// Service Tests /////////////
|
||||
|
||||
// #docregion ValueService
|
||||
describe('ValueService', () => {
|
||||
|
||||
// #docregion value-service-before-each
|
||||
let service: ValueService;
|
||||
|
||||
// #docregion value-service-inject-before-each
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({ providers: [ValueService] });
|
||||
// #enddocregion value-service-before-each
|
||||
service = TestBed.get(ValueService);
|
||||
// #docregion value-service-before-each
|
||||
});
|
||||
// #enddocregion value-service-before-each, value-service-inject-before-each
|
||||
|
||||
// #docregion value-service-inject-it
|
||||
it('should use ValueService', () => {
|
||||
service = TestBed.get(ValueService);
|
||||
expect(service.getValue()).toBe('real value');
|
||||
});
|
||||
// #enddocregion value-service-inject-it
|
||||
|
||||
it('can inject a default value when service is not provided', () => {
|
||||
// #docregion testbed-get-w-null
|
||||
service = TestBed.get(NotProvided, null); // service is null
|
||||
// #enddocregion testbed-get-w-null
|
||||
});
|
||||
|
||||
it('test should wait for ValueService.getPromiseValue', async(() => {
|
||||
service.getPromiseValue().then(
|
||||
value => expect(value).toBe('promise value')
|
||||
);
|
||||
}));
|
||||
|
||||
it('test should wait for ValueService.getObservableValue', async(() => {
|
||||
service.getObservableValue().subscribe(
|
||||
value => expect(value).toBe('observable value')
|
||||
);
|
||||
}));
|
||||
|
||||
// Must use done. See https://github.com/angular/angular/issues/10127
|
||||
it('test should wait for ValueService.getObservableDelayValue', (done: DoneFn) => {
|
||||
service.getObservableDelayValue().subscribe(value => {
|
||||
expect(value).toBe('observable delay value');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow the use of fakeAsync', fakeAsync(() => {
|
||||
let value: any;
|
||||
service.getPromiseValue().then((val: any) => value = val);
|
||||
tick(); // Trigger JS engine cycle until all promises resolve.
|
||||
expect(value).toBe('promise value');
|
||||
}));
|
||||
});
|
||||
// #enddocregion ValueService
|
||||
|
||||
describe('MasterService', () => {
|
||||
// #docregion master-service-before-each
|
||||
let masterService: MasterService;
|
||||
let valueServiceSpy: jasmine.SpyObj<ValueService>;
|
||||
|
||||
beforeEach(() => {
|
||||
const spy = jasmine.createSpyObj('ValueService', ['getValue']);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
// Provide both the service-to-test and its (spy) dependency
|
||||
providers: [
|
||||
MasterService,
|
||||
{ provide: ValueService, useValue: spy }
|
||||
]
|
||||
});
|
||||
// Inject both the service-to-test and its (spy) dependency
|
||||
masterService = TestBed.get(MasterService);
|
||||
valueServiceSpy = TestBed.get(ValueService);
|
||||
});
|
||||
// #enddocregion master-service-before-each
|
||||
|
||||
// #docregion master-service-it
|
||||
it('#getValue should return stubbed value from a spy', () => {
|
||||
const stubValue = 'stub value';
|
||||
valueServiceSpy.getValue.and.returnValue(stubValue);
|
||||
|
||||
expect(masterService.getValue())
|
||||
.toBe(stubValue, 'service returned stub value');
|
||||
expect(valueServiceSpy.getValue.calls.count())
|
||||
.toBe(1, 'spy method was called once');
|
||||
expect(valueServiceSpy.getValue.calls.mostRecent().returnValue)
|
||||
.toBe(stubValue);
|
||||
});
|
||||
// #enddocregion master-service-it
|
||||
});
|
||||
|
||||
describe('use inject within `it`', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({ providers: [ValueService] });
|
||||
});
|
||||
|
||||
it('should use modified providers',
|
||||
inject([ValueService], (service: ValueService) => {
|
||||
service.setValue('value modified in beforeEach');
|
||||
expect(service.getValue())
|
||||
.toBe('value modified in beforeEach');
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
describe('using async(inject) within beforeEach', () => {
|
||||
let serviceValue: string;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({ providers: [ValueService] });
|
||||
});
|
||||
|
||||
beforeEach(async(inject([ValueService], (service: ValueService) => {
|
||||
service.getPromiseValue().then(value => serviceValue = value);
|
||||
})));
|
||||
|
||||
it('should use asynchronously modified value ... in synchronous test', () => {
|
||||
expect(serviceValue).toBe('promise value');
|
||||
});
|
||||
});
|
||||
|
||||
/////////// Component Tests //////////////////
|
||||
|
||||
describe('TestBed component tests', () => {
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed
|
||||
.configureTestingModule({
|
||||
imports: [DemoModule],
|
||||
})
|
||||
// Compile everything in DemoModule
|
||||
.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];
|
||||
|
||||
click(heroes[0]);
|
||||
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 component', () => {
|
||||
const fixture = TestBed.createComponent(IoParentComponent);
|
||||
const comp = fixture.componentInstance;
|
||||
const heroName = comp.heroes[0].name; // first hero's name
|
||||
|
||||
fixture.detectChanges();
|
||||
const ngForRow = fixture.debugElement.query(By.directive(IoComponent)); // first hero ngForRow
|
||||
|
||||
const hero = ngForRow.context['hero']; // the hero object passed into the row
|
||||
expect(hero.name).toBe(heroName, 'ngRow.context.hero');
|
||||
|
||||
const rowComp = ngForRow.componentInstance;
|
||||
// jasmine.any is an "instance-of-type" test.
|
||||
expect(rowComp).toEqual(jasmine.any(IoComponent), 'component is IoComp');
|
||||
expect(rowComp.hero.name).toBe(heroName, 'component.hero');
|
||||
});
|
||||
|
||||
|
||||
// #docregion ButtonComp
|
||||
it('should support clicking a button', () => {
|
||||
const fixture = TestBed.createComponent(LightswitchComponent);
|
||||
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');
|
||||
|
||||
click(btn);
|
||||
fixture.detectChanges();
|
||||
expect(span.textContent).toMatch(/is on/i, 'after click');
|
||||
});
|
||||
// #enddocregion ButtonComp
|
||||
|
||||
// ngModel is async so we must wait for it with promise-based `whenStable`
|
||||
it('should support entering text in input box (ngModel)', async(() => {
|
||||
const expectedOrigName = 'John';
|
||||
const expectedNewName = 'Sally';
|
||||
|
||||
const fixture = TestBed.createComponent(InputComponent);
|
||||
fixture.detectChanges();
|
||||
|
||||
const comp = fixture.componentInstance;
|
||||
const input = <HTMLInputElement> fixture.debugElement.query(By.css('input')).nativeElement;
|
||||
|
||||
expect(comp.name).toBe(expectedOrigName,
|
||||
`At start name should be ${expectedOrigName} `);
|
||||
|
||||
// wait until ngModel binds comp.name to input box
|
||||
fixture.whenStable().then(() => {
|
||||
expect(input.value).toBe(expectedOrigName,
|
||||
`After ngModel updates input box, input.value should be ${expectedOrigName} `);
|
||||
|
||||
// simulate user entering new name in input
|
||||
input.value = expectedNewName;
|
||||
|
||||
// that change doesn't flow to the component immediately
|
||||
expect(comp.name).toBe(expectedOrigName,
|
||||
`comp.name should still be ${expectedOrigName} after value change, before binding happens`);
|
||||
|
||||
// dispatch a DOM event so that Angular learns of input value change.
|
||||
// then wait while ngModel pushes input.box value to comp.name
|
||||
input.dispatchEvent(newEvent('input'));
|
||||
return fixture.whenStable();
|
||||
})
|
||||
.then(() => {
|
||||
expect(comp.name).toBe(expectedNewName,
|
||||
`After ngModel updates the model, comp.name should be ${expectedNewName} `);
|
||||
});
|
||||
}));
|
||||
|
||||
// fakeAsync version of ngModel input test enables sync test style
|
||||
// synchronous `tick` replaces asynchronous promise-base `whenStable`
|
||||
it('should support entering text in input box (ngModel) - fakeAsync', fakeAsync(() => {
|
||||
const expectedOrigName = 'John';
|
||||
const expectedNewName = 'Sally';
|
||||
|
||||
const fixture = TestBed.createComponent(InputComponent);
|
||||
fixture.detectChanges();
|
||||
|
||||
const comp = fixture.componentInstance;
|
||||
const input = <HTMLInputElement> fixture.debugElement.query(By.css('input')).nativeElement;
|
||||
|
||||
expect(comp.name).toBe(expectedOrigName,
|
||||
`At start name should be ${expectedOrigName} `);
|
||||
|
||||
// wait until ngModel binds comp.name to input box
|
||||
tick();
|
||||
expect(input.value).toBe(expectedOrigName,
|
||||
`After ngModel updates input box, input.value should be ${expectedOrigName} `);
|
||||
|
||||
// simulate user entering new name in input
|
||||
input.value = expectedNewName;
|
||||
|
||||
// that change doesn't flow to the component immediately
|
||||
expect(comp.name).toBe(expectedOrigName,
|
||||
`comp.name should still be ${expectedOrigName} after value change, before binding happens`);
|
||||
|
||||
// dispatch a DOM event so that Angular learns of input value change.
|
||||
// then wait a tick while ngModel pushes input.box value to comp.name
|
||||
input.dispatchEvent(newEvent('input'));
|
||||
tick();
|
||||
expect(comp.name).toBe(expectedNewName,
|
||||
`After ngModel updates the model, comp.name should be ${expectedNewName} `);
|
||||
}));
|
||||
|
||||
// #docregion ReversePipeComp
|
||||
it('ReversePipeComp should reverse the input text', fakeAsync(() => {
|
||||
const inputText = 'the quick brown fox.';
|
||||
const expectedText = '.xof nworb kciuq eht';
|
||||
|
||||
const fixture = TestBed.createComponent(ReversePipeComponent);
|
||||
fixture.detectChanges();
|
||||
|
||||
const comp = fixture.componentInstance;
|
||||
const input = fixture.debugElement.query(By.css('input')).nativeElement as HTMLInputElement;
|
||||
const span = fixture.debugElement.query(By.css('span')).nativeElement as HTMLElement;
|
||||
|
||||
// simulate user entering new name in input
|
||||
input.value = inputText;
|
||||
|
||||
// dispatch a DOM event so that Angular learns of input value change.
|
||||
// then wait a tick while ngModel pushes input.box value to comp.text
|
||||
// and Angular updates the output span
|
||||
input.dispatchEvent(newEvent('input'));
|
||||
tick();
|
||||
fixture.detectChanges();
|
||||
expect(span.textContent).toBe(expectedText, 'output span');
|
||||
expect(comp.text).toBe(inputText, 'component.text');
|
||||
}));
|
||||
// #enddocregion ReversePipeComp
|
||||
|
||||
// Use this technique to find attached directives of any kind
|
||||
it('can examine attached directives and listeners', () => {
|
||||
const fixture = TestBed.createComponent(InputComponent);
|
||||
fixture.detectChanges();
|
||||
|
||||
const inputEl = fixture.debugElement.query(By.css('input'));
|
||||
|
||||
expect(inputEl.providerTokens).toContain(NgModel, 'NgModel directive');
|
||||
|
||||
const ngControl = inputEl.injector.get(NgControl);
|
||||
expect(ngControl).toEqual(jasmine.any(NgControl), 'NgControl directive');
|
||||
|
||||
expect(inputEl.listeners.length).toBeGreaterThan(2, 'several listeners attached');
|
||||
});
|
||||
|
||||
// #docregion dom-attributes
|
||||
it('BankAccountComponent 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(childComp, 'context is the child 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.styles['color']).toBe(comp.color, 'color style');
|
||||
expect(el.styles['width']).toBe(comp.width + 'px', 'width style');
|
||||
// #enddocregion dom-attributes
|
||||
|
||||
// Removed on 12/02/2016 when ceased public discussion of the `Renderer`. Revive in future?
|
||||
// expect(el.properties['customProperty']).toBe(true, 'customProperty');
|
||||
|
||||
// #docregion dom-attributes
|
||||
});
|
||||
// #enddocregion dom-attributes
|
||||
|
||||
|
||||
});
|
||||
|
||||
describe('TestBed component overrides:', () => {
|
||||
|
||||
it('should override ChildComp\'s template', () => {
|
||||
|
||||
const fixture = TestBed.configureTestingModule({
|
||||
declarations: [Child1Component],
|
||||
})
|
||||
.overrideComponent(Child1Component, {
|
||||
set: { template: '<span>Fake</span>' }
|
||||
})
|
||||
.createComponent(Child1Component);
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(fixture).toHaveText('Fake');
|
||||
});
|
||||
|
||||
it('should override TestProvidersComp\'s ValueService provider', () => {
|
||||
const fixture = TestBed.configureTestingModule({
|
||||
declarations: [TestProvidersComponent],
|
||||
})
|
||||
.overrideComponent(TestProvidersComponent, {
|
||||
remove: { providers: [ValueService]},
|
||||
add: { providers: [{ provide: ValueService, useClass: FakeValueService }] },
|
||||
|
||||
// Or replace them all (this component has only one provider)
|
||||
// set: { providers: [{ provide: ValueService, useClass: FakeValueService }] },
|
||||
})
|
||||
.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(ValueService, 'ValueService');
|
||||
});
|
||||
|
||||
it('should override TestViewProvidersComp\'s ValueService viewProvider', () => {
|
||||
const fixture = TestBed.configureTestingModule({
|
||||
declarations: [TestViewProvidersComponent],
|
||||
})
|
||||
.overrideComponent(TestViewProvidersComponent, {
|
||||
// remove: { viewProviders: [ValueService]},
|
||||
// add: { viewProviders: [{ provide: ValueService, useClass: FakeValueService }] },
|
||||
|
||||
// Or replace them all (this component has only one viewProvider)
|
||||
set: { viewProviders: [{ provide: ValueService, useClass: FakeValueService }] },
|
||||
})
|
||||
.createComponent(TestViewProvidersComponent);
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(fixture).toHaveText('injected value: faked value');
|
||||
});
|
||||
|
||||
it('injected provider should not be same as component\'s provider', () => {
|
||||
|
||||
// TestComponent is parent of TestProvidersComponent
|
||||
@Component({ template: '<my-service-comp></my-service-comp>' })
|
||||
class TestComponent {}
|
||||
|
||||
// 3 levels of ValueService provider: module, TestCompomponent, TestProvidersComponent
|
||||
const fixture = TestBed.configureTestingModule({
|
||||
declarations: [TestComponent, TestProvidersComponent],
|
||||
providers: [ValueService]
|
||||
})
|
||||
.overrideComponent(TestComponent, {
|
||||
set: { providers: [{ provide: ValueService, useValue: {} }] }
|
||||
})
|
||||
.overrideComponent(TestProvidersComponent, {
|
||||
set: { providers: [{ provide: ValueService, useClass: FakeValueService }] }
|
||||
})
|
||||
.createComponent(TestComponent);
|
||||
|
||||
let testBedProvider: ValueService;
|
||||
let tcProvider: ValueService;
|
||||
let tpcProvider: FakeValueService;
|
||||
|
||||
// `inject` uses TestBed's injector
|
||||
inject([ValueService], (s: ValueService) => testBedProvider = s)();
|
||||
tcProvider = fixture.debugElement.injector.get(ValueService) as ValueService;
|
||||
tpcProvider = fixture.debugElement.children[0].injector.get(ValueService) as FakeValueService;
|
||||
|
||||
expect(testBedProvider).not.toBe(tcProvider, 'testBed/tc not same providers');
|
||||
expect(testBedProvider).not.toBe(tpcProvider, 'testBed/tpc not same providers');
|
||||
|
||||
expect(testBedProvider instanceof ValueService).toBe(true, 'testBedProvider is ValueService');
|
||||
expect(tcProvider).toEqual({} as ValueService, 'tcProvider is {}');
|
||||
expect(tpcProvider instanceof FakeValueService).toBe(true, 'tpcProvider is FakeValueService');
|
||||
});
|
||||
|
||||
it('can access template local variables as references', () => {
|
||||
const fixture = TestBed.configureTestingModule({
|
||||
declarations: [ShellComponent, NeedsContentComponent, Child1Component, Child2Component, Child3Component],
|
||||
})
|
||||
.overrideComponent(ShellComponent, {
|
||||
set: {
|
||||
selector: 'test-shell',
|
||||
template: `
|
||||
<needs-content #nc>
|
||||
<child-1 #content text="My"></child-1>
|
||||
<child-2 #content text="dog"></child-2>
|
||||
<child-2 text="has"></child-2>
|
||||
<child-3 #content text="fleas"></child-3>
|
||||
<div #content>!</div>
|
||||
</needs-content>
|
||||
`
|
||||
}
|
||||
})
|
||||
.createComponent(ShellComponent);
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
// NeedsContentComp is the child of ShellComp
|
||||
const el = fixture.debugElement.children[0];
|
||||
const comp = el.componentInstance;
|
||||
|
||||
expect(comp.children.toArray().length).toBe(4,
|
||||
'three different child components and an ElementRef with #content');
|
||||
|
||||
expect(el.references['nc']).toBe(comp, '#nc reference to component');
|
||||
|
||||
// #docregion custom-predicate
|
||||
// Filter for DebugElements with a #content reference
|
||||
const contentRefs = el.queryAll( de => de.references['content']);
|
||||
// #enddocregion custom-predicate
|
||||
expect(contentRefs.length).toBe(4, 'elements w/ a #content reference');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('nested (one-deep) component override', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ParentComponent, FakeChildComponent]
|
||||
});
|
||||
});
|
||||
|
||||
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(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ParentComponent, FakeChildWithGrandchildComponent, FakeGrandchildComponent]
|
||||
});
|
||||
});
|
||||
|
||||
it('should use Fake Grandchild component', () => {
|
||||
const fixture = TestBed.createComponent(ParentComponent);
|
||||
fixture.detectChanges();
|
||||
expect(fixture).toHaveText('Parent(Fake Child(Fake Grandchild))');
|
||||
});
|
||||
});
|
||||
|
||||
describe('lifecycle hooks w/ MyIfParentComp', () => {
|
||||
let fixture: ComponentFixture<MyIfParentComponent>;
|
||||
let parent: MyIfParentComponent;
|
||||
let child: MyIfChildComponent;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [FormsModule],
|
||||
declarations: [MyIfChildComponent, MyIfParentComponent]
|
||||
});
|
||||
|
||||
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'));
|
||||
click(btn);
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(child.ngOnDestroyCalled).toBe(true);
|
||||
});
|
||||
|
||||
////// helpers ///
|
||||
/**
|
||||
* Get the MyIfChildComp from parent; fail w/ good message if cannot.
|
||||
*/
|
||||
function getChild() {
|
||||
|
||||
let childDe: DebugElement; // DebugElement that should hold the MyIfChildComp
|
||||
|
||||
// The Hard Way: requires detailed knowledge of the parent template
|
||||
try {
|
||||
childDe = fixture.debugElement.children[4].children[0];
|
||||
} catch (err) { /* we'll report the error */ }
|
||||
|
||||
// DebugElement.queryAll: if we wanted all of many instances:
|
||||
childDe = fixture.debugElement
|
||||
.queryAll(function (de) { return de.componentInstance instanceof MyIfChildComponent; })[0];
|
||||
|
||||
// WE'LL USE THIS APPROACH !
|
||||
// DebugElement.query: find first instance (if any)
|
||||
childDe = fixture.debugElement
|
||||
.query(function (de) { return de.componentInstance instanceof MyIfChildComponent; });
|
||||
|
||||
if (childDe && childDe.componentInstance) {
|
||||
child = childDe.componentInstance;
|
||||
} else {
|
||||
fail('Unable to find MyIfChildComp within MyIfParentComp');
|
||||
}
|
||||
|
||||
return child;
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
////////// Fakes ///////////
|
||||
|
||||
@Component({
|
||||
selector: 'child-1',
|
||||
template: `Fake Child`
|
||||
})
|
||||
class FakeChildComponent { }
|
||||
|
||||
@Component({
|
||||
selector: 'child-1',
|
||||
template: `Fake Child(<grandchild-1></grandchild-1>)`
|
||||
})
|
||||
class FakeChildWithGrandchildComponent { }
|
||||
|
||||
@Component({
|
||||
selector: 'grandchild-1',
|
||||
template: `Fake Grandchild`
|
||||
})
|
||||
class FakeGrandchildComponent { }
|
||||
|
||||
@Injectable()
|
||||
class FakeValueService extends ValueService {
|
||||
value = 'faked value';
|
||||
}
|
@ -6,9 +6,8 @@ import { Component, ContentChildren, Directive, EventEmitter,
|
||||
Pipe, PipeTransform,
|
||||
SimpleChange } from '@angular/core';
|
||||
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import 'rxjs/add/observable/of';
|
||||
import 'rxjs/add/operator/delay';
|
||||
import { of } from 'rxjs/observable/of';
|
||||
import { delay } from 'rxjs/operators';
|
||||
|
||||
////////// The App: Services and Components for the tests. //////////////
|
||||
|
||||
@ -17,37 +16,31 @@ export class Hero {
|
||||
}
|
||||
|
||||
////////// Services ///////////////
|
||||
// #docregion FancyService
|
||||
// #docregion ValueService
|
||||
@Injectable()
|
||||
export class FancyService {
|
||||
export class ValueService {
|
||||
protected value = 'real value';
|
||||
|
||||
getValue() { return this.value; }
|
||||
setValue(value: string) { this.value = value; }
|
||||
|
||||
getAsyncValue() { return Promise.resolve('async value'); }
|
||||
getObservableValue() { return of('observable value'); }
|
||||
|
||||
getObservableValue() { return Observable.of('observable value'); }
|
||||
|
||||
getTimeoutValue() {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => { resolve('timeout value'); }, 10);
|
||||
});
|
||||
}
|
||||
getPromiseValue() { return Promise.resolve('promise value'); }
|
||||
|
||||
getObservableDelayValue() {
|
||||
return Observable.of('observable delay value').delay(10);
|
||||
return of('observable delay value').pipe(delay(10));
|
||||
}
|
||||
}
|
||||
// #enddocregion FancyService
|
||||
// #enddocregion ValueService
|
||||
|
||||
// #docregion DependentService
|
||||
// #docregion MasterService
|
||||
@Injectable()
|
||||
export class DependentService {
|
||||
constructor(private dependentService: FancyService) { }
|
||||
getValue() { return this.dependentService.getValue(); }
|
||||
export class MasterService {
|
||||
constructor(private masterService: ValueService) { }
|
||||
getValue() { return this.masterService.getValue(); }
|
||||
}
|
||||
// #enddocregion DependentService
|
||||
// #enddocregion MasterService
|
||||
|
||||
/////////// Pipe ////////////////
|
||||
/*
|
||||
@ -102,19 +95,19 @@ export class BankAccountParentComponent {
|
||||
isClosed = true;
|
||||
}
|
||||
|
||||
// #docregion ButtonComp
|
||||
// #docregion LightswitchComp
|
||||
@Component({
|
||||
selector: 'button-comp',
|
||||
selector: 'lightswitch-comp',
|
||||
template: `
|
||||
<button (click)="clicked()">Click me!</button>
|
||||
<span>{{message}}</span>`
|
||||
})
|
||||
export class ButtonComponent {
|
||||
export class LightswitchComponent {
|
||||
isOn = false;
|
||||
clicked() { this.isOn = !this.isOn; }
|
||||
get message() { return `The light is ${this.isOn ? 'On' : 'Off'}`; }
|
||||
}
|
||||
// #enddocregion ButtonComp
|
||||
// #enddocregion LightswitchComp
|
||||
|
||||
@Component({
|
||||
selector: 'child-1',
|
||||
@ -231,31 +224,31 @@ export class MyIfComponent {
|
||||
|
||||
@Component({
|
||||
selector: 'my-service-comp',
|
||||
template: `injected value: {{fancyService.value}}`,
|
||||
providers: [FancyService]
|
||||
template: `injected value: {{valueService.value}}`,
|
||||
providers: [ValueService]
|
||||
})
|
||||
export class TestProvidersComponent {
|
||||
constructor(public fancyService: FancyService) {}
|
||||
constructor(public valueService: ValueService) {}
|
||||
}
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'my-service-comp',
|
||||
template: `injected value: {{fancyService.value}}`,
|
||||
viewProviders: [FancyService]
|
||||
template: `injected value: {{valueService.value}}`,
|
||||
viewProviders: [ValueService]
|
||||
})
|
||||
export class TestViewProvidersComponent {
|
||||
constructor(public fancyService: FancyService) {}
|
||||
constructor(public valueService: ValueService) {}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'external-template-comp',
|
||||
templateUrl: './bag-external-template.html'
|
||||
templateUrl: './demo-external-template.html'
|
||||
})
|
||||
export class ExternalTemplateComponent implements OnInit {
|
||||
serviceValue: string;
|
||||
|
||||
constructor(@Optional() private service: FancyService) { }
|
||||
constructor(@Optional() private service: ValueService) { }
|
||||
|
||||
ngOnInit() {
|
||||
if (this.service) { this.serviceValue = this.service.getValue(); }
|
||||
@ -376,9 +369,9 @@ export class ReversePipeComponent {
|
||||
export class ShellComponent { }
|
||||
|
||||
@Component({
|
||||
selector: 'bag-comp',
|
||||
selector: 'demo-comp',
|
||||
template: `
|
||||
<h1>Specs Bag</h1>
|
||||
<h1>Specs Demo</h1>
|
||||
<my-if-parent-comp></my-if-parent-comp>
|
||||
<hr>
|
||||
<h3>Input/Output Component</h3>
|
||||
@ -397,7 +390,7 @@ export class ShellComponent { }
|
||||
<input-value-comp></input-value-comp>
|
||||
<hr>
|
||||
<h3>Button Component</h3>
|
||||
<button-comp></button-comp>
|
||||
<lightswitch-comp></lightswitch-comp>
|
||||
<hr>
|
||||
<h3>Needs Content</h3>
|
||||
<needs-content #nc>
|
||||
@ -409,13 +402,13 @@ export class ShellComponent { }
|
||||
</needs-content>
|
||||
`
|
||||
})
|
||||
export class BagComponent { }
|
||||
export class DemoComponent { }
|
||||
//////// Aggregations ////////////
|
||||
|
||||
export const bagDeclarations = [
|
||||
BagComponent,
|
||||
export const demoDeclarations = [
|
||||
DemoComponent,
|
||||
BankAccountComponent, BankAccountParentComponent,
|
||||
ButtonComponent,
|
||||
LightswitchComponent,
|
||||
Child1Component, Child2Component, Child3Component,
|
||||
ExternalTemplateComponent, InnerCompWithExternalTemplateComponent,
|
||||
InputComponent,
|
||||
@ -427,7 +420,7 @@ export const bagDeclarations = [
|
||||
ReversePipe, ReversePipeComponent, ShellComponent
|
||||
];
|
||||
|
||||
export const bagProviders = [DependentService, FancyService];
|
||||
export const demoProviders = [MasterService, ValueService];
|
||||
|
||||
////////////////////
|
||||
////////////
|
||||
@ -437,10 +430,10 @@ import { FormsModule } from '@angular/forms';
|
||||
|
||||
@NgModule({
|
||||
imports: [BrowserModule, FormsModule],
|
||||
declarations: bagDeclarations,
|
||||
providers: bagProviders,
|
||||
entryComponents: [BagComponent],
|
||||
bootstrap: [BagComponent]
|
||||
declarations: demoDeclarations,
|
||||
providers: demoProviders,
|
||||
entryComponents: [DemoComponent],
|
||||
bootstrap: [DemoComponent]
|
||||
})
|
||||
export class BagModule { }
|
||||
export class DemoModule { }
|
||||
|
15
aio/content/examples/testing/src/app/dummy.module.ts
Normal file
15
aio/content/examples/testing/src/app/dummy.module.ts
Normal file
@ -0,0 +1,15 @@
|
||||
// These unused NgModules keep the Angular Language Service happy.
|
||||
// The AppModule registers the final versions of these components
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { AppComponent as app_initial } from './app-initial.component';
|
||||
@NgModule({ declarations: [ app_initial ] })
|
||||
export class AppModuleInitial {}
|
||||
|
||||
import { BannerComponent as bc_initial } from './banner/banner-initial.component';
|
||||
@NgModule({ declarations: [ bc_initial ] })
|
||||
export class BannerModuleInitial {}
|
||||
|
||||
import { BannerComponent as bc_external } from './banner/banner-external.component';
|
||||
@NgModule({ declarations: [ bc_external ] })
|
||||
export class BannerModuleExternal {}
|
@ -1,7 +1,7 @@
|
||||
import { HeroDetailComponent } from './hero-detail.component';
|
||||
import { Hero } from '../model';
|
||||
import { asyncData, ActivatedRouteStub } from '../../testing';
|
||||
|
||||
import { ActivatedRouteStub } from '../../testing';
|
||||
import { HeroDetailComponent } from './hero-detail.component';
|
||||
import { Hero } from '../model/hero';
|
||||
|
||||
////////// Tests ////////////////////
|
||||
|
||||
@ -12,22 +12,21 @@ describe('HeroDetailComponent - no TestBed', () => {
|
||||
let hds: any;
|
||||
let router: any;
|
||||
|
||||
beforeEach((done: any) => {
|
||||
expectedHero = new Hero(42, 'Bubba');
|
||||
activatedRoute = new ActivatedRouteStub();
|
||||
activatedRoute.testParamMap = { id: expectedHero.id };
|
||||
|
||||
beforeEach((done: DoneFn) => {
|
||||
expectedHero = {id: 42, name: 'Bubba' };
|
||||
const activatedRoute = new ActivatedRouteStub({ 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));
|
||||
hds.getHero.and.returnValue(asyncData(expectedHero));
|
||||
hds.saveHero.and.returnValue(asyncData(expectedHero));
|
||||
|
||||
comp = new HeroDetailComponent(hds, <any> activatedRoute, router);
|
||||
comp.ngOnInit();
|
||||
|
||||
// OnInit calls HDS.getHero; wait for it to get the fake hero
|
||||
hds.getHero.calls.first().returnValue.then(done);
|
||||
hds.getHero.calls.first().returnValue.subscribe(done);
|
||||
|
||||
});
|
||||
|
||||
it('should expose the hero retrieved from the service', () => {
|
||||
@ -45,11 +44,11 @@ describe('HeroDetailComponent - no TestBed', () => {
|
||||
expect(router.navigate.calls.any()).toBe(false, 'router.navigate not called yet');
|
||||
});
|
||||
|
||||
it('should navigate when click save resolves', (done: any) => {
|
||||
it('should navigate when click save resolves', (done: DoneFn) => {
|
||||
comp.save();
|
||||
// waits for async save to complete before navigating
|
||||
hds.saveHero.calls.first().returnValue
|
||||
.then(() => {
|
||||
.subscribe(() => {
|
||||
expect(router.navigate.calls.any()).toBe(true, 'router.navigate called');
|
||||
done();
|
||||
});
|
||||
|
@ -3,21 +3,20 @@ import {
|
||||
async, ComponentFixture, fakeAsync, inject, TestBed, tick
|
||||
} from '@angular/core/testing';
|
||||
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { DebugElement } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import {
|
||||
ActivatedRoute, ActivatedRouteStub, click, newEvent, Router, RouterStub
|
||||
ActivatedRoute, ActivatedRouteStub, asyncData, click, newEvent
|
||||
} from '../../testing';
|
||||
|
||||
import { Hero } from '../model';
|
||||
import { Hero } from '../model/hero';
|
||||
import { HeroDetailComponent } from './hero-detail.component';
|
||||
import { HeroDetailService } from './hero-detail.service';
|
||||
import { HeroModule } from './hero.module';
|
||||
|
||||
////// Testing Vars //////
|
||||
let activatedRoute: ActivatedRouteStub;
|
||||
let comp: HeroDetailComponent;
|
||||
let component: HeroDetailComponent;
|
||||
let fixture: ComponentFixture<HeroDetailComponent>;
|
||||
let page: Page;
|
||||
|
||||
@ -32,36 +31,38 @@ describe('HeroDetailComponent', () => {
|
||||
describe('with SharedModule setup', sharedModuleSetup);
|
||||
});
|
||||
|
||||
////////////////////
|
||||
///////////////////
|
||||
|
||||
function overrideSetup() {
|
||||
// #docregion hds-spy
|
||||
class HeroDetailServiceSpy {
|
||||
testHero = new Hero(42, 'Test Hero');
|
||||
testHero: Hero = {id: 42, name: 'Test Hero' };
|
||||
|
||||
/* emit cloned test hero */
|
||||
getHero = jasmine.createSpy('getHero').and.callFake(
|
||||
() => Promise
|
||||
.resolve(true)
|
||||
.then(() => Object.assign({}, this.testHero))
|
||||
() => asyncData(Object.assign({}, this.testHero))
|
||||
);
|
||||
|
||||
/* emit clone of test hero, with changes merged in */
|
||||
saveHero = jasmine.createSpy('saveHero').and.callFake(
|
||||
(hero: Hero) => Promise
|
||||
.resolve(true)
|
||||
.then(() => Object.assign(this.testHero, hero))
|
||||
(hero: Hero) => asyncData(Object.assign(this.testHero, hero))
|
||||
);
|
||||
}
|
||||
|
||||
// #enddocregion hds-spy
|
||||
|
||||
// the `id` value is irrelevant because ignored by service stub
|
||||
beforeEach(() => activatedRoute.testParamMap = { id: 99999 } );
|
||||
beforeEach(() => activatedRoute.setParamMap({ id: 99999 }));
|
||||
|
||||
// #docregion setup-override
|
||||
beforeEach( async(() => {
|
||||
beforeEach(async(() => {
|
||||
const routerSpy = createRouterSpy();
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [ HeroModule ],
|
||||
providers: [
|
||||
{ provide: ActivatedRoute, useValue: activatedRoute },
|
||||
{ provide: Router, useClass: RouterStub},
|
||||
{ provide: Router, useValue: routerSpy},
|
||||
// #enddocregion setup-override
|
||||
// HeroDetailService at this level is IRRELEVANT!
|
||||
{ provide: HeroDetailService, useValue: {} }
|
||||
@ -87,7 +88,7 @@ function overrideSetup() {
|
||||
// #docregion override-tests
|
||||
let hdsSpy: HeroDetailServiceSpy;
|
||||
|
||||
beforeEach( async(() => {
|
||||
beforeEach(async(() => {
|
||||
createComponent();
|
||||
// get the component's injected HeroDetailServiceSpy
|
||||
hdsSpy = fixture.debugElement.injector.get(HeroDetailService) as any;
|
||||
@ -108,7 +109,7 @@ function overrideSetup() {
|
||||
page.nameInput.value = newName;
|
||||
page.nameInput.dispatchEvent(newEvent('input')); // tell Angular
|
||||
|
||||
expect(comp.hero.name).toBe(newName, 'component hero has new name');
|
||||
expect(component.hero.name).toBe(newName, 'component hero has new name');
|
||||
expect(hdsSpy.testHero.name).toBe(origName, 'service hero unchanged before save');
|
||||
|
||||
click(page.saveBtn);
|
||||
@ -116,36 +117,40 @@ function overrideSetup() {
|
||||
|
||||
tick(); // wait for async save to complete
|
||||
expect(hdsSpy.testHero.name).toBe(newName, 'service hero has new name after save');
|
||||
expect(page.navSpy.calls.any()).toBe(true, 'router.navigate called');
|
||||
expect(page.navigateSpy.calls.any()).toBe(true, 'router.navigate called');
|
||||
}));
|
||||
// #enddocregion override-tests
|
||||
|
||||
it('fixture injected service is not the component injected service',
|
||||
inject([HeroDetailService], (service: HeroDetailService) => {
|
||||
// inject gets the service from the fixture
|
||||
inject([HeroDetailService], (fixtureService: HeroDetailService) => {
|
||||
|
||||
expect(service).toEqual(<any> {}, 'service injected from fixture');
|
||||
expect(hdsSpy).toBeTruthy('service injected into component');
|
||||
// use `fixture.debugElement.injector` to get service from component
|
||||
const componentService = fixture.debugElement.injector.get(HeroDetailService);
|
||||
|
||||
expect(fixtureService).not.toBe(componentService, 'service injected from fixture');
|
||||
}));
|
||||
}
|
||||
|
||||
////////////////////
|
||||
import { HEROES, FakeHeroService } from '../model/testing';
|
||||
import { HeroService } from '../model';
|
||||
import { getTestHeroes, TestHeroService, HeroService } from '../model/testing/test-hero.service';
|
||||
|
||||
const firstHero = HEROES[0];
|
||||
const firstHero = getTestHeroes()[0];
|
||||
|
||||
function heroModuleSetup() {
|
||||
// #docregion setup-hero-module
|
||||
beforeEach( async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
beforeEach(async(() => {
|
||||
const routerSpy = createRouterSpy();
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [ HeroModule ],
|
||||
// #enddocregion setup-hero-module
|
||||
// declarations: [ HeroDetailComponent ], // NO! DOUBLE DECLARATION
|
||||
// #docregion setup-hero-module
|
||||
providers: [
|
||||
{ provide: ActivatedRoute, useValue: activatedRoute },
|
||||
{ provide: HeroService, useClass: FakeHeroService },
|
||||
{ provide: Router, useClass: RouterStub},
|
||||
{ provide: HeroService, useClass: TestHeroService },
|
||||
{ provide: Router, useValue: routerSpy},
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
@ -156,9 +161,9 @@ function heroModuleSetup() {
|
||||
describe('when navigate to existing hero', () => {
|
||||
let expectedHero: Hero;
|
||||
|
||||
beforeEach( async(() => {
|
||||
beforeEach(async(() => {
|
||||
expectedHero = firstHero;
|
||||
activatedRoute.testParamMap = { id: expectedHero.id };
|
||||
activatedRoute.setParamMap({ id: expectedHero.id });
|
||||
createComponent();
|
||||
}));
|
||||
|
||||
@ -170,7 +175,7 @@ function heroModuleSetup() {
|
||||
|
||||
it('should navigate when click cancel', () => {
|
||||
click(page.cancelBtn);
|
||||
expect(page.navSpy.calls.any()).toBe(true, 'router.navigate called');
|
||||
expect(page.navigateSpy.calls.any()).toBe(true, 'router.navigate called');
|
||||
});
|
||||
|
||||
it('should save when click save but not navigate immediately', () => {
|
||||
@ -181,30 +186,31 @@ function heroModuleSetup() {
|
||||
|
||||
click(page.saveBtn);
|
||||
expect(saveSpy.calls.any()).toBe(true, 'HeroDetailService.save called');
|
||||
expect(page.navSpy.calls.any()).toBe(false, 'router.navigate not called');
|
||||
expect(page.navigateSpy.calls.any()).toBe(false, 'router.navigate not called');
|
||||
});
|
||||
|
||||
it('should navigate when click save and save resolves', fakeAsync(() => {
|
||||
click(page.saveBtn);
|
||||
tick(); // wait for async save to complete
|
||||
expect(page.navSpy.calls.any()).toBe(true, 'router.navigate called');
|
||||
expect(page.navigateSpy.calls.any()).toBe(true, 'router.navigate called');
|
||||
}));
|
||||
|
||||
// #docregion title-case-pipe
|
||||
it('should convert hero name to Title Case', () => {
|
||||
const inputName = 'quick BROWN fox';
|
||||
const titleCaseName = 'Quick Brown Fox';
|
||||
const { nameInput, nameDisplay } = page;
|
||||
|
||||
// simulate user entering new name into the input box
|
||||
page.nameInput.value = inputName;
|
||||
nameInput.value = inputName;
|
||||
|
||||
// dispatch a DOM event so that Angular learns of input value change.
|
||||
page.nameInput.dispatchEvent(newEvent('input'));
|
||||
nameInput.dispatchEvent(newEvent('input'));
|
||||
|
||||
// Tell Angular to update the output span through the title pipe
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(page.nameDisplay.textContent).toBe(titleCaseName);
|
||||
expect(nameDisplay.textContent).toBe(titleCaseName);
|
||||
});
|
||||
// #enddocregion title-case-pipe
|
||||
// #enddocregion selected-tests
|
||||
@ -214,10 +220,10 @@ function heroModuleSetup() {
|
||||
|
||||
// #docregion route-no-id
|
||||
describe('when navigate with no hero id', () => {
|
||||
beforeEach( async( createComponent ));
|
||||
beforeEach(async( createComponent ));
|
||||
|
||||
it('should have hero.id === 0', () => {
|
||||
expect(comp.hero.id).toBe(0);
|
||||
expect(component.hero.id).toBe(0);
|
||||
});
|
||||
|
||||
it('should display empty hero name', () => {
|
||||
@ -228,14 +234,14 @@ function heroModuleSetup() {
|
||||
|
||||
// #docregion route-bad-id
|
||||
describe('when navigate to non-existent hero id', () => {
|
||||
beforeEach( async(() => {
|
||||
activatedRoute.testParamMap = { id: 99999 };
|
||||
beforeEach(async(() => {
|
||||
activatedRoute.setParamMap({ 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');
|
||||
expect(page.gotoListSpy.calls.any()).toBe(true, 'comp.gotoList called');
|
||||
expect(page.navigateSpy.calls.any()).toBe(true, 'router.navigate called');
|
||||
});
|
||||
});
|
||||
// #enddocregion route-bad-id
|
||||
@ -263,23 +269,25 @@ import { TitleCasePipe } from '../shared/title-case.pipe';
|
||||
|
||||
function formsModuleSetup() {
|
||||
// #docregion setup-forms-module
|
||||
beforeEach( async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
beforeEach(async(() => {
|
||||
const routerSpy = createRouterSpy();
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [ FormsModule ],
|
||||
declarations: [ HeroDetailComponent, TitleCasePipe ],
|
||||
providers: [
|
||||
{ provide: ActivatedRoute, useValue: activatedRoute },
|
||||
{ provide: HeroService, useClass: FakeHeroService },
|
||||
{ provide: Router, useClass: RouterStub},
|
||||
{ provide: HeroService, useClass: TestHeroService },
|
||||
{ provide: Router, useValue: routerSpy},
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
// #enddocregion setup-forms-module
|
||||
|
||||
it('should display 1st hero\'s name', fakeAsync(() => {
|
||||
it('should display 1st hero\'s name', async(() => {
|
||||
const expectedHero = firstHero;
|
||||
activatedRoute.testParamMap = { id: expectedHero.id };
|
||||
activatedRoute.setParamMap({ id: expectedHero.id });
|
||||
createComponent().then(() => {
|
||||
expect(page.nameDisplay.textContent).toBe(expectedHero.name);
|
||||
});
|
||||
@ -291,23 +299,25 @@ import { SharedModule } from '../shared/shared.module';
|
||||
|
||||
function sharedModuleSetup() {
|
||||
// #docregion setup-shared-module
|
||||
beforeEach( async(() => {
|
||||
beforeEach(async(() => {
|
||||
const routerSpy = createRouterSpy();
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [ SharedModule ],
|
||||
declarations: [ HeroDetailComponent ],
|
||||
providers: [
|
||||
{ provide: ActivatedRoute, useValue: activatedRoute },
|
||||
{ provide: HeroService, useClass: FakeHeroService },
|
||||
{ provide: Router, useClass: RouterStub},
|
||||
{ provide: HeroService, useClass: TestHeroService },
|
||||
{ provide: Router, useValue: routerSpy},
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
// #enddocregion setup-shared-module
|
||||
|
||||
it('should display 1st hero\'s name', fakeAsync(() => {
|
||||
it('should display 1st hero\'s name', async(() => {
|
||||
const expectedHero = firstHero;
|
||||
activatedRoute.testParamMap = { id: expectedHero.id };
|
||||
activatedRoute.setParamMap({ id: expectedHero.id });
|
||||
createComponent().then(() => {
|
||||
expect(page.nameDisplay.textContent).toBe(expectedHero.name);
|
||||
});
|
||||
@ -320,45 +330,51 @@ function sharedModuleSetup() {
|
||||
/** Create the HeroDetailComponent, initialize it, set test variables */
|
||||
function createComponent() {
|
||||
fixture = TestBed.createComponent(HeroDetailComponent);
|
||||
comp = fixture.componentInstance;
|
||||
page = new Page();
|
||||
component = fixture.componentInstance;
|
||||
page = new Page(fixture);
|
||||
|
||||
// 1st change detection triggers ngOnInit which gets a hero
|
||||
fixture.detectChanges();
|
||||
return fixture.whenStable().then(() => {
|
||||
// 2nd change detection displays the async-fetched hero
|
||||
fixture.detectChanges();
|
||||
page.addPageElements();
|
||||
});
|
||||
}
|
||||
// #enddocregion create-component
|
||||
|
||||
// #docregion page
|
||||
class Page {
|
||||
gotoSpy: jasmine.Spy;
|
||||
navSpy: jasmine.Spy;
|
||||
// getter properties wait to query the DOM until called.
|
||||
get buttons() { return this.queryAll<HTMLButtonElement>('button'); }
|
||||
get saveBtn() { return this.buttons[0]; }
|
||||
get cancelBtn() { return this.buttons[1]; }
|
||||
get nameDisplay() { return this.query<HTMLElement>('span'); }
|
||||
get nameInput() { return this.query<HTMLInputElement>('input'); }
|
||||
|
||||
saveBtn: DebugElement;
|
||||
cancelBtn: DebugElement;
|
||||
nameDisplay: HTMLElement;
|
||||
nameInput: HTMLInputElement;
|
||||
gotoListSpy: jasmine.Spy;
|
||||
navigateSpy: jasmine.Spy;
|
||||
|
||||
constructor() {
|
||||
const router = TestBed.get(Router); // get router from root injector
|
||||
this.gotoSpy = spyOn(comp, 'gotoList').and.callThrough();
|
||||
this.navSpy = spyOn(router, 'navigate');
|
||||
constructor(fixture: ComponentFixture<HeroDetailComponent>) {
|
||||
// get the navigate spy from the injected router spy object
|
||||
const routerSpy = <any> fixture.debugElement.injector.get(Router);
|
||||
this.navigateSpy = routerSpy.navigate;
|
||||
|
||||
// spy on component's `gotoList()` method
|
||||
const component = fixture.componentInstance;
|
||||
this.gotoListSpy = spyOn(component, 'gotoList').and.callThrough();
|
||||
}
|
||||
|
||||
/** Add page elements after hero arrives */
|
||||
addPageElements() {
|
||||
if (comp.hero) {
|
||||
// have a hero so these elements are now in the DOM
|
||||
const 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;
|
||||
}
|
||||
//// query helpers ////
|
||||
private query<T>(selector: string): T {
|
||||
return fixture.nativeElement.querySelector(selector);
|
||||
}
|
||||
|
||||
private queryAll<T>(selector: string): T[] {
|
||||
return fixture.nativeElement.querySelectorAll(selector);
|
||||
}
|
||||
}
|
||||
// #enddocregion page
|
||||
|
||||
function createRouterSpy() {
|
||||
return jasmine.createSpyObj('Router', ['navigate']);
|
||||
}
|
||||
|
@ -2,7 +2,6 @@
|
||||
// #docplaster
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import 'rxjs/add/operator/map';
|
||||
|
||||
import { Hero } from '../model/hero';
|
||||
import { HeroDetailService } from './hero-detail.service';
|
||||
@ -29,18 +28,18 @@ export class HeroDetailComponent implements OnInit {
|
||||
// #docregion ng-on-init
|
||||
ngOnInit(): void {
|
||||
// get hero when `id` param changes
|
||||
this.route.paramMap.subscribe(p => this.getHero(p.has('id') && p.get('id')));
|
||||
this.route.paramMap.subscribe(pmap => this.getHero(pmap.get('id')));
|
||||
}
|
||||
// #enddocregion ng-on-init
|
||||
|
||||
private getHero(id: string): void {
|
||||
// when no id or id===0, create new hero
|
||||
// when no id or id===0, create new blank hero
|
||||
if (!id) {
|
||||
this.hero = new Hero();
|
||||
this.hero = { id: 0, name: '' } as Hero;
|
||||
return;
|
||||
}
|
||||
|
||||
this.heroDetailService.getHero(id).then(hero => {
|
||||
this.heroDetailService.getHero(id).subscribe(hero => {
|
||||
if (hero) {
|
||||
this.hero = hero;
|
||||
} else {
|
||||
@ -50,7 +49,7 @@ export class HeroDetailComponent implements OnInit {
|
||||
}
|
||||
|
||||
save(): void {
|
||||
this.heroDetailService.saveHero(this.hero).then(() => this.gotoList());
|
||||
this.heroDetailService.saveHero(this.hero).subscribe(() => this.gotoList());
|
||||
}
|
||||
|
||||
cancel() { this.gotoList(); }
|
||||
|
@ -1,5 +1,8 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
import { Hero } from '../model/hero';
|
||||
import { HeroService } from '../model/hero.service';
|
||||
|
||||
@ -10,13 +13,15 @@ export class HeroDetailService {
|
||||
// #enddocregion prototype
|
||||
|
||||
// Returns a clone which caller may modify safely
|
||||
getHero(id: number | string): Promise<Hero> {
|
||||
getHero(id: number | string): Observable<Hero> {
|
||||
if (typeof id === 'string') {
|
||||
id = parseInt(id as string, 10);
|
||||
}
|
||||
return this.heroService.getHero(id).then(hero => {
|
||||
return hero ? Object.assign({}, hero) : null; // clone or null
|
||||
});
|
||||
return this.heroService.getHero(id).pipe(
|
||||
map(hero => {
|
||||
return hero ? Object.assign({}, hero) : null; // clone or null
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
saveHero(hero: Hero) {
|
||||
|
@ -6,7 +6,7 @@
|
||||
margin: 0 0 2em 0;
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
width: 10em;
|
||||
width: 15em;
|
||||
}
|
||||
.heroes li {
|
||||
cursor: pointer;
|
||||
|
@ -4,15 +4,18 @@ import { async, ComponentFixture, fakeAsync, TestBed, tick
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { DebugElement } from '@angular/core';
|
||||
|
||||
import { addMatchers, newEvent, Router, RouterStub
|
||||
} from '../../testing';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { HEROES, FakeHeroService } from '../model/testing';
|
||||
import { addMatchers, newEvent } from '../../testing';
|
||||
|
||||
import { getTestHeroes, TestHeroService } from '../model/testing/test-hero.service';
|
||||
|
||||
import { HeroModule } from './hero.module';
|
||||
import { HeroListComponent } from './hero-list.component';
|
||||
import { HighlightDirective } from '../shared/highlight.directive';
|
||||
import { HeroService } from '../model';
|
||||
import { HeroService } from '../model/hero.service';
|
||||
|
||||
const HEROES = getTestHeroes();
|
||||
|
||||
let comp: HeroListComponent;
|
||||
let fixture: ComponentFixture<HeroListComponent>;
|
||||
@ -22,13 +25,15 @@ let page: Page;
|
||||
|
||||
describe('HeroListComponent', () => {
|
||||
|
||||
beforeEach( async(() => {
|
||||
beforeEach(async(() => {
|
||||
addMatchers();
|
||||
const routerSpy = jasmine.createSpyObj('Router', ['navigate']);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [HeroModule],
|
||||
providers: [
|
||||
{ provide: HeroService, useClass: FakeHeroService },
|
||||
{ provide: Router, useClass: RouterStub}
|
||||
{ provide: HeroService, useClass: TestHeroService },
|
||||
{ provide: Router, useValue: routerSpy}
|
||||
]
|
||||
})
|
||||
.compileComponents()
|
||||
@ -125,15 +130,14 @@ class Page {
|
||||
navSpy: jasmine.Spy;
|
||||
|
||||
constructor() {
|
||||
this.heroRows = fixture.debugElement.queryAll(By.css('li')).map(de => de.nativeElement);
|
||||
const heroRowNodes = fixture.nativeElement.querySelectorAll('li');
|
||||
this.heroRows = Array.from(heroRowNodes);
|
||||
|
||||
// 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');
|
||||
// Get the component's injected router navigation spy
|
||||
const routerSpy = fixture.debugElement.injector.get(Router);
|
||||
this.navSpy = routerSpy.navigate as jasmine.Spy;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
import { Hero } from '../model/hero';
|
||||
import { HeroService } from '../model/hero.service';
|
||||
|
||||
@ -10,7 +12,7 @@ import { HeroService } from '../model/hero.service';
|
||||
styleUrls: [ './hero-list.component.css' ]
|
||||
})
|
||||
export class HeroListComponent implements OnInit {
|
||||
heroes: Promise<Hero[]>;
|
||||
heroes: Observable<Hero[]>;
|
||||
selectedHero: Hero;
|
||||
|
||||
constructor(
|
||||
|
@ -0,0 +1,26 @@
|
||||
// #docregion , init
|
||||
import { InMemoryDbService } from 'angular-in-memory-web-api';
|
||||
import { QUOTES } from './twain/twain.data';
|
||||
|
||||
// Adjust to reduce number of quotes
|
||||
const maxQuotes = Infinity; // 0;
|
||||
|
||||
/** Create in-memory database of heroes and quotes */
|
||||
export class InMemoryDataService implements InMemoryDbService {
|
||||
createDb() {
|
||||
const heroes = [
|
||||
{ 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' }
|
||||
];
|
||||
|
||||
return { heroes, quotes: QUOTES.slice(0, maxQuotes) };
|
||||
}
|
||||
}
|
215
aio/content/examples/testing/src/app/model/hero.service.spec.ts
Normal file
215
aio/content/examples/testing/src/app/model/hero.service.spec.ts
Normal file
@ -0,0 +1,215 @@
|
||||
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
|
||||
|
||||
// Other imports
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { HttpClient, HttpResponse, HttpErrorResponse } from '@angular/common/http';
|
||||
|
||||
import { asyncData, asyncError } from '../../testing/async-observable-helpers';
|
||||
|
||||
import { Hero } from './hero';
|
||||
import { HeroService } from './hero.service';
|
||||
|
||||
describe ('HeroesService (with spies)', () => {
|
||||
// #docregion test-with-spies
|
||||
let httpClientSpy: { get: jasmine.Spy };
|
||||
let heroService: HeroService;
|
||||
|
||||
beforeEach(() => {
|
||||
// Todo: spy on other methods too
|
||||
httpClientSpy = jasmine.createSpyObj('HttpClient', ['get']);
|
||||
heroService = new HeroService(<any> httpClientSpy);
|
||||
});
|
||||
|
||||
it('should return expected heroes (HttpClient called once)', () => {
|
||||
const expectedHeroes: Hero[] =
|
||||
[{ id: 1, name: 'A' }, { id: 2, name: 'B' }];
|
||||
|
||||
httpClientSpy.get.and.returnValue(asyncData(expectedHeroes));
|
||||
|
||||
heroService.getHeroes().subscribe(
|
||||
heroes => expect(heroes).toEqual(expectedHeroes, 'expected heroes'),
|
||||
fail
|
||||
);
|
||||
expect(httpClientSpy.get.calls.count()).toBe(1, 'one call');
|
||||
});
|
||||
|
||||
it('should return an error when the server returns a 404', () => {
|
||||
const errorResponse = new HttpErrorResponse({
|
||||
error: 'test 404 error',
|
||||
status: 404, statusText: 'Not Found'
|
||||
});
|
||||
|
||||
httpClientSpy.get.and.returnValue(asyncError(errorResponse));
|
||||
|
||||
heroService.getHeroes().subscribe(
|
||||
heroes => fail('expected an error, not heroes'),
|
||||
error => expect(error.message).toContain('test 404 error')
|
||||
);
|
||||
});
|
||||
// #enddocregion test-with-spies
|
||||
|
||||
});
|
||||
|
||||
describe('HeroesService (with mocks)', () => {
|
||||
let httpClient: HttpClient;
|
||||
let httpTestingController: HttpTestingController;
|
||||
let heroService: HeroService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
// Import the HttpClient mocking services
|
||||
imports: [ HttpClientTestingModule ],
|
||||
// Provide the service-under-test
|
||||
providers: [ HeroService ]
|
||||
});
|
||||
|
||||
// Inject the http, test controller, and service-under-test
|
||||
// as they will be referenced by each test.
|
||||
httpClient = TestBed.get(HttpClient);
|
||||
httpTestingController = TestBed.get(HttpTestingController);
|
||||
heroService = TestBed.get(HeroService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// After every test, assert that there are no more pending requests.
|
||||
httpTestingController.verify();
|
||||
});
|
||||
|
||||
/// HeroService method tests begin ///
|
||||
describe('#getHeroes', () => {
|
||||
let expectedHeroes: Hero[];
|
||||
|
||||
beforeEach(() => {
|
||||
heroService = TestBed.get(HeroService);
|
||||
expectedHeroes = [
|
||||
{ id: 1, name: 'A' },
|
||||
{ id: 2, name: 'B' },
|
||||
] as Hero[];
|
||||
});
|
||||
|
||||
it('should return expected heroes (called once)', () => {
|
||||
heroService.getHeroes().subscribe(
|
||||
heroes => expect(heroes).toEqual(expectedHeroes, 'should return expected heroes'),
|
||||
fail
|
||||
);
|
||||
|
||||
// HeroService should have made one request to GET heroes from expected URL
|
||||
const req = httpTestingController.expectOne(heroService.heroesUrl);
|
||||
expect(req.request.method).toEqual('GET');
|
||||
|
||||
// Respond with the mock heroes
|
||||
req.flush(expectedHeroes);
|
||||
});
|
||||
|
||||
it('should be OK returning no heroes', () => {
|
||||
heroService.getHeroes().subscribe(
|
||||
heroes => expect(heroes.length).toEqual(0, 'should have empty heroes array'),
|
||||
fail
|
||||
);
|
||||
|
||||
const req = httpTestingController.expectOne(heroService.heroesUrl);
|
||||
req.flush([]); // Respond with no heroes
|
||||
});
|
||||
|
||||
it('should turn 404 into a user-friendly error', () => {
|
||||
const msg = 'Deliberate 404';
|
||||
heroService.getHeroes().subscribe(
|
||||
heroes => fail('expected to fail'),
|
||||
error => expect(error.message).toContain(msg)
|
||||
);
|
||||
|
||||
const req = httpTestingController.expectOne(heroService.heroesUrl);
|
||||
|
||||
// respond with a 404 and the error message in the body
|
||||
req.flush(msg, {status: 404, statusText: 'Not Found'});
|
||||
});
|
||||
|
||||
it('should return expected heroes (called multiple times)', () => {
|
||||
heroService.getHeroes().subscribe();
|
||||
heroService.getHeroes().subscribe();
|
||||
heroService.getHeroes().subscribe(
|
||||
heroes => expect(heroes).toEqual(expectedHeroes, 'should return expected heroes'),
|
||||
fail
|
||||
);
|
||||
|
||||
const requests = httpTestingController.match(heroService.heroesUrl);
|
||||
expect(requests.length).toEqual(3, 'calls to getHeroes()');
|
||||
|
||||
// Respond to each request with different mock hero results
|
||||
requests[0].flush([]);
|
||||
requests[1].flush([{id: 1, name: 'bob'}]);
|
||||
requests[2].flush(expectedHeroes);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#updateHero', () => {
|
||||
// Expecting the query form of URL so should not 404 when id not found
|
||||
const makeUrl = (id: number) => `${heroService.heroesUrl}/?id=${id}`;
|
||||
|
||||
it('should update a hero and return it', () => {
|
||||
|
||||
const updateHero: Hero = { id: 1, name: 'A' };
|
||||
|
||||
heroService.updateHero(updateHero).subscribe(
|
||||
data => expect(data).toEqual(updateHero, 'should return the hero'),
|
||||
fail
|
||||
);
|
||||
|
||||
// HeroService should have made one request to PUT hero
|
||||
const req = httpTestingController.expectOne(heroService.heroesUrl);
|
||||
expect(req.request.method).toEqual('PUT');
|
||||
expect(req.request.body).toEqual(updateHero);
|
||||
|
||||
// Expect server to return the hero after PUT
|
||||
const expectedResponse = new HttpResponse(
|
||||
{ status: 200, statusText: 'OK', body: updateHero });
|
||||
req.event(expectedResponse);
|
||||
});
|
||||
|
||||
it('should turn 404 error into user-facing error', () => {
|
||||
const msg = 'Deliberate 404';
|
||||
const updateHero: Hero = { id: 1, name: 'A' };
|
||||
heroService.updateHero(updateHero).subscribe(
|
||||
heroes => fail('expected to fail'),
|
||||
error => expect(error.message).toContain(msg)
|
||||
);
|
||||
|
||||
const req = httpTestingController.expectOne(heroService.heroesUrl);
|
||||
|
||||
// respond with a 404 and the error message in the body
|
||||
req.flush(msg, {status: 404, statusText: 'Not Found'});
|
||||
});
|
||||
|
||||
// #docregion network-error
|
||||
it('should turn network error into user-facing error', () => {
|
||||
const emsg = 'simulated network error';
|
||||
|
||||
const updateHero: Hero = { id: 1, name: 'A' };
|
||||
heroService.updateHero(updateHero).subscribe(
|
||||
heroes => fail('expected to fail'),
|
||||
error => expect(error.message).toContain(emsg)
|
||||
);
|
||||
|
||||
const req = httpTestingController.expectOne(heroService.heroesUrl);
|
||||
|
||||
// Create mock ErrorEvent, raised when something goes wrong at the network level.
|
||||
// Connection timeout, DNS error, offline, etc
|
||||
const errorEvent = new ErrorEvent('so sad', {
|
||||
message: emsg,
|
||||
// #enddocregion network-error
|
||||
// The rest of this is optional and not used.
|
||||
// Just showing that you could provide this too.
|
||||
filename: 'HeroService.ts',
|
||||
lineno: 42,
|
||||
colno: 21
|
||||
// #docregion network-error
|
||||
});
|
||||
|
||||
// Respond with mock error
|
||||
req.error(errorEvent);
|
||||
});
|
||||
// #enddocregion network-error
|
||||
});
|
||||
|
||||
// TODO: test other HeroService methods
|
||||
});
|
@ -1,30 +1,98 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
|
||||
|
||||
import { Hero } from './hero';
|
||||
import { HEROES } from './test-heroes';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { of } from 'rxjs/observable/of';
|
||||
import { catchError, map, tap } from 'rxjs/operators';
|
||||
|
||||
import { Hero } from './hero';
|
||||
|
||||
const httpOptions = {
|
||||
headers: new HttpHeaders({ 'Content-Type': 'application/json' })
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
/** Dummy HeroService. Pretend it makes real http requests */
|
||||
export class HeroService {
|
||||
getHeroes() {
|
||||
return Promise.resolve(HEROES);
|
||||
|
||||
readonly heroesUrl = 'api/heroes'; // URL to web api
|
||||
|
||||
constructor(private http: HttpClient) { }
|
||||
|
||||
/** GET heroes from the server */
|
||||
getHeroes (): Observable<Hero[]> {
|
||||
return this.http.get<Hero[]>(this.heroesUrl)
|
||||
.pipe(
|
||||
tap(heroes => this.log(`fetched heroes`)),
|
||||
catchError(this.handleError('getHeroes'))
|
||||
) as Observable<Hero[]>;
|
||||
}
|
||||
|
||||
getHero(id: number | string): Promise<Hero> {
|
||||
/** GET hero by id. Return `undefined` when id not found */
|
||||
getHero<Data>(id: number | string): Observable<Hero> {
|
||||
if (typeof id === 'string') {
|
||||
id = parseInt(id as string, 10);
|
||||
}
|
||||
return this.getHeroes().then(
|
||||
heroes => heroes.find(hero => hero.id === id)
|
||||
const url = `${this.heroesUrl}/?id=${id}`;
|
||||
return this.http.get<Hero[]>(url)
|
||||
.pipe(
|
||||
map(heroes => heroes[0]), // returns a {0|1} element array
|
||||
tap(h => {
|
||||
const outcome = h ? `fetched` : `did not find`;
|
||||
this.log(`${outcome} hero id=${id}`);
|
||||
}),
|
||||
catchError(this.handleError<Hero>(`getHero id=${id}`))
|
||||
);
|
||||
}
|
||||
|
||||
//////// Save methods //////////
|
||||
|
||||
/** POST: add a new hero to the server */
|
||||
addHero (hero: Hero): Observable<Hero> {
|
||||
return this.http.post<Hero>(this.heroesUrl, hero, httpOptions).pipe(
|
||||
tap((hero: Hero) => this.log(`added hero w/ id=${hero.id}`)),
|
||||
catchError(this.handleError<Hero>('addHero'))
|
||||
);
|
||||
}
|
||||
/** DELETE: delete the hero from the server */
|
||||
deleteHero (hero: Hero | number): Observable<Hero> {
|
||||
const id = typeof hero === 'number' ? hero : hero.id;
|
||||
const url = `${this.heroesUrl}/${id}`;
|
||||
|
||||
return this.http.delete<Hero>(url, httpOptions).pipe(
|
||||
tap(_ => this.log(`deleted hero id=${id}`)),
|
||||
catchError(this.handleError<Hero>('deleteHero'))
|
||||
);
|
||||
}
|
||||
|
||||
updateHero(hero: Hero): Promise<Hero> {
|
||||
return this.getHero(hero.id).then(h => {
|
||||
if (!h) {
|
||||
throw new Error(`Hero ${hero.id} not found`);
|
||||
}
|
||||
return Object.assign(h, hero);
|
||||
});
|
||||
/** PUT: update the hero on the server */
|
||||
updateHero (hero: Hero): Observable<any> {
|
||||
return this.http.put(this.heroesUrl, hero, httpOptions).pipe(
|
||||
tap(_ => this.log(`updated hero id=${hero.id}`)),
|
||||
catchError(this.handleError<any>('updateHero'))
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Returns a function that handles Http operation failures.
|
||||
* This error handler lets the app continue to run as if no error occurred.
|
||||
* @param operation - name of the operation that failed
|
||||
*/
|
||||
private handleError<T> (operation = 'operation') {
|
||||
return (error: HttpErrorResponse): Observable<T> => {
|
||||
|
||||
// TODO: send the error to remote logging infrastructure
|
||||
console.error(error); // log to console instead
|
||||
|
||||
const message = (error.error instanceof ErrorEvent) ?
|
||||
error.error.message :
|
||||
`server returned code ${error.status} with body "${error.error}"`;
|
||||
|
||||
// TODO: better job of transforming error for user consumption
|
||||
throw new Error(`${operation} failed: ${message}`);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
private log(message: string) {
|
||||
console.log('HeroService: ' + message);
|
||||
}
|
||||
}
|
||||
|
@ -1,20 +0,0 @@
|
||||
// #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);
|
||||
});
|
||||
});
|
@ -1,4 +1,8 @@
|
||||
export class Hero {
|
||||
constructor(public id = 0, public name = '') { }
|
||||
clone() { return new Hero(this.id, this.name); }
|
||||
export interface Hero {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
// SystemJS bug:
|
||||
// TS file must export something real in JS, not just interfaces
|
||||
export const _dummy = undefined;
|
||||
|
@ -1,3 +1,6 @@
|
||||
/**
|
||||
* Test the HeroService when implemented with the OLD HttpModule
|
||||
*/
|
||||
import {
|
||||
async, inject, TestBed
|
||||
} from '@angular/core/testing';
|
||||
@ -12,14 +15,11 @@ import {
|
||||
} from '@angular/http';
|
||||
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import 'rxjs/add/observable/of';
|
||||
|
||||
import 'rxjs/add/operator/catch';
|
||||
import 'rxjs/add/operator/do';
|
||||
import 'rxjs/add/operator/toPromise';
|
||||
import { of } from 'rxjs/observable/of';
|
||||
import { catchError, tap } from 'rxjs/operators';
|
||||
|
||||
import { Hero } from './hero';
|
||||
import { HttpHeroService as HeroService } from './http-hero.service';
|
||||
import { HttpHeroService } from './http-hero.service';
|
||||
|
||||
const makeHeroData = () => [
|
||||
{ id: 1, name: 'Windstorm' },
|
||||
@ -29,99 +29,100 @@ const makeHeroData = () => [
|
||||
] as Hero[];
|
||||
|
||||
//////// Tests /////////////
|
||||
describe('Http-HeroService (mockBackend)', () => {
|
||||
describe('HttpHeroService (using old HttpModule)', () => {
|
||||
let backend: MockBackend;
|
||||
let service: HttpHeroService;
|
||||
|
||||
beforeEach( async(() => {
|
||||
beforeEach( () => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [ HttpModule ],
|
||||
providers: [
|
||||
HeroService,
|
||||
HttpHeroService,
|
||||
{ provide: XHRBackend, useClass: MockBackend }
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
it('can instantiate service when inject service',
|
||||
inject([HeroService], (service: HeroService) => {
|
||||
expect(service instanceof HeroService).toBe(true);
|
||||
}));
|
||||
it('can instantiate service via DI', () => {
|
||||
service = TestBed.get(HttpHeroService);
|
||||
expect(service instanceof HttpHeroService).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
|
||||
it('can instantiate service with "new"', inject([Http], (http: Http) => {
|
||||
it('can instantiate service with "new"', () => {
|
||||
const http = TestBed.get(Http);
|
||||
expect(http).not.toBeNull('http should be provided');
|
||||
let service = new HeroService(http);
|
||||
expect(service instanceof HeroService).toBe(true, 'new service should be ok');
|
||||
}));
|
||||
let service = new HttpHeroService(http);
|
||||
expect(service instanceof HttpHeroService).toBe(true, 'new service should be ok');
|
||||
});
|
||||
|
||||
|
||||
it('can provide the mockBackend as XHRBackend',
|
||||
inject([XHRBackend], (backend: MockBackend) => {
|
||||
expect(backend).not.toBeNull('backend should be provided');
|
||||
}));
|
||||
it('can provide the mockBackend as XHRBackend', () => {
|
||||
const backend = TestBed.get(XHRBackend);
|
||||
expect(backend).not.toBeNull('backend should be provided');
|
||||
});
|
||||
|
||||
describe('when getHeroes', () => {
|
||||
let backend: MockBackend;
|
||||
let service: HeroService;
|
||||
let fakeHeroes: Hero[];
|
||||
let response: Response;
|
||||
let fakeHeroes: Hero[];
|
||||
let http: Http;
|
||||
let response: Response;
|
||||
|
||||
beforeEach(inject([Http, XHRBackend], (http: Http, be: MockBackend) => {
|
||||
backend = be;
|
||||
service = new HeroService(http);
|
||||
fakeHeroes = makeHeroData();
|
||||
let options = new ResponseOptions({status: 200, body: {data: fakeHeroes}});
|
||||
response = new Response(options);
|
||||
}));
|
||||
beforeEach(() => {
|
||||
|
||||
it('should have expected fake heroes (then)', async(inject([], () => {
|
||||
backend.connections.subscribe((c: MockConnection) => c.mockRespond(response));
|
||||
backend = TestBed.get(XHRBackend);
|
||||
http = TestBed.get(Http);
|
||||
|
||||
service.getHeroes().toPromise()
|
||||
// .then(() => Promise.reject('deliberate'))
|
||||
.then(heroes => {
|
||||
expect(heroes.length).toBe(fakeHeroes.length,
|
||||
'should have expected no. of heroes');
|
||||
});
|
||||
})));
|
||||
service = new HttpHeroService(http);
|
||||
fakeHeroes = makeHeroData();
|
||||
let options = new ResponseOptions({status: 200, body: {data: fakeHeroes}});
|
||||
response = new Response(options);
|
||||
});
|
||||
|
||||
it('should have expected fake heroes (Observable.do)', async(inject([], () => {
|
||||
backend.connections.subscribe((c: MockConnection) => c.mockRespond(response));
|
||||
it('should have expected fake heroes (then)', () => {
|
||||
backend.connections.subscribe((c: MockConnection) => c.mockRespond(response));
|
||||
|
||||
service.getHeroes()
|
||||
.do(heroes => {
|
||||
expect(heroes.length).toBe(fakeHeroes.length,
|
||||
'should have expected no. of heroes');
|
||||
})
|
||||
.toPromise();
|
||||
})));
|
||||
service.getHeroes().toPromise()
|
||||
// .then(() => Promise.reject('deliberate'))
|
||||
.then(heroes => {
|
||||
expect(heroes.length).toBe(fakeHeroes.length,
|
||||
'should have expected no. of heroes');
|
||||
})
|
||||
.catch(fail);
|
||||
});
|
||||
|
||||
it('should have expected fake heroes (Observable tap)', () => {
|
||||
backend.connections.subscribe((c: MockConnection) => c.mockRespond(response));
|
||||
|
||||
service.getHeroes().subscribe(
|
||||
heroes => {
|
||||
expect(heroes.length).toBe(fakeHeroes.length,
|
||||
'should have expected no. of heroes');
|
||||
},
|
||||
fail
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
it('should be OK returning no heroes', async(inject([], () => {
|
||||
let resp = new Response(new ResponseOptions({status: 200, body: {data: []}}));
|
||||
backend.connections.subscribe((c: MockConnection) => c.mockRespond(resp));
|
||||
it('should be OK returning no heroes', () => {
|
||||
let resp = new Response(new ResponseOptions({status: 200, body: {data: []}}));
|
||||
backend.connections.subscribe((c: MockConnection) => c.mockRespond(resp));
|
||||
|
||||
service.getHeroes()
|
||||
.do(heroes => {
|
||||
expect(heroes.length).toBe(0, 'should have no heroes');
|
||||
})
|
||||
.toPromise();
|
||||
})));
|
||||
service.getHeroes().subscribe(
|
||||
heroes => {
|
||||
expect(heroes.length).toBe(0, 'should have no heroes');
|
||||
},
|
||||
fail
|
||||
);
|
||||
});
|
||||
|
||||
it('should treat 404 as an Observable error', async(inject([], () => {
|
||||
let resp = new Response(new ResponseOptions({status: 404}));
|
||||
backend.connections.subscribe((c: MockConnection) => c.mockRespond(resp));
|
||||
it('should treat 404 as an Observable error', () => {
|
||||
let resp = new Response(new ResponseOptions({status: 404}));
|
||||
backend.connections.subscribe((c: MockConnection) => c.mockRespond(resp));
|
||||
|
||||
service.getHeroes()
|
||||
.do(heroes => {
|
||||
fail('should not respond with heroes');
|
||||
})
|
||||
.catch(err => {
|
||||
expect(err).toMatch(/Bad response status/, 'should catch bad response status code');
|
||||
return Observable.of(null); // failure is the expected test result
|
||||
})
|
||||
.toPromise();
|
||||
})));
|
||||
service.getHeroes().subscribe(
|
||||
heroes => fail('should not respond with heroes'),
|
||||
err => {
|
||||
expect(err).toMatch(/Bad response status/, 'should catch bad response status code');
|
||||
return of(null); // failure is the expected test result
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,3 +1,4 @@
|
||||
// The OLD Http module. See HeroService for use of the current HttpClient
|
||||
// #docplaster
|
||||
// #docregion
|
||||
import { Injectable } from '@angular/core';
|
||||
@ -5,12 +6,9 @@ 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';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { ErrorObservable } from 'rxjs/observable/ErrorObservable';
|
||||
import { catchError, map, tap } from 'rxjs/operators';
|
||||
|
||||
@Injectable()
|
||||
export class HttpHeroService {
|
||||
@ -19,16 +17,17 @@ export class HttpHeroService {
|
||||
constructor (private http: Http) {}
|
||||
|
||||
getHeroes (): Observable<Hero[]> {
|
||||
return this.http.get(this._heroesUrl)
|
||||
.map(this.extractData)
|
||||
// .do(data => console.log(data)) // eyeball results in the console
|
||||
.catch(this.handleError);
|
||||
return this.http.get(this._heroesUrl).pipe(
|
||||
map(this.extractData),
|
||||
// tap(data => console.log(data)), // eyeball results in the console
|
||||
catchError(this.handleError)
|
||||
);
|
||||
}
|
||||
|
||||
getHero(id: number | string) {
|
||||
return this.http
|
||||
.get('app/heroes/?id=${id}')
|
||||
.map((r: Response) => r.json().data as Hero[]);
|
||||
return this.http.get('app/heroes/?id=${id}').pipe(
|
||||
map((r: Response) => r.json().data as Hero[])
|
||||
);
|
||||
}
|
||||
|
||||
addHero (name: string): Observable<Hero> {
|
||||
@ -36,9 +35,10 @@ export class HttpHeroService {
|
||||
let headers = new Headers({ 'Content-Type': 'application/json' });
|
||||
let options = new RequestOptions({ headers: headers });
|
||||
|
||||
return this.http.post(this._heroesUrl, body, options)
|
||||
.map(this.extractData)
|
||||
.catch(this.handleError);
|
||||
return this.http.post(this._heroesUrl, body, options).pipe(
|
||||
map(this.extractData),
|
||||
catchError(this.handleError)
|
||||
);
|
||||
}
|
||||
|
||||
updateHero (hero: Hero): Observable<Hero> {
|
||||
@ -46,9 +46,10 @@ export class HttpHeroService {
|
||||
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);
|
||||
return this.http.put(this._heroesUrl, body, options).pipe(
|
||||
map(this.extractData),
|
||||
catchError(this.handleError)
|
||||
);
|
||||
}
|
||||
|
||||
private extractData(res: Response) {
|
||||
@ -63,6 +64,6 @@ export class HttpHeroService {
|
||||
// In a real world app, we might send the error to remote logging infrastructure
|
||||
let errMsg = error.message || 'Server error';
|
||||
console.error(errMsg); // log to console instead
|
||||
return Observable.throw(errMsg);
|
||||
return new ErrorObservable(errMsg);
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +0,0 @@
|
||||
// #docregion
|
||||
import { Hero } from './hero';
|
||||
|
||||
export const 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')
|
||||
];
|
@ -1,41 +0,0 @@
|
||||
// re-export for tester convenience
|
||||
export { Hero } from '../hero';
|
||||
export { HeroService } from '../hero.service';
|
||||
|
||||
import { Hero } from '../hero';
|
||||
import { HeroService } from '../hero.service';
|
||||
|
||||
export const HEROES: Hero[] = [
|
||||
new Hero(41, 'Bob'),
|
||||
new Hero(42, 'Carol'),
|
||||
new Hero(43, 'Ted'),
|
||||
new Hero(44, 'Alice'),
|
||||
new Hero(45, 'Speedy'),
|
||||
new Hero(46, 'Stealthy')
|
||||
];
|
||||
|
||||
export class FakeHeroService implements HeroService {
|
||||
|
||||
heroes = HEROES.map(h => h.clone());
|
||||
lastPromise: Promise<any>; // remember so we can spy on promise calls
|
||||
|
||||
getHero(id: number | string) {
|
||||
if (typeof id === 'string') {
|
||||
id = parseInt(id as string, 10);
|
||||
}
|
||||
let hero = this.heroes.find(h => h.id === id);
|
||||
return this.lastPromise = Promise.resolve(hero);
|
||||
}
|
||||
|
||||
getHeroes() {
|
||||
return this.lastPromise = Promise.resolve<Hero[]>(this.heroes);
|
||||
}
|
||||
|
||||
updateHero(hero: Hero): Promise<Hero> {
|
||||
return this.lastPromise = this.getHero(hero.id).then(h => {
|
||||
return h ?
|
||||
Object.assign(h, hero) :
|
||||
Promise.reject(`Hero ${hero.id} not found`) as any as Promise<Hero>;
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,192 @@
|
||||
// #docplaster
|
||||
// #docregion imports
|
||||
// Http testing module and mocking controller
|
||||
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
|
||||
|
||||
// Other imports
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
|
||||
|
||||
// #enddocregion imports
|
||||
import { HttpHeaders } from '@angular/common/http';
|
||||
|
||||
interface Data {
|
||||
name: string;
|
||||
}
|
||||
|
||||
const testUrl = '/data';
|
||||
|
||||
// #docregion setup
|
||||
describe('HttpClient testing', () => {
|
||||
let httpClient: HttpClient;
|
||||
let httpTestingController: HttpTestingController;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [ HttpClientTestingModule ]
|
||||
});
|
||||
|
||||
// Inject the http service and test controller for each test
|
||||
httpClient = TestBed.get(HttpClient);
|
||||
httpTestingController = TestBed.get(HttpTestingController);
|
||||
});
|
||||
// #enddocregion setup
|
||||
// #docregion afterEach
|
||||
afterEach(() => {
|
||||
// After every test, assert that there are no more pending requests.
|
||||
httpTestingController.verify();
|
||||
});
|
||||
// #enddocregion afterEach
|
||||
// #docregion setup
|
||||
/// Tests begin ///
|
||||
// #enddocregion setup
|
||||
// #docregion get-test
|
||||
it('can test HttpClient.get', () => {
|
||||
const testData: Data = {name: 'Test Data'};
|
||||
|
||||
// Make an HTTP GET request
|
||||
httpClient.get<Data>(testUrl)
|
||||
.subscribe(data =>
|
||||
// When observable resolves, result should match test data
|
||||
expect(data).toEqual(testData)
|
||||
);
|
||||
|
||||
// The following `expectOne()` will match the request's URL.
|
||||
// If no requests or multiple requests matched that URL
|
||||
// `expectOne()` would throw.
|
||||
const req = httpTestingController.expectOne('/data');
|
||||
|
||||
// Assert that the request is a GET.
|
||||
expect(req.request.method).toEqual('GET');
|
||||
|
||||
// Respond with mock data, causing Observable to resolve.
|
||||
// Subscribe callback asserts that correct data was returned.
|
||||
req.flush(testData);
|
||||
|
||||
// Finally, assert that there are no outstanding requests.
|
||||
httpTestingController.verify();
|
||||
});
|
||||
// #enddocregion get-test
|
||||
it('can test HttpClient.get with matching header', () => {
|
||||
const testData: Data = {name: 'Test Data'};
|
||||
|
||||
// Make an HTTP GET request with specific header
|
||||
httpClient.get<Data>(testUrl, {
|
||||
headers: new HttpHeaders({'Authorization': 'my-auth-token'})
|
||||
})
|
||||
.subscribe(data =>
|
||||
expect(data).toEqual(testData)
|
||||
);
|
||||
|
||||
// Find request with a predicate function.
|
||||
// #docregion predicate
|
||||
// Expect one request with an authorization header
|
||||
const req = httpTestingController.expectOne(
|
||||
req => req.headers.has('Authorization')
|
||||
);
|
||||
// #enddocregion predicate
|
||||
req.flush(testData);
|
||||
});
|
||||
|
||||
it('can test multiple requests', () => {
|
||||
let testData: Data[] = [
|
||||
{ name: 'bob' }, { name: 'carol' },
|
||||
{ name: 'ted' }, { name: 'alice' }
|
||||
];
|
||||
|
||||
// Make three requests in a row
|
||||
httpClient.get<Data[]>(testUrl)
|
||||
.subscribe(d => expect(d.length).toEqual(0, 'should have no data'));
|
||||
|
||||
httpClient.get<Data[]>(testUrl)
|
||||
.subscribe(d => expect(d).toEqual([testData[0]], 'should be one element array'));
|
||||
|
||||
httpClient.get<Data[]>(testUrl)
|
||||
.subscribe(d => expect(d).toEqual(testData, 'should be expected data'));
|
||||
|
||||
// #docregion multi-request
|
||||
// get all pending requests that match the given URL
|
||||
const requests = httpTestingController.match(testUrl);
|
||||
expect(requests.length).toEqual(3);
|
||||
|
||||
// Respond to each request with different results
|
||||
requests[0].flush([]);
|
||||
requests[1].flush([testData[0]]);
|
||||
requests[2].flush(testData);
|
||||
// #enddocregion multi-request
|
||||
});
|
||||
|
||||
// #docregion 404
|
||||
it('can test for 404 error', () => {
|
||||
const emsg = 'deliberate 404 error';
|
||||
|
||||
httpClient.get<Data[]>(testUrl).subscribe(
|
||||
data => fail('should have failed with the 404 error'),
|
||||
(error: HttpErrorResponse) => {
|
||||
expect(error.status).toEqual(404, 'status');
|
||||
expect(error.error).toEqual(emsg, 'message');
|
||||
}
|
||||
);
|
||||
|
||||
const req = httpTestingController.expectOne(testUrl);
|
||||
|
||||
// Respond with mock error
|
||||
req.flush(emsg, { status: 404, statusText: 'Not Found' });
|
||||
});
|
||||
// #enddocregion 404
|
||||
|
||||
// #docregion network-error
|
||||
it('can test for network error', () => {
|
||||
const emsg = 'simulated network error';
|
||||
|
||||
httpClient.get<Data[]>(testUrl).subscribe(
|
||||
data => fail('should have failed with the network error'),
|
||||
(error: HttpErrorResponse) => {
|
||||
expect(error.error.message).toEqual(emsg, 'message');
|
||||
}
|
||||
);
|
||||
|
||||
const req = httpTestingController.expectOne(testUrl);
|
||||
|
||||
// Create mock ErrorEvent, raised when something goes wrong at the network level.
|
||||
// Connection timeout, DNS error, offline, etc
|
||||
const errorEvent = new ErrorEvent('so sad', {
|
||||
message: emsg,
|
||||
// #enddocregion network-error
|
||||
// The rest of this is optional and not used.
|
||||
// Just showing that you could provide this too.
|
||||
filename: 'HeroService.ts',
|
||||
lineno: 42,
|
||||
colno: 21
|
||||
// #docregion network-error
|
||||
});
|
||||
|
||||
// Respond with mock error
|
||||
req.error(errorEvent);
|
||||
});
|
||||
// #enddocregion network-error
|
||||
|
||||
it('httpTestingController.verify should fail if HTTP response not simulated', () => {
|
||||
// Sends request
|
||||
httpClient.get('some/api').subscribe();
|
||||
|
||||
// verify() should fail because haven't handled the pending request.
|
||||
expect(() => httpTestingController.verify()).toThrow();
|
||||
|
||||
// Now get and flush the request so that afterEach() doesn't fail
|
||||
const req = httpTestingController.expectOne('some/api');
|
||||
req.flush(null);
|
||||
});
|
||||
|
||||
// Proves that verify in afterEach() really would catch error
|
||||
// if test doesn't simulate the HTTP response.
|
||||
//
|
||||
// Must disable this test because can't catch an error in an afterEach().
|
||||
// Uncomment if you want to confirm that afterEach() does the job.
|
||||
// it('afterEach() should fail when HTTP response not simulated',() => {
|
||||
// // Sends request which is never handled by this test
|
||||
// httpClient.get('some/api').subscribe();
|
||||
// });
|
||||
// #docregion setup
|
||||
});
|
||||
// #enddocregion setup
|
@ -0,0 +1,61 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { asyncData } from '../../../testing';
|
||||
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
// re-export for tester convenience
|
||||
export { Hero } from '../hero';
|
||||
export { HeroService } from '../hero.service';
|
||||
export { getTestHeroes } from './test-heroes';
|
||||
|
||||
import { Hero } from '../hero';
|
||||
import { HeroService } from '../hero.service';
|
||||
import { getTestHeroes } from './test-heroes';
|
||||
|
||||
@Injectable()
|
||||
/**
|
||||
* FakeHeroService pretends to make real http requests.
|
||||
* implements only as much of HeroService as is actually consumed by the app
|
||||
*/
|
||||
export class TestHeroService extends HeroService {
|
||||
|
||||
constructor() {
|
||||
super(null);
|
||||
}
|
||||
|
||||
heroes = getTestHeroes();
|
||||
lastResult: Observable<any>; // result from last method call
|
||||
|
||||
addHero(hero: Hero): Observable<Hero> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
deleteHero(hero: number | Hero): Observable<Hero> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
getHeroes(): Observable<Hero[]> {
|
||||
return this.lastResult = asyncData(this.heroes);
|
||||
}
|
||||
|
||||
getHero(id: number | string): Observable<Hero> {
|
||||
if (typeof id === 'string') {
|
||||
id = parseInt(id as string, 10);
|
||||
}
|
||||
let hero = this.heroes.find(h => h.id === id);
|
||||
return this.lastResult = asyncData(hero);
|
||||
}
|
||||
|
||||
updateHero(hero: Hero): Observable<Hero> {
|
||||
return this.lastResult = this.getHero(hero.id).pipe(
|
||||
map(h => {
|
||||
if (h) {
|
||||
return Object.assign(h, hero);
|
||||
}
|
||||
throw new Error(`Hero ${hero.id} not found`);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
import { Hero } from '../hero';
|
||||
|
||||
/** return fresh array of test heroes */
|
||||
export function getTestHeroes(): Hero[] {
|
||||
return [
|
||||
{id: 41, name: 'Bob' },
|
||||
{id: 42, name: 'Carol' },
|
||||
{id: 43, name: 'Ted' },
|
||||
{id: 44, name: 'Alice' },
|
||||
{id: 45, name: 'Speedy' },
|
||||
{id: 46, name: 'Stealthy' }
|
||||
];
|
||||
}
|
@ -4,12 +4,16 @@ 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 ]
|
||||
imports: [ CommonModule ],
|
||||
exports: [
|
||||
CommonModule,
|
||||
// SharedModule importers won't have to import FormsModule too
|
||||
FormsModule,
|
||||
HighlightDirective,
|
||||
TitleCasePipe
|
||||
],
|
||||
declarations: [ HighlightDirective, TitleCasePipe ]
|
||||
})
|
||||
export class SharedModule { }
|
||||
|
@ -1,92 +0,0 @@
|
||||
// #docplaster
|
||||
import { async, fakeAsync, ComponentFixture, TestBed, tick } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { DebugElement } from '@angular/core';
|
||||
|
||||
import { TwainService } from './twain.service';
|
||||
import { TwainComponent } from './twain.component';
|
||||
|
||||
describe('TwainComponent', () => {
|
||||
|
||||
let comp: TwainComponent;
|
||||
let fixture: ComponentFixture<TwainComponent>;
|
||||
|
||||
let spy: jasmine.Spy;
|
||||
let de: DebugElement;
|
||||
let el: HTMLElement;
|
||||
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)
|
||||
de = fixture.debugElement.query(By.css('.twain'));
|
||||
el = de.nativeElement;
|
||||
});
|
||||
// #enddocregion setup
|
||||
|
||||
// #docregion tests
|
||||
it('should not show quote before OnInit', () => {
|
||||
expect(el.textContent).toBe('', 'nothing displayed');
|
||||
expect(spy.calls.any()).toBe(false, 'getQuote not yet called');
|
||||
});
|
||||
|
||||
it('should still not show quote after component initialized', () => {
|
||||
fixture.detectChanges();
|
||||
// getQuote service is async => still has not returned with quote
|
||||
expect(el.textContent).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();
|
||||
|
||||
fixture.whenStable().then(() => { // wait for async getQuote
|
||||
fixture.detectChanges(); // update view with quote
|
||||
expect(el.textContent).toBe(testQuote);
|
||||
});
|
||||
}));
|
||||
// #enddocregion async-test
|
||||
|
||||
// #docregion fake-async-test
|
||||
it('should show quote after getQuote promise (fakeAsync)', fakeAsync(() => {
|
||||
fixture.detectChanges();
|
||||
tick(); // wait for async getQuote
|
||||
fixture.detectChanges(); // update view with quote
|
||||
expect(el.textContent).toBe(testQuote);
|
||||
}));
|
||||
// #enddocregion fake-async-test
|
||||
// #enddocregion tests
|
||||
|
||||
// #docregion done-test
|
||||
it('should show quote after getQuote promise (done)', (done: any) => {
|
||||
fixture.detectChanges();
|
||||
|
||||
// get the spy promise and wait for it to resolve
|
||||
spy.calls.mostRecent().returnValue.then(() => {
|
||||
fixture.detectChanges(); // update view with quote
|
||||
expect(el.textContent).toBe(testQuote);
|
||||
done();
|
||||
});
|
||||
});
|
||||
// #enddocregion done-test
|
||||
});
|
@ -1,116 +0,0 @@
|
||||
// #docplaster
|
||||
// When AppComponent learns to present quote with intervalTimer
|
||||
import { async, discardPeriodicTasks, fakeAsync, ComponentFixture, TestBed, tick } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { DebugElement } from '@angular/core';
|
||||
|
||||
import { TwainService } from './model';
|
||||
import { TwainComponent } from './twain.component';
|
||||
|
||||
xdescribe('TwainComponent', () => {
|
||||
|
||||
let comp: TwainComponent;
|
||||
let fixture: ComponentFixture<TwainComponent>;
|
||||
|
||||
const quotes = [
|
||||
'Test Quote 1',
|
||||
'Test Quote 2',
|
||||
'Test Quote 3'
|
||||
];
|
||||
|
||||
let spy: jasmine.Spy;
|
||||
let twainEl: DebugElement; // the element with the Twain quote
|
||||
let twainService: TwainService; // the actually injected service
|
||||
|
||||
function getQuote() { return twainEl.nativeElement.textContent; }
|
||||
|
||||
// #docregion setup
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ TwainComponent ],
|
||||
providers: [ TwainService ],
|
||||
});
|
||||
|
||||
fixture = TestBed.createComponent(TwainComponent);
|
||||
comp = fixture.componentInstance;
|
||||
|
||||
// TwainService actually injected into the component
|
||||
twainService = fixture.debugElement.injector.get(TwainService);
|
||||
|
||||
// Setup spy on the `getQuote` method
|
||||
spy = spyOn(twainService, 'getQuote')
|
||||
.and.returnValues(...quotes.map(q => Promise.resolve(q)));
|
||||
|
||||
// Get the Twain quote element by CSS selector (e.g., by class name)
|
||||
twainEl = fixture.debugElement.query(By.css('.twain'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// destroy component to stop the component timer
|
||||
fixture.destroy();
|
||||
});
|
||||
// #enddocregion setup
|
||||
|
||||
// #docregion tests
|
||||
it('should not show quote before OnInit', () => {
|
||||
expect(getQuote()).toBe('');
|
||||
});
|
||||
|
||||
it('should still not show quote after component initialized', () => {
|
||||
// because the getQuote service is async
|
||||
fixture.detectChanges(); // trigger data binding
|
||||
expect(getQuote()).toContain('not initialized');
|
||||
});
|
||||
|
||||
// WIP
|
||||
// If go this way, add jasmine.clock().uninstall(); to afterEach
|
||||
// it('should show quote after Angular "settles"', async(() => {
|
||||
// //jasmine.clock().install();
|
||||
// fixture.detectChanges(); // trigger data binding
|
||||
// fixture.whenStable().then(() => {
|
||||
// fixture.detectChanges(); // update view with the quote
|
||||
// expect(getQuote()).toBe(quotes[0]);
|
||||
// });
|
||||
// // jasmine.clock().tick(5000);
|
||||
// // fixture.whenStable().then(() => {
|
||||
// // fixture.detectChanges(); // update view with the quote
|
||||
// // expect(getQuote()).toBe(quotes[1]);
|
||||
// // });
|
||||
// }));
|
||||
|
||||
it('should show quote after getQuote promise returns', fakeAsync(() => {
|
||||
fixture.detectChanges(); // trigger data binding
|
||||
tick(); // wait for first async getQuote to return
|
||||
fixture.detectChanges(); // update view with the quote
|
||||
expect(getQuote()).toBe(quotes[0]);
|
||||
|
||||
// destroy component to stop the component timer before test ends
|
||||
// else test errors because still have timer in the queue
|
||||
fixture.destroy();
|
||||
}));
|
||||
|
||||
it('should show 2nd quote after 5 seconds pass', fakeAsync(() => {
|
||||
fixture.detectChanges(); // trigger data binding
|
||||
tick(5000); // wait for second async getQuote to return
|
||||
fixture.detectChanges(); // update view with the quote
|
||||
expect(getQuote()).toBe(quotes[1]);
|
||||
|
||||
// still have intervalTimer queuing requres
|
||||
// discardPeriodicTasks() else test errors
|
||||
discardPeriodicTasks();
|
||||
}));
|
||||
|
||||
fit('should show 3rd quote after 10 seconds pass', fakeAsync(() => {
|
||||
fixture.detectChanges(); // trigger data binding
|
||||
tick(5000); // wait for second async getQuote to return
|
||||
fixture.detectChanges(); // update view with the 2nd quote
|
||||
tick(5000); // wait for third async getQuote to return
|
||||
fixture.detectChanges(); // update view with the 3rd quote
|
||||
expect(getQuote()).toBe(quotes[2]);
|
||||
|
||||
// still have intervalTimer queuing requres
|
||||
// discardPeriodicTasks() else test errors
|
||||
discardPeriodicTasks();
|
||||
}));
|
||||
// #enddocregion tests
|
||||
});
|
@ -1,27 +0,0 @@
|
||||
// #docregion
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
|
||||
import { TwainService } from './twain.service';
|
||||
|
||||
@Component({
|
||||
selector: 'twain-quote',
|
||||
template: '<p class="twain"><i>{{quote}}</i></p>'
|
||||
})
|
||||
export class TwainComponent implements OnInit, OnDestroy {
|
||||
intervalId: number;
|
||||
quote = '-- not initialized yet --';
|
||||
constructor(private twainService: TwainService) { }
|
||||
|
||||
getQuote() {
|
||||
this.twainService.getQuote().then(quote => this.quote = quote);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.getQuote();
|
||||
this.intervalId = window.setInterval(() => this.getQuote(), 5000);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
clearInterval(this.intervalId);
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user