Compare commits

..

158 Commits

Author SHA1 Message Date
b40b5576df release: cut the v10.0.0-next.1 release 2020-04-08 12:28:12 -07:00
03f2f1ae47 build(bazel): fix runfiles resolve in karma-saucelabs.js after $location => $rootpath cleanup (#36511)
This wasn't caught by CI on the PR as this binary is only run once daily via a monitor job.

PR Close #36511
2020-04-08 12:13:27 -07:00
a8978ebf8e build: ts-circular-deps tool should normalize golden (#36505)
Currently the golden output of the circular-deps tool is purely
based on the order of source files passed to the tool, and on the
amount of imports inside source files.

This is actually resulting in deterministic output as running
the tool multiple times without any changes to source files,
results in the same output.

Though it seems like the tool is too strict and we can avoid
unnecessary golden changes if:

1. A source file that is part of a cycle is imported earlier (in terms
of how the analyzer visits them). This could result in the cycle path
starting with a different source file.

2. Source files which are not part of a cycle are imported earlier
(in terms of how the analyzer visits them). This could result in moved
items in the golden if re-approved (even though the cycles remain the same)

To fix this, we normalize the cycle path array that serves as
serializable data structure for the text-based goldens. Since
the paths represents a cycle, the path can be shifted in a
deterministic way so that cycles don't change unnecessarily
in the golden, and to simplify comparison of cycles.

Additionally, we sort the cycles in a deterministic way so
that the golden doesn't change unnecessarily (as explained above).

PR Close #36505
2020-04-08 12:12:58 -07:00
7d7b59efa5 docs: update the Support policy and schedule (#35390)
PR Close #35359

PR Close #35390
2020-04-08 12:12:32 -07:00
4cf89d4bd7 docs: replace browserslist with .browserslistrc (#36504)
In version 10, this file has been changed.

See: https://github.com/angular/angular-cli/pull/17367

PR Close #36504
2020-04-08 12:12:03 -07:00
d47b318fee build: remove fullTemplateTypeCheck from aio tsconfig (#36502)
`fullTemplateTypeCheck` is no longer required since we now use `strictTemplates` which is a superset of the former option.

Follow-up on: 04f61c0c3e (r38354112)

PR Close #36502
2020-04-08 12:11:28 -07:00
e485236502 test(language-service): Inline test cases in parsing-cases.ts (#36495)
This commit removes individual components from parsing-cases.ts and
colocate them with the actual tests. This makes the tests more readable.

PR Close #36495
2020-04-08 12:11:04 -07:00
41667de778 fix(zone.js): add issue numbers of @types/jasmine to the test cases (#34625)
Some cases will still need to use `spy as any` cast, because `@types/jasmine` have some issues,
1. The issue jasmine doesn't handle optional method properties, https://github.com/DefinitelyTyped/DefinitelyTyped/issues/43486
2. The issue jasmine doesn't handle overload method correctly, https://github.com/DefinitelyTyped/DefinitelyTyped/issues/42455

PR Close #34625
2020-04-08 12:10:34 -07:00
ef4736d052 build: update jasmine to 3.5 (#34625)
1. update jasmine to 3.5
2. update @types/jasmine to 3.5
3. update @types/jasminewd2 to 2.0.8

Also fix several cases, the new jasmine 3 will help to create test cases correctly,
such as in the `jasmine 2.x` version, the following case will pass

```
expect(1 == 2);
```

But in jsamine 3, the case will need to be

```
expect(1 == 2).toBeTrue();
```

PR Close #34625
2020-04-08 12:10:34 -07:00
db4a448439 release: cut the v10.0.0-next.0 release 2020-04-08 11:08:01 -07:00
7549c65502 build: update matching regex for bazel stamping (#36523)
Previously, the bazel stamping regex only matched on versions
0-9 for major and minor numbers, this update allows for matching
on any number for major, minor or patch.

PR Close #36523
2020-04-08 11:04:08 -07:00
1beccc1a6c docs: release notes for the v9.1.1 release 2020-04-07 16:37:55 -07:00
0075017b43 docs: update reactive forms page (#35969)
PR Close #35969
2020-04-07 15:24:17 -07:00
4374931b0e fix(zone.js): zone.js patch jest should handle done correctly (#36022)
`zone.js` supports jest `test.each()` methods, but it
introduces a bug, which is the `done()` function will not be handled correctly.

```
it('should work with done', done => {
  // done will be undefined.
});
```

The reason is the logic of monkey patching `test` method is different from `jasmine` patch

// jasmine patch
```
return testBody.length === 0
   ? () => testProxyZone.run(testBody, null)
   : done => testProxyZone.run(testBody, null, [done]);
```

// jest patch
```
 return function(...args) {
   return testProxyZone.run(testBody, null, args);
 };
```

the purpose of this change is to handle the following cases.

```
test.each([1, 2])('test.each', (arg1, arg2) => {
  expect(arg1).toBe(1);
  expect(arg2).toBe(2);
});
```

so in jest, it is a little complex, because the `testBody`'s parameter may be bigger than 1, so the
logic in `jasmine`

```
return testBody.length === 0
   ? () => testProxyZone.run(testBody, null)
   : done => testProxyZone.run(testBody, null, [done]);
```
will not work for `test.each` in jest.

So in this PR, I created a dynamic `Function` to return the correct length of paramters (which is required by jest core), to handle
1. normal `test` with or without `done`.
2. each with parameters with or without done.

PR Close #36022
2020-04-07 15:22:16 -07:00
64022f51d4 fix(compiler): resolve enum values in binary operations (#36461)
During static evaluation of expressions, the partial evaluator
may come across a binary + operator for which it needs to
evaluate its operands. Any of these operands may be a reference
to an enum member, in which case the enum member's value needs
to be used as literal value, not the enum member reference
itself. This commit fixes the behavior by resolving an
`EnumValue` when used as a literal value.

Fixes #35584
Resolves FW-1951

PR Close #36461
2020-04-07 15:21:51 -07:00
f9f6e2e1b3 style(compiler): reformat partial evaluator source tree (#36461)
PR Close #36461
2020-04-07 15:21:51 -07:00
aecf9de738 fix(ngcc): correctly identify relative Windows-style import paths (#36372)
Previously, `isRelativePath()` assumed paths are *nix-style. This caused
Windows-style paths (such as `C:\foo\some-package\some-file.js`) to not
be recognized as "relative" imports.

This commit fixes this by using the OS-agnostic `isRooted()` helper and
also accounting for both styles of path delimiters: `/` and `\`

PR Close #36372
2020-04-07 15:21:27 -07:00
81195a238b fix(language-service): use the HtmlAst to get the span of HTML tag (#36371)
The HTML tag may include `-` (e.g. `app-root`), so use the `HtmlAst` to get the span.

PR Close #36371
2020-04-07 15:10:33 -07:00
96a3de6364 style(docs-infra): removed extra , from _resources.scss file (#35935)
there was a typo in _resourcess.scss file there was an extra comma added some spaces too that were needed for proper styling of the code

PR Close #35935
2020-04-07 15:08:57 -07:00
00027130ea fix(docs-infra): fix resources page tabs text which is not visible on small screens (#35935)
on small mobile screens the top tab bar contains text which was not visible on small screens changed text size, margin and padding so that the text could be contained in these screens (320px to 480px)

PR Close #35935
2020-04-07 15:08:57 -07:00
4a18428de6 docs(forms): Remove unnecessary repeating periods (#36474)
PR Close #36474
2020-04-07 11:35:43 -07:00
7f28845f58 test(language-service): remove ng-for-cases.ts (#36470)
This commit removes ng-for-cases.ts and moves all test cases to
inline expressions in TEST_TEMPLATE.

PR Close #36470
2020-04-07 11:34:38 -07:00
1dd0b6cc18 test(language-service): remove ng-if-cases.ts (#36470)
This commit removes ng-if-cases.ts and moves all test cases to inline
expressions in TEST_TEMPLATE.

PR Close #36470
2020-04-07 11:34:38 -07:00
e0415dbf16 fix(router): state data missing in routerLink (#36462)
fixes #33173 - router state data is missing on routerLink when used
with non-anchor elements.

PR Close #36462
2020-04-07 11:31:25 -07:00
95fc3d4c5c fix(core): ngOnDestroy on multi providers called with incorrect context (#35840)
Currently destroy hooks are stored in memory as `[1, hook, 5, hook]` where
the numbers represent the index at which to find the context and `hook` is
the function to be invoked. This breaks down for `multi` providers,
because the value at the index will be an array of providers, resulting in
the hook being invoked with an array of all the multi provider values,
rather than the provider that was destroyed. In ViewEngine `ngOnDestroy`
wasn't being called for `multi` providers at all.

These changes fix the issue by changing the structure of the destroy hooks to `[1, hook, 5, [0, hook, 3, hook]]` where the indexes inside the inner array point to the provider inside of the multi provider array. Note that this is slightly different from the original design which called for the structure to be `[1, hook, 5, [hook, hook]`, because in the process of implementing it, I realized that we wouldn't get passing the correct context if only some of the `multi` providers have `ngOnDestroy` and others don't.

I've run the newly-added `view_destroy_hooks` benchmark against these changes and compared it to master. The difference seems to be insignificant (between 1% and 2% slower).

Fixes #35231.

PR Close #35840
2020-04-07 10:31:41 -07:00
e145fa13b1 test(language-service): delete expression-cases.ts (#36468)
This commit deletes `expression-cases.ts` and moves the test cases to
inline expressions in TEST_TEMPLATE.

PR Close #36468
2020-04-07 10:20:27 -07:00
5fa7b8ba56 fix(ngcc): detect non-emitted, non-imported TypeScript helpers (#36418)
When TypeScript downlevels ES2015+ code to ES5, it uses some helper
functions to emulate some ES2015+ features, such as spread syntax. The
TypeScript compiler can be configured to emit these helpers into the
transpiled code (which is controlled by the `noEmitHelpers` option -
false by default). It can also be configured to import these helpers
from the `tslib` module (which is controlled by the `importHelpers`
option - false by default).

While most of the time the helpers will be either emitted or imported,
it is possible that one configures their app to neither emit nor import
them. In that case, the helpers could, for example, be made available on
the global object. This is what `@nativescript/angular`
v9.0.0-next-2019-11-12-155500-01 does. See, for example, [common.js][1].

Ngcc must be able to detect and statically evaluate these helpers.
Previously, it was only able to detect emitted or imported helpers.

This commit adds support for detecting these helpers if they are neither
emitted nor imported. It does this by checking identifiers for which no
declaration (either concrete or inline) can be found against a list of
known TypeScript helper function names.

[1]: https://unpkg.com/browse/@nativescript/angular@9.0.0-next-2019-11-12-155500-01/common.js

PR Close #36418
2020-04-07 10:19:22 -07:00
2c7d366c82 refactor(language-service): provide service for attribute binding type (#36301)
This commit refactors the process for determining the type of an Angular
attribute to be use a function that takes an attribute name and returns
the Angular attribute kind and name, rather than requiring the user to
query match the attribute name with the regex and query the matching
array.

This refactor prepares for a future change that will improve the
experience of completing attributes in `()`, `[]`, or `[()]` contexts.

PR Close #36301
2020-04-07 10:18:32 -07:00
2dd6f25647 ci: manually set available resources for bazel on windows CI (#36458)
PR Close #36458
2020-04-07 10:12:29 -07:00
d2623f13d9 style(zone.js): fix lint errors after clang update (#36487)
Recent ZoneJS-related commit (416c786774) update the `promise.ts` file, but it looks like original PR was not rebased after clang update. As a result, the `lint` CircleCI job started to fail in master after merging that PR (https://github.com/angular/angular/pull/36311). This commit updates the format of the `promise.ts` script according to the new clang rules.

PR Close #36487
2020-04-07 10:10:34 -07:00
7d0af179e3 style(forms): reformat of the forms package after clang update (#36466)
PR Close #36466
2020-04-07 09:47:09 -07:00
416c786774 fix(zone.js): should not try to patch fetch if it is not writable (#36311)
Close #36142

In Firefox extensions, the `window.fetch` is not configurable, that means

```
const desc = Object.getOwnPropertyDescriptor(window, 'fetch');
desc.writable === false;
```

So in this case, we should not try to patch `fetch`, otherwise, it will
throw error ('fetch is ReadOnly`)

PR Close #36311
2020-04-06 15:34:33 -07:00
93302b7fb8 build: update to latest version of husky (#36459)
PR Close #36459
2020-04-06 15:33:26 -07:00
eb8c6c7eff test(language-service): Move pipe tests to TEST_TEMPLATE (#36407)
This commit simplifies the completion tests for pipes by moving them to TEST_TEMPLATE.

PR Close #36407
2020-04-06 15:32:33 -07:00
8660806ddc build(docs-infra): renamed e2e property of example-config.json to tests (#36143)
Each docs example has an `example-config.json` configuration file. Among
other things, this file can be used to specify what commands to run in
order to test the example. (If not specified, the `run-example-e2e.js`
script will run a default `yarn e2e` command.)

Previously, the property specifying the test commands was called `e2e`.
This is because in the past only e2e tests were run for docs examples.
Since recently, some examples may specify commands for other types of
tests (such as unit tests). Therefore, calling the property that holds
the list of test commands `e2e` no longer makes sense and can be
misleading to people looking at the configuration files.

This commit renamed the property to the more generic `tests`. In the
future, the `run-example-e2e.js` script (and corresponding npm script)
should be renamed and refactored to also avoid giving the impression
that only e2e tests are run.

Discussed in:
https://github.com/angular/angular/pull/36143#discussion_r395148379

PR Close #36143
2020-04-06 15:31:07 -07:00
4c5e085c93 build(docs-infra): update docs examples package.json templates wrt core-js (#36143)
The `core-js` dependency is no longer included in `package.json` for
`cli`-type examples, but only for the `systemjs` ones. This commit
updates the `package.json` templates to reflect that (and also updates
the `npm-packages` guide accordingly).

PR Close #36143
2020-04-06 15:31:07 -07:00
d707124fd9 test(docs-infra): fix unit tests and run them for specific docs examples on CI (#36143)
Previously, only e2e tests were run for docs examples on CI. As a
result, unit tests (which are included in the zipped archives we provide
for users to download and play with the examples locally) were often
outdated and broken.

This commit configures specific docs examples that have meaningful unit
tests to run them on CI (via the `run-example-e2e.js` script). Where
necessary, the unit tests are fixed to ensure they pass and reflect the
changes in the corresponding component/service.
This commit also removes some auto-generated unit tests that are not
meaningful (e.g. make trivial assertions, such that a component instance
is truthy) and are often broken anyway (e.g. because the corresponding
component has been changed in ways that make the tests fail).

PR Close #36143
2020-04-06 15:31:07 -07:00
aece3669e5 build(docs-infra): switch docs examples to Ivy (#36143)
The docs examples are switched to Ivy. On CI, the tests are run with
both Ivy and ViewEngine.

Partially addresses:
[FW-1609](https://angular-team.atlassian.net/browse/FW-1609)

PR Close #36143
2020-04-06 15:31:07 -07:00
09c84169d5 docs: fix broken guide link (tutorial/index --> tutorial) (#36453)
Replace broken link (`tutorial/index`) with `tutorial`.

PR Close #36453
2020-04-06 13:56:00 -07:00
d43c30688a fix(core): avoid migration error when non-existent symbol is imported (#36367)
In rare cases a project with configured `rootDirs` that has imports to
non-existent identifiers could fail in the migration.

This happens because based on the application code, the migration could
end up trying to resolve the `ts.Symbol` of such non-existent
identifiers. This isn't a problem usually, but due to a upstream bug
in the TypeScript compiler, a runtime error is thrown.

This is because TypeScript is unable to compute a relative path from the
originating source file to the imported source file which _should_
provide the non-existent identifier. An issue for this has been reported
upstream: https://github.com/microsoft/TypeScript/issues/37731. The
issue only surfaces since our migrations don't provide an absolute base
path that is used for resolving the root directories.

To fix this, we ensure that we never use relative paths when parsing
tsconfig files. More details can be found in the TS issue.

Fixes #36346.

PR Close #36367
2020-04-06 13:21:54 -07:00
ee70a18a75 fix(ngcc): don't crash on cyclic source-map references (#36452)
The source-map flattening was throwing an error when there
is a cyclic dependency between source files and source-maps.
The error was either a custom one describing the cycle, or a
"Maximum call stack size exceeded" one.

Now this is handled more leniently, resulting in a partially loaded
source file (or source-map) and a warning logged.

Fixes #35727
Fixes #35757
Fixes https://github.com/angular/angular-cli/issues/17106
Fixes https://github.com/angular/angular-cli/issues/17115

PR Close #36452
2020-04-06 13:19:53 -07:00
76a8cd57ae fix(ngcc): add process title (#36448)
Add process.title, so it's clearly in the task manager when ngcc is running

See: https://github.com/angular/angular/issues/36414#issuecomment-609644282

PR Close #36448
2020-04-06 13:19:17 -07:00
4c7f89ff6d ci: exclude ngcc directory from fw-compiler pull-approve rule (#35933)
ngcc has its own pull-approve group

PR Close #35933
2020-04-06 13:16:37 -07:00
f9fb8338f5 fix(ngcc): support ignoring deep-imports via package config (#36423)
Recently we added support for ignoring specified deep-import
warnings by providing sets of regular expressions within the
`ngcc.config.js` file. But this was only working for the project
level configuration.

This commit fixes ngcc so that it will also read these regular
expressions from package level configuration too.

Fixes #35750

PR Close #36423
2020-04-06 11:32:09 -07:00
6b3aa60446 fix(ngcc): support simple browser property in entry-points (#36396)
The `browser` package.json property is now supported to the same
level as `main` - i.e. it is sniffed for UMD, ESM5 and CommonJS.

The `browser` property can also contain an object with file overrides
but this is not supported by ngcc.

Fixes #36062

PR Close #36396
2020-04-06 11:31:10 -07:00
2463548fa7 fix(ngcc): sniff main property for ESM5 format (#36396)
Previously, `main` was only checked for `umd` or `commonjs`
formats. Now if there are `import` or `export` statements in the
source file it will be deemed to be in `esm5` format.

Fixes #35788

PR Close #36396
2020-04-06 11:31:10 -07:00
1140bbc25c refactor(language-service): reformat using clang-format (#36426)
clang-format was recently updated and any PRs that touch files in the
language service will have to reformat all the files.

Instead of changing the formatting in those PRs, this PR formats all
files in language-service package once and for all.

PR Close #36426
2020-04-06 09:29:42 -07:00
1b4df6484e docs: add missing command in WebWorker guide (#36397)
The command was accidentally removed in #36383

PR Close #36397
2020-04-06 09:29:17 -07:00
f40d51733a fix(dev-infra): use commit message validation from @angular/dev-infra-private (#36172)
Prior to this change we manage a local version of commit message validation
in addition to the commit message validation tool contained in the ng-dev
tooling.  By adding the ability to validate a range of commit messages
together, the remaining piece of commit message validation that is in the
local version is replicated.

We use both commands provided by the `ng-dev commit-message` tooling:
- pre-commit-validate: Set to automatically run on an git hook to validate
    commits as they are created locally.
- validate-range: Run by CI for every PR, testing that all of the commits
    added by the PR are valid when considered together.  Ensuring that all
    fixups are matched to another commit in the change.

PR Close #36172
2020-04-06 09:28:52 -07:00
e893c5a330 fix(compiler-cli): pass real source spans where they are empty (#31805)
Some consumers of functions that take `ParseSourceSpan`s currently pass
empty and incorrect source spans. This fixes those cases.

PR Close #31805
2020-04-06 09:28:27 -07:00
8be8466a00 style(ngcc): reformat of ngcc after clang update (#36447)
PR Close #36447
2020-04-06 09:26:57 -07:00
c8bef1259c docs: adding Doguhan Uluca as GDE resource (#34789)
PR Close #34789
2020-04-03 14:03:57 -07:00
568e9df1d6 fix(router): allow UrlMatcher to return null (#36402)
The matcher is allowed to return null per
https://angular.io/api/router/UrlMatcher#usage-notes

And run `yarn gulp format` to pick up recent clang format changes.

Closes #29824

BREAKING CHANGE: UrlMatcher's type now reflects that it could always return
    null.

    If you implemented your own Router or Recognizer class, please update it to
    handle matcher returning null.

PR Close #36402
2020-04-03 11:16:23 -07:00
a555fdba32 build: update pullapprove config to better handle global approvals (#36384)
Beginning with this change, global approvals will now require the approver
to include `Reviewed-for: global-approvers`, and a docs global approval
requires `Reviewed-for: global-docs-approvers`.

Historically, providing a review by any member of the global reviewer
groups would automatically be considered a global review.  This change
enforces that global approval should be an intentional, explicit action.

The global-approvers and global-docs-approvers group will not be
requested as reviews by pullapprove.

PR Close #36384
2020-04-03 11:11:30 -07:00
36535e9abd fix(dev-infra): correct pullapprove global approval regex (#36384)
PR Close #36384
2020-04-03 11:11:30 -07:00
6402a9ae2a build: rebuild yarn lock from scratch (#36377)
Rebuild the yarn lock file from scratch to collapse instances where
one package is able to satisfy multiple dependencies.  Currently we
have some situations where we have multiple versions when one would
work.

Example:
```
"@babel/code-frame@^7.0.0":
  version "7.0.0"
  resolved "https://registry.yarnpkg.com/@babel/cod
  integrity sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij
  dependencies:
    "@babel/highlight" "^7.0.0"

"@babel/code-frame@^7.5.5":
  version "7.5.5"
  resolved "https://registry.yarnpkg.com/@babel/cod
  integrity sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQ
  dependencies:
    "@babel/highlight" "^7.0.0"

"@babel/code-frame@^7.8.3":
  version "7.8.3"
  resolved "https://registry.yarnpkg.com/@babel/cod
  integrity sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0j
  dependencies:
    "@babel/highlight" "^7.8.3"
```

becomes

```
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.5.5", "@babel/code-frame@^7.8.3":
  version "7.8.3"
  resolved "https://registry.yarnpkg.com/@babel/cod
  integrity sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0j
  dependencies:
    "@babel/highlight" "^7.8.3"
```

PR Close #36377
2020-04-03 11:09:17 -07:00
ca25c957bf fix(ngcc): correctly detect imported TypeScript helpers (#36284)
The `NgccReflectionHost`s have logic for detecting certain known
declarations (such as `Object.assign()` and TypeScript helpers), which
allows the `PartialEvaluator` to evaluate expressions it would not be
able to statically evaluate otherwise.

In #36089, `DelegatingReflectionHost` was introduced, which delegates to
a TypeScript `ReflectionHost` when reflecting on TypeScript files, which
for ngcc's case means `.d.ts` files of dependencies. As a result, ngcc
lost the ability to detect TypeScript helpers imported from `tslib`,
because `DelegatingReflectionHost` was not able to apply the known
declaration detection logic while reflecting on `tslib`'s `.d.ts` files.

This commit fixes this by ensuring `DelegatingReflectionHost` calls the
`NgccReflectionHost`'s `detectKnownDeclaration()` method as necessary,
even when using the TypeScript `ReflectionHost`.

NOTE:
The previous commit exposed a bug in ngcc that was hidden due to the
tests' being inconsistent with how the `ReflectionHost`s are used in the
actual program. The changes in this commit are verified by ensuring the
failing tests are now passing (hence no new tests are added).

PR Close #36284
2020-04-03 11:08:46 -07:00
93f07aee6c test(ngcc): use DelegatingReflectionHost for testing NgccReflectionHosts (#36284)
In #36089, `DelegatingReflectionHost` was introduced. Under the hood, it
delegates another `NgccReflectionHost` in order to reflect over the
program's source files, while using a different TypeScript
`ReflectionHost` to reflect over `.d.ts` files (which is how external
dependencies are represented in the program).

Previously, the `NgccReflectionHost`s were used directly in tests. This
does not exercise them in the way they are exercised in the actual
program, because (when used directly) they will also reflect on `.d.ts`
files too (instead of delegating to the TypeScript `ReflectionHost`).
This could hide bugs that would happen on the actual program.

This commit fixes this by using the `DelegatingReflectionHost` in the
various `NgccReflectionHost` tests.

NOTE:
This change will cause some of the existing tests to start failing.
These failures demonstrate pre-existing bugs in ngcc, that were hidden
due to the tests' being inconsistent with how the `ReflectionHost`s are
used in the actual program. They will be fixed in the next commit.

PR Close #36284
2020-04-03 11:08:46 -07:00
0af6e9fcbb refactor(ngcc): move logic for identifying known declarations to method (#36284)
The `NgccReflectionHost`s have logic for detecting certain known
declarations (such as `Object.assign()` and TypeScript helpers), which
allows the `PartialEvaluator` to evaluate expressions it would not be
able to statically evaluate otherwise.

This commit moves the logic for identifying these known declarations to
dedicated methods. This is in preparation of allowing ngcc's
`DelegatingReflectionHost` (introduced in #36089) to also apply the
known declaration detection logic when reflecting on TypeScript sources.

PR Close #36284
2020-04-03 11:08:46 -07:00
008e12edda fix(benchpress): fix typings in lview_debug.ts (#36236)
* Installing & using benchpress was throwing typescript errors because typings were not defined for the getters in lview_debug.ts

PR Close #36236
2020-04-03 11:06:07 -07:00
1927d0c7db docs: place download section in angular forms to the top (#36075)
This commit partially addresses #35459

PR Close #36075
2020-04-03 11:05:31 -07:00
9a0a90feb3 docs: place download section in angular forms validation to the top of the page (#36074)
This commit partially addresses #35459

PR Close #36074
2020-04-03 11:03:36 -07:00
76d86d5a07 docs: place download section in angular pipes to the top (#36073)
This commit partially addresses #35459

PR Close #36073
2020-04-03 11:01:08 -07:00
de7a9a3b93 docs: place download section to the top of the page (#36067)
Previously, the download link to the example for the angular element
guide was in the middle of the page. To make it easier for the user to
find the download link, it has been placed to the top of the page.

This commit partially addresses #35459

PR Close #36067
2020-04-03 11:00:26 -07:00
04f61c0c3e build: enable strictTemplates in AIO (#36391)
PR Close #36391
2020-04-02 10:53:12 -07:00
c810ac7153 build: sort module resolution warnings in ts-circular-deps tool (#36361)
For better overview of modules that cannot be resolved in the
`ts-circular-deps` tool, the warnings are now sorted.

Additionally, an empty line between fixed and new circular dependencies
is now printed. That should slightly help with distinguishing.

PR Close #36361
2020-04-02 10:52:47 -07:00
c24ad560fa feat(core): undecorated-classes migration should handle derived abstract classes (#35339)
In version 10, undecorated base classes that use Angular features need
to be decorated explicitly with `@Directive()`. Additionally, derived
classes of abstract directives need to be decorated.

The migration already handles this for undecorated classes that are
not explicitly decorated, but since in V9, abstract directives can be
used, we also need to handle this for explicitly decorated abstract
directives. e.g.

```
@Directive()
export class Base {...}

// needs to be decorated by migration when updating from v9 to v10
export class Wrapped extends Base {}

@Component(...)
export class Cmp extends Wrapped {}
```

PR Close #35339
2020-04-02 10:51:49 -07:00
3d2db5c5f0 test: add integration test for undecorated-classes-with-decorated-fields migration (#35339)
We don't have an integration test for the `undecorated-classes-with-decorated-fields
migration. For consistency and to cover for the latest changes, we add
it to the `ng update` integration test.

PR Close #35339
2020-04-02 10:51:48 -07:00
32eafef6a7 fix(core): undecorated-classes-with-decorated-fields migration does not decorate derived classes (#35339)
The `undecorated-classes-with-decorated-fields` migration has been
introduced with 904a2018e0, but misses
logic for decorating derived classes of undecorated classes which use
Angular features. Example scenario:

```ts
export abstract class MyBaseClass {
  @Input() someInput = true;
}

export abstract class BaseClassTwo extends MyBaseClass {}

@Component(...)
export class MyButton extends BaseClassTwo {}
```

Both abstract classes would need to be migrated. Previously, the migration
only added `@Directive()` to `MyBaseClass`, but with this change, it
also decorates `BaseClassTwo`.

This is necessary because the Angular Compiler requires `BaseClassTwo` to
have a directive definition when it flattens the directive metadata for
`MyButton` in order to perform type checking. Technically, not decorating
`BaseClassTwo` does not break at runtime.

We basically want to enforce consistent use of `@Directive` to simplify the
mental model. [See the migration guide](https://angular.io/guide/migration-undecorated-classes#migrating-classes-that-use-field-decorators).

Fixes #34376.

PR Close #35339
2020-04-02 10:51:48 -07:00
2366480250 refactor(core): move schematic import manager to shared utils (#35339)
The import manager has been created for both the `missing-injectable`
and `undecorated-classes-with-di` migration. Both initial PRs brought
in the manager class, so the manager is duplicated in the schematics.

In order to reduce this duplication, and to expose the manager to other
schematics/migrations, we move it into the shared schematic utils.

PR Close #35339
2020-04-02 10:51:48 -07:00
8e55a11283 refactor(core): move schematic base classes logic into shared utils (#35339)
Moves the `findBaseClassDeclarations` method into the shared
schematic utilities. This method will be useful for future
migrations, and for planned changes to the
`undecorated-classes-with-decorated-fields` migration.

PR Close #35339
2020-04-02 10:51:48 -07:00
e9de28111d build: enable service-worker tests on saucelabs (#36129)
Enables the `service-worker` tests on Saucelabs and fixes some issues that were preventing them from running on IE. The issues were:
1. We were serving es2017 code during tests. I've set it to es5.
2. The check which was verifying whether the environment is supported ended up hitting a `require` call in the browser which caused it to fail on browsers that don't support the `URL` API.

PR Close #36129
2020-04-01 15:37:47 -07:00
75afd80ae8 refactor(compiler): add @nocollapse annotation using a synthetic comment (#35932)
In Ivy, Angular decorators are compiled into static fields that are
inserted into a class declaration in a TypeScript transform. When
targeting Closure compiler such fields need to be annotated with
`@nocollapse` to prevent them from being lifted from a static field into
a variable, as that would prevent the Ivy runtime from being able to
find the compiled definitions.

Previously, there was a bug in TypeScript where synthetic comments added
in a transform would not be emitted at all, so as a workaround a global
regex-replace was done in the emit's `writeFile` callback that would add
the `@nocollapse` annotation to all static Ivy definition fields. This
approach is no longer possible when ngtsc is running as TypeScript
plugin, as a plugin cannot control emit behavior.

The workaround is no longer necessary, as synthetic comments are now
properly emitted, likely as of
https://github.com/microsoft/TypeScript/pull/22141 which has been
released with TypeScript 2.8.

This change is required for running ngtsc as TypeScript plugin in
Bazel's `ts_library` rule, to move away from the custom `ngc_wrapped`
approach.

Resolves FW-1952

PR Close #35932
2020-04-01 15:37:06 -07:00
a5eb0e56b6 docs(zone.js): fix typos and align formatting (#35907)
PR Close #35907
2020-04-01 15:35:37 -07:00
4d458db1b5 docs: update and edit web-worker page (#36383)
PR Close #36383
2020-04-01 15:25:47 -07:00
24a92472bf ci: use dev-infra cli from local sources (#36326)
Use dev-infra cli from local sources rather than loading
from @angular/dev-infra-private builds.

PR Close #36326
2020-04-01 15:24:29 -07:00
43006bcc45 feat(dev-infra): standard CLI commands using yargs (#36326)
Creates a standard model for CLI commands provided by ng-dev.
Allows for us to have any of the tools/scripts extend to be
included in the ng-dev command, or be standalone using the same
yargs parser.

PR Close #36326
2020-04-01 15:24:28 -07:00
326240eb91 fix(ngcc): allow ngcc configuration to match pre-release versions of packages (#36370)
Ngcc supports providing a project-level configuration to affect how
certain dependencies are processed and also has a built-in fallback
configuration for some unmaintained packages. Each entry in these
configurations could be scoped to specific versions of a package by
providing a version range. If no version range is provided for a
package, it defaults to `*` (with the intention of matching any
version).

Previously, the installed version of a package was tested against the
version range using the [semver][1] package's `satisfies()` function
with the default options. By default, `satisfies()` does not match
pre-releases (see [here][2] for more details on reasoning). While this
makes sense when determining what version of a dependency to install
(trying to avoid unexpected breaking changes), it is not desired in the
case of ngcc.

This commit fixes it by explicitly specifying that pre-release versions
should be matched normally.

[1]: https://www.npmjs.com/package/semver
[2]: https://github.com/npm/node-semver#prerelease-tags

PR Close #36370
2020-04-01 13:32:32 -07:00
b59bc0e38c fix(platform-server): update xhr2 dependency (#36366)
Previous versions of xhr2 used the depreciated "new Buffer()".

Closes #36358

PR Close #36366
2020-04-01 13:31:38 -07:00
5516802142 fix(compiler): avoid undefined expressions in holey array (#36343)
From G3 bug ID: 116443331

The View Engine compiler crashes when it tries to compile a test in JIT mode
that includes the d3-scale-chromatic library [1]. The d3 package initializes
some arrays using the following pattern:

```js
export var scheme = new Array(3).concat(
  "d8b365f5f5f55ab4ac",
  // ... more entries
).map(colors);
```

The stack trace from the crash is as follows:

```
TypeError: Cannot read property 'visitExpression' of undefined
    at ../../../third_party/javascript/angular2/rc/packages/compiler/src/output/abstract_emitter.ts:505:39
    at JitEmitterVisitor.AbstractEmitterVisitor.visitAllObjects third_party/javascript/angular2/rc/packages/compiler/src/output/abstract_emitter.ts?l=526
    at JitEmitterVisitor.AbstractEmitterVisitor.visitAllExpressions third_party/javascript/angular2/rc/packages/compiler/src/output/abstract_emitter.ts?l=505
    at JitEmitterVisitor.AbstractEmitterVisitor.visitLiteralArrayExpr third_party/javascript/angular2/rc/packages/compiler/src/output/abstract_emitter.ts?l=484
    at LiteralArrayExpr.visitExpression third_party/javascript/angular2/rc/packages/compiler/src/output/output_ast.ts?l=791
    at ../../../third_party/javascript/angular2/rc/packages/compiler/src/output/abstract_emitter.ts:492:19
    at JitEmitterVisitor.AbstractEmitterVisitor.visitAllObjects third_party/javascript/angular2/rc/packages/compiler/src/output/abstract_emitter.ts?l=526
    at JitEmitterVisitor.AbstractEmitterVisitor.visitLiteralMapExpr third_party/javascript/angular2/rc/packages/compiler/src/output/abstract_emitter.ts?l=490
    at LiteralMapExpr.visitExpression third_party/javascript/angular2/rc/packages/compiler/src/output/output_ast.ts?l=819
    at ../../../third_party/javascript/angular2/rc/packages/compiler/src/output/abstract_emitter.ts:505:39
    at JitEmitterVisitor.AbstractEmitterVisitor.visitAllObjects third_party/javascript/angular2/rc/packages/compiler/src/output/abstract_emitter.ts?l=526
    at JitEmitterVisitor.AbstractEmitterVisitor.visitAllExpressions third_party/javascript/angular2/rc/packages/compiler/src/output/abstract_emitter.ts?l=505
    at JitEmitterVisitor.AbstractEmitterVisitor.visitInvokeFunctionExpr third_party/javascript/angular2/rc/packages/compiler/src/output/abstract_emitter.ts?l=318
    at JitEmitterVisitor.AbstractJsEmitterVisitor.visitInvokeFunctionExpr third_party/javascript/angular2/rc/packages/compiler/src/output/abstract_js_emitter.ts?l=112
    at InvokeFunctionExpr.visitExpression third_party/javascript/angular2/rc/packages/compiler/src/output/output_ast.ts?l=440
```

This is because the corresponding output AST for the array is of the form

```ts
[
  undefined,
  undefined,
  undefined,
  o.LiteralExpr,
  // ...
]
```

The output AST is clearly malformed and breaks the type representation of
`LiteralArrayExpr` in which every entry is expected to be of type `Expression`.

This commit fixes the bug by using a plain `for` loop to iterate over the
entire length of the holey array and convert undefined elements to
`LiteralExpr`.

[1]: https://github.com/d3/d3-scale-chromatic/blob/master/src/diverging/BrBG.js

PR Close #36343
2020-04-01 13:31:14 -07:00
cc4b813e75 fix(ngcc): handle bad path mappings when finding entry-points (#36331)
Previously, a bad baseUrl or path mapping passed to an `EntryPointFinder`
could cause the original `sourceDirectory` to be superceded by a higher
directory. This could result in none of the sourceDirectory entry-points being
processed.

Now missing basePaths computed from path-mappings are discarded with
a warning. Further, if the `baseUrl` is the root directory then a warning is
given as this is most likely an error in the tsconfig.json.

Resolves #36313
Resolves #36283

PR Close #36331
2020-04-01 13:30:46 -07:00
64631063ae docs: fix typo in Tests guide (#36330)
Fixing typo in testing.md

PR Close #36330
2020-04-01 13:30:16 -07:00
719224bffd feat(dev-infra): add support for new global approvers in pullapprove (#36324)
Pullapprove as added a few new features to allow for us to better
execute our expectation for global approvals. We need to allow for
an expectation that our global approver groups are not in the list
of approved groups. Additionally, since approval groups apply to
all files in the repo, the global approval groups also do not have
conditions defined for them, which means pullapprove verification
need to allow for no conditions need to be defined.

PR Close #36324
2020-04-01 13:25:48 -07:00
fe2b6923ba fix(language-service): infer type of elements of array-like objects (#36312)
Currently the language service only provides support for determining the
type of array-like members when the array-like object is an `Array`.
However, there are other kinds of array-like objects, including
`ReadonlyArray`s and `readonly`-property arrays. This commit adds
support for retrieving the element type of arbitrary array-like objects.

Closes #36191

PR Close #36312
2020-04-01 13:24:53 -07:00
38ad1d97ab fix(ngcc): handle entry-points within container folders (#36305)
The previous optimizations in #35756 to the
`DirectoryWalkerEntryPointFinder` were over zealous
with regard to packages that have entry-points stored
in "container" directories in the package, where the
container directory was not an entry-point itself.

Now we will also walk such "container" folders as long
as they do not contain `.js` files, which we regard as an
indicator that the directory will not contain entry-points.

Fixes #36216

PR Close #36305
2020-04-01 13:20:52 -07:00
372b9101e2 refactor(ngcc): simplify DirectoryWalkerEntryPointFinder (#36305)
This commit simplifies the `DirectoryWalkerEntryPointFinder` inter-method
calling to make it easier to follow, and also to support controlling
walking of a directory based on its children.

PR Close #36305
2020-04-01 13:20:52 -07:00
7e62aa0c6e refactor(ngcc): rename INVALID_ENTRY_POINT to INCOMPATIBLE_ENTRY_POINT (#36305)
This name better reflects its meaning.

PR Close #36305
2020-04-01 13:20:52 -07:00
36e927a8c6 fix(zone.js): UNPATCHED_EVENTS and PASSIVE_EVENTS should be string[] not boolean (#36258)
__zone_symbol__UNPATCHED_EVENTS and __zone_symbol__PASSIVE_EVENTS should be string[] type not boolean.
For example:
```
const config = window as ZoneGlobalConfigurations;
config.__zone_symbol__UNPATCHED_EVENTS = ['scroll'];
config.__zone_symbol__PASSIVE_EVENTS = ['scroll'];
```

PR Close #36258
2020-04-01 13:20:00 -07:00
9d8bb634f9 fix(benchpress): update dependencies (#36205)
* updated, added, and removed dependencies in package.json
* added dependencies to BUILD.bazel

PR Close #36205
2020-04-01 13:19:33 -07:00
c5c57f6737 build: update to clang 1.4.0 and only run clang format on changed files (#36203)
Update to clang@1.4.0 to gain support for optional changing and nullish
coalescing.  Because this would trigger a change on >1800 files in the
repository, also changes our format enforcement to only be run against
changed files.  This will allow us to incramentally roll out the value
add of the upgraded clang format.

PR Close #36203
2020-04-01 13:18:09 -07:00
51a89c32c4 docs: update and edit web-worker page (#36026)
PR Close #36026
2020-04-01 13:17:26 -07:00
e1ac2efe4a docs: make page titles and toc task-oriented (#36024)
PR Close #36024
2020-04-01 13:16:48 -07:00
702e17cfe2 docs: change page title and minor edit (#36021)
PR Close #36021
2020-04-01 13:16:18 -07:00
4d1d0fa03b docs: update ng-conf announcement to remove livestream link (#36382)
Since the livestream for ng-conf is not public this year,
(and is only available to ng-conf attendees), we are
removing the link from the angular.io homepage.

Instead, we are now pointing to the ng-conf homepage for
more information.

PR Close #36382
2020-04-01 13:14:42 -07:00
ec8bae1b27 docs: update end date of survey to middle of April (#36339)
PR Close #36339
2020-04-01 11:10:58 -07:00
08348fc2e8 build(docs-infra): rename duplicate test name (#36348)
When running `docs-test` for the docs generation, a warning is printed for a
duplicate test. This commit fixes this issue.

PR Close #36348
2020-03-31 11:06:16 -07:00
63fbc71439 build: don't use deprecated $(location) pre-declared variable (#36308)
$(location) is not recommended in the bazel docs as depending on context it will either return the value of $(execpath) or $(rootpath). rules_nodejs now supports $(rootpath) and $(execpath) in templated_args of nodejs_binary.

PR Close #36308
2020-03-31 11:02:56 -07:00
b44f7b5e16 fix(zone.js): fix 2 bluebird test cases for each/mapSeries (#36295)
`Bluebird.each` and `Bluebird.mapSeries` will accept a callback with `value` parameter,
the `value` should be the item in the array, not array itself.

For example:
```
const arr = [1, 2];
Bluebird.each(arr, function(value, idx) {
  console.log(`value: ${value}, idx: ${idx}`);
})
```

the output will be
```
value: 1, idx: 0
value: 2, idx: 1
```

This PR fix the test cases for `each` and `mapSeries` APIs.

PR Close #36295
2020-03-31 10:59:56 -07:00
c5df9ce474 build(zone.js): update zone.js version to 0.10.3 (#36214)
PR Close #36214
2020-03-31 10:59:17 -07:00
2510e7dad6 docs: fix typo in Schematics guide (#36328)
Fixing typo in schematics-for-libraries.md
PR Close #36328
2020-03-30 15:32:05 -07:00
80c68583d1 refactor(core): use more narrow QueryList import to avoid circular deps issue (#36286)
Prior to this commit, the `packages/core/src/render3/interfaces/query.ts` file used to import `QueryList` using `../../linker`, which contains a lot of re-exports and as a result, this one import caused a lot of circular deps cycles reported by the tool that checks such deps. In other places in the code the `QueryList` is imported using more narrow import (`linker/query_list`), so this commit uses the same pattern. This change allowed to reduce the number of known cycles from 343 to 207, the golden file was updated accordingly.

PR Close #36286
2020-03-30 15:31:39 -07:00
5b6ced5599 build: update to rules_nodejs 1.5.0 (#36307)
### New stuff

* The `ts_project` rule is a simple wrapper around the TypeScript compiler, `tsc`. This is an alternative to `ts_library` but not a replacement. Read more about the trade-offs at https://bazelbuild.github.io/rules_nodejs/TypeScript#alternatives or read the [API docs](https://bazelbuild.github.io/rules_nodejs/TypeScript#ts_project)
* `pkg_npm` can now be used as a dependency within your repo as well as for publishing to npm. It provides a `LinkablePackageInfo` which is our internal API to pass package name/path to downstream compilations, essentially providing the "Lerna" feature.
* There is experimental support for Bazel's "worker mode" in `rollup_bundle`, which essentially puts Rollup in watch mode. Add the `supports_workers = True` attribute to opt-in.
* Better support for [pre-defined label variables](https://docs.bazel.build/versions/master/be/make-variables.html#predefined_label_variables) like `$(rootpath)` and `$(execpath)` - we no longer recommend using `$(location)` at all.

See release notes https://github.com/bazelbuild/rules_nodejs/releases/tag/1.5.0 for more info.

PR Close #36307
2020-03-30 11:25:16 -07:00
a383116b43 docs: fix typo in Dependency Injection guide (#36304)
Fixing typo in dependency-injection-navtree.md

PR Close #36304
2020-03-30 11:14:55 -07:00
c6dd900f60 fix(ngcc): do not write entry-point manifest outside node_modules (#36299)
Fixes #36296

PR Close #36299
2020-03-30 11:03:26 -07:00
5ac308060d refactor(ngcc): rename workerCount to maxWorkerCount (#36298)
Now that we spawn workers lazily as needed, this private property is
really the upper limit on how many workers we might spawn.

PR Close #36298
2020-03-30 11:02:52 -07:00
bc089abd32 docs: add strictTemplates in place of fullTemplateTypeCheck (#35628)
Co-Authored-By: Igor Minar <iminar@google.com>

Updating the recommended defaults for template typechecking strictness to the most strict in order
to catch most of the errors at compile time.

See https://angular.io/guide/template-typecheck for more info.

PR Close #35628
2020-03-30 10:57:15 -07:00
c94a33c525 refactor(dev-infra): fix lint warnings for Pullapprove-related scripts (#36287)
The `dev-infra` scripts were added to the list of sources that should be verified with clang (b07b6edc2a), but the Pullapprove-related scripts that were merged before (83e4a76afa) doesn't pass these checks. This commit updates a couple scripts to have a proper formatting.

PR Close #36287
2020-03-28 23:21:12 -07:00
5cee709266 fix(ngcc): do not spawn more processes than intended in parallel mode (#36280)
When running in parallel mode, ngcc spawns multiple worker processed to
process the various entry-points. The number of max allowed processes is
determined by the number of CPU cores available to the OS. There is also
currently an [upper limit of 8][1]. The number of active workers is in
turn inferred by the number of [task assignments][2].

In the past, counting the entries of `ClusterMaster#taskAssignments` was
enough, because worker processes were spawned eagerly at the beginning
and corresponding entries were created in `taskAssignments`.
Since #35719 however, worker processes are spawned lazily on an as
needed basis. Because there is some delay between
[spawning a process][3] and [inserting it][4] into `taskAssignments`,
there is a short period of time when `taskAssignment.size` does not
actually represent the number of spawned processes. This can result in
spawning more than `ClusterMaster#workerCount` processes.

An example of this can be seen in #36278, where the debug logs indicate
9 worker processes had been spawned (`All 9 workers are currently busy`)
despite the hard limit of 8.

This commit fixes this by using `cluster.workers` to compute the number
of spawned worker processes. `cluster.workers` is updated synchronously
with `cluster.fork()` and thus reflects the number of spawned workers
accurately at all times.

[1]: https://github.com/angular/angular/blob/b8e9a30d3b6/packages/compiler-cli/ngcc/src/main.ts#L429
[2]: https://github.com/angular/angular/blob/b8e9a30d3b6/packages/compiler-cli/ngcc/src/execution/cluster/master.ts#L108
[3]: https://github.com/angular/angular/blob/b8e9a30d3b6/packages/compiler-cli/ngcc/src/execution/cluster/master.ts#L110
[4]: https://github.com/angular/angular/blob/b8e9a30d3b6/packages/compiler-cli/ngcc/src/execution/cluster/master.ts#L199

PR Close #36280
2020-03-27 14:12:28 -07:00
2d0847c307 docs: update release nots with blog post (#36268)
PR Close #36268
2020-03-27 11:18:23 -07:00
995cd15a69 fix(ngcc): correctly identify the package path of secondary entry-points (#36249)
Previously we only searched for package paths below the set of `basePaths`
that were computed from the `basePath` provided to ngcc and the set of
`pathMappings`.

In some scenarios, such as hoisted packages, the entry-point is not within
any of the `basePaths` identified above. For example:

```
project
  packages
    app
      node_modules
        app-lib (depends on lib1)
  node_modules
    lib1 (depends on lib2)
      node_modules
        lib2 (depends on lib3/entry-point)
    lib3
      entry-point
```

When CLI is compiling `app-lib` ngcc will be given
`project/packages/app/node_modules` as the `basePath.

If ngcc is asked to target `lib2`, the `targetPath` will be
`project/node_modules/lib1/node_modules/lib2`.

Since `lib2` depends upon `lib3/entry-point`, ngcc will need to compute
the package path for `project/node_modules/lib3/entry-point`.

Since `project/node_modules/lib3/entry-point` is not contained in the `basePath`
`project/packages/app/node_modules`, ngcc failed to compute the `packagePath`
correctly, instead assuming that it was the same as the entry-point path.

Now we also consider the nearest `node_modules` folder to the entry-point
path as an additional `basePath`. If one is found then we use the first
directory directly below that `node_modules` directory as the package path.

In the case of our example this extra `basePath` would be `project/node_modules`
which allows us to compute the `packagePath` of `project/node_modules/lib3`.

Fixes #35747

PR Close #36249
2020-03-27 11:17:45 -07:00
16497438d6 fix(core): run APP_INITIALIZERs before accessing LOCALE_ID token in Ivy TestBed (#36237)
Prior to this commit, Ivy TestBed was accessing locale ID before `APP_INITIALIZER` functions were called. This execution order is not consistent with the app bootstrap logic in `application_ref.ts`. This commit updates Ivy TestBed execution order to call initializers first (since they might affect `LOCALE_ID` token value) and accessing and setting locale ID after that.

Fixes #36230.

PR Close #36237
2020-03-27 11:16:57 -07:00
b54db86f43 build: fix bad pullapprove rule (#36232)
The dev-infra pull approve group contained a condition with
the glob having a string concatenation rather than a comma
separated list. This corrects this error, and in another PR
we will correct the verification scripts failure to catch
this.

PR Close #36232
2020-03-27 11:15:38 -07:00
44acf6734b build: allow custom module resolution for ts-circular-deps tests (#36226)
Currently the `ts-circular-deps` tool uses a hard-coded module resolver
that only works in the `angular/angular` repository.

If the tool is consumed in other repositories through the shared
dev-infra package, the module resolution won't work, and a few
resolvable imports (usually cross-entry-points) are accidentally
skipped. For each test, the resolution might differ, so tests can
now configure their module resolution in a configuration file.

Note that we intentionally don't rely on tsconfig's for module
resolution as parsing their mappings rather complicates the
circular dependency tool. Additionally, not every test has a
corresponding tsconfig file.

Also, hard-coding mappings to `@angular/*` while accepting a
path to the packages folder would work, but it would mean
that the circular deps tool is no longer self-contained. Rather,
and also for better flexibility, a custom resolver should be
specified.

PR Close #36226
2020-03-27 11:14:49 -07:00
b07b6edc2a build: add dev-infra to clang format sources to format (#36204)
PR Close #36204
2020-03-27 11:13:17 -07:00
83e4a76afa feat(dev-infra): handle excluding files via globs in pullapprove (#36162)
Updates the pullapprove verification script to handle
cases of excluding globs from groups.

PR Close #36162
2020-03-27 11:12:48 -07:00
bfa7b1a494 docs: correct a misleading sentence (#36155) (#36158)
Fixes #36155

PR Close #36158
2020-03-27 10:49:31 -07:00
8e0dec538e docs: clarify observables doc with new titles and tooltips (#36023)
PR Close #36023
2020-03-27 10:49:03 -07:00
1c385a1c22 build(docs-infra): do not include CI-specific config in docs examples ZIP archives (#36018)
In #35381, a new Protractor config file was introduced in docs examples,
`protractor-puppeteer.conf.js`, that was only supposed to be used on CI
and not be shipped with the ZIP archives provided for users to download
and experiment with the docs examples locally.

The logic to ignore the `protractor-puppeteer.conf.js` file was
incorrect, resulting in the file being retained in some examples (e.g.
[universal][1]). The problem was not immediately obvious, because most
examples explicitly specify all `**/*.js` files as ignored, but for
other examples the file was retained in the ZIP archive.

This commit fixes the logic to ensure the file is excluded from all docs
examples ZIP archives.

[1]: https://v9.angular.io/generated/zips/universal/universal.zip

PR Close #36018
2020-03-27 10:48:30 -07:00
f9bc84cd99 build(docs-infra): remove obsolete properties from zipper.json files (#36018)
The `removeSystemJsConfig` and `type` properties (present in some
`zipper.json` files) are now obsolete and are not taken into account by
the example zipper:
- `removeSystemJsConfig` is no longer relevant since most examples have
  been migrated to use the CLI.
- `type` is no longer relevant, because the project type is determined
  based on the `projectType` property in `example-config.json` files.

This commit removes these properties from `zipper.json` files and
updates the `example-zipper` docs to not mention them.

PR Close #36018
2020-03-27 10:48:30 -07:00
2d7c95fb70 fix(service-worker): prevent SW registration strategies from affecting app stabilization (#35870)
Previously, some of the built-in ServiceWorker registration strategies,
namely `registerWithDelay:<timeout>` and `registerWhenStable:<timeout>`,
would register potentially long-running timeout, thus preventing the app
from stabilizing before the timeouts expired. This was especially
problematic for the `registerWhenStable:<timeout>` strategy, which waits
for the app to stabilize, because the strategy itself would prevent the
app from stabilizing and thus the ServiceWorker would always be
registered after the timeout.

This commit fixes this by subscribing to the registration strategy
observable outside the Angular zone, thus not affecting the app's
stabilization.

PR Close #35870
2020-03-27 10:47:44 -07:00
29e8a64cf0 fix(service-worker): by default register the SW after 30s even the app never stabilizes (#35870)
Previously, when using the default ServiceWorker registration strategy
Angular would wait indefinitely for the [app to stabilize][1], before
registering the ServiceWorker script. This could lead to a situation
where the ServiceWorker would never be registered when there was a
long-running task (such as an interval or recurring timeout).

Such tasks can often be started by a 3rd-party dependency (beyond the
developer's control or even without them realizing). In addition, this
situation is particularly hard to detect, because the ServiceWorker is
typically not used during development and on production builds a
previous ServiceWorker instance might be already active.

This commit fixes this by changing the default registration strategy
from `registerWhenStable` to `registerWhenStable:30000`, which will
ensure that the ServiceWorker will be registered after 30s at the
latest, even if the app has not stabilized by then.

Fixes #34464

PR Close #35870
2020-03-27 10:47:44 -07:00
00efacf561 feat(service-worker): support timeout in registerWhenStable SW registration strategy (#35870)
Previously, when using the `registerWhenStable` ServiceWorker
registration strategy (which is also the default) Angular would wait
indefinitely for the [app to stabilize][1], before registering the
ServiceWorker script. This could lead to a situation where the
ServiceWorker would never be registered when there was a long-running
task (such as an interval or recurring timeout).

Such tasks can often be started by a 3rd-party dependency (beyond the
developer's control or even without them realizing). In addition, this
situation is particularly hard to detect, because the ServiceWorker is
typically not used during development and on production builds a
previous ServiceWorker instance might be already active.

This commit enhances the `registerWhenStable` registration strategy by
adding support for an optional `<timeout>` argument, which guarantees
that the ServiceWorker will be registered when the timeout expires, even
if the app has not stabilized yet.

For example, with `registerWhenStable:5000` the ServiceWorker will be
registered as soon as the app stabilizes or after 5 seconds if the app
has not stabilized by then.

Related to #34464.

[1]: https://angular.io/api/core/ApplicationRef#is-stable-examples

PR Close #35870
2020-03-27 10:47:44 -07:00
d96995b4e3 docs: update glossary defs for components, templates, and views (#35559)
PR Close #35559
2020-03-27 10:46:49 -07:00
fc3e5cb6d3 docs: coalesce release notes for the v9.1.0 release (#36247)
Merge all of the release notes across all v9.1.0 releases into a single one.

PR Close #36247
2020-03-25 14:11:07 -07:00
d37dad82f1 build: ensure that refs and shas for PRs only need to be requested once (#36207)
This is done by requesting the refs and shas for the PR when the
env.sh script is run.  Additionally, the env.sh script is now setup
to write all of the environment variables created to a cache file
and subsequent loads of the environment load the values from there.

The get-refs-and-shas-for-target.js script now also first attempts
to load the refs and shas from an environment variable before
falling back to requesting from github via the API.

PR Close #36207
2020-03-25 11:49:42 -07:00
22710fc353 docs: release notes for the v9.1.0 release 2020-03-25 09:44:22 -07:00
4f66250618 docs: release notes for the v9.1.0-rc.2 release 2020-03-24 16:18:10 -07:00
d783519835 fix(common): let KeyValuePipe accept type unions with null (#36093)
`KeyValuePipe` currently accepts `null` values as well as `Map`s and a
few others. However, due to the way in which TS overloads work, a type
of `T|null` will not be accepted by `KeyValuePipe`'s signatures, even
though both `T` and `null` individually would be.

To make this work, each signature that accepts some type `T` has been
duplicated with a second one below it that accepts a `T|null` and
includes `null` in its return type.

Fixes #35743

PR Close #36093
2020-03-24 14:41:41 -07:00
b8e9a30d3b fix(ngcc): use preserve whitespaces from tsconfig if provided (#36189)
Previously ngcc never preserved whitespaces but this is at odds
with how the ViewEngine compiler works. In ViewEngine, library
templates are recompiled with the current application's tsconfig
settings, which meant that whitespace preservation could be set
in the application tsconfig file.

This commit allows ngcc to use the `preserveWhitespaces` setting
from tsconfig when compiling library templates. One should be aware
that this disallows different projects with different tsconfig settings
to share the same node_modules folder, with regard to whitespace
preservation. But this is already the case in the current ngcc since
this configuration is hard coded right now.

Fixes #35871

PR Close #36189
2020-03-24 14:25:06 -07:00
32ce8b1326 feat(compiler): add dependency info and ng-content selectors to metadata (#35695)
This commit augments the `FactoryDef` declaration of Angular decorated
classes to contain information about the parameter decorators used in
the constructor. If no constructor is present, or none of the parameters
have any Angular decorators, then this will be represented using the
`null` type. Otherwise, a tuple type is used where the entry at index `i`
corresponds with parameter `i`. Each tuple entry can be one of two types:

1. If the associated parameter does not have any Angular decorators,
   the tuple entry will be the `null` type.
2. Otherwise, a type literal is used that may declare at least one of
   the following properties:
   - "attribute": if `@Attribute` is present. The injected attribute's
   name is used as string literal type, or the `unknown` type if the
   attribute name is not a string literal.
   - "self": if `@Self` is present, always of type `true`.
   - "skipSelf": if `@SkipSelf` is present, always of type `true`.
   - "host": if `@Host` is present, always of type `true`.
   - "optional": if `@Optional` is present, always of type `true`.

   A property is only present if the corresponding decorator is used.

   Note that the `@Inject` decorator is currently not included, as it's
   non-trivial to properly convert the token's value expression to a
   type that is valid in a declaration file.

Additionally, the `ComponentDefWithMeta` declaration that is created for
Angular components has been extended to include all selectors on
`ng-content` elements within the component's template.

This additional metadata is useful for tooling such as the Angular
Language Service, as it provides the ability to offer suggestions for
directives/components defined in libraries. At the moment, such
tooling extracts the necessary information from the _metadata.json_
manifest file as generated by ngc, however this metadata representation
is being replaced by the information emitted into the declaration files.

Resolves FW-1870

PR Close #35695
2020-03-24 14:21:42 -07:00
ff4eb0cb63 docs(elements): correct typo in custom elements image (#36090)
Fixes #36050

PR Close #36090
2020-03-24 10:36:11 -07:00
9ba46d9f88 fix(elements): correctly handle setting inputs to undefined (#36140)
Previously, when an input property was initially set to `undefined` it
would not be correctly recognized as a change (and trigger
`ngOnChanges()`).

This commit ensures that explicitly setting an input to `undefined` is
correctly handled the same as setting the property to any other value.
This aligns the behavior of Angular custom elements with that of the
corresponding components when used directly (not as custom elements).

PR Close #36140
2020-03-24 10:29:33 -07:00
b14ac96750 fix(elements): correctly set SimpleChange#firstChange for pre-existing inputs (#36140)
Previously, when an input property was set on an `NgElement` before
instantiating the underlying component, the `SimpleChange` object passed
to `ngOnChanges()` would have `firstChange` set to false, even if this
was the first change (as far as the component instance was concerned).

This commit fixes this by ensuring `SimpleChange#firstChange` is set to
true on first change, regardless if the property was set before or after
instantiating the component. This alignthe behavior of Angular custom
elements with that of the corresponding components when used directly
(not as custom elements).

Jira issue: [FW-2007](https://angular-team.atlassian.net/browse/FW-2007)

Fixes #36130

PR Close #36140
2020-03-24 10:29:32 -07:00
55dac05cf2 docs: fix typo in testing component with dependencies (#36219)
Fixes #36210

PR Close #36219
2020-03-24 10:19:47 -07:00
ae28d7c0b2 build(docs-infra): upgrade cli command docs sources to 56c648827 (#36224)
Updating [angular#master](https://github.com/angular/angular/tree/master) from [cli-builds#master](https://github.com/angular/cli-builds/tree/master).

##
Relevant changes in [commit range](f5dd5b16a...56c648827):

**Modified**
- help/build.json
- help/lint.json
- help/test.json

PR Close #36224
2020-03-24 10:16:49 -07:00
380de1e7b4 fix(ngcc): use path-mappings from tsconfig in dependency resolution (#36180)
When computing the dependencies between packages which are not in
node_modules, we may need to rely upon path-mappings to find the path
to the imported entry-point.

This commit allows ngcc to use the path-mappings from a tsconfig
file to find dependencies. By default any tsconfig.json file in the directory
above the `basePath` is loaded but it is possible to use a path to a
specific file by providing the `tsConfigPath` property to mainNgcc,
or to turn off loading any tsconfig file by setting `tsConfigPath` to `null`.
At the command line this is controlled via the `--tsconfig` option.

Fixes #36119

PR Close #36180
2020-03-24 10:16:12 -07:00
4f9717331d test(common): Add test for NgForOfContext.count (#36046)
`NgForOfContext.count` is the length of the iterable.

PR Close #36046
2020-03-24 10:15:11 -07:00
36fc28642a docs(common): Add missing entry for NgForOfContext.count (#36046)
`count` is available in `NgForOfContext` but it's missing in the docs.

PR Close #36046
2020-03-24 10:15:11 -07:00
a323b9b1a3 test(core): re-enable IE 10/11 test on SauceLabs (#35962)
I was not able to reproduce IE 10/11 failrue of the disabled
tests on SauceLabs any more. I did some cleanup of the test
in question but I doubt it was the root cause of the problem.

PR Close #35962
2020-03-24 10:14:47 -07:00
58f4254fba fix(dev-infra): use @angular/dev-infra-private package for pullapprove verification (#35996)
Adds devDependency on @angular/dev-infra-private and removes the verify script
from tools, relying instead on the script from ng-dev.

PR Close #35996
2020-03-24 10:14:05 -07:00
4419907b08 docs: release notes for the v9.1.0-rc.1 release 2020-03-23 13:22:17 -07:00
9d415f9c3f fix(docs-infra): change app-list-item to app-item-list (#35601)
The `app-list-item` component sounds like it is used for a single
item, however it renders a list of items. There were also
several changes in the documentation, where it was becoming
confusing if the `app-list-item` is using a single item or multiple
items. This commit fixes this issue. It renames the component and its
respective properties to make sure that the intention is very clear.

Closes #35598

PR Close #35601
2020-03-23 11:40:15 -07:00
fced8ee40e fix(localize): allow ICU expansion case to start with any character except } (#36123)
Previously, an expansion case could only start with an alpha numeric character.
This commit fixes this by allowing an expansion case to start with any character
except `}`.

The [ICU spec](http://userguide.icu-project.org/formatparse/messages) is pretty vague:

> Use a "select" argument to select sub-messages via a fixed set of keywords.

It does not specify what can be a "keyword" but from looking at the surrounding syntax it
appears that it can indeed be any string that does not contain a `}` character.

Closes #31586

PR Close #36123
2020-03-23 11:37:12 -07:00
1cb7b88505 fix(dev-infra): change circular deps positional params to camelCase (#36165)
Changes the positional params for the circular deps tooling to
use camelCase as it requires being defined in camelCase while
in strict mode.  Additionally, remove the `version()` call as
the boolean arguement does not exist in current versions and
throws errors on execution.

PR Close #36165
2020-03-23 11:36:28 -07:00
0e76b32aa5 fix(dev-infra): prep ts-circular-deps to load via node_modules (#36165)
to run ts-circular-deps via installed node_modules, we needed to set
the hashbang of the script to be a node environment, and discover the
project directory based on where the script is run rather than the
scripts file location.

PR Close #36165
2020-03-23 11:36:28 -07:00
e81ad3a1bc docs: Add asterisk info in template syntax guide (#36176)
Add helpful alert for asterisk syntax in the `ngFor` section of template syntax guide

PR Close #36176
2020-03-23 11:36:09 -07:00
c3a85ceabc docs: Change important alert of ngFor (#36176)
Update the important alert of ngFor so that it has a unique format with that of ngIf

PR Close #36176
2020-03-23 11:36:09 -07:00
85e0e366df docs(elements): Edge supports Web Components (#36182)
PR Close #36182
2020-03-23 11:35:49 -07:00
0ce8ad3493 fix(core): workaround Terser inlining bug (#36200)
This variable name change works around https://github.com/terser/terser/issues/615, which was causing the JIT production tests to fail in the Angular CLI repository (https://github.com/angular/angular-cli/issues/17264).

PR Close #36200
2020-03-23 11:34:39 -07:00
528e25a81a build(docs-infra): upgrade cli command docs sources to f5dd5b16a (#36159)
Updating [angular#master](https://github.com/angular/angular/tree/master) from [cli-builds#master](https://github.com/angular/cli-builds/tree/master).

##
Relevant changes in [commit range](6aa3c134c...f5dd5b16a):

**Modified**
- help/update.json

PR Close #36159
2020-03-20 13:57:18 -07:00
d25012e864 docs(zone.js): Typos on zone.md file and fixes on code examples. (#36138)
1. During reading the documentation I found some code examples that were refering to the class properties via methods, but without specifying the context `this`.
2. The 'onInvoke' hook was duplicated
3. A minor typo on `Zones and execution contexts` section
4. A minor typo on `Zones and async lifecycle hooks` section

PR Close #36138
2020-03-20 13:57:00 -07:00
c21c46a8e8 fix(docs-infra): include correct dependencies in StackBlitz examples (#36071)
Previously, all StackBlitz examples included the default dependencies
for `cli`-type projects. However, different example types may have
different `package.json` files with different dependencies.
For example, the [boilerplate `package.json`][1] for `elements` examples
includes an extra dependency on `@angular/elements`.

This commit changes `StackblitzBuilder` to use the dependencies that
correspond to each example type.
(NOTE: Manually verified the changes.)

Jira issue: [FW-2002][2]

[1]: https://github.com/angular/angular/blob/05d058622/aio/tools/examples/shared/boilerplate/elements/package.json
[2]: https://angular-team.atlassian.net/browse/FW-2002

PR Close #36071
2020-03-20 13:56:26 -07:00
9cc8bd5f7d refactor(docs-infra): clean up stackblitz-builder/builder.js script (#36071)
- Remove unused dependencies.
- Change `var` to `const/let`.
- Change regular functions as callbacks to arrow functions.
- Remove unnecessary intermediate variables.
- Switch from custom `_existsSync()` implementation to built-in
  `fs.existsSync()`.

PR Close #36071
2020-03-20 13:56:26 -07:00
ae3eaf8b16 test(compiler): remove whitespace in spans (#36169)
https://github.com/angular/angular/pull/36133 and https://github.com/angular/angular/pull/35986
caused a conflict in test after they both got merged to master.
This PR fixes the failed tests.

PR Close #36169
2020-03-20 12:52:31 -07:00
d714b95fb9 feat(compiler): Propagate value span of ExpressionBinding to ParsedProperty (#36133)
This commit propagates the correct value span in an ExpressionBinding of
a microsyntax expression to ParsedProperty, which in turn porpagates the
span to the template ASTs (both VE and Ivy).

PR Close #36133
2020-03-20 10:21:11 -07:00
912692137a fix(docs-infra): fix image name in example (#36127)
Closes #35618

PR Close #36127
2020-03-20 10:20:36 -07:00
6fc85073d2 feat(dev-infra): create commit-message validation script/tooling (#36117)
PR Close #36117
2020-03-20 10:20:12 -07:00
989dea7083 refactor(benchpress): delete broken code (#35922)
PR Close #35922
2020-03-20 10:19:48 -07:00
50 changed files with 8261 additions and 7691 deletions

View File

@ -1,3 +1,70 @@
<a name="10.0.0-next.1"></a>
# [10.0.0-next.1](https://github.com/angular/angular/compare/10.0.0-next.0...10.0.0-next.1) (2020-04-08)
This release contains various API docs improvements.
<a name="10.0.0-next.0"></a>
# [10.0.0-next.0](https://github.com/angular/angular/compare/9.1.0-rc.0...10.0.0-next.0) (2020-04-08)
### Bug Fixes
* **common:** let `KeyValuePipe` accept type unions with `null` ([#36093](https://github.com/angular/angular/issues/36093)) ([d783519](https://github.com/angular/angular/commit/d783519)), closes [#35743](https://github.com/angular/angular/issues/35743)
* **compiler:** avoid undefined expressions in holey array ([#36343](https://github.com/angular/angular/issues/36343)) ([5516802](https://github.com/angular/angular/commit/5516802))
* **compiler:** record correct end of expression ([#34690](https://github.com/angular/angular/issues/34690)) ([df890d7](https://github.com/angular/angular/commit/df890d7)), closes [#33477](https://github.com/angular/angular/issues/33477)
* **compiler:** resolve enum values in binary operations ([#36461](https://github.com/angular/angular/issues/36461)) ([64022f5](https://github.com/angular/angular/commit/64022f5)), closes [#35584](https://github.com/angular/angular/issues/35584)
* **compiler-cli:** pass real source spans where they are empty ([#31805](https://github.com/angular/angular/issues/31805)) ([e893c5a](https://github.com/angular/angular/commit/e893c5a))
* **core:** avoid migration error when non-existent symbol is imported ([#36367](https://github.com/angular/angular/issues/36367)) ([d43c306](https://github.com/angular/angular/commit/d43c306)), closes [#36346](https://github.com/angular/angular/issues/36346)
* **core:** ngOnDestroy on multi providers called with incorrect context ([#35840](https://github.com/angular/angular/issues/35840)) ([95fc3d4](https://github.com/angular/angular/commit/95fc3d4)), closes [#35231](https://github.com/angular/angular/issues/35231)
* **core:** run `APP_INITIALIZER`s before accessing `LOCALE_ID` token in Ivy TestBed ([#36237](https://github.com/angular/angular/issues/36237)) ([1649743](https://github.com/angular/angular/commit/1649743)), closes [#36230](https://github.com/angular/angular/issues/36230)
* **core:** undecorated-classes-with-decorated-fields migration does not decorate derived classes ([#35339](https://github.com/angular/angular/issues/35339)) ([32eafef](https://github.com/angular/angular/commit/32eafef)), closes [#34376](https://github.com/angular/angular/issues/34376)
* **core:** workaround Terser inlining bug ([#36200](https://github.com/angular/angular/issues/36200)) ([0ce8ad3](https://github.com/angular/angular/commit/0ce8ad3))
* **elements:** correctly handle setting inputs to `undefined` ([#36140](https://github.com/angular/angular/issues/36140)) ([9ba46d9](https://github.com/angular/angular/commit/9ba46d9))
* **elements:** correctly set `SimpleChange#firstChange` for pre-existing inputs ([#36140](https://github.com/angular/angular/issues/36140)) ([b14ac96](https://github.com/angular/angular/commit/b14ac96)), closes [#36130](https://github.com/angular/angular/issues/36130)
* **language-service:** infer type of elements of array-like objects ([#36312](https://github.com/angular/angular/issues/36312)) ([fe2b692](https://github.com/angular/angular/commit/fe2b692)), closes [#36191](https://github.com/angular/angular/issues/36191)
* **language-service:** use the `HtmlAst` to get the span of HTML tag ([#36371](https://github.com/angular/angular/issues/36371)) ([81195a2](https://github.com/angular/angular/commit/81195a2))
* **localize:** allow ICU expansion case to start with any character except `}` ([#36123](https://github.com/angular/angular/issues/36123)) ([fced8ee](https://github.com/angular/angular/commit/fced8ee)), closes [#31586](https://github.com/angular/angular/issues/31586)
* **ngcc:** add process title ([#36448](https://github.com/angular/angular/issues/36448)) ([76a8cd5](https://github.com/angular/angular/commit/76a8cd5)), closes [/github.com/angular/angular/issues/36414#issuecomment-609644282](https://github.com//github.com/angular/angular/issues/36414/issues/issuecomment-609644282)
* **ngcc:** allow ngcc configuration to match pre-release versions of packages ([#36370](https://github.com/angular/angular/issues/36370)) ([326240e](https://github.com/angular/angular/commit/326240e))
* **ngcc:** correctly detect imported TypeScript helpers ([#36284](https://github.com/angular/angular/issues/36284)) ([ca25c95](https://github.com/angular/angular/commit/ca25c95)), closes [#36089](https://github.com/angular/angular/issues/36089)
* **ngcc:** correctly identify relative Windows-style import paths ([#36372](https://github.com/angular/angular/issues/36372)) ([aecf9de](https://github.com/angular/angular/commit/aecf9de))
* **ngcc:** correctly identify the package path of secondary entry-points ([#36249](https://github.com/angular/angular/issues/36249)) ([995cd15](https://github.com/angular/angular/commit/995cd15)), closes [#35747](https://github.com/angular/angular/issues/35747)
* **ngcc:** detect non-emitted, non-imported TypeScript helpers ([#36418](https://github.com/angular/angular/issues/36418)) ([5fa7b8b](https://github.com/angular/angular/commit/5fa7b8b))
* **ngcc:** do not spawn more processes than intended in parallel mode ([#36280](https://github.com/angular/angular/issues/36280)) ([5cee709](https://github.com/angular/angular/commit/5cee709)), closes [#35719](https://github.com/angular/angular/issues/35719) [#36278](https://github.com/angular/angular/issues/36278) [/github.com/angular/angular/blob/b8e9a30d3b6/packages/compiler-cli/ngcc/src/main.ts#L429](https://github.com//github.com/angular/angular/blob/b8e9a30d3b6/packages/compiler-cli/ngcc/src/main.ts/issues/L429) [/github.com/angular/angular/blob/b8e9a30d3b6/packages/compiler-cli/ngcc/src/execution/cluster/master.ts#L108](https://github.com//github.com/angular/angular/blob/b8e9a30d3b6/packages/compiler-cli/ngcc/src/execution/cluster/master.ts/issues/L108) [/github.com/angular/angular/blob/b8e9a30d3b6/packages/compiler-cli/ngcc/src/execution/cluster/master.ts#L110](https://github.com//github.com/angular/angular/blob/b8e9a30d3b6/packages/compiler-cli/ngcc/src/execution/cluster/master.ts/issues/L110) [/github.com/angular/angular/blob/b8e9a30d3b6/packages/compiler-cli/ngcc/src/execution/cluster/master.ts#L199](https://github.com//github.com/angular/angular/blob/b8e9a30d3b6/packages/compiler-cli/ngcc/src/execution/cluster/master.ts/issues/L199)
* **ngcc:** do not write entry-point manifest outside node_modules ([#36299](https://github.com/angular/angular/issues/36299)) ([c6dd900](https://github.com/angular/angular/commit/c6dd900)), closes [#36296](https://github.com/angular/angular/issues/36296)
* **ngcc:** don't crash on cyclic source-map references ([#36452](https://github.com/angular/angular/issues/36452)) ([ee70a18](https://github.com/angular/angular/commit/ee70a18)), closes [#35727](https://github.com/angular/angular/issues/35727) [#35757](https://github.com/angular/angular/issues/35757)
* **ngcc:** handle bad path mappings when finding entry-points ([#36331](https://github.com/angular/angular/issues/36331)) ([cc4b813](https://github.com/angular/angular/commit/cc4b813)), closes [#36313](https://github.com/angular/angular/issues/36313) [#36283](https://github.com/angular/angular/issues/36283)
* **ngcc:** handle entry-points within container folders ([#36305](https://github.com/angular/angular/issues/36305)) ([38ad1d9](https://github.com/angular/angular/commit/38ad1d9)), closes [#35756](https://github.com/angular/angular/issues/35756) [#36216](https://github.com/angular/angular/issues/36216)
* **ngcc:** sniff `main` property for ESM5 format ([#36396](https://github.com/angular/angular/issues/36396)) ([2463548](https://github.com/angular/angular/commit/2463548)), closes [#35788](https://github.com/angular/angular/issues/35788)
* **ngcc:** support ignoring deep-imports via package config ([#36423](https://github.com/angular/angular/issues/36423)) ([f9fb833](https://github.com/angular/angular/commit/f9fb833)), closes [#35750](https://github.com/angular/angular/issues/35750)
* **ngcc:** support simple `browser` property in entry-points ([#36396](https://github.com/angular/angular/issues/36396)) ([6b3aa60](https://github.com/angular/angular/commit/6b3aa60)), closes [#36062](https://github.com/angular/angular/issues/36062)
* **ngcc:** use path-mappings from tsconfig in dependency resolution ([#36180](https://github.com/angular/angular/issues/36180)) ([380de1e](https://github.com/angular/angular/commit/380de1e)), closes [#36119](https://github.com/angular/angular/issues/36119)
* **ngcc:** use preserve whitespaces from tsconfig if provided ([#36189](https://github.com/angular/angular/issues/36189)) ([b8e9a30](https://github.com/angular/angular/commit/b8e9a30)), closes [#35871](https://github.com/angular/angular/issues/35871)
* **platform-server:** update `xhr2` dependency ([#36366](https://github.com/angular/angular/issues/36366)) ([b59bc0e](https://github.com/angular/angular/commit/b59bc0e)), closes [#36358](https://github.com/angular/angular/issues/36358)
* **router:** allow UrlMatcher to return null ([#36402](https://github.com/angular/angular/issues/36402)) ([568e9df](https://github.com/angular/angular/commit/568e9df)), closes [#29824](https://github.com/angular/angular/issues/29824)
* **router:** state data missing in routerLink ([#36462](https://github.com/angular/angular/issues/36462)) ([e0415db](https://github.com/angular/angular/commit/e0415db)), closes [#33173](https://github.com/angular/angular/issues/33173)
* **service-worker:** by default register the SW after 30s even the app never stabilizes ([#35870](https://github.com/angular/angular/issues/35870)) ([29e8a64](https://github.com/angular/angular/commit/29e8a64)), closes [#34464](https://github.com/angular/angular/issues/34464)
* **service-worker:** prevent SW registration strategies from affecting app stabilization ([#35870](https://github.com/angular/angular/issues/35870)) ([2d7c95f](https://github.com/angular/angular/commit/2d7c95f))
### Features
* **compiler:** add dependency info and ng-content selectors to metadata ([#35695](https://github.com/angular/angular/issues/35695)) ([32ce8b1](https://github.com/angular/angular/commit/32ce8b1))
* **compiler:** Propagate value span of ExpressionBinding to ParsedProperty ([#36133](https://github.com/angular/angular/issues/36133)) ([d714b95](https://github.com/angular/angular/commit/d714b95))
* **core:** undecorated-classes migration should handle derived abstract classes ([#35339](https://github.com/angular/angular/issues/35339)) ([c24ad56](https://github.com/angular/angular/commit/c24ad56))
* **service-worker:** support timeout in `registerWhenStable` SW registration strategy ([#35870](https://github.com/angular/angular/issues/35870)) ([00efacf](https://github.com/angular/angular/commit/00efacf)), closes [#34464](https://github.com/angular/angular/issues/34464)
### BREAKING CHANGES
* **router:** UrlMatcher's type now reflects that it could always return
null.
If you implemented your own Router or Recognizer class, please update it to
handle matcher returning null.
<a name="9.1.1"></a>
## [9.1.1](https://github.com/angular/angular/compare/9.1.0...9.1.1) (2020-04-07)
@ -32,7 +99,6 @@
* **router:** state data missing in routerLink ([#36462](https://github.com/angular/angular/issues/36462)) ([0e7a89a](https://github.com/angular/angular/commit/0e7a89a)), closes [#33173](https://github.com/angular/angular/issues/33173)
<a name="9.1.0"></a>
# [9.1.0](https://github.com/angular/angular/compare/9.0.0...9.1.0) (2020-03-25)

View File

@ -99,7 +99,7 @@ Project-specific [TypeScript](https://www.typescriptlang.org/) configuration fil
| APPLICATION-SPECIFIC CONFIG FILES | PURPOSE |
| :--------------------- | :------------------------------------------|
| `browserslist` | Configures sharing of target browsers and Node.js versions among various front-end tools. See [Browserslist on GitHub](https://github.com/browserslist/browserslist) for more information. |
| `.browserslistrc` | Configures sharing of target browsers and Node.js versions among various front-end tools. See [Browserslist on GitHub](https://github.com/browserslist/browserslist) for more information. |
| `karma.conf.js` | Application-specific [Karma](https://karma-runner.github.io/2.0/config/configuration-file.html) configuration. |
| `tsconfig.app.json` | Application-specific [TypeScript](https://www.typescriptlang.org/) configuration, including TypeScript and Angular template compiler options. See [TypeScript Configuration](guide/typescript-configuration) and [Angular Compiler Options](guide/angular-compiler-options). |
| `tsconfig.spec.json` | [TypeScript](https://www.typescriptlang.org/) configuration for the application tests. See [TypeScript Configuration](guide/typescript-configuration). |

View File

@ -101,7 +101,8 @@ The following table provides the status for Angular versions under support.
Version | Status | Released | Active Ends | LTS Ends
------- | ------ | ------------ | ------------ | ------------
^8.0.0 | Active | May 28, 2019 | Nov 28, 2019 | Nov 28, 2020
^9.0.0 | Active | Feb 06, 2020 | Aug 06, 2020 | Aug 06, 2021
^8.0.0 | LTS | May 28, 2019 | Nov 28, 2019 | Nov 28, 2020
^7.0.0 | LTS | Oct 18, 2018 | Apr 18, 2019 | Apr 18, 2020
Angular versions ^4.0.0, ^5.0.0 and ^6.0.0 are no longer under support.

View File

@ -53,7 +53,7 @@ The initial `tsconfig.json` for an Angular app typically looks like the followin
]
},
"angularCompilerOptions": {
"fullTemplateTypeCheck": true,
"strictTemplates": true,
"strictInjectionParameters": true
}
}
@ -62,7 +62,6 @@ The initial `tsconfig.json` for an Angular app typically looks like the followin
{@a noImplicitAny}
### *noImplicitAny* and *suppressImplicitAnyIndexErrors*
TypeScript developers disagree about whether the `noImplicitAny` flag should be `true` or `false`.
@ -96,6 +95,7 @@ For more information about how the TypeScript configuration affects compilation,
</div>
{@a typings}
## TypeScript typings
@ -146,7 +146,6 @@ For instance, to install typings for `jasmine` you run `npm install @types/jasmi
{@a target}
### *target*
By default, the target is `es2015`, which is supported only in modern browsers. You can configure the target to `es5` to specifically support legacy browsers. [Differential loading](guide/deployment#differential-loading) is also provided by the Angular CLI to support modern, and legacy browsers with separate bundles.

View File

@ -23,7 +23,7 @@
"build-local-with-viewengine": "yarn ~~build",
"prebuild-local-with-viewengine-ci": "node scripts/switch-to-viewengine && yarn setup-local-ci",
"build-local-with-viewengine-ci": "yarn ~~build --progress=false",
"extract-cli-command-docs": "node tools/transforms/cli-docs-package/extract-cli-commands.js 526c3cc37",
"extract-cli-command-docs": "node tools/transforms/cli-docs-package/extract-cli-commands.js 56c648827",
"lint": "yarn check-env && yarn docs-lint && ng lint && yarn example-lint && yarn tools-lint",
"test": "yarn check-env && ng test",
"pree2e": "yarn check-env && yarn update-webdriver",

View File

@ -37,7 +37,6 @@
],
"angularCompilerOptions": {
"disableTypeScriptVersionCheck": true,
"fullTemplateTypeCheck": true,
"strictInjectionParameters": true,
"strictTemplates": true
}

View File

@ -20,8 +20,17 @@ export type Golden = CircularDependency[];
* the source file objects are mapped to their relative file names.
*/
export function convertReferenceChainToGolden(refs: ReferenceChain[], baseDir: string): Golden {
return refs.map(
chain => chain.map(({fileName}) => convertPathToForwardSlash(relative(baseDir, fileName))));
return refs
.map(
// Normalize cycles as the paths can vary based on which node in the cycle is visited
// first in the analyzer. The paths represent cycles. Hence we can shift nodes in a
// deterministic way so that the goldens don't change unnecessarily and cycle comparison
// is simpler.
chain => normalizeCircularDependency(
chain.map(({fileName}) => convertPathToForwardSlash(relative(baseDir, fileName)))))
// Sort cycles so that the golden doesn't change unnecessarily when cycles are detected
// in different order (e.g. new imports cause cycles to be detected earlier or later).
.sort(compareCircularDependency);
}
/**
@ -44,6 +53,53 @@ export function compareGoldens(actual: Golden, expected: Golden) {
return {newCircularDeps, fixedCircularDeps};
}
/**
* Normalizes the a circular dependency by ensuring that the path starts with the first
* node in alphabetical order. Since the path array represents a cycle, we can make a
* specific node the first element in the path that represents the cycle.
*
* This method is helpful because the path of circular dependencies changes based on which
* file in the path has been visited first by the analyzer. e.g. Assume we have a circular
* dependency represented as: `A -> B -> C`. The analyzer will detect this cycle when it
* visits `A`. Though when a source file that is analyzed before `A` starts importing `B`,
* the cycle path will detected as `B -> C -> A`. This represents the same cycle, but is just
* different due to a limitation of using a data structure that can be written to a text-based
* golden file.
*
* To account for this non-deterministic behavior in goldens, we shift the circular
* dependency path to the first node based on alphabetical order. e.g. `A` will always
* be the first node in the path that represents the cycle.
*/
function normalizeCircularDependency(path: CircularDependency): CircularDependency {
if (path.length <= 1) {
return path;
}
let indexFirstNode: number = 0;
let valueFirstNode: string = path[0];
// Find a node in the cycle path that precedes all other elements
// in terms of alphabetical order.
for (let i = 1; i < path.length; i++) {
const value = path[i];
if (value.localeCompare(valueFirstNode, 'en') < 0) {
indexFirstNode = i;
valueFirstNode = value;
}
}
// If the alphabetically first node is already at start of the path, just
// return the actual path as no changes need to be made.
if (indexFirstNode === 0) {
return path;
}
// Move the determined first node (as of alphabetical order) to the start of a new
// path array. The nodes before the first node in the old path are then concatenated
// to the end of the new path. This is possible because the path represents a cycle.
return [...path.slice(indexFirstNode), ...path.slice(0, indexFirstNode)];
}
/** Checks whether the specified circular dependencies are equal. */
function isSameCircularDependency(actual: CircularDependency, expected: CircularDependency) {
if (actual.length !== expected.length) {
@ -56,3 +112,20 @@ function isSameCircularDependency(actual: CircularDependency, expected: Circular
}
return true;
}
/**
* Compares two circular dependencies by respecting the alphabetic order of nodes in the
* cycle paths. The first nodes which don't match in both paths are decisive on the order.
*/
function compareCircularDependency(a: CircularDependency, b: CircularDependency): number {
// Go through nodes in both cycle paths and determine whether `a` should be ordered
// before `b`. The first nodes which don't match decide on the order.
for (let i = 0; i < Math.min(a.length, b.length); i++) {
const compareValue = a[i].localeCompare(b[i], 'en');
if (compareValue !== 0) {
return compareValue;
}
}
// If all nodes are equal in the cycles, the order is based on the length of both cycles.
return a.length - b.length;
}

File diff suppressed because it is too large Load Diff

View File

@ -500,7 +500,7 @@ export declare abstract class UrlHandlingStrategy {
abstract shouldProcessUrl(url: UrlTree): boolean;
}
export declare type UrlMatcher = (segments: UrlSegment[], group: UrlSegmentGroup, route: Route) => UrlMatchResult;
export declare type UrlMatcher = (segments: UrlSegment[], group: UrlSegmentGroup, route: Route) => UrlMatchResult | null;
export declare type UrlMatchResult = {
consumed: UrlSegment[];

View File

@ -1,6 +1,6 @@
{
"name": "angular-srcs",
"version": "9.1.1",
"version": "10.0.0-next.1",
"private": true,
"description": "Angular - a web framework for modern web apps",
"homepage": "https://github.com/angular/angular",
@ -72,8 +72,8 @@
"@types/fs-extra": "4.0.2",
"@types/hammerjs": "2.0.35",
"@types/inquirer": "^0.0.44",
"@types/jasmine": "^2.8.8",
"@types/jasminewd2": "^2.0.6",
"@types/jasmine": "3.5.10",
"@types/jasminewd2": "^2.0.8",
"@types/minimist": "^1.2.0",
"@types/node": "^12.11.1",
"@types/selenium-webdriver": "3.0.7",
@ -105,8 +105,8 @@
"hammerjs": "2.0.8",
"http-server": "^0.11.1",
"incremental-dom": "0.4.1",
"jasmine": "^3.1.0",
"jasmine-core": "^3.1.0",
"jasmine": "^3.5.0",
"jasmine-core": "^3.5.0",
"jquery": "3.0.0",
"karma": "~4.1.0",
"karma-chrome-launcher": "^2.2.0",

View File

@ -29,8 +29,8 @@ const XSSI_PREFIX = ')]}\'\n';
{
describe('XhrBackend', () => {
let factory: MockXhrFactory = null !;
let backend: HttpXhrBackend = null !;
let factory: MockXhrFactory = null!;
let backend: HttpXhrBackend = null!;
beforeEach(() => {
factory = new MockXhrFactory();
backend = new HttpXhrBackend(factory);
@ -92,7 +92,7 @@ const XSSI_PREFIX = ')]}\'\n';
factory.mock.mockFlush(200, 'OK', JSON.stringify({data: 'some data'}));
expect(events.length).toBe(2);
const res = events[1] as HttpResponse<{data: string}>;
expect(res.body !.data).toBe('some data');
expect(res.body!.data).toBe('some data');
});
it('handles a blank json response', () => {
const events = trackEvents(backend.handle(TEST_POST.clone({responseType: 'json'})));
@ -106,14 +106,14 @@ const XSSI_PREFIX = ')]}\'\n';
factory.mock.mockFlush(500, 'Error', JSON.stringify({data: 'some data'}));
expect(events.length).toBe(2);
const res = events[1] as any as HttpErrorResponse;
expect(res.error !.data).toBe('some data');
expect(res.error!.data).toBe('some data');
});
it('handles a json error response with XSSI prefix', () => {
const events = trackEvents(backend.handle(TEST_POST.clone({responseType: 'json'})));
factory.mock.mockFlush(500, 'Error', XSSI_PREFIX + JSON.stringify({data: 'some data'}));
expect(events.length).toBe(2);
const res = events[1] as any as HttpErrorResponse;
expect(res.error !.data).toBe('some data');
expect(res.error!.data).toBe('some data');
});
it('handles a json string response', () => {
const events = trackEvents(backend.handle(TEST_POST.clone({responseType: 'json'})));
@ -128,7 +128,7 @@ const XSSI_PREFIX = ')]}\'\n';
factory.mock.mockFlush(200, 'OK', XSSI_PREFIX + JSON.stringify({data: 'some data'}));
expect(events.length).toBe(2);
const res = events[1] as HttpResponse<{data: string}>;
expect(res.body !.data).toBe('some data');
expect(res.body!.data).toBe('some data');
});
it('emits unsuccessful responses via the error path', done => {
backend.handle(TEST_POST).subscribe(undefined, (err: HttpErrorResponse) => {
@ -141,7 +141,7 @@ const XSSI_PREFIX = ')]}\'\n';
it('emits real errors via the error path', done => {
backend.handle(TEST_POST).subscribe(undefined, (err: HttpErrorResponse) => {
expect(err instanceof HttpErrorResponse).toBe(true);
expect(err.error instanceof Error);
expect(err.error instanceof Error).toBeTrue();
expect(err.url).toBe('/test');
done();
});

View File

@ -95,7 +95,7 @@ runInEachFileSystem(() => {
});
// The "test" compilation result is just the name of the decorator being compiled
// (suffixed with `(compiled)`)
handler.compile.and.callFake((decl: ts.Declaration, analysis: any) => {
(handler.compile as any).and.callFake((decl: ts.Declaration, analysis: any) => {
logs.push(`compile: ${(decl as any).name.text}@${analysis.decoratorName} (resolved: ${
analysis.resolved})`);
return `@${analysis.decoratorName} (compiled)`;
@ -183,7 +183,7 @@ runInEachFileSystem(() => {
it('should call detect on the decorator handlers with each class from the parsed file',
() => {
expect(testHandler.detect).toHaveBeenCalledTimes(5);
expect(testHandler.detect.calls.allArgs().map(args => args[1])).toEqual([
expect(testHandler.detect.calls.allArgs().map((args: any[]) => args[1])).toEqual([
null,
jasmine.arrayContaining([jasmine.objectContaining({name: 'Component'})]),
jasmine.arrayContaining([jasmine.objectContaining({name: 'Directive'})]),

View File

@ -34,9 +34,9 @@ describe('ClusterExecutor', () => {
beforeEach(() => {
masterRunSpy = spyOn(ClusterMaster.prototype, 'run')
.and.returnValue(Promise.resolve('CusterMaster#run()'));
.and.returnValue(Promise.resolve('CusterMaster#run()' as any));
workerRunSpy = spyOn(ClusterWorker.prototype, 'run')
.and.returnValue(Promise.resolve('CusterWorker#run()'));
.and.returnValue(Promise.resolve('CusterWorker#run()' as any));
createTaskCompletedCallback = jasmine.createSpy('createTaskCompletedCallback');
mockLogger = new MockLogger();

View File

@ -38,7 +38,7 @@ runInEachFileSystem(() => {
initMockFileSystem(fs, testFiles);
// Force single-process execution in unit tests by mocking available CPUs to 1.
spyOn(os, 'cpus').and.returnValue([{model: 'Mock CPU'}]);
spyOn(os, 'cpus').and.returnValue([{model: 'Mock CPU'} as any]);
});
it('should run ngcc without errors for esm2015', () => {
@ -962,7 +962,8 @@ runInEachFileSystem(() => {
.toMatch(ANGULAR_CORE_IMPORT_REGEX);
// Copies over files (unchanged) that did not need compiling
expect(fs.exists(_(`/node_modules/@angular/common/__ivy_ngcc__/esm5/src/version.js`)));
expect(fs.exists(_(`/node_modules/@angular/common/__ivy_ngcc__/esm5/src/version.js`)))
.toBeTrue();
expect(fs.readFile(_(`/node_modules/@angular/common/__ivy_ngcc__/esm5/src/version.js`)))
.toEqual(fs.readFile(_(`/node_modules/@angular/common/esm5/src/version.js`)));

View File

@ -66,6 +66,9 @@ runInEachFileSystem(() => {
});
spyOn(lockFile, 'read').and.callFake(() => {
log.push('read() => ' + lockFileContents);
if (lockFileContents === null) {
throw {code: 'ENOENT'};
}
return lockFileContents;
});
@ -99,6 +102,9 @@ runInEachFileSystem(() => {
});
spyOn(lockFile, 'read').and.callFake(() => {
log.push('read() => ' + lockFileContents);
if (lockFileContents === null) {
throw {code: 'ENOENT'};
}
return lockFileContents;
});
@ -148,6 +154,9 @@ runInEachFileSystem(() => {
});
spyOn(lockFile, 'read').and.callFake(() => {
log.push('read() => ' + lockFileContents);
if (lockFileContents === null) {
throw {code: 'ENOENT'};
}
return lockFileContents;
});

View File

@ -12,6 +12,8 @@ describe('unlocker', () => {
it('should attach a handler to the `disconnect` event', () => {
spyOn(process, 'on');
require('../../../src/locking/lock_file_with_child_process/unlocker');
expect(process.on).toHaveBeenCalledWith('disconnect', jasmine.any(Function));
// TODO: @JiaLiPassion, need to wait for @types/jasmine to handle the override case
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/42455
expect(process.on).toHaveBeenCalledWith('disconnect' as any, jasmine.any(Function));
});
});

View File

@ -70,7 +70,7 @@ runInEachFileSystem(() => {
const config = new NgccConfiguration(fs, _('/project'));
spyOn(config, 'getConfig').and.returnValue({
entryPoints: {[_('/project/node_modules/some_package/valid_entry_point')]: {ignore: true}}
});
} as any);
const entryPoint = getEntryPointInfo(
fs, config, new MockLogger(), SOME_PACKAGE,
_('/project/node_modules/some_package/valid_entry_point'));
@ -95,7 +95,8 @@ runInEachFileSystem(() => {
esm2015: './some_other.js',
};
spyOn(config, 'getConfig').and.returnValue({
entryPoints: {[_('/project/node_modules/some_package/valid_entry_point')]: {override}}
entryPoints: {[_('/project/node_modules/some_package/valid_entry_point')]: {override}},
versionRange: '*'
});
const entryPoint = getEntryPointInfo(
fs, config, new MockLogger(), SOME_PACKAGE,
@ -145,7 +146,9 @@ runInEachFileSystem(() => {
const override =
JSON.parse(createPackageJson('missing_package_json', {excludes: ['name']}));
spyOn(config, 'getConfig').and.returnValue({
entryPoints: {[_('/project/node_modules/some_package/missing_package_json')]: {override}}
entryPoints:
{[_('/project/node_modules/some_package/missing_package_json')]: {override}},
versionRange: '*'
});
const entryPoint = getEntryPointInfo(
fs, config, new MockLogger(), SOME_PACKAGE,
@ -279,7 +282,8 @@ runInEachFileSystem(() => {
]);
const config = new NgccConfiguration(fs, _('/project'));
spyOn(config, 'getConfig').and.returnValue({
entryPoints: {[_('/project/node_modules/some_package/missing_metadata')]: {}}
entryPoints: {[_('/project/node_modules/some_package/missing_metadata')]: {}},
versionRange: '*'
});
const entryPoint = getEntryPointInfo(
fs, config, new MockLogger(), SOME_PACKAGE,
@ -398,7 +402,7 @@ runInEachFileSystem(() => {
if (result === NO_ENTRY_POINT || result === INCOMPATIBLE_ENTRY_POINT) {
return fail(`Expected an entry point but got ${result}`);
}
entryPoint = result;
entryPoint = result as any;
});
it('should return `esm2015` format for `fesm2015` property', () => {

View File

@ -47,7 +47,7 @@ describe('CachedFileSystem', () => {
let lstatSpy: jasmine.Spy;
beforeEach(() => {
// For most of the tests the files are not symbolic links.
lstatSpy = spyOn(delegate, 'lstat').and.returnValue({isSymbolicLink: () => false});
lstatSpy = spyOn(delegate, 'lstat').and.returnValue({isSymbolicLink: () => false} as any);
});
it('should call delegate if not in cache', () => {
@ -93,7 +93,7 @@ describe('CachedFileSystem', () => {
describe('invalidateCaches()', () => {
it('should call the delegate `readFile()` if the path for the cached file has been invalidated',
() => {
spyOn(delegate, 'lstat').and.returnValue({isSymbolicLink: () => false});
spyOn(delegate, 'lstat').and.returnValue({isSymbolicLink: () => false} as any);
const spy = spyOn(delegate, 'readFile').and.returnValue('Some contents');
fs.readFile(abcPath); // Call once to fill the cache
spy.calls.reset();
@ -230,7 +230,7 @@ describe('CachedFileSystem', () => {
describe('moveFile()', () => {
beforeEach(() => {
// `moveFile()` relies upon `readFile` which calls through to `lstat()`, so stub it out.
spyOn(delegate, 'lstat').and.returnValue({isSymbolicLink: () => false});
spyOn(delegate, 'lstat').and.returnValue({isSymbolicLink: () => false} as any);
});
it('should call delegate', () => {

View File

@ -65,10 +65,12 @@ describe('NodeJSFileSystem', () => {
describe('readdir()', () => {
it('should delegate to fs.readdirSync()', () => {
const spy = spyOn(realFs, 'readdirSync').and.returnValue(['x', 'y/z']);
const spy = spyOn(realFs, 'readdirSync').and.returnValue(['x', 'y/z'] as any);
const result = fs.readdir(abcPath);
expect(result).toEqual([relativeFrom('x'), relativeFrom('y/z')]);
expect(spy).toHaveBeenCalledWith(abcPath);
// TODO: @JiaLiPassion need to wait for @types/jasmine update to handle optional parameters.
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/43486
expect(spy as any).toHaveBeenCalledWith(abcPath);
});
});
@ -88,7 +90,9 @@ describe('NodeJSFileSystem', () => {
const spy = spyOn(realFs, 'statSync').and.returnValue(stats);
const result = fs.stat(abcPath);
expect(result).toBe(stats);
expect(spy).toHaveBeenCalledWith(abcPath);
// TODO: @JiaLiPassion need to wait for @types/jasmine update to handle optional parameters.
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/43486
expect(spy as any).toHaveBeenCalledWith(abcPath);
});
});
@ -125,7 +129,7 @@ describe('NodeJSFileSystem', () => {
const xyPath = absoluteFrom('/x/y');
const mkdirCalls: string[] = [];
const existsCalls: string[] = [];
spyOn(realFs, 'mkdirSync').and.callFake((path: string) => mkdirCalls.push(path));
spyOn(realFs, 'mkdirSync').and.callFake(((path: string) => mkdirCalls.push(path)) as any);
spyOn(fs, 'exists').and.callFake((path: AbsoluteFsPath) => {
existsCalls.push(path);
switch (path) {
@ -171,12 +175,14 @@ describe('NodeJSFileSystem', () => {
}
return false;
});
spyOn(fs, 'stat').and.returnValue({isDirectory: () => true});
const mkdirSyncSpy = spyOn(realFs, 'mkdirSync').and.callFake((path: string) => {
spyOn(fs, 'stat').and.returnValue({isDirectory: () => true} as any);
const mkdirSyncSpy =
spyOn(realFs, 'mkdirSync').and.callFake(((path: string) => {
if (path === abcPath) {
throw new Error('It exists already. Supposedly.');
throw new Error(
'It exists already. Supposedly.');
}
});
}) as any);
fs.ensureDir(abcPath);
expect(mkdirSyncSpy).toHaveBeenCalledTimes(3);
@ -186,11 +192,12 @@ describe('NodeJSFileSystem', () => {
it('should fail if creating the directory throws and the directory does not exist', () => {
spyOn(fs, 'exists').and.returnValue(false);
spyOn(realFs, 'mkdirSync').and.callFake((path: string) => {
spyOn(realFs, 'mkdirSync')
.and.callFake(((path: string) => {
if (path === abcPath) {
throw new Error('Unable to create directory (for whatever reason).');
}
});
}) as any);
expect(() => fs.ensureDir(abcPath))
.toThrowError('Unable to create directory (for whatever reason).');
@ -210,12 +217,12 @@ describe('NodeJSFileSystem', () => {
}
return false;
});
spyOn(fs, 'stat').and.returnValue({isDirectory: isDirectorySpy});
spyOn(realFs, 'mkdirSync').and.callFake((path: string) => {
spyOn(fs, 'stat').and.returnValue({isDirectory: isDirectorySpy} as any);
spyOn(realFs, 'mkdirSync').and.callFake(((path: string) => {
if (path === abcPath) {
throw new Error('It exists already. Supposedly.');
}
});
}) as any);
expect(() => fs.ensureDir(abcPath)).toThrowError('It exists already. Supposedly.');
expect(isDirectorySpy).toHaveBeenCalledTimes(1);

View File

@ -775,18 +775,22 @@ runInEachFileSystem(() => {
describe('(visited file tracking)', () => {
it('should track each time a source file is visited', () => {
const addDependency = jasmine.createSpy('DependencyTracker');
const addDependency =
jasmine.createSpy<DependencyTracker['addDependency']>('DependencyTracker');
const {expression, checker} = makeExpression(
`class A { static foo = 42; } function bar() { return A.foo; }`, 'bar()');
const evaluator = makeEvaluator(checker, {...fakeDepTracker, addDependency});
evaluator.evaluate(expression);
expect(addDependency).toHaveBeenCalledTimes(2); // two declaration visited
expect(addDependency.calls.allArgs().map(args => [args[0].fileName, args[1].fileName]))
expect(
addDependency.calls.allArgs().map(
(args: Parameters<typeof addDependency>) => [args[0].fileName, args[1].fileName]))
.toEqual([[_('/entry.ts'), _('/entry.ts')], [_('/entry.ts'), _('/entry.ts')]]);
});
it('should track imported source files', () => {
const addDependency = jasmine.createSpy('DependencyTracker');
const addDependency =
jasmine.createSpy<DependencyTracker['addDependency']>('DependencyTracker');
const {expression, checker} =
makeExpression(`import {Y} from './other'; const A = Y;`, 'A', [
{name: _('/other.ts'), contents: `export const Y = 'test';`},
@ -795,7 +799,9 @@ runInEachFileSystem(() => {
const evaluator = makeEvaluator(checker, {...fakeDepTracker, addDependency});
evaluator.evaluate(expression);
expect(addDependency).toHaveBeenCalledTimes(2);
expect(addDependency.calls.allArgs().map(args => [args[0].fileName, args[1].fileName]))
expect(
addDependency.calls.allArgs().map(
(args: Parameters<typeof addDependency>) => [args[0].fileName, args[1].fileName]))
.toEqual([
[_('/entry.ts'), _('/entry.ts')],
[_('/entry.ts'), _('/other.ts')],
@ -803,7 +809,8 @@ runInEachFileSystem(() => {
});
it('should track files passed through during re-exports', () => {
const addDependency = jasmine.createSpy('DependencyTracker');
const addDependency =
jasmine.createSpy<DependencyTracker['addDependency']>('DependencyTracker');
const {expression, checker} =
makeExpression(`import * as mod from './direct-reexport';`, 'mod.value.property', [
{name: _('/const.ts'), contents: 'export const value = {property: "test"};'},
@ -823,7 +830,9 @@ runInEachFileSystem(() => {
const evaluator = makeEvaluator(checker, {...fakeDepTracker, addDependency});
evaluator.evaluate(expression);
expect(addDependency).toHaveBeenCalledTimes(2);
expect(addDependency.calls.allArgs().map(args => [args[0].fileName, args[1].fileName]))
expect(
addDependency.calls.allArgs().map(
(args: Parameters<typeof addDependency>) => [args[0].fileName, args[1].fileName]))
.toEqual([
[_('/entry.ts'), _('/direct-reexport.ts')],
// Not '/indirect-reexport.ts' or '/def.ts'.

View File

@ -41,7 +41,9 @@ describe('ngc transformer command-line', () => {
basePath = support.basePath;
outDir = path.join(basePath, 'built');
process.chdir(basePath);
write = (fileName: string, content: string) => { support.write(fileName, content); };
write = (fileName: string, content: string) => {
support.write(fileName, content);
};
write('tsconfig-base.json', `{
"compilerOptions": {
@ -96,8 +98,9 @@ describe('ngc transformer command-line', () => {
});
describe('errors', () => {
beforeEach(() => { errorSpy.and.stub(); });
beforeEach(() => {
errorSpy.and.stub();
});
it('should not print the stack trace if user input file does not exist', () => {
writeConfig(`{
@ -231,7 +234,6 @@ describe('ngc transformer command-line', () => {
});
describe('compile ngfactory files', () => {
it('should compile ngfactory files that are not referenced by root files', () => {
writeConfig(`{
"extends": "./tsconfig-base.json",
@ -1122,7 +1124,6 @@ describe('ngc transformer command-line', () => {
});
describe('with external symbol re-exports enabled', () => {
it('should be able to compile multiple libraries with summaries', () => {
// Note: we need to emit the generated code for the libraries
// into the node_modules, as that is the only way that we
@ -1559,11 +1560,15 @@ describe('ngc transformer command-line', () => {
originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL;
jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000;
const timerToken = 100;
spyOn(ts.sys, 'setTimeout').and.callFake((callback: () => void) => {
// TODO: @JiaLiPassion, need to wait @types/jasmine to handle optional method case
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/43486
spyOn(ts.sys as any, 'setTimeout').and.callFake((callback: () => void) => {
timer = callback;
return timerToken;
});
spyOn(ts.sys, 'clearTimeout').and.callFake((token: number) => {
// TODO: @JiaLiPassion, need to wait @types/jasmine to handle optional method case
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/43486
spyOn(ts.sys as any, 'clearTimeout').and.callFake((token: number) => {
if (token == timerToken) {
timer = undefined;
}
@ -1615,7 +1620,9 @@ describe('ngc transformer command-line', () => {
`);
});
afterEach(() => { jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout; });
afterEach(() => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout;
});
function writeAppConfig(location: string) {
writeConfig(`{
@ -1670,11 +1677,13 @@ describe('ngc transformer command-line', () => {
`);
}));
it('should recompile when the html file changes',
expectRecompile(() => { write('greet.html', '<p> Hello {{name}} again!</p>'); }));
it('should recompile when the html file changes', expectRecompile(() => {
write('greet.html', '<p> Hello {{name}} again!</p>');
}));
it('should recompile when the css file changes',
expectRecompile(() => { write('greet.css', `p.greeting { color: blue }`); }));
it('should recompile when the css file changes', expectRecompile(() => {
write('greet.css', `p.greeting { color: blue }`);
}));
});
describe('regressions', () => {
@ -2039,8 +2048,8 @@ describe('ngc transformer command-line', () => {
expect(exitCode).toBe(1, 'Compile was expected to fail');
const srcPathWithSep = `lib/`;
expect(messages[0])
.toEqual(
`${srcPathWithSep}test.component.ts(6,21): Error during template compile of 'TestComponent'
.toEqual(`${
srcPathWithSep}test.component.ts(6,21): Error during template compile of 'TestComponent'
Tagged template expressions are not supported in metadata in 't1'
't1' references 't2' at ${srcPathWithSep}indirect1.ts(3,27)
't2' contains the error at ${srcPathWithSep}indirect2.ts(4,27).
@ -2049,7 +2058,6 @@ describe('ngc transformer command-line', () => {
});
describe('tree shakeable services', () => {
function compileService(source: string): string {
write('service.ts', source);

View File

@ -13,8 +13,8 @@ describe('convertValueToOutputAst', () => {
it('should convert all array elements, including undefined', () => {
const ctx = null;
const value = new Array(3).concat('foo');
const expr = convertValueToOutputAst(ctx !, value) as o.LiteralArrayExpr;
expect(expr instanceof o.LiteralArrayExpr);
const expr = convertValueToOutputAst(ctx!, value) as o.LiteralArrayExpr;
expect(expr instanceof o.LiteralArrayExpr).toBe(true);
expect(expr.entries.length).toBe(4);
for (let i = 0; i < 4; ++i) {
expect(expr.entries[i] instanceof o.Expression).toBe(true);

View File

@ -5,7 +5,7 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {AUTO_STYLE, AnimationPlayer, animate, animateChild, group, query, sequence, stagger, state, style, transition, trigger, ɵAnimationGroupPlayer as AnimationGroupPlayer} from '@angular/animations';
import {animate, animateChild, AnimationPlayer, AUTO_STYLE, group, query, sequence, stagger, state, style, transition, trigger, ɵAnimationGroupPlayer as AnimationGroupPlayer} from '@angular/animations';
import {AnimationDriver, ɵAnimationEngine} from '@angular/animations/browser';
import {matchesElement} from '@angular/animations/browser/src/render/shared';
import {TransitionAnimationPlayer} from '@angular/animations/browser/src/render/transition_animation_engine';
@ -13,21 +13,23 @@ import {ENTER_CLASSNAME, LEAVE_CLASSNAME} from '@angular/animations/browser/src/
import {MockAnimationDriver, MockAnimationPlayer} from '@angular/animations/browser/testing';
import {CommonModule} from '@angular/common';
import {Component, HostBinding, ViewChild} from '@angular/core';
import {TestBed, fakeAsync, flushMicrotasks} from '@angular/core/testing';
import {fakeAsync, flushMicrotasks, TestBed} from '@angular/core/testing';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {HostListener} from '../../src/metadata/directives';
(function() {
// these tests are only mean't to be run within the DOM (for now)
if (isNode) return;
// these tests are only mean't to be run within the DOM (for now)
if (isNode) return;
describe('animation query tests', function() {
describe('animation query tests', function() {
function getLog(): MockAnimationPlayer[] {
return MockAnimationDriver.log as MockAnimationPlayer[];
}
function resetLog() { MockAnimationDriver.log = []; }
function resetLog() {
MockAnimationDriver.log = [];
}
beforeEach(() => {
resetLog();
@ -60,7 +62,7 @@ import {HostListener} from '../../src/metadata/directives';
query(
'@*',
[
style({ backgroundColor: 'blue' }),
style({backgroundColor: 'blue'}),
animate(1000, style({backgroundColor: 'red'})),
]),
]),
@ -68,26 +70,22 @@ import {HostListener} from '../../src/metadata/directives';
trigger(
'a',
[
transition('* => 1', [
animate(1000, style({ opacity: 0 }))
]),
transition('* => 1', [animate(1000, style({opacity: 0}))]),
]),
trigger(
'b',
[
transition('* => 1', [
animate(1000, style({ opacity: 0 })),
query('.b-inner', [
animate(1000, style({ opacity: 0 }))
]),
transition(
'* => 1',
[
animate(1000, style({opacity: 0})),
query('.b-inner', [animate(1000, style({opacity: 0}))]),
]),
]),
trigger(
'c',
[
transition('* => 1', [
animate(1000, style({ opacity: 0 }))
]),
transition('* => 1', [animate(1000, style({opacity: 0}))]),
]),
]
})
@ -137,7 +135,7 @@ import {HostListener} from '../../src/metadata/directives';
query(
':animating',
[
style({ backgroundColor: 'blue' }),
style({backgroundColor: 'blue'}),
animate(1000, style({backgroundColor: 'red'})),
]),
]),
@ -145,26 +143,22 @@ import {HostListener} from '../../src/metadata/directives';
trigger(
'a',
[
transition('* => 1', [
animate(1000, style({ opacity: 0 }))
]),
transition('* => 1', [animate(1000, style({opacity: 0}))]),
]),
trigger(
'b',
[
transition('* => 1', [
animate(1000, style({ opacity: 0 })),
query('.b-inner', [
animate(1000, style({ opacity: 0 }))
]),
transition(
'* => 1',
[
animate(1000, style({opacity: 0})),
query('.b-inner', [animate(1000, style({opacity: 0}))]),
]),
]),
trigger(
'c',
[
transition('* => 1', [
animate(1000, style({ opacity: 0 }))
]),
transition('* => 1', [animate(1000, style({opacity: 0}))]),
]),
]
})
@ -361,8 +355,7 @@ import {HostListener} from '../../src/metadata/directives';
expect(players[2].element.classList.contains('e-4')).toBeTruthy();
});
it('should be able to query all actively queued animation triggers via `@*:animating`',
() => {
it('should be able to query all actively queued animation triggers via `@*:animating`', () => {
@Component({
selector: 'ani-cmp',
template: `
@ -454,7 +447,8 @@ import {HostListener} from '../../src/metadata/directives';
expect(players.length).toEqual(1);
});
it('should collect styles for the same elements between queries', () => {
it(
'should collect styles for the same elements between queries', () => {
@Component({
selector: 'ani-cmp',
template: `
@ -546,12 +540,11 @@ import {HostListener} from '../../src/metadata/directives';
template: `
<div [@myAnimation]="exp"></div>
`,
animations: [trigger('myAnimation', [transition(
animations: [trigger(
'myAnimation',
[transition(
'* => go',
[
query(':self', style({opacity: '0.5'})),
animate(1000, style({opacity: '1'}))
])])]
[query(':self', style({opacity: '0.5'})), animate(1000, style({opacity: '1'}))])])]
})
class Cmp {
public exp: any;
@ -793,9 +786,7 @@ import {HostListener} from '../../src/metadata/directives';
expect(players.length).toEqual(1);
const player = players[0];
expect(player.keyframes).toEqual([
{height: '0px', offset: 0}, {height: '444px', offset: 1}
]);
expect(player.keyframes).toEqual([{height: '0px', offset: 0}, {height: '444px', offset: 1}]);
player.finish();
expect(player.element.style.height).toEqual('444px');
@ -903,7 +894,9 @@ import {HostListener} from '../../src/metadata/directives';
cmp.items = [0, 1, 2, 3, 4];
expect(() => { fixture.detectChanges(); }).toThrow();
expect(() => {
fixture.detectChanges();
}).toThrow();
const children = cmp.container.nativeElement.querySelectorAll('.child');
expect(children.length).toEqual(5);
@ -1049,7 +1042,7 @@ import {HostListener} from '../../src/metadata/directives';
})
class Cmp {
// TODO(issue/24571): remove '!'.
public items !: any[];
public items!: any[];
}
TestBed.configureTestingModule({declarations: [Cmp]});
@ -1071,7 +1064,7 @@ import {HostListener} from '../../src/metadata/directives';
expect(players.length).toEqual(5);
for (let i = 0; i < 5; i++) {
let player = players[i] !;
let player = players[i]!;
expect(player.keyframes).toEqual([
{opacity: '0', offset: 0},
{opacity: '1', offset: 1},
@ -1091,7 +1084,7 @@ import {HostListener} from '../../src/metadata/directives';
expect(players.length).toEqual(5);
for (let i = 0; i < 5; i++) {
let player = players[i] !;
let player = players[i]!;
expect(player.keyframes).toEqual([
{opacity: '1', offset: 0},
{opacity: '0', offset: 1},
@ -1130,7 +1123,7 @@ import {HostListener} from '../../src/metadata/directives';
class Cmp {
public exp: any;
// TODO(issue/24571): remove '!'.
public items !: any[];
public items!: any[];
}
TestBed.configureTestingModule({declarations: [Cmp]});
@ -1351,7 +1344,7 @@ import {HostListener} from '../../src/metadata/directives';
class Cmp {
public exp: any;
// TODO(issue/24571): remove '!'.
public items !: any[];
public items!: any[];
}
TestBed.configureTestingModule({declarations: [Cmp]});
@ -1369,7 +1362,9 @@ import {HostListener} from '../../src/metadata/directives';
expect(players.length).toEqual(5);
let count = 0;
players.forEach(p => { p.onDone(() => count++); });
players.forEach(p => {
p.onDone(() => count++);
});
expect(count).toEqual(0);
@ -1404,7 +1399,7 @@ import {HostListener} from '../../src/metadata/directives';
class Cmp {
public exp: any;
// TODO(issue/24571): remove '!'.
public items !: any[];
public items!: any[];
}
TestBed.configureTestingModule({declarations: [Cmp]});
@ -1422,7 +1417,9 @@ import {HostListener} from '../../src/metadata/directives';
expect(players.length).toEqual(5);
let count = 0;
players.forEach(p => { p.onDone(() => count++); });
players.forEach(p => {
p.onDone(() => count++);
});
expect(count).toEqual(0);
@ -1467,7 +1464,7 @@ import {HostListener} from '../../src/metadata/directives';
public exp1: any;
public exp2: any;
// TODO(issue/24571): remove '!'.
public items !: any[];
public items!: any[];
}
TestBed.configureTestingModule({declarations: [Cmp]});
@ -1485,7 +1482,9 @@ import {HostListener} from '../../src/metadata/directives';
expect(players.length).toEqual(5);
let count = 0;
players.forEach(p => { p.onDone(() => count++); });
players.forEach(p => {
p.onDone(() => count++);
});
resetLog();
@ -1500,7 +1499,9 @@ import {HostListener} from '../../src/metadata/directives';
players = getLog();
expect(players.length).toEqual(3);
players.forEach(p => { p.onDone(() => count++); });
players.forEach(p => {
p.onDone(() => count++);
});
cmp.exp1 = 'off';
fixture.detectChanges();
@ -1537,7 +1538,7 @@ import {HostListener} from '../../src/metadata/directives';
class Cmp {
public exp: any;
// TODO(issue/24571): remove '!'.
public items !: any[];
public items!: any[];
}
TestBed.configureTestingModule({declarations: [Cmp]});
@ -1555,7 +1556,9 @@ import {HostListener} from '../../src/metadata/directives';
expect(players.length).toEqual(5);
let count = 0;
players.forEach(p => { p.onDone(() => count++); });
players.forEach(p => {
p.onDone(() => count++);
});
expect(count).toEqual(0);
@ -1592,7 +1595,7 @@ import {HostListener} from '../../src/metadata/directives';
class Cmp {
public exp: any;
// TODO(issue/24571): remove '!'.
public items !: any[];
public items!: any[];
}
TestBed.configureTestingModule({declarations: [Cmp]});
@ -1631,8 +1634,7 @@ import {HostListener} from '../../src/metadata/directives';
'myAnimation',
[transition(
'* => on',
[query(
':enter', [style({opacity: 0}), animate(1000, style({opacity: 1}))])])])]
[query(':enter', [style({opacity: 0}), animate(1000, style({opacity: 1}))])])])]
})
class ParentCmp {
public exp: any;
@ -1680,8 +1682,7 @@ import {HostListener} from '../../src/metadata/directives';
`,
animations: [trigger(
'myAnimation',
[transition(
'* => on', [query(':leave', [animate(1000, style({opacity: 0}))])])])]
[transition('* => on', [query(':leave', [animate(1000, style({opacity: 0}))])])])]
})
class ParentCmp {
public exp: any;
@ -2018,9 +2019,9 @@ import {HostListener} from '../../src/metadata/directives';
</div>
`,
animations: [
trigger('child', [transition(
'a => z',
[style({opacity: 0}), animate(1000, style({opacity: 1}))])]),
trigger(
'child',
[transition('a => z', [style({opacity: 0}), animate(1000, style({opacity: 1}))])]),
trigger('parent', [transition(
'a => z',
[
@ -2076,28 +2077,19 @@ import {HostListener} from '../../src/metadata/directives';
</div>
`,
animations: [
trigger('w', [
transition('* => go', [
style({ width: 0 }),
animate(1800, style({ width: '100px' }))
])
]),
trigger('h', [
transition('* => go', [
style({ height: 0 }),
animate(1500, style({ height: '100px' }))
])
]),
trigger('parent', [
transition('* => go', [
style({ opacity: 0 }),
animate(1000, style({ opacity: 1 })),
query('.child', [
animateChild()
]),
animate(1000, style({ opacity: 0 }))
])
])
trigger(
'w',
[transition('* => go', [style({width: 0}), animate(1800, style({width: '100px'}))])]),
trigger(
'h', [transition(
'* => go', [style({height: 0}), animate(1500, style({height: '100px'}))])]),
trigger(
'parent', [transition(
'* => go',
[
style({opacity: 0}), animate(1000, style({opacity: 1})),
query('.child', [animateChild()]), animate(1000, style({opacity: 0}))
])])
]
})
class Cmp {
@ -2141,9 +2133,9 @@ import {HostListener} from '../../src/metadata/directives';
</div>
`,
animations: [
trigger('child', [transition(
'* => go',
[style({width: 0}), animate(1800, style({width: '100px'}))])]),
trigger(
'child',
[transition('* => go', [style({width: 0}), animate(1800, style({width: '100px'}))])]),
trigger('parent', [transition(
'* => go',
[
@ -2200,9 +2192,9 @@ import {HostListener} from '../../src/metadata/directives';
style({opacity: 0}), animate(1000, style({opacity: 1})),
query('.child', animateChild())
])]),
trigger('child', [transition(
'* => go',
[style({opacity: 0}), animate(1800, style({opacity: 1}))])])
trigger(
'child',
[transition('* => go', [style({opacity: 0}), animate(1800, style({opacity: 1}))])])
]
})
class Cmp {
@ -2434,7 +2426,7 @@ import {HostListener} from '../../src/metadata/directives';
const players = getLog();
expect(players.length).toEqual(1);
const element = players[0] !.element;
const element = players[0]!.element;
expect(element.innerText.trim()).toMatch(/this\s+child/mg);
}));
@ -2477,7 +2469,7 @@ import {HostListener} from '../../src/metadata/directives';
})
class Cmp {
// TODO(issue/24571): remove '!'.
public exp !: boolean;
public exp!: boolean;
}
TestBed.configureTestingModule({declarations: [Cmp]});
@ -2496,8 +2488,8 @@ import {HostListener} from '../../src/metadata/directives';
expect(players.length).toEqual(2);
const [p1, p2] = players;
expect(p1.element.classList.contains('a'));
expect(p2.element.classList.contains('d'));
expect(p1.element.classList.contains('a')).toBeTrue();
expect(p2.element.classList.contains('d')).toBeTrue();
cmp.exp = false;
fixture.detectChanges();
@ -2508,26 +2500,24 @@ import {HostListener} from '../../src/metadata/directives';
expect(players.length).toEqual(2);
const [p3, p4] = players;
expect(p3.element.classList.contains('a'));
expect(p4.element.classList.contains('d'));
expect(p3.element.classList.contains('a')).toBeTrue();
expect(p4.element.classList.contains('d')).toBeTrue();
});
it('should collect multiple root levels of :enter and :leave nodes', () => {
@Component({
selector: 'ani-cmp',
animations: [
trigger('pageAnimation', [
animations: [trigger(
'pageAnimation',
[
transition(':enter', []),
transition('* => *', [
query(':leave', [
animate('1s', style({ opacity: 0 }))
], { optional: true }),
query(':enter', [
animate('1s', style({ opacity: 1 }))
], { optional: true })
transition(
'* => *',
[
query(':leave', [animate('1s', style({opacity: 0}))], {optional: true}),
query(':enter', [animate('1s', style({opacity: 1}))], {optional: true})
])
])
],
])],
template: `
<div [@pageAnimation]="status">
<header>
@ -2657,7 +2647,7 @@ import {HostListener} from '../../src/metadata/directives';
})
class Cmp {
// TODO(issue/24571): remove '!'.
public exp !: boolean;
public exp!: boolean;
public log: string[] = [];
callback(event: any) {
this.log.push(event.element.getAttribute('data-name') + '-' + event.phaseName);
@ -2679,8 +2669,7 @@ import {HostListener} from '../../src/metadata/directives';
fixture.detectChanges();
flushMicrotasks();
expect(cmp.log).toEqual([
'c1-start', 'c1-done', 'c2-start', 'c2-done', 'p-start', 'c3-start', 'c3-done',
'p-done'
'c1-start', 'c1-done', 'c2-start', 'c2-done', 'p-start', 'c3-start', 'c3-done', 'p-done'
]);
}));
@ -2764,7 +2753,9 @@ import {HostListener} from '../../src/metadata/directives';
public log: string[] = [];
public remove = false;
track(event: any) { this.log.push(`${event.triggerName}-${event.phaseName}`); }
track(event: any) {
this.log.push(`${event.triggerName}-${event.phaseName}`);
}
}
@Component({
@ -2790,7 +2781,9 @@ import {HostListener} from '../../src/metadata/directives';
class ChildCmp {
public exp: any;
public log: string[] = [];
track(event: any) { this.log.push(`${event.triggerName}-${event.phaseName}`); }
track(event: any) {
this.log.push(`${event.triggerName}-${event.phaseName}`);
}
}
TestBed.configureTestingModule({declarations: [ParentCmp, ChildCmp]});
@ -2893,7 +2886,9 @@ import {HostListener} from '../../src/metadata/directives';
public child2Exp = '';
public log: string[] = [];
track(event: any) { this.log.push(`${event.triggerName}-${event.phaseName}`); }
track(event: any) {
this.log.push(`${event.triggerName}-${event.phaseName}`);
}
}
TestBed.configureTestingModule({declarations: [Cmp]});
@ -2968,8 +2963,8 @@ import {HostListener} from '../../src/metadata/directives';
engine.flush();
expect(engine.players.length).toEqual(1); // child player, parent cover, parent player
const groupPlayer = (engine.players[0] as TransitionAnimationPlayer)
.getRealPlayer() as AnimationGroupPlayer;
const groupPlayer = (engine.players[0] as TransitionAnimationPlayer).getRealPlayer() as
AnimationGroupPlayer;
const childPlayer = groupPlayer.players.find(player => {
if (player instanceof MockAnimationPlayer) {
return matchesElement(player.element, '.child');
@ -3248,9 +3243,9 @@ import {HostListener} from '../../src/metadata/directives';
const [p1, p2] = players;
expect(p1.duration).toEqual(750);
expect(p1.element.classList.contains('header'));
expect(p1.element.classList.contains('header')).toBeTrue();
expect(p2.duration).toEqual(250);
expect(p2.element.classList.contains('footer'));
expect(p2.element.classList.contains('footer')).toBeTrue();
});
it('should allow a parent animation to query and animate sub animations that are in a disabled region',
@ -3303,13 +3298,13 @@ import {HostListener} from '../../src/metadata/directives';
const [p1, p2] = players;
expect(p1.duration).toEqual(500);
expect(p1.element.classList.contains('child'));
expect(p1.element.classList.contains('child')).toBeTrue();
expect(p2.duration).toEqual(1000);
expect(p2.element.classList.contains('parent'));
});
expect(p2.element.classList.contains('parent')).toBeTrue();
});
});
});
});
})();
function cancelAllPlayers(players: AnimationPlayer[]) {

View File

@ -9,7 +9,7 @@
import {ResourceLoader, UrlResolver} from '@angular/compiler';
import {MockResourceLoader} from '@angular/compiler/testing';
import {AfterContentChecked, AfterContentInit, AfterViewChecked, AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, DebugElement, Directive, DoCheck, EventEmitter, HostBinding, Inject, Injectable, Input, OnChanges, OnDestroy, OnInit, Output, Pipe, PipeTransform, Provider, RendererFactory2, RendererType2, SimpleChange, SimpleChanges, TemplateRef, Type, ViewChild, ViewContainerRef, WrappedValue} from '@angular/core';
import {ComponentFixture, TestBed, fakeAsync} from '@angular/core/testing';
import {ComponentFixture, fakeAsync, TestBed} from '@angular/core/testing';
import {By} from '@angular/platform-browser/src/dom/debug/by';
import {isTextNode} from '@angular/platform-browser/testing/src/browser_util';
import {expect} from '@angular/platform-browser/testing/src/matchers';
@ -26,55 +26,53 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
(function() {
let renderLog: RenderLog;
let directiveLog: DirectiveLog;
let renderLog: RenderLog;
let directiveLog: DirectiveLog;
function createCompFixture<T>(template: string): ComponentFixture<TestComponent>;
function createCompFixture<T>(template: string, compType: Type<T>): ComponentFixture<T>;
function createCompFixture<T>(
function createCompFixture<T>(template: string): ComponentFixture<TestComponent>;
function createCompFixture<T>(template: string, compType: Type<T>): ComponentFixture<T>;
function createCompFixture<T>(
template: string, compType: Type<T> = <any>TestComponent): ComponentFixture<T> {
TestBed.overrideComponent(compType, {set: new Component({template})});
initHelpers();
return TestBed.createComponent(compType);
}
}
function initHelpers(): void {
function initHelpers(): void {
renderLog = TestBed.inject(RenderLog);
directiveLog = TestBed.inject(DirectiveLog);
patchLoggingRenderer2(TestBed.inject(RendererFactory2), renderLog);
}
}
function queryDirs(el: DebugElement, dirType: Type<any>): any {
function queryDirs(el: DebugElement, dirType: Type<any>): any {
const nodes = el.queryAllNodes(By.directive(dirType));
return nodes.map(node => node.injector.get(dirType));
}
}
function _bindSimpleProp<T>(bindAttr: string): ComponentFixture<TestComponent>;
function _bindSimpleProp<T>(bindAttr: string, compType: Type<T>): ComponentFixture<T>;
function _bindSimpleProp<T>(
function _bindSimpleProp<T>(bindAttr: string): ComponentFixture<TestComponent>;
function _bindSimpleProp<T>(bindAttr: string, compType: Type<T>): ComponentFixture<T>;
function _bindSimpleProp<T>(
bindAttr: string, compType: Type<T> = <any>TestComponent): ComponentFixture<T> {
const template = `<div ${bindAttr}></div>`;
return createCompFixture(template, compType);
}
}
function _bindSimpleValue(expression: any): ComponentFixture<TestComponent>;
function _bindSimpleValue<T>(expression: any, compType: Type<T>): ComponentFixture<T>;
function _bindSimpleValue<T>(
function _bindSimpleValue(expression: any): ComponentFixture<TestComponent>;
function _bindSimpleValue<T>(expression: any, compType: Type<T>): ComponentFixture<T>;
function _bindSimpleValue<T>(
expression: any, compType: Type<T> = <any>TestComponent): ComponentFixture<T> {
return _bindSimpleProp(`[id]='${expression}'`, compType);
}
}
function _bindAndCheckSimpleValue(
expression: any, compType: Type<any> = TestComponent): string[] {
function _bindAndCheckSimpleValue(expression: any, compType: Type<any> = TestComponent): string[] {
const ctx = _bindSimpleValue(expression, compType);
ctx.detectChanges(false);
return renderLog.log;
}
describe(`ChangeDetection`, () => {
}
describe(`ChangeDetection`, () => {
beforeEach(() => {
TestBed.configureCompiler({providers: TEST_COMPILER_PROVIDERS});
TestBed.configureTestingModule({
@ -112,79 +110,101 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
});
describe('expressions', () => {
it('should support literals',
fakeAsync(() => { expect(_bindAndCheckSimpleValue(10)).toEqual(['id=10']); }));
it('should support literals', fakeAsync(() => {
expect(_bindAndCheckSimpleValue(10)).toEqual(['id=10']);
}));
it('should strip quotes from literals',
fakeAsync(() => { expect(_bindAndCheckSimpleValue('"str"')).toEqual(['id=str']); }));
it('should strip quotes from literals', fakeAsync(() => {
expect(_bindAndCheckSimpleValue('"str"')).toEqual(['id=str']);
}));
it('should support newlines in literals',
fakeAsync(() => { expect(_bindAndCheckSimpleValue('"a\n\nb"')).toEqual(['id=a\n\nb']); }));
it('should support newlines in literals', fakeAsync(() => {
expect(_bindAndCheckSimpleValue('"a\n\nb"')).toEqual(['id=a\n\nb']);
}));
it('should support + operations',
fakeAsync(() => { expect(_bindAndCheckSimpleValue('10 + 2')).toEqual(['id=12']); }));
it('should support + operations', fakeAsync(() => {
expect(_bindAndCheckSimpleValue('10 + 2')).toEqual(['id=12']);
}));
it('should support - operations',
fakeAsync(() => { expect(_bindAndCheckSimpleValue('10 - 2')).toEqual(['id=8']); }));
it('should support - operations', fakeAsync(() => {
expect(_bindAndCheckSimpleValue('10 - 2')).toEqual(['id=8']);
}));
it('should support * operations',
fakeAsync(() => { expect(_bindAndCheckSimpleValue('10 * 2')).toEqual(['id=20']); }));
it('should support * operations', fakeAsync(() => {
expect(_bindAndCheckSimpleValue('10 * 2')).toEqual(['id=20']);
}));
it('should support / operations', fakeAsync(() => {
expect(_bindAndCheckSimpleValue('10 / 2')).toEqual([`id=${5.0}`]);
})); // dart exp=5.0, js exp=5
it('should support % operations',
fakeAsync(() => { expect(_bindAndCheckSimpleValue('11 % 2')).toEqual(['id=1']); }));
it('should support % operations', fakeAsync(() => {
expect(_bindAndCheckSimpleValue('11 % 2')).toEqual(['id=1']);
}));
it('should support == operations on identical',
fakeAsync(() => { expect(_bindAndCheckSimpleValue('1 == 1')).toEqual(['id=true']); }));
it('should support == operations on identical', fakeAsync(() => {
expect(_bindAndCheckSimpleValue('1 == 1')).toEqual(['id=true']);
}));
it('should support != operations',
fakeAsync(() => { expect(_bindAndCheckSimpleValue('1 != 1')).toEqual(['id=false']); }));
it('should support != operations', fakeAsync(() => {
expect(_bindAndCheckSimpleValue('1 != 1')).toEqual(['id=false']);
}));
it('should support == operations on coerceible',
fakeAsync(() => { expect(_bindAndCheckSimpleValue('1 == true')).toEqual([`id=true`]); }));
it('should support == operations on coerceible', fakeAsync(() => {
expect(_bindAndCheckSimpleValue('1 == true')).toEqual([`id=true`]);
}));
it('should support === operations on identical',
fakeAsync(() => { expect(_bindAndCheckSimpleValue('1 === 1')).toEqual(['id=true']); }));
it('should support === operations on identical', fakeAsync(() => {
expect(_bindAndCheckSimpleValue('1 === 1')).toEqual(['id=true']);
}));
it('should support !== operations',
fakeAsync(() => { expect(_bindAndCheckSimpleValue('1 !== 1')).toEqual(['id=false']); }));
it('should support !== operations', fakeAsync(() => {
expect(_bindAndCheckSimpleValue('1 !== 1')).toEqual(['id=false']);
}));
it('should support === operations on coerceible', fakeAsync(() => {
expect(_bindAndCheckSimpleValue('1 === true')).toEqual(['id=false']);
}));
it('should support true < operations',
fakeAsync(() => { expect(_bindAndCheckSimpleValue('1 < 2')).toEqual(['id=true']); }));
it('should support true < operations', fakeAsync(() => {
expect(_bindAndCheckSimpleValue('1 < 2')).toEqual(['id=true']);
}));
it('should support false < operations',
fakeAsync(() => { expect(_bindAndCheckSimpleValue('2 < 1')).toEqual(['id=false']); }));
it('should support false < operations', fakeAsync(() => {
expect(_bindAndCheckSimpleValue('2 < 1')).toEqual(['id=false']);
}));
it('should support false > operations',
fakeAsync(() => { expect(_bindAndCheckSimpleValue('1 > 2')).toEqual(['id=false']); }));
it('should support false > operations', fakeAsync(() => {
expect(_bindAndCheckSimpleValue('1 > 2')).toEqual(['id=false']);
}));
it('should support true > operations',
fakeAsync(() => { expect(_bindAndCheckSimpleValue('2 > 1')).toEqual(['id=true']); }));
it('should support true > operations', fakeAsync(() => {
expect(_bindAndCheckSimpleValue('2 > 1')).toEqual(['id=true']);
}));
it('should support true <= operations',
fakeAsync(() => { expect(_bindAndCheckSimpleValue('1 <= 2')).toEqual(['id=true']); }));
it('should support true <= operations', fakeAsync(() => {
expect(_bindAndCheckSimpleValue('1 <= 2')).toEqual(['id=true']);
}));
it('should support equal <= operations',
fakeAsync(() => { expect(_bindAndCheckSimpleValue('2 <= 2')).toEqual(['id=true']); }));
it('should support equal <= operations', fakeAsync(() => {
expect(_bindAndCheckSimpleValue('2 <= 2')).toEqual(['id=true']);
}));
it('should support false <= operations',
fakeAsync(() => { expect(_bindAndCheckSimpleValue('2 <= 1')).toEqual(['id=false']); }));
it('should support false <= operations', fakeAsync(() => {
expect(_bindAndCheckSimpleValue('2 <= 1')).toEqual(['id=false']);
}));
it('should support true >= operations',
fakeAsync(() => { expect(_bindAndCheckSimpleValue('2 >= 1')).toEqual(['id=true']); }));
it('should support true >= operations', fakeAsync(() => {
expect(_bindAndCheckSimpleValue('2 >= 1')).toEqual(['id=true']);
}));
it('should support equal >= operations',
fakeAsync(() => { expect(_bindAndCheckSimpleValue('2 >= 2')).toEqual(['id=true']); }));
it('should support equal >= operations', fakeAsync(() => {
expect(_bindAndCheckSimpleValue('2 >= 2')).toEqual(['id=true']);
}));
it('should support false >= operations',
fakeAsync(() => { expect(_bindAndCheckSimpleValue('1 >= 2')).toEqual(['id=false']); }));
it('should support false >= operations', fakeAsync(() => {
expect(_bindAndCheckSimpleValue('1 >= 2')).toEqual(['id=false']);
}));
it('should support true && operations', fakeAsync(() => {
expect(_bindAndCheckSimpleValue('true && true')).toEqual(['id=true']);
@ -202,17 +222,21 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
expect(_bindAndCheckSimpleValue('false || false')).toEqual(['id=false']);
}));
it('should support negate',
fakeAsync(() => { expect(_bindAndCheckSimpleValue('!true')).toEqual(['id=false']); }));
it('should support negate', fakeAsync(() => {
expect(_bindAndCheckSimpleValue('!true')).toEqual(['id=false']);
}));
it('should support double negate',
fakeAsync(() => { expect(_bindAndCheckSimpleValue('!!true')).toEqual(['id=true']); }));
it('should support double negate', fakeAsync(() => {
expect(_bindAndCheckSimpleValue('!!true')).toEqual(['id=true']);
}));
it('should support true conditionals',
fakeAsync(() => { expect(_bindAndCheckSimpleValue('1 < 2 ? 1 : 2')).toEqual(['id=1']); }));
it('should support true conditionals', fakeAsync(() => {
expect(_bindAndCheckSimpleValue('1 < 2 ? 1 : 2')).toEqual(['id=1']);
}));
it('should support false conditionals',
fakeAsync(() => { expect(_bindAndCheckSimpleValue('1 > 2 ? 1 : 2')).toEqual(['id=2']); }));
it('should support false conditionals', fakeAsync(() => {
expect(_bindAndCheckSimpleValue('1 > 2 ? 1 : 2')).toEqual(['id=2']);
}));
it('should support keyed access to a list item', fakeAsync(() => {
expect(_bindAndCheckSimpleValue('["foo", "bar"][0]')).toEqual(['id=foo']);
@ -245,14 +269,14 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
describe('safe navigation operator', () => {
it('should support reading properties of nulls', fakeAsync(() => {
const ctx = _bindSimpleValue('address?.city', Person);
ctx.componentInstance.address = null !;
ctx.componentInstance.address = null!;
ctx.detectChanges(false);
expect(renderLog.log).toEqual(['id=null']);
}));
it('should support calling methods on nulls', fakeAsync(() => {
const ctx = _bindSimpleValue('address?.toString()', Person);
ctx.componentInstance.address = null !;
ctx.componentInstance.address = null!;
ctx.detectChanges(false);
expect(renderLog.log).toEqual(['id=null']);
}));
@ -273,7 +297,7 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
it('should support short-circuting safe navigation', fakeAsync(() => {
const ctx = _bindSimpleValue('value?.address.city', PersonHolder);
ctx.componentInstance.value = null !;
ctx.componentInstance.value = null!;
ctx.detectChanges(false);
expect(renderLog.log).toEqual(['id=null']);
}));
@ -301,7 +325,7 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
expect(() => {
const ctx = _bindSimpleValue('value?.address.city', PersonHolder);
const person = new Person();
person.address = null !;
person.address = null!;
ctx.componentInstance.value = person;
ctx.detectChanges(false);
}).toThrow();
@ -444,8 +468,9 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
expect(renderLog.log).toEqual(['id=BA']);
}));
it('should escape values in literals that indicate interpolation',
fakeAsync(() => { expect(_bindAndCheckSimpleValue('"$"')).toEqual(['id=$']); }));
it('should escape values in literals that indicate interpolation', fakeAsync(() => {
expect(_bindAndCheckSimpleValue('"$"')).toEqual(['id=$']);
}));
it('should read locals', fakeAsync(() => {
const ctx = createCompFixture(
@ -479,8 +504,7 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
}));
it('should support calling pure pipes with different number of arguments', fakeAsync(() => {
const ctx =
_bindSimpleValue('name | multiArgPipe:"a":"b" | multiArgPipe:0:1:2', Person);
const ctx = _bindSimpleValue('name | multiArgPipe:"a":"b" | multiArgPipe:0:1:2', Person);
ctx.componentInstance.name = 'value';
ctx.detectChanges(false);
expect(renderLog.loggedValues).toEqual(['value a b default 0 1 2']);
@ -537,7 +561,7 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
it('should call pure pipes only if the arguments change', fakeAsync(() => {
const ctx = _bindSimpleValue('name | countingPipe', Person);
// change from undefined -> null
ctx.componentInstance.name = null !;
ctx.componentInstance.name = null!;
ctx.detectChanges(false);
expect(renderLog.loggedValues).toEqual(['null state:0']);
ctx.detectChanges(false);
@ -553,14 +577,9 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
// change from some value -> some other value
ctx.componentInstance.name = 'bart';
ctx.detectChanges(false);
expect(renderLog.loggedValues).toEqual([
'null state:0', 'bob state:1', 'bart state:2'
]);
expect(renderLog.loggedValues).toEqual(['null state:0', 'bob state:1', 'bart state:2']);
ctx.detectChanges(false);
expect(renderLog.loggedValues).toEqual([
'null state:0', 'bob state:1', 'bart state:2'
]);
expect(renderLog.loggedValues).toEqual(['null state:0', 'bob state:1', 'bart state:2']);
}));
modifiedInIvy('Pure pipes are instantiated differently in view engine and ivy')
@ -662,7 +681,9 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
}));
it('should throw when trying to assign to a local', fakeAsync(() => {
expect(() => { _bindSimpleProp('(event)="$event=1"'); })
expect(() => {
_bindSimpleProp('(event)="$event=1"');
})
.toThrowError(new RegExp(
'Cannot assign value (.*) to template variable (.*). Template variables are read-only.'));
}));
@ -675,7 +696,6 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
expect(ctx.componentInstance.a).toEqual(1);
}));
});
});
describe('RendererFactory', () => {
@ -683,8 +703,11 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
fakeAsync(() => {
const ctx = createCompFixture('<div testDirective [a]="42"></div>');
const rf = TestBed.inject(RendererFactory2);
spyOn(rf, 'begin');
spyOn(rf, 'end');
// TODO: @JiaLiPassion, need to wait @types/jasmine to fix the
// optional method infer issue.
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/43486
spyOn(rf as any, 'begin');
spyOn(rf as any, 'end');
expect(rf.begin).not.toHaveBeenCalled();
expect(rf.end).not.toHaveBeenCalled();
@ -822,8 +845,7 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
]);
}));
it('should be called on every detectChanges run, except for checkNoChanges',
fakeAsync(() => {
it('should be called on every detectChanges run, except for checkNoChanges', fakeAsync(() => {
const ctx = createCompFixture('<div testDirective="dir"></div>');
ctx.detectChanges(false);
@ -862,9 +884,7 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
ctx.detectChanges(false);
expect(directiveLog.filter(['ngAfterContentInit'])).toEqual([
'dir.ngAfterContentInit'
]);
expect(directiveLog.filter(['ngAfterContentInit'])).toEqual(['dir.ngAfterContentInit']);
// reset directives
directiveLog.clear();
@ -893,9 +913,7 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
}
expect(errored).toBe(true);
expect(directiveLog.filter(['ngAfterContentInit'])).toEqual([
'dir.ngAfterContentInit'
]);
expect(directiveLog.filter(['ngAfterContentInit'])).toEqual(['dir.ngAfterContentInit']);
directiveLog.clear();
// Second change detection also fails, but this time ngAfterContentInit should not be
@ -923,8 +941,7 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
]);
}));
it('should be called on every detectChanges run, except for checkNoChanges',
fakeAsync(() => {
it('should be called on every detectChanges run, except for checkNoChanges', fakeAsync(() => {
const ctx = createCompFixture('<div testDirective="dir"></div>');
ctx.detectChanges(false);
@ -1036,15 +1053,12 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
]);
}));
it('should be called on every detectChanges run, except for checkNoChanges',
fakeAsync(() => {
it('should be called on every detectChanges run, except for checkNoChanges', fakeAsync(() => {
const ctx = createCompFixture('<div testDirective="dir"></div>');
ctx.detectChanges(false);
expect(directiveLog.filter(['ngAfterViewChecked'])).toEqual([
'dir.ngAfterViewChecked'
]);
expect(directiveLog.filter(['ngAfterViewChecked'])).toEqual(['dir.ngAfterViewChecked']);
// reset directives
directiveLog.clear();
@ -1057,9 +1071,7 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
// re-verify that changes are still detected
ctx.detectChanges(false);
expect(directiveLog.filter(['ngAfterViewChecked'])).toEqual([
'dir.ngAfterViewChecked'
]);
expect(directiveLog.filter(['ngAfterViewChecked'])).toEqual(['dir.ngAfterViewChecked']);
}));
it('should be called in reverse order so the child is always notified before the parent',
@ -1134,14 +1146,11 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
ctx.detectChanges(false);
ctx.destroy();
expect(directiveLog.filter(['ngOnDestroy'])).toEqual([
'pipeWithOnDestroy.ngOnDestroy'
]);
expect(directiveLog.filter(['ngOnDestroy'])).toEqual(['pipeWithOnDestroy.ngOnDestroy']);
}));
it('should call ngOnDestroy on an injectable class', fakeAsync(() => {
TestBed.overrideDirective(
TestDirective, {set: {providers: [InjectableWithLifecycle]}});
TestBed.overrideDirective(TestDirective, {set: {providers: [InjectableWithLifecycle]}});
const ctx = createCompFixture('<div testDirective="dir"></div>', TestComponent);
@ -1300,7 +1309,6 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
ctx.detectChanges();
expect(renderLog.log).toEqual(['{{hello}}']);
}));
it('Reattaches in the original cd mode', fakeAsync(() => {
@ -1319,14 +1327,13 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
ctx.detectChanges();
expect(cmp.renderCount).toBe(count);
}));
});
describe('multi directive order', () => {
modifiedInIvy('order of bindings to directive inputs is different in ivy')
.it('should follow the DI order for the same element', fakeAsync(() => {
const ctx = createCompFixture(
'<div orderCheck2="2" orderCheck0="0" orderCheck1="1"></div>');
const ctx =
createCompFixture('<div orderCheck2="2" orderCheck0="0" orderCheck1="1"></div>');
ctx.detectChanges(false);
ctx.destroy();
@ -1356,7 +1363,7 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
class Comp {
name = 'Tom';
// TODO(issue/24571): remove '!'.
@ViewChild('vc', {read: ViewContainerRef, static: true}) vc !: ViewContainerRef;
@ViewChild('vc', {read: ViewContainerRef, static: true}) vc!: ViewContainerRef;
// TODO(issue/24571): remove '!'.
@ViewChild(TemplateRef, {static: true}) template !: TemplateRef<any>;
}
@ -1378,8 +1385,7 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
@Directive({selector: '[i]'})
class DummyDirective {
@Input()
i: any;
@Input() i: any;
}
@Component({
@ -1389,7 +1395,9 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
})
class MainComp {
constructor(public cdRef: ChangeDetectorRef) {}
log(id: string) { log.push(`main-${id}`); }
log(id: string) {
log.push(`main-${id}`);
}
}
@Component({
@ -1399,11 +1407,12 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
})
class OuterComp {
// TODO(issue/24571): remove '!'.
@ContentChild(TemplateRef, {static: true})
tpl !: TemplateRef<any>;
@ContentChild(TemplateRef, {static: true}) tpl!: TemplateRef<any>;
constructor(public cdRef: ChangeDetectorRef) {}
log(id: string) { log.push(`outer-${id}`); }
log(id: string) {
log.push(`outer-${id}`);
}
}
@Component({
@ -1413,15 +1422,15 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
})
class InnerComp {
// TODO(issue/24571): remove '!'.
@ContentChild(TemplateRef, {static: true})
tpl !: TemplateRef<any>;
@ContentChild(TemplateRef, {static: true}) tpl!: TemplateRef<any>;
// TODO(issue/24571): remove '!'.
@Input()
outerTpl !: TemplateRef<any>;
@Input() outerTpl!: TemplateRef<any>;
constructor(public cdRef: ChangeDetectorRef) {}
log(id: string) { log.push(`inner-${id}`); }
log(id: string) {
log.push(`inner-${id}`);
}
}
let ctx: ComponentFixture<MainComp>;
@ -1442,13 +1451,11 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
it('should dirty check projected views in regular order', () => {
ctx.detectChanges(false);
expect(log).toEqual(
['main-start', 'outer-start', 'inner-start', 'main-tpl', 'outer-tpl']);
expect(log).toEqual(['main-start', 'outer-start', 'inner-start', 'main-tpl', 'outer-tpl']);
log = [];
ctx.detectChanges(false);
expect(log).toEqual(
['main-start', 'outer-start', 'inner-start', 'main-tpl', 'outer-tpl']);
expect(log).toEqual(['main-start', 'outer-start', 'inner-start', 'main-tpl', 'outer-tpl']);
});
it('should not dirty check projected views if neither the declaration nor the insertion place is dirty checked',
@ -1524,8 +1531,7 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
@Directive({selector: '[someDir]'})
class SomeDir {
@HostBinding('class.foo')
fooClass = true;
@HostBinding('class.foo') fooClass = true;
}
const ctx =
@ -1542,8 +1548,12 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
describe('lifecycle asserts', () => {
let logged: string[];
function log(value: string) { logged.push(value); }
function clearLog() { logged = []; }
function log(value: string) {
logged.push(value);
}
function clearLog() {
logged = [];
}
function expectOnceAndOnlyOnce(log: string) {
expect(logged.indexOf(log) >= 0)
@ -1552,7 +1562,9 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
.toBeTruthy(`'${log}' logged more than once. Log was ${JSON.stringify(logged)}`);
}
beforeEach(() => { clearLog(); });
beforeEach(() => {
clearLog();
});
enum LifetimeMethods {
None = 0,
@ -1586,16 +1598,26 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
private thrown = LifetimeMethods.None;
// TODO(issue/24571): remove '!'.
@Input() inp !: boolean;
@Input() inp!: boolean;
@Output() outp = new EventEmitter<any>();
constructor() {}
ngDoCheck() { this.check(LifetimeMethods.ngDoCheck); }
ngOnInit() { this.check(LifetimeMethods.ngOnInit); }
ngOnChanges() { this.check(LifetimeMethods.ngOnChanges); }
ngAfterViewInit() { this.check(LifetimeMethods.ngAfterViewInit); }
ngAfterContentInit() { this.check(LifetimeMethods.ngAfterContentInit); }
ngDoCheck() {
this.check(LifetimeMethods.ngDoCheck);
}
ngOnInit() {
this.check(LifetimeMethods.ngOnInit);
}
ngOnChanges() {
this.check(LifetimeMethods.ngOnChanges);
}
ngAfterViewInit() {
this.check(LifetimeMethods.ngAfterViewInit);
}
ngAfterContentInit() {
this.check(LifetimeMethods.ngAfterContentInit);
}
private check(method: LifetimeMethods) {
log(`MyChild::${LifetimeMethods[method]}()`);
@ -1623,10 +1645,18 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
})
class MyComponent {
constructor(private changeDetectionRef: ChangeDetectorRef) {}
ngDoCheck() { this.check(LifetimeMethods.ngDoCheck); }
ngOnInit() { this.check(LifetimeMethods.ngOnInit); }
ngAfterViewInit() { this.check(LifetimeMethods.ngAfterViewInit); }
ngAfterContentInit() { this.check(LifetimeMethods.ngAfterContentInit); }
ngDoCheck() {
this.check(LifetimeMethods.ngDoCheck);
}
ngOnInit() {
this.check(LifetimeMethods.ngOnInit);
}
ngAfterViewInit() {
this.check(LifetimeMethods.ngAfterViewInit);
}
ngAfterContentInit() {
this.check(LifetimeMethods.ngAfterContentInit);
}
onOutp() {
log('<RECURSION START>');
this.changeDetectionRef.detectChanges();
@ -1668,14 +1698,16 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
}
forEachMethod(LifetimeMethods.InitMethodsAndChanges, method => {
it(`should ensure that init hooks are called once an only once with recursion in ${LifetimeMethods[method]} `,
it(`should ensure that init hooks are called once an only once with recursion in ${
LifetimeMethods[method]} `,
() => {
// Ensure all the init methods are called once.
ensureOneInit({childRecursion: method, childThrows: LifetimeMethods.None});
});
});
forEachMethod(LifetimeMethods.All, method => {
it(`should ensure that init hooks are called once an only once with a throw in ${LifetimeMethods[method]} `,
it(`should ensure that init hooks are called once an only once with a throw in ${
LifetimeMethods[method]} `,
() => {
// Ensure all the init methods are called once.
// the first cycle throws but the next cycle should complete the inits.
@ -1684,7 +1716,7 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
});
});
});
});
});
})();
@Injectable()
@ -1748,7 +1780,9 @@ class DirectiveLog {
this.entries.push(new DirectiveLogEntry(directiveName, method));
}
clear() { this.entries = []; }
clear() {
this.entries = [];
}
filter(methods: string[]): string[] {
return this.entries.filter((entry) => methods.indexOf(entry.method) !== -1)
@ -1760,32 +1794,44 @@ class DirectiveLog {
@Pipe({name: 'countingPipe'})
class CountingPipe implements PipeTransform {
state: number = 0;
transform(value: any) { return `${value} state:${this.state++}`; }
transform(value: any) {
return `${value} state:${this.state++}`;
}
}
@Pipe({name: 'countingImpurePipe', pure: false})
class CountingImpurePipe implements PipeTransform {
state: number = 0;
transform(value: any) { return `${value} state:${this.state++}`; }
transform(value: any) {
return `${value} state:${this.state++}`;
}
}
@Pipe({name: 'pipeWithOnDestroy'})
class PipeWithOnDestroy implements PipeTransform, OnDestroy {
constructor(private directiveLog: DirectiveLog) {}
ngOnDestroy() { this.directiveLog.add('pipeWithOnDestroy', 'ngOnDestroy'); }
ngOnDestroy() {
this.directiveLog.add('pipeWithOnDestroy', 'ngOnDestroy');
}
transform(value: any): any { return null; }
transform(value: any): any {
return null;
}
}
@Pipe({name: 'identityPipe'})
class IdentityPipe implements PipeTransform {
transform(value: any) { return value; }
transform(value: any) {
return value;
}
}
@Pipe({name: 'wrappedPipe'})
class WrappedPipe implements PipeTransform {
transform(value: any) { return WrappedValue.wrap(value); }
transform(value: any) {
return WrappedValue.wrap(value);
}
}
@Pipe({name: 'multiArgPipe'})
@ -1854,7 +1900,9 @@ class Gh9882 implements AfterContentInit {
constructor(private _viewContainer: ViewContainerRef, private _templateRef: TemplateRef<Object>) {
}
ngAfterContentInit(): any { this._viewContainer.createEmbeddedView(this._templateRef); }
ngAfterContentInit(): any {
this._viewContainer.createEmbeddedView(this._templateRef);
}
}
@Directive({selector: '[testDirective]', exportAs: 'testDirective'})
@ -1863,21 +1911,25 @@ class TestDirective implements OnInit, DoCheck, OnChanges, AfterContentInit, Aft
@Input() a: any;
@Input() b: any;
// TODO(issue/24571): remove '!'.
changes !: SimpleChanges;
changes!: SimpleChanges;
event: any;
eventEmitter: EventEmitter<string> = new EventEmitter<string>();
// TODO(issue/24571): remove '!'.
@Input('testDirective') name !: string;
@Input('testDirective') name!: string;
// TODO(issue/24571): remove '!'.
@Input() throwOn !: string;
@Input() throwOn!: string;
constructor(public log: DirectiveLog) {}
onEvent(event: any) { this.event = event; }
onEvent(event: any) {
this.event = event;
}
ngDoCheck() { this.log.add(this.name, 'ngDoCheck'); }
ngDoCheck() {
this.log.add(this.name, 'ngDoCheck');
}
ngOnInit() {
this.log.add(this.name, 'ngOnInit');
@ -1935,20 +1987,24 @@ class InjectableWithLifecycle {
name = 'injectable';
constructor(public log: DirectiveLog) {}
ngOnDestroy() { this.log.add(this.name, 'ngOnDestroy'); }
ngOnDestroy() {
this.log.add(this.name, 'ngOnDestroy');
}
}
@Directive({selector: '[onDestroyDirective]'})
class OnDestroyDirective implements OnDestroy {
@Output('destroy') emitter = new EventEmitter<string>(false);
ngOnDestroy() { this.emitter.emit('destroyed'); }
ngOnDestroy() {
this.emitter.emit('destroyed');
}
}
@Directive({selector: '[orderCheck0]'})
class OrderCheckDirective0 {
// TODO(issue/24571): remove '!'.
private _name !: string;
private _name!: string;
@Input('orderCheck0')
set name(value: string) {
@ -1962,7 +2018,7 @@ class OrderCheckDirective0 {
@Directive({selector: '[orderCheck1]'})
class OrderCheckDirective1 {
// TODO(issue/24571): remove '!'.
private _name !: string;
private _name!: string;
@Input('orderCheck1')
set name(value: string) {
@ -1976,7 +2032,7 @@ class OrderCheckDirective1 {
@Directive({selector: '[orderCheck2]'})
class OrderCheckDirective2 {
// TODO(issue/24571): remove '!'.
private _name !: string;
private _name!: string;
@Input('orderCheck2')
set name(value: string) {
@ -2001,21 +2057,25 @@ class TestLocals {
@Component({selector: 'root', template: 'empty'})
class Person {
// TODO(issue/24571): remove '!'.
age !: number;
age!: number;
// TODO(issue/24571): remove '!'.
name !: string;
name!: string;
address: Address|null = null;
// TODO(issue/24571): remove '!'.
phones !: number[];
phones!: number[];
init(name: string, address: Address|null = null) {
this.name = name;
this.address = address;
}
sayHi(m: any): string { return `Hi, ${m}`; }
sayHi(m: any): string {
return `Hi, ${m}`;
}
passThrough(val: any): any { return val; }
passThrough(val: any): any {
return val;
}
toString(): string {
const address = this.address == null ? '' : ' address=' + this.address.toString();
@ -2040,11 +2100,17 @@ class Address {
return this._zipcode;
}
set city(v) { this._city = v; }
set city(v) {
this._city = v;
}
set zipcode(v) { this._zipcode = v; }
set zipcode(v) {
this._zipcode = v;
}
toString(): string { return this.city || '-'; }
toString(): string {
return this.city || '-';
}
}
@Component({selector: 'root', template: 'empty'})
@ -2061,14 +2127,16 @@ class TestData {
@Component({selector: 'root', template: 'empty'})
class TestDataWithGetter {
// TODO(issue/24571): remove '!'.
public fn !: Function;
public fn!: Function;
get a() { return this.fn(); }
get a() {
return this.fn();
}
}
class Holder<T> {
// TODO(issue/24571): remove '!'.
value !: T;
value!: T;
}
@Component({selector: 'root', template: 'empty'})

View File

@ -25,30 +25,45 @@ describe('global utils', () => {
describe('publishDefaultGlobalUtils', () => {
beforeEach(() => publishDefaultGlobalUtils());
it('should publish getComponent', () => { assertPublished('getComponent', getComponent); });
it('should publish getComponent', () => {
assertPublished('getComponent', getComponent);
});
it('should publish getContext', () => { assertPublished('getContext', getContext); });
it('should publish getContext', () => {
assertPublished('getContext', getContext);
});
it('should publish getListeners', () => { assertPublished('getListeners', getListeners); });
it('should publish getListeners', () => {
assertPublished('getListeners', getListeners);
});
it('should publish getOwningComponent',
() => { assertPublished('getOwningComponent', getOwningComponent); });
it('should publish getOwningComponent', () => {
assertPublished('getOwningComponent', getOwningComponent);
});
it('should publish getRootComponents',
() => { assertPublished('getRootComponents', getRootComponents); });
it('should publish getRootComponents', () => {
assertPublished('getRootComponents', getRootComponents);
});
it('should publish getDirectives', () => { assertPublished('getDirectives', getDirectives); });
it('should publish getDirectives', () => {
assertPublished('getDirectives', getDirectives);
});
it('should publish getHostComponent',
() => { assertPublished('getHostElement', getHostElement); });
it('should publish getHostComponent', () => {
assertPublished('getHostElement', getHostElement);
});
it('should publish getInjector', () => { assertPublished('getInjector', getInjector); });
it('should publish getInjector', () => {
assertPublished('getInjector', getInjector);
});
it('should publish applyChanges', () => { assertPublished('applyChanges', applyChanges); });
it('should publish applyChanges', () => {
assertPublished('applyChanges', applyChanges);
});
});
});
function assertPublished(name: string, value: {}) {
function assertPublished(name: string, value: Function) {
const w = global as any as GlobalDevModeContainer;
expect(w[GLOBAL_PUBLISH_EXPANDO_KEY][name]).toBe(value);
}

View File

@ -12,24 +12,21 @@ import {AttributeMarker, TAttributes, TNode, TNodeType} from '../../src/render3/
import {CssSelector, CssSelectorList, SelectorFlags} from '../../src/render3/interfaces/projection';
import {extractAttrsAndClassesFromSelector, getProjectAsAttrValue, isNodeMatchingSelector, isNodeMatchingSelectorList, stringifyCSSSelectorList} from '../../src/render3/node_selector_matcher';
function testLStaticData(tagName: string, attrs: TAttributes | null): TNode {
return createTNode(null !, null, TNodeType.Element, 0, tagName, attrs);
function testLStaticData(tagName: string, attrs: TAttributes|null): TNode {
return createTNode(null!, null, TNodeType.Element, 0, tagName, attrs);
}
describe('css selector matching', () => {
function isMatching(
tagName: string, attrsOrTNode: TAttributes | TNode | null, selector: CssSelector): boolean {
tagName: string, attrsOrTNode: TAttributes|TNode|null, selector: CssSelector): boolean {
const tNode = (!attrsOrTNode || Array.isArray(attrsOrTNode)) ?
createTNode(null !, null, TNodeType.Element, 0, tagName, attrsOrTNode as TAttributes) :
createTNode(null!, null, TNodeType.Element, 0, tagName, attrsOrTNode as TAttributes) :
(attrsOrTNode as TNode);
return isNodeMatchingSelector(tNode, selector, true);
}
describe('isNodeMatchingSimpleSelector', () => {
describe('element matching', () => {
it('should match element name only if names are the same', () => {
expect(isMatching('span', null, ['span']))
.toBeTruthy(`Selector 'span' should match <span>`);
@ -55,11 +52,9 @@ describe('css selector matching', () => {
});
describe('attributes matching', () => {
// TODO: do we need to differentiate no value and empty value? that is: title vs. title="" ?
it('should match single attribute without value', () => {
expect(isMatching('span', ['title', ''], [
'', 'title', ''
])).toBeTruthy(`Selector '[title]' should match <span title>`);
@ -81,10 +76,13 @@ describe('css selector matching', () => {
])).toBeFalsy(`Selector '[other]' should NOT match <span title="">'`);
});
it('should match namespaced attributes', () => {
// TODO: this case will not work, need more discussion
// https://github.com/angular/angular/pull/34625#discussion_r401791275
xit('should match namespaced attributes', () => {
expect(isMatching(
'span', [AttributeMarker.NamespaceURI, 'http://some/uri', 'title', 'name'],
['', 'title', '']));
['', 'title', '']))
.toBeTruthy();
});
it('should match selector with one attribute without value when element has several attributes',
@ -226,7 +224,6 @@ describe('css selector matching', () => {
});
describe('class matching', () => {
it('should match with a class selector when an element has multiple classes', () => {
expect(isMatching('span', ['class', 'foo bar'], [
'', SelectorFlags.CLASS, 'foo'
@ -326,7 +323,6 @@ describe('css selector matching', () => {
});
describe('negations', () => {
it('should match when negation part is null', () => {
expect(isMatching('span', null, ['span'])).toBeTruthy(`Selector 'span' should match <span>`);
});
@ -434,13 +430,11 @@ describe('css selector matching', () => {
expect(isMatching('div', ['name', 'name', 'title', '', 'class', 'foo bar'], selector))
.toBeFalsy();
});
});
describe('isNodeMatchingSelectorList', () => {
function isAnyMatching(
tagName: string, attrs: string[] | null, selector: CssSelectorList): boolean {
tagName: string, attrs: string[]|null, selector: CssSelectorList): boolean {
return isNodeMatchingSelectorList(testLStaticData(tagName, attrs), selector, false);
}
@ -466,16 +460,18 @@ describe('css selector matching', () => {
});
describe('reading the ngProjectAs attribute value', function() {
function testTNode(attrs: TAttributes | null) { return testLStaticData('tag', attrs); }
function testTNode(attrs: TAttributes|null) {
return testLStaticData('tag', attrs);
}
it('should get ngProjectAs value if present', function() {
expect(getProjectAsAttrValue(testTNode([AttributeMarker.ProjectAs, ['tag', 'foo', 'bar']])))
.toEqual(['tag', 'foo', 'bar']);
});
it('should return null if there are no attributes',
function() { expect(getProjectAsAttrValue(testTNode(null))).toBe(null); });
it('should return null if there are no attributes', function() {
expect(getProjectAsAttrValue(testTNode(null))).toBe(null);
});
it('should return if ngProjectAs is not present', function() {
expect(getProjectAsAttrValue(testTNode(['foo', 'bar']))).toBe(null);
@ -484,15 +480,13 @@ describe('css selector matching', () => {
it('should not accidentally identify ngProjectAs in attribute values', function() {
expect(getProjectAsAttrValue(testTNode(['foo', AttributeMarker.ProjectAs]))).toBe(null);
});
});
});
describe('stringifyCSSSelectorList', () => {
it('should stringify selector with a tag name only',
() => { expect(stringifyCSSSelectorList([['button']])).toBe('button'); });
it('should stringify selector with a tag name only', () => {
expect(stringifyCSSSelectorList([['button']])).toBe('button');
});
it('should stringify selector with attributes', () => {
expect(stringifyCSSSelectorList([['', 'id', '']])).toBe('[id]');

View File

@ -15,7 +15,10 @@ describe('utils', () => {
let clearTimeoutSpy: jasmine.Spy;
beforeEach(() => {
setTimeoutSpy = spyOn(window, 'setTimeout').and.returnValue(42);
// TODO: @JiaLiPassion, need to wait @types/jasmine to fix the wrong return
// type infer issue.
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/43486
setTimeoutSpy = spyOn(window, 'setTimeout').and.returnValue(42 as any);
clearTimeoutSpy = spyOn(window, 'clearTimeout');
});
@ -81,8 +84,9 @@ describe('utils', () => {
expect(camelToDashCase('foo1Bar2Baz3Qux4')).toBe('foo1-bar2-baz3-qux4');
});
it('should keep existing dashes',
() => { expect(camelToDashCase('fooBar-baz-Qux')).toBe('foo-bar-baz--qux'); });
it('should keep existing dashes', () => {
expect(camelToDashCase('fooBar-baz-Qux')).toBe('foo-bar-baz--qux');
});
});
describe('createCustomEvent()', () => {
@ -97,7 +101,6 @@ describe('utils', () => {
expect(event.cancelable).toBe(false);
expect(event.detail).toEqual(value);
});
});
describe('isElement()', () => {
@ -129,7 +132,7 @@ describe('utils', () => {
it('should return true for functions', () => {
const obj = {foo: function() {}, bar: () => null, baz() {}};
const fns = [
function(){},
function() {},
() => null,
obj.foo,
obj.bar,
@ -180,7 +183,7 @@ describe('utils', () => {
</ul>
</div>
`;
li = div.querySelector('li') !;
li = div.querySelector('li')!;
});
it('should return whether the element matches the selector', () => {
@ -216,7 +219,9 @@ describe('utils', () => {
];
values.forEach((v1, i) => {
values.forEach((v2, j) => { expect(strictEquals(v1, v2)).toBe(i === j); });
values.forEach((v2, j) => {
expect(strictEquals(v1, v2)).toBe(i === j);
});
});
});

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Inject, ReflectiveInjector, forwardRef, resolveForwardRef} from '@angular/core';
import {forwardRef, Inject, ReflectiveInjector, resolveForwardRef} from '@angular/core';
{
describe('forwardRef examples', () => {
@ -26,7 +26,9 @@ import {Inject, ReflectiveInjector, forwardRef, resolveForwardRef} from '@angula
// Door attempts to inject Lock, despite it not being defined yet.
// forwardRef makes this possible.
constructor(@Inject(forwardRef(() => Lock)) lock: Lock) { this.lock = lock; }
constructor(@Inject(forwardRef(() => Lock)) lock: Lock) {
this.lock = lock;
}
}
// Only at this point Lock is defined.
@ -42,7 +44,7 @@ import {Inject, ReflectiveInjector, forwardRef, resolveForwardRef} from '@angula
it('can be unwrapped', () => {
// #docregion resolve_forward_ref
const ref = forwardRef(() => 'refValue');
expect(resolveForwardRef(ref)).toEqual('refValue');
expect(resolveForwardRef(ref as any)).toEqual('refValue');
expect(resolveForwardRef('regularValue')).toEqual('regularValue');
// #enddocregion
});

View File

@ -168,9 +168,10 @@ describe('completions', () => {
});
it('should be able to get completions in an empty interpolation', () => {
const marker = mockHost.getLocationMarkerFor(PARSING_CASES, 'empty-interpolation');
const completions = ngLS.getCompletionsAtPosition(PARSING_CASES, marker.start);
expectContain(completions, CompletionKind.PROPERTY, ['title', 'subTitle']);
mockHost.override(TEST_TEMPLATE, `{{ ~{cursor} }}`);
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
const completions = ngLS.getCompletionsAtPosition(TEST_TEMPLATE, marker.start);
expectContain(completions, CompletionKind.PROPERTY, ['title', 'hero']);
});
it('should suggest $any() type cast function in an interpolation', () => {
@ -282,9 +283,14 @@ describe('completions', () => {
describe('with a *ngIf', () => {
it('should be able to get completions for exported *ngIf variable', () => {
const marker = mockHost.getLocationMarkerFor(PARSING_CASES, 'promised-person-name');
const completions = ngLS.getCompletionsAtPosition(PARSING_CASES, marker.start);
expectContain(completions, CompletionKind.PROPERTY, ['name', 'age', 'street']);
mockHost.override(TEST_TEMPLATE, `
<div *ngIf="heroP | async as h">
{{ h.~{cursor} }}
</div>
`);
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
const completions = ngLS.getCompletionsAtPosition(TEST_TEMPLATE, marker.start);
expectContain(completions, CompletionKind.PROPERTY, ['id', 'name']);
});
});
@ -368,9 +374,14 @@ describe('completions', () => {
});
it('should be able to infer the type of a ngForOf with an async pipe', () => {
const marker = mockHost.getLocationMarkerFor(PARSING_CASES, 'async-person-name');
const completions = ngLS.getCompletionsAtPosition(PARSING_CASES, marker.start);
expectContain(completions, CompletionKind.PROPERTY, ['name', 'age', 'street']);
mockHost.override(TEST_TEMPLATE, `
<div *ngFor="let h of heroesP | async">
{{ h.~{cursor} }}
</div>
`);
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
const completions = ngLS.getCompletionsAtPosition(TEST_TEMPLATE, marker.start);
expectContain(completions, CompletionKind.PROPERTY, ['id', 'name']);
});
it('should be able to resolve variable in nested loop', () => {
@ -498,14 +509,27 @@ describe('completions', () => {
describe('with references', () => {
it('should list references', () => {
const marker = mockHost.getLocationMarkerFor(PARSING_CASES, 'test-comp-content');
const completions = ngLS.getCompletionsAtPosition(PARSING_CASES, marker.start);
expectContain(completions, CompletionKind.REFERENCE, ['div', 'test1', 'test2']);
mockHost.override(TEST_TEMPLATE, `
<div #myDiv>
<test-comp #test1>
{{ ~{cursor} }}
</test-comp>
</div>
<test-comp #test2></test-comp>
`);
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
const completions = ngLS.getCompletionsAtPosition(TEST_TEMPLATE, marker.start);
expectContain(completions, CompletionKind.REFERENCE, ['myDiv', 'test1', 'test2']);
});
it('should reference the component', () => {
const marker = mockHost.getLocationMarkerFor(PARSING_CASES, 'test-comp-after-test');
const completions = ngLS.getCompletionsAtPosition(PARSING_CASES, marker.start);
mockHost.override(TEST_TEMPLATE, `
<test-comp #test1>
{{ test1.~{cursor} }}
</test-comp>
`);
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
const completions = ngLS.getCompletionsAtPosition(TEST_TEMPLATE, marker.start);
expectContain(completions, CompletionKind.PROPERTY, ['name', 'testEvent']);
});

View File

@ -342,7 +342,7 @@ describe('diagnostics', () => {
expect(category).toBe(ts.DiagnosticCategory.Error);
expect(messageText)
.toBe(
`Identifier 'missingField' is not defined. '{ implicitPerson: Person; }' does not contain such a member`,
`Identifier 'missingField' is not defined. '{ implicitPerson: Hero; }' does not contain such a member`,
);
const span = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'emb');
expect(start).toBe(span.start);
@ -361,7 +361,7 @@ describe('diagnostics', () => {
expect(category).toBe(ts.DiagnosticCategory.Error);
expect(messageText)
.toBe(
`Identifier 'missingField' is not defined. 'Person' does not contain such a member`,
`Identifier 'missingField' is not defined. 'Hero' does not contain such a member`,
);
const span = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'emb');
expect(start).toBe(span.start);

View File

@ -28,7 +28,7 @@ describe('html_info', () => {
const elements = SchemaInformation.instance.allKnownElements();
for (const element of elements) {
for (const prop of SchemaInformation.instance.propertiesOf(element)) {
expect(domRegistry.hasProperty(element, prop, []));
expect(domRegistry.hasProperty(element, prop, [])).toBeTrue();
}
}
});

View File

@ -16,16 +16,13 @@ import * as ParsingCases from './parsing-cases';
imports: [CommonModule, FormsModule],
declarations: [
AppComponent,
ParsingCases.AsyncForUsingComponent,
ParsingCases.CaseIncompleteOpen,
ParsingCases.CaseMissingClosing,
ParsingCases.CaseUnknown,
ParsingCases.CounterDirective,
ParsingCases.EmptyInterpolation,
ParsingCases.HintModel,
ParsingCases.NoValueAttribute,
ParsingCases.NumberModel,
ParsingCases.References,
ParsingCases.StringModel,
ParsingCases.TemplateReference,
ParsingCases.TestComponent,

View File

@ -63,45 +63,6 @@ export class HintModel {
hintChange: EventEmitter<string> = new EventEmitter();
}
interface Person {
name: string;
age: number;
street: string;
}
@Component({
template: `
<div *ngFor="let person of people | async">
{{person.~{async-person-name}name}}
</div>
<div *ngIf="promisedPerson | async as person">
{{person.~{promised-person-name}name}}
</div>
`,
})
export class AsyncForUsingComponent {
people: Promise<Person[]> = Promise.resolve([]);
promisedPerson: Promise<Person> = Promise.resolve({
name: 'John Doe',
age: 42,
street: '123 Angular Ln',
});
}
@Component({
template: `
<div #div>
<test-comp #test1>
{{~{test-comp-content}}}
{{test1.~{test-comp-after-test}name}}
{{div.~{test-comp-after-div}.innerText}}
</test-comp>
</div>
<test-comp #test2></test-comp>`,
})
export class References {
}
class CounterDirectiveContext<T> {
constructor(public $implicit: T) {}
}
@ -121,8 +82,8 @@ export class CounterDirective implements OnChanges {
}
interface WithContextDirectiveContext {
$implicit: {implicitPerson: Person;};
nonImplicitPerson: Person;
$implicit: {implicitPerson: Hero;};
nonImplicitPerson: Hero;
}
@Directive({selector: '[withContext]'})
@ -156,7 +117,9 @@ export class TemplateReference {
*/
title = 'Some title';
hero: Hero = {id: 1, name: 'Windstorm'};
heroP = Promise.resolve(this.hero);
heroes: Hero[] = [this.hero];
heroesP = Promise.resolve(this.heroes);
tupleArray: [string, Hero] = ['test', this.hero];
league: Hero[][] = [this.heroes];
heroesByName: {[name: string]: Hero} = {};
@ -171,11 +134,3 @@ export class TemplateReference {
constNames = [{name: 'name'}] as const;
private myField = 'My Field';
}
@Component({
template: '{{~{empty-interpolation}}}',
})
export class EmptyInterpolation {
title = 'Some title';
subTitle = 'Some sub title';
}

View File

@ -94,7 +94,7 @@ describe('TypeScriptServiceHost', () => {
const tsLS = ts.createLanguageService(tsLSHost);
const ngLSHost = new TypeScriptServiceHost(tsLSHost, tsLS);
const templates = ngLSHost.getTemplates('/app/parsing-cases.ts');
expect(templates.length).toBe(8);
expect(templates.length).toBe(5);
});
it('should be able to find external template', () => {

View File

@ -17,7 +17,9 @@ import {HammerGestureConfig, HammerGesturesPlugin,} from '@angular/platform-brow
let fakeConsole: any;
if (isNode) return;
beforeEach(() => { fakeConsole = {warn: jasmine.createSpy('console.warn')}; });
beforeEach(() => {
fakeConsole = {warn: jasmine.createSpy('console.warn')};
});
describe('with no custom loader', () => {
beforeEach(() => {
@ -25,7 +27,7 @@ import {HammerGestureConfig, HammerGesturesPlugin,} from '@angular/platform-brow
});
it('should implement addGlobalEventListener', () => {
spyOn(plugin, 'addEventListener').and.callFake(() => {});
spyOn(plugin, 'addEventListener').and.callFake(() => () => {});
expect(() => {
plugin.addGlobalEventListener('document', 'swipe', () => {});
@ -61,7 +63,9 @@ import {HammerGestureConfig, HammerGesturesPlugin,} from '@angular/platform-brow
// Inject the NgZone so that we can make it available to the plugin through a fake
// EventManager.
let ngZone: NgZone;
beforeEach(inject([NgZone], (z: NgZone) => { ngZone = z; }));
beforeEach(inject([NgZone], (z: NgZone) => {
ngZone = z;
}));
beforeEach(() => {
originalHammerGlobal = (window as any).Hammer;
@ -84,13 +88,15 @@ import {HammerGestureConfig, HammerGesturesPlugin,} from '@angular/platform-brow
plugin = new HammerGesturesPlugin(document, hammerConfig, fakeConsole, loader);
// Use a fake EventManager that has access to the NgZone.
plugin.manager = { getZone: () => ngZone } as EventManager;
plugin.manager = {getZone: () => ngZone} as EventManager;
someElement = document.createElement('div');
someListener = () => {};
});
afterEach(() => { (window as any).Hammer = originalHammerGlobal; });
afterEach(() => {
(window as any).Hammer = originalHammerGlobal;
});
it('should not log a warning when HammerJS is not loaded', () => {
plugin.addEventListener(someElement, 'swipe', () => {});

View File

@ -51,7 +51,6 @@ import {KeyEventsPlugin} from '@angular/platform-browser/src/dom/events/key_even
.toEqual({'domEventName': 'keydown', 'fullKey': 'control.shift'});
expect(KeyEventsPlugin.parseEventName('keyup.control.shift'))
.toEqual({'domEventName': 'keyup', 'fullKey': 'control.shift'});
});
it('should alias esc to escape', () => {
@ -62,11 +61,10 @@ import {KeyEventsPlugin} from '@angular/platform-browser/src/dom/events/key_even
it('should implement addGlobalEventListener', () => {
const plugin = new KeyEventsPlugin(document);
spyOn(plugin, 'addEventListener').and.callFake(() => {});
spyOn(plugin, 'addEventListener').and.callFake(() => () => {});
expect(() => plugin.addGlobalEventListener('window', 'keyup.control.esc', () => {}))
.not.toThrowError();
});
});
}

View File

@ -36,7 +36,8 @@ export type Routes = Route[];
* @publicApi
*/
export type UrlMatchResult = {
consumed: UrlSegment[]; posParams?: {[name: string]: UrlSegment};
consumed: UrlSegment[];
posParams?: {[name: string]: UrlSegment};
};
/**
@ -64,7 +65,7 @@ export type UrlMatchResult = {
* @publicApi
*/
export type UrlMatcher = (segments: UrlSegment[], group: UrlSegmentGroup, route: Route) =>
UrlMatchResult;
UrlMatchResult|null;
/**
*
@ -109,7 +110,7 @@ export type ResolveData = {
* @see `Route#loadChildren`.
* @publicApi
*/
export type LoadChildrenCallback = () => Type<any>| NgModuleFactory<any>| Observable<Type<any>>|
export type LoadChildrenCallback = () => Type<any>|NgModuleFactory<any>|Observable<Type<any>>|
Promise<NgModuleFactory<any>|Type<any>|any>;
/**
@ -123,7 +124,7 @@ export type LoadChildrenCallback = () => Type<any>| NgModuleFactory<any>| Observ
* @see `Route#loadChildren`.
* @publicApi
*/
export type LoadChildren = LoadChildrenCallback | DeprecatedLoadChildren;
export type LoadChildren = LoadChildrenCallback|DeprecatedLoadChildren;
/**
* A string of the form `path/to/file#exportName` that acts as a URL for a set of routes to load.
@ -147,7 +148,7 @@ export type DeprecatedLoadChildren = string;
* @see `RouterLink`
* @publicApi
*/
export type QueryParamsHandling = 'merge' | 'preserve' | '';
export type QueryParamsHandling = 'merge'|'preserve'|'';
/**
*
@ -156,9 +157,9 @@ export type QueryParamsHandling = 'merge' | 'preserve' | '';
* @see `Route#runGuardsAndResolvers`
* @publicApi
*/
export type RunGuardsAndResolvers = 'pathParamsChange' | 'pathParamsOrQueryParamsChange' |
'paramsChange' | 'paramsOrQueryParamsChange' | 'always' |
((from: ActivatedRouteSnapshot, to: ActivatedRouteSnapshot) => boolean);
export type RunGuardsAndResolvers =
'pathParamsChange'|'pathParamsOrQueryParamsChange'|'paramsChange'|'paramsOrQueryParamsChange'|
'always'|((from: ActivatedRouteSnapshot, to: ActivatedRouteSnapshot) => boolean);
/**
* A configuration object that defines a single route.
@ -519,36 +520,36 @@ function validateNode(route: Route, fullPath: string): void {
}
if (!route.component && !route.children && !route.loadChildren &&
(route.outlet && route.outlet !== PRIMARY_OUTLET)) {
throw new Error(
`Invalid configuration of route '${fullPath}': a componentless route without children or loadChildren cannot have a named outlet set`);
throw new Error(`Invalid configuration of route '${
fullPath}': a componentless route without children or loadChildren cannot have a named outlet set`);
}
if (route.redirectTo && route.children) {
throw new Error(
`Invalid configuration of route '${fullPath}': redirectTo and children cannot be used together`);
throw new Error(`Invalid configuration of route '${
fullPath}': redirectTo and children cannot be used together`);
}
if (route.redirectTo && route.loadChildren) {
throw new Error(
`Invalid configuration of route '${fullPath}': redirectTo and loadChildren cannot be used together`);
throw new Error(`Invalid configuration of route '${
fullPath}': redirectTo and loadChildren cannot be used together`);
}
if (route.children && route.loadChildren) {
throw new Error(
`Invalid configuration of route '${fullPath}': children and loadChildren cannot be used together`);
throw new Error(`Invalid configuration of route '${
fullPath}': children and loadChildren cannot be used together`);
}
if (route.redirectTo && route.component) {
throw new Error(
`Invalid configuration of route '${fullPath}': redirectTo and component cannot be used together`);
throw new Error(`Invalid configuration of route '${
fullPath}': redirectTo and component cannot be used together`);
}
if (route.path && route.matcher) {
throw new Error(
`Invalid configuration of route '${fullPath}': path and matcher cannot be used together`);
}
if (route.redirectTo === void 0 && !route.component && !route.children && !route.loadChildren) {
throw new Error(
`Invalid configuration of route '${fullPath}'. One of the following must be provided: component, redirectTo, children or loadChildren`);
throw new Error(`Invalid configuration of route '${
fullPath}'. One of the following must be provided: component, redirectTo, children or loadChildren`);
}
if (route.path === void 0 && route.matcher === void 0) {
throw new Error(
`Invalid configuration of route '${fullPath}': routes must have either a path or a matcher specified`);
throw new Error(`Invalid configuration of route '${
fullPath}': routes must have either a path or a matcher specified`);
}
if (typeof route.path === 'string' && route.path.charAt(0) === '/') {
throw new Error(`Invalid configuration of route '${fullPath}': path cannot start with a slash`);
@ -556,12 +557,12 @@ function validateNode(route: Route, fullPath: string): void {
if (route.path === '' && route.redirectTo !== void 0 && route.pathMatch === void 0) {
const exp =
`The default value of 'pathMatch' is 'prefix', but often the intent is to use 'full'.`;
throw new Error(
`Invalid configuration of route '{path: "${fullPath}", redirectTo: "${route.redirectTo}"}': please provide 'pathMatch'. ${exp}`);
throw new Error(`Invalid configuration of route '{path: "${fullPath}", redirectTo: "${
route.redirectTo}"}': please provide 'pathMatch'. ${exp}`);
}
if (route.pathMatch !== void 0 && route.pathMatch !== 'full' && route.pathMatch !== 'prefix') {
throw new Error(
`Invalid configuration of route '${fullPath}': pathMatch can only be set to 'prefix' or 'full'`);
throw new Error(`Invalid configuration of route '${
fullPath}': pathMatch can only be set to 'prefix' or 'full'`);
}
if (route.children) {
validateConfig(route.children, fullPath);

View File

@ -3708,7 +3708,7 @@ describe('Integration', () => {
router.navigate(['/user/:fedor']);
advance(fixture);
expect(navigateSpy.calls.mostRecent().args[1].queryParams);
expect(navigateSpy.calls.mostRecent().args[1]!.queryParams);
})));
});

View File

@ -7,8 +7,8 @@
*/
import {isPlatformBrowser} from '@angular/common';
import {APP_INITIALIZER, ApplicationRef, InjectionToken, Injector, ModuleWithProviders, NgModule, PLATFORM_ID} from '@angular/core';
import {Observable, of } from 'rxjs';
import {APP_INITIALIZER, ApplicationRef, InjectionToken, Injector, ModuleWithProviders, NgModule, NgZone, PLATFORM_ID} from '@angular/core';
import {Observable, merge, of } from 'rxjs';
import {delay, filter, take} from 'rxjs/operators';
import {NgswCommChannel} from './low_level';
@ -55,8 +55,12 @@ export abstract class SwRegistrationOptions {
* registered (e.g. there might be a long-running timeout or polling interval, preventing the app
* to stabilize). The available option are:
*
* - `registerWhenStable`: Register as soon as the application stabilizes (no pending
* micro-/macro-tasks).
* - `registerWhenStable:<timeout>`: Register as soon as the application stabilizes (no pending
* micro-/macro-tasks) but no later than `<timeout>` milliseconds. If the app hasn't
* stabilized after `<timeout>` milliseconds (for example, due to a recurrent asynchronous
* task), the ServiceWorker will be registered anyway.
* If `<timeout>` is omitted, the ServiceWorker will only be registered once the app
* stabilizes.
* - `registerImmediately`: Register immediately.
* - `registerWithDelay:<timeout>`: Register with a delay of `<timeout>` milliseconds. For
* example, use `registerWithDelay:5000` to register the ServiceWorker after 5 seconds. If
@ -96,17 +100,19 @@ export function ngswAppInitializer(
if (typeof options.registrationStrategy === 'function') {
readyToRegister$ = options.registrationStrategy();
} else {
const [strategy, ...args] = (options.registrationStrategy || 'registerWhenStable').split(':');
const [strategy, ...args] =
(options.registrationStrategy || 'registerWhenStable:30000').split(':');
switch (strategy) {
case 'registerImmediately':
readyToRegister$ = of (null);
break;
case 'registerWithDelay':
readyToRegister$ = of (null).pipe(delay(+args[0] || 0));
readyToRegister$ = delayWithTimeout(+args[0] || 0);
break;
case 'registerWhenStable':
const appRef = injector.get<ApplicationRef>(ApplicationRef);
readyToRegister$ = appRef.isStable.pipe(filter(stable => stable));
readyToRegister$ = !args[0] ? whenStable(injector) :
merge(whenStable(injector), delayWithTimeout(+args[0]));
break;
default:
// Unknown strategy.
@ -116,14 +122,28 @@ export function ngswAppInitializer(
}
// Don't return anything to avoid blocking the application until the SW is registered.
// Also, run outside the Angular zone to avoid preventing the app from stabilizing (especially
// given that some registration strategies wait for the app to stabilize).
// Catch and log the error if SW registration fails to avoid uncaught rejection warning.
readyToRegister$.pipe(take(1)).subscribe(
() => navigator.serviceWorker.register(script, {scope: options.scope})
.catch(err => console.error('Service worker registration failed with:', err)));
const ngZone = injector.get(NgZone);
ngZone.runOutsideAngular(
() => readyToRegister$.pipe(take(1)).subscribe(
() =>
navigator.serviceWorker.register(script, {scope: options.scope})
.catch(err => console.error('Service worker registration failed with:', err))));
};
return initializer;
}
function delayWithTimeout(timeout: number): Observable<unknown> {
return of (null).pipe(delay(timeout));
}
function whenStable(injector: Injector): Observable<unknown> {
const appRef = injector.get(ApplicationRef);
return appRef.isStable.pipe(filter(stable => stable));
}
export function ngswCommChannelFactory(
opts: SwRegistrationOptions, platformId: string): NgswCommChannel {
return new NgswCommChannel(

View File

@ -9,7 +9,7 @@
import {PLATFORM_ID} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {NgswCommChannel} from '@angular/service-worker/src/low_level';
import {SwRegistrationOptions, ngswCommChannelFactory} from '@angular/service-worker/src/module';
import {ngswCommChannelFactory, SwRegistrationOptions} from '@angular/service-worker/src/module';
import {SwPush} from '@angular/service-worker/src/push';
import {SwUpdate} from '@angular/service-worker/src/update';
import {MockPushManager, MockPushSubscription, MockServiceWorkerContainer, MockServiceWorkerRegistration, patchDecodeBase64} from '@angular/service-worker/testing/mock';
@ -32,14 +32,18 @@ import {MockPushManager, MockPushSubscription, MockServiceWorkerContainer, MockS
mock.setupSw();
(comm as any).registration.subscribe((reg: any) => { done(); });
(comm as any).registration.subscribe((reg: any) => {
done();
});
});
it('can access the registration when it comes after subscription', done => {
const mock = new MockServiceWorkerContainer();
const comm = new NgswCommChannel(mock as any);
const regPromise = mock.getRegistration() as any as MockServiceWorkerRegistration;
(comm as any).registration.subscribe((reg: any) => { done(); });
(comm as any).registration.subscribe((reg: any) => {
done();
});
mock.setupSw();
});
@ -158,7 +162,7 @@ import {MockPushManager, MockPushSubscription, MockServiceWorkerContainer, MockS
});
describe('requestSubscription()', () => {
it('returns a promise that resolves to the subscription', async() => {
it('returns a promise that resolves to the subscription', async () => {
const promise = push.requestSubscription({serverPublicKey: 'test'});
expect(promise).toEqual(jasmine.any(Promise));
@ -166,7 +170,7 @@ import {MockPushManager, MockPushSubscription, MockServiceWorkerContainer, MockS
expect(sub).toEqual(jasmine.any(MockPushSubscription));
});
it('calls `PushManager.subscribe()` (with appropriate options)', async() => {
it('calls `PushManager.subscribe()` (with appropriate options)', async () => {
const decode = (charCodeArr: Uint8Array) =>
Array.from(charCodeArr).map(c => String.fromCharCode(c)).join('');
@ -179,16 +183,16 @@ import {MockPushManager, MockPushSubscription, MockServiceWorkerContainer, MockS
expect(pmSubscribeSpy).toHaveBeenCalledTimes(1);
expect(pmSubscribeSpy).toHaveBeenCalledWith({
applicationServerKey: jasmine.any(Uint8Array),
applicationServerKey: jasmine.any(Uint8Array) as any,
userVisibleOnly: true,
});
const actualAppServerKey = pmSubscribeSpy.calls.first().args[0].applicationServerKey;
const actualAppServerKeyStr = decode(actualAppServerKey);
const actualAppServerKey = pmSubscribeSpy.calls.first().args[0]!.applicationServerKey;
const actualAppServerKeyStr = decode(actualAppServerKey as Uint8Array);
expect(actualAppServerKeyStr).toBe(appServerKeyStr);
});
it('emits the new `PushSubscription` on `SwPush.subscription`', async() => {
it('emits the new `PushSubscription` on `SwPush.subscription`', async () => {
const subscriptionSpy = jasmine.createSpy('subscriptionSpy');
push.subscription.subscribe(subscriptionSpy);
const sub = await push.requestSubscription({serverPublicKey: 'test'});
@ -204,7 +208,7 @@ import {MockPushManager, MockPushSubscription, MockServiceWorkerContainer, MockS
psUnsubscribeSpy = spyOn(MockPushSubscription.prototype, 'unsubscribe').and.callThrough();
});
it('rejects if currently not subscribed to push notifications', async() => {
it('rejects if currently not subscribed to push notifications', async () => {
try {
await push.unsubscribe();
throw new Error('`unsubscribe()` should fail');
@ -213,15 +217,17 @@ import {MockPushManager, MockPushSubscription, MockServiceWorkerContainer, MockS
}
});
it('calls `PushSubscription.unsubscribe()`', async() => {
it('calls `PushSubscription.unsubscribe()`', async () => {
await push.requestSubscription({serverPublicKey: 'test'});
await push.unsubscribe();
expect(psUnsubscribeSpy).toHaveBeenCalledTimes(1);
});
it('rejects if `PushSubscription.unsubscribe()` fails', async() => {
psUnsubscribeSpy.and.callFake(() => { throw new Error('foo'); });
it('rejects if `PushSubscription.unsubscribe()` fails', async () => {
psUnsubscribeSpy.and.callFake(() => {
throw new Error('foo');
});
try {
await push.requestSubscription({serverPublicKey: 'test'});
@ -232,7 +238,7 @@ import {MockPushManager, MockPushSubscription, MockServiceWorkerContainer, MockS
}
});
it('rejects if `PushSubscription.unsubscribe()` returns false', async() => {
it('rejects if `PushSubscription.unsubscribe()` returns false', async () => {
psUnsubscribeSpy.and.returnValue(Promise.resolve(false));
try {
@ -244,7 +250,7 @@ import {MockPushManager, MockPushSubscription, MockServiceWorkerContainer, MockS
}
});
it('emits `null` on `SwPush.subscription`', async() => {
it('emits `null` on `SwPush.subscription`', async () => {
const subscriptionSpy = jasmine.createSpy('subscriptionSpy');
push.subscription.subscribe(subscriptionSpy);
@ -254,7 +260,7 @@ import {MockPushManager, MockPushSubscription, MockServiceWorkerContainer, MockS
expect(subscriptionSpy).toHaveBeenCalledWith(null);
});
it('does not emit on `SwPush.subscription` on failure', async() => {
it('does not emit on `SwPush.subscription` on failure', async () => {
const subscriptionSpy = jasmine.createSpy('subscriptionSpy');
const initialSubEmit = new Promise(resolve => subscriptionSpy.and.callFake(resolve));
@ -271,7 +277,9 @@ import {MockPushManager, MockPushSubscription, MockServiceWorkerContainer, MockS
subscriptionSpy.calls.reset();
// Error due to `PushSubscription.unsubscribe()` error.
psUnsubscribeSpy.and.callFake(() => { throw new Error('foo'); });
psUnsubscribeSpy.and.callFake(() => {
throw new Error('foo');
});
await push.unsubscribe().catch(() => undefined);
expect(subscriptionSpy).not.toHaveBeenCalled();
@ -338,7 +346,7 @@ import {MockPushManager, MockPushSubscription, MockServiceWorkerContainer, MockS
push.subscription.subscribe(subscriptionSpy);
});
it('emits on worker-driven changes (i.e. when the controller changes)', async() => {
it('emits on worker-driven changes (i.e. when the controller changes)', async () => {
// Initial emit for the current `ServiceWorkerController`.
await nextSubEmitPromise;
expect(subscriptionSpy).toHaveBeenCalledTimes(1);
@ -353,7 +361,7 @@ import {MockPushManager, MockPushSubscription, MockServiceWorkerContainer, MockS
expect(subscriptionSpy).toHaveBeenCalledWith(null);
});
it('emits on subscription changes (i.e. when subscribing/unsubscribing)', async() => {
it('emits on subscription changes (i.e. when subscribing/unsubscribing)', async () => {
await nextSubEmitPromise;
subscriptionSpy.calls.reset();
@ -391,11 +399,16 @@ import {MockPushManager, MockPushSubscription, MockServiceWorkerContainer, MockS
});
it('gives an error when registering', done => {
push.requestSubscription({serverPublicKey: 'test'}).catch(err => { done(); });
push.requestSubscription({serverPublicKey: 'test'}).catch(err => {
done();
});
});
it('gives an error when unsubscribing',
done => { push.unsubscribe().catch(err => { done(); }); });
it('gives an error when unsubscribing', done => {
push.unsubscribe().catch(err => {
done();
});
});
});
});
@ -461,7 +474,9 @@ import {MockPushManager, MockPushSubscription, MockServiceWorkerContainer, MockS
});
});
return update.activateUpdate()
.catch(err => { expect(err.message).toEqual('Failed to activate'); })
.catch(err => {
expect(err.message).toEqual('Failed to activate');
})
.then(() => done())
.catch(err => done.fail(err));
});
@ -475,8 +490,12 @@ import {MockPushManager, MockPushSubscription, MockServiceWorkerContainer, MockS
expect(() => TestBed.inject(SwUpdate)).not.toThrow();
});
describe('with no SW', () => {
beforeEach(() => { comm = new NgswCommChannel(undefined); });
it('can be instantiated', () => { update = new SwUpdate(comm); });
beforeEach(() => {
comm = new NgswCommChannel(undefined);
});
it('can be instantiated', () => {
update = new SwUpdate(comm);
});
it('does not crash on subscription to observables', () => {
update = new SwUpdate(comm);
update.available.toPromise().catch(err => fail(err));
@ -484,11 +503,15 @@ import {MockPushManager, MockPushSubscription, MockServiceWorkerContainer, MockS
});
it('gives an error when checking for updates', done => {
update = new SwUpdate(comm);
update.checkForUpdate().catch(err => { done(); });
update.checkForUpdate().catch(err => {
done();
});
});
it('gives an error when activating updates', done => {
update = new SwUpdate(comm);
update.activateUpdate().catch(err => { done(); });
update.activateUpdate().catch(err => {
done();
});
});
});
});

View File

@ -7,7 +7,7 @@
*/
import {ApplicationRef, PLATFORM_ID} from '@angular/core';
import {TestBed, fakeAsync, flushMicrotasks, tick} from '@angular/core/testing';
import {fakeAsync, flushMicrotasks, TestBed, tick} from '@angular/core/testing';
import {Subject} from 'rxjs';
import {filter, take} from 'rxjs/operators';
@ -30,10 +30,10 @@ describe('ServiceWorkerModule', () => {
beforeEach(
() => swRegisterSpy =
spyOn(navigator.serviceWorker, 'register').and.returnValue(Promise.resolve()));
spyOn(navigator.serviceWorker, 'register').and.returnValue(Promise.resolve(null as any)));
describe('register()', () => {
const configTestBed = async(opts: SwRegistrationOptions) => {
const configTestBed = async (opts: SwRegistrationOptions) => {
TestBed.configureTestingModule({
imports: [ServiceWorkerModule.register('sw.js', opts)],
providers: [{provide: PLATFORM_ID, useValue: 'browser'}],
@ -42,35 +42,35 @@ describe('ServiceWorkerModule', () => {
await untilStable();
};
it('sets the registration options', async() => {
it('sets the registration options', async () => {
await configTestBed({enabled: true, scope: 'foo'});
expect(TestBed.inject(SwRegistrationOptions)).toEqual({enabled: true, scope: 'foo'});
expect(swRegisterSpy).toHaveBeenCalledWith('sw.js', {scope: 'foo'});
});
it('can disable the SW', async() => {
it('can disable the SW', async () => {
await configTestBed({enabled: false});
expect(TestBed.inject(SwUpdate).isEnabled).toBe(false);
expect(swRegisterSpy).not.toHaveBeenCalled();
});
it('can enable the SW', async() => {
it('can enable the SW', async () => {
await configTestBed({enabled: true});
expect(TestBed.inject(SwUpdate).isEnabled).toBe(true);
expect(swRegisterSpy).toHaveBeenCalledWith('sw.js', {scope: undefined});
});
it('defaults to enabling the SW', async() => {
it('defaults to enabling the SW', async () => {
await configTestBed({});
expect(TestBed.inject(SwUpdate).isEnabled).toBe(true);
expect(swRegisterSpy).toHaveBeenCalledWith('sw.js', {scope: undefined});
});
it('catches and a logs registration errors', async() => {
it('catches and a logs registration errors', async () => {
const consoleErrorSpy = spyOn(console, 'error');
swRegisterSpy.and.returnValue(Promise.reject('no reason'));
@ -92,7 +92,7 @@ describe('ServiceWorkerModule', () => {
});
};
it('sets the registration options (and overwrites those set via `.register()`', async() => {
it('sets the registration options (and overwrites those set via `.register()`', async () => {
configTestBed({enabled: true, scope: 'provider'});
await untilStable();
@ -100,7 +100,7 @@ describe('ServiceWorkerModule', () => {
expect(swRegisterSpy).toHaveBeenCalledWith('sw.js', {scope: 'provider'});
});
it('can disable the SW', async() => {
it('can disable the SW', async () => {
configTestBed({enabled: false}, {enabled: true});
await untilStable();
@ -108,7 +108,7 @@ describe('ServiceWorkerModule', () => {
expect(swRegisterSpy).not.toHaveBeenCalled();
});
it('can enable the SW', async() => {
it('can enable the SW', async () => {
configTestBed({enabled: true}, {enabled: false});
await untilStable();
@ -116,7 +116,7 @@ describe('ServiceWorkerModule', () => {
expect(swRegisterSpy).toHaveBeenCalledWith('sw.js', {scope: undefined});
});
it('defaults to enabling the SW', async() => {
it('defaults to enabling the SW', async () => {
configTestBed({}, {enabled: false});
await untilStable();
@ -141,13 +141,13 @@ describe('ServiceWorkerModule', () => {
],
});
// Dummy `get()` call to initialize the test "app".
// Dummy `inject()` call to initialize the test "app".
TestBed.inject(ApplicationRef);
return isStableSub;
};
it('defaults to registering the SW when the app stabilizes', fakeAsync(() => {
it('defaults to registering the SW when the app stabilizes (under 30s)', fakeAsync(() => {
const isStableSub = configTestBedWithMockedStability();
isStableSub.next(false);
@ -156,13 +156,91 @@ describe('ServiceWorkerModule', () => {
tick();
expect(swRegisterSpy).not.toHaveBeenCalled();
tick(20000);
expect(swRegisterSpy).not.toHaveBeenCalled();
isStableSub.next(true);
tick();
expect(swRegisterSpy).toHaveBeenCalledWith('sw.js', {scope: undefined});
}));
it('registers the SW when the app stabilizes with `registerWhenStable`', fakeAsync(() => {
it('defaults to registering the SW after 30s if the app does not stabilize sooner',
fakeAsync(() => {
const isStableSub = configTestBedWithMockedStability();
tick(29999);
expect(swRegisterSpy).not.toHaveBeenCalled();
tick(1);
expect(swRegisterSpy).toHaveBeenCalledWith('sw.js', {scope: undefined});
}));
it('registers the SW when the app stabilizes with `registerWhenStable:<timeout>`',
fakeAsync(() => {
const isStableSub = configTestBedWithMockedStability('registerWhenStable:1000');
isStableSub.next(false);
isStableSub.next(false);
tick();
expect(swRegisterSpy).not.toHaveBeenCalled();
tick(500);
expect(swRegisterSpy).not.toHaveBeenCalled();
isStableSub.next(true);
tick();
expect(swRegisterSpy).toHaveBeenCalledWith('sw.js', {scope: undefined});
}));
it('registers the SW after `timeout` if the app does not stabilize with `registerWhenStable:<timeout>`',
fakeAsync(() => {
configTestBedWithMockedStability('registerWhenStable:1000');
tick(999);
expect(swRegisterSpy).not.toHaveBeenCalled();
tick(1);
expect(swRegisterSpy).toHaveBeenCalledWith('sw.js', {scope: undefined});
}));
it('registers the SW asap (asynchronously) before the app stabilizes with `registerWhenStable:0`',
fakeAsync(() => {
const isStableSub = configTestBedWithMockedStability('registerWhenStable:0');
// Create a microtask.
Promise.resolve();
flushMicrotasks();
expect(swRegisterSpy).not.toHaveBeenCalled();
tick(0);
expect(swRegisterSpy).toHaveBeenCalledWith('sw.js', {scope: undefined});
}));
it('registers the SW only when the app stabilizes with `registerWhenStable:`',
fakeAsync(() => {
const isStableSub = configTestBedWithMockedStability('registerWhenStable:');
isStableSub.next(false);
isStableSub.next(false);
tick();
expect(swRegisterSpy).not.toHaveBeenCalled();
tick(60000);
expect(swRegisterSpy).not.toHaveBeenCalled();
isStableSub.next(true);
tick();
expect(swRegisterSpy).toHaveBeenCalledWith('sw.js', {scope: undefined});
}));
it('registers the SW only when the app stabilizes with `registerWhenStable`',
fakeAsync(() => {
const isStableSub = configTestBedWithMockedStability('registerWhenStable');
isStableSub.next(false);
@ -171,6 +249,9 @@ describe('ServiceWorkerModule', () => {
tick();
expect(swRegisterSpy).not.toHaveBeenCalled();
tick(60000);
expect(swRegisterSpy).not.toHaveBeenCalled();
isStableSub.next(true);
tick();

View File

@ -11,18 +11,18 @@ import {CacheDatabase} from '../src/db-cache';
import {Driver, DriverReadyState} from '../src/driver';
import {AssetGroupConfig, DataGroupConfig, Manifest} from '../src/manifest';
import {sha1} from '../src/sha1';
import {MockCache, clearAllCaches} from '../testing/cache';
import {clearAllCaches, MockCache} from '../testing/cache';
import {MockRequest, MockResponse} from '../testing/fetch';
import {MockFileSystemBuilder, MockServerStateBuilder, tmpHashTableForFs} from '../testing/mock';
import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
(function() {
// Skip environments that don't support the minimum APIs needed to run the SW tests.
if (!SwTestHarness.envIsSupported()) {
// Skip environments that don't support the minimum APIs needed to run the SW tests.
if (!SwTestHarness.envIsSupported()) {
return;
}
}
const dist =
const dist =
new MockFileSystemBuilder()
.addFile('/foo.txt', 'this is foo')
.addFile('/bar.txt', 'this is bar')
@ -35,11 +35,10 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
.addUnhashedFile('/unhashed/a.txt', 'this is unhashed', {'Cache-Control': 'max-age=10'})
.addUnhashedFile('/unhashed/b.txt', 'this is unhashed b', {'Cache-Control': 'no-cache'})
.addUnhashedFile('/api/foo', 'this is api foo', {'Cache-Control': 'no-cache'})
.addUnhashedFile(
'/api-static/bar', 'this is static api bar', {'Cache-Control': 'no-cache'})
.addUnhashedFile('/api-static/bar', 'this is static api bar', {'Cache-Control': 'no-cache'})
.build();
const distUpdate =
const distUpdate =
new MockFileSystemBuilder()
.addFile('/foo.txt', 'this is foo v2')
.addFile('/bar.txt', 'this is bar')
@ -49,18 +48,17 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
.addFile('/quuux.txt', 'this is quuux v2')
.addFile('/lazy/unchanged1.txt', 'this is unchanged (1)')
.addFile('/lazy/unchanged2.txt', 'this is unchanged (2)')
.addUnhashedFile(
'/unhashed/a.txt', 'this is unhashed v2', {'Cache-Control': 'max-age=10'})
.addUnhashedFile('/unhashed/a.txt', 'this is unhashed v2', {'Cache-Control': 'max-age=10'})
.addUnhashedFile('/ignored/file1', 'this is not handled by the SW')
.addUnhashedFile('/ignored/dir/file2', 'this is not handled by the SW either')
.build();
const brokenFs = new MockFileSystemBuilder()
const brokenFs = new MockFileSystemBuilder()
.addFile('/foo.txt', 'this is foo (broken)')
.addFile('/bar.txt', 'this is bar (broken)')
.build();
const brokenManifest: Manifest = {
const brokenManifest: Manifest = {
configVersion: 1,
timestamp: 1234567890123,
index: '/foo.txt',
@ -76,9 +74,9 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
dataGroups: [],
navigationUrls: processNavigationUrls(''),
hashTable: tmpHashTableForFs(brokenFs, {'/foo.txt': true}),
};
};
const brokenLazyManifest: Manifest = {
const brokenLazyManifest: Manifest = {
configVersion: 1,
timestamp: 1234567890123,
index: '/foo.txt',
@ -105,27 +103,27 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
dataGroups: [],
navigationUrls: processNavigationUrls(''),
hashTable: tmpHashTableForFs(brokenFs, {'/bar.txt': true}),
};
};
// Manifest without navigation urls to test backward compatibility with
// versions < 6.0.0.
interface ManifestV5 {
// Manifest without navigation urls to test backward compatibility with
// versions < 6.0.0.
interface ManifestV5 {
configVersion: number;
appData?: {[key: string]: string};
index: string;
assetGroups?: AssetGroupConfig[];
dataGroups?: DataGroupConfig[];
hashTable: {[url: string]: string};
}
}
// To simulate versions < 6.0.0
const manifestOld: ManifestV5 = {
// To simulate versions < 6.0.0
const manifestOld: ManifestV5 = {
configVersion: 1,
index: '/foo.txt',
hashTable: tmpHashTableForFs(dist),
};
};
const manifest: Manifest = {
const manifest: Manifest = {
configVersion: 1,
timestamp: 1234567890123,
appData: {
@ -193,9 +191,9 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
],
navigationUrls: processNavigationUrls(''),
hashTable: tmpHashTableForFs(dist),
};
};
const manifestUpdate: Manifest = {
const manifestUpdate: Manifest = {
configVersion: 1,
timestamp: 1234567890123,
appData: {
@ -248,41 +246,39 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
'!/ignored/dir/**',
]),
hashTable: tmpHashTableForFs(distUpdate),
};
};
const serverBuilderBase =
const serverBuilderBase =
new MockServerStateBuilder()
.withStaticFiles(dist)
.withRedirect('/redirected.txt', '/redirect-target.txt', 'this was a redirect')
.withError('/error.txt');
const server = serverBuilderBase.withManifest(manifest).build();
const server = serverBuilderBase.withManifest(manifest).build();
const serverRollback =
const serverRollback =
serverBuilderBase.withManifest({...manifest, timestamp: manifest.timestamp + 1}).build();
const serverUpdate =
const serverUpdate =
new MockServerStateBuilder()
.withStaticFiles(distUpdate)
.withManifest(manifestUpdate)
.withRedirect('/redirected.txt', '/redirect-target.txt', 'this was a redirect')
.build();
const brokenServer =
const brokenServer =
new MockServerStateBuilder().withStaticFiles(brokenFs).withManifest(brokenManifest).build();
const brokenLazyServer = new MockServerStateBuilder()
.withStaticFiles(brokenFs)
.withManifest(brokenLazyManifest)
.build();
const brokenLazyServer =
new MockServerStateBuilder().withStaticFiles(brokenFs).withManifest(brokenLazyManifest).build();
const server404 = new MockServerStateBuilder().withStaticFiles(dist).build();
const server404 = new MockServerStateBuilder().withStaticFiles(dist).build();
const manifestHash = sha1(JSON.stringify(manifest));
const manifestUpdateHash = sha1(JSON.stringify(manifestUpdate));
const manifestHash = sha1(JSON.stringify(manifest));
const manifestUpdateHash = sha1(JSON.stringify(manifestUpdate));
describe('Driver', () => {
describe('Driver', () => {
let scope: SwTestHarness;
let driver: Driver;
@ -296,19 +292,19 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
driver = new Driver(scope, scope, new CacheDatabase(scope, scope));
});
it('activates without waiting', async() => {
it('activates without waiting', async () => {
const skippedWaiting = await scope.startup(true);
expect(skippedWaiting).toBe(true);
});
it('claims all clients, after activation', async() => {
it('claims all clients, after activation', async () => {
const claimSpy = spyOn(scope.clients, 'claim');
await scope.startup(true);
expect(claimSpy).toHaveBeenCalledTimes(1);
});
it('cleans up old `@angular/service-worker` caches, after activation', async() => {
it('cleans up old `@angular/service-worker` caches, after activation', async () => {
const claimSpy = spyOn(scope.clients, 'claim');
const cleanupOldSwCachesSpy = spyOn(driver, 'cleanupOldSwCaches');
@ -322,7 +318,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
expect(claimSpy).toHaveBeenCalledBefore(cleanupOldSwCachesSpy);
});
it('does not blow up if cleaning up old `@angular/service-worker` caches fails', async() => {
it('does not blow up if cleaning up old `@angular/service-worker` caches fails', async () => {
spyOn(driver, 'cleanupOldSwCaches').and.callFake(() => Promise.reject('Ooops'));
// Automatically advance time to trigger idle tasks as they are added.
@ -338,7 +334,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
server.assertNoOtherRequests();
});
it('initializes prefetched content correctly, after activation', async() => {
it('initializes prefetched content correctly, after activation', async () => {
// Automatically advance time to trigger idle tasks as they are added.
scope.autoAdvanceTime = true;
await scope.startup(true);
@ -354,7 +350,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
server.assertNoOtherRequests();
});
it('initializes prefetched content correctly, after a request kicks it off', async() => {
it('initializes prefetched content correctly, after a request kicks it off', async () => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized;
server.assertSawRequestFor('ngsw.json');
@ -366,7 +362,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
server.assertNoOtherRequests();
});
it('initializes the service worker on fetch if it has not yet been initialized', async() => {
it('initializes the service worker on fetch if it has not yet been initialized', async () => {
// Driver is initially uninitialized.
expect(driver.initialized).toBeNull();
expect(driver['latestHash']).toBeNull();
@ -385,7 +381,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
server.assertNoOtherRequests();
});
it('initializes the service worker on message if it has not yet been initialized', async() => {
it('initializes the service worker on message if it has not yet been initialized', async () => {
// Driver is initially uninitialized.
expect(driver.initialized).toBeNull();
expect(driver['latestHash']).toBeNull();
@ -408,7 +404,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
server.assertNoOtherRequests();
});
it('handles non-relative URLs', async() => {
it('handles non-relative URLs', async () => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized;
server.clearRequests();
@ -416,19 +412,19 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
server.assertNoOtherRequests();
});
it('handles actual errors from the browser', async() => {
it('handles actual errors from the browser', async () => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized;
server.clearRequests();
const [resPromise, done] = scope.handleFetch(new MockRequest('/error.txt'), 'default');
await done;
const res = (await resPromise) !;
const res = (await resPromise)!;
expect(res.status).toEqual(504);
expect(res.statusText).toEqual('Gateway Timeout');
});
it('handles redirected responses', async() => {
it('handles redirected responses', async () => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized;
server.clearRequests();
@ -436,7 +432,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
server.assertNoOtherRequests();
});
it('caches lazy content on-request', async() => {
it('caches lazy content on-request', async () => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized;
server.clearRequests();
@ -450,11 +446,11 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
server.assertNoOtherRequests();
});
it('updates to new content when requested', async() => {
it('updates to new content when requested', async () => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized;
const client = scope.clients.getMock('default') !;
const client = scope.clients.getMock('default')!;
expect(client.messages).toEqual([]);
scope.updateServerState(serverUpdate);
@ -483,7 +479,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
serverUpdate.assertNoOtherRequests();
});
it('detects new version even if only `manifest.timestamp` is different', async() => {
it('detects new version even if only `manifest.timestamp` is different', async () => {
expect(await makeRequest(scope, '/foo.txt', 'newClient')).toEqual('this is foo');
await driver.initialized;
@ -496,11 +492,11 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
expect(await makeRequest(scope, '/foo.txt', 'newestClient')).toEqual('this is foo');
});
it('updates a specific client to new content on request', async() => {
it('updates a specific client to new content on request', async () => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized;
const client = scope.clients.getMock('default') !;
const client = scope.clients.getMock('default')!;
expect(client.messages).toEqual([]);
scope.updateServerState(serverUpdate);
@ -524,7 +520,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo v2');
});
it('handles empty client ID', async() => {
it('handles empty client ID', async () => {
// Initialize the SW.
expect(await makeNavigationRequest(scope, '/foo/file1', '')).toEqual('this is foo');
expect(await makeNavigationRequest(scope, '/bar/file2', null)).toEqual('this is foo');
@ -539,7 +535,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
expect(await makeNavigationRequest(scope, '/bar/file2', null)).toEqual('this is foo v2');
});
it('checks for updates on restart', async() => {
it('checks for updates on restart', async () => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized;
@ -561,7 +557,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
serverUpdate.assertNoOtherRequests();
});
it('checks for updates on navigation', async() => {
it('checks for updates on navigation', async () => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized;
server.clearRequests();
@ -574,7 +570,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
server.assertSawRequestFor('ngsw.json');
});
it('does not make concurrent checks for updates on navigation', async() => {
it('does not make concurrent checks for updates on navigation', async () => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized;
server.clearRequests();
@ -590,7 +586,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
server.assertNoOtherRequests();
});
it('preserves multiple client assignments across restarts', async() => {
it('preserves multiple client assignments across restarts', async () => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized;
@ -610,11 +606,11 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
serverUpdate.assertNoOtherRequests();
});
it('updates when refreshed', async() => {
it('updates when refreshed', async () => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized;
const client = scope.clients.getMock('default') !;
const client = scope.clients.getMock('default')!;
scope.updateServerState(serverUpdate);
expect(await driver.checkForUpdate()).toEqual(true);
@ -637,7 +633,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
serverUpdate.assertNoOtherRequests();
});
it('cleans up properly when manually requested', async() => {
it('cleans up properly when manually requested', async () => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized;
@ -657,7 +653,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
serverUpdate.assertNoOtherRequests();
});
it('cleans up properly on restart', async() => {
it('cleans up properly on restart', async () => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized;
@ -688,7 +684,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
expect(hasOriginalCaches).toEqual(false);
});
it('shows notifications for push notifications', async() => {
it('shows notifications for push notifications', async () => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized;
await scope.handlePush({
@ -701,7 +697,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
title: 'This is a test',
options: {title: 'This is a test', body: 'Test body'},
}]);
expect(scope.clients.getMock('default') !.messages).toEqual([{
expect(scope.clients.getMock('default')!.messages).toEqual([{
type: 'PUSH',
data: {
notification: {
@ -712,12 +708,12 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
}]);
});
it('broadcasts notification click events with action', async() => {
it('broadcasts notification click events with action', async () => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized;
await scope.handleClick(
{title: 'This is a test with action', body: 'Test body with action'}, 'button');
const message: any = scope.clients.getMock('default') !.messages[0];
const message: any = scope.clients.getMock('default')!.messages[0];
expect(message.type).toEqual('NOTIFICATION_CLICK');
expect(message.data.action).toEqual('button');
@ -725,12 +721,12 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
expect(message.data.notification.body).toEqual('Test body with action');
});
it('broadcasts notification click events without action', async() => {
it('broadcasts notification click events without action', async () => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized;
await scope.handleClick(
{title: 'This is a test without action', body: 'Test body without action'});
const message: any = scope.clients.getMock('default') !.messages[0];
const message: any = scope.clients.getMock('default')!.messages[0];
expect(message.type).toEqual('NOTIFICATION_CLICK');
expect(message.data.action).toBeUndefined();
@ -738,7 +734,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
expect(message.data.notification.body).toEqual('Test body without action');
});
it('prefetches updates to lazy cache when set', async() => {
it('prefetches updates to lazy cache when set', async () => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized;
@ -783,14 +779,14 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
serverUpdate.assertNoOtherRequests();
});
it('should bypass serviceworker on ngsw-bypass parameter', async() => {
it('should bypass serviceworker on ngsw-bypass parameter', async () => {
await makeRequest(scope, '/foo.txt', undefined, {headers: {'ngsw-bypass': 'true'}});
server.assertNoRequestFor('/foo.txt');
await makeRequest(scope, '/foo.txt', undefined, {headers: {'ngsw-bypass': 'anything'}});
server.assertNoRequestFor('/foo.txt');
await makeRequest(scope, '/foo.txt', undefined, {headers: {'ngsw-bypass': null !}});
await makeRequest(scope, '/foo.txt', undefined, {headers: {'ngsw-bypass': null!}});
server.assertNoRequestFor('/foo.txt');
await makeRequest(scope, '/foo.txt', undefined, {headers: {'NGSW-bypass': 'upperCASE'}});
@ -848,10 +844,9 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
await makeRequest(scope, '/bar?ngsw-byapass&testparam2');
server.assertSawRequestFor('/bar');
});
it('unregisters when manifest 404s', async() => {
it('unregisters when manifest 404s', async () => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized;
@ -861,7 +856,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
expect(await scope.caches.keys()).toEqual([]);
});
it('does not unregister or change state when offline (i.e. manifest 504s)', async() => {
it('does not unregister or change state when offline (i.e. manifest 504s)', async () => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized;
server.online = false;
@ -873,10 +868,10 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
});
it('does not unregister or change state when status code is 503 (service unavailable)',
async() => {
async () => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized;
spyOn(server, 'fetch').and.callFake((req: Request) => new MockResponse(null, {
spyOn(server, 'fetch').and.callFake(async (req: Request) => new MockResponse(null, {
status: 503,
statusText: 'Service Unavailable'
}));
@ -890,7 +885,8 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
describe('cache naming', () => {
// Helpers
const cacheKeysFor = (baseHref: string) =>
[`ngsw:${baseHref}:db:control`, `ngsw:${baseHref}:${manifestHash}:assets:assets:cache`,
[`ngsw:${baseHref}:db:control`,
`ngsw:${baseHref}:${manifestHash}:assets:assets:cache`,
`ngsw:${baseHref}:db:ngsw:${baseHref}:${manifestHash}:assets:assets:meta`,
`ngsw:${baseHref}:${manifestHash}:assets:other:cache`,
`ngsw:${baseHref}:db:ngsw:${baseHref}:${manifestHash}:assets:other:meta`,
@ -904,14 +900,14 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
`ngsw:${baseHref}:db:ngsw:${baseHref}:43:data:dynamic:api-static:age`,
];
const getClientAssignments = async(sw: SwTestHarness, baseHref: string) => {
const getClientAssignments = async (sw: SwTestHarness, baseHref: string) => {
const cache = await sw.caches.open(`ngsw:${baseHref}:db:control`) as unknown as MockCache;
const dehydrated = cache.dehydrate();
return JSON.parse(dehydrated['/assignments'].body !);
return JSON.parse(dehydrated['/assignments'].body!);
};
const initializeSwFor =
async(baseHref: string, initialCacheState = '{}', serverState = server) => {
async (baseHref: string, initialCacheState = '{}', serverState = server) => {
const newScope = new SwTestHarnessBuilder(`http://localhost${baseHref}`)
.withCacheState(initialCacheState)
.withServerState(serverState)
@ -924,7 +920,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
return newScope;
};
it('includes the SW scope in all cache names', async() => {
it('includes the SW scope in all cache names', async () => {
// Default SW with scope `/`.
await makeRequest(scope, '/foo.txt');
await driver.initialized;
@ -941,7 +937,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
expect(fooCacheNames.every(name => name.includes('/foo/'))).toBe(true);
});
it('does not affect caches from other scopes', async() => {
it('does not affect caches from other scopes', async () => {
// Create SW with scope `/foo/`.
const fooScope = await initializeSwFor('/foo/');
const fooAssignments = await getClientAssignments(fooScope, '/foo/');
@ -964,7 +960,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
expect(fooAssignments2).toEqual({_foo_: manifestHash});
});
it('updates existing caches for same scope', async() => {
it('updates existing caches for same scope', async () => {
// Create SW with scope `/foo/`.
const fooScope = await initializeSwFor('/foo/');
await makeRequest(fooScope, '/foo.txt', '_bar_');
@ -1000,27 +996,27 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
});
describe('unhashed requests', () => {
beforeEach(async() => {
beforeEach(async () => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized;
server.clearRequests();
});
it('are cached appropriately', async() => {
it('are cached appropriately', async () => {
expect(await makeRequest(scope, '/unhashed/a.txt')).toEqual('this is unhashed');
server.assertSawRequestFor('/unhashed/a.txt');
expect(await makeRequest(scope, '/unhashed/a.txt')).toEqual('this is unhashed');
server.assertNoOtherRequests();
});
it(`doesn't error when 'Cache-Control' is 'no-cache'`, async() => {
it(`doesn't error when 'Cache-Control' is 'no-cache'`, async () => {
expect(await makeRequest(scope, '/unhashed/b.txt')).toEqual('this is unhashed b');
server.assertSawRequestFor('/unhashed/b.txt');
expect(await makeRequest(scope, '/unhashed/b.txt')).toEqual('this is unhashed b');
server.assertNoOtherRequests();
});
it('avoid opaque responses', async() => {
it('avoid opaque responses', async () => {
expect(await makeRequest(scope, '/unhashed/a.txt', 'default', {
credentials: 'include'
})).toEqual('this is unhashed');
@ -1029,7 +1025,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
server.assertNoOtherRequests();
});
it('expire according to Cache-Control headers', async() => {
it('expire according to Cache-Control headers', async () => {
expect(await makeRequest(scope, '/unhashed/a.txt')).toEqual('this is unhashed');
server.clearRequests();
@ -1052,7 +1048,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
server.assertNoOtherRequests();
});
it('survive serialization', async() => {
it('survive serialization', async () => {
expect(await makeRequest(scope, '/unhashed/a.txt')).toEqual('this is unhashed');
server.clearRequests();
@ -1075,7 +1071,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
server.assertNoRequestFor('/unhashed/a.txt');
});
it('get carried over during updates', async() => {
it('get carried over during updates', async () => {
expect(await makeRequest(scope, '/unhashed/a.txt')).toEqual('this is unhashed');
server.clearRequests();
@ -1108,28 +1104,28 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
const navRequest = (url: string, init = {}) =>
makeNavigationRequest(scope, url, undefined, init);
beforeEach(async() => {
beforeEach(async () => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized;
server.clearRequests();
});
it('redirects to index on a route-like request', async() => {
it('redirects to index on a route-like request', async () => {
expect(await navRequest('/baz')).toEqual('this is foo');
server.assertNoOtherRequests();
});
it('redirects to index on a request to the origin URL request', async() => {
it('redirects to index on a request to the origin URL request', async () => {
expect(await navRequest('http://localhost/')).toEqual('this is foo');
server.assertNoOtherRequests();
});
it('does not redirect to index on a non-navigation request', async() => {
it('does not redirect to index on a non-navigation request', async () => {
expect(await navRequest('/baz', {mode: undefined})).toBeNull();
server.assertSawRequestFor('/baz');
});
it('does not redirect to index on a request that does not accept HTML', async() => {
it('does not redirect to index on a request that does not accept HTML', async () => {
expect(await navRequest('/baz', {headers: {}})).toBeNull();
server.assertSawRequestFor('/baz');
@ -1137,7 +1133,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
server.assertSawRequestFor('/qux');
});
it('does not redirect to index on a request with an extension', async() => {
it('does not redirect to index on a request with an extension', async () => {
expect(await navRequest('/baz.html')).toBeNull();
server.assertSawRequestFor('/baz.html');
@ -1146,7 +1142,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
server.assertNoOtherRequests();
});
it('does not redirect to index if the URL contains `__`', async() => {
it('does not redirect to index if the URL contains `__`', async () => {
expect(await navRequest('/baz/x__x')).toBeNull();
server.assertSawRequestFor('/baz/x__x');
@ -1161,13 +1157,13 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
});
describe('(with custom `navigationUrls`)', () => {
beforeEach(async() => {
beforeEach(async () => {
scope.updateServerState(serverUpdate);
await driver.checkForUpdate();
serverUpdate.clearRequests();
});
it('redirects to index on a request that matches any positive pattern', async() => {
it('redirects to index on a request that matches any positive pattern', async () => {
expect(await navRequest('/foo/file0')).toBeNull();
serverUpdate.assertSawRequestFor('/foo/file0');
@ -1178,24 +1174,22 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
serverUpdate.assertNoOtherRequests();
});
it('does not redirect to index on a request that matches any negative pattern', async() => {
it('does not redirect to index on a request that matches any negative pattern', async () => {
expect(await navRequest('/ignored/file1')).toBe('this is not handled by the SW');
serverUpdate.assertSawRequestFor('/ignored/file1');
expect(await navRequest('/ignored/dir/file2'))
.toBe('this is not handled by the SW either');
expect(await navRequest('/ignored/dir/file2')).toBe('this is not handled by the SW either');
serverUpdate.assertSawRequestFor('/ignored/dir/file2');
expect(await navRequest('/ignored/directory/file2')).toBe('this is foo v2');
serverUpdate.assertNoOtherRequests();
});
it('strips URL query before checking `navigationUrls`', async() => {
it('strips URL query before checking `navigationUrls`', async () => {
expect(await navRequest('/foo/file1?query=/a/b')).toBe('this is foo v2');
serverUpdate.assertNoOtherRequests();
expect(await navRequest('/ignored/file1?query=/a/b'))
.toBe('this is not handled by the SW');
expect(await navRequest('/ignored/file1?query=/a/b')).toBe('this is not handled by the SW');
serverUpdate.assertSawRequestFor('/ignored/file1');
expect(await navRequest('/ignored/dir/file2?query=/a/b'))
@ -1203,7 +1197,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
serverUpdate.assertSawRequestFor('/ignored/dir/file2');
});
it('strips registration scope before checking `navigationUrls`', async() => {
it('strips registration scope before checking `navigationUrls`', async () => {
expect(await navRequest('http://localhost/ignored/file1'))
.toBe('this is not handled by the SW');
serverUpdate.assertSawRequestFor('/ignored/file1');
@ -1212,7 +1206,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
});
describe('cleanupOldSwCaches()', () => {
it('should delete the correct caches', async() => {
it('should delete the correct caches', async () => {
const oldSwCacheNames = [
// Example cache names from the beta versions of `@angular/service-worker`.
'ngsw:active',
@ -1238,12 +1232,12 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
expect(await scope.caches.keys()).toEqual(otherCacheNames);
});
it('should delete other caches even if deleting one of them fails', async() => {
it('should delete other caches even if deleting one of them fails', async () => {
const oldSwCacheNames = ['ngsw:active', 'ngsw:staged', 'ngsw:manifest:a1b2c3:super:duper'];
const deleteSpy = spyOn(scope.caches, 'delete')
const deleteSpy =
spyOn(scope.caches, 'delete')
.and.callFake(
(cacheName: string) =>
Promise.reject(`Failed to delete cache '${cacheName}'.`));
(cacheName: string) => Promise.reject(`Failed to delete cache '${cacheName}'.`));
await Promise.all(oldSwCacheNames.map(name => scope.caches.open(name)));
const error = await driver.cleanupOldSwCaches().catch(err => err);
@ -1255,7 +1249,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
});
describe('bugs', () => {
it('does not crash with bad index hash', async() => {
it('does not crash with bad index hash', async () => {
scope = new SwTestHarnessBuilder().withServerState(brokenServer).build();
(scope.registration as any).scope = 'http://site.com';
driver = new Driver(scope, scope, new CacheDatabase(scope, scope));
@ -1263,7 +1257,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo (broken)');
});
it('enters degraded mode when update has a bad index', async() => {
it('enters degraded mode when update has a bad index', async () => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized;
server.clearRequests();
@ -1281,7 +1275,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
expect(driver.state).toEqual(DriverReadyState.EXISTING_CLIENTS_ONLY);
});
it('enters degraded mode when failing to write to cache', async() => {
it('enters degraded mode when failing to write to cache', async () => {
// Initialize the SW.
await makeRequest(scope, '/foo.txt');
await driver.initialized;
@ -1304,7 +1298,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
});
it('keeps serving api requests with freshness strategy when failing to write to cache',
async() => {
async () => {
// Initialize the SW.
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized;
@ -1325,7 +1319,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
});
it('keeps serving api requests with performance strategy when failing to write to cache',
async() => {
async () => {
// Initialize the SW.
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized;
@ -1347,7 +1341,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
it('keeps serving mutating api requests when failing to write to cache',
// sw can invalidate LRU cache entry and try to write to cache storage on mutating request
async() => {
async () => {
// Initialize the SW.
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized;
@ -1366,7 +1360,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
server.assertSawRequestFor('/api/foo');
});
it('enters degraded mode when something goes wrong with the latest version', async() => {
it('enters degraded mode when something goes wrong with the latest version', async () => {
await driver.initialized;
// Two clients on initial version.
@ -1406,7 +1400,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
});
it('recovers from degraded `EXISTING_CLIENTS_ONLY` mode as soon as there is a valid update',
async() => {
async () => {
await driver.initialized;
expect(driver.state).toBe(DriverReadyState.NORMAL);
@ -1421,8 +1415,8 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
expect(driver.state).toBe(DriverReadyState.NORMAL);
});
it('ignores invalid `only-if-cached` requests ', async() => {
const requestFoo = (cache: RequestCache | 'only-if-cached', mode: RequestMode) =>
it('ignores invalid `only-if-cached` requests ', async () => {
const requestFoo = (cache: RequestCache|'only-if-cached', mode: RequestMode) =>
makeRequest(scope, '/foo.txt', undefined, {cache, mode});
expect(await requestFoo('default', 'no-cors')).toBe('this is foo');
@ -1430,9 +1424,10 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
expect(await requestFoo('only-if-cached', 'no-cors')).toBeNull();
});
it('ignores passive mixed content requests ', async() => {
it('ignores passive mixed content requests ', async () => {
const scopeFetchSpy = spyOn(scope, 'fetch').and.callThrough();
const getRequestUrls = () => scopeFetchSpy.calls.allArgs().map(args => args[0].url);
const getRequestUrls = () =>
(scopeFetchSpy.calls.allArgs() as [Request][]).map(args => args[0].url);
const httpScopeUrl = 'http://mock.origin.dev';
const httpsScopeUrl = 'https://mock.origin.dev';
@ -1475,7 +1470,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
// Test this bug: https://github.com/angular/angular/issues/27209
it('fills previous versions of manifests with default navigation urls for backwards compatibility',
async() => {
async () => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized;
scope.updateServerState(serverUpdate);
@ -1483,12 +1478,12 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
});
});
});
});
});
})();
async function makeRequest(
scope: SwTestHarness, url: string, clientId: string | null = 'default', init?: Object):
Promise<string|null> {
scope: SwTestHarness, url: string, clientId: string|null = 'default',
init?: Object): Promise<string|null> {
const [resPromise, done] = scope.handleFetch(new MockRequest(url, init), clientId);
await done;
const res = await resPromise;
@ -1496,16 +1491,17 @@ async function makeRequest(
return res.text();
}
return null;
}
}
function makeNavigationRequest(
scope: SwTestHarness, url: string, clientId?: string | null, init: Object = {}):
Promise<string|null> {
scope: SwTestHarness, url: string, clientId?: string|null,
init: Object = {}): Promise<string|null> {
return makeRequest(scope, url, clientId, {
headers: {
Accept: 'text/plain, text/html, text/css',
...(init as any).headers,
},
mode: 'navigate', ...init,
mode: 'navigate',
...init,
});
}
}

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ChangeDetectorRef, Component, EventEmitter, Input, NO_ERRORS_SCHEMA, NgModule, NgModuleFactory, NgZone, OnChanges, OnDestroy, Output, SimpleChange, SimpleChanges, Testability, destroyPlatform, forwardRef} from '@angular/core';
import {ChangeDetectorRef, Component, destroyPlatform, EventEmitter, forwardRef, Input, NgModule, NgModuleFactory, NgZone, NO_ERRORS_SCHEMA, OnChanges, OnDestroy, Output, SimpleChange, SimpleChanges, Testability} from '@angular/core';
import {async, fakeAsync, flushMicrotasks, tick} from '@angular/core/testing';
import {BrowserModule} from '@angular/platform-browser';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
@ -14,7 +14,6 @@ import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import * as angular from '../../common/src/angular1';
import {$EXCEPTION_HANDLER, $ROOT_SCOPE} from '../../common/src/constants';
import {html, multiTrim, withEachNg1Version} from '../../common/test/helpers/common_test_helpers';
import {UpgradeAdapter, UpgradeAdapterRef} from '../src/upgrade_adapter';
@ -24,7 +23,6 @@ declare global {
withEachNg1Version(() => {
describe('adapter: ng1 to ng2', () => {
beforeEach(() => destroyPlatform());
afterEach(() => destroyPlatform());
@ -113,11 +111,12 @@ withEachNg1Version(() => {
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
adapter.bootstrap(element, ['ng1']).ready((ref) => {
expect(platformRef.bootstrapModule).toHaveBeenCalledWith(jasmine.any(Function), [
{providers: []}, jasmine.any(Object)
{providers: []}, jasmine.any(Object) as any
]);
expect(platformRef.bootstrapModuleFactory)
.toHaveBeenCalledWith(
jasmine.any(NgModuleFactory), {providers: [], ngZone: jasmine.any(NgZone)});
jasmine.any(NgModuleFactory),
jasmine.objectContaining({ngZone: jasmine.any(NgZone), providers: []}));
ref.dispose();
});
}));
@ -232,7 +231,9 @@ withEachNg1Version(() => {
})
class Ng2 {
l: any;
constructor() { this.l = l; }
constructor() {
this.l = l;
}
}
@NgModule({
@ -262,7 +263,9 @@ withEachNg1Version(() => {
@Component({selector: 'my-app', template: '<my-child [value]="value"></my-child>'})
class AppComponent {
value?: number;
constructor() { appComponent = this; }
constructor() {
appComponent = this;
}
}
@Component({
@ -272,7 +275,9 @@ withEachNg1Version(() => {
class ChildComponent {
valueFromPromise?: number;
@Input()
set value(v: number) { expect(NgZone.isInAngularZone()).toBe(true); }
set value(v: number) {
expect(NgZone.isInAngularZone()).toBe(true);
}
constructor(private zone: NgZone) {}
@ -352,14 +357,15 @@ withEachNg1Version(() => {
const element = html('<ng2></ng2>');
adapter.bootstrap(element, ['ng1']).ready((ref) => {
expect(multiTrim(document.body.textContent !)).toBe('It works');
expect(multiTrim(document.body.textContent!)).toBe('It works');
});
}));
it('should bind properties, events', async(() => {
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
const ng1Module =
angular.module_('ng1', []).value($EXCEPTION_HANDLER, (err: any) => { throw err; });
const ng1Module = angular.module_('ng1', []).value($EXCEPTION_HANDLER, (err: any) => {
throw err;
});
ng1Module.run(($rootScope: any) => {
$rootScope.name = 'world';
@ -409,8 +415,8 @@ withEachNg1Version(() => {
}
const actValue = changes[prop].currentValue;
if (actValue != value) {
throw new Error(
`Expected changes record for'${prop}' to be '${value}' but was '${actValue}'`);
throw new Error(`Expected changes record for'${prop}' to be '${
value}' but was '${actValue}'`);
}
};
@ -458,7 +464,7 @@ withEachNg1Version(() => {
| modelA: {{modelA}}; modelB: {{modelB}}; eventA: {{eventA}}; eventB: {{eventB}};
</div>`);
adapter.bootstrap(element, ['ng1']).ready((ref) => {
expect(multiTrim(document.body.textContent !))
expect(multiTrim(document.body.textContent!))
.toEqual(
'ignore: -; ' +
'literal: Text; interpolate: Hello world; ' +
@ -466,7 +472,7 @@ withEachNg1Version(() => {
'modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;');
ref.ng1RootScope.$apply('name = "everyone"');
expect(multiTrim(document.body.textContent !))
expect(multiTrim(document.body.textContent!))
.toEqual(
'ignore: -; ' +
'literal: Text; interpolate: Hello everyone; ' +
@ -475,7 +481,6 @@ withEachNg1Version(() => {
ref.dispose();
});
}));
it('should support two-way binding and event listener', async(() => {
@ -541,9 +546,9 @@ withEachNg1Version(() => {
ngOnChangesCount = 0;
firstChangesCount = 0;
// TODO(issue/24571): remove '!'.
initialValue !: string;
initialValue!: string;
// TODO(issue/24571): remove '!'.
@Input() foo !: string;
@Input() foo!: string;
ngOnChanges(changes: SimpleChanges) {
this.ngOnChangesCount++;
@ -590,7 +595,9 @@ withEachNg1Version(() => {
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
const ng1Module = angular.module_('ng1', []);
ng1Module.run(($rootScope: any /** TODO #9100 */) => { $rootScope.modelA = 'A'; });
ng1Module.run(($rootScope: any /** TODO #9100 */) => {
$rootScope.modelA = 'A';
});
let ng2Instance: Ng2;
@Component({selector: 'ng2', template: '{{_value}}'})
@ -598,11 +605,21 @@ withEachNg1Version(() => {
private _value: any = '';
private _onChangeCallback: (_: any) => void = () => {};
private _onTouchedCallback: () => void = () => {};
constructor() { ng2Instance = this; }
writeValue(value: any) { this._value = value; }
registerOnChange(fn: any) { this._onChangeCallback = fn; }
registerOnTouched(fn: any) { this._onTouchedCallback = fn; }
doTouch() { this._onTouchedCallback(); }
constructor() {
ng2Instance = this;
}
writeValue(value: any) {
this._value = value;
}
registerOnChange(fn: any) {
this._onChangeCallback = fn;
}
registerOnTouched(fn: any) {
this._onTouchedCallback = fn;
}
doTouch() {
this._onTouchedCallback();
}
doChange(newValue: string) {
this._value = newValue;
this._onChangeCallback(newValue);
@ -653,14 +670,18 @@ withEachNg1Version(() => {
return {
template: '<div ng-if="!destroyIt"><ng2></ng2></div>',
controller: function($rootScope: any, $timeout: Function) {
$timeout(() => { $rootScope.destroyIt = true; });
$timeout(() => {
$rootScope.destroyIt = true;
});
}
};
});
@Component({selector: 'ng2', template: 'test'})
class Ng2 {
ngOnDestroy() { onDestroyed.emit('destroyed'); }
ngOnDestroy() {
onDestroyed.emit('destroyed');
}
}
@NgModule({
@ -673,7 +694,9 @@ withEachNg1Version(() => {
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
const element = html('<ng1></ng1>');
adapter.bootstrap(element, ['ng1']).ready((ref) => {
onDestroyed.subscribe(() => { ref.dispose(); });
onDestroyed.subscribe(() => {
ref.dispose();
});
});
}));
@ -689,7 +712,9 @@ withEachNg1Version(() => {
@Component({selector: 'ng2-inner', template: 'test'})
class Ng2InnerComponent implements OnDestroy {
ngOnDestroy() { destroyed = true; }
ngOnDestroy() {
destroyed = true;
}
}
@NgModule({
@ -789,7 +814,7 @@ withEachNg1Version(() => {
@Component({selector: 'ng2', template: 'ng2-{{ itemId }}(<ng-content></ng-content>)'})
class Ng2Component {
// TODO(issue/24571): remove '!'.
@Input() itemId !: string;
@Input() itemId!: string;
}
@NgModule({imports: [BrowserModule], declarations: [Ng2Component]})
@ -838,7 +863,7 @@ withEachNg1Version(() => {
ng1Module.directive('rootComponent', adapter.downgradeNg2Component(RootComponent));
document.body.innerHTML = '<root-component></root-component>';
adapter.bootstrap(document.body.firstElementChild !, ['myExample']).ready((ref) => {
adapter.bootstrap(document.body.firstElementChild!, ['myExample']).ready((ref) => {
expect(multiTrim(document.body.textContent)).toEqual('It works!');
ref.dispose();
});
@ -868,7 +893,9 @@ withEachNg1Version(() => {
dataA = 'foo';
dataB = 'bar';
constructor() { ng2ComponentInstance = this; }
constructor() {
ng2ComponentInstance = this;
}
}
// Define `ng1Module`
@ -888,8 +915,8 @@ withEachNg1Version(() => {
const element = html(`<ng2></ng2>`);
adapter.bootstrap(element, ['ng1Module']).ready(ref => {
const ng1 = element.querySelector('ng1') !;
const ng1Controller = angular.element(ng1).controller !('ng1');
const ng1 = element.querySelector('ng1')!;
const ng1Controller = angular.element(ng1).controller!('ng1');
expect(multiTrim(element.textContent)).toBe('Inside: foo, bar | Outside: foo, bar');
@ -932,7 +959,9 @@ withEachNg1Version(() => {
dataA = {value: 'foo'};
dataB = {value: 'bar'};
constructor() { ng2ComponentInstance = this; }
constructor() {
ng2ComponentInstance = this;
}
}
// Define `ng1Module`
@ -952,8 +981,8 @@ withEachNg1Version(() => {
const element = html(`<ng2></ng2>`);
adapter.bootstrap(element, ['ng1Module']).ready(ref => {
const ng1 = element.querySelector('ng1') !;
const ng1Controller = angular.element(ng1).controller !('ng1');
const ng1 = element.querySelector('ng1')!;
const ng1Controller = angular.element(ng1).controller!('ng1');
expect(multiTrim(element.textContent)).toBe('Inside: foo, bar | Outside: foo, bar');
@ -996,7 +1025,9 @@ withEachNg1Version(() => {
dataA = {value: 'foo'};
dataB = {value: 'bar'};
constructor() { ng2ComponentInstance = this; }
constructor() {
ng2ComponentInstance = this;
}
}
// Define `ng1Module`
@ -1016,8 +1047,8 @@ withEachNg1Version(() => {
const element = html(`<ng2></ng2>`);
adapter.bootstrap(element, ['ng1Module']).ready(ref => {
const ng1 = element.querySelector('ng1') !;
const ng1Controller = angular.element(ng1).controller !('ng1');
const ng1 = element.querySelector('ng1')!;
const ng1Controller = angular.element(ng1).controller!('ng1');
expect(multiTrim(element.textContent)).toBe('Inside: foo, bar | Outside: foo, bar');
@ -1077,8 +1108,8 @@ withEachNg1Version(() => {
const element = html(`<ng2></ng2>`);
adapter.bootstrap(element, ['ng1Module']).ready(ref => {
const ng1 = element.querySelector('ng1') !;
const ng1Controller = angular.element(ng1).controller !('ng1');
const ng1 = element.querySelector('ng1')!;
const ng1Controller = angular.element(ng1).controller!('ng1');
expect(multiTrim(element.textContent)).toBe('Inside: - | Outside: foo, bar');
@ -1204,7 +1235,9 @@ withEachNg1Version(() => {
restrict: 'E',
template: '{{someText}} - Length: {{data.length}}',
scope: {data: '='},
controller: function($scope: any) { $scope.someText = 'ng1 - Data: ' + $scope.data; }
controller: function($scope: any) {
$scope.someText = 'ng1 - Data: ' + $scope.data;
}
};
};
@ -1248,7 +1281,9 @@ withEachNg1Version(() => {
restrict: 'E',
template: '{{someText}} - Length: {{data.length}}',
scope: {data: '='},
link: function($scope: any) { $scope.someText = 'ng1 - Data: ' + $scope.data; }
link: function($scope: any) {
$scope.someText = 'ng1 - Data: ' + $scope.data;
}
};
};
@ -1291,7 +1326,9 @@ withEachNg1Version(() => {
cbFn(200, `${method}:${url}`);
});
const ng1 = () => { return {templateUrl: 'url.html'}; };
const ng1 = () => {
return {templateUrl: 'url.html'};
};
ng1Module.directive('ng1', ng1);
@Component({selector: 'ng2', template: '<ng1></ng1>'})
class Ng2 {
@ -1320,7 +1357,13 @@ withEachNg1Version(() => {
cbFn(200, `${method}:${url}`);
});
const ng1 = () => { return {templateUrl() { return 'url.html'; }}; };
const ng1 = () => {
return {
templateUrl() {
return 'url.html';
}
};
};
ng1Module.directive('ng1', ng1);
@Component({selector: 'ng2', template: '<ng1></ng1>'})
class Ng2 {
@ -1345,7 +1388,9 @@ withEachNg1Version(() => {
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
const ng1Module = angular.module_('ng1', []);
const ng1 = () => { return {template: ''}; };
const ng1 = () => {
return {template: ''};
};
ng1Module.directive('ng1', ng1);
@Component({selector: 'ng2', template: '<ng1></ng1>'})
@ -1371,7 +1416,13 @@ withEachNg1Version(() => {
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
const ng1Module = angular.module_('ng1', []);
const ng1 = () => { return {template() { return ''; }}; };
const ng1 = () => {
return {
template() {
return '';
}
};
};
ng1Module.directive('ng1', ng1);
@Component({selector: 'ng2', template: '<ng1></ng1>'})
@ -1398,7 +1449,9 @@ withEachNg1Version(() => {
const ng1Module = angular.module_('ng1', []);
ng1Module.run(($templateCache: any) => $templateCache.put('url.html', 'WORKS'));
const ng1 = () => { return {templateUrl: 'url.html'}; };
const ng1 = () => {
return {templateUrl: 'url.html'};
};
ng1Module.directive('ng1', ng1);
@Component({selector: 'ng2', template: '<ng1></ng1>'})
@ -1431,13 +1484,20 @@ withEachNg1Version(() => {
'{{ctl.scope}}; {{ctl.isClass}}; {{ctl.hasElement}}; {{ctl.isPublished()}}',
controllerAs: 'ctl',
controller: class {
scope: any; hasElement: string; $element: any; isClass: any;
scope: any;
hasElement: string;
$element: any;
isClass: any;
constructor($scope: any, $element: any) {
this.verifyIAmAClass();
this.scope = $scope.$parent.$parent == $scope.$root ? 'scope' : 'wrong-scope';
this.hasElement = $element[0].nodeName;
this.$element = $element;
} verifyIAmAClass() { this.isClass = 'isClass'; } isPublished() {
}
verifyIAmAClass() {
this.isClass = 'isClass';
}
isPublished() {
return this.$element.controller('ng1') == this ? 'published' : 'not-published';
}
}
@ -1543,7 +1603,9 @@ withEachNg1Version(() => {
template: '{{ctl.status}}',
require: 'ng1',
controllerAs: 'ctrl',
controller: class {status = 'WORKS';},
controller: class {
status = 'WORKS';
},
link: function(scope: any, element: any, attrs: any, linkController: any) {
expect(scope.$root).toEqual($rootScope);
expect(element[0].nodeName).toEqual('NG1');
@ -1577,7 +1639,13 @@ withEachNg1Version(() => {
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
const ng1Module = angular.module_('ng1', []);
const parent = () => { return {controller: class {parent = 'PARENT';}}; };
const parent = () => {
return {
controller: class {
parent = 'PARENT';
}
};
};
const ng1 = () => {
return {
scope: {title: '@'},
@ -1585,7 +1653,9 @@ withEachNg1Version(() => {
template: '{{parent.parent}}:{{ng1.status}}',
require: ['ng1', '^parent', '?^^notFound'],
controllerAs: 'ctrl',
controller: class {status = 'WORKS';},
controller: class {
status = 'WORKS';
},
link: function(scope: any, element: any, attrs: any, linkControllers: any) {
expect(linkControllers[0].status).toEqual('WORKS');
expect(linkControllers[1].parent).toEqual('PARENT');
@ -1633,15 +1703,20 @@ withEachNg1Version(() => {
scope: {},
bindToController: true,
controllerAs: '$ctrl',
controller: class {$onInit() { $onInitSpyA(); }}
controller: class {
$onInit() {
$onInitSpyA();
}
}
}))
.directive(
'ng1B', () => ({
.directive('ng1B', () => ({
template: '',
scope: {},
bindToController: false,
controllerAs: '$ctrl',
controller: function(this: any) { this.$onInit = $onInitSpyB; }
controller: function(this: any) {
this.$onInit = $onInitSpyB;
}
}))
.directive('ng2', adapter.downgradeNg2Component(Ng2Component));
@ -1718,7 +1793,9 @@ withEachNg1Version(() => {
@Component({selector: 'ng2', template: '<ng1-a></ng1-a> | <ng1-b></ng1-b>'})
class Ng2Component {
constructor(cd: ChangeDetectorRef) { changeDetector = cd; }
constructor(cd: ChangeDetectorRef) {
changeDetector = cd;
}
}
angular.module_('ng1', [])
@ -1727,15 +1804,20 @@ withEachNg1Version(() => {
scope: {},
bindToController: true,
controllerAs: '$ctrl',
controller: class {$doCheck() { $doCheckSpyA(); }}
controller: class {
$doCheck() {
$doCheckSpyA();
}
}
}))
.directive(
'ng1B', () => ({
.directive('ng1B', () => ({
template: '',
scope: {},
bindToController: false,
controllerAs: '$ctrl',
controller: function(this: any) { this.$doCheck = $doCheckSpyB; }
controller: function(this: any) {
this.$doCheck = $doCheckSpyB;
}
}))
.directive('ng2', adapter.downgradeNg2Component(Ng2Component));
@ -1773,7 +1855,9 @@ withEachNg1Version(() => {
@Component({selector: 'ng2', template: '<ng1-a></ng1-a> | <ng1-b></ng1-b>'})
class Ng2Component {
constructor(cd: ChangeDetectorRef) { changeDetector = cd; }
constructor(cd: ChangeDetectorRef) {
changeDetector = cd;
}
}
angular.module_('ng1', [])
@ -1835,15 +1919,20 @@ withEachNg1Version(() => {
scope: {},
bindToController: true,
controllerAs: '$ctrl',
controller: class {$postLink() { $postLinkSpyA(); }}
controller: class {
$postLink() {
$postLinkSpyA();
}
}
}))
.directive(
'ng1B', () => ({
.directive('ng1B', () => ({
template: '',
scope: {},
bindToController: false,
controllerAs: '$ctrl',
controller: function(this: any) { this.$postLink = $postLinkSpyB; }
controller: function(this: any) {
this.$postLink = $postLinkSpyB;
}
}))
.directive('ng2', adapter.downgradeNg2Component(Ng2Component));
@ -1924,7 +2013,9 @@ withEachNg1Version(() => {
template: '<ng1-a [valA]="val"></ng1-a> | <ng1-b [valB]="val"></ng1-b>'
})
class Ng2Component {
constructor() { ng2Instance = this; }
constructor() {
ng2Instance = this;
}
}
angular.module_('ng1', [])
@ -1937,15 +2028,15 @@ withEachNg1Version(() => {
this.$onChanges = $onChangesControllerSpyA;
}
}))
.directive(
'ng1B',
() => ({
.directive('ng1B', () => ({
template: '',
scope: {valB: '<'},
bindToController: false,
controllerAs: '$ctrl',
controller: class {
$onChanges(changes: SimpleChanges) { $onChangesControllerSpyB(changes); }
$onChanges(changes: SimpleChanges) {
$onChangesControllerSpyB(changes);
}
}
}))
.directive('ng2', adapter.downgradeNg2Component(Ng2Component))
@ -2022,7 +2113,9 @@ withEachNg1Version(() => {
})
class Ng2Component {
ng2Destroy: boolean = false;
constructor() { ng2ComponentInstance = this; }
constructor() {
ng2ComponentInstance = this;
}
}
// On browsers that don't support `requestAnimationFrame` (IE 9, Android <= 4.3),
@ -2036,15 +2129,20 @@ withEachNg1Version(() => {
scope: {},
bindToController: true,
controllerAs: '$ctrl',
controller: class {$onDestroy() { $onDestroySpyA(); }}
controller: class {
$onDestroy() {
$onDestroySpyA();
}
}
}))
.directive(
'ng1B', () => ({
.directive('ng1B', () => ({
template: '',
scope: {},
bindToController: false,
controllerAs: '$ctrl',
controller: function(this: any) { this.$onDestroy = $onDestroySpyB; }
controller: function(this: any) {
this.$onDestroy = $onDestroySpyB;
}
}))
.directive('ng2', adapter.downgradeNg2Component(Ng2Component));
@ -2112,7 +2210,9 @@ withEachNg1Version(() => {
})
class Ng2Component {
ng2Destroy: boolean = false;
constructor() { ng2ComponentInstance = this; }
constructor() {
ng2ComponentInstance = this;
}
}
// On browsers that don't support `requestAnimationFrame` (IE 9, Android <= 4.3),
@ -2187,7 +2287,9 @@ withEachNg1Version(() => {
@Component({selector: 'ng2', template: '<div *ngIf="!ng2Destroy"><ng1></ng1></div>'})
class Ng2Component {
ng2Destroy: boolean = false;
constructor() { ng2ComponentInstance = this; }
constructor() {
ng2ComponentInstance = this;
}
}
// On browsers that don't support `requestAnimationFrame` (IE 9, Android <= 4.3),
@ -2233,7 +2335,9 @@ withEachNg1Version(() => {
@Component({selector: 'ng2', template: '<div *ngIf="!ng2Destroy"><ng1></ng1></div>'})
class Ng2Component {
ng2Destroy: boolean = false;
constructor() { ng2ComponentInstance = this; }
constructor() {
ng2ComponentInstance = this;
}
}
// On browsers that don't support `requestAnimationFrame` (IE 9, Android <= 4.3),
@ -2245,8 +2349,8 @@ withEachNg1Version(() => {
.component('ng1', {
controller: class {
constructor(private $element: angular.IAugmentedJQuery) {} $onInit() {
this.$element.on !('$destroy', elementDestroyListener);
this.$element.contents !().on !('$destroy', descendantDestroyListener);
this.$element.on!('$destroy', elementDestroyListener);
this.$element.contents!().on!('$destroy', descendantDestroyListener);
}
},
template: '<div></div>'
@ -2287,8 +2391,8 @@ withEachNg1Version(() => {
const ng1Component: angular.IComponent = {
controller: class {
constructor(private $element: angular.IAugmentedJQuery) {} $onInit() {
this.$element.data !('test', 1);
this.$element.contents !().data !('test', 2);
this.$element.data!('test', 1);
this.$element.contents!().data!('test', 2);
ng1ComponentElement = this.$element;
}
@ -2301,7 +2405,9 @@ withEachNg1Version(() => {
class Ng2ComponentA {
destroyIt = false;
constructor() { ng2ComponentAInstance = this; }
constructor() {
ng2ComponentAInstance = this;
}
}
@Component({selector: 'ng2B', template: '<ng1></ng1>'})
@ -2330,15 +2436,15 @@ withEachNg1Version(() => {
const $rootScope = ref.ng1RootScope as any;
tick();
$rootScope.$digest();
expect(ng1ComponentElement.data !('test')).toBe(1);
expect(ng1ComponentElement.contents !().data !('test')).toBe(2);
expect(ng1ComponentElement.data!('test')).toBe(1);
expect(ng1ComponentElement.contents!().data!('test')).toBe(2);
ng2ComponentAInstance.destroyIt = true;
tick();
$rootScope.$digest();
expect(ng1ComponentElement.data !('test')).toBeUndefined();
expect(ng1ComponentElement.contents !().data !('test')).toBeUndefined();
expect(ng1ComponentElement.data!('test')).toBeUndefined();
expect(ng1ComponentElement.contents!().data!('test')).toBeUndefined();
});
}));
@ -2353,10 +2459,10 @@ withEachNg1Version(() => {
const ng1Component: angular.IComponent = {
controller: class {
constructor(private $element: angular.IAugmentedJQuery) {} $onInit() {
ng1DescendantElement = this.$element.contents !();
ng1DescendantElement = this.$element.contents!();
this.$element.on !('click', elementClickListener);
ng1DescendantElement.on !('click', descendantClickListener);
this.$element.on!('click', elementClickListener);
ng1DescendantElement.on!('click', descendantClickListener);
}
},
template: '<div></div>'
@ -2367,7 +2473,9 @@ withEachNg1Version(() => {
class Ng2ComponentA {
destroyIt = false;
constructor() { ng2ComponentAInstance = this; }
constructor() {
ng2ComponentAInstance = this;
}
}
@Component({selector: 'ng2B', template: '<ng1></ng1>'})
@ -2420,7 +2528,11 @@ withEachNg1Version(() => {
const ng1Directive: angular.IDirective = {
template: '',
link: {pre: () => log.push('ng1-pre')},
controller: class {constructor() { log.push('ng1-ctrl'); }}
controller: class {
constructor() {
log.push('ng1-ctrl');
}
}
};
// Define `Ng2Component`
@ -2577,7 +2689,11 @@ withEachNg1Version(() => {
const ng1Directive: angular.IDirective = {
template: '',
link: () => log.push('ng1-post'),
controller: class {$postLink() { log.push('ng1-$post'); }}
controller: class {
$postLink() {
log.push('ng1-$post');
}
}
};
// Define `Ng2Component`
@ -2627,13 +2743,17 @@ withEachNg1Version(() => {
class Ng2ComponentA {
value = 'foo';
showB = false;
constructor() { ng2ComponentAInstance = this; }
constructor() {
ng2ComponentAInstance = this;
}
}
@Component({selector: 'ng2B', template: 'ng2B({{ value }})'})
class Ng2ComponentB {
value = 'bar';
constructor() { ng2ComponentBInstance = this; }
constructor() {
ng2ComponentBInstance = this;
}
}
// Define `ng1Module`
@ -2678,7 +2798,10 @@ withEachNg1Version(() => {
template: 'ng1(<div ng-transclude>{{ $ctrl.value }}</div>)',
transclude: true,
controller: class {
value = 'from-ng1'; constructor() { ng1ControllerInstances.push(this); }
value = 'from-ng1';
constructor() {
ng1ControllerInstances.push(this);
}
}
};
@ -2697,7 +2820,9 @@ withEachNg1Version(() => {
})
class Ng2Component {
value = 'from-ng2';
constructor() { ng2ComponentInstance = this; }
constructor() {
ng2ComponentInstance = this;
}
}
// Define `ng1Module`
@ -2756,7 +2881,9 @@ withEachNg1Version(() => {
class Ng2Component {
x = 'foo';
y = 'bar';
constructor() { ng2ComponentInstance = this; }
constructor() {
ng2ComponentInstance = this;
}
}
// Define `ng1Module`
@ -2798,8 +2925,12 @@ withEachNg1Version(() => {
const ng1Component: angular.IComponent = {
template: 'ng1(default(<div ng-transclude="">fallback-{{ $ctrl.value }}</div>))',
transclude: {slotX: 'contentX', slotY: 'contentY'},
controller:
class {value = 'ng1'; constructor() { ng1ControllerInstances.push(this); }}
controller: class {
value = 'ng1';
constructor() {
ng1ControllerInstances.push(this);
}
}
};
// Define `Ng2Component`
@ -2830,7 +2961,9 @@ withEachNg1Version(() => {
class Ng2Component {
x = 'foo';
y = 'bar';
constructor() { ng2ComponentInstance = this; }
constructor() {
ng2ComponentInstance = this;
}
}
// Define `ng1Module`
@ -2880,7 +3013,11 @@ withEachNg1Version(() => {
)`,
transclude: {slotX: '?contentX', slotY: '?contentY'},
controller: class {
x = 'ng1X'; y = 'ng1Y'; constructor() { ng1ControllerInstances.push(this); }
x = 'ng1X';
y = 'ng1Y';
constructor() {
ng1ControllerInstances.push(this);
}
}
};
@ -2896,7 +3033,9 @@ withEachNg1Version(() => {
class Ng2Component {
x = 'ng2X';
y = 'ng2Y';
constructor() { ng2ComponentInstance = this; }
constructor() {
ng2ComponentInstance = this;
}
}
// Define `ng1Module`
@ -3000,7 +3139,9 @@ withEachNg1Version(() => {
x = 'foo';
y = 'bar';
show = true;
constructor() { ng2ComponentInstance = this; }
constructor() {
ng2ComponentInstance = this;
}
}
// Define `ng1Module`
@ -3202,13 +3343,18 @@ withEachNg1Version(() => {
const ng1Module = angular.module_('ng1', []);
let a1Injector: angular.IInjectorService|undefined;
ng1Module.run([
'$injector', function($injector: angular.IInjectorService) { a1Injector = $injector; }
'$injector',
function($injector: angular.IInjectorService) {
a1Injector = $injector;
}
]);
const element = html('<div></div>');
window.name = 'NG_DEFER_BOOTSTRAP!' + window.name;
adapter.bootstrap(element, [ng1Module.name]).ready((ref) => { ref.dispose(); });
adapter.bootstrap(element, [ng1Module.name]).ready((ref) => {
ref.dispose();
});
tick(100);
@ -3275,7 +3421,7 @@ withEachNg1Version(() => {
document.body.innerHTML = '<ng2 name="World">project</ng2>';
adapter.bootstrap(document.body.firstElementChild !, ['myExample']).ready((ref) => {
adapter.bootstrap(document.body.firstElementChild!, ['myExample']).ready((ref) => {
expect(multiTrim(document.body.textContent))
.toEqual('ng2[ng1[Hello World!](transclude)](project)');
ref.dispose();

View File

@ -43,10 +43,9 @@ Zone.__load_patch('jest', (context: any, Zone: ZoneType) => {
function wrapTestFactoryInZone(originalJestFn: Function) {
return function(this: unknown, ...tableArgs: any[]) {
const testFn = originalJestFn.apply(this, tableArgs);
return function(this: unknown, ...args: any[]) {
args[1] = wrapTestInZone(args[1]);
return testFn.apply(this, args);
return originalJestFn.apply(this, tableArgs).apply(this, args);
};
};
}
@ -64,16 +63,21 @@ Zone.__load_patch('jest', (context: any, Zone: ZoneType) => {
/**
* Gets a function wrapping the body of a jest `it/beforeEach/afterEach` block to
* execute in a ProxyZone zone.
* This will run in the `testProxyZone`.
* This will run in the `proxyZone`.
*/
function wrapTestInZone(testBody: Function): Function {
if (typeof testBody !== 'function') {
return testBody;
}
// The `done` callback is only passed through if the function expects at least one argument.
// Note we have to make a function with correct number of arguments, otherwise jest will
// think that all functions are sync or async.
return function(this: unknown, ...args: any[]) { return proxyZone.run(testBody, this, args); };
const wrappedFunc = function() {
return proxyZone.run(testBody, null, arguments as any);
};
// Update the length of wrappedFunc to be the same as the length of the testBody
// So jest core can handle whether the test function has `done()` or not correctly
Object.defineProperty(
wrappedFunc, 'length', {configurable: true, writable: true, enumerable: false});
wrappedFunc.length = testBody.length;
return wrappedFunc;
}
['describe', 'xdescribe', 'fdescribe'].forEach(methodName => {

View File

@ -512,7 +512,7 @@ interface ZoneGlobalConfigurations {
* Users can achieve this goal by defining `__zone_symbol__UNPATCHED_EVENTS = ['scroll',
* 'mousemove'];` before importing `zone.js`.
*/
__zone_symbol__UNPATCHED_EVENTS?: boolean;
__zone_symbol__UNPATCHED_EVENTS?: string[];
/**
* Define the event names of the passive listeners.
@ -528,7 +528,7 @@ interface ZoneGlobalConfigurations {
*
* The preceding code makes all scroll event listeners passive.
*/
__zone_symbol__PASSIVE_EVENTS?: boolean;
__zone_symbol__PASSIVE_EVENTS?: string[];
/**
* Disable wrapping uncaught promise rejection.

View File

@ -6,10 +6,18 @@ function assertInsideSyncDescribeZone() {
}
describe('describe', () => {
assertInsideSyncDescribeZone();
beforeEach(() => { assertInsideProxyZone(); });
beforeAll(() => { assertInsideProxyZone(); });
afterEach(() => { assertInsideProxyZone(); });
afterAll(() => { assertInsideProxyZone(); });
beforeEach(() => {
assertInsideProxyZone();
});
beforeAll(() => {
assertInsideProxyZone();
});
afterEach(() => {
assertInsideProxyZone();
});
afterAll(() => {
assertInsideProxyZone();
});
});
describe.each([[1, 2]])('describe.each', (arg1, arg2) => {
assertInsideSyncDescribeZone();
@ -17,23 +25,78 @@ describe.each([[1, 2]])('describe.each', (arg1, arg2) => {
expect(arg2).toBe(2);
});
describe('test', () => {
it('it', () => { assertInsideProxyZone(); });
it('it', () => {
assertInsideProxyZone();
});
it.each([[1, 2]])('it.each', (arg1, arg2) => {
assertInsideProxyZone();
expect(arg1).toBe(1);
expect(arg2).toBe(2);
});
test('test', () => { assertInsideProxyZone(); });
test.each([[]])('test.each', () => { assertInsideProxyZone(); });
test('test', () => {
assertInsideProxyZone();
});
test.each([[]])('test.each', () => {
assertInsideProxyZone();
});
});
it('it', () => { assertInsideProxyZone(); });
it.each([[1, 2]])('it.each', (arg1, arg2) => {
it('it', () => {
assertInsideProxyZone();
});
it('it with done', done => {
assertInsideProxyZone();
done();
});
it.each([[1, 2]])('it.each', (arg1, arg2, done) => {
assertInsideProxyZone();
expect(arg1).toBe(1);
expect(arg2).toBe(2);
done();
});
it.each([2])('it.each with 1D array', arg1 => {
assertInsideProxyZone();
expect(arg1).toBe(2);
});
it.each([2])('it.each with 1D array and done', (arg1, done) => {
assertInsideProxyZone();
expect(arg1).toBe(2);
done();
});
it.each`
foo | bar
${1} | ${2}
`('it.each should work with table as a tagged template literal', ({foo, bar}) => {
expect(foo).toBe(1);
expect(bar).toBe(2);
});
it.each`
foo | bar
${1} | ${2}
`('it.each should work with table as a tagged template literal with done', ({foo, bar}, done) => {
expect(foo).toBe(1);
expect(bar).toBe(2);
done();
});
it.each`
foo | bar
${1} | ${2}
`('(async) it.each should work with table as a tagged template literal', async ({foo, bar}) => {
expect(foo).toBe(1);
expect(bar).toBe(2);
});
test('test', () => {
assertInsideProxyZone();
});
test.each([[]])('test.each', () => {
assertInsideProxyZone();
});
test('test', () => { assertInsideProxyZone(); });
test.each([[]])('test.each', () => { assertInsideProxyZone(); });
test.todo('todo');

View File

@ -21,7 +21,9 @@ describe('crypto test', () => {
const zoneASpec = {
name: 'A',
onScheduleTask: (delegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task):
Task => { return delegate.scheduleTask(targetZone, task); }
Task => {
return delegate.scheduleTask(targetZone, task);
}
};
const zoneA = Zone.current.fork(zoneASpec);
spyOn(zoneASpec, 'onScheduleTask').and.callThrough();
@ -41,10 +43,12 @@ describe('crypto test', () => {
done();
return;
}
const zoneASpec: ZoneSpec = {
const zoneASpec = {
name: 'A',
onScheduleTask: (delegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task):
Task => { return delegate.scheduleTask(targetZone, task); }
Task => {
return delegate.scheduleTask(targetZone, task);
}
};
const zoneA = Zone.current.fork(zoneASpec);
spyOn(zoneASpec, 'onScheduleTask').and.callThrough();

View File

@ -43,7 +43,7 @@ console.log(`BUILD_SCM_LOCAL_CHANGES ${LOCAL_CHANGES}`);
// This will ignore non-version tags which would break unit tests expecting a valid version
// number in the package headers
const BUILD_SCM_VERSION_RAW =
_exec(`git describe --match [0-9].[0-9].[0-9]* --abbrev=7 --tags HEAD`);
_exec(`git describe --match [0-9]*.[0-9]*.[0-9]* --abbrev=7 --tags HEAD`);
// Reformat `git describe` version string into a more semver-ish string
// From: 5.2.0-rc.0-57-g757f886

View File

@ -10,7 +10,7 @@
const shell = require('shelljs');
const karmaBin = require.resolve('karma/bin/karma');
const runfiles = require(process.env['BAZEL_NODE_RUNFILES_HELPER']);
const sauceService = runfiles.resolve(process.argv[2]);
const sauceService = runfiles.resolveWorkspaceRelative(process.argv[2]);
process.argv = [
process.argv[0],
karmaBin,
@ -61,7 +61,7 @@ try {
}
console.error(`Launching karma ${karmaBin}...`);
module.constructor._load(karmaBin, this, /*isMain=*/true);
module.constructor._load(karmaBin, this, /*isMain=*/ true);
} catch (e) {
console.error(e.stack || e);
process.exit(1);

View File

@ -1300,17 +1300,12 @@
"@types/rx" "*"
"@types/through" "*"
"@types/jasmine@*":
"@types/jasmine@*", "@types/jasmine@3.5.10":
version "3.5.10"
resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-3.5.10.tgz#a1a41012012b5da9d4b205ba9eba58f6cce2ab7b"
integrity sha512-3F8qpwBAiVc5+HPJeXJpbrl+XjawGmciN5LgiO7Gv1pl1RHtjoMNqZpqEksaPJW05ViKe8snYInRs6xB25Xdew==
"@types/jasmine@^2.8.8":
version "2.8.16"
resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-2.8.16.tgz#a6cb24b1149d65293bd616923500014838e14e7d"
integrity sha512-056oRlBBp7MDzr+HoU5su099s/s7wjZ3KcHxLfv+Byqb9MwdLUvsfLgw1VS97hsh3ddxSPyQu+olHMnoVTUY6g==
"@types/jasminewd2@^2.0.6":
"@types/jasminewd2@^2.0.8":
version "2.0.8"
resolved "https://registry.yarnpkg.com/@types/jasminewd2/-/jasminewd2-2.0.8.tgz#67afe5098d5ef2386073a7b7384b69a840dfe93b"
integrity sha512-d9p31r7Nxk0ZH0U39PTH0hiDlJ+qNVGjlt1ucOoTUptxb2v+Y5VMnsxfwN+i3hK4yQnqBi3FMmoMFcd1JHDxdg==
@ -8002,7 +7997,7 @@ istanbul-reports@^1.3.0:
dependencies:
handlebars "^4.0.3"
jasmine-core@^3.1.0, jasmine-core@^3.3, jasmine-core@~3.5.0:
jasmine-core@^3.3, jasmine-core@^3.5.0, jasmine-core@~3.5.0:
version "3.5.0"
resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.5.0.tgz#132c23e645af96d85c8bca13c8758b18429fc1e4"
integrity sha512-nCeAiw37MIMA9w9IXso7bRaLl+c/ef3wnxsoSAlYrzS+Ot0zTG6nU8G/cIfGkqpkjX2wNaIW9RFG0TwIFnG6bA==
@ -8029,7 +8024,7 @@ jasmine@2.8.0:
glob "^7.0.6"
jasmine-core "~2.8.0"
jasmine@^3.1.0, jasmine@~3.5.0:
jasmine@^3.5.0, jasmine@~3.5.0:
version "3.5.0"
resolved "https://registry.yarnpkg.com/jasmine/-/jasmine-3.5.0.tgz#7101eabfd043a1fc82ac24e0ab6ec56081357f9e"
integrity sha512-DYypSryORqzsGoMazemIHUfMkXM7I7easFaxAvNM3Mr6Xz3Fy36TupTrAOxZWN8MVKEU5xECv22J4tUQf3uBzQ==