Compare commits
260 Commits
Author | SHA1 | Date | |
---|---|---|---|
79fb9d449c | |||
73a93d3ab6 | |||
8eda5a152b | |||
7b82ce0c67 | |||
2eb5fe699f | |||
f99febcdf9 | |||
36cbfb1771 | |||
1f5315f6f7 | |||
eeebe621fe | |||
05f279df49 | |||
485d67bfed | |||
a1592f5a20 | |||
a251374ecd | |||
2b00c17091 | |||
81724f5790 | |||
1f06b6c99b | |||
6790709b93 | |||
9f7f67121c | |||
db49beae15 | |||
97609daea9 | |||
abcb03cb82 | |||
4f09f7db73 | |||
f1e14a3224 | |||
50de03a83a | |||
65555fe35d | |||
6da3867d63 | |||
c9488b5432 | |||
b22c376123 | |||
8a6f3723ca | |||
ccb0ec9c35 | |||
0c9a087809 | |||
2515ff660b | |||
70c79cb969 | |||
ed04e99c95 | |||
623adbbdf7 | |||
3660ff80b7 | |||
3b4d9dc576 | |||
8c6c2fc80d | |||
3886bfadb0 | |||
b35ab4f0e6 | |||
39d979c5fa | |||
ff980032e7 | |||
2fe401dfbb | |||
b4421bb96b | |||
ecb28bf5aa | |||
5c5164b6e7 | |||
b8a081a8a5 | |||
59d80c471a | |||
9e5b0794c5 | |||
1c1fd98591 | |||
d815e4137f | |||
fe0c5bfdb3 | |||
1975c0a4d2 | |||
efde073ab9 | |||
a68c29da4b | |||
becb775d08 | |||
a273491be0 | |||
7d45386262 | |||
82fcb325a1 | |||
115c874779 | |||
8cbebc673d | |||
94b2673c1f | |||
bc73dcb448 | |||
fefa171d83 | |||
5c84b91543 | |||
00b37310e1 | |||
bc27b95771 | |||
31f352c043 | |||
ad62eaa612 | |||
66c2d089f0 | |||
c1bf82adb9 | |||
c05d24e0fe | |||
adbb920ae8 | |||
f871fecf66 | |||
9f919f762a | |||
6df45a6d47 | |||
71128e2392 | |||
38980f1813 | |||
76f30524be | |||
721343349b | |||
4c19a2dba9 | |||
4aacbbe04b | |||
19c2d5b3d4 | |||
2f79aab084 | |||
63b178ec3d | |||
a48bf0bdb6 | |||
22dc8adae5 | |||
04a023c31a | |||
c12e553ff1 | |||
c8eb6182bc | |||
0a7a542edd | |||
9d7ad34873 | |||
63c2a2a74a | |||
2e0de01372 | |||
88e080003d | |||
8a62f0a36c | |||
6c8863aa09 | |||
fe92614c91 | |||
166bb8e048 | |||
c7da5d8cfd | |||
153738dce9 | |||
ce4aa5cb93 | |||
0647582292 | |||
c5620d1c7a | |||
e3a73dff45 | |||
5881f34787 | |||
acffa22a35 | |||
159e8b4fda | |||
d52dd0a8d1 | |||
05252769bf | |||
9c36a3520d | |||
1b282c278f | |||
c9fece997c | |||
c8817f39a9 | |||
2f1aec4744 | |||
e55127906a | |||
789ff49bcf | |||
df02d6dd86 | |||
fb8028a130 | |||
29647bb815 | |||
2911e99baf | |||
3952367bf3 | |||
ea6aade4ce | |||
26baf15b12 | |||
25c5cba7b3 | |||
fb06037392 | |||
90f8a1622e | |||
8c9edb8484 | |||
52cd20d4fe | |||
c7567b65f2 | |||
0fdd1bb929 | |||
559c647db7 | |||
42e2e7cf57 | |||
adad1706e0 | |||
a169743324 | |||
cea7fbe93f | |||
b907e5a2bc | |||
77d2cbda4a | |||
a730fc703f | |||
af26914ba9 | |||
7f7bc64186 | |||
33af76929f | |||
edbf3d2fe3 | |||
a39445fe09 | |||
0b05448a7d | |||
852a73ef82 | |||
b8bfc03875 | |||
c4887ab10a | |||
1abd3977be | |||
98961e3d44 | |||
3f89d3094b | |||
484d3d9a64 | |||
37f3b92ff5 | |||
50cd655c6c | |||
05d1b84f52 | |||
69452231df | |||
4f6bef5b32 | |||
ec96332559 | |||
ee9f0b5d9a | |||
a135f48b6d | |||
61b4c26893 | |||
f1cb46081c | |||
0ec925bd2f | |||
5f1b861525 | |||
f0d70545e8 | |||
26341c7fd4 | |||
e9f4f1b416 | |||
9cd534bd63 | |||
2f8e1fbab8 | |||
c7a6adc771 | |||
8fb2b473ca | |||
5886090d50 | |||
3988ebf432 | |||
5099b79545 | |||
038d06d2e9 | |||
9e1aff9fe6 | |||
a41f331cb4 | |||
71628f1837 | |||
df878a6b60 | |||
48d7f4e8b5 | |||
66f5d27e50 | |||
91dd160b21 | |||
1c44b71fd2 | |||
a5e0ae501d | |||
2d0e642dbe | |||
9ea656f20e | |||
97ae7aed41 | |||
678b4209c8 | |||
b7be4f55be | |||
e7c72ab556 | |||
af785f9e91 | |||
1ac5d68827 | |||
2c987625ae | |||
a77f567403 | |||
110c81f359 | |||
ef4b5c7e59 | |||
c69362442d | |||
6c8791ee32 | |||
274dc1e972 | |||
d9bd86050b | |||
076374ba4f | |||
e117b1ffd2 | |||
8d7fbb614b | |||
a31cfc521c | |||
55a1ce7adf | |||
9f3da659aa | |||
8f9aeaaa67 | |||
b9a5ce1c06 | |||
f67229efa3 | |||
f707f545aa | |||
62f4ea5f0f | |||
ecc3406ca6 | |||
e244b5180e | |||
f85d3d7857 | |||
b404d47b16 | |||
815d1ffa19 | |||
d1063c62b3 | |||
3a0b7355e5 | |||
3bdd4e249f | |||
2c1f55069f | |||
e72f741e78 | |||
f0bcfd0e78 | |||
82e06766b8 | |||
eea1600a38 | |||
8f8c390c75 | |||
23a96dca2d | |||
6f7df8a1fa | |||
92298e5271 | |||
27f0817000 | |||
4596fc0217 | |||
46de203f85 | |||
d752a8907b | |||
4fe369e188 | |||
d8930bbdc2 | |||
ad7be5087c | |||
a4405d7c6f | |||
88f7ddb27d | |||
98f5acebdb | |||
ff78149ec2 | |||
66b7870da7 | |||
82088a8489 | |||
ebcf762132 | |||
ed6b68babf | |||
2e09115c0c | |||
4a8d56a820 | |||
0a3dd872e3 | |||
3e690e0062 | |||
7f8d6c1066 | |||
c6d502f7f8 | |||
7aff3641a1 | |||
2194b5a5c3 | |||
8a35290686 | |||
e40519c32a | |||
b560189c0e | |||
59cfc8a729 | |||
72ed2e90d0 | |||
4e82a76998 | |||
51d5b433d0 | |||
cc0d0a9d1e | |||
82f26fe5f5 |
@ -12,8 +12,8 @@
|
|||||||
## IMPORTANT
|
## IMPORTANT
|
||||||
# If you change the `docker_image` version, also change the `cache_key` suffix and the version of
|
# If you change the `docker_image` version, also change the `cache_key` suffix and the version of
|
||||||
# `com_github_bazelbuild_buildtools` in the `/WORKSPACE` file.
|
# `com_github_bazelbuild_buildtools` in the `/WORKSPACE` file.
|
||||||
var_1: &docker_image angular/ngcontainer:0.3.3
|
var_1: &docker_image angular/ngcontainer:0.6.0
|
||||||
var_2: &cache_key v2-angular-{{ .Branch }}-{{ checksum "yarn.lock" }}-0.3.3
|
var_2: &cache_key v2-angular-{{ .Branch }}-{{ checksum "yarn.lock" }}-0.6.0
|
||||||
|
|
||||||
# Define common ENV vars
|
# Define common ENV vars
|
||||||
var_3: &define_env_vars
|
var_3: &define_env_vars
|
||||||
@ -48,7 +48,7 @@ jobs:
|
|||||||
|
|
||||||
# Check BUILD.bazel formatting before we have a node_modules directory
|
# Check BUILD.bazel formatting before we have a node_modules directory
|
||||||
# Then we don't need any exclude pattern to avoid checking those files
|
# 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 \)) ||
|
- run: 'buildifier -mode=check $(find . -type f \( -name "*.bzl" -or -name BUILD.bazel -or -name BUILD \)) ||
|
||||||
(echo "BUILD files not formatted. Please run ''yarn buildifier''" ; exit 1)'
|
(echo "BUILD files not formatted. Please run ''yarn buildifier''" ; exit 1)'
|
||||||
# Run the skylark linter to check our Bazel rules
|
# Run the skylark linter to check our Bazel rules
|
||||||
# deprecated-api is disabled because we use actions.new_file(genfiles_dir)
|
# deprecated-api is disabled because we use actions.new_file(genfiles_dir)
|
||||||
@ -148,6 +148,7 @@ jobs:
|
|||||||
- run: bazel run @yarn//:yarn
|
- run: bazel run @yarn//:yarn
|
||||||
- run: bazel query --output=label //... | xargs bazel test --define=compile=local --build_tag_filters=ivy-local --test_tag_filters=-manual,ivy-local
|
- run: bazel query --output=label //... | xargs bazel test --define=compile=local --build_tag_filters=ivy-local --test_tag_filters=-manual,ivy-local
|
||||||
|
|
||||||
|
# This job should only be run on PR builds, where `CIRCLE_PR_NUMBER` is defined.
|
||||||
aio_preview:
|
aio_preview:
|
||||||
<<: *job_defaults
|
<<: *job_defaults
|
||||||
environment:
|
environment:
|
||||||
@ -158,13 +159,28 @@ jobs:
|
|||||||
- restore_cache:
|
- restore_cache:
|
||||||
key: *cache_key
|
key: *cache_key
|
||||||
- run: yarn install --frozen-lockfile --non-interactive
|
- run: yarn install --frozen-lockfile --non-interactive
|
||||||
- run: ./aio/scripts/build-artifacts.sh $AIO_SNAPSHOT_ARTIFACT_PATH
|
- run: ./aio/scripts/build-artifacts.sh $AIO_SNAPSHOT_ARTIFACT_PATH $CIRCLE_PR_NUMBER $CIRCLE_SHA1
|
||||||
- store_artifacts:
|
- store_artifacts:
|
||||||
path: *aio_preview_artifact_path
|
path: *aio_preview_artifact_path
|
||||||
# The `destination` needs to be kept in synch with the value of
|
# The `destination` needs to be kept in synch with the value of
|
||||||
# `AIO_ARTIFACT_PATH` in `aio/aio-builds-setup/Dockerfile`
|
# `AIO_ARTIFACT_PATH` in `aio/aio-builds-setup/Dockerfile`
|
||||||
destination: aio/dist/aio-snapshot.tgz
|
destination: aio/dist/aio-snapshot.tgz
|
||||||
|
|
||||||
|
# This job should only be run on PR builds, where `CIRCLE_PR_NUMBER` is defined.
|
||||||
|
test_aio_preview:
|
||||||
|
<<: *job_defaults
|
||||||
|
steps:
|
||||||
|
- checkout:
|
||||||
|
<<: *post_checkout
|
||||||
|
- restore_cache:
|
||||||
|
key: *cache_key
|
||||||
|
- run: yarn install --cwd aio --frozen-lockfile --non-interactive
|
||||||
|
- run:
|
||||||
|
name: Wait for preview and run tests
|
||||||
|
command: |
|
||||||
|
source "./scripts/ci/env.sh" print
|
||||||
|
xvfb-run --auto-servernum node aio/scripts/test-preview.js $CIRCLE_PR_NUMBER $CIRCLE_SHA1 $AIO_MIN_PWA_SCORE
|
||||||
|
|
||||||
# This job exists only for backwards-compatibility with old scripts and tests
|
# This job exists only for backwards-compatibility with old scripts and tests
|
||||||
# that rely on the pre-Bazel dist/packages-dist layout.
|
# that rely on the pre-Bazel dist/packages-dist layout.
|
||||||
# It duplicates some work with the job above: we build the bazel packages
|
# It duplicates some work with the job above: we build the bazel packages
|
||||||
@ -251,7 +267,11 @@ jobs:
|
|||||||
<<: *post_checkout
|
<<: *post_checkout
|
||||||
- restore_cache:
|
- restore_cache:
|
||||||
key: *cache_key
|
key: *cache_key
|
||||||
- run: xvfb-run --auto-servernum ./aio/scripts/test-production.sh
|
- run:
|
||||||
|
name: Run tests against the deployed apps
|
||||||
|
command: |
|
||||||
|
source "./scripts/ci/env.sh" print
|
||||||
|
xvfb-run --auto-servernum ./aio/scripts/test-production.sh $AIO_MIN_PWA_SCORE
|
||||||
|
|
||||||
workflows:
|
workflows:
|
||||||
version: 2
|
version: 2
|
||||||
@ -262,6 +282,13 @@ workflows:
|
|||||||
- test_ivy_jit
|
- test_ivy_jit
|
||||||
- test_ivy_aot
|
- test_ivy_aot
|
||||||
- build-packages-dist
|
- build-packages-dist
|
||||||
|
- aio_preview:
|
||||||
|
# Only run on PR builds. (There can be no previews for non-PR builds.)
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
only: /pull\/\d+/
|
||||||
|
- test_aio_preview:
|
||||||
|
requires:
|
||||||
- aio_preview
|
- aio_preview
|
||||||
- integration_test:
|
- integration_test:
|
||||||
requires:
|
requires:
|
||||||
@ -291,6 +318,7 @@ workflows:
|
|||||||
branches:
|
branches:
|
||||||
only:
|
only:
|
||||||
- master
|
- master
|
||||||
|
|
||||||
notify:
|
notify:
|
||||||
webhooks:
|
webhooks:
|
||||||
- url: https://ngbuilds.io/circle-build
|
- url: https://ngbuilds.io/circle-build
|
7
.github/angular-robot.yml
vendored
7
.github/angular-robot.yml
vendored
@ -3,11 +3,8 @@
|
|||||||
#options for the size plugin
|
#options for the size plugin
|
||||||
size:
|
size:
|
||||||
disabled: false
|
disabled: false
|
||||||
maxSizeIncrease: 1000
|
maxSizeIncrease: 2000
|
||||||
circleCiStatusName: "ci/circleci: build-packages-dist"
|
circleCiStatusName: "ci/circleci: test"
|
||||||
status:
|
|
||||||
disabled: false
|
|
||||||
context: "ci/angular: size"
|
|
||||||
|
|
||||||
# options for the merge plugin
|
# options for the merge plugin
|
||||||
merge:
|
merge:
|
||||||
|
@ -24,7 +24,6 @@
|
|||||||
# petebacondarwin - Pete Bacon Darwin
|
# petebacondarwin - Pete Bacon Darwin
|
||||||
# pkozlowski-opensource - Pawel Kozlowski
|
# pkozlowski-opensource - Pawel Kozlowski
|
||||||
# robwormald - Rob Wormald
|
# robwormald - Rob Wormald
|
||||||
# vicb - Victor Berchet
|
|
||||||
# vikerman - Vikram Subramanian
|
# vikerman - Vikram Subramanian
|
||||||
|
|
||||||
|
|
||||||
@ -125,7 +124,6 @@ groups:
|
|||||||
users:
|
users:
|
||||||
- alexeagle
|
- alexeagle
|
||||||
- mhevery
|
- mhevery
|
||||||
- vicb
|
|
||||||
- IgorMinar #fallback
|
- IgorMinar #fallback
|
||||||
|
|
||||||
core:
|
core:
|
||||||
@ -197,7 +195,6 @@ groups:
|
|||||||
- mhevery #primary
|
- mhevery #primary
|
||||||
- jasonaden
|
- jasonaden
|
||||||
- kara
|
- kara
|
||||||
- vicb
|
|
||||||
- IgorMinar
|
- IgorMinar
|
||||||
- jenniferfell #docs only
|
- jenniferfell #docs only
|
||||||
|
|
||||||
@ -222,8 +219,7 @@ groups:
|
|||||||
- "aio/content/guide/i18n.md"
|
- "aio/content/guide/i18n.md"
|
||||||
- "aio/content/examples/i18n/*"
|
- "aio/content/examples/i18n/*"
|
||||||
users:
|
users:
|
||||||
- vicb #primary
|
- alxhub #primary
|
||||||
- alxhub
|
|
||||||
- IgorMinar #fallback
|
- IgorMinar #fallback
|
||||||
- mhevery #fallback
|
- mhevery #fallback
|
||||||
- jenniferfell #docs only
|
- jenniferfell #docs only
|
||||||
@ -235,7 +231,6 @@ groups:
|
|||||||
- "aio/content/guide/aot-compiler.md"
|
- "aio/content/guide/aot-compiler.md"
|
||||||
users:
|
users:
|
||||||
- alxhub #primary
|
- alxhub #primary
|
||||||
- vicb
|
|
||||||
- mhevery
|
- mhevery
|
||||||
- IgorMinar #fallback
|
- IgorMinar #fallback
|
||||||
- jenniferfell #docs only
|
- jenniferfell #docs only
|
||||||
@ -260,7 +255,6 @@ groups:
|
|||||||
users:
|
users:
|
||||||
- alexeagle
|
- alexeagle
|
||||||
- alxhub
|
- alxhub
|
||||||
- vicb
|
|
||||||
- IgorMinar #fallback
|
- IgorMinar #fallback
|
||||||
- mhevery #fallback
|
- mhevery #fallback
|
||||||
|
|
||||||
@ -273,7 +267,6 @@ groups:
|
|||||||
- "packages/common/http/*"
|
- "packages/common/http/*"
|
||||||
users:
|
users:
|
||||||
- pkozlowski-opensource #primary
|
- pkozlowski-opensource #primary
|
||||||
- vicb
|
|
||||||
- IgorMinar #fallback
|
- IgorMinar #fallback
|
||||||
- mhevery #fallback
|
- mhevery #fallback
|
||||||
|
|
||||||
@ -322,7 +315,6 @@ groups:
|
|||||||
users:
|
users:
|
||||||
- kyliau #primary
|
- kyliau #primary
|
||||||
# needs secondary
|
# needs secondary
|
||||||
- vicb
|
|
||||||
- IgorMinar #fallback
|
- IgorMinar #fallback
|
||||||
- mhevery #fallback
|
- mhevery #fallback
|
||||||
- jenniferfell #docs only
|
- jenniferfell #docs only
|
||||||
@ -336,7 +328,6 @@ groups:
|
|||||||
- "aio/content/images/guide/router/*"
|
- "aio/content/images/guide/router/*"
|
||||||
users:
|
users:
|
||||||
- jasonaden #primary
|
- jasonaden #primary
|
||||||
- vicb
|
|
||||||
- IgorMinar #fallback
|
- IgorMinar #fallback
|
||||||
- mhevery #fallback
|
- mhevery #fallback
|
||||||
- jenniferfell #docs only
|
- jenniferfell #docs only
|
||||||
@ -379,10 +370,9 @@ groups:
|
|||||||
files:
|
files:
|
||||||
- "packages/platform-browser/*"
|
- "packages/platform-browser/*"
|
||||||
users:
|
users:
|
||||||
- vicb #primary
|
- mhevery #primary
|
||||||
# needs secondary
|
# needs secondary
|
||||||
- IgorMinar #fallback
|
- IgorMinar #fallback
|
||||||
- mhevery #fallback
|
|
||||||
|
|
||||||
platform-server:
|
platform-server:
|
||||||
conditions:
|
conditions:
|
||||||
@ -393,7 +383,6 @@ groups:
|
|||||||
users:
|
users:
|
||||||
- vikerman #primary
|
- vikerman #primary
|
||||||
- alxhub #secondary
|
- alxhub #secondary
|
||||||
- vicb
|
|
||||||
- IgorMinar #fallback
|
- IgorMinar #fallback
|
||||||
- mhevery #fallback
|
- mhevery #fallback
|
||||||
- jenniferfell #docs only
|
- jenniferfell #docs only
|
||||||
@ -403,10 +392,9 @@ groups:
|
|||||||
files:
|
files:
|
||||||
- "packages/platform-webworker/*"
|
- "packages/platform-webworker/*"
|
||||||
users:
|
users:
|
||||||
- vicb #primary
|
- mhevery #primary
|
||||||
# needs secondary
|
# needs secondary
|
||||||
- IgorMinar #fallback
|
- IgorMinar #fallback
|
||||||
- mhevery #fallback
|
|
||||||
|
|
||||||
service-worker:
|
service-worker:
|
||||||
conditions:
|
conditions:
|
||||||
|
@ -2,7 +2,7 @@ language: node_js
|
|||||||
sudo: false
|
sudo: false
|
||||||
dist: trusty
|
dist: trusty
|
||||||
node_js:
|
node_js:
|
||||||
- '8.9.1'
|
- '10.9.0'
|
||||||
|
|
||||||
addons:
|
addons:
|
||||||
# firefox: "38.0"
|
# firefox: "38.0"
|
||||||
@ -49,12 +49,14 @@ env:
|
|||||||
- CI_MODE=browserstack_optional
|
- CI_MODE=browserstack_optional
|
||||||
- CI_MODE=aio_tools_test
|
- CI_MODE=aio_tools_test
|
||||||
- CI_MODE=aio
|
- CI_MODE=aio
|
||||||
|
- CI_MODE=aio_local
|
||||||
- CI_MODE=aio_e2e AIO_SHARD=0
|
- CI_MODE=aio_e2e AIO_SHARD=0
|
||||||
- CI_MODE=aio_e2e AIO_SHARD=1
|
- CI_MODE=aio_e2e AIO_SHARD=1
|
||||||
|
|
||||||
matrix:
|
matrix:
|
||||||
fast_finish: true
|
fast_finish: true
|
||||||
allow_failures:
|
allow_failures:
|
||||||
|
- env: "CI_MODE=aio_local"
|
||||||
- env: "CI_MODE=saucelabs_optional"
|
- env: "CI_MODE=saucelabs_optional"
|
||||||
- env: "CI_MODE=browserstack_optional"
|
- env: "CI_MODE=browserstack_optional"
|
||||||
|
|
||||||
|
68
CHANGELOG.md
68
CHANGELOG.md
@ -1,3 +1,71 @@
|
|||||||
|
<a name="6.1.10"></a>
|
||||||
|
## [6.1.10](https://github.com/angular/angular/compare/6.1.9...6.1.10) (2018-10-10)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **platform-browser:** fix [#22155](https://github.com/angular/angular/issues/22155), destroy hammer manager when `HammerInstance.off()` is run ([#22156](https://github.com/angular/angular/issues/22156)) ([3b4d9dc](https://github.com/angular/angular/commit/3b4d9dc))
|
||||||
|
* **upgrade:** properly destroy upgraded component elements and descendants ([#26209](https://github.com/angular/angular/issues/26209)) ([623adbb](https://github.com/angular/angular/commit/623adbb)), closes [#26208](https://github.com/angular/angular/issues/26208)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<a name="6.1.9"></a>
|
||||||
|
## [6.1.9](https://github.com/angular/angular/compare/6.1.8...6.1.9) (2018-09-26)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **service-worker:** do not blow up when caches are unwritable ([#26042](https://github.com/angular/angular/issues/26042)) ([a169743](https://github.com/angular/angular/commit/a169743))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<a name="6.1.8"></a>
|
||||||
|
## [6.1.8](https://github.com/angular/angular/compare/6.1.7...6.1.8) (2018-09-19)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **bazel:** allow compile_strategy to be (privately) imported ([#25080](https://github.com/angular/angular/issues/25080)) ([2d0e642](https://github.com/angular/angular/commit/2d0e642))
|
||||||
|
* **bazel:** correct type concatenated to devmode_js ([#25467](https://github.com/angular/angular/issues/25467)) ([91dd160](https://github.com/angular/angular/commit/91dd160))
|
||||||
|
* **bazel:** move bazel managed runtime deps for downstream usage ([#25690](https://github.com/angular/angular/issues/25690)) ([48d7f4e](https://github.com/angular/angular/commit/48d7f4e))
|
||||||
|
* **bazel:** specify the package and lock files using the workspace ([#25694](https://github.com/angular/angular/issues/25694)) ([678b420](https://github.com/angular/angular/commit/678b420))
|
||||||
|
* **compiler:** Fix look up of entryComponents in AOT Summaries ([#24892](https://github.com/angular/angular/issues/24892)) ([a31cfc5](https://github.com/angular/angular/commit/a31cfc5))
|
||||||
|
* **router:** mount correct component if router outlet was not instantiated and if using a route reuse strategy ([#25313](https://github.com/angular/angular/issues/25313)) ([#25314](https://github.com/angular/angular/issues/25314)) ([e117b1f](https://github.com/angular/angular/commit/e117b1f))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **bazel:** add additional parameters to `ts_api_guardian_test` def ([#25694](https://github.com/angular/angular/issues/25694)) ([97ae7ae](https://github.com/angular/angular/commit/97ae7ae))
|
||||||
|
* **ivy:** enable .ngfactory.js generation in g3 only ([#25392](https://github.com/angular/angular/issues/25392)) ([1c44b71](https://github.com/angular/angular/commit/1c44b71))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<a name="6.1.7"></a>
|
||||||
|
## [6.1.7](https://github.com/angular/angular/compare/6.1.6...6.1.7) (2018-09-06)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **bazel:** protractor rule should include *.e2e-spec.js ([#25701](https://github.com/angular/angular/issues/25701)) ([ed6b68b](https://github.com/angular/angular/commit/ed6b68b))
|
||||||
|
* **core:** size regression with closure compiler ([#25531](https://github.com/angular/angular/issues/25531)) ([ebcf762](https://github.com/angular/angular/commit/ebcf762))
|
||||||
|
* **docs-infra:** show "suggest edits" only for /guide and /tutorial dirs ([#24378](https://github.com/angular/angular/issues/24378)) ([66b7870](https://github.com/angular/angular/commit/66b7870))
|
||||||
|
* **upgrade:** trigger `$destroy` event on upgraded component element ([#25357](https://github.com/angular/angular/issues/25357)) ([82e0676](https://github.com/angular/angular/commit/82e0676)), closes [#25334](https://github.com/angular/angular/issues/25334)
|
||||||
|
* **router:** warn if navigation triggered outside Angular zone ([#24959](https://github.com/angular/angular/issues/24959)) ([23a96dc](https://github.com/angular/angular/commit/23a96dc)), closes [#15770](https://github.com/angular/angular/issues/15770) [#15946](https://github.com/angular/angular/issues/15946) [#24728](https://github.com/angular/angular/issues/24728)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<a name="6.1.6"></a>
|
||||||
|
## [6.1.6](https://github.com/angular/angular/compare/6.1.5...6.1.6) (2018-08-29)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **bazel:** Cache fileNameToModuleName lookups ([#25731](https://github.com/angular/angular/issues/25731)) ([3e690e0](https://github.com/angular/angular/commit/3e690e0))
|
||||||
|
* **bazel:** only lookup amd module-name tags in .d.ts files ([#25710](https://github.com/angular/angular/issues/25710)) ([7aff364](https://github.com/angular/angular/commit/7aff364))
|
||||||
|
|
||||||
|
|
||||||
|
Note: the 6.1.5 release on npm accidentally glitched-out midway, so we cut 6.1.6 instead. sorry! :-)
|
||||||
|
|
||||||
<a name="6.1.4"></a>
|
<a name="6.1.4"></a>
|
||||||
## [6.1.4](https://github.com/angular/angular/compare/6.1.3...6.1.4) (2018-08-22)
|
## [6.1.4](https://github.com/angular/angular/compare/6.1.3...6.1.4) (2018-08-22)
|
||||||
|
|
||||||
|
@ -71,6 +71,8 @@ Before you submit your Pull Request (PR) consider the following guidelines:
|
|||||||
|
|
||||||
1. Search [GitHub](https://github.com/angular/angular/pulls) for an open or closed PR
|
1. Search [GitHub](https://github.com/angular/angular/pulls) for an open or closed PR
|
||||||
that relates to your submission. You don't want to duplicate effort.
|
that relates to your submission. You don't want to duplicate effort.
|
||||||
|
1. Be sure that an issue describes the problem you're fixing, or documents the design for the feature you'd like to add.
|
||||||
|
Discussing the design up front helps to ensure that we're ready to accept your work.
|
||||||
1. Please sign our [Contributor License Agreement (CLA)](#cla) before sending PRs.
|
1. Please sign our [Contributor License Agreement (CLA)](#cla) before sending PRs.
|
||||||
We cannot accept code without this. Make sure you sign with the primary email address of the Git identity that has been granted access to the Angular repository.
|
We cannot accept code without this. Make sure you sign with the primary email address of the Git identity that has been granted access to the Angular repository.
|
||||||
1. Fork the angular/angular repo.
|
1. Fork the angular/angular repo.
|
||||||
|
102
WORKSPACE
102
WORKSPACE
@ -3,59 +3,58 @@ workspace(name = "angular")
|
|||||||
#
|
#
|
||||||
# Download Bazel toolchain dependencies as needed by build actions
|
# Download Bazel toolchain dependencies as needed by build actions
|
||||||
#
|
#
|
||||||
|
|
||||||
http_archive(
|
|
||||||
name = "build_bazel_rules_nodejs",
|
|
||||||
urls = ["https://github.com/bazelbuild/rules_nodejs/archive/0.11.4.zip"],
|
|
||||||
strip_prefix = "rules_nodejs-0.11.4",
|
|
||||||
sha256 = "c31c4ead696944a50fad2b3ee9dfbbeffe31a8dcca0b21b9bf5b3e6c6b069801",
|
|
||||||
)
|
|
||||||
|
|
||||||
http_archive(
|
|
||||||
name = "bazel_skylib",
|
|
||||||
urls = ["https://github.com/bazelbuild/bazel-skylib/archive/0.3.1.zip"],
|
|
||||||
strip_prefix = "bazel-skylib-0.3.1",
|
|
||||||
sha256 = "95518adafc9a2b656667bbf517a952e54ce7f350779d0dd95133db4eb5c27fb1",
|
|
||||||
)
|
|
||||||
|
|
||||||
http_archive(
|
|
||||||
name = "io_bazel_rules_webtesting",
|
|
||||||
url = "https://github.com/bazelbuild/rules_webtesting/archive/0.2.1.zip",
|
|
||||||
strip_prefix = "rules_webtesting-0.2.1",
|
|
||||||
sha256 = "7d490aadff9b5262e5251fa69427ab2ffd1548422467cb9f9e1d110e2c36f0fa",
|
|
||||||
)
|
|
||||||
|
|
||||||
http_archive(
|
http_archive(
|
||||||
name = "build_bazel_rules_typescript",
|
name = "build_bazel_rules_typescript",
|
||||||
url = "https://github.com/bazelbuild/rules_typescript/archive/0.16.0.zip",
|
url = "https://github.com/bazelbuild/rules_typescript/archive/0.17.0.zip",
|
||||||
strip_prefix = "rules_typescript-0.16.0",
|
strip_prefix = "rules_typescript-0.17.0",
|
||||||
sha256 = "e65c5639a42e2f6d3f9d2bda62487d6b42734830dda45be1620c3e2b1115070c",
|
sha256 = "1626ee2cc9770af6950bfc77dffa027f9aedf330fe2ea2ee7e504428927bd95d",
|
||||||
|
)
|
||||||
|
load("@build_bazel_rules_typescript//:package.bzl", "rules_typescript_dependencies")
|
||||||
|
rules_typescript_dependencies()
|
||||||
|
|
||||||
|
http_archive(
|
||||||
|
name = "bazel_toolchains",
|
||||||
|
urls = [
|
||||||
|
"https://mirror.bazel.build/github.com/bazelbuild/bazel-toolchains/archive/5124557861ebf4c0b67f98180bff1f8551e0b421.tar.gz",
|
||||||
|
"https://github.com/bazelbuild/bazel-toolchains/archive/5124557861ebf4c0b67f98180bff1f8551e0b421.tar.gz",
|
||||||
|
],
|
||||||
|
strip_prefix = "bazel-toolchains-5124557861ebf4c0b67f98180bff1f8551e0b421",
|
||||||
|
sha256 = "c3b08805602cd1d2b67ebe96407c1e8c6ed3d4ce55236ae2efe2f1948f38168d",
|
||||||
)
|
)
|
||||||
|
|
||||||
http_archive(
|
http_archive(
|
||||||
name = "io_bazel_rules_go",
|
name = "io_bazel_rules_sass",
|
||||||
url = "https://github.com/bazelbuild/rules_go/releases/download/0.10.3/rules_go-0.10.3.tar.gz",
|
url = "https://github.com/bazelbuild/rules_sass/archive/1.11.0.zip",
|
||||||
sha256 = "feba3278c13cde8d67e341a837f69a029f698d7a27ddbb2a202be7a10b22142a",
|
strip_prefix = "rules_sass-1.11.0",
|
||||||
|
sha256 = "dbe9fb97d5a7833b2a733eebc78c9c1e3880f676ac8af16e58ccf2139cbcad03",
|
||||||
)
|
)
|
||||||
|
|
||||||
# This commit matches the version of buildifier in angular/ngcontainer
|
# 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
|
# If you change this, also check if it matches the version in the angular/ngcontainer
|
||||||
# version in /.circleci/config.yml
|
# version in /.circleci/config.yml
|
||||||
BAZEL_BUILDTOOLS_VERSION = "82b21607e00913b16fe1c51bec80232d9d6de31c"
|
BAZEL_BUILDTOOLS_VERSION = "49a6c199e3fbf5d94534b2771868677d3f9c6de9"
|
||||||
|
|
||||||
http_archive(
|
http_archive(
|
||||||
name = "com_github_bazelbuild_buildtools",
|
name = "com_github_bazelbuild_buildtools",
|
||||||
url = "https://github.com/bazelbuild/buildtools/archive/%s.zip" % BAZEL_BUILDTOOLS_VERSION,
|
url = "https://github.com/bazelbuild/buildtools/archive/%s.zip" % BAZEL_BUILDTOOLS_VERSION,
|
||||||
strip_prefix = "buildtools-%s" % BAZEL_BUILDTOOLS_VERSION,
|
strip_prefix = "buildtools-%s" % BAZEL_BUILDTOOLS_VERSION,
|
||||||
sha256 = "edb24c2f9c55b10a820ec74db0564415c0cf553fa55e9fc709a6332fb6685eff",
|
sha256 = "edf39af5fc257521e4af4c40829fffe8fba6d0ebff9f4dd69a6f8f1223ae047b",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Fetching the Bazel source code allows us to compile the Skylark linter
|
# Fetching the Bazel source code allows us to compile the Skylark linter
|
||||||
http_archive(
|
http_archive(
|
||||||
name = "io_bazel",
|
name = "io_bazel",
|
||||||
url = "https://github.com/bazelbuild/bazel/archive/968f87900dce45a7af749a965b72dbac51b176b3.zip",
|
url = "https://github.com/bazelbuild/bazel/archive/0.17.1.zip",
|
||||||
strip_prefix = "bazel-968f87900dce45a7af749a965b72dbac51b176b3",
|
strip_prefix = "bazel-0.17.1",
|
||||||
sha256 = "e373d2ae24955c1254c495c9c421c009d88966565c35e4e8444c082cb1f0f48f",
|
sha256 = "ace8cced3b21e64a8fdad68508e9b0644201ec848ad583651719841d567fc66d",
|
||||||
|
)
|
||||||
|
|
||||||
|
http_archive(
|
||||||
|
name = "io_bazel_skydoc",
|
||||||
|
# TODO: switch to upstream when https://github.com/bazelbuild/skydoc/pull/103 is merged
|
||||||
|
url = "https://github.com/alexeagle/skydoc/archive/fe2e9f888d28e567fef62ec9d4a93c425526d701.zip",
|
||||||
|
strip_prefix = "skydoc-fe2e9f888d28e567fef62ec9d4a93c425526d701",
|
||||||
|
sha256 = "7bfb5545f59792a2745f2523b9eef363f9c3e7274791c030885e7069f8116016",
|
||||||
)
|
)
|
||||||
|
|
||||||
# We have a source dependency on the Devkit repository, because it's built with
|
# We have a source dependency on the Devkit repository, because it's built with
|
||||||
@ -73,9 +72,9 @@ http_archive(
|
|||||||
|
|
||||||
http_archive(
|
http_archive(
|
||||||
name = "org_brotli",
|
name = "org_brotli",
|
||||||
url = "https://github.com/google/brotli/archive/f9b8c02673c576a3e807edbf3a9328e9e7af6d7c.zip",
|
url = "https://github.com/google/brotli/archive/v1.0.5.zip",
|
||||||
strip_prefix = "brotli-f9b8c02673c576a3e807edbf3a9328e9e7af6d7c",
|
strip_prefix = "brotli-1.0.5",
|
||||||
sha256 = "8a517806d2b7c8505ba5c53934e7d7c70d341b68ffd268e9044d35b564a48828",
|
sha256 = "774b893a0700b0692a76e2e5b7e7610dbbe330ffbe3fe864b4b52ca718061d5a",
|
||||||
)
|
)
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -98,12 +97,18 @@ local_repository(
|
|||||||
# Load and install our dependencies downloaded above.
|
# Load and install our dependencies downloaded above.
|
||||||
#
|
#
|
||||||
|
|
||||||
load("@build_bazel_rules_nodejs//:defs.bzl", "check_bazel_version", "node_repositories", "yarn_install")
|
load("@build_bazel_rules_nodejs//:defs.bzl", "check_bazel_version", "node_repositories")
|
||||||
|
|
||||||
check_bazel_version("0.15.0")
|
check_bazel_version("0.17.0", """
|
||||||
|
If you are on a Mac and using Homebrew, there is a breaking change to the installation in Bazel 0.16
|
||||||
|
See https://blog.bazel.build/2018/08/22/bazel-homebrew.html
|
||||||
|
|
||||||
|
""")
|
||||||
node_repositories(
|
node_repositories(
|
||||||
package_json = ["//:package.json"],
|
package_json = ["//:package.json"],
|
||||||
preserve_symlinks = True,
|
preserve_symlinks = True,
|
||||||
|
node_version = "10.9.0",
|
||||||
|
yarn_version = "1.9.2",
|
||||||
)
|
)
|
||||||
|
|
||||||
load("@io_bazel_rules_go//go:def.bzl", "go_rules_dependencies", "go_register_toolchains")
|
load("@io_bazel_rules_go//go:def.bzl", "go_rules_dependencies", "go_register_toolchains")
|
||||||
@ -127,20 +132,11 @@ load("@angular//:index.bzl", "ng_setup_workspace")
|
|||||||
|
|
||||||
ng_setup_workspace()
|
ng_setup_workspace()
|
||||||
|
|
||||||
#
|
##################################
|
||||||
# Ask Bazel to manage these toolchain dependencies for us.
|
# Skylark documentation generation
|
||||||
# Bazel will run `yarn install` when one of these toolchains is requested during
|
|
||||||
# a build.
|
|
||||||
#
|
|
||||||
|
|
||||||
yarn_install(
|
load("@io_bazel_rules_sass//sass:sass_repositories.bzl", "sass_repositories")
|
||||||
name = "ts-api-guardian_runtime_deps",
|
sass_repositories()
|
||||||
package_json = "//tools/ts-api-guardian:package.json",
|
|
||||||
yarn_lock = "//tools/ts-api-guardian:yarn.lock",
|
|
||||||
)
|
|
||||||
|
|
||||||
yarn_install(
|
load("@io_bazel_skydoc//skylark:skylark.bzl", "skydoc_repositories")
|
||||||
name = "http-server_runtime_deps",
|
skydoc_repositories()
|
||||||
package_json = "//tools/http-server:package.json",
|
|
||||||
yarn_lock = "//tools/http-server:yarn.lock",
|
|
||||||
)
|
|
||||||
|
@ -22,8 +22,8 @@ Here are the most important tasks you might need to use:
|
|||||||
* `yarn start` - run a development web server that watches the files; then builds the doc-viewer and reloads the page, as necessary.
|
* `yarn start` - run a development web server that watches the files; then builds the doc-viewer and reloads the page, as necessary.
|
||||||
* `yarn serve-and-sync` - run both the `docs-watch` and `start` in the same console.
|
* `yarn serve-and-sync` - run both the `docs-watch` and `start` in the same console.
|
||||||
* `yarn lint` - check that the doc-viewer code follows our style rules.
|
* `yarn lint` - check that the doc-viewer code follows our style rules.
|
||||||
* `yarn test` - run all the unit tests once.
|
* `yarn test` - watch all the source files, for the doc-viewer, and run all the unit tests when any change.
|
||||||
* `yarn test --watch` - watch all the source files, for the doc-viewer, and run all the unit tests when any change.
|
* `yarn test --watch=false` - run all the unit tests once.
|
||||||
* `yarn e2e` - run all the e2e tests for the doc-viewer.
|
* `yarn e2e` - run all the e2e tests for the doc-viewer.
|
||||||
|
|
||||||
* `yarn docs` - generate all the docs from the source files.
|
* `yarn docs` - generate all the docs from the source files.
|
||||||
@ -56,14 +56,9 @@ It's necessary to remove the temporary files, because otherwise they're displaye
|
|||||||
|
|
||||||
## Using ServiceWorker locally
|
## Using ServiceWorker locally
|
||||||
|
|
||||||
Since abb36e3cb, running `yarn start --prod` will no longer set up the ServiceWorker, which
|
Running `yarn start` (even when explicitly targeting production mode) does not set up the
|
||||||
would require manually running `yarn sw-manifest` and `yarn sw-copy` (something that is not possible
|
ServiceWorker. If you want to test the ServiceWorker locally, you can use `yarn build` and then
|
||||||
with webpack serving the files from memory).
|
serve the files in `dist/` with `yarn http-server dist -p 4200`.
|
||||||
|
|
||||||
If you want to test ServiceWorker locally, you can use `yarn build` and serve the files in `dist/`
|
|
||||||
with `yarn http-server dist -p 4200`.
|
|
||||||
|
|
||||||
For more details see #16745.
|
|
||||||
|
|
||||||
|
|
||||||
## Guide to authoring
|
## Guide to authoring
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
# Periodically clean up builds that do not correspond to currently open PRs
|
# Periodically clean up builds that do not correspond to currently open PRs
|
||||||
0 12 * * * root /usr/local/bin/aio-clean-up >> /var/log/cron.log 2>&1
|
0 12 * * * /usr/local/bin/aio-clean-up >> /var/log/cron.log 2>&1
|
||||||
|
@ -36,6 +36,11 @@ server {
|
|||||||
access_log {{$AIO_NGINX_LOGS_DIR}}/access.log;
|
access_log {{$AIO_NGINX_LOGS_DIR}}/access.log;
|
||||||
error_log {{$AIO_NGINX_LOGS_DIR}}/error.log;
|
error_log {{$AIO_NGINX_LOGS_DIR}}/error.log;
|
||||||
|
|
||||||
|
error_page 404 /404.html;
|
||||||
|
location "=/404.html" {
|
||||||
|
internal;
|
||||||
|
}
|
||||||
|
|
||||||
location "~/[^/]+\.[^/]+$" {
|
location "~/[^/]+\.[^/]+$" {
|
||||||
try_files $uri $uri/ =404;
|
try_files $uri $uri/ =404;
|
||||||
}
|
}
|
||||||
@ -66,6 +71,21 @@ server {
|
|||||||
return 200 '';
|
return 200 '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Check PRs previewability
|
||||||
|
location "~^/can-have-public-preview/\d+/?$" {
|
||||||
|
if ($request_method != "GET") {
|
||||||
|
add_header Allow "GET";
|
||||||
|
return 405;
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy_pass_request_headers on;
|
||||||
|
proxy_redirect off;
|
||||||
|
proxy_method GET;
|
||||||
|
proxy_pass http://{{$AIO_PREVIEW_SERVER_HOSTNAME}}:{{$AIO_PREVIEW_SERVER_PORT}}$request_uri;
|
||||||
|
|
||||||
|
resolver 127.0.0.1;
|
||||||
|
}
|
||||||
|
|
||||||
# Notify about CircleCI builds
|
# Notify about CircleCI builds
|
||||||
location "~^/circle-build/?$" {
|
location "~^/circle-build/?$" {
|
||||||
if ($request_method != "POST") {
|
if ($request_method != "POST") {
|
||||||
|
@ -5,12 +5,12 @@ import * as shell from 'shelljs';
|
|||||||
import {HIDDEN_DIR_PREFIX} from '../common/constants';
|
import {HIDDEN_DIR_PREFIX} from '../common/constants';
|
||||||
import {GithubApi} from '../common/github-api';
|
import {GithubApi} from '../common/github-api';
|
||||||
import {GithubPullRequests} from '../common/github-pull-requests';
|
import {GithubPullRequests} from '../common/github-pull-requests';
|
||||||
import {assertNotMissingOrEmpty, createLogger, getPrInfoFromDownloadPath} from '../common/utils';
|
import {assertNotMissingOrEmpty, getPrInfoFromDownloadPath, Logger} from '../common/utils';
|
||||||
|
|
||||||
// Classes
|
// Classes
|
||||||
export class BuildCleaner {
|
export class BuildCleaner {
|
||||||
|
|
||||||
private logger = createLogger('BuildCleaner');
|
private logger = new Logger('BuildCleaner');
|
||||||
|
|
||||||
// Constructor
|
// Constructor
|
||||||
constructor(protected buildsDir: string, protected githubOrg: string, protected githubRepo: string,
|
constructor(protected buildsDir: string, protected githubOrg: string, protected githubRepo: string,
|
||||||
@ -122,6 +122,6 @@ export class BuildCleaner {
|
|||||||
this.logger.log(`Existing downloads: ${existingDownloads.length}`);
|
this.logger.log(`Existing downloads: ${existingDownloads.length}`);
|
||||||
this.logger.log(`Removing ${toRemove.length} download(s): ${toRemove.join(', ')}`);
|
this.logger.log(`Removing ${toRemove.length} download(s): ${toRemove.join(', ')}`);
|
||||||
|
|
||||||
toRemove.forEach(filePath => shell.rm(filePath));
|
toRemove.forEach(filePath => shell.rm(path.join(this.downloadsDir, filePath)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -62,7 +62,7 @@ export class CircleCiApi {
|
|||||||
if (response.status !== 200) {
|
if (response.status !== 200) {
|
||||||
throw new Error(`${baseUrl}: ${response.status} - ${response.statusText}`);
|
throw new Error(`${baseUrl}: ${response.status} - ${response.statusText}`);
|
||||||
}
|
}
|
||||||
return response.json<BuildInfo>();
|
return response.json();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`CircleCI build info request failed (${error.message})`);
|
throw new Error(`CircleCI build info request failed (${error.message})`);
|
||||||
}
|
}
|
||||||
@ -77,7 +77,7 @@ export class CircleCiApi {
|
|||||||
const baseUrl = `${CIRCLE_CI_API_URL}/${this.githubOrg}/${this.githubRepo}/${buildNumber}`;
|
const baseUrl = `${CIRCLE_CI_API_URL}/${this.githubOrg}/${this.githubRepo}/${buildNumber}`;
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${baseUrl}/artifacts?${this.tokenParam}`);
|
const response = await fetch(`${baseUrl}/artifacts?${this.tokenParam}`);
|
||||||
const artifacts = await response.json<ArtifactResponse>();
|
const artifacts = await response.json() as ArtifactResponse;
|
||||||
const artifact = artifacts.find(item => item.path === artifactPath);
|
const artifact = artifacts.find(item => item.path === artifactPath);
|
||||||
if (!artifact) {
|
if (!artifact) {
|
||||||
throw new Error(`Missing artifact (${artifactPath}) for CircleCI build: ${buildNumber}`);
|
throw new Error(`Missing artifact (${artifactPath}) for CircleCI build: ${buildNumber}`);
|
||||||
|
@ -38,7 +38,8 @@ export class GithubApi {
|
|||||||
return this.request<T>('post', path, data);
|
return this.request<T>('post', path, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getPaginated<T>(pathname: string, baseParams: RequestParams = {}, currentPage: number = 0): Promise<T[]> {
|
// In GitHub API paginated requests, page numbering is 1-based. (https://developer.github.com/v3/#pagination)
|
||||||
|
public getPaginated<T>(pathname: string, baseParams: RequestParams = {}, currentPage: number = 1): Promise<T[]> {
|
||||||
const perPage = 100;
|
const perPage = 100;
|
||||||
const params = {
|
const params = {
|
||||||
...baseParams,
|
...baseParams,
|
||||||
|
@ -74,6 +74,6 @@ export class GithubPullRequests {
|
|||||||
*/
|
*/
|
||||||
public fetchFiles(pr: number): Promise<FileInfo[]> {
|
public fetchFiles(pr: number): Promise<FileInfo[]> {
|
||||||
assert(pr > 0, `Invalid PR number: ${pr}`);
|
assert(pr > 0, `Invalid PR number: ${pr}`);
|
||||||
return this.api.get<FileInfo[]>(`/repos/${this.repoSlug}/pulls/${pr}/files`);
|
return this.api.getPaginated<FileInfo>(`/repos/${this.repoSlug}/pulls/${pr}/files`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
export const runTests = (specFiles: string[], helpers?: string[]) => {
|
// We can't use `import...from` here, because of the following mess:
|
||||||
// We can't use `import` here, because of the following mess:
|
// - GitHub project `jasmine/jasmine` is `jasmine-core` on npm and its typings `@types/jasmine`.
|
||||||
// - GitHub project `jasmine/jasmine` is `jasmine-core` on npm and its typings `@types/jasmine`.
|
// - GitHub project `jasmine/jasmine-npm` is `jasmine` on npm and has no typings.
|
||||||
// - GitHub project `jasmine/jasmine-npm` is `jasmine` on npm and has no typings.
|
//
|
||||||
//
|
// Using `import...from 'jasmine'` here, would import from `@types/jasmine` (which refers to the
|
||||||
// Using `import...from 'jasmine'` here, would import from `@types/jasmine` (which refers to the
|
// `jasmine-core` module and the `jasmine` module).
|
||||||
// `jasmine-core` module and the `jasmine` module).
|
import Jasmine = require('jasmine');
|
||||||
// tslint:disable-next-line: no-var-requires variable-name
|
import 'source-map-support/register';
|
||||||
const Jasmine = require('jasmine');
|
|
||||||
|
export const runTests = (specFiles: string[]) => {
|
||||||
const config = {
|
const config = {
|
||||||
helpers,
|
|
||||||
random: true,
|
random: true,
|
||||||
spec_files: specFiles,
|
spec_files: specFiles,
|
||||||
stopSpecOnExpectationFailure: true,
|
stopSpecOnExpectationFailure: true,
|
||||||
@ -16,7 +16,7 @@ export const runTests = (specFiles: string[], helpers?: string[]) => {
|
|||||||
|
|
||||||
process.on('unhandledRejection', (reason: any) => console.log('Unhandled rejection:', reason));
|
process.on('unhandledRejection', (reason: any) => console.log('Unhandled rejection:', reason));
|
||||||
|
|
||||||
const runner = new Jasmine();
|
const runner = new Jasmine({});
|
||||||
runner.loadConfig(config);
|
runner.loadConfig(config);
|
||||||
runner.onComplete((passed: boolean) => process.exit(passed ? 0 : 1));
|
runner.onComplete((passed: boolean) => process.exit(passed ? 0 : 1));
|
||||||
runner.execute();
|
runner.execute();
|
||||||
|
@ -74,12 +74,25 @@ export const getEnvVar = (name: string, isOptional = false): string => {
|
|||||||
return value || '';
|
return value || '';
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createLogger(scope: string) {
|
/**
|
||||||
const padding = ' '.repeat(20 - scope.length);
|
* A basic logger implementation.
|
||||||
return {
|
* Delegates to `console`, but prepends each message with the current date and specified scope (i.e caller).
|
||||||
error: (...args: any[]) => console.error(`[${new Date()}]`, `${scope}:${padding}`, ...args),
|
*/
|
||||||
info: (...args: any[]) => console.info(`[${new Date()}]`, `${scope}:${padding}`, ...args),
|
export class Logger {
|
||||||
log: (...args: any[]) => console.log(`[${new Date()}]`, `${scope}:${padding}`, ...args),
|
private padding = ' '.repeat(20 - this.scope.length);
|
||||||
warn: (...args: any[]) => console.warn(`[${new Date()}]`, `${scope}:${padding}`, ...args),
|
|
||||||
};
|
/**
|
||||||
|
* Create a new `Logger` instance for the specified `scope`.
|
||||||
|
* @param scope The logger's scope (added to all messages).
|
||||||
|
*/
|
||||||
|
constructor(private scope: string) {}
|
||||||
|
|
||||||
|
public error(...args: any[]) { this.callMethod('error', args); }
|
||||||
|
public info(...args: any[]) { this.callMethod('info', args); }
|
||||||
|
public log(...args: any[]) { this.callMethod('log', args); }
|
||||||
|
public warn(...args: any[]) { this.callMethod('warn', args); }
|
||||||
|
|
||||||
|
private callMethod(method: 'error' | 'info' | 'log' | 'warn', args: any[]) {
|
||||||
|
console[method](`[${new Date()}]`, `${this.scope}:${this.padding}`, ...args);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,14 +5,14 @@ import * as fs from 'fs';
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as shell from 'shelljs';
|
import * as shell from 'shelljs';
|
||||||
import {HIDDEN_DIR_PREFIX} from '../common/constants';
|
import {HIDDEN_DIR_PREFIX} from '../common/constants';
|
||||||
import {assertNotMissingOrEmpty, computeShortSha, createLogger} from '../common/utils';
|
import {assertNotMissingOrEmpty, computeShortSha, Logger} from '../common/utils';
|
||||||
import {ChangedPrVisibilityEvent, CreatedBuildEvent} from './build-events';
|
import {ChangedPrVisibilityEvent, CreatedBuildEvent} from './build-events';
|
||||||
import {PreviewServerError} from './preview-error';
|
import {PreviewServerError} from './preview-error';
|
||||||
|
|
||||||
// Classes
|
// Classes
|
||||||
export class BuildCreator extends EventEmitter {
|
export class BuildCreator extends EventEmitter {
|
||||||
|
|
||||||
private logger = createLogger('BuildCreator');
|
private logger = new Logger('BuildCreator');
|
||||||
|
|
||||||
// Constructor
|
// Constructor
|
||||||
constructor(protected buildsDir: string) {
|
constructor(protected buildsDir: string) {
|
||||||
|
@ -4,7 +4,7 @@ import {dirname} from 'path';
|
|||||||
import {mkdir} from 'shelljs';
|
import {mkdir} from 'shelljs';
|
||||||
import {promisify} from 'util';
|
import {promisify} from 'util';
|
||||||
import {CircleCiApi} from '../common/circle-ci-api';
|
import {CircleCiApi} from '../common/circle-ci-api';
|
||||||
import {assert, assertNotMissingOrEmpty, computeArtifactDownloadPath, createLogger} from '../common/utils';
|
import {assert, assertNotMissingOrEmpty, computeArtifactDownloadPath, Logger} from '../common/utils';
|
||||||
import {PreviewServerError} from './preview-error';
|
import {PreviewServerError} from './preview-error';
|
||||||
|
|
||||||
export interface GithubInfo {
|
export interface GithubInfo {
|
||||||
@ -19,7 +19,7 @@ export interface GithubInfo {
|
|||||||
* A helper that can get information about builds and download build artifacts.
|
* A helper that can get information about builds and download build artifacts.
|
||||||
*/
|
*/
|
||||||
export class BuildRetriever {
|
export class BuildRetriever {
|
||||||
private logger = createLogger('BuildRetriever');
|
private logger = new Logger('BuildRetriever');
|
||||||
constructor(private api: CircleCiApi, private downloadSizeLimit: number, private downloadDir: string) {
|
constructor(private api: CircleCiApi, private downloadSizeLimit: number, private downloadDir: string) {
|
||||||
assert(downloadSizeLimit > 0, 'Invalid parameter "downloadSizeLimit" should be a number greater than 0.');
|
assert(downloadSizeLimit > 0, 'Invalid parameter "downloadSizeLimit" should be a number greater than 0.');
|
||||||
assertNotMissingOrEmpty('downloadDir', downloadDir);
|
assertNotMissingOrEmpty('downloadDir', downloadDir);
|
||||||
@ -34,7 +34,7 @@ export class BuildRetriever {
|
|||||||
const buildInfo = await this.api.getBuildInfo(buildNum);
|
const buildInfo = await this.api.getBuildInfo(buildNum);
|
||||||
const githubInfo: GithubInfo = {
|
const githubInfo: GithubInfo = {
|
||||||
org: buildInfo.username,
|
org: buildInfo.username,
|
||||||
pr: getPrfromBranch(buildInfo.branch),
|
pr: getPrFromBranch(buildInfo.branch),
|
||||||
repo: buildInfo.reponame,
|
repo: buildInfo.reponame,
|
||||||
sha: buildInfo.vcs_revision,
|
sha: buildInfo.vcs_revision,
|
||||||
success: !buildInfo.failed,
|
success: !buildInfo.failed,
|
||||||
@ -73,7 +73,7 @@ export class BuildRetriever {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPrfromBranch(branch: string): number {
|
function getPrFromBranch(branch: string): number {
|
||||||
// CircleCI only exposes PR numbers via the `branch` field :-(
|
// CircleCI only exposes PR numbers via the `branch` field :-(
|
||||||
const match = /^pull\/(\d+)$/.exec(branch);
|
const match = /^pull\/(\d+)$/.exec(branch);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
|
@ -2,11 +2,12 @@
|
|||||||
import * as bodyParser from 'body-parser';
|
import * as bodyParser from 'body-parser';
|
||||||
import * as express from 'express';
|
import * as express from 'express';
|
||||||
import * as http from 'http';
|
import * as http from 'http';
|
||||||
|
import {AddressInfo} from 'net';
|
||||||
import {CircleCiApi} from '../common/circle-ci-api';
|
import {CircleCiApi} from '../common/circle-ci-api';
|
||||||
import {GithubApi} from '../common/github-api';
|
import {GithubApi} from '../common/github-api';
|
||||||
import {GithubPullRequests} from '../common/github-pull-requests';
|
import {GithubPullRequests} from '../common/github-pull-requests';
|
||||||
import {GithubTeams} from '../common/github-teams';
|
import {GithubTeams} from '../common/github-teams';
|
||||||
import {assert, assertNotMissingOrEmpty, createLogger} from '../common/utils';
|
import {assert, assertNotMissingOrEmpty, Logger} from '../common/utils';
|
||||||
import {BuildCreator} from './build-creator';
|
import {BuildCreator} from './build-creator';
|
||||||
import {ChangedPrVisibilityEvent, CreatedBuildEvent} from './build-events';
|
import {ChangedPrVisibilityEvent, CreatedBuildEvent} from './build-events';
|
||||||
import {BuildRetriever} from './build-retriever';
|
import {BuildRetriever} from './build-retriever';
|
||||||
@ -31,7 +32,7 @@ export interface PreviewServerConfig {
|
|||||||
trustedPrLabel: string;
|
trustedPrLabel: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const logger = createLogger('PreviewServer');
|
const logger = new Logger('PreviewServer');
|
||||||
|
|
||||||
// Classes
|
// Classes
|
||||||
export class PreviewServerFactory {
|
export class PreviewServerFactory {
|
||||||
@ -52,7 +53,7 @@ export class PreviewServerFactory {
|
|||||||
const httpServer = http.createServer(middleware as any);
|
const httpServer = http.createServer(middleware as any);
|
||||||
|
|
||||||
httpServer.on('listening', () => {
|
httpServer.on('listening', () => {
|
||||||
const info = httpServer.address();
|
const info = httpServer.address() as AddressInfo;
|
||||||
logger.info(`Up and running (and listening on ${info.address}:${info.port})...`);
|
logger.info(`Up and running (and listening on ${info.address}:${info.port})...`);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -63,10 +64,36 @@ export class PreviewServerFactory {
|
|||||||
buildCreator: BuildCreator, cfg: PreviewServerConfig): express.Express {
|
buildCreator: BuildCreator, cfg: PreviewServerConfig): express.Express {
|
||||||
const middleware = express();
|
const middleware = express();
|
||||||
const jsonParser = bodyParser.json();
|
const jsonParser = bodyParser.json();
|
||||||
|
const significantFilesRe = new RegExp(cfg.significantFilesPattern);
|
||||||
|
|
||||||
// RESPOND TO IS-ALIVE PING
|
// RESPOND TO IS-ALIVE PING
|
||||||
middleware.get(/^\/health-check\/?$/, (_req, res) => res.sendStatus(200));
|
middleware.get(/^\/health-check\/?$/, (_req, res) => res.sendStatus(200));
|
||||||
|
|
||||||
|
// RESPOND TO CAN-HAVE-PUBLIC-PREVIEW CHECK
|
||||||
|
const canHavePublicPreviewRe = /^\/can-have-public-preview\/(\d+)\/?$/;
|
||||||
|
middleware.get(canHavePublicPreviewRe, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const pr = +canHavePublicPreviewRe.exec(req.url)![1];
|
||||||
|
|
||||||
|
if (!await buildVerifier.getSignificantFilesChanged(pr, significantFilesRe)) {
|
||||||
|
// Cannot have preview: PR did not touch relevant files: `aio/` or `packages/` (except for spec files).
|
||||||
|
res.send({canHavePublicPreview: false, reason: 'No significant files touched.'});
|
||||||
|
logger.log(`PR:${pr} - Cannot have a public preview, because it did not touch any significant files.`);
|
||||||
|
} else if (!await buildVerifier.getPrIsTrusted(pr)) {
|
||||||
|
// Cannot have preview: PR not automatically verifiable as "trusted".
|
||||||
|
res.send({canHavePublicPreview: false, reason: 'Not automatically verifiable as "trusted".'});
|
||||||
|
logger.log(`PR:${pr} - Cannot have a public preview, because not automatically verifiable as "trusted".`);
|
||||||
|
} else {
|
||||||
|
// Can have preview.
|
||||||
|
res.send({canHavePublicPreview: true, reason: null});
|
||||||
|
logger.log(`PR:${pr} - Can have a public preview.`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('Previewability check error', err);
|
||||||
|
respondWithError(res, err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// CIRCLE_CI BUILD COMPLETE WEBHOOK
|
// CIRCLE_CI BUILD COMPLETE WEBHOOK
|
||||||
middleware.post(/^\/circle-build\/?$/, jsonParser, async (req, res) => {
|
middleware.post(/^\/circle-build\/?$/, jsonParser, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
@ -107,7 +134,7 @@ export class PreviewServerFactory {
|
|||||||
`Invalid webhook: expected "githubRepo" property to equal "${cfg.githubRepo}" but got "${repo}".`);
|
`Invalid webhook: expected "githubRepo" property to equal "${cfg.githubRepo}" but got "${repo}".`);
|
||||||
|
|
||||||
// Do not deploy unless this PR has touched relevant files: `aio/` or `packages/` (except for spec files)
|
// Do not deploy unless this PR has touched relevant files: `aio/` or `packages/` (except for spec files)
|
||||||
if (!await buildVerifier.getSignificantFilesChanged(pr, new RegExp(cfg.significantFilesPattern))) {
|
if (!await buildVerifier.getSignificantFilesChanged(pr, significantFilesRe)) {
|
||||||
res.sendStatus(204);
|
res.sendStatus(204);
|
||||||
logger.log(`PR:${pr}, Build:${buildNum} - ` +
|
logger.log(`PR:${pr}, Build:${buildNum} - ` +
|
||||||
`Skipping preview processing because this PR did not touch any significant files.`);
|
`Skipping preview processing because this PR did not touch any significant files.`);
|
||||||
|
@ -11,7 +11,7 @@ import {
|
|||||||
AIO_NGINX_PORT_HTTPS,
|
AIO_NGINX_PORT_HTTPS,
|
||||||
AIO_WWW_USER,
|
AIO_WWW_USER,
|
||||||
} from '../common/env-variables';
|
} from '../common/env-variables';
|
||||||
import {computeShortSha, createLogger} from '../common/utils';
|
import {computeShortSha, Logger} from '../common/utils';
|
||||||
|
|
||||||
// Interfaces - Types
|
// Interfaces - Types
|
||||||
export interface CmdResult { success: boolean; err: Error | null; stdout: string; stderr: string; }
|
export interface CmdResult { success: boolean; err: Error | null; stdout: string; stderr: string; }
|
||||||
@ -31,7 +31,7 @@ class Helper {
|
|||||||
https: AIO_NGINX_PORT_HTTPS,
|
https: AIO_NGINX_PORT_HTTPS,
|
||||||
};
|
};
|
||||||
|
|
||||||
private logger = createLogger('TestHelper');
|
private logger = new Logger('TestHelper');
|
||||||
|
|
||||||
// Constructor
|
// Constructor
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -105,7 +105,7 @@ class Helper {
|
|||||||
Object.keys(this.portPerScheme).forEach(scheme => suiteFactory(scheme, this.portPerScheme[scheme]));
|
Object.keys(this.portPerScheme).forEach(scheme => suiteFactory(scheme, this.portPerScheme[scheme]));
|
||||||
}
|
}
|
||||||
|
|
||||||
public verifyResponse(status: number | [number, string], regex = /^/): VerifyCmdResultFn {
|
public verifyResponse(status: number | [number, string], regex: string | RegExp = /^/): VerifyCmdResultFn {
|
||||||
let statusCode: number;
|
let statusCode: number;
|
||||||
let statusText: string;
|
let statusText: string;
|
||||||
|
|
||||||
@ -180,26 +180,42 @@ class Helper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DefaultCurlOptions {
|
||||||
|
defaultMethod?: CurlOptions['method'];
|
||||||
|
defaultOptions?: CurlOptions['options'];
|
||||||
|
defaultHeaders?: CurlOptions['headers'];
|
||||||
|
defaultData?: CurlOptions['data'];
|
||||||
|
defaultExtraPath?: CurlOptions['extraPath'];
|
||||||
|
}
|
||||||
|
|
||||||
interface CurlOptions {
|
interface CurlOptions {
|
||||||
method?: string;
|
method?: string;
|
||||||
options?: string;
|
options?: string;
|
||||||
|
headers?: string[];
|
||||||
data?: any;
|
data?: any;
|
||||||
url?: string;
|
url?: string;
|
||||||
extraPath?: string;
|
extraPath?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeCurl(baseUrl: string) {
|
export function makeCurl(baseUrl: string, {
|
||||||
|
defaultMethod = 'POST',
|
||||||
|
defaultOptions = '',
|
||||||
|
defaultHeaders = ['Content-Type: application/json'],
|
||||||
|
defaultData = {},
|
||||||
|
defaultExtraPath = '',
|
||||||
|
}: DefaultCurlOptions = {}) {
|
||||||
return function curl({
|
return function curl({
|
||||||
method = 'POST',
|
method = defaultMethod,
|
||||||
options = '',
|
options = defaultOptions,
|
||||||
data = {},
|
headers = defaultHeaders,
|
||||||
|
data = defaultData,
|
||||||
url = baseUrl,
|
url = baseUrl,
|
||||||
extraPath = '',
|
extraPath = defaultExtraPath,
|
||||||
}: CurlOptions) {
|
}: CurlOptions) {
|
||||||
const dataString = data ? JSON.stringify(data) : '';
|
const dataString = data ? JSON.stringify(data) : '';
|
||||||
const cmd = `curl -iLX ${method} ` +
|
const cmd = `curl -iLX ${method} ` +
|
||||||
`${options} ` +
|
`${options} ` +
|
||||||
`--header "Content-Type: application/json" ` +
|
headers.map(header => `--header "${header}" `).join('') +
|
||||||
`--data '${dataString}' ` +
|
`--data '${dataString}' ` +
|
||||||
`${url}${extraPath}`;
|
`${url}${extraPath}`;
|
||||||
return helper.runCmd(cmd);
|
return helper.runCmd(cmd);
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
import * as nock from 'nock';
|
import * as nock from 'nock';
|
||||||
import * as tar from 'tar-stream';
|
import * as tar from 'tar-stream';
|
||||||
import {gzipSync} from 'zlib';
|
import {gzipSync} from 'zlib';
|
||||||
import {createLogger, getEnvVar} from '../common/utils';
|
import {getEnvVar, Logger} from '../common/utils';
|
||||||
import {BuildNums, PrNums, SHA} from './constants';
|
import {BuildNums, PrNums, SHA} from './constants';
|
||||||
|
|
||||||
// We are using the `nock` library to fake responses from REST requests, when testing.
|
// We are using the `nock` library to fake responses from REST requests, when testing.
|
||||||
@ -14,7 +14,7 @@ import {BuildNums, PrNums, SHA} from './constants';
|
|||||||
// below and return a suitable response. This is quite complicated to setup since the
|
// below and return a suitable response. This is quite complicated to setup since the
|
||||||
// response from, say, CircleCI will affect what request is made to, say, Github.
|
// response from, say, CircleCI will affect what request is made to, say, Github.
|
||||||
|
|
||||||
const logger = createLogger('NOCK');
|
const logger = new Logger('mock-external-apis');
|
||||||
|
|
||||||
const log = (...args: any[]) => {
|
const log = (...args: any[]) => {
|
||||||
// Filter out non-matching URL checks
|
// Filter out non-matching URL checks
|
||||||
@ -76,7 +76,7 @@ const GITHUB_PULLS_URL = `/repos/${AIO_GITHUB_ORGANIZATION}/${AIO_GITHUB_REPO}/p
|
|||||||
const GITHUB_TEAMS_URL = `/orgs/${AIO_GITHUB_ORGANIZATION}/teams`;
|
const GITHUB_TEAMS_URL = `/orgs/${AIO_GITHUB_ORGANIZATION}/teams`;
|
||||||
|
|
||||||
const getIssueUrl = (prNum: number) => `${GITHUB_ISSUES_URL}/${prNum}`;
|
const getIssueUrl = (prNum: number) => `${GITHUB_ISSUES_URL}/${prNum}`;
|
||||||
const getFilesUrl = (prNum: number) => `${GITHUB_PULLS_URL}/${prNum}/files`;
|
const getFilesUrl = (prNum: number, pageNum = 1) => `${GITHUB_PULLS_URL}/${prNum}/files?page=${pageNum}&per_page=100`;
|
||||||
const getCommentUrl = (prNum: number) => `${getIssueUrl(prNum)}/comments`;
|
const getCommentUrl = (prNum: number) => `${getIssueUrl(prNum)}/comments`;
|
||||||
const getTeamMembershipUrl = (teamId: number, username: string) => `/teams/${teamId}/memberships/${username}`;
|
const getTeamMembershipUrl = (teamId: number, username: string) => `/teams/${teamId}/memberships/${username}`;
|
||||||
|
|
||||||
@ -97,7 +97,7 @@ const githubApi = nock(GITHUB_API_HOST).log(log).persist().matchHeader('Authoriz
|
|||||||
//////////////////////////////
|
//////////////////////////////
|
||||||
|
|
||||||
// GENERAL responses
|
// GENERAL responses
|
||||||
githubApi.get(GITHUB_TEAMS_URL + '?page=0&per_page=100').reply(200, TEST_TEAM_INFO);
|
githubApi.get(GITHUB_TEAMS_URL + '?page=1&per_page=100').reply(200, TEST_TEAM_INFO);
|
||||||
githubApi.post(getCommentUrl(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER)).reply(200);
|
githubApi.post(getCommentUrl(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER)).reply(200);
|
||||||
|
|
||||||
// BUILD_INFO errors
|
// BUILD_INFO errors
|
||||||
|
@ -3,6 +3,7 @@ import * as path from 'path';
|
|||||||
import {rm} from 'shelljs';
|
import {rm} from 'shelljs';
|
||||||
import {AIO_BUILDS_DIR, AIO_NGINX_HOSTNAME, AIO_NGINX_PORT_HTTP, AIO_NGINX_PORT_HTTPS} from '../common/env-variables';
|
import {AIO_BUILDS_DIR, AIO_NGINX_HOSTNAME, AIO_NGINX_PORT_HTTP, AIO_NGINX_PORT_HTTPS} from '../common/env-variables';
|
||||||
import {computeShortSha} from '../common/utils';
|
import {computeShortSha} from '../common/utils';
|
||||||
|
import {PrNums} from './constants';
|
||||||
import {helper as h} from './helper';
|
import {helper as h} from './helper';
|
||||||
import {customMatchers} from './jasmine-custom-matchers';
|
import {customMatchers} from './jasmine-custom-matchers';
|
||||||
|
|
||||||
@ -252,6 +253,42 @@ describe(`nginx`, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe(`${host}/can-have-public-preview`, () => {
|
||||||
|
const baseUrl = `${scheme}://${host}/can-have-public-preview`;
|
||||||
|
|
||||||
|
|
||||||
|
it('should disallow non-GET requests', async () => {
|
||||||
|
await Promise.all([
|
||||||
|
h.runCmd(`curl -iLX POST ${baseUrl}/42`).then(h.verifyResponse([405, 'Not Allowed'])),
|
||||||
|
h.runCmd(`curl -iLX PUT ${baseUrl}/42`).then(h.verifyResponse([405, 'Not Allowed'])),
|
||||||
|
h.runCmd(`curl -iLX PATCH ${baseUrl}/42`).then(h.verifyResponse([405, 'Not Allowed'])),
|
||||||
|
h.runCmd(`curl -iLX DELETE ${baseUrl}/42`).then(h.verifyResponse([405, 'Not Allowed'])),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should pass requests through to the preview server', async () => {
|
||||||
|
await h.runCmd(`curl -iLX GET ${baseUrl}/${PrNums.CHANGED_FILES_ERROR}`).
|
||||||
|
then(h.verifyResponse(500, /CHANGED_FILES_ERROR/));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should respond with 404 for unknown paths', async () => {
|
||||||
|
const cmdPrefix = `curl -iLX GET ${baseUrl}`;
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
h.runCmd(`${cmdPrefix}/foo/42`).then(h.verifyResponse(404)),
|
||||||
|
h.runCmd(`${cmdPrefix}-foo/42`).then(h.verifyResponse(404)),
|
||||||
|
h.runCmd(`${cmdPrefix}nfoo/42`).then(h.verifyResponse(404)),
|
||||||
|
h.runCmd(`${cmdPrefix}/42/foo`).then(h.verifyResponse(404)),
|
||||||
|
h.runCmd(`${cmdPrefix}/f00`).then(h.verifyResponse(404)),
|
||||||
|
h.runCmd(`${cmdPrefix}/`).then(h.verifyResponse(404)),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
describe(`${host}/circle-build`, () => {
|
describe(`${host}/circle-build`, () => {
|
||||||
|
|
||||||
it('should disallow non-POST requests', done => {
|
it('should disallow non-POST requests', done => {
|
||||||
@ -287,6 +324,7 @@ describe(`nginx`, () => {
|
|||||||
h.runCmd(`${cmdPrefix}/circle-build/42`).then(h.verifyResponse(404)),
|
h.runCmd(`${cmdPrefix}/circle-build/42`).then(h.verifyResponse(404)),
|
||||||
]).then(done);
|
]).then(done);
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
@ -18,6 +18,92 @@ describe('preview-server', () => {
|
|||||||
afterEach(() => h.cleanUp());
|
afterEach(() => h.cleanUp());
|
||||||
|
|
||||||
|
|
||||||
|
describe(`${host}/can-have-public-preview`, () => {
|
||||||
|
const curl = makeCurl(`${host}/can-have-public-preview`, {
|
||||||
|
defaultData: null,
|
||||||
|
defaultExtraPath: `/${PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER}`,
|
||||||
|
defaultHeaders: [],
|
||||||
|
defaultMethod: 'GET',
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should disallow non-GET requests', async () => {
|
||||||
|
const bodyRegex = /^Unknown resource in request/;
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
curl({method: 'POST'}).then(h.verifyResponse(404, bodyRegex)),
|
||||||
|
curl({method: 'PUT'}).then(h.verifyResponse(404, bodyRegex)),
|
||||||
|
curl({method: 'PATCH'}).then(h.verifyResponse(404, bodyRegex)),
|
||||||
|
curl({method: 'DELETE'}).then(h.verifyResponse(404, bodyRegex)),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should respond with 404 for unknown paths', async () => {
|
||||||
|
const bodyRegex = /^Unknown resource in request/;
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
curl({extraPath: `/foo/${PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER}`}).then(h.verifyResponse(404, bodyRegex)),
|
||||||
|
curl({extraPath: `-foo/${PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER}`}).then(h.verifyResponse(404, bodyRegex)),
|
||||||
|
curl({extraPath: `nfoo/${PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER}`}).then(h.verifyResponse(404, bodyRegex)),
|
||||||
|
curl({extraPath: `/${PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER}/foo`}).then(h.verifyResponse(404, bodyRegex)),
|
||||||
|
curl({extraPath: '/f00'}).then(h.verifyResponse(404, bodyRegex)),
|
||||||
|
curl({extraPath: '/'}).then(h.verifyResponse(404, bodyRegex)),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should respond with 500 if checking for significant file changes fails', async () => {
|
||||||
|
await Promise.all([
|
||||||
|
curl({extraPath: `/${PrNums.CHANGED_FILES_404}`}).then(h.verifyResponse(500, /CHANGED_FILES_404/)),
|
||||||
|
curl({extraPath: `/${PrNums.CHANGED_FILES_ERROR}`}).then(h.verifyResponse(500, /CHANGED_FILES_ERROR/)),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should respond with 200 (false) if no significant files were touched', async () => {
|
||||||
|
const expectedResponse = JSON.stringify({
|
||||||
|
canHavePublicPreview: false,
|
||||||
|
reason: 'No significant files touched.',
|
||||||
|
});
|
||||||
|
|
||||||
|
await curl({extraPath: `/${PrNums.CHANGED_FILES_NONE}`}).then(h.verifyResponse(200, expectedResponse));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should respond with 500 if checking "trusted" status fails', async () => {
|
||||||
|
await curl({extraPath: `/${PrNums.TRUST_CHECK_ERROR}`}).then(h.verifyResponse(500, 'TRUST_CHECK_ERROR'));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should respond with 200 (false) if the PR is not automatically verifiable as "trusted"', async () => {
|
||||||
|
const expectedResponse = JSON.stringify({
|
||||||
|
canHavePublicPreview: false,
|
||||||
|
reason: 'Not automatically verifiable as \\"trusted\\".',
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
curl({extraPath: `/${PrNums.TRUST_CHECK_INACTIVE_TRUSTED_USER}`}).then(h.verifyResponse(200, expectedResponse)),
|
||||||
|
curl({extraPath: `/${PrNums.TRUST_CHECK_UNTRUSTED}`}).then(h.verifyResponse(200, expectedResponse)),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should respond with 200 (true) if the PR can have a public preview', async () => {
|
||||||
|
const expectedResponse = JSON.stringify({
|
||||||
|
canHavePublicPreview: true,
|
||||||
|
reason: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
curl({extraPath: `/${PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER}`}).then(h.verifyResponse(200, expectedResponse)),
|
||||||
|
curl({extraPath: `/${PrNums.TRUST_CHECK_TRUSTED_LABEL}`}).then(h.verifyResponse(200, expectedResponse)),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
describe(`${host}/circle-build`, () => {
|
describe(`${host}/circle-build`, () => {
|
||||||
|
|
||||||
const curl = makeCurl(`${host}/circle-build`);
|
const curl = makeCurl(`${host}/circle-build`);
|
||||||
|
@ -7,43 +7,49 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prebuild": "yarn clean-dist",
|
"prebuild": "yarn clean-dist",
|
||||||
"build": "tsc",
|
"build": "yarn ~~build",
|
||||||
"build-watch": "yarn build --watch",
|
"prebuild-watch": "yarn prebuild",
|
||||||
|
"build-watch": "yarn ~~build-watch",
|
||||||
"clean-dist": "node --eval \"require('shelljs').rm('-rf', 'dist')\"",
|
"clean-dist": "node --eval \"require('shelljs').rm('-rf', 'dist')\"",
|
||||||
"dev": "concurrently --kill-others --raw --success first \"yarn build-watch\" \"yarn test-watch\"",
|
"predev": "yarn build || true",
|
||||||
|
"dev": "run-p ~~build-watch ~~test-watch",
|
||||||
"lint": "tslint --project tsconfig.json",
|
"lint": "tslint --project tsconfig.json",
|
||||||
"pre~~test-only": "yarn lint",
|
|
||||||
"~~test-only": "node dist/test",
|
|
||||||
"pretest": "yarn build",
|
"pretest": "yarn build",
|
||||||
"test": "yarn ~~test-only",
|
"test": "yarn ~~test-only",
|
||||||
"pretest-watch": "yarn build",
|
"pretest-watch": "yarn pretest",
|
||||||
"test-watch": "nodemon --exec \"yarn ~~test-only\" --watch dist"
|
"test-watch": "yarn ~~test-watch",
|
||||||
|
"~~build": "tsc",
|
||||||
|
"~~build-watch": "yarn ~~build --watch",
|
||||||
|
"pre~~test-only": "yarn lint",
|
||||||
|
"~~test-only": "node dist/test",
|
||||||
|
"~~test-watch": "nodemon --delay 1 --exec \"yarn ~~test-only\" --watch dist"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"body-parser": "^1.18.2",
|
"body-parser": "^1.18.3",
|
||||||
"delete-empty": "^2.0.0",
|
"delete-empty": "^2.0.0",
|
||||||
"express": "^4.15.4",
|
"express": "^4.16.3",
|
||||||
"jasmine": "^2.8.0",
|
"jasmine": "^3.2.0",
|
||||||
"nock": "^9.2.5",
|
"nock": "^9.6.1",
|
||||||
"node-fetch": "^2.1.2",
|
"node-fetch": "^2.2.0",
|
||||||
"shelljs": "^0.8.1",
|
"shelljs": "^0.8.2",
|
||||||
"tar-stream": "^1.6.0",
|
"source-map-support": "^0.5.9",
|
||||||
"tslib": "^1.7.1"
|
"tar-stream": "^1.6.1",
|
||||||
|
"tslib": "^1.9.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/body-parser": "^1.16.5",
|
"@types/body-parser": "^1.17.0",
|
||||||
"@types/express": "^4.0.37",
|
"@types/express": "^4.16.0",
|
||||||
"@types/jasmine": "^2.6.0",
|
"@types/jasmine": "^2.8.8",
|
||||||
"@types/nock": "^9.1.3",
|
"@types/nock": "^9.3.0",
|
||||||
"@types/node": "^8.0.30",
|
"@types/node": "^10.9.2",
|
||||||
"@types/node-fetch": "^1.6.8",
|
"@types/node-fetch": "^2.1.2",
|
||||||
"@types/shelljs": "^0.8.0",
|
"@types/shelljs": "^0.8.0",
|
||||||
"@types/supertest": "^2.0.3",
|
"@types/supertest": "^2.0.5",
|
||||||
"concurrently": "^3.5.0",
|
"nodemon": "^1.18.3",
|
||||||
"nodemon": "^1.12.1",
|
"npm-run-all": "^4.1.5",
|
||||||
"supertest": "^3.0.0",
|
"supertest": "^3.1.0",
|
||||||
"tslint": "^5.7.0",
|
"tslint": "^5.11.0",
|
||||||
"tslint-jasmine-noSkipOrFocus": "^1.0.8",
|
"tslint-jasmine-noSkipOrFocus": "^1.0.9",
|
||||||
"typescript": "^2.5.2"
|
"typescript": "^3.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,25 +5,28 @@ import * as shell from 'shelljs';
|
|||||||
import {BuildCleaner} from '../../lib/clean-up/build-cleaner';
|
import {BuildCleaner} from '../../lib/clean-up/build-cleaner';
|
||||||
import {HIDDEN_DIR_PREFIX} from '../../lib/common/constants';
|
import {HIDDEN_DIR_PREFIX} from '../../lib/common/constants';
|
||||||
import {GithubPullRequests} from '../../lib/common/github-pull-requests';
|
import {GithubPullRequests} from '../../lib/common/github-pull-requests';
|
||||||
|
import {Logger} from '../../lib/common/utils';
|
||||||
|
|
||||||
const EXISTING_BUILDS = [10, 20, 30, 40];
|
const EXISTING_BUILDS = [10, 20, 30, 40];
|
||||||
const EXISTING_DOWNLOADS = [
|
const EXISTING_DOWNLOADS = [
|
||||||
'downloads/10-ABCDEF0-build.zip',
|
'10-ABCDEF0-build.zip',
|
||||||
'downloads/10-1234567-build.zip',
|
'10-1234567-build.zip',
|
||||||
'downloads/20-ABCDEF0-build.zip',
|
'20-ABCDEF0-build.zip',
|
||||||
'downloads/20-1234567-build.zip',
|
'20-1234567-build.zip',
|
||||||
];
|
];
|
||||||
const OPEN_PRS = [10, 40];
|
const OPEN_PRS = [10, 40];
|
||||||
const ANY_DATE = jasmine.any(String);
|
const ANY_DATE = jasmine.any(String);
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
describe('BuildCleaner', () => {
|
describe('BuildCleaner', () => {
|
||||||
|
let loggerErrorSpy: jasmine.Spy;
|
||||||
|
let loggerLogSpy: jasmine.Spy;
|
||||||
let cleaner: BuildCleaner;
|
let cleaner: BuildCleaner;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(console, 'error');
|
loggerErrorSpy = spyOn(Logger.prototype, 'error');
|
||||||
spyOn(console, 'log');
|
loggerLogSpy = spyOn(Logger.prototype, 'log');
|
||||||
cleaner = new BuildCleaner('/foo/bar', 'baz', 'qux', '12345', 'downloads', 'build.zip');
|
cleaner = new BuildCleaner('/foo/bar', 'baz', 'qux', '12345', '/downloads', 'build.zip');
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('constructor()', () => {
|
describe('constructor()', () => {
|
||||||
@ -51,11 +54,13 @@ describe('BuildCleaner', () => {
|
|||||||
toThrowError('Missing or empty required parameter \'githubToken\'!');
|
toThrowError('Missing or empty required parameter \'githubToken\'!');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should throw if \'downloadsDir\' is empty', () => {
|
it('should throw if \'downloadsDir\' is empty', () => {
|
||||||
expect(() => new BuildCleaner('/foo/bar', 'baz', 'qux', '12345', '', 'build.zip')).
|
expect(() => new BuildCleaner('/foo/bar', 'baz', 'qux', '12345', '', 'build.zip')).
|
||||||
toThrowError('Missing or empty required parameter \'downloadsDir\'!');
|
toThrowError('Missing or empty required parameter \'downloadsDir\'!');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should throw if \'artifactPath\' is empty', () => {
|
it('should throw if \'artifactPath\' is empty', () => {
|
||||||
expect(() => new BuildCleaner('/foo/bar', 'baz', 'qux', '12345', 'downloads', '')).
|
expect(() => new BuildCleaner('/foo/bar', 'baz', 'qux', '12345', 'downloads', '')).
|
||||||
toThrowError('Missing or empty required parameter \'artifactPath\'!');
|
toThrowError('Missing or empty required parameter \'artifactPath\'!');
|
||||||
@ -85,9 +90,12 @@ describe('BuildCleaner', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should return a promise', () => {
|
it('should return a promise', async () => {
|
||||||
const promise = cleaner.cleanUp();
|
const promise = cleaner.cleanUp();
|
||||||
expect(promise).toEqual(jasmine.any(Promise));
|
expect(promise).toEqual(jasmine.any(Promise));
|
||||||
|
|
||||||
|
// Do not complete the test and release the spies synchronously, to avoid running the actual implementations.
|
||||||
|
await promise;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -160,6 +168,7 @@ describe('BuildCleaner', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should reject if \'removeUnnecessaryDownloads()\' rejects', async () => {
|
it('should reject if \'removeUnnecessaryDownloads()\' rejects', async () => {
|
||||||
try {
|
try {
|
||||||
cleanerRemoveUnnecessaryDownloadsSpy.and.callFake(() => Promise.reject('Test'));
|
cleanerRemoveUnnecessaryDownloadsSpy.and.callFake(() => Promise.reject('Test'));
|
||||||
@ -168,6 +177,7 @@ describe('BuildCleaner', () => {
|
|||||||
expect(err).toBe('Test');
|
expect(err).toBe('Test');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -277,11 +287,14 @@ describe('BuildCleaner', () => {
|
|||||||
prDeferred.resolve([{id: 0, number: 1}, {id: 1, number: 2}, {id: 2, number: 3}]);
|
prDeferred.resolve([{id: 0, number: 1}, {id: 1, number: 2}, {id: 2, number: 3}]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should log the number of open PRs', () => {
|
it('should log the number of open PRs', () => {
|
||||||
promise.then(prNumbers => {
|
promise.then(prNumbers => {
|
||||||
expect(console.log).toHaveBeenCalledWith(ANY_DATE, 'BuildCleaner: ', `Open pull requests: ${prNumbers}`);
|
expect(loggerLogSpy).toHaveBeenCalledWith(
|
||||||
|
ANY_DATE, 'BuildCleaner: ', `Open pull requests: ${prNumbers}`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -301,9 +314,9 @@ describe('BuildCleaner', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should get the contents of the builds directory', () => {
|
it('should get the contents of the downloads directory', () => {
|
||||||
expect(fsReaddirSpy).toHaveBeenCalled();
|
expect(fsReaddirSpy).toHaveBeenCalled();
|
||||||
expect(fsReaddirSpy.calls.argsFor(0)[0]).toBe('downloads');
|
expect(fsReaddirSpy.calls.argsFor(0)[0]).toBe('/downloads');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -317,7 +330,7 @@ describe('BuildCleaner', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should resolve with the returned files (as numbers)', done => {
|
it('should resolve with the returned file names', done => {
|
||||||
promise.then(result => {
|
promise.then(result => {
|
||||||
expect(result).toEqual(EXISTING_DOWNLOADS);
|
expect(result).toEqual(EXISTING_DOWNLOADS);
|
||||||
done();
|
done();
|
||||||
@ -383,8 +396,7 @@ describe('BuildCleaner', () => {
|
|||||||
|
|
||||||
cleaner.removeDir('/foo/bar');
|
cleaner.removeDir('/foo/bar');
|
||||||
|
|
||||||
expect(console.error).toHaveBeenCalledWith(
|
expect(loggerErrorSpy).toHaveBeenCalledWith('ERROR: Unable to remove \'/foo/bar\' due to:', 'Test');
|
||||||
jasmine.any(String), 'BuildCleaner: ', 'ERROR: Unable to remove \'/foo/bar\' due to:', 'Test');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
@ -401,8 +413,8 @@ describe('BuildCleaner', () => {
|
|||||||
it('should log the number of existing builds and builds to be removed', () => {
|
it('should log the number of existing builds and builds to be removed', () => {
|
||||||
cleaner.removeUnnecessaryBuilds([1, 2, 3], [3, 4, 5, 6]);
|
cleaner.removeUnnecessaryBuilds([1, 2, 3], [3, 4, 5, 6]);
|
||||||
|
|
||||||
expect(console.log).toHaveBeenCalledWith(ANY_DATE, 'BuildCleaner: ', 'Existing builds: 3');
|
expect(loggerLogSpy).toHaveBeenCalledWith('Existing builds: 3');
|
||||||
expect(console.log).toHaveBeenCalledWith(ANY_DATE, 'BuildCleaner: ', 'Removing 2 build(s): 1, 2');
|
expect(loggerLogSpy).toHaveBeenCalledWith('Removing 2 build(s): 1, 2');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -454,25 +466,36 @@ describe('BuildCleaner', () => {
|
|||||||
|
|
||||||
|
|
||||||
describe('removeUnnecessaryDownloads()', () => {
|
describe('removeUnnecessaryDownloads()', () => {
|
||||||
|
let shellRmSpy: jasmine.Spy;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(shell, 'rm');
|
shellRmSpy = spyOn(shell, 'rm');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should log the number of existing downloads and downloads to be removed', () => {
|
||||||
|
cleaner.removeUnnecessaryDownloads(EXISTING_DOWNLOADS, OPEN_PRS);
|
||||||
|
|
||||||
|
expect(loggerLogSpy).toHaveBeenCalledWith('Existing downloads: 4');
|
||||||
|
expect(loggerLogSpy).toHaveBeenCalledWith('Removing 2 download(s): 20-ABCDEF0-build.zip, 20-1234567-build.zip');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should construct full paths to directories (by prepending \'downloadsDir\')', () => {
|
||||||
|
cleaner.removeUnnecessaryDownloads(['dl-1', 'dl-2', 'dl-3'], []);
|
||||||
|
|
||||||
|
expect(shellRmSpy).toHaveBeenCalledWith(normalize('/downloads/dl-1'));
|
||||||
|
expect(shellRmSpy).toHaveBeenCalledWith(normalize('/downloads/dl-2'));
|
||||||
|
expect(shellRmSpy).toHaveBeenCalledWith(normalize('/downloads/dl-3'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should remove the downloads that do not correspond to open PRs', () => {
|
it('should remove the downloads that do not correspond to open PRs', () => {
|
||||||
cleaner.removeUnnecessaryDownloads(EXISTING_DOWNLOADS, OPEN_PRS);
|
cleaner.removeUnnecessaryDownloads(EXISTING_DOWNLOADS, OPEN_PRS);
|
||||||
expect(shell.rm).toHaveBeenCalledTimes(2);
|
expect(shellRmSpy).toHaveBeenCalledTimes(2);
|
||||||
expect(shell.rm).toHaveBeenCalledWith('downloads/20-ABCDEF0-build.zip');
|
expect(shellRmSpy).toHaveBeenCalledWith(normalize('/downloads/20-ABCDEF0-build.zip'));
|
||||||
expect(shell.rm).toHaveBeenCalledWith('downloads/20-1234567-build.zip');
|
expect(shellRmSpy).toHaveBeenCalledWith(normalize('/downloads/20-1234567-build.zip'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should log the number of existing builds and builds to be removed', () => {
|
|
||||||
cleaner.removeUnnecessaryDownloads(EXISTING_DOWNLOADS, OPEN_PRS);
|
|
||||||
|
|
||||||
expect(console.log).toHaveBeenCalledWith(ANY_DATE, 'BuildCleaner: ', 'Existing downloads: 4');
|
|
||||||
expect(console.log).toHaveBeenCalledWith(ANY_DATE, 'BuildCleaner: ',
|
|
||||||
'Removing 2 download(s): downloads/20-ABCDEF0-build.zip, downloads/20-1234567-build.zip');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -126,8 +126,8 @@ describe('GithubApi', () => {
|
|||||||
(api as any).getPaginated('/foo/bar');
|
(api as any).getPaginated('/foo/bar');
|
||||||
(api as any).getPaginated('/foo/bar', {baz: 'qux'});
|
(api as any).getPaginated('/foo/bar', {baz: 'qux'});
|
||||||
|
|
||||||
expect(api.get).toHaveBeenCalledWith('/foo/bar', {page: 0, per_page: 100});
|
expect(api.get).toHaveBeenCalledWith('/foo/bar', {page: 1, per_page: 100});
|
||||||
expect(api.get).toHaveBeenCalledWith('/foo/bar', {baz: 'qux', page: 0, per_page: 100});
|
expect(api.get).toHaveBeenCalledWith('/foo/bar', {baz: 'qux', page: 1, per_page: 100});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -162,9 +162,9 @@ describe('GithubApi', () => {
|
|||||||
const paramsForPage = (page: number) => ({baz: 'qux', page, per_page: 100});
|
const paramsForPage = (page: number) => ({baz: 'qux', page, per_page: 100});
|
||||||
|
|
||||||
expect(apiGetSpy).toHaveBeenCalledTimes(3);
|
expect(apiGetSpy).toHaveBeenCalledTimes(3);
|
||||||
expect(apiGetSpy.calls.argsFor(0)).toEqual(['/foo/bar', paramsForPage(0)]);
|
expect(apiGetSpy.calls.argsFor(0)).toEqual(['/foo/bar', paramsForPage(1)]);
|
||||||
expect(apiGetSpy.calls.argsFor(1)).toEqual(['/foo/bar', paramsForPage(1)]);
|
expect(apiGetSpy.calls.argsFor(1)).toEqual(['/foo/bar', paramsForPage(2)]);
|
||||||
expect(apiGetSpy.calls.argsFor(2)).toEqual(['/foo/bar', paramsForPage(2)]);
|
expect(apiGetSpy.calls.argsFor(2)).toEqual(['/foo/bar', paramsForPage(3)]);
|
||||||
|
|
||||||
expect(data).toEqual(allItems);
|
expect(data).toEqual(allItems);
|
||||||
|
|
||||||
|
@ -4,13 +4,13 @@ import {GithubPullRequests} from '../../lib/common/github-pull-requests';
|
|||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
describe('GithubPullRequests', () => {
|
describe('GithubPullRequests', () => {
|
||||||
|
|
||||||
let githubApi: jasmine.SpyObj<GithubApi>;
|
let githubApi: jasmine.SpyObj<GithubApi>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
githubApi = jasmine.createSpyObj('githubApi', ['post', 'get', 'getPaginated']);
|
githubApi = jasmine.createSpyObj('githubApi', ['post', 'get', 'getPaginated']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
describe('constructor()', () => {
|
describe('constructor()', () => {
|
||||||
|
|
||||||
it('should throw if \'githubOrg\' is missing or empty', () => {
|
it('should throw if \'githubOrg\' is missing or empty', () => {
|
||||||
@ -95,16 +95,14 @@ describe('GithubPullRequests', () => {
|
|||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
describe('fetchAll()', () => {
|
describe('fetchAll()', () => {
|
||||||
let prs: GithubPullRequests;
|
let prs: GithubPullRequests;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => prs = new GithubPullRequests(githubApi, 'foo', 'bar'));
|
||||||
prs = new GithubPullRequests(githubApi, 'foo', 'bar');
|
|
||||||
spyOn(console, 'log');
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should call \'getPaginated()\' with the correct pathname and params', () => {
|
it('should call \'getPaginated()\' with the correct pathname and params', () => {
|
||||||
@ -131,8 +129,10 @@ describe('GithubPullRequests', () => {
|
|||||||
githubApi.getPaginated.and.returnValue('Test');
|
githubApi.getPaginated.and.returnValue('Test');
|
||||||
expect(prs.fetchAll() as any).toBe('Test');
|
expect(prs.fetchAll() as any).toBe('Test');
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
describe('fetchFiles()', () => {
|
describe('fetchFiles()', () => {
|
||||||
let prs: GithubPullRequests;
|
let prs: GithubPullRequests;
|
||||||
|
|
||||||
@ -141,21 +141,21 @@ describe('GithubPullRequests', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should make a GET request to GitHub with the correct pathname', () => {
|
it('should make a paginated GET request to GitHub with the correct pathname', () => {
|
||||||
prs.fetchFiles(42);
|
prs.fetchFiles(42);
|
||||||
expect(githubApi.get).toHaveBeenCalledWith('/repos/foo/bar/pulls/42/files');
|
expect(githubApi.getPaginated).toHaveBeenCalledWith('/repos/foo/bar/pulls/42/files');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should resolve with the data returned from GitHub', done => {
|
it('should resolve with the data returned from GitHub', done => {
|
||||||
const expected: any = [{ sha: 'ABCDE', filename: 'a/b/c'}, { sha: '12345', filename: 'x/y/z' }];
|
const expected: any = [{sha: 'ABCDE', filename: 'a/b/c'}, {sha: '12345', filename: 'x/y/z'}];
|
||||||
githubApi.get.and.callFake(() => Promise.resolve(expected));
|
githubApi.getPaginated.and.callFake(() => Promise.resolve(expected));
|
||||||
prs.fetch(42).then(data => {
|
prs.fetchFiles(42).then(data => {
|
||||||
expect(data).toEqual(expected);
|
expect(data).toEqual(expected);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
// Imports
|
// Imports
|
||||||
|
import {resolve as resolvePath} from 'path';
|
||||||
import {
|
import {
|
||||||
assert,
|
assert,
|
||||||
assertNotMissingOrEmpty,
|
assertNotMissingOrEmpty,
|
||||||
@ -6,6 +7,7 @@ import {
|
|||||||
computeShortSha,
|
computeShortSha,
|
||||||
getEnvVar,
|
getEnvVar,
|
||||||
getPrInfoFromDownloadPath,
|
getPrInfoFromDownloadPath,
|
||||||
|
Logger,
|
||||||
} from '../../lib/common/utils';
|
} from '../../lib/common/utils';
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
@ -19,6 +21,7 @@ describe('utils', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
describe('assert', () => {
|
describe('assert', () => {
|
||||||
it('should throw if passed a false value', () => {
|
it('should throw if passed a false value', () => {
|
||||||
expect(() => assert(false, 'error message')).toThrowError('error message');
|
expect(() => assert(false, 'error message')).toThrowError('error message');
|
||||||
@ -29,6 +32,7 @@ describe('utils', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
describe('computeArtifactDownloadPath', () => {
|
describe('computeArtifactDownloadPath', () => {
|
||||||
it('should compute an absolute path based on the artifact info provided', () => {
|
it('should compute an absolute path based on the artifact info provided', () => {
|
||||||
const downloadDir = '/a/b/c';
|
const downloadDir = '/a/b/c';
|
||||||
@ -36,10 +40,11 @@ describe('utils', () => {
|
|||||||
const sha = 'ABCDEF1234567';
|
const sha = 'ABCDEF1234567';
|
||||||
const artifactPath = 'a/path/to/file.zip';
|
const artifactPath = 'a/path/to/file.zip';
|
||||||
const path = computeArtifactDownloadPath(downloadDir, pr, sha, artifactPath);
|
const path = computeArtifactDownloadPath(downloadDir, pr, sha, artifactPath);
|
||||||
expect(path).toEqual('/a/b/c/123-ABCDEF1-file.zip');
|
expect(path).toBe(resolvePath('/a/b/c/123-ABCDEF1-file.zip'));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
describe('getPrInfoFromDownloadPath', () => {
|
describe('getPrInfoFromDownloadPath', () => {
|
||||||
it('should extract the PR and SHA from the file path', () => {
|
it('should extract the PR and SHA from the file path', () => {
|
||||||
const {pr, sha} = getPrInfoFromDownloadPath('a/b/c/12345-ABCDE-artifact.zip');
|
const {pr, sha} = getPrInfoFromDownloadPath('a/b/c/12345-ABCDE-artifact.zip');
|
||||||
@ -48,6 +53,7 @@ describe('utils', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
describe('assertNotMissingOrEmpty()', () => {
|
describe('assertNotMissingOrEmpty()', () => {
|
||||||
|
|
||||||
it('should throw if passed an empty value', () => {
|
it('should throw if passed an empty value', () => {
|
||||||
@ -122,4 +128,79 @@ describe('utils', () => {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('Logger', () => {
|
||||||
|
let consoleErrorSpy: jasmine.Spy;
|
||||||
|
let consoleInfoSpy: jasmine.Spy;
|
||||||
|
let consoleLogSpy: jasmine.Spy;
|
||||||
|
let consoleWarnSpy: jasmine.Spy;
|
||||||
|
let logger: Logger;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
consoleErrorSpy = spyOn(console, 'error');
|
||||||
|
consoleInfoSpy = spyOn(console, 'info');
|
||||||
|
consoleLogSpy = spyOn(console, 'log');
|
||||||
|
consoleWarnSpy = spyOn(console, 'warn');
|
||||||
|
|
||||||
|
logger = new Logger('TestScope');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should delegate to `console`', () => {
|
||||||
|
logger.error('foo');
|
||||||
|
expect(consoleErrorSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(consoleErrorSpy.calls.argsFor(0)).toContain('foo');
|
||||||
|
|
||||||
|
logger.info('bar');
|
||||||
|
expect(consoleInfoSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(consoleInfoSpy.calls.argsFor(0)).toContain('bar');
|
||||||
|
|
||||||
|
logger.log('baz');
|
||||||
|
expect(consoleLogSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(consoleLogSpy.calls.argsFor(0)).toContain('baz');
|
||||||
|
|
||||||
|
logger.warn('qux');
|
||||||
|
expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(consoleWarnSpy.calls.argsFor(0)).toContain('qux');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should prepend messages with the current date and logger\'s scope', () => {
|
||||||
|
const mockDate = new Date(1337);
|
||||||
|
const expectedDateStr = `[${mockDate}]`;
|
||||||
|
const expectedScopeStr = 'TestScope: ';
|
||||||
|
|
||||||
|
jasmine.clock().mockDate(mockDate);
|
||||||
|
jasmine.clock().withMock(() => {
|
||||||
|
logger.error();
|
||||||
|
logger.info();
|
||||||
|
logger.log();
|
||||||
|
logger.warn();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(consoleErrorSpy).toHaveBeenCalledWith(expectedDateStr, expectedScopeStr);
|
||||||
|
expect(consoleInfoSpy).toHaveBeenCalledWith(expectedDateStr, expectedScopeStr);
|
||||||
|
expect(consoleLogSpy).toHaveBeenCalledWith(expectedDateStr, expectedScopeStr);
|
||||||
|
expect(consoleWarnSpy).toHaveBeenCalledWith(expectedDateStr, expectedScopeStr);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should pass all arguments to `console`', () => {
|
||||||
|
const someString = jasmine.any(String);
|
||||||
|
|
||||||
|
logger.error('foo1', 'foo2');
|
||||||
|
expect(consoleErrorSpy).toHaveBeenCalledWith(someString, someString, 'foo1', 'foo2');
|
||||||
|
|
||||||
|
logger.info('bar1', 'bar2');
|
||||||
|
expect(consoleInfoSpy).toHaveBeenCalledWith(someString, someString, 'bar1', 'bar2');
|
||||||
|
|
||||||
|
logger.log('baz1', 'baz2');
|
||||||
|
expect(consoleLogSpy).toHaveBeenCalledWith(someString, someString, 'baz1', 'baz2');
|
||||||
|
|
||||||
|
logger.warn('qux1', 'qux2');
|
||||||
|
expect(consoleWarnSpy).toHaveBeenCalledWith(someString, someString, 'qux1', 'qux2');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
declare namespace jasmine {
|
|
||||||
export interface DoneFn extends Function {
|
|
||||||
(): void;
|
|
||||||
fail: (message: Error | string) => void;
|
|
||||||
}
|
|
||||||
}
|
|
@ -3,5 +3,4 @@ import {runTests} from '../lib/common/run-tests';
|
|||||||
|
|
||||||
// Run
|
// Run
|
||||||
const specFiles = [`${__dirname}/**/*.spec.js`];
|
const specFiles = [`${__dirname}/**/*.spec.js`];
|
||||||
const helpers = [`${__dirname}/helpers.js`];
|
runTests(specFiles);
|
||||||
runTests(specFiles, helpers);
|
|
||||||
|
@ -5,6 +5,7 @@ import * as fs from 'fs';
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as shell from 'shelljs';
|
import * as shell from 'shelljs';
|
||||||
import {SHORT_SHA_LEN} from '../../lib/common/constants';
|
import {SHORT_SHA_LEN} from '../../lib/common/constants';
|
||||||
|
import {Logger} from '../../lib/common/utils';
|
||||||
import {BuildCreator} from '../../lib/preview-server/build-creator';
|
import {BuildCreator} from '../../lib/preview-server/build-creator';
|
||||||
import {ChangedPrVisibilityEvent, CreatedBuildEvent} from '../../lib/preview-server/build-events';
|
import {ChangedPrVisibilityEvent, CreatedBuildEvent} from '../../lib/preview-server/build-events';
|
||||||
import {PreviewServerError} from '../../lib/preview-server/preview-error';
|
import {PreviewServerError} from '../../lib/preview-server/preview-error';
|
||||||
@ -491,7 +492,7 @@ describe('BuildCreator', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cpExecCbs = [];
|
cpExecCbs = [];
|
||||||
|
|
||||||
consoleWarnSpy = spyOn(console, 'warn');
|
consoleWarnSpy = spyOn(Logger.prototype, 'warn');
|
||||||
shellChmodSpy = spyOn(shell, 'chmod');
|
shellChmodSpy = spyOn(shell, 'chmod');
|
||||||
shellRmSpy = spyOn(shell, 'rm');
|
shellRmSpy = spyOn(shell, 'rm');
|
||||||
cpExecSpy = spyOn(cp, 'exec').and.callFake((_: string, cb: (...args: any[]) => void) => cpExecCbs.push(cb));
|
cpExecSpy = spyOn(cp, 'exec').and.callFake((_: string, cb: (...args: any[]) => void) => cpExecCbs.push(cb));
|
||||||
@ -513,8 +514,7 @@ describe('BuildCreator', () => {
|
|||||||
|
|
||||||
it('should log (as a warning) any stderr output if extracting succeeded', done => {
|
it('should log (as a warning) any stderr output if extracting succeeded', done => {
|
||||||
(bc as any).extractArchive('foo', 'bar').
|
(bc as any).extractArchive('foo', 'bar').
|
||||||
then(() => expect(consoleWarnSpy)
|
then(() => expect(consoleWarnSpy).toHaveBeenCalledWith('This is stderr')).
|
||||||
.toHaveBeenCalledWith(jasmine.any(String), 'BuildCreator: ', 'This is stderr')).
|
|
||||||
then(done);
|
then(done);
|
||||||
|
|
||||||
cpExecCbs[0](null, 'This is stdout', 'This is stderr');
|
cpExecCbs[0](null, 'This is stdout', 'This is stderr');
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as nock from 'nock';
|
import * as nock from 'nock';
|
||||||
|
import {resolve as resolvePath} from 'path';
|
||||||
import {BuildInfo, CircleCiApi} from '../../lib/common/circle-ci-api';
|
import {BuildInfo, CircleCiApi} from '../../lib/common/circle-ci-api';
|
||||||
|
import {Logger} from '../../lib/common/utils';
|
||||||
import {BuildRetriever} from '../../lib/preview-server/build-retriever';
|
import {BuildRetriever} from '../../lib/preview-server/build-retriever';
|
||||||
|
|
||||||
describe('BuildRetriever', () => {
|
describe('BuildRetriever', () => {
|
||||||
const MAX_DOWNLOAD_SIZE = 10000;
|
const MAX_DOWNLOAD_SIZE = 10000;
|
||||||
const DOWNLOAD_DIR = '/DOWNLOAD/DIR';
|
const DOWNLOAD_DIR = resolvePath('/DOWNLOAD/DIR');
|
||||||
const BASE_URL = 'http://test.com';
|
const BASE_URL = 'http://test.com';
|
||||||
const ARTIFACT_PATH = '/some/path/build.zip';
|
const ARTIFACT_PATH = '/some/path/build.zip';
|
||||||
|
|
||||||
@ -29,10 +31,6 @@ describe('BuildRetriever', () => {
|
|||||||
vcs_revision: 'COMMIT',
|
vcs_revision: 'COMMIT',
|
||||||
};
|
};
|
||||||
|
|
||||||
spyOn(console, 'log');
|
|
||||||
spyOn(console, 'warn');
|
|
||||||
spyOn(console, 'error');
|
|
||||||
|
|
||||||
api = new CircleCiApi('ORG', 'REPO', 'TOKEN');
|
api = new CircleCiApi('ORG', 'REPO', 'TOKEN');
|
||||||
spyOn(api, 'getBuildInfo').and.callFake(() => Promise.resolve(BUILD_INFO));
|
spyOn(api, 'getBuildInfo').and.callFake(() => Promise.resolve(BUILD_INFO));
|
||||||
getBuildArtifactUrlSpy = spyOn(api, 'getBuildArtifactUrl')
|
getBuildArtifactUrlSpy = spyOn(api, 'getBuildArtifactUrl')
|
||||||
@ -91,6 +89,7 @@ describe('BuildRetriever', () => {
|
|||||||
let retriever: BuildRetriever;
|
let retriever: BuildRetriever;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
spyOn(Logger.prototype, 'warn');
|
||||||
retriever = new BuildRetriever(api, MAX_DOWNLOAD_SIZE, DOWNLOAD_DIR);
|
retriever = new BuildRetriever(api, MAX_DOWNLOAD_SIZE, DOWNLOAD_DIR);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -133,11 +132,14 @@ describe('BuildRetriever', () => {
|
|||||||
|
|
||||||
it('should write the artifact file to disk', async () => {
|
it('should write the artifact file to disk', async () => {
|
||||||
const artifactRequest = nock(BASE_URL).get(ARTIFACT_PATH).reply(200, ARTIFACT_CONTENTS);
|
const artifactRequest = nock(BASE_URL).get(ARTIFACT_PATH).reply(200, ARTIFACT_CONTENTS);
|
||||||
|
const downloadPath = resolvePath(`${DOWNLOAD_DIR}/777-COMMIT-build.zip`);
|
||||||
|
|
||||||
await retriever.downloadBuildArtifact(12345, 777, 'COMMIT', ARTIFACT_PATH);
|
await retriever.downloadBuildArtifact(12345, 777, 'COMMIT', ARTIFACT_PATH);
|
||||||
expect(writeFileSpy)
|
expect(writeFileSpy).toHaveBeenCalledWith(downloadPath, jasmine.any(Buffer), jasmine.any(Function));
|
||||||
.toHaveBeenCalledWith(`${DOWNLOAD_DIR}/777-COMMIT-build.zip`, jasmine.any(Buffer), jasmine.any(Function));
|
|
||||||
const buffer: Buffer = writeFileSpy.calls.mostRecent().args[1];
|
const buffer: Buffer = writeFileSpy.calls.mostRecent().args[1];
|
||||||
expect(buffer.toString()).toEqual(ARTIFACT_CONTENTS);
|
expect(buffer.toString()).toEqual(ARTIFACT_CONTENTS);
|
||||||
|
|
||||||
artifactRequest.done();
|
artifactRequest.done();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -2,11 +2,11 @@
|
|||||||
import * as express from 'express';
|
import * as express from 'express';
|
||||||
import * as http from 'http';
|
import * as http from 'http';
|
||||||
import * as supertest from 'supertest';
|
import * as supertest from 'supertest';
|
||||||
import {promisify} from 'util';
|
|
||||||
import {CircleCiApi} from '../../lib/common/circle-ci-api';
|
import {CircleCiApi} from '../../lib/common/circle-ci-api';
|
||||||
import {GithubApi} from '../../lib/common/github-api';
|
import {GithubApi} from '../../lib/common/github-api';
|
||||||
import {GithubPullRequests} from '../../lib/common/github-pull-requests';
|
import {GithubPullRequests} from '../../lib/common/github-pull-requests';
|
||||||
import {GithubTeams} from '../../lib/common/github-teams';
|
import {GithubTeams} from '../../lib/common/github-teams';
|
||||||
|
import {Logger} from '../../lib/common/utils';
|
||||||
import {BuildCreator} from '../../lib/preview-server/build-creator';
|
import {BuildCreator} from '../../lib/preview-server/build-creator';
|
||||||
import {ChangedPrVisibilityEvent, CreatedBuildEvent} from '../../lib/preview-server/build-events';
|
import {ChangedPrVisibilityEvent, CreatedBuildEvent} from '../../lib/preview-server/build-events';
|
||||||
import {BuildRetriever, GithubInfo} from '../../lib/preview-server/build-retriever';
|
import {BuildRetriever, GithubInfo} from '../../lib/preview-server/build-retriever';
|
||||||
@ -38,15 +38,18 @@ describe('PreviewServerFactory', () => {
|
|||||||
significantFilesPattern: '^(?:aio|packages)\\/(?!.*[._]spec\\.[jt]s$)',
|
significantFilesPattern: '^(?:aio|packages)\\/(?!.*[._]spec\\.[jt]s$)',
|
||||||
trustedPrLabel: 'trusted: pr-label',
|
trustedPrLabel: 'trusted: pr-label',
|
||||||
};
|
};
|
||||||
|
let loggerErrorSpy: jasmine.Spy;
|
||||||
|
let loggerInfoSpy: jasmine.Spy;
|
||||||
|
let loggerLogSpy: jasmine.Spy;
|
||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
const createPreviewServer = (partialConfig: Partial<PreviewServerConfig> = {}) =>
|
const createPreviewServer = (partialConfig: Partial<PreviewServerConfig> = {}) =>
|
||||||
PreviewServerFactory.create({...defaultConfig, ...partialConfig});
|
PreviewServerFactory.create({...defaultConfig, ...partialConfig});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(console, 'error');
|
loggerErrorSpy = spyOn(Logger.prototype, 'error');
|
||||||
spyOn(console, 'info');
|
loggerInfoSpy = spyOn(Logger.prototype, 'info');
|
||||||
spyOn(console, 'log');
|
loggerLogSpy = spyOn(Logger.prototype, 'log');
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('create()', () => {
|
describe('create()', () => {
|
||||||
@ -140,11 +143,10 @@ describe('PreviewServerFactory', () => {
|
|||||||
const server = createPreviewServer();
|
const server = createPreviewServer();
|
||||||
server.address = () => ({address: 'foo', family: '', port: 1337});
|
server.address = () => ({address: 'foo', family: '', port: 1337});
|
||||||
|
|
||||||
expect(console.info).not.toHaveBeenCalled();
|
expect(loggerInfoSpy).not.toHaveBeenCalled();
|
||||||
|
|
||||||
server.emit('listening');
|
server.emit('listening');
|
||||||
expect(console.info).toHaveBeenCalledWith(
|
expect(loggerInfoSpy).toHaveBeenCalledWith('Up and running (and listening on foo:1337)...');
|
||||||
jasmine.any(String), 'PreviewServer: ', 'Up and running (and listening on foo:1337)...');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
@ -241,10 +243,6 @@ describe('PreviewServerFactory', () => {
|
|||||||
let buildCreator: BuildCreator;
|
let buildCreator: BuildCreator;
|
||||||
let agent: supertest.SuperTest<supertest.Test>;
|
let agent: supertest.SuperTest<supertest.Test>;
|
||||||
|
|
||||||
// Helpers
|
|
||||||
const promisifyRequest = async (req: supertest.Request) => await promisify(req.end.bind(req))();
|
|
||||||
const verifyRequests = async (reqs: supertest.Request[]) => await Promise.all(reqs.map(promisifyRequest));
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const circleCiApi = new CircleCiApi(defaultConfig.githubOrg, defaultConfig.githubRepo,
|
const circleCiApi = new CircleCiApi(defaultConfig.githubOrg, defaultConfig.githubRepo,
|
||||||
defaultConfig.circleCiToken);
|
defaultConfig.circleCiToken);
|
||||||
@ -261,10 +259,11 @@ describe('PreviewServerFactory', () => {
|
|||||||
agent = supertest.agent(middleware);
|
agent = supertest.agent(middleware);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
describe('GET /health-check', () => {
|
describe('GET /health-check', () => {
|
||||||
|
|
||||||
it('should respond with 200', async () => {
|
it('should respond with 200', async () => {
|
||||||
await verifyRequests([
|
await Promise.all([
|
||||||
agent.get('/health-check').expect(200),
|
agent.get('/health-check').expect(200),
|
||||||
agent.get('/health-check/').expect(200),
|
agent.get('/health-check/').expect(200),
|
||||||
]);
|
]);
|
||||||
@ -272,7 +271,7 @@ describe('PreviewServerFactory', () => {
|
|||||||
|
|
||||||
|
|
||||||
it('should respond with 404 for non-GET requests', async () => {
|
it('should respond with 404 for non-GET requests', async () => {
|
||||||
await verifyRequests([
|
await Promise.all([
|
||||||
agent.put('/health-check').expect(404),
|
agent.put('/health-check').expect(404),
|
||||||
agent.post('/health-check').expect(404),
|
agent.post('/health-check').expect(404),
|
||||||
agent.patch('/health-check').expect(404),
|
agent.patch('/health-check').expect(404),
|
||||||
@ -282,7 +281,7 @@ describe('PreviewServerFactory', () => {
|
|||||||
|
|
||||||
|
|
||||||
it('should respond with 404 if the path does not match exactly', async () => {
|
it('should respond with 404 if the path does not match exactly', async () => {
|
||||||
await verifyRequests([
|
await Promise.all([
|
||||||
agent.get('/health-check/foo').expect(404),
|
agent.get('/health-check/foo').expect(404),
|
||||||
agent.get('/health-check-foo').expect(404),
|
agent.get('/health-check-foo').expect(404),
|
||||||
agent.get('/health-checknfoo').expect(404),
|
agent.get('/health-checknfoo').expect(404),
|
||||||
@ -294,7 +293,104 @@ describe('PreviewServerFactory', () => {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('/circle-build', () => {
|
|
||||||
|
describe('GET /can-have-public-preview/<pr>', () => {
|
||||||
|
const baseUrl = '/can-have-public-preview';
|
||||||
|
const pr = 777;
|
||||||
|
const url = `${baseUrl}/${pr}`;
|
||||||
|
let bvGetPrIsTrustedSpy: jasmine.Spy;
|
||||||
|
let bvGetSignificantFilesChangedSpy: jasmine.Spy;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
bvGetPrIsTrustedSpy = spyOn(buildVerifier, 'getPrIsTrusted').and.returnValue(Promise.resolve(true));
|
||||||
|
bvGetSignificantFilesChangedSpy = spyOn(buildVerifier, 'getSignificantFilesChanged').
|
||||||
|
and.returnValue(Promise.resolve(true));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should respond with 404 for non-GET requests', async () => {
|
||||||
|
await Promise.all([
|
||||||
|
agent.put(url).expect(404),
|
||||||
|
agent.post(url).expect(404),
|
||||||
|
agent.patch(url).expect(404),
|
||||||
|
agent.delete(url).expect(404),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should respond with 404 if the path does not match exactly', async () => {
|
||||||
|
await Promise.all([
|
||||||
|
agent.get('/can-have-public-preview/42/foo').expect(404),
|
||||||
|
agent.get('/can-have-public-preview-foo/42').expect(404),
|
||||||
|
agent.get('/can-have-public-previewnfoo/42').expect(404),
|
||||||
|
agent.get('/foo/can-have-public-preview/42').expect(404),
|
||||||
|
agent.get('/foo-can-have-public-preview/42').expect(404),
|
||||||
|
agent.get('/fooncan-have-public-preview/42').expect(404),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should respond appropriately if the PR did not touch any significant files', async () => {
|
||||||
|
bvGetSignificantFilesChangedSpy.and.returnValue(Promise.resolve(false));
|
||||||
|
|
||||||
|
const expectedResponse = {canHavePublicPreview: false, reason: 'No significant files touched.'};
|
||||||
|
const expectedLog = `PR:${pr} - Cannot have a public preview, because it did not touch any significant files.`;
|
||||||
|
|
||||||
|
await agent.get(url).expect(200, expectedResponse);
|
||||||
|
|
||||||
|
expect(bvGetSignificantFilesChangedSpy).toHaveBeenCalledWith(pr, jasmine.any(RegExp));
|
||||||
|
expect(bvGetPrIsTrustedSpy).not.toHaveBeenCalled();
|
||||||
|
expect(loggerLogSpy).toHaveBeenCalledWith(expectedLog);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should respond appropriately if the PR is not automatically verifiable as "trusted"', async () => {
|
||||||
|
bvGetPrIsTrustedSpy.and.returnValue(Promise.resolve(false));
|
||||||
|
|
||||||
|
const expectedResponse = {canHavePublicPreview: false, reason: 'Not automatically verifiable as "trusted".'};
|
||||||
|
const expectedLog =
|
||||||
|
`PR:${pr} - Cannot have a public preview, because not automatically verifiable as "trusted".`;
|
||||||
|
|
||||||
|
await agent.get(url).expect(200, expectedResponse);
|
||||||
|
|
||||||
|
expect(bvGetSignificantFilesChangedSpy).toHaveBeenCalledWith(pr, jasmine.any(RegExp));
|
||||||
|
expect(bvGetPrIsTrustedSpy).toHaveBeenCalledWith(pr);
|
||||||
|
expect(loggerLogSpy).toHaveBeenCalledWith(expectedLog);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should respond appropriately if the PR can have a preview', async () => {
|
||||||
|
const expectedResponse = {canHavePublicPreview: true, reason: null};
|
||||||
|
const expectedLog = `PR:${pr} - Can have a public preview.`;
|
||||||
|
|
||||||
|
await agent.get(url).expect(200, expectedResponse);
|
||||||
|
|
||||||
|
expect(bvGetSignificantFilesChangedSpy).toHaveBeenCalledWith(pr, jasmine.any(RegExp));
|
||||||
|
expect(bvGetPrIsTrustedSpy).toHaveBeenCalledWith(pr);
|
||||||
|
expect(loggerLogSpy).toHaveBeenCalledWith(expectedLog);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should respond with error if `getSignificantFilesChanged()` fails', async () => {
|
||||||
|
bvGetSignificantFilesChangedSpy.and.callFake(() => Promise.reject('getSignificantFilesChanged error'));
|
||||||
|
|
||||||
|
await agent.get(url).expect(500, 'getSignificantFilesChanged error');
|
||||||
|
expect(loggerErrorSpy).toHaveBeenCalledWith('Previewability check error', 'getSignificantFilesChanged error');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should respond with error if `getPrIsTrusted()` fails', async () => {
|
||||||
|
const error = new Error('getPrIsTrusted error');
|
||||||
|
bvGetPrIsTrustedSpy.and.callFake(() => { throw error; });
|
||||||
|
|
||||||
|
await agent.get(url).expect(500, 'getPrIsTrusted error');
|
||||||
|
expect(loggerErrorSpy).toHaveBeenCalledWith('Previewability check error', error);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('POST /circle-build', () => {
|
||||||
let getGithubInfoSpy: jasmine.Spy;
|
let getGithubInfoSpy: jasmine.Spy;
|
||||||
let getSignificantFilesChangedSpy: jasmine.Spy;
|
let getSignificantFilesChangedSpy: jasmine.Spy;
|
||||||
let downloadBuildArtifactSpy: jasmine.Spy;
|
let downloadBuildArtifactSpy: jasmine.Spy;
|
||||||
@ -359,7 +455,7 @@ describe('PreviewServerFactory', () => {
|
|||||||
await agent.post(URL).send(BASIC_PAYLOAD).expect(204);
|
await agent.post(URL).send(BASIC_PAYLOAD).expect(204);
|
||||||
expect(getGithubInfoSpy).not.toHaveBeenCalled();
|
expect(getGithubInfoSpy).not.toHaveBeenCalled();
|
||||||
expect(getSignificantFilesChangedSpy).not.toHaveBeenCalled();
|
expect(getSignificantFilesChangedSpy).not.toHaveBeenCalled();
|
||||||
expect(console.log).toHaveBeenCalledWith(jasmine.any(String), 'PreviewServer: ',
|
expect(loggerLogSpy).toHaveBeenCalledWith(
|
||||||
'Build:12345, Job:lint -', 'Skipping preview processing because this is not the "aio_preview" job.');
|
'Build:12345, Job:lint -', 'Skipping preview processing because this is not the "aio_preview" job.');
|
||||||
expect(downloadBuildArtifactSpy).not.toHaveBeenCalled();
|
expect(downloadBuildArtifactSpy).not.toHaveBeenCalled();
|
||||||
expect(getPrIsTrustedSpy).not.toHaveBeenCalled();
|
expect(getPrIsTrustedSpy).not.toHaveBeenCalled();
|
||||||
@ -371,7 +467,7 @@ describe('PreviewServerFactory', () => {
|
|||||||
await agent.post(URL).send(BASIC_PAYLOAD).expect(204);
|
await agent.post(URL).send(BASIC_PAYLOAD).expect(204);
|
||||||
expect(getGithubInfoSpy).toHaveBeenCalledWith(BUILD_NUM);
|
expect(getGithubInfoSpy).toHaveBeenCalledWith(BUILD_NUM);
|
||||||
expect(getSignificantFilesChangedSpy).toHaveBeenCalledWith(PR, jasmine.any(RegExp));
|
expect(getSignificantFilesChangedSpy).toHaveBeenCalledWith(PR, jasmine.any(RegExp));
|
||||||
expect(console.log).toHaveBeenCalledWith(jasmine.any(String), 'PreviewServer: ',
|
expect(loggerLogSpy).toHaveBeenCalledWith(
|
||||||
'PR:777, Build:12345 - Skipping preview processing because this PR did not touch any significant files.');
|
'PR:777, Build:12345 - Skipping preview processing because this PR did not touch any significant files.');
|
||||||
expect(downloadBuildArtifactSpy).not.toHaveBeenCalled();
|
expect(downloadBuildArtifactSpy).not.toHaveBeenCalled();
|
||||||
expect(getPrIsTrustedSpy).not.toHaveBeenCalled();
|
expect(getPrIsTrustedSpy).not.toHaveBeenCalled();
|
||||||
@ -467,7 +563,7 @@ describe('PreviewServerFactory', () => {
|
|||||||
|
|
||||||
|
|
||||||
it('should respond with 404 for non-POST requests', async () => {
|
it('should respond with 404 for non-POST requests', async () => {
|
||||||
await verifyRequests([
|
await Promise.all([
|
||||||
agent.get(url).expect(404),
|
agent.get(url).expect(404),
|
||||||
agent.put(url).expect(404),
|
agent.put(url).expect(404),
|
||||||
agent.patch(url).expect(404),
|
agent.patch(url).expect(404),
|
||||||
@ -482,7 +578,7 @@ describe('PreviewServerFactory', () => {
|
|||||||
const request1 = agent.post(url);
|
const request1 = agent.post(url);
|
||||||
const request2 = agent.post(url).send();
|
const request2 = agent.post(url).send();
|
||||||
|
|
||||||
await verifyRequests([
|
await Promise.all([
|
||||||
request1.expect(400, responseBody),
|
request1.expect(400, responseBody),
|
||||||
request2.expect(400, responseBody),
|
request2.expect(400, responseBody),
|
||||||
]);
|
]);
|
||||||
@ -495,7 +591,7 @@ describe('PreviewServerFactory', () => {
|
|||||||
const request1 = agent.post(url).send({});
|
const request1 = agent.post(url).send({});
|
||||||
const request2 = agent.post(url).send({number: null});
|
const request2 = agent.post(url).send({number: null});
|
||||||
|
|
||||||
await verifyRequests([
|
await Promise.all([
|
||||||
request1.expect(400, `${responseBodyPrefix} {}`),
|
request1.expect(400, `${responseBodyPrefix} {}`),
|
||||||
request2.expect(400, `${responseBodyPrefix} {"number":null}`),
|
request2.expect(400, `${responseBodyPrefix} {"number":null}`),
|
||||||
]);
|
]);
|
||||||
@ -503,7 +599,7 @@ describe('PreviewServerFactory', () => {
|
|||||||
|
|
||||||
|
|
||||||
it('should call \'BuildVerifier#gtPrIsTrusted()\' with the correct arguments', async () => {
|
it('should call \'BuildVerifier#gtPrIsTrusted()\' with the correct arguments', async () => {
|
||||||
await promisifyRequest(createRequest(+pr));
|
await createRequest(+pr);
|
||||||
expect(bvGetPrIsTrustedSpy).toHaveBeenCalledWith(9);
|
expect(bvGetPrIsTrustedSpy).toHaveBeenCalledWith(9);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -511,9 +607,8 @@ describe('PreviewServerFactory', () => {
|
|||||||
it('should propagate errors from BuildVerifier', async () => {
|
it('should propagate errors from BuildVerifier', async () => {
|
||||||
bvGetPrIsTrustedSpy.and.callFake(() => Promise.reject('Test'));
|
bvGetPrIsTrustedSpy.and.callFake(() => Promise.reject('Test'));
|
||||||
|
|
||||||
const req = createRequest(+pr).expect(500, 'Test');
|
await createRequest(+pr).expect(500, 'Test');
|
||||||
|
|
||||||
await promisifyRequest(req);
|
|
||||||
expect(bvGetPrIsTrustedSpy).toHaveBeenCalledWith(9);
|
expect(bvGetPrIsTrustedSpy).toHaveBeenCalledWith(9);
|
||||||
expect(bcUpdatePrVisibilitySpy).not.toHaveBeenCalled();
|
expect(bcUpdatePrVisibilitySpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@ -522,19 +617,17 @@ describe('PreviewServerFactory', () => {
|
|||||||
it('should call \'BuildCreator#updatePrVisibility()\' with the correct arguments', async () => {
|
it('should call \'BuildCreator#updatePrVisibility()\' with the correct arguments', async () => {
|
||||||
bvGetPrIsTrustedSpy.and.callFake((pr2: number) => Promise.resolve(pr2 === 42));
|
bvGetPrIsTrustedSpy.and.callFake((pr2: number) => Promise.resolve(pr2 === 42));
|
||||||
|
|
||||||
await promisifyRequest(createRequest(24));
|
await createRequest(24);
|
||||||
expect(bcUpdatePrVisibilitySpy).toHaveBeenCalledWith(24, false);
|
expect(bcUpdatePrVisibilitySpy).toHaveBeenCalledWith(24, false);
|
||||||
|
|
||||||
await promisifyRequest(createRequest(42));
|
await createRequest(42);
|
||||||
expect(bcUpdatePrVisibilitySpy).toHaveBeenCalledWith(42, true);
|
expect(bcUpdatePrVisibilitySpy).toHaveBeenCalledWith(42, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should propagate errors from BuildCreator', async () => {
|
it('should propagate errors from BuildCreator', async () => {
|
||||||
bcUpdatePrVisibilitySpy.and.callFake(() => Promise.reject('Test'));
|
bcUpdatePrVisibilitySpy.and.callFake(() => Promise.reject('Test'));
|
||||||
|
await createRequest(+pr).expect(500, 'Test');
|
||||||
const req = createRequest(+pr).expect(500, 'Test');
|
|
||||||
await verifyRequests([req]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -544,7 +637,7 @@ describe('PreviewServerFactory', () => {
|
|||||||
bvGetPrIsTrustedSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false));
|
bvGetPrIsTrustedSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false));
|
||||||
|
|
||||||
const reqs = [4, 2].map(num => createRequest(num).expect(200, http.STATUS_CODES[200]));
|
const reqs = [4, 2].map(num => createRequest(num).expect(200, http.STATUS_CODES[200]));
|
||||||
await verifyRequests(reqs);
|
await Promise.all(reqs);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -552,7 +645,7 @@ describe('PreviewServerFactory', () => {
|
|||||||
bvGetPrIsTrustedSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false));
|
bvGetPrIsTrustedSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false));
|
||||||
|
|
||||||
const reqs = [4, 2].map(num => createRequest(num, 'labeled').expect(200, http.STATUS_CODES[200]));
|
const reqs = [4, 2].map(num => createRequest(num, 'labeled').expect(200, http.STATUS_CODES[200]));
|
||||||
await verifyRequests(reqs);
|
await Promise.all(reqs);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -560,14 +653,13 @@ describe('PreviewServerFactory', () => {
|
|||||||
bvGetPrIsTrustedSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false));
|
bvGetPrIsTrustedSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false));
|
||||||
|
|
||||||
const reqs = [4, 2].map(num => createRequest(num, 'unlabeled').expect(200, http.STATUS_CODES[200]));
|
const reqs = [4, 2].map(num => createRequest(num, 'unlabeled').expect(200, http.STATUS_CODES[200]));
|
||||||
await verifyRequests(reqs);
|
await Promise.all(reqs);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should respond with 200 (and do nothing) if \'action\' implies no visibility change', async () => {
|
it('should respond with 200 (and do nothing) if \'action\' implies no visibility change', async () => {
|
||||||
const promises = ['foo', 'notlabeled'].
|
const promises = ['foo', 'notlabeled'].
|
||||||
map(action => createRequest(+pr, action).expect(200, http.STATUS_CODES[200])).
|
map(action => createRequest(+pr, action).expect(200, http.STATUS_CODES[200]));
|
||||||
map(promisifyRequest);
|
|
||||||
|
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
expect(bvGetPrIsTrustedSpy).not.toHaveBeenCalled();
|
expect(bvGetPrIsTrustedSpy).not.toHaveBeenCalled();
|
||||||
@ -584,7 +676,7 @@ describe('PreviewServerFactory', () => {
|
|||||||
it('should respond with 404', async () => {
|
it('should respond with 404', async () => {
|
||||||
const responseFor = (method: string) => `Unknown resource in request: ${method.toUpperCase()} /some/url`;
|
const responseFor = (method: string) => `Unknown resource in request: ${method.toUpperCase()} /some/url`;
|
||||||
|
|
||||||
await verifyRequests([
|
await Promise.all([
|
||||||
agent.get('/some/url').expect(404, responseFor('get')),
|
agent.get('/some/url').expect(404, responseFor('get')),
|
||||||
agent.put('/some/url').expect(404, responseFor('put')),
|
agent.put('/some/url').expect(404, responseFor('put')),
|
||||||
agent.post('/some/url').expect(404, responseFor('post')),
|
agent.post('/some/url').expect(404, responseFor('post')),
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -2,6 +2,7 @@
|
|||||||
set -eu -o pipefail
|
set -eu -o pipefail
|
||||||
|
|
||||||
# Set up env variables
|
# Set up env variables
|
||||||
|
export AIO_CIRCLE_CI_TOKEN=UNUSED_CIRCLE_CI_TOKEN
|
||||||
export AIO_GITHUB_TOKEN=$(head -c -1 /aio-secrets/GITHUB_TOKEN 2>/dev/null)
|
export AIO_GITHUB_TOKEN=$(head -c -1 /aio-secrets/GITHUB_TOKEN 2>/dev/null)
|
||||||
|
|
||||||
# Run the clean-up
|
# Run the clean-up
|
||||||
|
@ -5,4 +5,5 @@ TODO (gkalpak): Add docs. Mention:
|
|||||||
- Testing on CI.
|
- Testing on CI.
|
||||||
Relevant files: `scripts/ci/test-aio.sh`, `aio/aio-builds-setup/scripts/test.sh`
|
Relevant files: `scripts/ci/test-aio.sh`, `aio/aio-builds-setup/scripts/test.sh`
|
||||||
- Deploying from CI.
|
- Deploying from CI.
|
||||||
Relevant files: `scripts/ci/deploy.sh`, `aio/scripts/deploy-to-firebase.sh`
|
Relevant files: `.circleci/config.yml`, `scripts/ci/deploy.sh`, `aio/scripts/build-artifacts.sh`,
|
||||||
|
`aio/scripts/deploy-to-firebase.sh`
|
||||||
|
@ -34,34 +34,31 @@ container:
|
|||||||
|
|
||||||
|
|
||||||
### On CI (CircleCI)
|
### On CI (CircleCI)
|
||||||
- Build job completes successfully.
|
- The CI script builds the angular.io project.
|
||||||
- The CI script checks whether the build job was initiated by a PR against the angular/angular
|
|
||||||
master branch.
|
|
||||||
- The CI script checks whether the PR has touched any files that might affect the angular.io app
|
|
||||||
(currently the `aio/` or `packages/` directories, ignoring spec files).
|
|
||||||
- The CI script gzips and stores the build artifacts in the CI infrastructure.
|
- The CI script gzips and stores the build artifacts in the CI infrastructure.
|
||||||
- When the build completes CircleCI triggers a webhook on the preview-server.
|
- When the build completes, CircleCI triggers a webhook on the preview-server.
|
||||||
|
|
||||||
More info on how to set things up on CI can be found [here](misc--integrate-with-ci.md).
|
More info on how to set things up on CI can be found [here](misc--integrate-with-ci.md).
|
||||||
|
|
||||||
|
|
||||||
### Hosting build artifacts
|
### Hosting build artifacts
|
||||||
|
|
||||||
- nginx receives the webhook trigger and passes it through to the preview server.
|
- nginx receives the webhook trigger and passes it through to the preview server.
|
||||||
|
- The preview-server runs several preliminary checks to determine whether the request is valid and
|
||||||
|
whether the corresponding PR can have a (public or non-public) preview (more details can be found
|
||||||
|
[here](overview--security-model.md)).
|
||||||
- The preview-server makes a request to CircleCI for the URL of the AIO build artifacts.
|
- The preview-server makes a request to CircleCI for the URL of the AIO build artifacts.
|
||||||
- The preview-server makes a request to this URL to receive the artifact - failing if the size
|
- The preview-server makes a request to this URL to receive the artifact - failing if the size
|
||||||
exceeds the specified max file size - and stores it in a temporary location.
|
exceeds the specified max file size - and stores it in a temporary location.
|
||||||
- The preview-server runs several checks to determine whether the request should be accepted and
|
- The preview-server runs more checks to determine whether the preview should be publicly accessible
|
||||||
whether it should be publicly accessible or stored for later verification (more details can be
|
or stored for later verification (more details can be found [here](overview--security-model.md)).
|
||||||
found [here](overview--security-model.md)).
|
|
||||||
- The preview-server changes the "visibility" of the associated PR, if necessary. For example, if
|
- The preview-server changes the "visibility" of the associated PR, if necessary. For example, if
|
||||||
builds for the same PR had been previously deployed as non-public and the current build has been
|
builds for the same PR had been previously deployed as non-public and the current build has been
|
||||||
automatically verified, all previous builds are made public as well.
|
automatically verified, all previous builds are made public as well.
|
||||||
If the PR transitions from "non-public" to "public", the preview-server posts a comment on the
|
If the PR transitions from "non-public" to "public", the preview-server posts a comment on the
|
||||||
corresponding PR on GitHub mentioning the SHAs and the links where the previews can be found.
|
corresponding PR on GitHub mentioning the SHAs and the links where the previews can be found.
|
||||||
- The preview-server verifies that it is not trying to overwrite an existing build.
|
- The preview-server verifies that it is not trying to overwrite an existing build.
|
||||||
- The preview-server deploys the artifacts to a sub-directory named after the PR number and the first
|
- The preview-server deploys the artifacts to a sub-directory named after the PR number and the
|
||||||
few characters of the SHA: `<PR>/<SHA>/`
|
first few characters of the SHA: `<PR>/<SHA>/`
|
||||||
(Non-publicly accessible PRs will be stored in a different location, but again derived from the PR
|
(Non-publicly accessible PRs will be stored in a different location, but again derived from the PR
|
||||||
number and SHA.)
|
number and SHA.)
|
||||||
- If the PR is publicly accessible, the preview-server posts a comment on the corresponding PR on
|
- If the PR is publicly accessible, the preview-server posts a comment on the corresponding PR on
|
||||||
@ -101,8 +98,8 @@ More info on the possible HTTP status codes and their meaning can be found
|
|||||||
|
|
||||||
### Removing obsolete artifacts
|
### Removing obsolete artifacts
|
||||||
In order to avoid flooding the disk with unnecessary build artifacts, there is a cronjob that runs a
|
In order to avoid flooding the disk with unnecessary build artifacts, there is a cronjob that runs a
|
||||||
clean-up tasks once a day. The task retrieves all open PRs from GitHub and removes all directories
|
clean-up task once a day. The task retrieves all open PRs from GitHub and removes all directories
|
||||||
that do not correspond with an open PR.
|
that do not correspond to an open PR.
|
||||||
|
|
||||||
|
|
||||||
### Health-check
|
### Health-check
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
# Overview - HTTP Status Codes
|
# Overview - HTTP Status Codes
|
||||||
|
|
||||||
|
|
||||||
This is a list of all the possible HTTP status codes returned by the nginx and preview servers, along
|
This is a list of all the possible HTTP status codes returned by the nginx and preview servers,
|
||||||
with a brief explanation of what they mean:
|
along with a brief explanation of what they mean:
|
||||||
|
|
||||||
|
|
||||||
## `http://*.ngbuilds.io/*`
|
## `http://*.ngbuilds.io/*`
|
||||||
@ -25,6 +25,23 @@ with a brief explanation of what they mean:
|
|||||||
File not found.
|
File not found.
|
||||||
|
|
||||||
|
|
||||||
|
## `https://ngbuilds.io/can-have-public-preview/<pr>`
|
||||||
|
|
||||||
|
- **200 (OK)**:
|
||||||
|
Whether the PR can have a public preview (based on its author, label, changed files).
|
||||||
|
_Response type:_ JSON
|
||||||
|
_Response format:_
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
canHavePublicPreview: boolean,
|
||||||
|
reason: string | null,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **405 (Method Not Allowed)**:
|
||||||
|
Request method other than GET.
|
||||||
|
|
||||||
|
|
||||||
## `https://ngbuilds.io/circle-build`
|
## `https://ngbuilds.io/circle-build`
|
||||||
|
|
||||||
- **201 (Created)**:
|
- **201 (Created)**:
|
||||||
|
@ -11,8 +11,8 @@ part of the CI process and serving them publicly.
|
|||||||
|
|
||||||
## Security objectives
|
## Security objectives
|
||||||
|
|
||||||
- **Prevent hosting arbitrary content to on servers.**
|
- **Prevent hosting arbitrary content on our servers.**
|
||||||
Since there is no restriction on who can submit a PR, we cannot allow arbitrary untrusted PRs'
|
Since there is no restriction on who can submit a PR, we cannot allow arbitrary, untrusted PRs'
|
||||||
build artifacts to be hosted.
|
build artifacts to be hosted.
|
||||||
|
|
||||||
- **Prevent overwriting other people's hosted build artifacts.**
|
- **Prevent overwriting other people's hosted build artifacts.**
|
||||||
@ -40,40 +40,49 @@ part of the CI process and serving them publicly.
|
|||||||
### In a nutshell
|
### In a nutshell
|
||||||
The implemented approach can be broken up to the following sub-tasks:
|
The implemented approach can be broken up to the following sub-tasks:
|
||||||
|
|
||||||
0. Receive notification from CircleCI of a completed build.
|
1. Receive notification from CircleCI of a completed build.
|
||||||
1. Verify that the build is valid and download the artifact.
|
2. Verify that the build is valid and can have a preview.
|
||||||
2. Fetch the PR's metadata, including author and labels.
|
3. Download the build artifact.
|
||||||
3. Check whether the PR can be automatically verified as "trusted" (based on its author or labels).
|
4. Fetch the PR's metadata, including author and labels.
|
||||||
4. If necessary, update the corresponding PR's verification status.
|
5. Check whether the PR can be automatically verified as "trusted" (based on its author or labels).
|
||||||
5. Deploy the artifacts to the corresponding PR's directory.
|
6. If necessary, update the corresponding PR's verification status.
|
||||||
6. Prevent overwriting previously deployed artifacts (which ensures that the guarantees established
|
7. Deploy the artifacts to the corresponding PR's directory.
|
||||||
|
8. Prevent overwriting previously deployed artifacts (which ensures that the guarantees established
|
||||||
during deployment will remain valid until the artifacts are removed).
|
during deployment will remain valid until the artifacts are removed).
|
||||||
7. Prevent hosted preview files from accessing anything outside their directory.
|
9. Prevent hosted preview files from accessing anything outside their directory.
|
||||||
|
|
||||||
|
|
||||||
### Implementation details
|
### Implementation details
|
||||||
This section describes how each of the aforementioned sub-tasks is accomplished:
|
This section describes how each of the aforementioned sub-tasks is accomplished:
|
||||||
|
|
||||||
0. **Receive notification from CircleCI of a completed build**
|
1. **Receive notification from CircleCI of a completed build**
|
||||||
|
|
||||||
CircleCI is configured to trigger a webhook on our preview-server whenever a build completes.
|
CircleCI is configured to trigger a webhook on our preview-server whenever a build completes.
|
||||||
The payload contains the number of the build that completed.
|
The payload contains the number of the build that completed.
|
||||||
|
|
||||||
1. **Verify that the build is valid and download the artifact.**
|
2. **Verify that the build is valid and can have a preview.**
|
||||||
|
|
||||||
We cannot trust that the data in the webhook trigger is authentic, so we only extract the build
|
We cannot trust that the data in the webhook trigger is authentic, so we only extract the build
|
||||||
number and then run a direct query against the CircleCI API to get hold of the real data for
|
number and then run a direct query against the CircleCI API to get hold of the real data for
|
||||||
the given build number.
|
the given build number.
|
||||||
|
|
||||||
If the build was not successful then we ignore this trigger. Otherwise we check that the
|
We perform a number of preliminary checks:
|
||||||
associated github organisation and repository are what we expect (e.g. angular/angular).
|
- Was the webhook triggered by the designated CircleCI job (currently `aio_preview`)?
|
||||||
|
- Was the build successful?
|
||||||
|
- Are the associated GitHub organisation and repository what we expect (e.g. `angular/angular`)?
|
||||||
|
- Has the PR touched any files that might affect the angular.io app (currently the `aio/` or
|
||||||
|
`packages/` directories, ignoring spec files)?
|
||||||
|
|
||||||
Next we make another call to the CircleCI API to get a list of the URLS for artifacts of that
|
If any of the preliminary checks fails, the process is aborted and not preview is generated.
|
||||||
|
|
||||||
|
3. **Download the build artifact.**
|
||||||
|
|
||||||
|
Next we make another call to the CircleCI API to get a list of the URLs for artifacts of that
|
||||||
build. If there is one that matches the configured artifact path, we download the contents of the
|
build. If there is one that matches the configured artifact path, we download the contents of the
|
||||||
build artifact and store it in a local folder. This download has a maximum size limit to prevent
|
build artifact and store it in a local folder. This download has a maximum size limit to prevent
|
||||||
PRs from producing artifacts that are so large they would cause the preview server to crash.
|
PRs from producing artifacts that are so large they would cause the preview server to crash.
|
||||||
|
|
||||||
2. **Fetch the PR's metadata, including author and labels**.
|
4. **Fetch the PR's metadata, including author and labels**.
|
||||||
|
|
||||||
Once we have securely downloaded the artifact for a build, we retrieve the PR's metadata -
|
Once we have securely downloaded the artifact for a build, we retrieve the PR's metadata -
|
||||||
including the author's username and the labels - using the
|
including the author's username and the labels - using the
|
||||||
@ -81,7 +90,7 @@ This section describes how each of the aforementioned sub-tasks is accomplished:
|
|||||||
To avoid rate-limit restrictions, we use a Personal Access Token (issued by
|
To avoid rate-limit restrictions, we use a Personal Access Token (issued by
|
||||||
[@mary-poppins](https://github.com/mary-poppins)).
|
[@mary-poppins](https://github.com/mary-poppins)).
|
||||||
|
|
||||||
3. **Check whether the PR can be automatically verified as "trusted"**.
|
5. **Check whether the PR can be automatically verified as "trusted"**.
|
||||||
|
|
||||||
"Trusted" means that we are confident that the build artifacts are suitable for being deployed
|
"Trusted" means that we are confident that the build artifacts are suitable for being deployed
|
||||||
and publicly accessible on the preview server. There are two ways to check that:
|
and publicly accessible on the preview server. There are two ways to check that:
|
||||||
@ -93,31 +102,32 @@ This section describes how each of the aforementioned sub-tasks is accomplished:
|
|||||||
`read:org` scope issued by a user that can "see" the specified GitHub organization.
|
`read:org` scope issued by a user that can "see" the specified GitHub organization.
|
||||||
Here too, we use the token by @mary-poppins.
|
Here too, we use the token by @mary-poppins.
|
||||||
|
|
||||||
4. **If necessary update the corresponding PR's verification status**.
|
6. **If necessary update the corresponding PR's verification status**.
|
||||||
|
|
||||||
Once we have determined whether the PR is considered "trusted", we update its "visibility" (i.e.
|
Once we have determined whether the PR is considered "trusted", we update its "visibility" (i.e.
|
||||||
whether it is publicly accessible or not), based on the new verification status. For example, if
|
whether it is publicly accessible or not), based on the new verification status. For example, if
|
||||||
a PR was initially considered "not trusted" but the check triggered by a new build determined
|
a PR was initially considered "not trusted" but the check triggered by a new build determined
|
||||||
otherwise, the PR (and all the previously hosted previews) are made public. It works the same
|
otherwise, the PR (and all the previously downloaded previews) are made public. It works the same
|
||||||
way if a PR has gone from "trusted" to "not trusted".
|
way if a PR has gone from "trusted" to "not trusted".
|
||||||
|
|
||||||
5. **Deploy the artifacts to the corresponding PR's directory.**
|
7. **Deploy the artifacts to the corresponding PR's directory.**
|
||||||
|
|
||||||
With the preceding steps, we have verified that the build artifacts are valid.
|
With the preceding steps, we have verified that the build artifacts are valid. Additionally, we
|
||||||
Additionally, we have determined whether the PR can be trusted to have its previews
|
have determined whether the PR can be trusted to have its previews publicly accessible or whether
|
||||||
publicly accessible or whether further verification is necessary. The artifacts will be stored to
|
further verification is necessary.
|
||||||
the PR's directory, but will not be publicly accessible unless the PR has been verified.
|
|
||||||
Essentially, as long as sub-tasks 1, 2 and 3 can be securely accomplished, it is possible to
|
|
||||||
"project" the trust we have in a team's members through the PR to the build artifacts.
|
|
||||||
|
|
||||||
6. **Prevent overwriting previously deployed artifacts**.
|
The artifacts will be stored to the PR's directory, but will not be publicly accessible unless
|
||||||
|
the PR has been verified. Essentially, as long as sub-tasks 2, 3, 4 and 5 can be securely
|
||||||
|
accomplished, it is possible to "project" the trust we have in a team's members through the PR to
|
||||||
|
the build artifacts.
|
||||||
|
|
||||||
|
8. **Prevent overwriting previously deployed artifacts**.
|
||||||
|
|
||||||
In order to enforce this restriction (and ensure that the deployed artifacts' validity is
|
In order to enforce this restriction (and ensure that the deployed artifacts' validity is
|
||||||
preserved throughout their "lifetime"), the server that handles the artifacts (currently a Node.js
|
preserved throughout their "lifetime"), the server that handles the artifacts (currently a Node.js Express server) rejects builds that have already been handled.
|
||||||
Express server) rejects builds that have already been handled.
|
|
||||||
_Note: A PR can contain multiple builds; one for each SHA that was built on CircleCI._
|
_Note: A PR can contain multiple builds; one for each SHA that was built on CircleCI._
|
||||||
|
|
||||||
7. **Prevent hosted preview files from accessing anything outside their directory.**
|
9. **Prevent hosted preview files from accessing anything outside their directory.**
|
||||||
|
|
||||||
Nginx (which is used to serve the hosted preview) has been configured to not follow symlinks
|
Nginx (which is used to serve the hosted preview) has been configured to not follow symlinks
|
||||||
outside of the directory where the preview files are stored.
|
outside of the directory where the preview files are stored.
|
||||||
@ -130,10 +140,10 @@ This section describes how each of the aforementioned sub-tasks is accomplished:
|
|||||||
This means that any secret access keys need only be stored on the preview-server and not on any of
|
This means that any secret access keys need only be stored on the preview-server and not on any of
|
||||||
the CI build infrastructure (e.g. CircleCI).
|
the CI build infrastructure (e.g. CircleCI).
|
||||||
|
|
||||||
- Each trusted PR author has full control over the content that is hosted as a preview for their PRs.
|
- Each trusted PR author has full control over the content that is hosted as a preview for their
|
||||||
Part of the security model relies on the trustworthiness of these authors.
|
PRs. Part of the security model relies on the trustworthiness of these authors.
|
||||||
|
|
||||||
- Adding the specified label on a PR to mark it as trusted, gives the author full control over
|
- Adding the specified label on a PR to mark it as trusted, gives the author full control over the
|
||||||
the content that is hosted for the specific PR preview (e.g. by pushing more commits to it).
|
content that is hosted for the specific PR preview (e.g. by pushing more commits to it). The user
|
||||||
The user adding the label is responsible for ensuring that this control is not abused and that
|
adding the label is responsible for ensuring that this control is not abused and that the PR is
|
||||||
the PR is either closed (one way of another) or the access is revoked.
|
either closed (one way of another) or the access is revoked.
|
||||||
|
@ -8,7 +8,7 @@ Necessary secrets:
|
|||||||
1. `GITHUB_TOKEN`
|
1. `GITHUB_TOKEN`
|
||||||
- Used for:
|
- Used for:
|
||||||
- Retrieving open PRs without rate-limiting.
|
- Retrieving open PRs without rate-limiting.
|
||||||
- Retrieving PR author.
|
- Retrieving PR info, such as author, labels, changed files.
|
||||||
- Retrieving members of the trusted GitHub teams.
|
- Retrieving members of the trusted GitHub teams.
|
||||||
- Posting comments with preview links on PRs.
|
- Posting comments with preview links on PRs.
|
||||||
|
|
||||||
@ -25,8 +25,9 @@ Necessary secrets:
|
|||||||
- Generate new token with the `public_repo` scope.
|
- Generate new token with the `public_repo` scope.
|
||||||
|
|
||||||
2. `CIRCLE_CI_TOKEN`
|
2. `CIRCLE_CI_TOKEN`
|
||||||
- Visit https://circleci.com/gh/angular/angular/edit#api
|
- Visit https://circleci.com/gh/angular/angular/edit#api.
|
||||||
- Create an API token with `Build Artifacts` scope
|
- Create an API token with `Build Artifacts` scope.
|
||||||
|
|
||||||
|
|
||||||
## Save secrets on the VM
|
## Save secrets on the VM
|
||||||
|
|
||||||
|
@ -33,7 +33,6 @@
|
|||||||
"src/assets",
|
"src/assets",
|
||||||
"src/generated",
|
"src/generated",
|
||||||
"src/app/search/search-worker.js",
|
"src/app/search/search-worker.js",
|
||||||
"src/favicon.ico",
|
|
||||||
"src/pwa-manifest.json",
|
"src/pwa-manifest.json",
|
||||||
"src/google385281288605d160.html",
|
"src/google385281288605d160.html",
|
||||||
{
|
{
|
||||||
@ -62,7 +61,8 @@
|
|||||||
"src": "src/environments/environment.ts",
|
"src": "src/environments/environment.ts",
|
||||||
"replaceWith": "src/environments/environment.next.ts"
|
"replaceWith": "src/environments/environment.next.ts"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"serviceWorker": true
|
||||||
},
|
},
|
||||||
"stable": {
|
"stable": {
|
||||||
"fileReplacements": [
|
"fileReplacements": [
|
||||||
@ -70,7 +70,8 @@
|
|||||||
"src": "src/environments/environment.ts",
|
"src": "src/environments/environment.ts",
|
||||||
"replaceWith": "src/environments/environment.stable.ts"
|
"replaceWith": "src/environments/environment.stable.ts"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"serviceWorker": true
|
||||||
},
|
},
|
||||||
"archive": {
|
"archive": {
|
||||||
"fileReplacements": [
|
"fileReplacements": [
|
||||||
@ -78,7 +79,8 @@
|
|||||||
"src": "src/environments/environment.ts",
|
"src": "src/environments/environment.ts",
|
||||||
"replaceWith": "src/environments/environment.archive.ts"
|
"replaceWith": "src/environments/environment.archive.ts"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"serviceWorker": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -123,7 +125,6 @@
|
|||||||
"src/assets",
|
"src/assets",
|
||||||
"src/generated",
|
"src/generated",
|
||||||
"src/app/search/search-worker.js",
|
"src/app/search/search-worker.js",
|
||||||
"src/favicon.ico",
|
|
||||||
"src/pwa-manifest.json",
|
"src/pwa-manifest.json",
|
||||||
"src/google385281288605d160.html",
|
"src/google385281288605d160.html",
|
||||||
{
|
{
|
||||||
|
@ -1,351 +1,259 @@
|
|||||||
'use strict'; // necessary for es6 output in node
|
'use strict'; // necessary for es6 output in node
|
||||||
|
|
||||||
import { browser, element, by, ElementFinder } from 'protractor';
|
import { browser } from 'protractor';
|
||||||
import { logging, promise } from 'selenium-webdriver';
|
import { logging } from 'selenium-webdriver';
|
||||||
|
import * as openClose from './open-close.po';
|
||||||
|
import * as statusSlider from './status-slider.po';
|
||||||
|
import * as toggle from './toggle.po';
|
||||||
|
import * as enterLeave from './enter-leave.po';
|
||||||
|
import * as auto from './auto.po';
|
||||||
|
import * as filterStagger from './filter-stagger.po';
|
||||||
|
import * as heroGroups from './hero-groups';
|
||||||
|
import { getLinkById, sleepFor } from './util';
|
||||||
|
|
||||||
/**
|
|
||||||
* The tests here basically just checking that the end styles
|
|
||||||
* of each animation are in effect.
|
|
||||||
*
|
|
||||||
* Relies on the Angular testability only becoming stable once
|
|
||||||
* animation(s) have finished.
|
|
||||||
*
|
|
||||||
* Ideally we'd use https://developer.mozilla.org/en-US/docs/Web/API/Document/getAnimations
|
|
||||||
* but they're not supported in Chrome at the moment. The upcoming nganimate polyfill
|
|
||||||
* may also add some introspection support.
|
|
||||||
*/
|
|
||||||
describe('Animation Tests', () => {
|
describe('Animation Tests', () => {
|
||||||
|
const openCloseHref = getLinkById('open-close');
|
||||||
|
const statusSliderHref = getLinkById('status');
|
||||||
|
const toggleHref = getLinkById('toggle');
|
||||||
|
const enterLeaveHref = getLinkById('enter-leave');
|
||||||
|
const autoHref = getLinkById('auto');
|
||||||
|
const filterHref = getLinkById('heroes');
|
||||||
|
const heroGroupsHref = getLinkById('hero-groups');
|
||||||
|
|
||||||
const INACTIVE_COLOR = 'rgba(238, 238, 238, 1)';
|
beforeAll(() => {
|
||||||
const ACTIVE_COLOR = 'rgba(207, 216, 220, 1)';
|
|
||||||
const NO_TRANSFORM_MATRIX_REGEX = /matrix\(1,\s*0,\s*0,\s*1,\s*0,\s*0\)/;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
browser.get('');
|
browser.get('');
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('basic states', () => {
|
describe('Open/Close Component', () => {
|
||||||
|
|
||||||
let host: ElementFinder;
|
beforeAll(async () => {
|
||||||
|
await openCloseHref.click();
|
||||||
beforeEach(() => {
|
sleepFor();
|
||||||
host = element(by.css('app-hero-list-basic'));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('animates between active and inactive', () => {
|
it('should be open', async () => {
|
||||||
addInactiveHero();
|
let text = await openClose.getComponentText();
|
||||||
|
const toggleButton = openClose.getToggleButton();
|
||||||
|
const container = openClose.getComponentContainer();
|
||||||
|
|
||||||
let li = host.element(by.css('li'));
|
if (text.includes('Closed')) {
|
||||||
|
await toggleButton.click();
|
||||||
|
sleepFor();
|
||||||
|
}
|
||||||
|
|
||||||
expect(getScaleX(li)).toBe(1.0);
|
text = await openClose.getComponentText();
|
||||||
expect(li.getCssValue('backgroundColor')).toBe(INACTIVE_COLOR);
|
const containerHeight = await container.getCssValue('height');
|
||||||
|
|
||||||
li.click();
|
expect(text).toContain('The box is now Open!');
|
||||||
browser.driver.sleep(300);
|
expect(containerHeight).toBe('200px');
|
||||||
expect(getScaleX(li)).toBe(1.1);
|
|
||||||
expect(li.getCssValue('backgroundColor')).toBe(ACTIVE_COLOR);
|
|
||||||
|
|
||||||
li.click();
|
|
||||||
browser.driver.sleep(300);
|
|
||||||
expect(getScaleX(li)).toBe(1.0);
|
|
||||||
expect(li.getCssValue('backgroundColor')).toBe(INACTIVE_COLOR);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should be closed', async () => {
|
||||||
|
let text = await openClose.getComponentText();
|
||||||
|
const toggleButton = openClose.getToggleButton();
|
||||||
|
const container = openClose.getComponentContainer();
|
||||||
|
|
||||||
|
if (text.includes('Open')) {
|
||||||
|
await toggleButton.click();
|
||||||
|
sleepFor();
|
||||||
|
}
|
||||||
|
|
||||||
|
text = await openClose.getComponentText();
|
||||||
|
const containerHeight = await container.getCssValue('height');
|
||||||
|
|
||||||
|
expect(text).toContain('The box is now Closed!');
|
||||||
|
expect(containerHeight).toBe('100px');
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('styles inline in transitions', () => {
|
it('should log animation events', async () => {
|
||||||
|
const toggleButton = openClose.getToggleButton();
|
||||||
|
const loggingCheckbox = openClose.getLoggingCheckbox();
|
||||||
|
await loggingCheckbox.click();
|
||||||
|
await toggleButton.click();
|
||||||
|
|
||||||
let host: ElementFinder;
|
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
|
||||||
|
|
||||||
beforeEach(function() {
|
const animationMessages = logs.filter(({ message }) => message.indexOf('Animation') !== -1 ? true : false);
|
||||||
host = element(by.css('app-hero-list-inline-styles'));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('are not kept after animation', () => {
|
|
||||||
addInactiveHero();
|
|
||||||
|
|
||||||
let li = host.element(by.css('li'));
|
|
||||||
|
|
||||||
li.click();
|
|
||||||
browser.driver.sleep(300);
|
|
||||||
expect(getScaleX(li)).toBe(1.0);
|
|
||||||
expect(li.getCssValue('backgroundColor')).toBe(INACTIVE_COLOR);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('combined transition syntax', () => {
|
|
||||||
|
|
||||||
let host: ElementFinder;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
host = element(by.css('app-hero-list-combined-transitions'));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('animates between active and inactive', () => {
|
|
||||||
addInactiveHero();
|
|
||||||
|
|
||||||
let li = host.element(by.css('li'));
|
|
||||||
|
|
||||||
expect(getScaleX(li)).toBe(1.0);
|
|
||||||
expect(li.getCssValue('backgroundColor')).toBe(INACTIVE_COLOR);
|
|
||||||
|
|
||||||
li.click();
|
|
||||||
browser.driver.sleep(300);
|
|
||||||
expect(getScaleX(li)).toBe(1.1);
|
|
||||||
expect(li.getCssValue('backgroundColor')).toBe(ACTIVE_COLOR);
|
|
||||||
|
|
||||||
li.click();
|
|
||||||
browser.driver.sleep(300);
|
|
||||||
expect(getScaleX(li)).toBe(1.0);
|
|
||||||
expect(li.getCssValue('backgroundColor')).toBe(INACTIVE_COLOR);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('two-way transition syntax', () => {
|
|
||||||
|
|
||||||
let host: ElementFinder;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
host = element(by.css('app-hero-list-twoway'));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('animates between active and inactive', () => {
|
|
||||||
addInactiveHero();
|
|
||||||
|
|
||||||
let li = host.element(by.css('li'));
|
|
||||||
|
|
||||||
expect(getScaleX(li)).toBe(1.0);
|
|
||||||
expect(li.getCssValue('backgroundColor')).toBe(INACTIVE_COLOR);
|
|
||||||
|
|
||||||
li.click();
|
|
||||||
browser.driver.sleep(300);
|
|
||||||
expect(getScaleX(li)).toBe(1.1);
|
|
||||||
expect(li.getCssValue('backgroundColor')).toBe(ACTIVE_COLOR);
|
|
||||||
|
|
||||||
li.click();
|
|
||||||
browser.driver.sleep(300);
|
|
||||||
expect(getScaleX(li)).toBe(1.0);
|
|
||||||
expect(li.getCssValue('backgroundColor')).toBe(INACTIVE_COLOR);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('enter & leave', () => {
|
|
||||||
|
|
||||||
let host: ElementFinder;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
host = element(by.css('app-hero-list-enter-leave'));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('adds and removes element', () => {
|
|
||||||
addInactiveHero();
|
|
||||||
|
|
||||||
let li = host.element(by.css('li'));
|
|
||||||
expect(li.getCssValue('transform')).toMatch(NO_TRANSFORM_MATRIX_REGEX);
|
|
||||||
|
|
||||||
removeHero();
|
|
||||||
expect(li.isPresent()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('enter & leave & states', () => {
|
|
||||||
|
|
||||||
let host: ElementFinder;
|
|
||||||
|
|
||||||
beforeEach(function() {
|
|
||||||
host = element(by.css('app-hero-list-enter-leave-states'));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('adds and removes and animates between active and inactive', () => {
|
|
||||||
addInactiveHero();
|
|
||||||
|
|
||||||
let li = host.element(by.css('li'));
|
|
||||||
|
|
||||||
expect(li.getCssValue('transform')).toMatch(NO_TRANSFORM_MATRIX_REGEX);
|
|
||||||
|
|
||||||
li.click();
|
|
||||||
browser.driver.sleep(300);
|
|
||||||
expect(getScaleX(li)).toBe(1.1);
|
|
||||||
|
|
||||||
li.click();
|
|
||||||
browser.driver.sleep(300);
|
|
||||||
expect(li.getCssValue('transform')).toMatch(NO_TRANSFORM_MATRIX_REGEX);
|
|
||||||
|
|
||||||
removeHero();
|
|
||||||
expect(li.isPresent()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('auto style calc', () => {
|
|
||||||
|
|
||||||
let host: ElementFinder;
|
|
||||||
|
|
||||||
beforeEach(function() {
|
|
||||||
host = element(by.css('app-hero-list-auto'));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('adds and removes element', () => {
|
|
||||||
addInactiveHero();
|
|
||||||
|
|
||||||
let li = host.element(by.css('li'));
|
|
||||||
expect(li.getCssValue('height')).toBe('50px');
|
|
||||||
|
|
||||||
removeHero();
|
|
||||||
expect(li.isPresent()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('different timings', () => {
|
|
||||||
|
|
||||||
let host: ElementFinder;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
host = element(by.css('app-hero-list-timings'));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('adds and removes element', () => {
|
|
||||||
addInactiveHero();
|
|
||||||
|
|
||||||
let li = host.element(by.css('li'));
|
|
||||||
expect(li.getCssValue('transform')).toMatch(NO_TRANSFORM_MATRIX_REGEX);
|
|
||||||
expect(li.getCssValue('opacity')).toMatch('1');
|
|
||||||
|
|
||||||
removeHero();
|
|
||||||
expect(li.isPresent()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('multiple keyframes', () => {
|
|
||||||
|
|
||||||
let host: ElementFinder;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
host = element(by.css('app-hero-list-multistep'));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('adds and removes element', () => {
|
|
||||||
addInactiveHero();
|
|
||||||
|
|
||||||
let li = host.element(by.css('li'));
|
|
||||||
expect(li.getCssValue('transform')).toMatch(NO_TRANSFORM_MATRIX_REGEX);
|
|
||||||
expect(li.getCssValue('opacity')).toMatch('1');
|
|
||||||
|
|
||||||
removeHero();
|
|
||||||
expect(li.isPresent()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('parallel groups', () => {
|
|
||||||
|
|
||||||
let host: ElementFinder;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
host = element(by.css('app-hero-list-groups'));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('adds and removes element', () => {
|
|
||||||
addInactiveHero();
|
|
||||||
|
|
||||||
let li = host.element(by.css('li'));
|
|
||||||
expect(li.getCssValue('transform')).toMatch(NO_TRANSFORM_MATRIX_REGEX);
|
|
||||||
expect(li.getCssValue('opacity')).toMatch('1');
|
|
||||||
|
|
||||||
removeHero(700);
|
|
||||||
expect(li.isPresent()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('adding active heroes', () => {
|
|
||||||
|
|
||||||
let host: ElementFinder;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
host = element(by.css('app-hero-list-basic'));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('animates between active and inactive', () => {
|
|
||||||
addActiveHero();
|
|
||||||
|
|
||||||
let li = host.element(by.css('li'));
|
|
||||||
|
|
||||||
expect(getScaleX(li)).toBe(1.1);
|
|
||||||
expect(li.getCssValue('backgroundColor')).toBe(ACTIVE_COLOR);
|
|
||||||
|
|
||||||
li.click();
|
|
||||||
browser.driver.sleep(300);
|
|
||||||
expect(getScaleX(li)).toBe(1.0);
|
|
||||||
expect(li.getCssValue('backgroundColor')).toBe(INACTIVE_COLOR);
|
|
||||||
|
|
||||||
li.click();
|
|
||||||
browser.driver.sleep(300);
|
|
||||||
expect(getScaleX(li)).toBe(1.1);
|
|
||||||
expect(li.getCssValue('backgroundColor')).toBe(ACTIVE_COLOR);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('callbacks', () => {
|
|
||||||
it('fires a callback on start and done', () => {
|
|
||||||
addActiveHero();
|
|
||||||
browser.manage().logs().get(logging.Type.BROWSER)
|
|
||||||
.then((logs: logging.Entry[]) => {
|
|
||||||
const animationMessages = logs.filter((log) => {
|
|
||||||
return log.message.indexOf('Animation') !== -1 ? true : false;
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(animationMessages.length).toBeGreaterThan(0);
|
expect(animationMessages.length).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Status Slider Component', () => {
|
||||||
|
const activeColor = 'rgba(255, 165, 0, 1)';
|
||||||
|
const inactiveColor = 'rgba(0, 0, 255, 1)';
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await statusSliderHref.click();
|
||||||
|
sleepFor(2000);
|
||||||
});
|
});
|
||||||
|
|
||||||
function addActiveHero(sleep?: number) {
|
it('should be inactive with an orange background', async () => {
|
||||||
sleep = sleep || 500;
|
let text = await statusSlider.getComponentText();
|
||||||
element(by.buttonText('Add active hero')).click();
|
const toggleButton = statusSlider.getToggleButton();
|
||||||
browser.driver.sleep(sleep);
|
const container = statusSlider.getComponentContainer();
|
||||||
|
|
||||||
|
if (text === 'Active') {
|
||||||
|
await toggleButton.click();
|
||||||
|
sleepFor(2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
function addInactiveHero(sleep?: number) {
|
text = await statusSlider.getComponentText();
|
||||||
sleep = sleep || 500;
|
const bgColor = await container.getCssValue('backgroundColor');
|
||||||
element(by.buttonText('Add inactive hero')).click();
|
|
||||||
browser.driver.sleep(sleep);
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeHero(sleep?: number) {
|
expect(text).toBe('Inactive');
|
||||||
sleep = sleep || 500;
|
expect(bgColor).toBe(inactiveColor);
|
||||||
element(by.buttonText('Remove hero')).click();
|
|
||||||
browser.driver.sleep(sleep);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getScaleX(el: ElementFinder) {
|
|
||||||
return Promise.all([
|
|
||||||
getBoundingClientWidth(el),
|
|
||||||
getOffsetWidth(el)
|
|
||||||
]).then(function(promiseResolutions) {
|
|
||||||
let clientWidth = promiseResolutions[0];
|
|
||||||
let offsetWidth = promiseResolutions[1];
|
|
||||||
return clientWidth / offsetWidth;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should be active with a blue background', async () => {
|
||||||
|
let text = await statusSlider.getComponentText();
|
||||||
|
const toggleButton = statusSlider.getToggleButton();
|
||||||
|
const container = statusSlider.getComponentContainer();
|
||||||
|
|
||||||
|
if (text === 'Inactive') {
|
||||||
|
await toggleButton.click();
|
||||||
|
sleepFor(2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBoundingClientWidth(el: ElementFinder) {
|
text = await statusSlider.getComponentText();
|
||||||
return browser.executeScript(
|
const bgColor = await container.getCssValue('backgroundColor');
|
||||||
'return arguments[0].getBoundingClientRect().width',
|
|
||||||
el.getWebElement()
|
|
||||||
) as PromiseLike<number>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getOffsetWidth(el: ElementFinder) {
|
expect(text).toBe('Active');
|
||||||
return browser.executeScript(
|
expect(bgColor).toBe(activeColor);
|
||||||
'return arguments[0].offsetWidth',
|
});
|
||||||
el.getWebElement()
|
});
|
||||||
) as PromiseLike<number>;
|
|
||||||
}
|
describe('Toggle Animations Component', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await toggleHref.click();
|
||||||
|
sleepFor();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should disabled animations on the child element', async () => {
|
||||||
|
const toggleButton = toggle.getToggleAnimationsButton();
|
||||||
|
|
||||||
|
await toggleButton.click();
|
||||||
|
|
||||||
|
const container = toggle.getComponentContainer();
|
||||||
|
const cssClasses = await container.getAttribute('class');
|
||||||
|
|
||||||
|
expect(cssClasses).toContain('ng-animate-disabled');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Enter/Leave Component', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await enterLeaveHref.click();
|
||||||
|
sleepFor(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should attach a flyInOut trigger to the list of items', async () => {
|
||||||
|
const heroesList = enterLeave.getHeroesList();
|
||||||
|
const hero = heroesList.get(0);
|
||||||
|
const cssClasses = await hero.getAttribute('class');
|
||||||
|
const transform = await hero.getCssValue('transform');
|
||||||
|
|
||||||
|
expect(cssClasses).toContain('ng-trigger-flyInOut');
|
||||||
|
expect(transform).toBe('matrix(1, 0, 0, 1, 0, 0)');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove the hero from the list when clicked', async () => {
|
||||||
|
const heroesList = enterLeave.getHeroesList();
|
||||||
|
const total = await heroesList.count();
|
||||||
|
const hero = heroesList.get(0);
|
||||||
|
|
||||||
|
await hero.click();
|
||||||
|
await sleepFor(100);
|
||||||
|
const newTotal = await heroesList.count();
|
||||||
|
|
||||||
|
expect(newTotal).toBeLessThan(total);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Auto Calculation Component', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await autoHref.click();
|
||||||
|
sleepFor(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should attach a shrinkOut trigger to the list of items', async () => {
|
||||||
|
const heroesList = auto.getHeroesList();
|
||||||
|
const hero = heroesList.get(0);
|
||||||
|
const cssClasses = await hero.getAttribute('class');
|
||||||
|
|
||||||
|
expect(cssClasses).toContain('ng-trigger-shrinkOut');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove the hero from the list when clicked', async () => {
|
||||||
|
const heroesList = auto.getHeroesList();
|
||||||
|
const total = await heroesList.count();
|
||||||
|
const hero = heroesList.get(0);
|
||||||
|
|
||||||
|
await hero.click();
|
||||||
|
await sleepFor(250);
|
||||||
|
const newTotal = await heroesList.count();
|
||||||
|
|
||||||
|
expect(newTotal).toBeLessThan(total);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Filter/Stagger Component', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await filterHref.click();
|
||||||
|
sleepFor();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should attach a filterAnimations trigger to the list container', async () => {
|
||||||
|
const heroesList = filterStagger.getComponentContainer();
|
||||||
|
const cssClasses = await heroesList.getAttribute('class');
|
||||||
|
|
||||||
|
expect(cssClasses).toContain('ng-trigger-filterAnimation');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filter down the list when a search is performed', async () => {
|
||||||
|
const heroesList = filterStagger.getHeroesList();
|
||||||
|
const total = await heroesList.count();
|
||||||
|
const formInput = filterStagger.getFormInput();
|
||||||
|
|
||||||
|
await formInput.sendKeys('Mag');
|
||||||
|
await sleepFor(500);
|
||||||
|
const newTotal = await heroesList.count();
|
||||||
|
|
||||||
|
expect(newTotal).toBeLessThan(total);
|
||||||
|
expect(newTotal).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Hero Groups Component', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await heroGroupsHref.click();
|
||||||
|
sleepFor(300);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should attach a flyInOut trigger to the list of items', async () => {
|
||||||
|
const heroesList = heroGroups.getHeroesList();
|
||||||
|
const hero = heroesList.get(0);
|
||||||
|
const cssClasses = await hero.getAttribute('class');
|
||||||
|
const transform = await hero.getCssValue('transform');
|
||||||
|
const opacity = await hero.getCssValue('opacity');
|
||||||
|
|
||||||
|
expect(cssClasses).toContain('ng-trigger-flyInOut');
|
||||||
|
expect(transform).toBe('matrix(1, 0, 0, 1, 0, 0)');
|
||||||
|
expect(opacity).toBe('1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove the hero from the list when clicked', async () => {
|
||||||
|
const heroesList = heroGroups.getHeroesList();
|
||||||
|
const total = await heroesList.count();
|
||||||
|
const hero = heroesList.get(0);
|
||||||
|
|
||||||
|
await hero.click();
|
||||||
|
await sleepFor(300);
|
||||||
|
const newTotal = await heroesList.count();
|
||||||
|
|
||||||
|
expect(newTotal).toBeLessThan(total);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
19
aio/content/examples/animations/e2e/src/auto.po.ts
Normal file
19
aio/content/examples/animations/e2e/src/auto.po.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { by } from 'protractor';
|
||||||
|
import { locate } from './util';
|
||||||
|
|
||||||
|
export function getPage() {
|
||||||
|
return by.css('app-hero-list-auto-page');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getComponent() {
|
||||||
|
return by.css('app-hero-list-auto');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getComponentContainer() {
|
||||||
|
const findContainer = () => by.css('ul');
|
||||||
|
return locate(getComponent(), findContainer());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getHeroesList() {
|
||||||
|
return getComponentContainer().all(by.css('li'));
|
||||||
|
}
|
19
aio/content/examples/animations/e2e/src/enter-leave.po.ts
Normal file
19
aio/content/examples/animations/e2e/src/enter-leave.po.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { by } from 'protractor';
|
||||||
|
import { locate } from './util';
|
||||||
|
|
||||||
|
export function getPage() {
|
||||||
|
return by.css('app-hero-list-enter-leave-page');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getComponent() {
|
||||||
|
return by.css('app-hero-list-enter-leave');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getComponentContainer() {
|
||||||
|
const findContainer = () => by.css('ul');
|
||||||
|
return locate(getComponent(), findContainer());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getHeroesList() {
|
||||||
|
return getComponentContainer().all(by.css('li'));
|
||||||
|
}
|
20
aio/content/examples/animations/e2e/src/filter-stagger.po.ts
Normal file
20
aio/content/examples/animations/e2e/src/filter-stagger.po.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { by } from 'protractor';
|
||||||
|
import { locate } from './util';
|
||||||
|
|
||||||
|
export function getPage() {
|
||||||
|
return by.css('app-hero-list-page');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getComponentContainer() {
|
||||||
|
const findContainer = () => by.css('ul');
|
||||||
|
return locate(getPage(), findContainer());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getHeroesList() {
|
||||||
|
return getComponentContainer().all(by.css('li'));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFormInput() {
|
||||||
|
const formInput = () => by.css('form > input');
|
||||||
|
return locate(getPage(), formInput());
|
||||||
|
}
|
19
aio/content/examples/animations/e2e/src/hero-groups.ts
Normal file
19
aio/content/examples/animations/e2e/src/hero-groups.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { by } from 'protractor';
|
||||||
|
import { locate } from './util';
|
||||||
|
|
||||||
|
export function getPage() {
|
||||||
|
return by.css('app-hero-list-groups-page');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getComponent() {
|
||||||
|
return by.css('app-hero-list-groups');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getComponentContainer() {
|
||||||
|
const findContainer = () => by.css('ul');
|
||||||
|
return locate(getComponent(), findContainer());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getHeroesList() {
|
||||||
|
return getComponentContainer().all(by.css('li'));
|
||||||
|
}
|
33
aio/content/examples/animations/e2e/src/open-close.po.ts
Normal file
33
aio/content/examples/animations/e2e/src/open-close.po.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { by } from 'protractor';
|
||||||
|
import { locate } from './util';
|
||||||
|
|
||||||
|
export function getPage() {
|
||||||
|
return by.css('app-open-close-page');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getComponent() {
|
||||||
|
return by.css('app-open-close');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getToggleButton() {
|
||||||
|
const toggleButton = () => by.buttonText('Toggle Open/Close');
|
||||||
|
return locate(getComponent(), toggleButton());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLoggingCheckbox() {
|
||||||
|
const loggingCheckbox = () => by.css('section > input[type="checkbox"]');
|
||||||
|
return locate(getPage(), loggingCheckbox());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getComponentContainer() {
|
||||||
|
const findContainer = () => by.css('div');
|
||||||
|
return locate(getComponent(), findContainer());
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getComponentText() {
|
||||||
|
const findContainerText = () => by.css('div');
|
||||||
|
const contents = locate(getComponent(), findContainerText());
|
||||||
|
const componentText = await contents.getText();
|
||||||
|
|
||||||
|
return componentText;
|
||||||
|
}
|
28
aio/content/examples/animations/e2e/src/status-slider.po.ts
Normal file
28
aio/content/examples/animations/e2e/src/status-slider.po.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { by } from 'protractor';
|
||||||
|
import { locate } from './util';
|
||||||
|
|
||||||
|
export function getPage() {
|
||||||
|
return by.css('app-status-slider-page');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getComponent() {
|
||||||
|
return by.css('app-status-slider');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getToggleButton() {
|
||||||
|
const toggleButton = () => by.buttonText('Toggle Status');
|
||||||
|
return locate(getComponent(), toggleButton());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getComponentContainer() {
|
||||||
|
const findContainer = () => by.css('div');
|
||||||
|
return locate(getComponent(), findContainer());
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getComponentText() {
|
||||||
|
const findContainerText = () => by.css('div');
|
||||||
|
const contents = locate(getComponent(), findContainerText());
|
||||||
|
const componentText = await contents.getText();
|
||||||
|
|
||||||
|
return componentText;
|
||||||
|
}
|
25
aio/content/examples/animations/e2e/src/toggle.po.ts
Normal file
25
aio/content/examples/animations/e2e/src/toggle.po.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { by } from 'protractor';
|
||||||
|
import { locate } from './util';
|
||||||
|
|
||||||
|
export function getPage() {
|
||||||
|
return by.css('app-toggle-animations-child-page');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getComponent() {
|
||||||
|
return by.css('app-open-close-toggle');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getToggleButton() {
|
||||||
|
const toggleButton = () => by.buttonText('Toggle Open/Closed');
|
||||||
|
return locate(getComponent(), toggleButton());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getToggleAnimationsButton() {
|
||||||
|
const toggleAnimationsButton = () => by.buttonText('Toggle Animations');
|
||||||
|
return locate(getComponent(), toggleAnimationsButton());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getComponentContainer() {
|
||||||
|
const findContainer = () => by.css('div');
|
||||||
|
return locate(getComponent()).all(findContainer()).get(0);
|
||||||
|
}
|
19
aio/content/examples/animations/e2e/src/util.ts
Normal file
19
aio/content/examples/animations/e2e/src/util.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { Locator, ElementFinder, browser, by, element } from 'protractor';
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* locate(finder1, finder2) => element(finder1).element(finder2).element(finderN);
|
||||||
|
*/
|
||||||
|
export function locate(locator: Locator, ...locators: Locator[]) {
|
||||||
|
return locators.reduce((current: ElementFinder, next: Locator) => {
|
||||||
|
return current.element(next);
|
||||||
|
}, element(locator)) as ElementFinder;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sleepFor(time = 1000) {
|
||||||
|
return await browser.sleep(time);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLinkById(id: string) {
|
||||||
|
return element(by.css(`a[id=${id}]`));
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
<p>
|
||||||
|
Angular's animations library makes it easy to define and apply animation effects such as page and list transitions.
|
||||||
|
</p>
|
15
aio/content/examples/animations/src/app/about.component.ts
Normal file
15
aio/content/examples/animations/src/app/about.component.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-about',
|
||||||
|
templateUrl: './about.component.html',
|
||||||
|
styleUrls: ['./about.component.css']
|
||||||
|
})
|
||||||
|
export class AboutComponent implements OnInit {
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
11
aio/content/examples/animations/src/app/animations.1.ts
Normal file
11
aio/content/examples/animations/src/app/animations.1.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
// #docregion
|
||||||
|
import { animation, style, animate } from '@angular/animations';
|
||||||
|
|
||||||
|
export const transAnimation = animation([
|
||||||
|
style({
|
||||||
|
height: '{{ height }}',
|
||||||
|
opacity: '{{ opacity }}',
|
||||||
|
backgroundColor: '{{ backgroundColor }}'
|
||||||
|
}),
|
||||||
|
animate('{{ time }}')
|
||||||
|
]);
|
74
aio/content/examples/animations/src/app/animations.ts
Normal file
74
aio/content/examples/animations/src/app/animations.ts
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
// #docregion reusable
|
||||||
|
import {
|
||||||
|
animation, trigger, animateChild, group,
|
||||||
|
transition, animate, style, query
|
||||||
|
} from '@angular/animations';
|
||||||
|
|
||||||
|
export const transAnimation = animation([
|
||||||
|
style({
|
||||||
|
height: '{{ height }}',
|
||||||
|
opacity: '{{ opacity }}',
|
||||||
|
backgroundColor: '{{ backgroundColor }}'
|
||||||
|
}),
|
||||||
|
animate('{{ time }}')
|
||||||
|
]);
|
||||||
|
// #enddocregion reusable
|
||||||
|
|
||||||
|
// Routable animations
|
||||||
|
// #docregion route-animations
|
||||||
|
export const slideInAnimation =
|
||||||
|
// #docregion style-view
|
||||||
|
trigger('routeAnimations', [
|
||||||
|
transition('HomePage <=> AboutPage', [
|
||||||
|
style({ position: 'relative' }),
|
||||||
|
query(':enter, :leave', [
|
||||||
|
style({
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: '100%'
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
// #enddocregion style-view
|
||||||
|
// #docregion query
|
||||||
|
query(':enter', [
|
||||||
|
style({ left: '-100%'})
|
||||||
|
]),
|
||||||
|
query(':leave', animateChild()),
|
||||||
|
group([
|
||||||
|
query(':leave', [
|
||||||
|
animate('300ms ease-out', style({ left: '100%'}))
|
||||||
|
]),
|
||||||
|
query(':enter', [
|
||||||
|
animate('300ms ease-out', style({ left: '0%'}))
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
query(':enter', animateChild()),
|
||||||
|
]),
|
||||||
|
transition('* <=> FilterPage', [
|
||||||
|
style({ position: 'relative' }),
|
||||||
|
query(':enter, :leave', [
|
||||||
|
style({
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: '100%'
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
query(':enter', [
|
||||||
|
style({ left: '-100%'})
|
||||||
|
]),
|
||||||
|
query(':leave', animateChild()),
|
||||||
|
group([
|
||||||
|
query(':leave', [
|
||||||
|
animate('200ms ease-out', style({ left: '100%'}))
|
||||||
|
]),
|
||||||
|
query(':enter', [
|
||||||
|
animate('300ms ease-out', style({ left: '0%'}))
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
query(':enter', animateChild()),
|
||||||
|
])
|
||||||
|
// #enddocregion query
|
||||||
|
]);
|
||||||
|
// #enddocregion route-animations
|
35
aio/content/examples/animations/src/app/app.component.1.ts
Normal file
35
aio/content/examples/animations/src/app/app.component.1.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
// #docplaster
|
||||||
|
// #docregion imports
|
||||||
|
import { Component, HostBinding } from '@angular/core';
|
||||||
|
import {
|
||||||
|
trigger,
|
||||||
|
state,
|
||||||
|
style,
|
||||||
|
animate,
|
||||||
|
transition,
|
||||||
|
// ...
|
||||||
|
} from '@angular/animations';
|
||||||
|
|
||||||
|
// #enddocregion imports
|
||||||
|
|
||||||
|
// #docregion decorator, toggle-app-animations
|
||||||
|
@Component({
|
||||||
|
selector: 'app-root',
|
||||||
|
templateUrl: 'app.component.html',
|
||||||
|
styleUrls: ['app.component.css'],
|
||||||
|
animations: [
|
||||||
|
// animation triggers go here
|
||||||
|
]
|
||||||
|
})
|
||||||
|
// #enddocregion decorator
|
||||||
|
export class AppComponent {
|
||||||
|
@HostBinding('@.disabled')
|
||||||
|
public animationsDisabled = false;
|
||||||
|
// #enddocregion toggle-app-animations
|
||||||
|
|
||||||
|
toggleAnimations() {
|
||||||
|
this.animationsDisabled = !this.animationsDisabled;
|
||||||
|
}
|
||||||
|
// #docregion toggle-app-animations
|
||||||
|
}
|
||||||
|
// #enddocregion toggle-app-animations
|
@ -0,0 +1,7 @@
|
|||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
section {
|
||||||
|
margin-top: 100px;
|
||||||
|
}
|
21
aio/content/examples/animations/src/app/app.component.html
Normal file
21
aio/content/examples/animations/src/app/app.component.html
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<h1>Animations</h1>
|
||||||
|
|
||||||
|
Toggle All Animations <input type="checkbox" [checked]="!animationsDisabled" (click)="toggleAnimations()"/>
|
||||||
|
|
||||||
|
<nav>
|
||||||
|
<a id="home" routerLink="/home" routerLinkActive="active">Home</a>
|
||||||
|
<a id="about" routerLink="/about" routerLinkActive="active">About</a>
|
||||||
|
<a id="open-close" routerLink="/open-close" routerLinkActive="active">Open/Close</a>
|
||||||
|
<a id="status" routerLink="/status" routerLinkActive="active">Status Slider</a>
|
||||||
|
<a id="toggle" routerLink="/toggle" routerLinkActive="active">Toggle Animations</a>
|
||||||
|
<a id="enter-leave" routerLink="/enter-leave" routerLinkActive="active">Enter/Leave</a>
|
||||||
|
<a id="auto" routerLink="/auto" routerLinkActive="active">Auto Calculation</a>
|
||||||
|
<a id="heroes" routerLink="/heroes" routerLinkActive="active">Filter/Stagger</a>
|
||||||
|
<a id="hero-groups" routerLink="/hero-groups" routerLinkActive="active">Hero Groups</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- #docregion route-animations-outlet -->
|
||||||
|
<div [@routeAnimations]="prepareRoute(outlet)" >
|
||||||
|
<router-outlet #outlet="outlet"></router-outlet>
|
||||||
|
</div>
|
||||||
|
<!-- #enddocregion route-animations-outlet -->
|
47
aio/content/examples/animations/src/app/app.component.ts
Normal file
47
aio/content/examples/animations/src/app/app.component.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
// #docplaster
|
||||||
|
// #docregion imports
|
||||||
|
import { Component, HostBinding } from '@angular/core';
|
||||||
|
import {
|
||||||
|
trigger,
|
||||||
|
state,
|
||||||
|
style,
|
||||||
|
animate,
|
||||||
|
transition,
|
||||||
|
// ...
|
||||||
|
} from '@angular/animations';
|
||||||
|
|
||||||
|
// #enddocregion imports
|
||||||
|
import { RouterOutlet } from '@angular/router';
|
||||||
|
import { slideInAnimation } from './animations';
|
||||||
|
|
||||||
|
// #docregion decorator, toggle-app-animations, define
|
||||||
|
@Component({
|
||||||
|
selector: 'app-root',
|
||||||
|
templateUrl: 'app.component.html',
|
||||||
|
styleUrls: ['app.component.css'],
|
||||||
|
animations: [
|
||||||
|
// #enddocregion decorator
|
||||||
|
slideInAnimation
|
||||||
|
// #docregion decorator
|
||||||
|
// animation triggers go here
|
||||||
|
]
|
||||||
|
})
|
||||||
|
// #enddocregion decorator, define
|
||||||
|
export class AppComponent {
|
||||||
|
@HostBinding('@.disabled')
|
||||||
|
public animationsDisabled = false;
|
||||||
|
// #enddocregion toggle-app-animations
|
||||||
|
|
||||||
|
// #docregion prepare-router-outlet
|
||||||
|
prepareRoute(outlet: RouterOutlet) {
|
||||||
|
return outlet && outlet.activatedRouteData && outlet.activatedRouteData['animation'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// #enddocregion prepare-router-outlet
|
||||||
|
|
||||||
|
toggleAnimations() {
|
||||||
|
this.animationsDisabled = !this.animationsDisabled;
|
||||||
|
}
|
||||||
|
// #docregion toggle-app-animations
|
||||||
|
}
|
||||||
|
// #enddocregion toggle-app-animations
|
13
aio/content/examples/animations/src/app/app.module.1.ts
Normal file
13
aio/content/examples/animations/src/app/app.module.1.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
BrowserModule,
|
||||||
|
BrowserAnimationsModule
|
||||||
|
],
|
||||||
|
declarations: [ ],
|
||||||
|
bootstrap: [ ]
|
||||||
|
})
|
||||||
|
export class AppModule { }
|
@ -1,43 +1,63 @@
|
|||||||
// #docplaster
|
// #docregion route-animation-data
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
// #docregion animations-module
|
|
||||||
import { BrowserModule } from '@angular/platform-browser';
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
// #enddocregion animations-module
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { AppComponent } from './app.component';
|
||||||
import { HeroTeamBuilderComponent } from './hero-team-builder.component';
|
import { OpenCloseComponent } from './open-close.component';
|
||||||
import { HeroListBasicComponent } from './hero-list-basic.component';
|
import { OpenClosePageComponent } from './open-close-page.component';
|
||||||
import { HeroListInlineStylesComponent } from './hero-list-inline-styles.component';
|
import { OpenCloseChildComponent } from './open-close.component.4';
|
||||||
import { HeroListEnterLeaveComponent } from './hero-list-enter-leave.component';
|
import { ToggleAnimationsPageComponent } from './toggle-animations-page.component';
|
||||||
import { HeroListEnterLeaveStatesComponent } from './hero-list-enter-leave-states.component';
|
import { StatusSliderComponent } from './status-slider.component';
|
||||||
import { HeroListCombinedTransitionsComponent } from './hero-list-combined-transitions.component';
|
import { StatusSliderPageComponent } from './status-slider-page.component';
|
||||||
import { HeroListTwowayComponent } from './hero-list-twoway.component';
|
import { HeroListPageComponent } from './hero-list-page.component';
|
||||||
import { HeroListAutoComponent } from './hero-list-auto.component';
|
import { HeroListGroupPageComponent } from './hero-list-group-page.component';
|
||||||
import { HeroListGroupsComponent } from './hero-list-groups.component';
|
import { HeroListGroupsComponent } from './hero-list-groups.component';
|
||||||
import { HeroListMultistepComponent } from './hero-list-multistep.component';
|
import { HeroListEnterLeavePageComponent } from './hero-list-enter-leave-page.component';
|
||||||
import { HeroListTimingsComponent } from './hero-list-timings.component';
|
import { HeroListEnterLeaveComponent } from './hero-list-enter-leave.component';
|
||||||
// #docregion animations-module
|
import { HeroListAutoCalcPageComponent } from './hero-list-auto-page.component';
|
||||||
|
import { HeroListAutoComponent } from './hero-list-auto.component';
|
||||||
|
import { HomeComponent } from './home.component';
|
||||||
|
import { AboutComponent } from './about.component';
|
||||||
|
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [ BrowserModule, BrowserAnimationsModule ],
|
imports: [
|
||||||
// ... more stuff ...
|
BrowserModule,
|
||||||
// #enddocregion animations-module
|
BrowserAnimationsModule,
|
||||||
declarations: [
|
RouterModule.forRoot([
|
||||||
HeroTeamBuilderComponent,
|
{ path: '', pathMatch: 'full', redirectTo: '/enter-leave' },
|
||||||
HeroListBasicComponent,
|
{ path: 'open-close', component: OpenClosePageComponent },
|
||||||
HeroListInlineStylesComponent,
|
{ path: 'status', component: StatusSliderPageComponent },
|
||||||
HeroListCombinedTransitionsComponent,
|
{ path: 'toggle', component: ToggleAnimationsPageComponent },
|
||||||
HeroListTwowayComponent,
|
{ path: 'heroes', component: HeroListPageComponent, data: {animation: 'FilterPage'} },
|
||||||
HeroListEnterLeaveComponent,
|
{ path: 'hero-groups', component: HeroListGroupPageComponent },
|
||||||
HeroListEnterLeaveStatesComponent,
|
{ path: 'enter-leave', component: HeroListEnterLeavePageComponent },
|
||||||
HeroListAutoComponent,
|
{ path: 'auto', component: HeroListAutoCalcPageComponent },
|
||||||
HeroListTimingsComponent,
|
{ path: 'home', component: HomeComponent, data: {animation: 'HomePage'} },
|
||||||
HeroListMultistepComponent,
|
{ path: 'about', component: AboutComponent, data: {animation: 'AboutPage'} },
|
||||||
HeroListGroupsComponent
|
|
||||||
|
])
|
||||||
],
|
],
|
||||||
bootstrap: [ HeroTeamBuilderComponent ]
|
// #enddocregion route-animation-data
|
||||||
// #docregion animations-module
|
declarations: [
|
||||||
|
AppComponent,
|
||||||
|
StatusSliderComponent,
|
||||||
|
OpenCloseComponent,
|
||||||
|
OpenCloseChildComponent,
|
||||||
|
OpenClosePageComponent,
|
||||||
|
StatusSliderPageComponent,
|
||||||
|
ToggleAnimationsPageComponent,
|
||||||
|
HeroListPageComponent,
|
||||||
|
HeroListGroupsComponent,
|
||||||
|
HeroListGroupPageComponent,
|
||||||
|
HeroListEnterLeavePageComponent,
|
||||||
|
HeroListEnterLeaveComponent,
|
||||||
|
HeroListAutoCalcPageComponent,
|
||||||
|
HeroListAutoComponent,
|
||||||
|
HomeComponent,
|
||||||
|
AboutComponent
|
||||||
|
],
|
||||||
|
bootstrap: [AppComponent]
|
||||||
})
|
})
|
||||||
export class AppModule { }
|
export class AppModule { }
|
||||||
// #enddocregion animations-module
|
|
||||||
|
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { HEROES } from './mock-heroes';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-hero-list-auto-page',
|
||||||
|
template: `
|
||||||
|
<section>
|
||||||
|
<h2>Automatic Calculation</h2>
|
||||||
|
|
||||||
|
<app-hero-list-auto [heroes]="heroes" (remove)="onRemove($event)"></app-hero-list-auto>
|
||||||
|
</section>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
export class HeroListAutoCalcPageComponent {
|
||||||
|
heroes = HEROES.slice();
|
||||||
|
|
||||||
|
onRemove(id: number) {
|
||||||
|
this.heroes = this.heroes.filter(hero => hero.id !== id);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
<ul class="heroes">
|
||||||
|
<li *ngFor="let hero of heroes"
|
||||||
|
[@shrinkOut]="'in'" (click)="removeHero(hero.id)">
|
||||||
|
<div class="inner">
|
||||||
|
<span class="badge">{{ hero.id }}</span>
|
||||||
|
<span>{{ hero.name }}</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
@ -1,6 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
Component,
|
Component,
|
||||||
Input
|
Input,
|
||||||
|
Output,
|
||||||
|
EventEmitter
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import {
|
import {
|
||||||
trigger,
|
trigger,
|
||||||
@ -10,38 +12,30 @@ import {
|
|||||||
transition
|
transition
|
||||||
} from '@angular/animations';
|
} from '@angular/animations';
|
||||||
|
|
||||||
import { Hero } from './hero.service';
|
import { Hero } from './hero';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-hero-list-auto',
|
selector: 'app-hero-list-auto',
|
||||||
// #docregion template
|
templateUrl: 'hero-list-auto.component.html',
|
||||||
template: `
|
styleUrls: ['./hero-list-page.component.css'],
|
||||||
<ul>
|
// #docregion auto-calc
|
||||||
<li *ngFor="let hero of heroes"
|
|
||||||
[@shrinkOut]="'in'">
|
|
||||||
{{hero.name}}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
`,
|
|
||||||
// #enddocregion template
|
|
||||||
styleUrls: ['./hero-list.component.css'],
|
|
||||||
|
|
||||||
/* When the element leaves (transition "in => void" occurs),
|
|
||||||
* get the element's current computed height and animate
|
|
||||||
* it down to 0.
|
|
||||||
*/
|
|
||||||
// #docregion animationdef
|
|
||||||
animations: [
|
animations: [
|
||||||
trigger('shrinkOut', [
|
trigger('shrinkOut', [
|
||||||
state('in', style({height: '*'})),
|
state('in', style({ height: '*' })),
|
||||||
transition('* => void', [
|
transition('* => void', [
|
||||||
style({height: '*'}),
|
style({ height: '*' }),
|
||||||
animate(250, style({height: 0}))
|
animate(250, style({ height: 0 }))
|
||||||
])
|
])
|
||||||
])
|
])
|
||||||
]
|
]
|
||||||
// #enddocregion animationdef
|
// #enddocregion auto-calc
|
||||||
})
|
})
|
||||||
export class HeroListAutoComponent {
|
export class HeroListAutoComponent {
|
||||||
@Input() heroes: Hero[];
|
@Input() heroes: Hero[];
|
||||||
|
|
||||||
|
@Output() remove = new EventEmitter<number>();
|
||||||
|
|
||||||
|
removeHero(id: number) {
|
||||||
|
this.remove.emit(id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,70 +0,0 @@
|
|||||||
// #docplaster
|
|
||||||
// #docregion
|
|
||||||
// #docregion imports
|
|
||||||
import {
|
|
||||||
Component,
|
|
||||||
Input
|
|
||||||
} from '@angular/core';
|
|
||||||
import {
|
|
||||||
trigger,
|
|
||||||
state,
|
|
||||||
style,
|
|
||||||
animate,
|
|
||||||
transition
|
|
||||||
} from '@angular/animations';
|
|
||||||
// #enddocregion imports
|
|
||||||
|
|
||||||
import { Hero } from './hero.service';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-hero-list-basic',
|
|
||||||
// #enddocregion
|
|
||||||
/* The click event calls hero.toggleState(), which
|
|
||||||
* causes the state of that hero to switch from
|
|
||||||
* active to inactive or vice versa.
|
|
||||||
*/
|
|
||||||
// #docregion
|
|
||||||
// #docregion template
|
|
||||||
template: `
|
|
||||||
<ul>
|
|
||||||
<li *ngFor="let hero of heroes"
|
|
||||||
[@heroState]="hero.state"
|
|
||||||
(click)="hero.toggleState()">
|
|
||||||
{{hero.name}}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
`,
|
|
||||||
// #enddocregion template
|
|
||||||
styleUrls: ['./hero-list.component.css'],
|
|
||||||
// #enddocregion
|
|
||||||
/**
|
|
||||||
* Define two states, "inactive" and "active", and the end
|
|
||||||
* styles that apply whenever the element is in those states.
|
|
||||||
* Then define animations for transitioning between the states,
|
|
||||||
* one in each direction
|
|
||||||
*/
|
|
||||||
// #docregion
|
|
||||||
// #docregion animationdef
|
|
||||||
animations: [
|
|
||||||
trigger('heroState', [
|
|
||||||
// #docregion states
|
|
||||||
state('inactive', style({
|
|
||||||
backgroundColor: '#eee',
|
|
||||||
transform: 'scale(1)'
|
|
||||||
})),
|
|
||||||
state('active', style({
|
|
||||||
backgroundColor: '#cfd8dc',
|
|
||||||
transform: 'scale(1.1)'
|
|
||||||
})),
|
|
||||||
// #enddocregion states
|
|
||||||
// #docregion transitions
|
|
||||||
transition('inactive => active', animate('100ms ease-in')),
|
|
||||||
transition('active => inactive', animate('100ms ease-out'))
|
|
||||||
// #enddocregion transitions
|
|
||||||
])
|
|
||||||
]
|
|
||||||
// #enddocregion animationdef
|
|
||||||
})
|
|
||||||
export class HeroListBasicComponent {
|
|
||||||
@Input() heroes: Hero[];
|
|
||||||
}
|
|
@ -1,59 +0,0 @@
|
|||||||
// #docregion
|
|
||||||
// #docregion imports
|
|
||||||
import {
|
|
||||||
Component,
|
|
||||||
Input
|
|
||||||
} from '@angular/core';
|
|
||||||
import {
|
|
||||||
trigger,
|
|
||||||
state,
|
|
||||||
style,
|
|
||||||
animate,
|
|
||||||
transition
|
|
||||||
} from '@angular/animations';
|
|
||||||
// #enddocregion imports
|
|
||||||
|
|
||||||
import { Hero } from './hero.service';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-hero-list-combined-transitions',
|
|
||||||
// #docregion template
|
|
||||||
template: `
|
|
||||||
<ul>
|
|
||||||
<li *ngFor="let hero of heroes"
|
|
||||||
[@heroState]="hero.state"
|
|
||||||
(click)="hero.toggleState()">
|
|
||||||
{{hero.name}}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
`,
|
|
||||||
// #enddocregion template
|
|
||||||
styleUrls: ['./hero-list.component.css'],
|
|
||||||
/*
|
|
||||||
* Define two states, "inactive" and "active", and the end
|
|
||||||
* styles that apply whenever the element is in those states.
|
|
||||||
* Then define an animated transition between these two
|
|
||||||
* states, in *both* directions.
|
|
||||||
*/
|
|
||||||
// #docregion animationdef
|
|
||||||
animations: [
|
|
||||||
trigger('heroState', [
|
|
||||||
state('inactive', style({
|
|
||||||
backgroundColor: '#eee',
|
|
||||||
transform: 'scale(1)'
|
|
||||||
})),
|
|
||||||
state('active', style({
|
|
||||||
backgroundColor: '#cfd8dc',
|
|
||||||
transform: 'scale(1.1)'
|
|
||||||
})),
|
|
||||||
// #docregion transitions
|
|
||||||
transition('inactive => active, active => inactive',
|
|
||||||
animate('100ms ease-out'))
|
|
||||||
// #enddocregion transitions
|
|
||||||
])
|
|
||||||
]
|
|
||||||
// #enddocregion animationdef
|
|
||||||
})
|
|
||||||
export class HeroListCombinedTransitionsComponent {
|
|
||||||
@Input() heroes: Hero[];
|
|
||||||
}
|
|
@ -0,0 +1,20 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { HEROES } from './mock-heroes';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-hero-list-enter-leave-page',
|
||||||
|
template: `
|
||||||
|
<section>
|
||||||
|
<h2>Enter/Leave</h2>
|
||||||
|
|
||||||
|
<app-hero-list-enter-leave [heroes]="heroes" (remove)="onRemove($event)"></app-hero-list-enter-leave>
|
||||||
|
</section>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
export class HeroListEnterLeavePageComponent {
|
||||||
|
heroes = HEROES.slice();
|
||||||
|
|
||||||
|
onRemove(id: number) {
|
||||||
|
this.heroes = this.heroes.filter(hero => hero.id !== id);
|
||||||
|
}
|
||||||
|
}
|
@ -1,63 +0,0 @@
|
|||||||
import {
|
|
||||||
Component,
|
|
||||||
Input
|
|
||||||
} from '@angular/core';
|
|
||||||
import {
|
|
||||||
trigger,
|
|
||||||
state,
|
|
||||||
style,
|
|
||||||
animate,
|
|
||||||
transition
|
|
||||||
} from '@angular/animations';
|
|
||||||
|
|
||||||
import { Hero } from './hero.service';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-hero-list-enter-leave-states',
|
|
||||||
// #docregion template
|
|
||||||
template: `
|
|
||||||
<ul>
|
|
||||||
<li *ngFor="let hero of heroes"
|
|
||||||
(click)="hero.toggleState()"
|
|
||||||
[@heroState]="hero.state">
|
|
||||||
{{hero.name}}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
`,
|
|
||||||
// #enddocregion template
|
|
||||||
styleUrls: ['./hero-list.component.css'],
|
|
||||||
/* The elements here have two possible states based
|
|
||||||
* on the hero state, "active", or "inactive". We animate
|
|
||||||
* six transitions: Between the two states in both directions,
|
|
||||||
* and between each state and void. With this we can animate
|
|
||||||
* the enter and leave of elements differently based on which
|
|
||||||
* state they are in when they are added and removed.
|
|
||||||
*/
|
|
||||||
// #docregion animationdef
|
|
||||||
animations: [
|
|
||||||
trigger('heroState', [
|
|
||||||
state('inactive', style({transform: 'translateX(0) scale(1)'})),
|
|
||||||
state('active', style({transform: 'translateX(0) scale(1.1)'})),
|
|
||||||
transition('inactive => active', animate('100ms ease-in')),
|
|
||||||
transition('active => inactive', animate('100ms ease-out')),
|
|
||||||
transition('void => inactive', [
|
|
||||||
style({transform: 'translateX(-100%) scale(1)'}),
|
|
||||||
animate(100)
|
|
||||||
]),
|
|
||||||
transition('inactive => void', [
|
|
||||||
animate(100, style({transform: 'translateX(100%) scale(1)'}))
|
|
||||||
]),
|
|
||||||
transition('void => active', [
|
|
||||||
style({transform: 'translateX(0) scale(0)'}),
|
|
||||||
animate(200)
|
|
||||||
]),
|
|
||||||
transition('active => void', [
|
|
||||||
animate(200, style({transform: 'translateX(0) scale(0)'}))
|
|
||||||
])
|
|
||||||
])
|
|
||||||
]
|
|
||||||
// #enddocregion animationdef
|
|
||||||
})
|
|
||||||
export class HeroListEnterLeaveStatesComponent {
|
|
||||||
@Input() heroes: Hero[];
|
|
||||||
}
|
|
@ -1,6 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
Component,
|
Component,
|
||||||
Input
|
Input,
|
||||||
|
Output,
|
||||||
|
EventEmitter
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import {
|
import {
|
||||||
trigger,
|
trigger,
|
||||||
@ -10,37 +12,34 @@ import {
|
|||||||
transition
|
transition
|
||||||
} from '@angular/animations';
|
} from '@angular/animations';
|
||||||
|
|
||||||
import { Hero } from './hero.service';
|
import { Hero } from './hero';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-hero-list-enter-leave',
|
selector: 'app-hero-list-enter-leave',
|
||||||
// #docregion template
|
// #docregion template
|
||||||
template: `
|
template: `
|
||||||
<ul>
|
<ul class="heroes">
|
||||||
<li *ngFor="let hero of heroes"
|
<li *ngFor="let hero of heroes"
|
||||||
[@flyInOut]="'in'">
|
[@flyInOut]="'in'" (click)="removeHero(hero.id)">
|
||||||
{{hero.name}}
|
<div class="inner">
|
||||||
|
<span class="badge">{{ hero.id }}</span>
|
||||||
|
<span>{{ hero.name }}</span>
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
`,
|
`,
|
||||||
// #enddocregion template
|
// #enddocregion template
|
||||||
styleUrls: ['./hero-list.component.css'],
|
styleUrls: ['./hero-list-page.component.css'],
|
||||||
/* The element here always has the state "in" when it
|
|
||||||
* is present. We animate two transitions: From void
|
|
||||||
* to in and from in to void, to achieve an animated
|
|
||||||
* enter and leave transition. The element enters from
|
|
||||||
* the left and leaves to the right using translateX.
|
|
||||||
*/
|
|
||||||
// #docregion animationdef
|
// #docregion animationdef
|
||||||
animations: [
|
animations: [
|
||||||
trigger('flyInOut', [
|
trigger('flyInOut', [
|
||||||
state('in', style({transform: 'translateX(0)'})),
|
state('in', style({ transform: 'translateX(0)' })),
|
||||||
transition('void => *', [
|
transition('void => *', [
|
||||||
style({transform: 'translateX(-100%)'}),
|
style({ transform: 'translateX(-100%)' }),
|
||||||
animate(100)
|
animate(100)
|
||||||
]),
|
]),
|
||||||
transition('* => void', [
|
transition('* => void', [
|
||||||
animate(100, style({transform: 'translateX(100%)'}))
|
animate(100, style({ transform: 'translateX(100%)' }))
|
||||||
])
|
])
|
||||||
])
|
])
|
||||||
]
|
]
|
||||||
@ -48,4 +47,10 @@ import { Hero } from './hero.service';
|
|||||||
})
|
})
|
||||||
export class HeroListEnterLeaveComponent {
|
export class HeroListEnterLeaveComponent {
|
||||||
@Input() heroes: Hero[];
|
@Input() heroes: Hero[];
|
||||||
|
|
||||||
|
@Output() remove = new EventEmitter<number>();
|
||||||
|
|
||||||
|
removeHero(id: number) {
|
||||||
|
this.remove.emit(id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { HEROES } from './mock-heroes';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-hero-list-groups-page',
|
||||||
|
template: `
|
||||||
|
<section>
|
||||||
|
<h2>Hero List Group</h2>
|
||||||
|
|
||||||
|
<app-hero-list-groups [heroes]="heroes" (remove)="onRemove($event)"></app-hero-list-groups>
|
||||||
|
</section>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
export class HeroListGroupPageComponent {
|
||||||
|
heroes = HEROES.slice();
|
||||||
|
|
||||||
|
onRemove(id: number) {
|
||||||
|
this.heroes = this.heroes.filter(hero => hero.id !== id);
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
Component,
|
Component,
|
||||||
Input
|
Input,
|
||||||
|
Output,
|
||||||
|
EventEmitter
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import {
|
import {
|
||||||
trigger,
|
trigger,
|
||||||
@ -11,45 +13,31 @@ import {
|
|||||||
group
|
group
|
||||||
} from '@angular/animations';
|
} from '@angular/animations';
|
||||||
|
|
||||||
import { Hero } from './hero.service';
|
import { Hero } from './hero';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-hero-list-groups',
|
selector: 'app-hero-list-groups',
|
||||||
template: `
|
template: `
|
||||||
<ul>
|
<ul class="heroes">
|
||||||
<li *ngFor="let hero of heroes"
|
<li *ngFor="let hero of heroes"
|
||||||
[@flyInOut]="'in'">
|
[@flyInOut]="'in'" (click)="removeHero(hero.id)">
|
||||||
{{hero.name}}
|
<div class="inner">
|
||||||
|
<span class="badge">{{ hero.id }}</span>
|
||||||
|
<span>{{ hero.name }}</span>
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
`,
|
`,
|
||||||
styleUrls: ['./hero-list.component.css'],
|
styleUrls: ['./hero-list-page.component.css'],
|
||||||
styles: [`
|
|
||||||
li {
|
|
||||||
padding: 0 !important;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
`],
|
|
||||||
/* The element here always has the state "in" when it
|
|
||||||
* is present. We animate two transitions: From void
|
|
||||||
* to in and from in to void, to achieve an animated
|
|
||||||
* enter and leave transition.
|
|
||||||
*
|
|
||||||
* The transitions have *parallel group* that allow
|
|
||||||
* animating several properties at the same time but
|
|
||||||
* with different timing configurations. On enter
|
|
||||||
* (void => *) we start the opacity animation 0.1s
|
|
||||||
* earlier than the translation/width animation.
|
|
||||||
* On leave (* => void) we do the opposite -
|
|
||||||
* the translation/width animation begins immediately
|
|
||||||
* and the opacity animation 0.1s later.
|
|
||||||
*/
|
|
||||||
// #docregion animationdef
|
// #docregion animationdef
|
||||||
animations: [
|
animations: [
|
||||||
trigger('flyInOut', [
|
trigger('flyInOut', [
|
||||||
state('in', style({width: 120, transform: 'translateX(0)', opacity: 1})),
|
state('in', style({
|
||||||
|
width: 120,
|
||||||
|
transform: 'translateX(0)', opacity: 1
|
||||||
|
})),
|
||||||
transition('void => *', [
|
transition('void => *', [
|
||||||
style({width: 10, transform: 'translateX(50px)', opacity: 0}),
|
style({ width: 10, transform: 'translateX(50px)', opacity: 0 }),
|
||||||
group([
|
group([
|
||||||
animate('0.3s 0.1s ease', style({
|
animate('0.3s 0.1s ease', style({
|
||||||
transform: 'translateX(0)',
|
transform: 'translateX(0)',
|
||||||
@ -77,4 +65,10 @@ import { Hero } from './hero.service';
|
|||||||
})
|
})
|
||||||
export class HeroListGroupsComponent {
|
export class HeroListGroupsComponent {
|
||||||
@Input() heroes: Hero[];
|
@Input() heroes: Hero[];
|
||||||
|
|
||||||
|
@Output() remove = new EventEmitter<number>();
|
||||||
|
|
||||||
|
removeHero(id: number) {
|
||||||
|
this.remove.emit(id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,60 +0,0 @@
|
|||||||
// #docregion
|
|
||||||
// #docregion imports
|
|
||||||
import {
|
|
||||||
Component,
|
|
||||||
Input,
|
|
||||||
} from '@angular/core';
|
|
||||||
import {
|
|
||||||
trigger,
|
|
||||||
style,
|
|
||||||
animate,
|
|
||||||
transition
|
|
||||||
} from '@angular/animations';
|
|
||||||
// #enddocregion imports
|
|
||||||
|
|
||||||
import { Hero } from './hero.service';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-hero-list-inline-styles',
|
|
||||||
// #docregion template
|
|
||||||
template: `
|
|
||||||
<ul>
|
|
||||||
<li *ngFor="let hero of heroes"
|
|
||||||
[@heroState]="hero.state"
|
|
||||||
(click)="hero.toggleState()">
|
|
||||||
{{hero.name}}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
`,
|
|
||||||
// #enddocregion template
|
|
||||||
styleUrls: ['./hero-list.component.css'],
|
|
||||||
/**
|
|
||||||
* Define two states, "inactive" and "active", and the end
|
|
||||||
* styles that apply whenever the element is in those states.
|
|
||||||
* Then define an animation for the inactive => active transition.
|
|
||||||
* This animation has no end styles, but only styles that are
|
|
||||||
* defined inline inside the transition and thus are only kept
|
|
||||||
* as long as the animation is running.
|
|
||||||
*/
|
|
||||||
// #docregion animationdef
|
|
||||||
animations: [
|
|
||||||
trigger('heroState', [
|
|
||||||
// #docregion transitions
|
|
||||||
transition('inactive => active', [
|
|
||||||
style({
|
|
||||||
backgroundColor: '#cfd8dc',
|
|
||||||
transform: 'scale(1.3)'
|
|
||||||
}),
|
|
||||||
animate('80ms ease-in', style({
|
|
||||||
backgroundColor: '#eee',
|
|
||||||
transform: 'scale(1)'
|
|
||||||
}))
|
|
||||||
]),
|
|
||||||
// #enddocregion transitions
|
|
||||||
])
|
|
||||||
]
|
|
||||||
// #enddocregion animationdef
|
|
||||||
})
|
|
||||||
export class HeroListInlineStylesComponent {
|
|
||||||
@Input() heroes: Hero[];
|
|
||||||
}
|
|
@ -1,71 +0,0 @@
|
|||||||
import {
|
|
||||||
Component,
|
|
||||||
Input,
|
|
||||||
} from '@angular/core';
|
|
||||||
import {
|
|
||||||
trigger,
|
|
||||||
state,
|
|
||||||
style,
|
|
||||||
animate,
|
|
||||||
transition,
|
|
||||||
keyframes,
|
|
||||||
AnimationEvent
|
|
||||||
} from '@angular/animations';
|
|
||||||
|
|
||||||
import { Hero } from './hero.service';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-hero-list-multistep',
|
|
||||||
// #docregion template
|
|
||||||
template: `
|
|
||||||
<ul>
|
|
||||||
<li *ngFor="let hero of heroes"
|
|
||||||
(@flyInOut.start)="animationStarted($event)"
|
|
||||||
(@flyInOut.done)="animationDone($event)"
|
|
||||||
[@flyInOut]="'in'">
|
|
||||||
{{hero.name}}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
`,
|
|
||||||
// #enddocregion template
|
|
||||||
styleUrls: ['./hero-list.component.css'],
|
|
||||||
/* The element here always has the state "in" when it
|
|
||||||
* is present. We animate two transitions: From void
|
|
||||||
* to in and from in to void, to achieve an animated
|
|
||||||
* enter and leave transition. Each transition is
|
|
||||||
* defined in terms of multiple keyframes, to give it
|
|
||||||
* a bounce effect.
|
|
||||||
*/
|
|
||||||
// #docregion animationdef
|
|
||||||
animations: [
|
|
||||||
trigger('flyInOut', [
|
|
||||||
state('in', style({transform: 'translateX(0)'})),
|
|
||||||
transition('void => *', [
|
|
||||||
animate(300, keyframes([
|
|
||||||
style({opacity: 0, transform: 'translateX(-100%)', offset: 0}),
|
|
||||||
style({opacity: 1, transform: 'translateX(15px)', offset: 0.3}),
|
|
||||||
style({opacity: 1, transform: 'translateX(0)', offset: 1.0})
|
|
||||||
]))
|
|
||||||
]),
|
|
||||||
transition('* => void', [
|
|
||||||
animate(300, keyframes([
|
|
||||||
style({opacity: 1, transform: 'translateX(0)', offset: 0}),
|
|
||||||
style({opacity: 1, transform: 'translateX(-15px)', offset: 0.7}),
|
|
||||||
style({opacity: 0, transform: 'translateX(100%)', offset: 1.0})
|
|
||||||
]))
|
|
||||||
])
|
|
||||||
])
|
|
||||||
]
|
|
||||||
// #enddocregion animationdef
|
|
||||||
})
|
|
||||||
export class HeroListMultistepComponent {
|
|
||||||
@Input() heroes: Hero[];
|
|
||||||
|
|
||||||
animationStarted(event: AnimationEvent) {
|
|
||||||
console.warn('Animation started: ', event);
|
|
||||||
}
|
|
||||||
|
|
||||||
animationDone(event: AnimationEvent) {
|
|
||||||
console.warn('Animation done: ', event);
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,94 @@
|
|||||||
|
.heroes {
|
||||||
|
margin: 0 0 2em 0;
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 0;
|
||||||
|
width: 15em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heroes li {
|
||||||
|
position: relative;
|
||||||
|
height: 2.3em;
|
||||||
|
overflow:hidden;
|
||||||
|
margin: .5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heroes li > .inner {
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: #EEE;
|
||||||
|
padding: .3em 0;
|
||||||
|
height: 1.6em;
|
||||||
|
border-radius: 4px;
|
||||||
|
width: 19em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heroes li:hover > .inner {
|
||||||
|
color: #607D8B;
|
||||||
|
background-color: #DDD;
|
||||||
|
transform: translateX(.1em);
|
||||||
|
}
|
||||||
|
|
||||||
|
.heroes a {
|
||||||
|
color: #888;
|
||||||
|
text-decoration: none;
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
width: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heroes a:hover {
|
||||||
|
color:#607D8B;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heroes .badge {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: small;
|
||||||
|
color: white;
|
||||||
|
padding: 0.8em 0.7em 0 0.7em;
|
||||||
|
background-color: #607D8B;
|
||||||
|
line-height: 1em;
|
||||||
|
position: relative;
|
||||||
|
left: -1px;
|
||||||
|
top: -4px;
|
||||||
|
height: 1.8em;
|
||||||
|
min-width: 16px;
|
||||||
|
text-align: right;
|
||||||
|
margin-right: .8em;
|
||||||
|
border-radius: 4px 0 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
background-color: #eee;
|
||||||
|
border: none;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
cursor: hand;
|
||||||
|
font-family: Arial;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: #cfd8dc;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.delete {
|
||||||
|
position: relative;
|
||||||
|
left: 24em;
|
||||||
|
top: -32px;
|
||||||
|
background-color: gray !important;
|
||||||
|
color: white;
|
||||||
|
display: inherit;
|
||||||
|
padding: 5px 8px;
|
||||||
|
width: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
font-size: 100%;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
width: 11em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heroes input {
|
||||||
|
position: relative;
|
||||||
|
top: -3px;
|
||||||
|
width: 12em;
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
<!-- #docplaster -->
|
||||||
|
<h2>Filter/Stagger</h2>
|
||||||
|
|
||||||
|
<form>
|
||||||
|
<input #criteria (input)="updateCriteria(criteria.value)" placeholder="Search Heroes" />
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- #docregion filter-animations -->
|
||||||
|
<ul class="heroes" [@filterAnimation]="heroTotal">
|
||||||
|
<!-- #enddocregion filter-animations -->
|
||||||
|
<li *ngFor="let hero of heroes" class="hero">
|
||||||
|
<div class="inner">
|
||||||
|
<span class="badge">{{ hero.id }}</span>
|
||||||
|
<span>{{ hero.name }}</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<!-- #docregion filter-animations -->
|
||||||
|
</ul>
|
||||||
|
<!-- #enddocregion filter-animations -->
|
@ -0,0 +1,81 @@
|
|||||||
|
// #docplaster
|
||||||
|
import { Component, HostBinding, OnInit } from '@angular/core';
|
||||||
|
import { trigger, transition, animate, style, query, stagger } from '@angular/animations';
|
||||||
|
import { HEROES } from './mock-heroes';
|
||||||
|
|
||||||
|
// #docregion filter-animations
|
||||||
|
@Component({
|
||||||
|
// #enddocregion filter-animations
|
||||||
|
selector: 'app-hero-list-page',
|
||||||
|
templateUrl: 'hero-list-page.component.html',
|
||||||
|
styleUrls: ['hero-list-page.component.css'],
|
||||||
|
// #docregion page-animations, filter-animations
|
||||||
|
animations: [
|
||||||
|
// #enddocregion filter-animations
|
||||||
|
trigger('pageAnimations', [
|
||||||
|
transition(':enter', [
|
||||||
|
query('.hero, form', [
|
||||||
|
style({opacity: 0, transform: 'translateY(-100px)'}),
|
||||||
|
stagger(-30, [
|
||||||
|
animate('500ms cubic-bezier(0.35, 0, 0.25, 1)', style({ opacity: 1, transform: 'none' }))
|
||||||
|
])
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
// #enddocregion page-animations
|
||||||
|
// #docregion increment
|
||||||
|
// #docregion filter-animations
|
||||||
|
trigger('filterAnimation', [
|
||||||
|
transition(':enter, * => 0, * => -1', []),
|
||||||
|
transition(':increment', [
|
||||||
|
query(':enter', [
|
||||||
|
style({ opacity: 0, width: '0px' }),
|
||||||
|
stagger(50, [
|
||||||
|
animate('300ms ease-out', style({ opacity: 1, width: '*' })),
|
||||||
|
]),
|
||||||
|
], { optional: true })
|
||||||
|
]),
|
||||||
|
transition(':decrement', [
|
||||||
|
query(':leave', [
|
||||||
|
stagger(50, [
|
||||||
|
animate('300ms ease-out', style({ opacity: 0, width: '0px' })),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
// #enddocregion increment
|
||||||
|
// #docregion page-animations
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class HeroListPageComponent implements OnInit {
|
||||||
|
// #enddocregion filter-animations
|
||||||
|
@HostBinding('@pageAnimations')
|
||||||
|
public animatePage = true;
|
||||||
|
|
||||||
|
_heroes = [];
|
||||||
|
// #docregion filter-animations
|
||||||
|
heroTotal = -1;
|
||||||
|
// #enddocregion filter-animations
|
||||||
|
get heroes() {
|
||||||
|
return this._heroes;
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this._heroes = HEROES;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCriteria(criteria: string) {
|
||||||
|
criteria = criteria ? criteria.trim() : '';
|
||||||
|
|
||||||
|
this._heroes = HEROES.filter(hero => hero.name.toLowerCase().includes(criteria.toLowerCase()));
|
||||||
|
const newTotal = this.heroes.length;
|
||||||
|
|
||||||
|
if (this.heroTotal !== newTotal) {
|
||||||
|
this.heroTotal = newTotal;
|
||||||
|
} else if (!criteria) {
|
||||||
|
this.heroTotal = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// #docregion filter-animations
|
||||||
|
}
|
||||||
|
// #enddocregion filter-animations
|
@ -1,58 +0,0 @@
|
|||||||
import {
|
|
||||||
Component,
|
|
||||||
Input
|
|
||||||
} from '@angular/core';
|
|
||||||
import {
|
|
||||||
trigger,
|
|
||||||
state,
|
|
||||||
style,
|
|
||||||
animate,
|
|
||||||
transition
|
|
||||||
} from '@angular/animations';
|
|
||||||
|
|
||||||
import { Hero } from './hero.service';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-hero-list-timings',
|
|
||||||
template: `
|
|
||||||
<ul>
|
|
||||||
<li *ngFor="let hero of heroes"
|
|
||||||
[@flyInOut]="'in'"
|
|
||||||
(click)="hero.toggleState()">
|
|
||||||
{{hero.name}}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
`,
|
|
||||||
styleUrls: ['./hero-list.component.css'],
|
|
||||||
/* The element here always has the state "in" when it
|
|
||||||
* is present. We animate two transitions: From void
|
|
||||||
* to in and from in to void, to achieve an animated
|
|
||||||
* enter and leave transition. The element enters from
|
|
||||||
* the left and leaves to the right using translateX,
|
|
||||||
* and fades in/out using opacity. We use different easings
|
|
||||||
* for enter and leave.
|
|
||||||
*/
|
|
||||||
// #docregion animationdef
|
|
||||||
animations: [
|
|
||||||
trigger('flyInOut', [
|
|
||||||
state('in', style({opacity: 1, transform: 'translateX(0)'})),
|
|
||||||
transition('void => *', [
|
|
||||||
style({
|
|
||||||
opacity: 0,
|
|
||||||
transform: 'translateX(-100%)'
|
|
||||||
}),
|
|
||||||
animate('0.2s ease-in')
|
|
||||||
]),
|
|
||||||
transition('* => void', [
|
|
||||||
animate('0.2s 0.1s ease-out', style({
|
|
||||||
opacity: 0,
|
|
||||||
transform: 'translateX(100%)'
|
|
||||||
}))
|
|
||||||
])
|
|
||||||
])
|
|
||||||
]
|
|
||||||
// #enddocregion animationdef
|
|
||||||
})
|
|
||||||
export class HeroListTimingsComponent {
|
|
||||||
@Input() heroes: Hero[];
|
|
||||||
}
|
|
@ -1,58 +0,0 @@
|
|||||||
// #docregion
|
|
||||||
// #docregion imports
|
|
||||||
import {
|
|
||||||
Component,
|
|
||||||
Input
|
|
||||||
} from '@angular/core';
|
|
||||||
import {
|
|
||||||
trigger,
|
|
||||||
state,
|
|
||||||
style,
|
|
||||||
animate,
|
|
||||||
transition
|
|
||||||
} from '@angular/animations';
|
|
||||||
// #enddocregion imports
|
|
||||||
|
|
||||||
import { Hero } from './hero.service';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-hero-list-twoway',
|
|
||||||
// #docregion template
|
|
||||||
template: `
|
|
||||||
<ul>
|
|
||||||
<li *ngFor="let hero of heroes"
|
|
||||||
[@heroState]="hero.state"
|
|
||||||
(click)="hero.toggleState()">
|
|
||||||
{{hero.name}}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
`,
|
|
||||||
// #enddocregion template
|
|
||||||
styleUrls: ['./hero-list.component.css'],
|
|
||||||
/*
|
|
||||||
* Define two states, "inactive" and "active", and the end
|
|
||||||
* styles that apply whenever the element is in those states.
|
|
||||||
* Then define an animated transition between these two
|
|
||||||
* states, in *both* directions.
|
|
||||||
*/
|
|
||||||
// #docregion animationdef
|
|
||||||
animations: [
|
|
||||||
trigger('heroState', [
|
|
||||||
state('inactive', style({
|
|
||||||
backgroundColor: '#eee',
|
|
||||||
transform: 'scale(1)'
|
|
||||||
})),
|
|
||||||
state('active', style({
|
|
||||||
backgroundColor: '#cfd8dc',
|
|
||||||
transform: 'scale(1.1)'
|
|
||||||
})),
|
|
||||||
// #docregion transitions
|
|
||||||
transition('inactive <=> active', animate('100ms ease-out'))
|
|
||||||
// #enddocregion transitions
|
|
||||||
])
|
|
||||||
]
|
|
||||||
// #enddocregion animationdef
|
|
||||||
})
|
|
||||||
export class HeroListTwowayComponent {
|
|
||||||
@Input() heroes: Hero[];
|
|
||||||
}
|
|
@ -1,99 +0,0 @@
|
|||||||
import { Component } from '@angular/core';
|
|
||||||
|
|
||||||
import { Hero, HeroService } from './hero.service';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-root',
|
|
||||||
template: `
|
|
||||||
<div class="buttons">
|
|
||||||
<button [disabled]="!heroService.canAdd()" (click)="heroService.addInactive()">Add inactive hero</button>
|
|
||||||
<button [disabled]="!heroService.canAdd()" (click)="heroService.addActive()">Add active hero</button>
|
|
||||||
<button [disabled]="!heroService.canRemove()" (click)="heroService.remove()">Remove hero</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="columns">
|
|
||||||
<div class="column">
|
|
||||||
<h4>Basic State</h4>
|
|
||||||
<p>Switch between active/inactive on click.</p>
|
|
||||||
<app-hero-list-basic [heroes]="heroes"></app-hero-list-basic>
|
|
||||||
</div>
|
|
||||||
<div class="column">
|
|
||||||
<h4>Styles inline in transitions</h4>
|
|
||||||
<p>Animated effect on click, no persistend end styles.</p>
|
|
||||||
<app-hero-list-inline-styles [heroes]="heroes"></app-hero-list-inline-styles>
|
|
||||||
</div>
|
|
||||||
<div class="column">
|
|
||||||
<h4>Combined transition syntax</h4>
|
|
||||||
<p>Switch between active/inactive on click. Define just one transition used in both directions.</p>
|
|
||||||
<app-hero-list-combined-transitions [heroes]="heroes"></app-hero-list-combined-transitions>
|
|
||||||
</div>
|
|
||||||
<div class="column">
|
|
||||||
<h4>Two-way transition syntax</h4>
|
|
||||||
<p>Switch between active/inactive on click. Define just one transition used in both directions using the <=> syntax.</p>
|
|
||||||
<app-hero-list-twoway [heroes]="heroes"></app-hero-list-twoway>
|
|
||||||
</div>
|
|
||||||
<div class="column">
|
|
||||||
<h4>Enter & Leave</h4>
|
|
||||||
<p>Enter and leave animations using the void state.</p>
|
|
||||||
<app-hero-list-enter-leave [heroes]="heroes"></app-hero-list-enter-leave>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="columns">
|
|
||||||
<div class="column">
|
|
||||||
<h4>Enter & Leave & States</h4>
|
|
||||||
<p>
|
|
||||||
Enter and leave animations combined with active/inactive state animations.
|
|
||||||
Different enter and leave transitions depending on state.
|
|
||||||
</p>
|
|
||||||
<app-hero-list-enter-leave-states [heroes]="heroes"></app-hero-list-enter-leave-states>
|
|
||||||
</div>
|
|
||||||
<div class="column">
|
|
||||||
<h4>Auto Style Calc</h4>
|
|
||||||
<p>Leave animation from the current computed height using the auto-style value *.</p>
|
|
||||||
<app-hero-list-auto [heroes]="heroes"></app-hero-list-auto>
|
|
||||||
</div>
|
|
||||||
<div class="column">
|
|
||||||
<h4>Different Timings</h4>
|
|
||||||
<p>Enter and leave animations with different easings, ease-in for enter, ease-out for leave.</p>
|
|
||||||
<app-hero-list-timings [heroes]="heroes"></app-hero-list-timings>
|
|
||||||
</div>
|
|
||||||
<div class="column">
|
|
||||||
<h4>Multiple Keyframes</h4>
|
|
||||||
<p>Enter and leave animations with three keyframes in each, to give the transition some bounce.</p>
|
|
||||||
<app-hero-list-multistep [heroes]="heroes"></app-hero-list-multistep>
|
|
||||||
</div>
|
|
||||||
<div class="column">
|
|
||||||
<h4>Parallel Groups</h4>
|
|
||||||
<p>Enter and leave animations with multiple properties animated in parallel with different timings.</p>
|
|
||||||
<app-hero-list-groups [heroes]="heroes"></app-hero-list-groups>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
styles: [`
|
|
||||||
.buttons {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
button {
|
|
||||||
padding: 1.5em 3em;
|
|
||||||
}
|
|
||||||
.columns {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
.column {
|
|
||||||
flex: 1;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
.column p {
|
|
||||||
min-height: 6em;
|
|
||||||
}
|
|
||||||
`],
|
|
||||||
providers: [HeroService]
|
|
||||||
})
|
|
||||||
export class HeroTeamBuilderComponent {
|
|
||||||
heroes: Hero[];
|
|
||||||
|
|
||||||
constructor(private heroService: HeroService) {
|
|
||||||
this.heroes = heroService.heroes;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,54 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core';
|
|
||||||
|
|
||||||
// #docregion hero
|
|
||||||
export class Hero {
|
|
||||||
constructor(public name: string, public state = 'inactive') { }
|
|
||||||
|
|
||||||
toggleState() {
|
|
||||||
this.state = this.state === 'active' ? 'inactive' : 'active';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// #enddocregion hero
|
|
||||||
|
|
||||||
const ALL_HEROES = [
|
|
||||||
'Windstorm',
|
|
||||||
'RubberMan',
|
|
||||||
'Bombasto',
|
|
||||||
'Magneta',
|
|
||||||
'Dynama',
|
|
||||||
'Narco',
|
|
||||||
'Celeritas',
|
|
||||||
'Dr IQ',
|
|
||||||
'Magma',
|
|
||||||
'Tornado',
|
|
||||||
'Mr. Nice'
|
|
||||||
].map(name => new Hero(name));
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class HeroService {
|
|
||||||
|
|
||||||
heroes: Hero[] = [];
|
|
||||||
|
|
||||||
canAdd() {
|
|
||||||
return this.heroes.length < ALL_HEROES.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
canRemove() {
|
|
||||||
return this.heroes.length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
addActive(active = true) {
|
|
||||||
let hero = ALL_HEROES[this.heroes.length];
|
|
||||||
hero.state = active ? 'active' : 'inactive';
|
|
||||||
this.heroes.push(hero);
|
|
||||||
}
|
|
||||||
|
|
||||||
addInactive() {
|
|
||||||
this.addActive(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
remove() {
|
|
||||||
this.heroes.length -= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
4
aio/content/examples/animations/src/app/hero.ts
Normal file
4
aio/content/examples/animations/src/app/hero.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export class Hero {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
<p>
|
||||||
|
Welcome to Animations in Angular!
|
||||||
|
</p>
|
15
aio/content/examples/animations/src/app/home.component.ts
Normal file
15
aio/content/examples/animations/src/app/home.component.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-home',
|
||||||
|
templateUrl: './home.component.html',
|
||||||
|
styleUrls: ['./home.component.css']
|
||||||
|
})
|
||||||
|
export class HomeComponent implements OnInit {
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insert-remove-container {
|
||||||
|
border: 1px solid #dddddd;
|
||||||
|
margin-top: 1em;
|
||||||
|
padding: 20px 20px 0px 20px;
|
||||||
|
color: #000000;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
<!-- #docplaster -->
|
||||||
|
<nav>
|
||||||
|
<button (click)="toggle()">Toggle Insert/Remove</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- #docregion insert-remove-->
|
||||||
|
<div @myInsertRemoveTrigger *ngIf="isShown" class="insert-remove-container">
|
||||||
|
<p>The box is inserted</p>
|
||||||
|
</div>
|
||||||
|
<!-- #enddocregion insert-remove-->
|
@ -0,0 +1,29 @@
|
|||||||
|
// #docplaster
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { trigger, transition, animate, style } from '@angular/animations';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-insert-remove',
|
||||||
|
animations: [
|
||||||
|
// #docregion enter-leave-trigger
|
||||||
|
trigger('myInsertRemoveTrigger', [
|
||||||
|
transition(':enter', [
|
||||||
|
style({ opacity: 0 }),
|
||||||
|
animate('5s', style({ opacity: 1 })),
|
||||||
|
]),
|
||||||
|
transition(':leave', [
|
||||||
|
animate('5s', style({ opacity: 0 }))
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
// #enddocregion enter-leave-trigger
|
||||||
|
],
|
||||||
|
templateUrl: 'insert-remove.component.html',
|
||||||
|
styleUrls: ['insert-remove.component.css']
|
||||||
|
})
|
||||||
|
export class InsertRemoveComponent {
|
||||||
|
isShown = false;
|
||||||
|
|
||||||
|
toggle() {
|
||||||
|
this.isShown = !this.isShown;
|
||||||
|
}
|
||||||
|
}
|
15
aio/content/examples/animations/src/app/mock-heroes.ts
Normal file
15
aio/content/examples/animations/src/app/mock-heroes.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
// #docregion
|
||||||
|
import { Hero } from './hero';
|
||||||
|
|
||||||
|
export const HEROES: Hero[] = [
|
||||||
|
{ id: 11, name: 'Mr. Nice' },
|
||||||
|
{ id: 12, name: 'Narco' },
|
||||||
|
{ id: 13, name: 'Bombasto' },
|
||||||
|
{ id: 14, name: 'Celeritas' },
|
||||||
|
{ id: 15, name: 'Magneta' },
|
||||||
|
{ id: 16, name: 'RubberMan' },
|
||||||
|
{ id: 17, name: 'Dynama' },
|
||||||
|
{ id: 18, name: 'Dr IQ' },
|
||||||
|
{ id: 19, name: 'Magma' },
|
||||||
|
{ id: 20, name: 'Tornado' }
|
||||||
|
];
|
@ -0,0 +1,20 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-open-close-page',
|
||||||
|
template: `
|
||||||
|
<section>
|
||||||
|
<h2>Open Close Component</h2>
|
||||||
|
<input type="checkbox" [checked]="logging" (click)="toggleLogging()"/> Console Log Animation Events
|
||||||
|
|
||||||
|
<app-open-close [logging]="logging"></app-open-close>
|
||||||
|
</section>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
export class OpenClosePageComponent {
|
||||||
|
logging = false;
|
||||||
|
|
||||||
|
toggleLogging() {
|
||||||
|
this.logging = !this.logging;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
<!-- #docplaster -->
|
||||||
|
<nav>
|
||||||
|
<button (click)="toggle()">Toggle Open/Close</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- #docregion compare, trigger -->
|
||||||
|
<div [@openClose]="isOpen ? 'open' : 'closed'" class="open-close-container">
|
||||||
|
<p>The box is now {{ isOpen ? 'Open' : 'Closed' }}!</p>
|
||||||
|
</div>
|
||||||
|
<!-- #enddocregion compare, trigger -->
|
@ -0,0 +1,40 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { trigger, transition, state, animate, style, keyframes } from '@angular/animations';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-open-close',
|
||||||
|
animations: [
|
||||||
|
// #docregion trigger
|
||||||
|
trigger('openClose', [
|
||||||
|
state('open', style({
|
||||||
|
height: '200px',
|
||||||
|
opacity: 1,
|
||||||
|
backgroundColor: 'yellow'
|
||||||
|
})),
|
||||||
|
state('close', style({
|
||||||
|
height: '100px',
|
||||||
|
opacity: 0.5,
|
||||||
|
backgroundColor: 'green'
|
||||||
|
})),
|
||||||
|
// ...
|
||||||
|
transition('* => *', [
|
||||||
|
animate('1s', keyframes ( [
|
||||||
|
style({ opacity: 0.1, offset: 0.1 }),
|
||||||
|
style({ opacity: 0.6, offset: 0.2 }),
|
||||||
|
style({ opacity: 1, offset: 0.5 }),
|
||||||
|
style({ opacity: 0.2, offset: 0.7 })
|
||||||
|
]))
|
||||||
|
])
|
||||||
|
])
|
||||||
|
// #enddocregion trigger
|
||||||
|
],
|
||||||
|
templateUrl: 'open-close.component.html',
|
||||||
|
styleUrls: ['open-close.component.css']
|
||||||
|
})
|
||||||
|
export class OpenCloseKeyframeComponent {
|
||||||
|
isOpen = false;
|
||||||
|
|
||||||
|
toggle() {
|
||||||
|
this.isOpen = !this.isOpen;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
<!-- #docplaster -->
|
||||||
|
<nav>
|
||||||
|
<button (click)="toggle()">Toggle Boolean/Close</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- #docregion trigger-boolean -->
|
||||||
|
<div [@openClose]="isOpen ? true : false" class="open-close-container">
|
||||||
|
<!-- #enddocregion trigger-boolean -->
|
||||||
|
<p>The box is now {{ isOpen ? 'Open' : 'Closed' }}!</p>
|
||||||
|
<!-- #docregion trigger-boolean -->
|
||||||
|
</div>
|
||||||
|
<!-- #enddocregion trigger-boolean -->
|
@ -0,0 +1,24 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { trigger, transition, state, animate, style } from '@angular/animations';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-open-close-boolean',
|
||||||
|
// #docregion trigger-boolean
|
||||||
|
animations: [
|
||||||
|
trigger('openClose', [
|
||||||
|
state('true', style({ height: '*' })),
|
||||||
|
state('false', style({ height: '0px' })),
|
||||||
|
transition('false <=> true', animate(500))
|
||||||
|
])
|
||||||
|
],
|
||||||
|
// #enddocregion trigger-boolean
|
||||||
|
templateUrl: 'open-close.component.2.html',
|
||||||
|
styleUrls: ['open-close.component.css']
|
||||||
|
})
|
||||||
|
export class OpenCloseBooleanComponent {
|
||||||
|
isOpen = false;
|
||||||
|
|
||||||
|
toggle() {
|
||||||
|
this.isOpen = !this.isOpen;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
<!-- #docplaster -->
|
||||||
|
<nav>
|
||||||
|
<button (click)="toggle()">Toggle Open/Close</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- #docregion callbacks -->
|
||||||
|
<div [@openClose]="isOpen ? 'open' : 'closed'"
|
||||||
|
(@openClose.start)="onAnimationEvent($event)"
|
||||||
|
(@openClose.done)="onAnimationEvent($event)"
|
||||||
|
class="open-close-container">
|
||||||
|
<!-- #enddocregion callbacks -->
|
||||||
|
<p>The box is now {{ isOpen ? 'Open' : 'Closed' }}!</p>
|
||||||
|
<!-- #docregion callbacks -->
|
||||||
|
</div>
|
||||||
|
<!-- #enddocregion callbacks -->
|
@ -0,0 +1,48 @@
|
|||||||
|
// #docplaster
|
||||||
|
// #docregion reusable
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { useAnimation, transition, trigger, style, animate } from '@angular/animations';
|
||||||
|
import { transAnimation } from './animations';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
// #enddocregion reusable
|
||||||
|
selector: 'app-open-close-reusable',
|
||||||
|
// #docregion runtime
|
||||||
|
animations: [
|
||||||
|
transition('open => closed', [
|
||||||
|
style({
|
||||||
|
height: '200 px',
|
||||||
|
opacity: '{{ opacity }}',
|
||||||
|
backgroundcolor: 'yelow'
|
||||||
|
}),
|
||||||
|
animate('{{ time }}'),
|
||||||
|
], {
|
||||||
|
params: {
|
||||||
|
time: '1s',
|
||||||
|
opacity: '1'
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
// #enddocregion runtime
|
||||||
|
// #docregion reusable
|
||||||
|
trigger('openClose', [
|
||||||
|
transition('open => closed', [
|
||||||
|
useAnimation(transAnimation, {
|
||||||
|
params: {
|
||||||
|
height: 0,
|
||||||
|
opacity: 1,
|
||||||
|
backgroundColor: 'red',
|
||||||
|
time: '1s'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
])
|
||||||
|
])
|
||||||
|
// #docregion runtime
|
||||||
|
],
|
||||||
|
// #enddocregion runtime
|
||||||
|
// #enddocregion reusable
|
||||||
|
templateUrl: 'open-close.component.html',
|
||||||
|
styleUrls: ['open-close.component.css']
|
||||||
|
// #docregion reusable
|
||||||
|
})
|
||||||
|
// #enddocregion reusable
|
||||||
|
export class OpenCloseReusableComponent { }
|
@ -0,0 +1,12 @@
|
|||||||
|
<nav>
|
||||||
|
<button (click)="toggleAnimations()">Toggle Animations</button>
|
||||||
|
<button (click)="toggle()">Toggle Open/Closed</button>
|
||||||
|
</nav>
|
||||||
|
<!-- #docregion toggle-animation -->
|
||||||
|
<div [@.disabled]="isDisabled">
|
||||||
|
<div [@childAnimation]="isOpen ? 'open' : 'closed'"
|
||||||
|
class="open-close-container">
|
||||||
|
<p>The box is now {{ isOpen ? 'Open' : 'Closed' }}!</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- #enddocregion toggle-animation -->
|
@ -0,0 +1,47 @@
|
|||||||
|
// #docplaster
|
||||||
|
// #docregion
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { trigger, transition, state, animate, style } from '@angular/animations';
|
||||||
|
|
||||||
|
// #docregion toggle-animation
|
||||||
|
@Component({
|
||||||
|
// #enddocregion toggle-animation
|
||||||
|
selector: 'app-open-close-toggle',
|
||||||
|
templateUrl: 'open-close.component.4.html',
|
||||||
|
styleUrls: ['open-close.component.css'],
|
||||||
|
// #docregion toggle-animation
|
||||||
|
animations: [
|
||||||
|
trigger('childAnimation', [
|
||||||
|
// ...
|
||||||
|
// #enddocregion toggle-animation
|
||||||
|
state('open', style({
|
||||||
|
width: '250px',
|
||||||
|
opacity: 1,
|
||||||
|
backgroundColor: 'yellow'
|
||||||
|
})),
|
||||||
|
state('closed', style({
|
||||||
|
width: '100px',
|
||||||
|
opacity: 0.5,
|
||||||
|
backgroundColor: 'green'
|
||||||
|
})),
|
||||||
|
transition('* => *', [
|
||||||
|
animate('1s')
|
||||||
|
]),
|
||||||
|
// #docregion toggle-animation
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class OpenCloseChildComponent {
|
||||||
|
isDisabled = false;
|
||||||
|
isOpen = false;
|
||||||
|
// #enddocregion toggle-animation
|
||||||
|
toggleAnimations() {
|
||||||
|
this.isDisabled = !this.isDisabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle() {
|
||||||
|
this.isOpen = !this.isOpen;
|
||||||
|
}
|
||||||
|
// #docregion toggle-animation
|
||||||
|
}
|
||||||
|
// #enddocregion toggle-animation
|
@ -0,0 +1,12 @@
|
|||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.open-close-container {
|
||||||
|
border: 1px solid #dddddd;
|
||||||
|
margin-top: 1em;
|
||||||
|
padding: 20px 20px 0px 20px;
|
||||||
|
color: #000000;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
<!-- #docplaster -->
|
||||||
|
<nav>
|
||||||
|
<button (click)="toggle()">Toggle Open/Close</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- #docregion compare, trigger -->
|
||||||
|
<div [@openClose]="isOpen ? 'open' : 'closed'"
|
||||||
|
(@openClose.start)="onAnimationEvent($event)"
|
||||||
|
(@openClose.done)="onAnimationEvent($event)"
|
||||||
|
class="open-close-container">
|
||||||
|
<p>The box is now {{ isOpen ? 'Open' : 'Closed' }}!</p>
|
||||||
|
</div>
|
||||||
|
<!-- #enddocregion compare, trigger -->
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user