mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
test: remove view-engine-only tests (#43884)
This commit removes most tests that were designated as only covering View Engine code. It also removes tag filters from CI and local commands to run tests. In a few cases (such as with the packages/compiler tests), this tag was improperly applied, and certain test cases have been added back running in Ivy mode. This commit also empties `@angular/compiler/testing` as it is no longer necessary (this is safe since compiler packages are not public API). It can be deleted in the future. PR Close #43884
This commit is contained in:
parent
f44cb57c12
commit
bb9ff6003c
114 changed files with 184 additions and 20756 deletions
|
|
@ -344,7 +344,7 @@ jobs:
|
|||
# See /tools/saucelabs/README.md for more info
|
||||
command: |
|
||||
yarn bazel run //tools/saucelabs:sauce_service_setup
|
||||
TESTS=$(./node_modules/.bin/bazelisk query --output label '(kind(karma_web_test, ...) intersect attr("tags", "saucelabs", ...)) except attr("tags", "view-engine-only", ...) except attr("tags", "fixme-saucelabs", ...)')
|
||||
TESTS=$(./node_modules/.bin/bazelisk query --output label '(kind(karma_web_test, ...) intersect attr("tags", "saucelabs", ...)) except attr("tags", "fixme-saucelabs", ...)')
|
||||
yarn bazel test --config=saucelabs ${TESTS}
|
||||
yarn bazel run //tools/saucelabs:sauce_service_stop
|
||||
no_output_timeout: 40m
|
||||
|
|
@ -723,11 +723,11 @@ jobs:
|
|||
- setup_win
|
||||
- run:
|
||||
name: Build all windows CI targets
|
||||
command: bazel build --build_tag_filters=-view-engine-only //packages/compiler-cli/...
|
||||
command: bazel build //packages/compiler-cli/...
|
||||
no_output_timeout: 15m
|
||||
- run:
|
||||
name: Test all windows CI targets
|
||||
command: bazel test --test_tag_filters="-view-engine-only,-browser:chromium-local" //packages/compiler-cli/...
|
||||
command: bazel test --test_tag_filters="-browser:chromium-local" //packages/compiler-cli/...
|
||||
no_output_timeout: 15m
|
||||
|
||||
workflows:
|
||||
|
|
|
|||
|
|
@ -1,48 +0,0 @@
|
|||
load("//tools:defaults.bzl", "ts_devserver", "ts_library")
|
||||
load("@npm//@angular/dev-infra-private/bazel/benchmark/component_benchmark:benchmark_test.bzl", "benchmark_test")
|
||||
load("//modules/benchmarks:e2e_test.bzl", "e2e_test")
|
||||
|
||||
package(default_visibility = ["//modules/benchmarks:__subpackages__"])
|
||||
|
||||
ts_library(
|
||||
name = "ng2_next",
|
||||
srcs = glob(["*.ts"]),
|
||||
tsconfig = "//modules/benchmarks:tsconfig-build.json",
|
||||
deps = [
|
||||
"//modules/benchmarks/src:util_lib",
|
||||
"//modules/benchmarks/src/tree:util_lib",
|
||||
"//packages/common",
|
||||
"//packages/core",
|
||||
"//packages/platform-browser",
|
||||
],
|
||||
)
|
||||
|
||||
ts_devserver(
|
||||
name = "devserver",
|
||||
entry_module = "angular/modules/benchmarks/src/tree/ng2_next/index",
|
||||
port = 4200,
|
||||
scripts = [
|
||||
"@npm//:node_modules/tslib/tslib.js",
|
||||
"//tools/rxjs:rxjs_umd_modules",
|
||||
],
|
||||
static_files = ["index.html"],
|
||||
deps = [":ng2_next"],
|
||||
)
|
||||
|
||||
benchmark_test(
|
||||
name = "perf",
|
||||
server = ":devserver",
|
||||
deps = [
|
||||
"//modules/benchmarks/src/tree:detect_changes_perf_tests_lib",
|
||||
"//modules/benchmarks/src/tree:perf_tests_lib",
|
||||
],
|
||||
)
|
||||
|
||||
e2e_test(
|
||||
name = "e2e",
|
||||
server = ":devserver",
|
||||
deps = [
|
||||
"//modules/benchmarks/src/tree:detect_changes_e2e_tests_lib",
|
||||
"//modules/benchmarks/src/tree:e2e_tests_lib",
|
||||
],
|
||||
)
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
# Ng2 Next Benchmark
|
||||
|
||||
This benchmark uses the upcoming view engine for Angular, which moves
|
||||
more functionality from codegen into runtime to reduce generated code size.
|
||||
|
||||
As we introduce more runtime code, we need to be very careful to not
|
||||
regress in performance, compared to the pure codegen solution.
|
||||
|
||||
## Initial results: size of Deep Tree Benchmark
|
||||
|
||||
File size for Tree benchmark template,
|
||||
view class of the component + the 2 embedded view classes (without imports nor host view factory):
|
||||
|
||||
| bytes | ratio | bytes (gzip) | ratio (gzip)
|
||||
------------------------------ | ----- | ----- | ------------ | ------------
|
||||
Source template + annotation | 245 | 1x | 159 | 1x
|
||||
Gen code (Closure minified) | 2693 | 11.9x | 746 | 4.7x
|
||||
New View Engine (minified) | 868 | 3.5x | 436 | 2.7x
|
||||
|
||||
## Initial results: performance of Deep Tree Benchmark
|
||||
|
||||
Measured locally on a MacBook Pro.
|
||||
|
||||
BENCHMARK deepTree....
|
||||
Description:
|
||||
- bundles: false
|
||||
- depth: 11
|
||||
- forceGc: false
|
||||
- regressionSlopeMetric: scriptTime
|
||||
- sampleSize: 20
|
||||
- userAgent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Safari/537.36
|
||||
|
||||
...createOnly | gcAmount | gcTime | majorGcTime | pureScriptTime | renderTime | scriptTime
|
||||
--------------- | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ | ------------------
|
||||
ng2 | 11461.24+-21% | 12.35+-42% | 1.15+-429% | 72.49+-4% | 49.61+-4% | 82.69+-6%
|
||||
ng2 next | 6207.77+-93% | 9.84+-84% | 3.35+-238% | 73.95+-4% | 49.86+-4% | 77.53+-10%
|
||||
|
||||
...update | gcAmount | gcTime | majorGcTime | pureScriptTime | renderTime | scriptTime
|
||||
--------------- | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ | ------------------
|
||||
ng2 | 0.00 | 0.00+-435% | 0.00+-435% | 13.34+-8% | 28.55+-8% | 13.34+-8%
|
||||
ng2 next | 175.02+-435% | 0.74+-435% | 0.00+-302% | 20.55+-12% | 28.00+-6% | 20.55+-12%
|
||||
|
||||
...pure cd (10x) | gcAmount | gcTime | majorGcTime | pureScriptTime | renderTime | scriptTime
|
||||
--------------- | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ | ------------------
|
||||
ng2 | 2155.57+-238% | 0.24+-238% | 0.00+-238% | 19.32+-9% | 2.54+-6% | 19.32+-9%
|
||||
ng2 next | 908.12+-366% | 1.62+-325% | 0.49+-435% | 30.66+-6% | 2.62+-19% | 30.66+-6%
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<!-- Prevent the browser from requesting any favicon. -->
|
||||
<link rel="icon" href="data:,">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h2>Params</h2>
|
||||
<form>
|
||||
Depth:
|
||||
<input type="number" name="depth" placeholder="depth" value="10">
|
||||
<br>
|
||||
<button>Apply</button>
|
||||
</form>
|
||||
|
||||
<h2>Ng2 Next Tree Benchmark</h2>
|
||||
<p>
|
||||
<button id="destroyDom">destroyDom</button>
|
||||
<button id="createDom">createDom</button>
|
||||
<button id="detectChanges">detectChanges</button>
|
||||
<button id="updateDomProfile">profile updateDom</button>
|
||||
<button id="createDomProfile">profile createDom</button>
|
||||
<button id="detectChangesProfile">profile detectChanges</button>
|
||||
</p>
|
||||
|
||||
<div>
|
||||
Change detection runs:<span id="numberOfChecks"></span>
|
||||
</div>
|
||||
<div>
|
||||
<tree id="root">Loading...</tree>
|
||||
</div>
|
||||
|
||||
<!--load location for ts_devserver-->
|
||||
<script src="/app_bundle.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 {enableProdMode} from '@angular/core';
|
||||
|
||||
import {bindAction, profile} from '../../util';
|
||||
import {buildTree, emptyTree} from '../util';
|
||||
|
||||
import {AppModule, TreeComponent} from './tree';
|
||||
|
||||
let tree: TreeComponent;
|
||||
let appMod: AppModule;
|
||||
let detectChangesRuns = 0;
|
||||
|
||||
function destroyDom() {
|
||||
tree.data = emptyTree;
|
||||
appMod.tick();
|
||||
}
|
||||
|
||||
function createDom() {
|
||||
tree.data = buildTree();
|
||||
appMod.tick();
|
||||
}
|
||||
|
||||
function detectChanges() {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
appMod.tick();
|
||||
}
|
||||
detectChangesRuns += 10;
|
||||
numberOfChecksEl.textContent = `${detectChangesRuns}`;
|
||||
}
|
||||
|
||||
function noop() {}
|
||||
|
||||
const numberOfChecksEl = document.getElementById('numberOfChecks');
|
||||
|
||||
enableProdMode();
|
||||
appMod = new AppModule();
|
||||
appMod.bootstrap();
|
||||
tree = appMod.componentRef.instance;
|
||||
|
||||
bindAction('#destroyDom', destroyDom);
|
||||
bindAction('#createDom', createDom);
|
||||
bindAction('#detectChanges', detectChanges);
|
||||
bindAction('#detectChangesProfile', profile(detectChanges, noop, 'detectChanges'));
|
||||
bindAction('#updateDomProfile', profile(createDom, noop, 'update'));
|
||||
bindAction('#createDomProfile', profile(createDom, destroyDom, 'create'));
|
||||
|
|
@ -1,140 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 {NgIf} from '@angular/common';
|
||||
import {ComponentFactory, ComponentFactoryResolver, ComponentRef, ErrorHandler, Injector, NgModuleRef, RendererFactory2, Sanitizer, TemplateRef, ViewContainerRef, ɵand as anchorDef, ɵArgumentType as ArgumentType, ɵBindingFlags as BindingFlags, ɵccf as createComponentFactory, ɵdid as directiveDef, ɵeld as elementDef, ɵinitServicesIfNeeded as initServicesIfNeeded, ɵNodeFlags as NodeFlags, ɵted as textDef, ɵvid as viewDef, ɵViewDefinition as ViewDefinition, ɵViewFlags as ViewFlags} from '@angular/core';
|
||||
import {SafeStyle, ɵDomRendererFactory2 as DomRendererFactory2, ɵDomSanitizerImpl as DomSanitizerImpl} from '@angular/platform-browser';
|
||||
|
||||
import {emptyTree, TreeNode} from '../util';
|
||||
|
||||
let trustedEmptyColor: SafeStyle;
|
||||
let trustedGreyColor: SafeStyle;
|
||||
|
||||
export class TreeComponent {
|
||||
data: TreeNode = emptyTree;
|
||||
get bgColor() {
|
||||
return this.data.depth % 2 ? trustedEmptyColor : trustedGreyColor;
|
||||
}
|
||||
}
|
||||
|
||||
let viewFlags = ViewFlags.None;
|
||||
|
||||
function TreeComponent_Host(): ViewDefinition {
|
||||
return viewDef(viewFlags, [
|
||||
elementDef(0, NodeFlags.None, null, null, 1, 'tree', null, null, null, null, TreeComponent_0),
|
||||
directiveDef(1, NodeFlags.Component, null, 0, TreeComponent, []),
|
||||
]);
|
||||
}
|
||||
|
||||
function TreeComponent_1() {
|
||||
return viewDef(
|
||||
viewFlags,
|
||||
[
|
||||
elementDef(
|
||||
0, NodeFlags.None, null, null, 1, 'tree', null, null, null, null, TreeComponent_0),
|
||||
directiveDef(1, NodeFlags.Component, null, 0, TreeComponent, [], {data: [0, 'data']}),
|
||||
],
|
||||
(check, view) => {
|
||||
const cmp = view.component;
|
||||
check(view, 1, ArgumentType.Inline, cmp.data.left);
|
||||
});
|
||||
}
|
||||
|
||||
function TreeComponent_2() {
|
||||
return viewDef(
|
||||
viewFlags,
|
||||
[
|
||||
elementDef(
|
||||
0, NodeFlags.None, null, null, 1, 'tree', null, null, null, null, TreeComponent_0),
|
||||
directiveDef(1, NodeFlags.Component, null, 0, TreeComponent, [], {data: [0, 'data']}),
|
||||
],
|
||||
(check, view) => {
|
||||
const cmp = view.component;
|
||||
check(view, 1, ArgumentType.Inline, cmp.data.left);
|
||||
});
|
||||
}
|
||||
|
||||
function TreeComponent_0(): ViewDefinition {
|
||||
return viewDef(
|
||||
viewFlags,
|
||||
[
|
||||
elementDef(
|
||||
0, NodeFlags.None, null, null, 1, 'span', null,
|
||||
[[BindingFlags.TypeElementStyle, 'backgroundColor', null]]),
|
||||
textDef(1, null, [' ', ' ']),
|
||||
anchorDef(NodeFlags.EmbeddedViews, null, null, 1, null, TreeComponent_1),
|
||||
directiveDef(
|
||||
3, NodeFlags.None, null, 0, NgIf, [ViewContainerRef, TemplateRef], {ngIf: [0, 'ngIf']}),
|
||||
anchorDef(NodeFlags.EmbeddedViews, null, null, 1, null, TreeComponent_2),
|
||||
directiveDef(
|
||||
5, NodeFlags.None, null, 0, NgIf, [ViewContainerRef, TemplateRef], {ngIf: [0, 'ngIf']}),
|
||||
],
|
||||
(check, view) => {
|
||||
const cmp = view.component;
|
||||
check(view, 3, ArgumentType.Inline, cmp.data.left != null);
|
||||
check(view, 5, ArgumentType.Inline, cmp.data.right != null);
|
||||
},
|
||||
(check, view) => {
|
||||
const cmp = view.component;
|
||||
check(view, 0, ArgumentType.Inline, cmp.bgColor);
|
||||
check(view, 1, ArgumentType.Inline, cmp.data.value);
|
||||
});
|
||||
}
|
||||
|
||||
export class AppModule implements Injector, NgModuleRef<any> {
|
||||
private sanitizer: DomSanitizerImpl;
|
||||
private componentFactory: ComponentFactory<TreeComponent>;
|
||||
private renderer2: RendererFactory2;
|
||||
|
||||
componentRef: ComponentRef<TreeComponent>;
|
||||
|
||||
constructor() {
|
||||
initServicesIfNeeded();
|
||||
this.sanitizer = new DomSanitizerImpl(document);
|
||||
this.renderer2 = new DomRendererFactory2(null, null, null);
|
||||
trustedEmptyColor = this.sanitizer.bypassSecurityTrustStyle('');
|
||||
trustedGreyColor = this.sanitizer.bypassSecurityTrustStyle('grey');
|
||||
this.componentFactory =
|
||||
createComponentFactory('#root', TreeComponent, TreeComponent_Host, {}, {}, []);
|
||||
}
|
||||
|
||||
get(token: any, notFoundValue: any = Injector.THROW_IF_NOT_FOUND): any {
|
||||
switch (token) {
|
||||
case RendererFactory2:
|
||||
return this.renderer2;
|
||||
case Sanitizer:
|
||||
return this.sanitizer;
|
||||
case ErrorHandler:
|
||||
return null;
|
||||
case NgModuleRef:
|
||||
return this;
|
||||
}
|
||||
return Injector.NULL.get(token, notFoundValue);
|
||||
}
|
||||
|
||||
bootstrap() {
|
||||
this.componentRef =
|
||||
this.componentFactory.create(Injector.NULL, [], this.componentFactory.selector, this);
|
||||
}
|
||||
|
||||
tick() {
|
||||
this.componentRef.changeDetectorRef.detectChanges();
|
||||
}
|
||||
|
||||
get injector() {
|
||||
return this;
|
||||
}
|
||||
get componentFactoryResolver(): ComponentFactoryResolver {
|
||||
return null;
|
||||
}
|
||||
get instance() {
|
||||
return this;
|
||||
}
|
||||
destroy() {}
|
||||
onDestroy(callback: () => void) {}
|
||||
}
|
||||
|
|
@ -24,10 +24,7 @@
|
|||
"/ ": "",
|
||||
"postinstall": "node scripts/webdriver-manager-update.js && node --preserve-symlinks --preserve-symlinks-main ./tools/postinstall-patches.js",
|
||||
"prepare": "husky install",
|
||||
"test": "bazelisk test --build_tag_filters=-view-engine-only --test_tag_filters=-view-engine-only",
|
||||
"test-view-engine-only": "bazelisk test --config=view-engine --build_tag_filters=view-engine-only --test_tag_filters=view-engine-only",
|
||||
"test-ivy-aot": "echo \\`test-ivy-aot\\` no longer valid, use \\`yarn test\\` instead",
|
||||
"test-non-ivy": "echo \\`test-ivy-aot\\` no longer valid, use \\`yarn test-view-engine-only\\` instead",
|
||||
"test": "bazelisk test",
|
||||
"test-tsec": "bazelisk test //... --build_tag_filters=tsec --test_tag_filters=tsec",
|
||||
"lint": "yarn -s tslint && yarn -s ng-dev format changed --check",
|
||||
"tslint": "tslint -c tslint.json --project tsconfig-tslint.json",
|
||||
|
|
|
|||
|
|
@ -82,7 +82,6 @@ jasmine_node_test(
|
|||
# file is based on non-ivy output and therefore won't work for ngc and Ivy at the same time.
|
||||
# TODO: Update test to rely on Ivy partial compilation after View Engine's removal, update the
|
||||
# golden files as appropriate for the change in compilation.
|
||||
tags = ["view-engine-only"],
|
||||
deps = ["@npm//diff"],
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
load("@build_bazel_rules_nodejs//:index.bzl", "nodejs_test")
|
||||
load("//tools:defaults.bzl", "nodejs_binary")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
|
@ -22,49 +21,3 @@ nodejs_binary(
|
|||
],
|
||||
entry_point = "//packages/compiler-cli:src/extract_i18n.ts",
|
||||
)
|
||||
|
||||
nodejs_test(
|
||||
name = "integrationtest",
|
||||
data = [
|
||||
":ngc_bin",
|
||||
":ng_xi18n",
|
||||
"@nodejs//:node",
|
||||
"@npm//domino",
|
||||
"@npm//chokidar",
|
||||
"@npm//source-map-support",
|
||||
"@npm//shelljs",
|
||||
"@npm//typescript",
|
||||
"@npm//reflect-metadata",
|
||||
"@npm//rxjs",
|
||||
"@npm//tslib",
|
||||
"@npm//jasmine/bin:jasmine",
|
||||
"@npm//xhr2",
|
||||
"@npm//@types/node",
|
||||
"@npm//@types/jasmine",
|
||||
# we need to reference zone.d.ts typing file from zone.js build target
|
||||
# instead of npm because angular repo will not depends on npm zone.js
|
||||
# any longer.
|
||||
"//packages/zone.js/lib:zone_d_ts",
|
||||
# we need to reference zone.js npm_package build target
|
||||
# instead of npm because angular repo will not depends on npm zone.js
|
||||
# any longer, so we need to build a zone.js npm release first.
|
||||
"//packages/zone.js:npm_package",
|
||||
"//packages/animations:npm_package",
|
||||
"//packages/common:npm_package",
|
||||
"//packages/compiler:npm_package",
|
||||
"//packages/compiler-cli:npm_package",
|
||||
"//packages/core:npm_package",
|
||||
"//packages/forms:npm_package",
|
||||
"//packages/platform-browser:npm_package",
|
||||
"//packages/platform-browser-dynamic:npm_package",
|
||||
"//packages/platform-server:npm_package",
|
||||
"//packages/router:npm_package",
|
||||
] + glob(["**/*"]),
|
||||
entry_point = "test.js",
|
||||
tags = [
|
||||
# TODO(josephperrott): reenable or remove test after investigating the cause of failures
|
||||
# on windows CI runs.
|
||||
"manual",
|
||||
"view-engine-only",
|
||||
],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,42 +0,0 @@
|
|||
load("//packages/bazel:index.bzl", "ng_module")
|
||||
load("//tools:defaults.bzl", "jasmine_node_test")
|
||||
load(":extract_flat_module_index.bzl", "extract_flat_module_index")
|
||||
|
||||
ng_module(
|
||||
name = "test_module",
|
||||
testonly = True,
|
||||
srcs = glob(["*.ts"]),
|
||||
api_extractor = "//packages/bazel/src/api-extractor:api_extractor",
|
||||
compiler = "//packages/bazel/src/ngc-wrapped",
|
||||
entry_point = "index.ts",
|
||||
flat_module_out_file = "flat_module_filename",
|
||||
module_name = "some_npm_module",
|
||||
ng_xi18n = "//packages/bazel/src/ngc-wrapped:xi18n",
|
||||
node_modules = "@npm//typescript:typescript__typings",
|
||||
tags = [
|
||||
# Disabled as this test is specific to the flat module indexing of metadata.json files that
|
||||
# the old ngc compiler does. Ivy has no metadata.json files so this test does not apply.
|
||||
"view-engine-only",
|
||||
],
|
||||
deps = [
|
||||
"//packages/core",
|
||||
"@npm//tslib",
|
||||
],
|
||||
)
|
||||
|
||||
extract_flat_module_index(
|
||||
name = "flat_module_index",
|
||||
testonly = True,
|
||||
deps = [":test_module"],
|
||||
)
|
||||
|
||||
jasmine_node_test(
|
||||
name = "test",
|
||||
srcs = ["spec.js"],
|
||||
data = [":flat_module_index"],
|
||||
tags = [
|
||||
# Disabled as this test is specific to the flat module indexing of metadata.json files that
|
||||
# the old ngc compiler does. Ivy has no metadata.json files so this test does not apply.
|
||||
"view-engine-only",
|
||||
],
|
||||
)
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 {NgModule} from '@angular/core';
|
||||
import {Parent} from './parent';
|
||||
|
||||
@NgModule({imports: [Parent]})
|
||||
export class Child {
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
# Copyright Google LLC All Rights Reserved.
|
||||
#
|
||||
# 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
|
||||
"""Test utility to extract the "flat_module_metadata" from transitive Angular deps.
|
||||
"""
|
||||
|
||||
def _extract_flat_module_index(ctx):
|
||||
files = []
|
||||
for dep in ctx.attr.deps:
|
||||
if hasattr(dep, "angular") and hasattr(dep.angular, "flat_module_metadata"):
|
||||
flat_module = dep.angular.flat_module_metadata
|
||||
files.append(flat_module.typings_file)
|
||||
|
||||
# The flat module metadata file could be `None` for targets
|
||||
# built with Ivy. No metadata files are generated in ngtsc.
|
||||
if flat_module.metadata_file != None:
|
||||
files.append(flat_module.metadata_file)
|
||||
return [DefaultInfo(files = depset(files))]
|
||||
|
||||
extract_flat_module_index = rule(
|
||||
implementation = _extract_flat_module_index,
|
||||
attrs = {
|
||||
"deps": attr.label_list(),
|
||||
},
|
||||
)
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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
|
||||
*/
|
||||
|
||||
export * from './child';
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 {NgModule} from '@angular/core';
|
||||
|
||||
@NgModule({})
|
||||
export class Parent {
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const PKG = 'angular/packages/compiler-cli/integrationtest/bazel/ng_module';
|
||||
describe('flat module index', () => {
|
||||
describe('child metadata', () => {
|
||||
it('should have contents', () => {
|
||||
const metadata = fs.readFileSync(
|
||||
require.resolve(`${PKG}/flat_module_filename.metadata.json`), {encoding: 'utf-8'});
|
||||
expect(metadata).toContain('"__symbolic":"module"');
|
||||
expect(metadata).toContain('"__symbolic":"reference","module":"@angular/core"');
|
||||
expect(metadata).toContain(
|
||||
'"origins":{"Child":"./child","ɵangular_packages_compiler_cli_integrationtest_bazel_ng_module_test_module_a":"./parent"}');
|
||||
expect(metadata).toContain('"importAs":"some_npm_module"');
|
||||
});
|
||||
});
|
||||
describe('child typings', () => {
|
||||
it('should have contents', () => {
|
||||
const dts =
|
||||
fs.readFileSync(require.resolve(`${PKG}/flat_module_filename.d.ts`), {encoding: 'utf-8'});
|
||||
|
||||
expect(dts).toContain('export * from \'./index\';');
|
||||
expect(dts).toContain(
|
||||
'export { Parent as ɵangular_packages_compiler_cli_integrationtest_bazel_ng_module_test_module_a } from \'./parent\';');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -52,45 +52,6 @@ jasmine_node_test(
|
|||
],
|
||||
)
|
||||
|
||||
# ngc_spec
|
||||
ts_library(
|
||||
name = "ngc_lib",
|
||||
testonly = True,
|
||||
srcs = [
|
||||
"ngc_spec.ts",
|
||||
],
|
||||
deps = [
|
||||
":test_utils",
|
||||
"//packages/compiler",
|
||||
"//packages/compiler-cli",
|
||||
"@npm//tsickle",
|
||||
"@npm//typescript",
|
||||
],
|
||||
)
|
||||
|
||||
jasmine_node_test(
|
||||
name = "ngc",
|
||||
timeout = "long", # 900 seconds
|
||||
bootstrap = ["//tools/testing:node_es5"],
|
||||
data = [
|
||||
"@npm//@angular/common-12",
|
||||
"@npm//@angular/core-12",
|
||||
"@npm//@angular/platform-browser-12",
|
||||
"@npm//@angular/router-12",
|
||||
],
|
||||
tags = [
|
||||
# Disabled as these tests are specific to the old ngc compiler, and not ngtsc which has its
|
||||
# own tests under //packages/compiler-cli/test/ngtsc.
|
||||
"view-engine-only",
|
||||
],
|
||||
deps = [
|
||||
":ngc_lib",
|
||||
"//packages/core",
|
||||
"@npm//rxjs",
|
||||
"@npm//yargs",
|
||||
],
|
||||
)
|
||||
|
||||
# perform_watch_spec
|
||||
ts_library(
|
||||
name = "perform_watch_lib",
|
||||
|
|
|
|||
|
|
@ -1,36 +1,5 @@
|
|||
load("//tools:defaults.bzl", "jasmine_node_test", "ts_library")
|
||||
|
||||
# check_types_spec
|
||||
ts_library(
|
||||
name = "check_types_lib",
|
||||
testonly = True,
|
||||
srcs = ["check_types_spec.ts"],
|
||||
deps = [
|
||||
"//packages/compiler-cli",
|
||||
"//packages/compiler-cli/test:test_utils",
|
||||
"@npm//typescript",
|
||||
],
|
||||
)
|
||||
|
||||
jasmine_node_test(
|
||||
name = "check_types",
|
||||
timeout = "long", # 900 seconds
|
||||
bootstrap = ["//tools/testing:node_es5"],
|
||||
data = [
|
||||
"@npm//@angular/common-12",
|
||||
"@npm//@angular/core-12",
|
||||
],
|
||||
tags = [
|
||||
# Disabled as these tests pertain to typechecking in the old ngc compiler. The Ivy ngtsc
|
||||
# compiler has its own typechecking implementation and tests.
|
||||
"view-engine-only",
|
||||
],
|
||||
deps = [
|
||||
":check_types_lib",
|
||||
"//packages/core",
|
||||
],
|
||||
)
|
||||
|
||||
# typescript_version_spec
|
||||
ts_library(
|
||||
name = "typescript_version_lib",
|
||||
|
|
|
|||
|
|
@ -1,40 +0,0 @@
|
|||
load("//tools:defaults.bzl", "jasmine_node_test", "ts_library")
|
||||
|
||||
ts_library(
|
||||
name = "test_lib",
|
||||
testonly = True,
|
||||
srcs = glob(["**/*.ts"]),
|
||||
deps = [
|
||||
"//packages:types",
|
||||
"//packages/compiler",
|
||||
"//packages/compiler-cli",
|
||||
"//packages/compiler-cli/src/ngtsc/reflection",
|
||||
"//packages/compiler-cli/src/transformers/downlevel_decorators_transform",
|
||||
"//packages/compiler-cli/test:test_utils",
|
||||
"//packages/compiler/test:test_utils",
|
||||
"//packages/core",
|
||||
"//packages/platform-browser",
|
||||
"@npm//typescript",
|
||||
],
|
||||
)
|
||||
|
||||
jasmine_node_test(
|
||||
name = "test",
|
||||
timeout = "long", # 900 seconds
|
||||
bootstrap = ["//tools/testing:node_es5"],
|
||||
data = [
|
||||
"@npm//@angular/common-12",
|
||||
"@npm//@angular/core-12",
|
||||
"@npm//@angular/router-12",
|
||||
],
|
||||
tags = [
|
||||
# Disabled as these tests pertain to the old compiler and not ngtsc, which doesn't use any
|
||||
# of these transformer utilities.
|
||||
"view-engine-only",
|
||||
],
|
||||
deps = [
|
||||
":test_lib",
|
||||
"//packages/core",
|
||||
"@npm//source-map",
|
||||
],
|
||||
)
|
||||
|
|
@ -1,401 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 * as compiler from '@angular/compiler';
|
||||
import ts from 'typescript';
|
||||
|
||||
import {MetadataCollector} from '../../src/metadata/collector';
|
||||
import {CompilerHost, CompilerOptions, LibrarySummary} from '../../src/transformers/api';
|
||||
import {createCompilerHost, TsCompilerAotCompilerTypeCheckHostAdapter} from '../../src/transformers/compiler_host';
|
||||
import {Directory, Entry, MockAotContext, MockCompilerHost} from '../mocks';
|
||||
|
||||
const dummyModule = 'export let foo: any[];';
|
||||
const aGeneratedFile = new compiler.GeneratedFile(
|
||||
'/tmp/src/index.ts', '/tmp/src/index.ngfactory.ts',
|
||||
[new compiler.DeclareVarStmt('x', new compiler.LiteralExpr(1))]);
|
||||
const aGeneratedFileText = `var x:any = 1;\n`;
|
||||
|
||||
describe('NgCompilerHost', () => {
|
||||
let codeGenerator: {generateFile: jasmine.Spy; findGeneratedFileNames: jasmine.Spy;};
|
||||
|
||||
beforeEach(() => {
|
||||
codeGenerator = {
|
||||
generateFile: jasmine.createSpy('generateFile').and.returnValue(null),
|
||||
findGeneratedFileNames: jasmine.createSpy('findGeneratedFileNames').and.returnValue([]),
|
||||
};
|
||||
});
|
||||
|
||||
function createNgHost({files = {}}: {files?: Directory} = {}): CompilerHost {
|
||||
const context = new MockAotContext('/tmp/', files);
|
||||
return new MockCompilerHost(context) as ts.CompilerHost;
|
||||
}
|
||||
|
||||
function createHost({
|
||||
files = {},
|
||||
options = {
|
||||
basePath: '/tmp',
|
||||
moduleResolution: ts.ModuleResolutionKind.NodeJs,
|
||||
},
|
||||
rootNames = ['/tmp/index.ts'],
|
||||
ngHost = createNgHost({files}),
|
||||
librarySummaries = [],
|
||||
}: {
|
||||
files?: Directory,
|
||||
options?: CompilerOptions,
|
||||
rootNames?: string[],
|
||||
ngHost?: CompilerHost,
|
||||
librarySummaries?: LibrarySummary[]
|
||||
} = {}) {
|
||||
return new TsCompilerAotCompilerTypeCheckHostAdapter(
|
||||
rootNames, options, ngHost, new MetadataCollector(), codeGenerator,
|
||||
new Map(
|
||||
librarySummaries.map(entry => [entry.fileName, entry] as [string, LibrarySummary])));
|
||||
}
|
||||
|
||||
describe('fileNameToModuleName', () => {
|
||||
let host: TsCompilerAotCompilerTypeCheckHostAdapter;
|
||||
beforeEach(() => {
|
||||
host = createHost();
|
||||
});
|
||||
|
||||
it('should use a package import when accessing a package from a source file', () => {
|
||||
expect(host.fileNameToModuleName('/tmp/node_modules/@angular/core.d.ts', '/tmp/main.ts'))
|
||||
.toBe('@angular/core');
|
||||
});
|
||||
|
||||
it('should allow an import o a package whose name contains dot (e.g. @angular.io)', () => {
|
||||
expect(host.fileNameToModuleName('/tmp/node_modules/@angular.io/core.d.ts', '/tmp/main.ts'))
|
||||
.toBe('@angular.io/core');
|
||||
});
|
||||
|
||||
it('should use a package import when accessing a package from another package', () => {
|
||||
expect(host.fileNameToModuleName(
|
||||
'/tmp/node_modules/mod1/index.d.ts', '/tmp/node_modules/mod2/index.d.ts'))
|
||||
.toBe('mod1/index');
|
||||
expect(host.fileNameToModuleName(
|
||||
'/tmp/node_modules/@angular/core/index.d.ts',
|
||||
'/tmp/node_modules/@angular/common/index.d.ts'))
|
||||
.toBe('@angular/core/index');
|
||||
});
|
||||
|
||||
it('should use a relative import when accessing a file in the same package', () => {
|
||||
expect(host.fileNameToModuleName(
|
||||
'/tmp/node_modules/mod/a/child.d.ts', '/tmp/node_modules/mod/index.d.ts'))
|
||||
.toBe('./a/child');
|
||||
expect(host.fileNameToModuleName(
|
||||
'/tmp/node_modules/@angular/core/src/core.d.ts',
|
||||
'/tmp/node_modules/@angular/core/index.d.ts'))
|
||||
.toBe('./src/core');
|
||||
});
|
||||
|
||||
it('should use a relative import when accessing a source file from a source file', () => {
|
||||
expect(host.fileNameToModuleName('/tmp/src/a/child.ts', '/tmp/src/index.ts'))
|
||||
.toBe('./a/child');
|
||||
});
|
||||
|
||||
it('should use a relative import when accessing generated files, even if crossing packages',
|
||||
() => {
|
||||
expect(host.fileNameToModuleName(
|
||||
'/tmp/node_modules/mod2/b.ngfactory.d.ts',
|
||||
'/tmp/node_modules/mod1/a.ngfactory.d.ts'))
|
||||
.toBe('../mod2/b.ngfactory');
|
||||
});
|
||||
|
||||
it('should support multiple rootDirs when accessing a source file form a source file', () => {
|
||||
const hostWithMultipleRoots = createHost({
|
||||
options: {
|
||||
basePath: '/tmp/',
|
||||
rootDirs: [
|
||||
'src/a',
|
||||
'src/b',
|
||||
]
|
||||
}
|
||||
});
|
||||
// both files are in the rootDirs
|
||||
expect(hostWithMultipleRoots.fileNameToModuleName('/tmp/src/b/b.ts', '/tmp/src/a/a.ts'))
|
||||
.toBe('./b');
|
||||
|
||||
// one file is not in the rootDirs
|
||||
expect(hostWithMultipleRoots.fileNameToModuleName('/tmp/src/c/c.ts', '/tmp/src/a/a.ts'))
|
||||
.toBe('../c/c');
|
||||
});
|
||||
|
||||
it('should error if accessing a source file from a package', () => {
|
||||
expect(
|
||||
() => host.fileNameToModuleName(
|
||||
'/tmp/src/a/child.ts', '/tmp/node_modules/@angular/core.d.ts'))
|
||||
.toThrowError(
|
||||
'Trying to import a source file from a node_modules package: ' +
|
||||
'import /tmp/src/a/child.ts from /tmp/node_modules/@angular/core.d.ts');
|
||||
});
|
||||
|
||||
it('should use the provided implementation if any', () => {
|
||||
const ngHost = createNgHost();
|
||||
ngHost.fileNameToModuleName = () => 'someResult';
|
||||
const host = createHost({ngHost});
|
||||
expect(host.fileNameToModuleName('a', 'b')).toBe('someResult');
|
||||
});
|
||||
});
|
||||
|
||||
describe('moduleNameToFileName', () => {
|
||||
it('should resolve an import using the containing file', () => {
|
||||
const host = createHost({files: {'tmp': {'src': {'a': {'child.d.ts': dummyModule}}}}});
|
||||
expect(host.moduleNameToFileName('./a/child', '/tmp/src/index.ts'))
|
||||
.toBe('/tmp/src/a/child.d.ts');
|
||||
});
|
||||
|
||||
it('should allow to skip the containing file for package imports', () => {
|
||||
const host =
|
||||
createHost({files: {'tmp': {'node_modules': {'@core': {'index.d.ts': dummyModule}}}}});
|
||||
expect(host.moduleNameToFileName('@core/index')).toBe('/tmp/node_modules/@core/index.d.ts');
|
||||
});
|
||||
|
||||
it('should use the provided implementation if any', () => {
|
||||
const ngHost = createNgHost();
|
||||
ngHost.moduleNameToFileName = () => 'someResult';
|
||||
const host = createHost({ngHost});
|
||||
expect(host.moduleNameToFileName('a', 'b')).toBe('someResult');
|
||||
});
|
||||
|
||||
it('should work well with windows paths', () => {
|
||||
const host = createHost({
|
||||
rootNames: ['\\tmp\\index.ts'],
|
||||
options: {basePath: '\\tmp'},
|
||||
files: {'tmp': {'node_modules': {'@core': {'index.d.ts': dummyModule}}}}
|
||||
});
|
||||
expect(host.moduleNameToFileName('@core/index')).toBe('/tmp/node_modules/@core/index.d.ts');
|
||||
});
|
||||
});
|
||||
|
||||
describe('resourceNameToFileName', () => {
|
||||
it('should resolve a relative import', () => {
|
||||
const host = createHost({files: {'tmp': {'src': {'a': {'child.html': '<div>'}}}}});
|
||||
expect(host.resourceNameToFileName('./a/child.html', '/tmp/src/index.ts'))
|
||||
.toBe('/tmp/src/a/child.html');
|
||||
|
||||
expect(host.resourceNameToFileName('./a/non-existing.html', '/tmp/src/index.ts')).toBe(null);
|
||||
});
|
||||
|
||||
it('should resolve package paths as relative paths', () => {
|
||||
const host = createHost({files: {'tmp': {'src': {'a': {'child.html': '<div>'}}}}});
|
||||
expect(host.resourceNameToFileName('a/child.html', '/tmp/src/index.ts'))
|
||||
.toBe('/tmp/src/a/child.html');
|
||||
});
|
||||
|
||||
it('should resolve absolute paths as package paths', () => {
|
||||
const host = createHost({files: {'tmp': {'node_modules': {'a': {'child.html': '<div>'}}}}});
|
||||
expect(host.resourceNameToFileName('/a/child.html', ''))
|
||||
.toBe('/tmp/node_modules/a/child.html');
|
||||
});
|
||||
|
||||
it('should use the provided implementation if any', () => {
|
||||
const ngHost = createNgHost();
|
||||
ngHost.resourceNameToFileName = () => 'someResult';
|
||||
const host = createHost({ngHost});
|
||||
expect(host.resourceNameToFileName('a', 'b')).toBe('someResult');
|
||||
});
|
||||
it('should resolve Sass imports to generated .css files', () => {
|
||||
const host = createHost({files: {'tmp': {'src': {'a': {'style.css': 'h1: bold'}}}}});
|
||||
expect(host.resourceNameToFileName('./a/style.scss', '/tmp/src/index.ts'))
|
||||
.toBe('/tmp/src/a/style.css');
|
||||
});
|
||||
it('should resolve Less imports to generated .css files', () => {
|
||||
const host = createHost({files: {'tmp': {'src': {'a': {'style.css': 'h1: bold'}}}}});
|
||||
expect(host.resourceNameToFileName('./a/style.less', '/tmp/src/index.ts'))
|
||||
.toBe('/tmp/src/a/style.css');
|
||||
});
|
||||
it('should resolve Stylus imports to generated .css files', () => {
|
||||
const host = createHost({files: {'tmp': {'src': {'a': {'style.css': 'h1: bold'}}}}});
|
||||
expect(host.resourceNameToFileName('./a/style.styl', '/tmp/src/index.ts'))
|
||||
.toBe('/tmp/src/a/style.css');
|
||||
});
|
||||
});
|
||||
|
||||
describe('addGeneratedFile', () => {
|
||||
function generate(path: string, files: {}) {
|
||||
codeGenerator.findGeneratedFileNames.and.returnValue([`${path}.ngfactory.ts`]);
|
||||
codeGenerator.generateFile.and.returnValue(
|
||||
new compiler.GeneratedFile(`${path}.ts`, `${path}.ngfactory.ts`, []));
|
||||
const host = createHost({
|
||||
files,
|
||||
options: {
|
||||
basePath: '/tmp',
|
||||
moduleResolution: ts.ModuleResolutionKind.NodeJs,
|
||||
// Request UMD, which should get default module names
|
||||
module: ts.ModuleKind.UMD
|
||||
},
|
||||
});
|
||||
return host.getSourceFile(`${path}.ngfactory.ts`, ts.ScriptTarget.Latest);
|
||||
}
|
||||
|
||||
it('should include a moduleName when the file is in node_modules', () => {
|
||||
const genSf = generate(
|
||||
'/tmp/node_modules/@angular/core/core',
|
||||
{'tmp': {'node_modules': {'@angular': {'core': {'core.ts': `// some content`}}}}});
|
||||
expect(genSf.moduleName).toBe('@angular/core/core.ngfactory');
|
||||
});
|
||||
|
||||
it('should not get tripped on nested node_modules', () => {
|
||||
const genSf = generate('/tmp/node_modules/lib1/node_modules/lib2/thing', {
|
||||
'tmp':
|
||||
{'node_modules': {'lib1': {'node_modules': {'lib2': {'thing.ts': `// some content`}}}}}
|
||||
});
|
||||
expect(genSf.moduleName).toBe('lib2/thing.ngfactory');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSourceFile', () => {
|
||||
it('should cache source files by name', () => {
|
||||
const host = createHost({files: {'tmp': {'src': {'index.ts': ``}}}});
|
||||
|
||||
const sf1 = host.getSourceFile('/tmp/src/index.ts', ts.ScriptTarget.Latest);
|
||||
const sf2 = host.getSourceFile('/tmp/src/index.ts', ts.ScriptTarget.Latest);
|
||||
expect(sf1).toBe(sf2);
|
||||
});
|
||||
|
||||
it('should generate code when asking for the base name and add it as referencedFiles', () => {
|
||||
codeGenerator.findGeneratedFileNames.and.returnValue(['/tmp/src/index.ngfactory.ts']);
|
||||
codeGenerator.generateFile.and.returnValue(aGeneratedFile);
|
||||
const host = createHost({
|
||||
files: {
|
||||
'tmp': {
|
||||
'src': {
|
||||
'index.ts': `
|
||||
/// <reference path="main.ts"/>
|
||||
`
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const sf = host.getSourceFile('/tmp/src/index.ts', ts.ScriptTarget.Latest);
|
||||
expect(sf.referencedFiles[0].fileName).toBe('main.ts');
|
||||
expect(sf.referencedFiles[1].fileName).toBe('/tmp/src/index.ngfactory.ts');
|
||||
|
||||
const genSf = host.getSourceFile('/tmp/src/index.ngfactory.ts', ts.ScriptTarget.Latest);
|
||||
expect(genSf.text).toBe(aGeneratedFileText);
|
||||
|
||||
// the codegen should have been cached
|
||||
expect(codeGenerator.generateFile).toHaveBeenCalledTimes(1);
|
||||
expect(codeGenerator.findGeneratedFileNames).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should generate code when asking for the generated name first', () => {
|
||||
codeGenerator.findGeneratedFileNames.and.returnValue(['/tmp/src/index.ngfactory.ts']);
|
||||
codeGenerator.generateFile.and.returnValue(aGeneratedFile);
|
||||
const host = createHost({
|
||||
files: {
|
||||
'tmp': {
|
||||
'src': {
|
||||
'index.ts': `
|
||||
/// <reference path="main.ts"/>
|
||||
`
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const genSf = host.getSourceFile('/tmp/src/index.ngfactory.ts', ts.ScriptTarget.Latest);
|
||||
expect(genSf.text).toBe(aGeneratedFileText);
|
||||
|
||||
const sf = host.getSourceFile('/tmp/src/index.ts', ts.ScriptTarget.Latest);
|
||||
expect(sf.referencedFiles[0].fileName).toBe('main.ts');
|
||||
expect(sf.referencedFiles[1].fileName).toBe('/tmp/src/index.ngfactory.ts');
|
||||
|
||||
// the codegen should have been cached
|
||||
expect(codeGenerator.generateFile).toHaveBeenCalledTimes(1);
|
||||
expect(codeGenerator.findGeneratedFileNames).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should clear old generated references if the original host cached them', () => {
|
||||
codeGenerator.findGeneratedFileNames.and.returnValue(['/tmp/src/index.ngfactory.ts']);
|
||||
|
||||
const sfText = `
|
||||
/// <reference path="main.ts"/>
|
||||
`;
|
||||
const ngHost = createNgHost({files: {'tmp': {'src': {'index.ts': sfText}}}});
|
||||
const sf = ts.createSourceFile('/tmp/src/index.ts', sfText, ts.ScriptTarget.Latest);
|
||||
ngHost.getSourceFile = () => sf;
|
||||
|
||||
codeGenerator.findGeneratedFileNames.and.returnValue(['/tmp/src/index.ngfactory.ts']);
|
||||
codeGenerator.generateFile.and.returnValue(
|
||||
new compiler.GeneratedFile('/tmp/src/index.ts', '/tmp/src/index.ngfactory.ts', []));
|
||||
const host1 = createHost({ngHost});
|
||||
|
||||
host1.getSourceFile('/tmp/src/index.ts', ts.ScriptTarget.Latest);
|
||||
expect(sf.referencedFiles.length).toBe(2);
|
||||
expect(sf.referencedFiles[0].fileName).toBe('main.ts');
|
||||
expect(sf.referencedFiles[1].fileName).toBe('/tmp/src/index.ngfactory.ts');
|
||||
|
||||
codeGenerator.findGeneratedFileNames.and.returnValue([]);
|
||||
codeGenerator.generateFile.and.returnValue(null);
|
||||
const host2 = createHost({ngHost});
|
||||
|
||||
host2.getSourceFile('/tmp/src/index.ts', ts.ScriptTarget.Latest);
|
||||
expect(sf.referencedFiles.length).toBe(1);
|
||||
expect(sf.referencedFiles[0].fileName).toBe('main.ts');
|
||||
});
|
||||
|
||||
it('should generate for tsx files', () => {
|
||||
codeGenerator.findGeneratedFileNames.and.returnValue(['/tmp/src/index.ngfactory.ts']);
|
||||
codeGenerator.generateFile.and.returnValue(aGeneratedFile);
|
||||
const host = createHost({files: {'tmp': {'src': {'index.tsx': ``}}}});
|
||||
|
||||
const genSf = host.getSourceFile('/tmp/src/index.ngfactory.ts', ts.ScriptTarget.Latest);
|
||||
expect(genSf.text).toBe(aGeneratedFileText);
|
||||
|
||||
const sf = host.getSourceFile('/tmp/src/index.tsx', ts.ScriptTarget.Latest);
|
||||
expect(sf.referencedFiles[0].fileName).toBe('/tmp/src/index.ngfactory.ts');
|
||||
|
||||
// the codegen should have been cached
|
||||
expect(codeGenerator.generateFile).toHaveBeenCalledTimes(1);
|
||||
expect(codeGenerator.findGeneratedFileNames).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateSourceFile', () => {
|
||||
it('should update source files', () => {
|
||||
codeGenerator.findGeneratedFileNames.and.returnValue(['/tmp/src/index.ngfactory.ts']);
|
||||
codeGenerator.generateFile.and.returnValue(aGeneratedFile);
|
||||
const host = createHost({files: {'tmp': {'src': {'index.ts': ''}}}});
|
||||
|
||||
let genSf = host.getSourceFile('/tmp/src/index.ngfactory.ts', ts.ScriptTarget.Latest);
|
||||
expect(genSf.text).toBe(aGeneratedFileText);
|
||||
|
||||
host.updateGeneratedFile(new compiler.GeneratedFile(
|
||||
'/tmp/src/index.ts', '/tmp/src/index.ngfactory.ts',
|
||||
[new compiler.DeclareVarStmt('x', new compiler.LiteralExpr(2))]));
|
||||
genSf = host.getSourceFile('/tmp/src/index.ngfactory.ts', ts.ScriptTarget.Latest);
|
||||
expect(genSf.text).toBe(`var x:any = 2;\n`);
|
||||
});
|
||||
|
||||
it('should error if the imports changed', () => {
|
||||
codeGenerator.findGeneratedFileNames.and.returnValue(['/tmp/src/index.ngfactory.ts']);
|
||||
codeGenerator.generateFile.and.returnValue(new compiler.GeneratedFile(
|
||||
'/tmp/src/index.ts', '/tmp/src/index.ngfactory.ts',
|
||||
[new compiler.DeclareVarStmt(
|
||||
'x',
|
||||
new compiler.ExternalExpr(new compiler.ExternalReference('aModule', 'aName')))]));
|
||||
const host = createHost({files: {'tmp': {'src': {'index.ts': ''}}}});
|
||||
|
||||
host.getSourceFile('/tmp/src/index.ngfactory.ts', ts.ScriptTarget.Latest);
|
||||
|
||||
expect(
|
||||
() => host.updateGeneratedFile(new compiler.GeneratedFile(
|
||||
'/tmp/src/index.ts', '/tmp/src/index.ngfactory.ts',
|
||||
[new compiler.DeclareVarStmt(
|
||||
'x',
|
||||
new compiler.ExternalExpr(
|
||||
new compiler.ExternalReference('otherModule', 'aName')))])))
|
||||
.toThrowError([
|
||||
`Illegal State: external references changed in /tmp/src/index.ngfactory.ts.`,
|
||||
`Old: aModule.`, `New: otherModule`
|
||||
].join('\n'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,868 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 ts from 'typescript';
|
||||
|
||||
import {TypeScriptReflectionHost} from '../../src/ngtsc/reflection';
|
||||
import {getDownlevelDecoratorsTransform} from '../../src/transformers/downlevel_decorators_transform/index';
|
||||
import {MockAotContext, MockCompilerHost} from '../mocks';
|
||||
|
||||
const TEST_FILE_INPUT = '/test.ts';
|
||||
const TEST_FILE_OUTPUT = `/test.js`;
|
||||
const TEST_FILE_DTS_OUTPUT = `/test.d.ts`;
|
||||
|
||||
describe('downlevel decorator transform', () => {
|
||||
let host: MockCompilerHost;
|
||||
let context: MockAotContext;
|
||||
let diagnostics: ts.Diagnostic[];
|
||||
let isClosureEnabled: boolean;
|
||||
let skipClassDecorators: boolean;
|
||||
|
||||
beforeEach(() => {
|
||||
diagnostics = [];
|
||||
context = new MockAotContext('/', {
|
||||
'dom_globals.d.ts': `
|
||||
declare class HTMLElement {};
|
||||
declare class Document {};
|
||||
`
|
||||
});
|
||||
host = new MockCompilerHost(context);
|
||||
isClosureEnabled = false;
|
||||
skipClassDecorators = false;
|
||||
});
|
||||
|
||||
function transform(
|
||||
contents: string, compilerOptions: ts.CompilerOptions = {},
|
||||
preTransformers: ts.TransformerFactory<ts.SourceFile>[] = []) {
|
||||
context.writeFile(TEST_FILE_INPUT, contents);
|
||||
const program = ts.createProgram(
|
||||
[TEST_FILE_INPUT, '/dom_globals.d.ts'], {
|
||||
module: ts.ModuleKind.CommonJS,
|
||||
importHelpers: true,
|
||||
lib: ['dom', 'es2015'],
|
||||
target: ts.ScriptTarget.ES2017,
|
||||
declaration: true,
|
||||
experimentalDecorators: true,
|
||||
emitDecoratorMetadata: false,
|
||||
...compilerOptions
|
||||
},
|
||||
host);
|
||||
const testFile = program.getSourceFile(TEST_FILE_INPUT);
|
||||
const typeChecker = program.getTypeChecker();
|
||||
const reflectionHost = new TypeScriptReflectionHost(typeChecker);
|
||||
const transformers: ts.CustomTransformers = {
|
||||
before: [
|
||||
...preTransformers,
|
||||
getDownlevelDecoratorsTransform(
|
||||
program.getTypeChecker(), reflectionHost, diagnostics,
|
||||
/* isCore */ false, isClosureEnabled, skipClassDecorators)
|
||||
]
|
||||
};
|
||||
let output: string|null = null;
|
||||
let dtsOutput: string|null = null;
|
||||
const emitResult = program.emit(
|
||||
testFile, ((fileName, outputText) => {
|
||||
if (fileName === TEST_FILE_OUTPUT) {
|
||||
output = outputText;
|
||||
} else if (fileName === TEST_FILE_DTS_OUTPUT) {
|
||||
dtsOutput = outputText;
|
||||
}
|
||||
}),
|
||||
undefined, undefined, transformers);
|
||||
diagnostics.push(...emitResult.diagnostics);
|
||||
expect(output).not.toBeNull();
|
||||
return {
|
||||
output: omitLeadingWhitespace(output!),
|
||||
dtsOutput: dtsOutput ? omitLeadingWhitespace(dtsOutput) : null
|
||||
};
|
||||
}
|
||||
|
||||
it('should downlevel decorators for @Injectable decorated class', () => {
|
||||
const {output} = transform(`
|
||||
import {Injectable} from '@angular/core';
|
||||
|
||||
export class ClassInject {};
|
||||
|
||||
@Injectable()
|
||||
export class MyService {
|
||||
constructor(v: ClassInject) {}
|
||||
}
|
||||
`);
|
||||
|
||||
expect(diagnostics.length).toBe(0);
|
||||
expect(output).toContain(dedent`
|
||||
MyService.decorators = [
|
||||
{ type: core_1.Injectable }
|
||||
];
|
||||
MyService.ctorParameters = () => [
|
||||
{ type: ClassInject }
|
||||
];`);
|
||||
expect(output).not.toContain('tslib');
|
||||
});
|
||||
|
||||
it('should downlevel decorators for @Directive decorated class', () => {
|
||||
const {output} = transform(`
|
||||
import {Directive} from '@angular/core';
|
||||
|
||||
export class ClassInject {};
|
||||
|
||||
@Directive()
|
||||
export class MyDir {
|
||||
constructor(v: ClassInject) {}
|
||||
}
|
||||
`);
|
||||
|
||||
expect(diagnostics.length).toBe(0);
|
||||
expect(output).toContain(dedent`
|
||||
MyDir.decorators = [
|
||||
{ type: core_1.Directive }
|
||||
];
|
||||
MyDir.ctorParameters = () => [
|
||||
{ type: ClassInject }
|
||||
];`);
|
||||
expect(output).not.toContain('tslib');
|
||||
});
|
||||
|
||||
it('should downlevel decorators for @Component decorated class', () => {
|
||||
const {output} = transform(`
|
||||
import {Component} from '@angular/core';
|
||||
|
||||
export class ClassInject {};
|
||||
|
||||
@Component({template: 'hello'})
|
||||
export class MyComp {
|
||||
constructor(v: ClassInject) {}
|
||||
}
|
||||
`);
|
||||
|
||||
expect(diagnostics.length).toBe(0);
|
||||
expect(output).toContain(dedent`
|
||||
MyComp.decorators = [
|
||||
{ type: core_1.Component, args: [{ template: 'hello' },] }
|
||||
];
|
||||
MyComp.ctorParameters = () => [
|
||||
{ type: ClassInject }
|
||||
];`);
|
||||
expect(output).not.toContain('tslib');
|
||||
});
|
||||
|
||||
it('should downlevel decorators for @Pipe decorated class', () => {
|
||||
const {output} = transform(`
|
||||
import {Pipe} from '@angular/core';
|
||||
|
||||
export class ClassInject {};
|
||||
|
||||
@Pipe({selector: 'hello'})
|
||||
export class MyPipe {
|
||||
constructor(v: ClassInject) {}
|
||||
}
|
||||
`);
|
||||
|
||||
expect(diagnostics.length).toBe(0);
|
||||
expect(output).toContain(dedent`
|
||||
MyPipe.decorators = [
|
||||
{ type: core_1.Pipe, args: [{ selector: 'hello' },] }
|
||||
];
|
||||
MyPipe.ctorParameters = () => [
|
||||
{ type: ClassInject }
|
||||
];`);
|
||||
expect(output).not.toContain('tslib');
|
||||
});
|
||||
|
||||
it('should not downlevel non-Angular class decorators', () => {
|
||||
const {output} = transform(`
|
||||
@SomeUnknownDecorator()
|
||||
export class MyClass {}
|
||||
`);
|
||||
|
||||
expect(diagnostics.length).toBe(0);
|
||||
expect(output).toContain(dedent`
|
||||
MyClass = (0, tslib_1.__decorate)([
|
||||
SomeUnknownDecorator()
|
||||
], MyClass);
|
||||
`);
|
||||
expect(output).not.toContain('MyClass.decorators');
|
||||
});
|
||||
|
||||
it('should not downlevel non-Angular class decorators generated by a builder', () => {
|
||||
const {output} = transform(`
|
||||
@DecoratorBuilder().customClassDecorator
|
||||
export class MyClass {}
|
||||
`);
|
||||
|
||||
expect(diagnostics.length).toBe(0);
|
||||
expect(output).toContain(dedent`
|
||||
MyClass = (0, tslib_1.__decorate)([
|
||||
DecoratorBuilder().customClassDecorator
|
||||
], MyClass);
|
||||
`);
|
||||
expect(output).not.toContain('MyClass.decorators');
|
||||
});
|
||||
|
||||
it('should downlevel Angular-decorated class member', () => {
|
||||
const {output} = transform(`
|
||||
import {Input} from '@angular/core';
|
||||
|
||||
export class MyDir {
|
||||
@Input() disabled: boolean = false;
|
||||
}
|
||||
`);
|
||||
|
||||
expect(diagnostics.length).toBe(0);
|
||||
expect(output).toContain(dedent`
|
||||
MyDir.propDecorators = {
|
||||
disabled: [{ type: core_1.Input }]
|
||||
};
|
||||
`);
|
||||
expect(output).not.toContain('tslib');
|
||||
});
|
||||
|
||||
it('should not downlevel class member with unknown decorator', () => {
|
||||
const {output} = transform(`
|
||||
export class MyDir {
|
||||
@SomeDecorator() disabled: boolean = false;
|
||||
}
|
||||
`);
|
||||
|
||||
expect(diagnostics.length).toBe(0);
|
||||
expect(output).toContain(dedent`
|
||||
(0, tslib_1.__decorate)([
|
||||
SomeDecorator()
|
||||
], MyDir.prototype, "disabled", void 0);
|
||||
`);
|
||||
expect(output).not.toContain('MyClass.propDecorators');
|
||||
});
|
||||
|
||||
// Angular is not concerned with type information for decorated class members. Instead,
|
||||
// the type is omitted. This also helps with server side rendering as DOM globals which
|
||||
// are used as types, do not load at runtime. https://github.com/angular/angular/issues/30586.
|
||||
it('should downlevel Angular-decorated class member but not preserve type', () => {
|
||||
context.writeFile('/other-file.ts', `export class MyOtherClass {}`);
|
||||
const {output} = transform(`
|
||||
import {Input} from '@angular/core';
|
||||
import {MyOtherClass} from './other-file';
|
||||
|
||||
export class MyDir {
|
||||
@Input() trigger: HTMLElement;
|
||||
@Input() fromOtherFile: MyOtherClass;
|
||||
}
|
||||
`);
|
||||
|
||||
expect(diagnostics.length).toBe(0);
|
||||
expect(output).toContain(dedent`
|
||||
MyDir.propDecorators = {
|
||||
trigger: [{ type: core_1.Input }],
|
||||
fromOtherFile: [{ type: core_1.Input }]
|
||||
};
|
||||
`);
|
||||
expect(output).not.toContain('HTMLElement');
|
||||
expect(output).not.toContain('MyOtherClass');
|
||||
});
|
||||
|
||||
it('should capture constructor type metadata with `emitDecoratorMetadata` enabled', () => {
|
||||
context.writeFile('/other-file.ts', `export class MyOtherClass {}`);
|
||||
const {output} = transform(
|
||||
`
|
||||
import {Directive} from '@angular/core';
|
||||
import {MyOtherClass} from './other-file';
|
||||
|
||||
@Directive()
|
||||
export class MyDir {
|
||||
constructor(other: MyOtherClass) {}
|
||||
}
|
||||
`,
|
||||
{emitDecoratorMetadata: true});
|
||||
|
||||
expect(diagnostics.length).toBe(0);
|
||||
expect(output).toContain('const other_file_1 = require("./other-file");');
|
||||
expect(output).toContain(dedent`
|
||||
MyDir.decorators = [
|
||||
{ type: core_1.Directive }
|
||||
];
|
||||
MyDir.ctorParameters = () => [
|
||||
{ type: other_file_1.MyOtherClass }
|
||||
];
|
||||
`);
|
||||
});
|
||||
|
||||
it('should capture constructor type metadata with `emitDecoratorMetadata` disabled', () => {
|
||||
context.writeFile('/other-file.ts', `export class MyOtherClass {}`);
|
||||
const {output, dtsOutput} = transform(
|
||||
`
|
||||
import {Directive} from '@angular/core';
|
||||
import {MyOtherClass} from './other-file';
|
||||
|
||||
@Directive()
|
||||
export class MyDir {
|
||||
constructor(other: MyOtherClass) {}
|
||||
}
|
||||
`,
|
||||
{emitDecoratorMetadata: false});
|
||||
|
||||
expect(diagnostics.length).toBe(0);
|
||||
expect(output).toContain('const other_file_1 = require("./other-file");');
|
||||
expect(output).toContain(dedent`
|
||||
MyDir.decorators = [
|
||||
{ type: core_1.Directive }
|
||||
];
|
||||
MyDir.ctorParameters = () => [
|
||||
{ type: other_file_1.MyOtherClass }
|
||||
];
|
||||
`);
|
||||
expect(dtsOutput).toContain('import');
|
||||
});
|
||||
|
||||
it('should properly serialize constructor parameter with external qualified name type', () => {
|
||||
context.writeFile('/other-file.ts', `export class MyOtherClass {}`);
|
||||
const {output} = transform(`
|
||||
import {Directive} from '@angular/core';
|
||||
import * as externalFile from './other-file';
|
||||
|
||||
@Directive()
|
||||
export class MyDir {
|
||||
constructor(other: externalFile.MyOtherClass) {}
|
||||
}
|
||||
`);
|
||||
|
||||
expect(diagnostics.length).toBe(0);
|
||||
expect(output).toContain('const externalFile = require("./other-file");');
|
||||
expect(output).toContain(dedent`
|
||||
MyDir.decorators = [
|
||||
{ type: core_1.Directive }
|
||||
];
|
||||
MyDir.ctorParameters = () => [
|
||||
{ type: externalFile.MyOtherClass }
|
||||
];
|
||||
`);
|
||||
});
|
||||
|
||||
it('should properly serialize constructor parameter with local qualified name type', () => {
|
||||
const {output} = transform(`
|
||||
import {Directive} from '@angular/core';
|
||||
|
||||
namespace other {
|
||||
export class OtherClass {}
|
||||
};
|
||||
|
||||
@Directive()
|
||||
export class MyDir {
|
||||
constructor(other: other.OtherClass) {}
|
||||
}
|
||||
`);
|
||||
|
||||
expect(diagnostics.length).toBe(0);
|
||||
expect(output).toContain('var other;');
|
||||
expect(output).toContain(dedent`
|
||||
MyDir.decorators = [
|
||||
{ type: core_1.Directive }
|
||||
];
|
||||
MyDir.ctorParameters = () => [
|
||||
{ type: other.OtherClass }
|
||||
];
|
||||
`);
|
||||
});
|
||||
|
||||
it('should properly downlevel constructor parameter decorators', () => {
|
||||
const {output} = transform(`
|
||||
import {Inject, Directive, DOCUMENT} from '@angular/core';
|
||||
|
||||
@Directive()
|
||||
export class MyDir {
|
||||
constructor(@Inject(DOCUMENT) document: Document) {}
|
||||
}
|
||||
`);
|
||||
|
||||
expect(diagnostics.length).toBe(0);
|
||||
expect(output).toContain(dedent`
|
||||
MyDir.decorators = [
|
||||
{ type: core_1.Directive }
|
||||
];
|
||||
MyDir.ctorParameters = () => [
|
||||
{ type: Document, decorators: [{ type: core_1.Inject, args: [core_1.DOCUMENT,] }] }
|
||||
];
|
||||
`);
|
||||
});
|
||||
|
||||
it('should properly downlevel constructor parameters with union type', () => {
|
||||
const {output} = transform(`
|
||||
import {Optional, Directive, NgZone} from '@angular/core';
|
||||
|
||||
@Directive()
|
||||
export class MyDir {
|
||||
constructor(@Optional() ngZone: NgZone|null) {}
|
||||
}
|
||||
`);
|
||||
|
||||
expect(diagnostics.length).toBe(0);
|
||||
expect(output).toContain(dedent`
|
||||
MyDir.decorators = [
|
||||
{ type: core_1.Directive }
|
||||
];
|
||||
MyDir.ctorParameters = () => [
|
||||
{ type: core_1.NgZone, decorators: [{ type: core_1.Optional }] }
|
||||
];
|
||||
`);
|
||||
});
|
||||
|
||||
it('should add @nocollapse if closure compiler is enabled', () => {
|
||||
isClosureEnabled = true;
|
||||
const {output} = transform(`
|
||||
import {Directive} from '@angular/core';
|
||||
|
||||
export class ClassInject {};
|
||||
|
||||
@Directive()
|
||||
export class MyDir {
|
||||
constructor(v: ClassInject) {}
|
||||
}
|
||||
`);
|
||||
|
||||
expect(diagnostics.length).toBe(0);
|
||||
expect(output).toContain(dedent`
|
||||
/** @type {!Array<{type: !Function, args: (undefined|!Array<?>)}>} */
|
||||
MyDir.decorators = [
|
||||
{ type: core_1.Directive }
|
||||
];
|
||||
`);
|
||||
expect(output).toContain(dedent`
|
||||
/**
|
||||
* @type {function(): !Array<(null|{
|
||||
* type: ?,
|
||||
* decorators: (undefined|!Array<{type: !Function, args: (undefined|!Array<?>)}>),
|
||||
* })>}
|
||||
* @nocollapse
|
||||
*/
|
||||
MyDir.ctorParameters = () => [
|
||||
{ type: ClassInject }
|
||||
];
|
||||
`);
|
||||
expect(output).not.toContain('tslib');
|
||||
});
|
||||
|
||||
it('should not retain unused type imports due to decorator downleveling with ' +
|
||||
'`emitDecoratorMetadata` enabled.',
|
||||
() => {
|
||||
context.writeFile('/external.ts', `
|
||||
export class ErrorHandler {}
|
||||
export class ClassInject {}
|
||||
`);
|
||||
const {output} = transform(
|
||||
`
|
||||
import {Directive} from '@angular/core';
|
||||
import {ErrorHandler, ClassInject} from './external';
|
||||
|
||||
@Directive()
|
||||
export class MyDir {
|
||||
private _errorHandler: ErrorHandler;
|
||||
constructor(v: ClassInject) {}
|
||||
}
|
||||
`,
|
||||
{module: ts.ModuleKind.ES2015, emitDecoratorMetadata: true});
|
||||
|
||||
expect(diagnostics.length).toBe(0);
|
||||
expect(output).not.toContain('tslib');
|
||||
expect(output).not.toContain('ErrorHandler');
|
||||
});
|
||||
|
||||
it('should not retain unused type imports due to decorator downleveling with ' +
|
||||
'`emitDecoratorMetadata` disabled',
|
||||
() => {
|
||||
context.writeFile('/external.ts', `
|
||||
export class ErrorHandler {}
|
||||
export class ClassInject {}
|
||||
`);
|
||||
const {output} = transform(
|
||||
`
|
||||
import {Directive} from '@angular/core';
|
||||
import {ErrorHandler, ClassInject} from './external';
|
||||
|
||||
@Directive()
|
||||
export class MyDir {
|
||||
private _errorHandler: ErrorHandler;
|
||||
constructor(v: ClassInject) {}
|
||||
}
|
||||
`,
|
||||
{module: ts.ModuleKind.ES2015, emitDecoratorMetadata: false});
|
||||
|
||||
expect(diagnostics.length).toBe(0);
|
||||
expect(output).not.toContain('tslib');
|
||||
expect(output).not.toContain('ErrorHandler');
|
||||
});
|
||||
|
||||
it('should not generate invalid reference due to conflicting parameter name', () => {
|
||||
context.writeFile('/external.ts', `
|
||||
export class Dep {
|
||||
greet() {}
|
||||
}
|
||||
`);
|
||||
const {output} = transform(
|
||||
`
|
||||
import {Directive} from '@angular/core';
|
||||
import {Dep} from './external';
|
||||
|
||||
@Directive()
|
||||
export class MyDir {
|
||||
constructor(Dep: Dep) {
|
||||
Dep.greet();
|
||||
}
|
||||
}
|
||||
`,
|
||||
{emitDecoratorMetadata: false});
|
||||
|
||||
expect(diagnostics.length).toBe(0);
|
||||
expect(output).not.toContain('tslib');
|
||||
expect(output).toContain(`external_1 = require("./external");`);
|
||||
expect(output).toContain(dedent`
|
||||
MyDir.decorators = [
|
||||
{ type: core_1.Directive }
|
||||
];
|
||||
MyDir.ctorParameters = () => [
|
||||
{ type: external_1.Dep }
|
||||
];
|
||||
`);
|
||||
});
|
||||
|
||||
it('should be able to serialize circular constructor parameter type', () => {
|
||||
const {output} = transform(`
|
||||
import {Directive, Optional, Inject, SkipSelf} from '@angular/core';
|
||||
|
||||
@Directive()
|
||||
export class MyDir {
|
||||
constructor(@Optional() @SkipSelf() @Inject(MyDir) parentDir: MyDir|null) {}
|
||||
}
|
||||
`);
|
||||
|
||||
expect(diagnostics.length).toBe(0);
|
||||
expect(output).toContain(dedent`
|
||||
MyDir.decorators = [
|
||||
{ type: core_1.Directive }
|
||||
];
|
||||
MyDir.ctorParameters = () => [
|
||||
{ type: MyDir, decorators: [{ type: core_1.Optional }, { type: core_1.SkipSelf }, { type: core_1.Inject, args: [MyDir,] }] }
|
||||
];
|
||||
`);
|
||||
});
|
||||
|
||||
it('should create diagnostic if property name is non-serializable', () => {
|
||||
transform(`
|
||||
import {Directive, ViewChild, TemplateRef} from '@angular/core';
|
||||
|
||||
@Directive()
|
||||
export class MyDir {
|
||||
@ViewChild(TemplateRef) ['some' + 'name']: TemplateRef<any>|undefined;
|
||||
}
|
||||
`);
|
||||
|
||||
expect(diagnostics.length).toBe(1);
|
||||
expect(diagnostics[0].messageText as string)
|
||||
.toBe(`Cannot process decorators for class element with non-analyzable name.`);
|
||||
});
|
||||
|
||||
it('should not capture constructor parameter types when not resolving to a value', () => {
|
||||
context.writeFile('/external.ts', `
|
||||
export interface IState {}
|
||||
export type IOverlay = {hello: true}&IState;
|
||||
export default interface {
|
||||
hello: false;
|
||||
}
|
||||
export const enum KeyCodes {A, B}
|
||||
`);
|
||||
const {output} = transform(`
|
||||
import {Directive, Inject} from '@angular/core';
|
||||
import * as angular from './external';
|
||||
import {IOverlay, KeyCodes} from './external';
|
||||
import TypeFromDefaultImport from './external';
|
||||
|
||||
@Directive()
|
||||
export class MyDir {
|
||||
constructor(@Inject('$state') param: angular.IState,
|
||||
@Inject('$overlay') other: IOverlay,
|
||||
@Inject('$default') default: TypeFromDefaultImport,
|
||||
@Inject('$keyCodes') keyCodes: KeyCodes) {}
|
||||
}
|
||||
`);
|
||||
|
||||
expect(diagnostics.length).toBe(0);
|
||||
expect(output).not.toContain('external');
|
||||
expect(output).toContain(dedent`
|
||||
MyDir.decorators = [
|
||||
{ type: core_1.Directive }
|
||||
];
|
||||
MyDir.ctorParameters = () => [
|
||||
{ type: undefined, decorators: [{ type: core_1.Inject, args: ['$state',] }] },
|
||||
{ type: undefined, decorators: [{ type: core_1.Inject, args: ['$overlay',] }] },
|
||||
{ type: undefined, decorators: [{ type: core_1.Inject, args: ['$default',] }] },
|
||||
{ type: undefined, decorators: [{ type: core_1.Inject, args: ['$keyCodes',] }] }
|
||||
];
|
||||
`);
|
||||
});
|
||||
|
||||
it('should allow preceding custom transformers to strip decorators', () => {
|
||||
const stripAllDecoratorsTransform: ts.TransformerFactory<ts.SourceFile> = context => {
|
||||
return (sourceFile: ts.SourceFile) => {
|
||||
const visitNode = (node: ts.Node): ts.Node => {
|
||||
if (ts.isClassDeclaration(node) || ts.isClassElement(node)) {
|
||||
const cloned = ts.getMutableClone(node);
|
||||
(cloned.decorators as undefined) = undefined;
|
||||
return cloned;
|
||||
}
|
||||
return ts.visitEachChild(node, visitNode, context);
|
||||
};
|
||||
return visitNode(sourceFile) as ts.SourceFile;
|
||||
};
|
||||
};
|
||||
|
||||
const {output} = transform(
|
||||
`
|
||||
import {Directive} from '@angular/core';
|
||||
|
||||
export class MyInjectedClass {}
|
||||
|
||||
@Directive()
|
||||
export class MyDir {
|
||||
constructor(someToken: MyInjectedClass) {}
|
||||
}
|
||||
`,
|
||||
{}, [stripAllDecoratorsTransform]);
|
||||
|
||||
expect(diagnostics.length).toBe(0);
|
||||
expect(output).not.toContain('MyDir.decorators');
|
||||
expect(output).not.toContain('MyDir.ctorParameters');
|
||||
expect(output).not.toContain('tslib');
|
||||
});
|
||||
|
||||
it('should capture a non-const enum used as a constructor type', () => {
|
||||
const {output} = transform(`
|
||||
import {Component} from '@angular/core';
|
||||
|
||||
export enum Values {A, B};
|
||||
|
||||
@Component({template: 'hello'})
|
||||
export class MyComp {
|
||||
constructor(v: Values) {}
|
||||
}
|
||||
`);
|
||||
|
||||
expect(diagnostics.length).toBe(0);
|
||||
expect(output).toContain(dedent`
|
||||
MyComp.decorators = [
|
||||
{ type: core_1.Component, args: [{ template: 'hello' },] }
|
||||
];
|
||||
MyComp.ctorParameters = () => [
|
||||
{ type: Values }
|
||||
];`);
|
||||
expect(output).not.toContain('tslib');
|
||||
});
|
||||
|
||||
describe('class decorators skipped', () => {
|
||||
beforeEach(() => skipClassDecorators = true);
|
||||
|
||||
it('should not downlevel Angular class decorators', () => {
|
||||
const {output} = transform(`
|
||||
import {Injectable} from '@angular/core';
|
||||
|
||||
@Injectable()
|
||||
export class MyService {}
|
||||
`);
|
||||
|
||||
expect(diagnostics.length).toBe(0);
|
||||
expect(output).not.toContain('MyService.decorators');
|
||||
expect(output).toContain(dedent`
|
||||
MyService = (0, tslib_1.__decorate)([
|
||||
(0, core_1.Injectable)()
|
||||
], MyService);
|
||||
`);
|
||||
});
|
||||
|
||||
it('should downlevel constructor parameters', () => {
|
||||
const {output} = transform(`
|
||||
import {Injectable} from '@angular/core';
|
||||
|
||||
@Injectable()
|
||||
export class InjectClass {}
|
||||
|
||||
@Injectable()
|
||||
export class MyService {
|
||||
constructor(dep: InjectClass) {}
|
||||
}
|
||||
`);
|
||||
|
||||
expect(diagnostics.length).toBe(0);
|
||||
expect(output).not.toContain('MyService.decorators');
|
||||
expect(output).toContain('MyService.ctorParameters');
|
||||
expect(output).toContain(dedent`
|
||||
MyService.ctorParameters = () => [
|
||||
{ type: InjectClass }
|
||||
];
|
||||
MyService = (0, tslib_1.__decorate)([
|
||||
(0, core_1.Injectable)()
|
||||
], MyService);
|
||||
`);
|
||||
});
|
||||
|
||||
it('should downlevel constructor parameter decorators', () => {
|
||||
const {output} = transform(`
|
||||
import {Injectable, Inject} from '@angular/core';
|
||||
|
||||
@Injectable()
|
||||
export class InjectClass {}
|
||||
|
||||
@Injectable()
|
||||
export class MyService {
|
||||
constructor(@Inject('test') dep: InjectClass) {}
|
||||
}
|
||||
`);
|
||||
|
||||
expect(diagnostics.length).toBe(0);
|
||||
expect(output).not.toContain('MyService.decorators');
|
||||
expect(output).toContain('MyService.ctorParameters');
|
||||
expect(output).toContain(dedent`
|
||||
MyService.ctorParameters = () => [
|
||||
{ type: InjectClass, decorators: [{ type: core_1.Inject, args: ['test',] }] }
|
||||
];
|
||||
MyService = (0, tslib_1.__decorate)([
|
||||
(0, core_1.Injectable)()
|
||||
], MyService);
|
||||
`);
|
||||
});
|
||||
|
||||
it('should downlevel class member Angular decorators', () => {
|
||||
const {output} = transform(`
|
||||
import {Injectable, Input} from '@angular/core';
|
||||
|
||||
export class MyService {
|
||||
@Input() disabled: boolean;
|
||||
}
|
||||
`);
|
||||
|
||||
expect(diagnostics.length).toBe(0);
|
||||
expect(output).not.toContain('tslib');
|
||||
expect(output).toContain(dedent`
|
||||
MyService.propDecorators = {
|
||||
disabled: [{ type: core_1.Input }]
|
||||
};
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('transforming multiple files', () => {
|
||||
it('should work correctly for multiple files that import distinct declarations', () => {
|
||||
context.writeFile('foo_service.d.ts', `
|
||||
export declare class Foo {};
|
||||
`);
|
||||
context.writeFile('foo.ts', `
|
||||
import {Injectable} from '@angular/core';
|
||||
import {Foo} from './foo_service';
|
||||
|
||||
@Injectable()
|
||||
export class MyService {
|
||||
constructor(foo: Foo) {}
|
||||
}
|
||||
`);
|
||||
|
||||
context.writeFile('bar_service.d.ts', `
|
||||
export declare class Bar {};
|
||||
`);
|
||||
context.writeFile('bar.ts', `
|
||||
import {Injectable} from '@angular/core';
|
||||
import {Bar} from './bar_service';
|
||||
|
||||
@Injectable()
|
||||
export class MyService {
|
||||
constructor(bar: Bar) {}
|
||||
}
|
||||
`);
|
||||
|
||||
const {program, transformers} = createProgramWithTransform(['/foo.ts', '/bar.ts']);
|
||||
program.emit(undefined, undefined, undefined, undefined, transformers);
|
||||
|
||||
expect(context.readFile('/foo.js')).toContain(`import { Foo } from './foo_service';`);
|
||||
expect(context.readFile('/bar.js')).toContain(`import { Bar } from './bar_service';`);
|
||||
});
|
||||
|
||||
it('should not result in a stack overflow for a large number of files', () => {
|
||||
// The decorators transform used to patch `ts.EmitResolver.isReferencedAliasDeclaration`
|
||||
// repeatedly for each source file in the program, causing a stack overflow once a large
|
||||
// number of source files was reached. This test verifies that emit succeeds even when there's
|
||||
// lots of source files. See https://github.com/angular/angular/issues/40276.
|
||||
context.writeFile('foo.d.ts', `
|
||||
export declare class Foo {};
|
||||
`);
|
||||
|
||||
// A somewhat minimal number of source files that used to trigger a stack overflow.
|
||||
const numberOfTestFiles = 6500;
|
||||
const files: string[] = [];
|
||||
for (let i = 0; i < numberOfTestFiles; i++) {
|
||||
const file = `/${i}.ts`;
|
||||
files.push(file);
|
||||
context.writeFile(file, `
|
||||
import {Injectable} from '@angular/core';
|
||||
import {Foo} from './foo';
|
||||
|
||||
@Injectable()
|
||||
export class MyService {
|
||||
constructor(foo: Foo) {}
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
const {program, transformers} = createProgramWithTransform(files);
|
||||
|
||||
let written = 0;
|
||||
program.emit(undefined, (fileName, outputText) => {
|
||||
written++;
|
||||
|
||||
// The below assertion throws an explicit error instead of using a Jasmine expectation,
|
||||
// as we want to abort on the first failure, if any. This avoids as many as `numberOfFiles`
|
||||
// expectation failures, which would bloat the test output.
|
||||
if (!outputText.includes(`import { Foo } from './foo';`)) {
|
||||
throw new Error(`Transform failed to preserve the import in ${fileName}:\n${outputText}`);
|
||||
}
|
||||
}, undefined, undefined, transformers);
|
||||
expect(written).toBe(numberOfTestFiles);
|
||||
});
|
||||
|
||||
function createProgramWithTransform(files: string[]) {
|
||||
const program = ts.createProgram(
|
||||
files, {
|
||||
moduleResolution: ts.ModuleResolutionKind.NodeJs,
|
||||
importHelpers: true,
|
||||
lib: [],
|
||||
module: ts.ModuleKind.ESNext,
|
||||
target: ts.ScriptTarget.Latest,
|
||||
declaration: false,
|
||||
experimentalDecorators: true,
|
||||
emitDecoratorMetadata: false,
|
||||
},
|
||||
host);
|
||||
const typeChecker = program.getTypeChecker();
|
||||
const reflectionHost = new TypeScriptReflectionHost(typeChecker);
|
||||
const transformers: ts.CustomTransformers = {
|
||||
before: [getDownlevelDecoratorsTransform(
|
||||
program.getTypeChecker(), reflectionHost, diagnostics,
|
||||
/* isCore */ false, isClosureEnabled, skipClassDecorators)]
|
||||
};
|
||||
return {program, transformers};
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/** Template string function that can be used to dedent a given string literal. */
|
||||
export function dedent(strings: TemplateStringsArray, ...values: any[]) {
|
||||
let joinedString = '';
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
joinedString += `${strings[i]}${values[i]}`;
|
||||
}
|
||||
joinedString += strings[strings.length - 1];
|
||||
return omitLeadingWhitespace(joinedString);
|
||||
}
|
||||
|
||||
/** Omits the leading whitespace for each line of the given text. */
|
||||
function omitLeadingWhitespace(text: string): string {
|
||||
return text.replace(/^\s+/gm, '');
|
||||
}
|
||||
|
|
@ -1,194 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 ts from 'typescript';
|
||||
|
||||
import {isClassMetadata, MetadataCollector} from '../../src/metadata/index';
|
||||
import {getInlineResourcesTransformFactory, InlineResourcesMetadataTransformer} from '../../src/transformers/inline_resources';
|
||||
import {MetadataCache} from '../../src/transformers/metadata_cache';
|
||||
import {MockAotContext, MockCompilerHost} from '../mocks';
|
||||
|
||||
describe('inline resources transformer', () => {
|
||||
describe('decorator input', () => {
|
||||
describe('should not touch unrecognized decorators', () => {
|
||||
it('Not from @angular/core', () => {
|
||||
expect(convert(`declare const Component: Function;
|
||||
@Component({templateUrl: './thing.html'}) class Foo {}`))
|
||||
.toContain('templateUrl');
|
||||
});
|
||||
it('missing @ sign', () => {
|
||||
expect(convert(`import {Component} from '@angular/core';
|
||||
Component({templateUrl: './thing.html'}) class Foo {}`))
|
||||
.toContain('templateUrl');
|
||||
});
|
||||
it('too many arguments to @Component', () => {
|
||||
expect(convert(`import {Component} from '@angular/core';
|
||||
@Component(1, {templateUrl: './thing.html'}) class Foo {}`))
|
||||
.toContain('templateUrl');
|
||||
});
|
||||
it('wrong argument type to @Component', () => {
|
||||
expect(convert(`import {Component} from '@angular/core';
|
||||
@Component([{templateUrl: './thing.html'}]) class Foo {}`))
|
||||
.toContain('templateUrl');
|
||||
});
|
||||
});
|
||||
|
||||
it('should replace templateUrl', () => {
|
||||
const actual = convert(`import {Component} from '@angular/core';
|
||||
@Component({
|
||||
templateUrl: './thing.html',
|
||||
otherProp: 3,
|
||||
}) export class Foo {}`);
|
||||
expect(actual).not.toContain('templateUrl:');
|
||||
expect(actual.replace(/\s+/g, ' '))
|
||||
.toContain(
|
||||
'Foo = __decorate([ (0, core_1.Component)({ template: "Some template", otherProp: 3 }) ], Foo)');
|
||||
});
|
||||
it('should allow different quotes', () => {
|
||||
const actual = convert(`import {Component} from '@angular/core';
|
||||
@Component({"templateUrl": \`./thing.html\`}) export class Foo {}`);
|
||||
expect(actual).not.toContain('templateUrl:');
|
||||
expect(actual).toContain('{ template: "Some template" }');
|
||||
});
|
||||
it('should replace styleUrls', () => {
|
||||
const actual = convert(`import {Component} from '@angular/core';
|
||||
@Component({
|
||||
styleUrls: ['./thing1.css', './thing2.css'],
|
||||
})
|
||||
export class Foo {}`);
|
||||
expect(actual).not.toContain('styleUrls:');
|
||||
expect(actual).toContain('styles: [".some_style {}", ".some_other_style {}"]');
|
||||
});
|
||||
it('should preserve existing styles', () => {
|
||||
const actual = convert(`import {Component} from '@angular/core';
|
||||
@Component({
|
||||
styles: ['h1 { color: blue }'],
|
||||
styleUrls: ['./thing1.css'],
|
||||
})
|
||||
export class Foo {}`);
|
||||
expect(actual).not.toContain('styleUrls:');
|
||||
expect(actual).toContain(`styles: ['h1 { color: blue }', ".some_style {}"]`);
|
||||
});
|
||||
it('should handle empty styleUrls', () => {
|
||||
const actual = convert(`import {Component} from '@angular/core';
|
||||
@Component({styleUrls: [], styles: []}) export class Foo {}`);
|
||||
expect(actual).not.toContain('styleUrls:');
|
||||
expect(actual).not.toContain('styles:');
|
||||
});
|
||||
});
|
||||
describe('annotation input', () => {
|
||||
it('should replace templateUrl', () => {
|
||||
const actual = convert(`import {Component} from '@angular/core';
|
||||
declare const NotComponent: Function;
|
||||
|
||||
export class Foo {
|
||||
static decorators: {type: Function, args?: any[]}[] = [
|
||||
{
|
||||
type: NotComponent,
|
||||
args: [],
|
||||
},{
|
||||
type: Component,
|
||||
args: [{
|
||||
templateUrl: './thing.html'
|
||||
}],
|
||||
}];
|
||||
}
|
||||
`);
|
||||
expect(actual).not.toContain('templateUrl:');
|
||||
expect(actual.replace(/\s+/g, ' '))
|
||||
.toMatch(
|
||||
/Foo\.decorators = [{ .*type: core_1\.Component, args: [{ template: "Some template" }]/);
|
||||
});
|
||||
it('should replace styleUrls', () => {
|
||||
const actual = convert(`import {Component} from '@angular/core';
|
||||
declare const NotComponent: Function;
|
||||
|
||||
export class Foo {
|
||||
static decorators: {type: Function, args?: any[]}[] = [{
|
||||
type: Component,
|
||||
args: [{
|
||||
styleUrls: ['./thing1.css', './thing2.css'],
|
||||
}],
|
||||
}];
|
||||
}
|
||||
`);
|
||||
expect(actual).not.toContain('styleUrls:');
|
||||
expect(actual.replace(/\s+/g, ' '))
|
||||
.toMatch(
|
||||
/Foo\.decorators = [{ .*type: core_1\.Component, args: [{ style: "Some template" }]/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('metadata transformer', () => {
|
||||
it('should transform decorators', () => {
|
||||
const source = `import {Component} from '@angular/core';
|
||||
@Component({
|
||||
templateUrl: './thing.html',
|
||||
styleUrls: ['./thing1.css', './thing2.css'],
|
||||
styles: ['h1 { color: red }'],
|
||||
})
|
||||
export class Foo {}
|
||||
`;
|
||||
const sourceFile = ts.createSourceFile(
|
||||
'someFile.ts', source, ts.ScriptTarget.Latest, /* setParentNodes */ true);
|
||||
const cache = new MetadataCache(
|
||||
new MetadataCollector(), /* strict */ true,
|
||||
[new InlineResourcesMetadataTransformer(
|
||||
{loadResource, resourceNameToFileName: (u: string) => u})]);
|
||||
const metadata = cache.getMetadata(sourceFile);
|
||||
expect(metadata).toBeDefined('Expected metadata from test source file');
|
||||
if (metadata) {
|
||||
const classData = metadata.metadata['Foo'];
|
||||
expect(classData && isClassMetadata(classData))
|
||||
.toBeDefined(`Expected metadata to contain data for Foo`);
|
||||
if (classData && isClassMetadata(classData)) {
|
||||
expect(JSON.stringify(classData)).not.toContain('templateUrl');
|
||||
expect(JSON.stringify(classData)).toContain('"template":"Some template"');
|
||||
expect(JSON.stringify(classData)).not.toContain('styleUrls');
|
||||
expect(JSON.stringify(classData))
|
||||
.toContain('"styles":["h1 { color: red }",".some_style {}",".some_other_style {}"]');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function loadResource(path: string): Promise<string>|string {
|
||||
if (path === './thing.html') return 'Some template';
|
||||
if (path === './thing1.css') return '.some_style {}';
|
||||
if (path === './thing2.css') return '.some_other_style {}';
|
||||
throw new Error('No fake data for path ' + path);
|
||||
}
|
||||
|
||||
function convert(source: string) {
|
||||
const baseFileName = 'someFile';
|
||||
const moduleName = '/' + baseFileName;
|
||||
const fileName = moduleName + '.ts';
|
||||
const context = new MockAotContext('/', {[baseFileName + '.ts']: source});
|
||||
const host = new MockCompilerHost(context);
|
||||
|
||||
const program = ts.createProgram(
|
||||
[fileName], {
|
||||
module: ts.ModuleKind.CommonJS,
|
||||
target: ts.ScriptTarget.ES2017,
|
||||
},
|
||||
host);
|
||||
const moduleSourceFile = program.getSourceFile(fileName);
|
||||
const transformers: ts.CustomTransformers = {
|
||||
before: [getInlineResourcesTransformFactory(
|
||||
program, {loadResource, resourceNameToFileName: (u: string) => u})]
|
||||
};
|
||||
let result = '';
|
||||
program.emit(
|
||||
moduleSourceFile, (emittedFileName, data, writeByteOrderMark, onError, sourceFiles) => {
|
||||
if (fileName.startsWith(moduleName)) {
|
||||
result = data;
|
||||
}
|
||||
}, undefined, undefined, transformers);
|
||||
return result;
|
||||
}
|
||||
|
|
@ -1,262 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 ts from 'typescript';
|
||||
|
||||
import {MetadataCollector, ModuleMetadata} from '../../src/metadata/index';
|
||||
import {getExpressionLoweringTransformFactory, LoweringRequest, LowerMetadataTransform, RequestLocationMap} from '../../src/transformers/lower_expressions';
|
||||
import {MetadataCache} from '../../src/transformers/metadata_cache';
|
||||
import {Directory, MockAotContext, MockCompilerHost} from '../mocks';
|
||||
|
||||
const DEFAULT_FIELDS_TO_LOWER = ['useFactory', 'useValue', 'data'];
|
||||
|
||||
describe('Expression lowering', () => {
|
||||
describe('transform', () => {
|
||||
it('should be able to lower a simple expression', () => {
|
||||
expect(convert('const a = 1 +◊b: 2◊;')).toBe('const b = 2; const a = 1 + b; export { b };');
|
||||
});
|
||||
|
||||
it('should be able to lower an expression in a decorator', () => {
|
||||
expect(convert(`
|
||||
import {Component} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
provider: [{provide: 'someToken', useFactory:◊l: () => null◊}]
|
||||
})
|
||||
class MyClass {}
|
||||
`)).toContain('const l = () => null; exports.l = l;');
|
||||
});
|
||||
|
||||
it('should be able to export a variable if the whole value is lowered', () => {
|
||||
expect(convert('/*a*/ const a =◊b: () => null◊;'))
|
||||
.toBe('/*a*/ const a = () => null; const b = a; export { b };');
|
||||
});
|
||||
});
|
||||
|
||||
describe('collector', () => {
|
||||
it('should request a lowering for useValue', () => {
|
||||
const collected = collect(`
|
||||
import {Component} from '@angular/core';
|
||||
|
||||
enum SomeEnum {
|
||||
OK,
|
||||
NotOK
|
||||
}
|
||||
|
||||
@Component({
|
||||
provider: [{provide: 'someToken', useValue:◊enum: SomeEnum.OK◊}]
|
||||
})
|
||||
export class MyClass {}
|
||||
`);
|
||||
expect(collected.requests.has(collected.annotations[0].start))
|
||||
.toBeTruthy('did not find the useValue');
|
||||
});
|
||||
|
||||
it('should not request a lowering for useValue with a reference to a static property', () => {
|
||||
const collected = collect(`
|
||||
import {Component} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
provider: [{provide: 'someToken', useValue:◊value: MyClass.someMethod◊}]
|
||||
})
|
||||
export class MyClass {
|
||||
static someMethod() {}
|
||||
}
|
||||
`);
|
||||
expect(collected.requests.size).toBe(0);
|
||||
});
|
||||
|
||||
it('should request a lowering for useFactory', () => {
|
||||
const collected = collect(`
|
||||
import {Component} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
provider: [{provide: 'someToken', useFactory:◊lambda: () => null◊}]
|
||||
})
|
||||
export class MyClass {}
|
||||
`);
|
||||
expect(collected.requests.has(collected.annotations[0].start))
|
||||
.toBeTruthy('did not find the useFactory');
|
||||
});
|
||||
|
||||
it('should request a lowering for data', () => {
|
||||
const collected = collect(`
|
||||
import {Component} from '@angular/core';
|
||||
|
||||
enum SomeEnum {
|
||||
OK,
|
||||
NotOK
|
||||
}
|
||||
|
||||
@Component({
|
||||
provider: [{provide: 'someToken', data:◊enum: SomeEnum.OK◊}]
|
||||
})
|
||||
export class MyClass {}
|
||||
`);
|
||||
expect(collected.requests.has(collected.annotations[0].start))
|
||||
.toBeTruthy('did not find the data field');
|
||||
});
|
||||
|
||||
it('should not lower a non-module', () => {
|
||||
const collected = collect(`
|
||||
declare const global: any;
|
||||
const ngDevMode: boolean = (function(global: any) {
|
||||
return global.ngDevMode = true;
|
||||
})(typeof window != 'undefined' && window || typeof self != 'undefined' && self || typeof global != 'undefined' && global);
|
||||
`);
|
||||
expect(collected.requests.size).toBe(0, 'unexpected rewriting');
|
||||
});
|
||||
|
||||
it('should throw a validation exception for invalid files', () => {
|
||||
const cache = new MetadataCache(
|
||||
new MetadataCollector({}), /* strict */ true,
|
||||
[new LowerMetadataTransform(DEFAULT_FIELDS_TO_LOWER)]);
|
||||
const sourceFile = ts.createSourceFile(
|
||||
'foo.ts', `
|
||||
import {Injectable} from '@angular/core';
|
||||
|
||||
class SomeLocalClass {}
|
||||
@Injectable()
|
||||
export class SomeClass {
|
||||
constructor(a: SomeLocalClass) {}
|
||||
}
|
||||
`,
|
||||
ts.ScriptTarget.Latest, true);
|
||||
expect(() => cache.getMetadata(sourceFile)).toThrow();
|
||||
});
|
||||
|
||||
it('should not report validation errors on a .d.ts file', () => {
|
||||
const cache = new MetadataCache(
|
||||
new MetadataCollector({}), /* strict */ true,
|
||||
[new LowerMetadataTransform(DEFAULT_FIELDS_TO_LOWER)]);
|
||||
const dtsFile = ts.createSourceFile(
|
||||
'foo.d.ts', `
|
||||
import {Injectable} from '@angular/core';
|
||||
|
||||
class SomeLocalClass {}
|
||||
@Injectable()
|
||||
export class SomeClass {
|
||||
constructor(a: SomeLocalClass) {}
|
||||
}
|
||||
`,
|
||||
ts.ScriptTarget.Latest, true);
|
||||
expect(() => cache.getMetadata(dtsFile)).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Helpers
|
||||
|
||||
interface Annotation {
|
||||
start: number;
|
||||
length: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
function getAnnotations(annotatedSource: string):
|
||||
{unannotatedSource: string, annotations: Annotation[]} {
|
||||
const annotations: {start: number, length: number, name: string}[] = [];
|
||||
let adjustment = 0;
|
||||
const unannotatedSource = annotatedSource.replace(
|
||||
/◊([a-zA-Z]+):(.*)◊/g,
|
||||
(text: string, name: string, source: string, index: number): string => {
|
||||
annotations.push({start: index + adjustment, length: source.length, name});
|
||||
adjustment -= text.length - source.length;
|
||||
return source;
|
||||
});
|
||||
return {unannotatedSource, annotations};
|
||||
}
|
||||
|
||||
// Transform helpers
|
||||
|
||||
function convert(annotatedSource: string) {
|
||||
const {annotations, unannotatedSource} = getAnnotations(annotatedSource);
|
||||
|
||||
const baseFileName = 'someFile';
|
||||
const moduleName = '/' + baseFileName;
|
||||
const fileName = moduleName + '.ts';
|
||||
const context = new MockAotContext('/', {[baseFileName + '.ts']: unannotatedSource});
|
||||
const host = new MockCompilerHost(context);
|
||||
|
||||
const sourceFile = ts.createSourceFile(
|
||||
fileName, unannotatedSource, ts.ScriptTarget.Latest, /* setParentNodes */ true);
|
||||
const requests = new Map<number, LoweringRequest>();
|
||||
|
||||
for (const annotation of annotations) {
|
||||
const node = findNode(sourceFile, annotation.start, annotation.length);
|
||||
if (!node) throw new Error('Invalid test specification. Could not find the node to substitute');
|
||||
const location = node.pos;
|
||||
requests.set(location, {name: annotation.name, kind: node.kind, location, end: node.end});
|
||||
}
|
||||
|
||||
const program = ts.createProgram(
|
||||
[fileName], {module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2017}, host);
|
||||
const moduleSourceFile = program.getSourceFile(fileName)!;
|
||||
const transformers: ts.CustomTransformers = {
|
||||
before: [getExpressionLoweringTransformFactory(
|
||||
{
|
||||
getRequests(sourceFile: ts.SourceFile): RequestLocationMap {
|
||||
if (sourceFile.fileName == moduleSourceFile.fileName) {
|
||||
return requests;
|
||||
} else {
|
||||
return new Map();
|
||||
}
|
||||
}
|
||||
},
|
||||
program)]
|
||||
};
|
||||
let result: string = '';
|
||||
const emitResult = program.emit(
|
||||
moduleSourceFile, (emittedFileName, data, writeByteOrderMark, onError, sourceFiles) => {
|
||||
if (fileName.startsWith(moduleName)) {
|
||||
result = data;
|
||||
}
|
||||
}, undefined, undefined, transformers);
|
||||
return normalizeResult(result);
|
||||
}
|
||||
|
||||
function findNode(node: ts.Node, start: number, length: number): ts.Node|undefined {
|
||||
function find(node: ts.Node): ts.Node|undefined {
|
||||
if (node.getFullStart() == start && node.getEnd() == start + length) {
|
||||
return node;
|
||||
}
|
||||
if (node.getFullStart() <= start && node.getEnd() >= start + length) {
|
||||
return ts.forEachChild(node, find);
|
||||
}
|
||||
}
|
||||
return ts.forEachChild(node, find);
|
||||
}
|
||||
|
||||
function normalizeResult(result: string): string {
|
||||
// Remove TypeScript prefixes
|
||||
// Remove new lines
|
||||
// Squish adjacent spaces
|
||||
// Remove prefix and postfix spaces
|
||||
return result.replace('"use strict";', ' ')
|
||||
.replace('exports.__esModule = true;', ' ')
|
||||
.replace('Object.defineProperty(exports, "__esModule", { value: true });', ' ')
|
||||
.replace(/\n/g, ' ')
|
||||
.replace(/ +/g, ' ')
|
||||
.replace(/^ /g, '')
|
||||
.replace(/ $/g, '');
|
||||
}
|
||||
|
||||
// Collector helpers
|
||||
|
||||
function collect(annotatedSource: string) {
|
||||
const {annotations, unannotatedSource} = getAnnotations(annotatedSource);
|
||||
const transformer = new LowerMetadataTransform(DEFAULT_FIELDS_TO_LOWER);
|
||||
const cache = new MetadataCache(new MetadataCollector({}), false, [transformer]);
|
||||
const sourceFile = ts.createSourceFile(
|
||||
'someName.ts', unannotatedSource, ts.ScriptTarget.Latest, /* setParentNodes */ true);
|
||||
return {
|
||||
metadata: cache.getMetadata(sourceFile),
|
||||
requests: transformer.getRequests(sourceFile),
|
||||
annotations
|
||||
};
|
||||
}
|
||||
|
|
@ -1,178 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 ts from 'typescript';
|
||||
|
||||
import {METADATA_VERSION, MetadataCollector, ModuleMetadata} from '../../src/metadata';
|
||||
import {MetadataReaderHost, readMetadata} from '../../src/transformers/metadata_reader';
|
||||
import {Directory, Entry, MockAotContext} from '../mocks';
|
||||
|
||||
describe('metadata reader', () => {
|
||||
let host: MetadataReaderHost;
|
||||
|
||||
beforeEach(() => {
|
||||
const context = new MockAotContext('/tmp/src', clone(FILES));
|
||||
const metadataCollector = new MetadataCollector();
|
||||
host = {
|
||||
fileExists: (fileName) => context.fileExists(fileName),
|
||||
readFile: (fileName) => context.readFile(fileName),
|
||||
getSourceFileMetadata: (fileName) => {
|
||||
const sourceText = context.readFile(fileName);
|
||||
return sourceText != null ? metadataCollector.getMetadata(ts.createSourceFile(
|
||||
fileName, sourceText, ts.ScriptTarget.Latest)) :
|
||||
undefined;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
it('should be able to read a metadata file', () => {
|
||||
expect(readMetadata('node_modules/@angular/core.d.ts', host)).toEqual([
|
||||
{__symbolic: 'module', version: METADATA_VERSION, metadata: {foo: {__symbolic: 'class'}}}
|
||||
]);
|
||||
});
|
||||
|
||||
it('should be able to read metadata from an otherwise unused .d.ts file ', () => {
|
||||
expect(readMetadata('node_modules/@angular/unused.d.ts', host)).toEqual([dummyMetadata]);
|
||||
});
|
||||
|
||||
it('should be able to read empty metadata ', () => {
|
||||
expect(readMetadata('node_modules/@angular/empty.d.ts', host)).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return undefined for missing modules', () => {
|
||||
expect(readMetadata('node_modules/@angular/missing.d.ts', host)).toBeUndefined();
|
||||
});
|
||||
|
||||
it(`should add missing v${METADATA_VERSION} metadata from v1 metadata and .d.ts files`, () => {
|
||||
expect(readMetadata('metadata_versions/v1.d.ts', host)).toEqual([
|
||||
{__symbolic: 'module', version: 1, metadata: {foo: {__symbolic: 'class'}}}, {
|
||||
__symbolic: 'module',
|
||||
version: METADATA_VERSION,
|
||||
metadata: {
|
||||
foo: {__symbolic: 'class'},
|
||||
aType: {__symbolic: 'interface'},
|
||||
Bar: {__symbolic: 'class', members: {ngOnInit: [{__symbolic: 'method'}]}},
|
||||
BarChild: {__symbolic: 'class', extends: {__symbolic: 'reference', name: 'Bar'}},
|
||||
ReExport: {__symbolic: 'reference', module: './lib/utils2', name: 'ReExport'},
|
||||
},
|
||||
exports: [{from: './lib/utils2', export: ['Export']}],
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it(`should upgrade a missing metadata file into v${METADATA_VERSION}`, () => {
|
||||
expect(readMetadata('metadata_versions/v1_empty.d.ts', host)).toEqual([{
|
||||
__symbolic: 'module',
|
||||
version: METADATA_VERSION,
|
||||
metadata: {},
|
||||
exports: [{from: './lib/utils'}]
|
||||
}]);
|
||||
});
|
||||
|
||||
it(`should upgrade v3 metadata into v${METADATA_VERSION}`, () => {
|
||||
expect(readMetadata('metadata_versions/v3.d.ts', host)).toEqual([
|
||||
{__symbolic: 'module', version: 3, metadata: {foo: {__symbolic: 'class'}}}, {
|
||||
__symbolic: 'module',
|
||||
version: METADATA_VERSION,
|
||||
metadata: {
|
||||
foo: {__symbolic: 'class'},
|
||||
aType: {__symbolic: 'interface'},
|
||||
Bar: {__symbolic: 'class', members: {ngOnInit: [{__symbolic: 'method'}]}},
|
||||
BarChild: {__symbolic: 'class', extends: {__symbolic: 'reference', name: 'Bar'}},
|
||||
ReExport: {__symbolic: 'reference', module: './lib/utils2', name: 'ReExport'},
|
||||
}
|
||||
// Note: exports is missing because it was elided in the original.
|
||||
}
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
const dummyModule = 'export let foo: any[];';
|
||||
const dummyMetadata: ModuleMetadata = {
|
||||
__symbolic: 'module',
|
||||
version: METADATA_VERSION,
|
||||
metadata:
|
||||
{foo: {__symbolic: 'error', message: 'Variable not initialized', line: 0, character: 11}}
|
||||
};
|
||||
const FILES: Entry = {
|
||||
'tmp': {
|
||||
'src': {
|
||||
'main.ts': `
|
||||
import * as c from '@angular/core';
|
||||
import * as r from '@angular/router';
|
||||
import * as u from './lib/utils';
|
||||
import * as cs from './lib/collections';
|
||||
import * as u2 from './lib2/utils2';
|
||||
`,
|
||||
'lib': {
|
||||
'utils.ts': dummyModule,
|
||||
'collections.ts': dummyModule,
|
||||
},
|
||||
'lib2': {'utils2.ts': dummyModule},
|
||||
'node_modules': {
|
||||
'@angular': {
|
||||
'core.d.ts': dummyModule,
|
||||
'core.metadata.json': `{"__symbolic":"module", "version": ${
|
||||
METADATA_VERSION}, "metadata": {"foo": {"__symbolic": "class"}}}`,
|
||||
'router': {'index.d.ts': dummyModule, 'src': {'providers.d.ts': dummyModule}},
|
||||
'unused.d.ts': dummyModule,
|
||||
'empty.d.ts': 'export declare var a: string;',
|
||||
'empty.metadata.json': '[]',
|
||||
}
|
||||
},
|
||||
'metadata_versions': {
|
||||
'v1.d.ts': `
|
||||
import {ReExport} from './lib/utils2';
|
||||
export {ReExport};
|
||||
|
||||
export {Export} from './lib/utils2';
|
||||
|
||||
export type aType = number;
|
||||
|
||||
export declare class Bar {
|
||||
ngOnInit() {}
|
||||
}
|
||||
export declare class BarChild extends Bar {}
|
||||
`,
|
||||
'v1.metadata.json':
|
||||
`{"__symbolic":"module", "version": 1, "metadata": {"foo": {"__symbolic": "class"}}}`,
|
||||
'v1_empty.d.ts': `
|
||||
export * from './lib/utils';
|
||||
`,
|
||||
'v3.d.ts': `
|
||||
import {ReExport} from './lib/utils2';
|
||||
export {ReExport};
|
||||
|
||||
export {Export} from './lib/utils2';
|
||||
|
||||
export type aType = number;
|
||||
|
||||
export declare class Bar {
|
||||
ngOnInit() {}
|
||||
}
|
||||
export declare class BarChild extends Bar {}
|
||||
`,
|
||||
'v3.metadata.json':
|
||||
`{"__symbolic":"module", "version": 3, "metadata": {"foo": {"__symbolic": "class"}}}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function clone(entry: Entry): Entry {
|
||||
if (typeof entry === 'string') {
|
||||
return entry;
|
||||
} else {
|
||||
const result: Directory = {};
|
||||
for (const name in entry) {
|
||||
result[name] = clone(entry[name]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,612 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 {ParseLocation, ParseSourceFile, ParseSourceSpan} from '@angular/compiler';
|
||||
import * as o from '@angular/compiler/src/output/output_ast';
|
||||
import ts from 'typescript';
|
||||
|
||||
import {TypeScriptNodeEmitter} from '../../src/transformers/node_emitter';
|
||||
import {Directory, MockAotContext, MockCompilerHost} from '../mocks';
|
||||
|
||||
const sourceMap = require('source-map');
|
||||
|
||||
const someGenFilePath = '/somePackage/someGenFile';
|
||||
const someGenFileName = someGenFilePath + '.ts';
|
||||
const anotherModuleUrl = '/somePackage/someOtherPath';
|
||||
|
||||
const sameModuleIdentifier = new o.ExternalReference(null, 'someLocalId', null);
|
||||
|
||||
const externalModuleIdentifier = new o.ExternalReference(anotherModuleUrl, 'someExternalId', null);
|
||||
|
||||
describe('TypeScriptNodeEmitter', () => {
|
||||
let context: MockAotContext;
|
||||
let host: MockCompilerHost;
|
||||
let emitter: TypeScriptNodeEmitter;
|
||||
let someVar: o.ReadVarExpr;
|
||||
|
||||
beforeEach(() => {
|
||||
context = new MockAotContext('/', FILES);
|
||||
host = new MockCompilerHost(context);
|
||||
emitter = new TypeScriptNodeEmitter(false);
|
||||
someVar = o.variable('someVar', null, null);
|
||||
});
|
||||
|
||||
function emitStmt(
|
||||
stmt: o.Statement|o.Statement[], format: Format = Format.Flat, preamble?: string): string {
|
||||
const stmts = Array.isArray(stmt) ? stmt : [stmt];
|
||||
|
||||
const program = ts.createProgram(
|
||||
[someGenFileName], {module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2017}, host);
|
||||
const moduleSourceFile = program.getSourceFile(someGenFileName);
|
||||
const transformers: ts.CustomTransformers = {
|
||||
before: [() => {
|
||||
return sourceFile => {
|
||||
const [newSourceFile] = emitter.updateSourceFile(sourceFile, stmts, preamble);
|
||||
return newSourceFile;
|
||||
};
|
||||
}]
|
||||
};
|
||||
let result: string = '';
|
||||
program.emit(moduleSourceFile, (fileName, data) => {
|
||||
if (fileName.startsWith(someGenFilePath)) {
|
||||
result = data;
|
||||
}
|
||||
}, undefined, undefined, transformers);
|
||||
return normalizeResult(result, format);
|
||||
}
|
||||
|
||||
it('should declare variables', () => {
|
||||
expect(emitStmt(someVar.set(o.literal(1)).toDeclStmt())).toEqual(`var someVar = 1;`);
|
||||
expect(emitStmt(someVar.set(o.literal(1)).toDeclStmt(null, [o.StmtModifier.Final])))
|
||||
.toEqual(`var someVar = 1;`);
|
||||
expect(emitStmt(someVar.set(o.literal(1)).toDeclStmt(null, [o.StmtModifier.Exported])))
|
||||
.toEqual(`var someVar = 1; exports.someVar = someVar;`);
|
||||
});
|
||||
|
||||
describe('declare variables with ExternExpressions as values', () => {
|
||||
it('should create no reexport if the identifier is in the same module', () => {
|
||||
// identifier is in the same module -> no reexport
|
||||
expect(emitStmt(someVar.set(o.importExpr(sameModuleIdentifier)).toDeclStmt(null, [
|
||||
o.StmtModifier.Exported
|
||||
]))).toEqual('var someVar = someLocalId; exports.someVar = someVar;');
|
||||
});
|
||||
|
||||
it('should create no reexport if the variable is not exported', () => {
|
||||
expect(emitStmt(someVar.set(o.importExpr(externalModuleIdentifier)).toDeclStmt()))
|
||||
.toEqual(
|
||||
`const i0 = require("/somePackage/someOtherPath"); var someVar = i0.someExternalId;`);
|
||||
});
|
||||
|
||||
it('should create no reexport if the variable is typed', () => {
|
||||
expect(emitStmt(someVar.set(o.importExpr(externalModuleIdentifier))
|
||||
.toDeclStmt(o.DYNAMIC_TYPE, [o.StmtModifier.Exported])))
|
||||
.toEqual(
|
||||
`const i0 = require("/somePackage/someOtherPath"); var someVar = i0.someExternalId; exports.someVar = someVar;`);
|
||||
});
|
||||
|
||||
it('should create a reexport', () => {
|
||||
const result = emitStmt(someVar.set(o.importExpr(externalModuleIdentifier)).toDeclStmt(null, [
|
||||
o.StmtModifier.Exported
|
||||
]));
|
||||
expect(result).toContain(`var someOtherPath_1 = require("/somePackage/someOtherPath");`);
|
||||
if (!result.includes('exports.someVar = someOtherPath_1.someExternalId;') &&
|
||||
// In TS 3.9 re-exports of namespaced imports are defined as getters
|
||||
!result.includes(
|
||||
'Object.defineProperty(exports, "someVar", { enumerable: true, get: function () { return someOtherPath_1.someExternalId; } });')) {
|
||||
fail(
|
||||
'Expected `someVar` to be exported directly or via a `definedProperty` call. Instead got:\n' +
|
||||
result);
|
||||
}
|
||||
});
|
||||
|
||||
it('should create multiple reexports from the same file', () => {
|
||||
const someVar2 = o.variable('someVar2');
|
||||
const externalModuleIdentifier2 =
|
||||
new o.ExternalReference(anotherModuleUrl, 'someExternalId2', null);
|
||||
const result = emitStmt([
|
||||
someVar.set(o.importExpr(externalModuleIdentifier))
|
||||
.toDeclStmt(null, [o.StmtModifier.Exported]),
|
||||
someVar2.set(o.importExpr(externalModuleIdentifier2))
|
||||
.toDeclStmt(null, [o.StmtModifier.Exported])
|
||||
]);
|
||||
expect(result).toContain(`var someOtherPath_1 = require("/somePackage/someOtherPath");`);
|
||||
if (!result.includes(
|
||||
'exports.someVar = someOtherPath_1.someExternalId;' +
|
||||
'exports.someVar2 = someOtherPath_1.someExternalId2;') &&
|
||||
// In TS 3.9 re-exports of namespaced imports are defined as getters
|
||||
!result.includes(
|
||||
'Object.defineProperty(exports, "someVar", { enumerable: true, get: function () { return someOtherPath_1.someExternalId; } }); ' +
|
||||
'Object.defineProperty(exports, "someVar2", { enumerable: true, get: function () { return someOtherPath_1.someExternalId2; } })')) {
|
||||
fail(
|
||||
'Expected `someVar` and `someVar2` to be exported directly or via a `definedProperty` call. Instead got:\n' +
|
||||
result);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should read and write variables', () => {
|
||||
expect(emitStmt(someVar.toStmt())).toEqual(`someVar;`);
|
||||
expect(emitStmt(someVar.set(o.literal(1)).toStmt())).toEqual(`someVar = 1;`);
|
||||
expect(emitStmt(someVar.set(o.variable('someOtherVar').set(o.literal(1))).toStmt()))
|
||||
.toEqual(`someVar = someOtherVar = 1;`);
|
||||
});
|
||||
|
||||
it('should read and write keys', () => {
|
||||
expect(emitStmt(o.variable('someMap').key(o.variable('someKey')).toStmt()))
|
||||
.toEqual(`someMap[someKey];`);
|
||||
expect(emitStmt(o.variable('someMap').key(o.variable('someKey')).set(o.literal(1)).toStmt()))
|
||||
.toEqual(`someMap[someKey] = 1;`);
|
||||
});
|
||||
|
||||
it('should read and write properties', () => {
|
||||
expect(emitStmt(o.variable('someObj').prop('someProp').toStmt())).toEqual(`someObj.someProp;`);
|
||||
expect(emitStmt(o.variable('someObj').prop('someProp').set(o.literal(1)).toStmt()))
|
||||
.toEqual(`someObj.someProp = 1;`);
|
||||
});
|
||||
|
||||
it('should invoke functions and methods and constructors', () => {
|
||||
expect(emitStmt(o.variable('someFn').callFn([o.literal(1)]).toStmt())).toEqual('someFn(1);');
|
||||
expect(emitStmt(o.variable('someObj').prop('someMethod').callFn([o.literal(1)]).toStmt()))
|
||||
.toEqual('someObj.someMethod(1);');
|
||||
expect(emitStmt(o.variable('SomeClass').instantiate([o.literal(1)]).toStmt()))
|
||||
.toEqual('new SomeClass(1);');
|
||||
});
|
||||
|
||||
it('should invoke functions and methods and constructors', () => {
|
||||
expect(emitStmt(o.variable('someFn').callFn([o.literal(1)]).toStmt())).toEqual('someFn(1);');
|
||||
expect(emitStmt(o.variable('someObj').prop('someMethod').callFn([o.literal(1)]).toStmt()))
|
||||
.toEqual('someObj.someMethod(1);');
|
||||
expect(emitStmt(o.variable('SomeClass').instantiate([o.literal(1)]).toStmt()))
|
||||
.toEqual('new SomeClass(1);');
|
||||
});
|
||||
|
||||
it('should support literals', () => {
|
||||
expect(emitStmt(o.literal(0).toStmt())).toEqual('0;');
|
||||
expect(emitStmt(o.literal(true).toStmt())).toEqual('true;');
|
||||
expect(emitStmt(o.literal('someStr').toStmt())).toEqual(`"someStr";`);
|
||||
expect(emitStmt(o.literalArr([o.literal(1)]).toStmt())).toEqual(`[1];`);
|
||||
expect(emitStmt(o.literalMap([
|
||||
{key: 'someKey', value: o.literal(1), quoted: false},
|
||||
{key: 'a', value: o.literal('a'), quoted: false},
|
||||
{key: 'b', value: o.literal('b'), quoted: true},
|
||||
{key: '*', value: o.literal('star'), quoted: false},
|
||||
]).toStmt())
|
||||
.replace(/\s+/gm, ''))
|
||||
.toEqual(`({someKey:1,a:"a","b":"b","*":"star"});`);
|
||||
|
||||
// Regressions #22774
|
||||
expect(emitStmt(o.literal('\\0025BC').toStmt())).toEqual('"\\\\0025BC";');
|
||||
expect(emitStmt(o.literal('"some value"').toStmt())).toEqual('"\\"some value\\"";');
|
||||
expect(emitStmt(o.literal('"some \\0025BC value"').toStmt()))
|
||||
.toEqual('"\\"some \\\\0025BC value\\"";');
|
||||
expect(emitStmt(o.literal('\n \\0025BC \n ').toStmt())).toEqual('"\\n \\\\0025BC \\n ";');
|
||||
expect(emitStmt(o.literal('\r \\0025BC \r ').toStmt())).toEqual('"\\r \\\\0025BC \\r ";');
|
||||
});
|
||||
|
||||
it('should support blank literals', () => {
|
||||
expect(emitStmt(o.literal(null).toStmt())).toEqual('null;');
|
||||
expect(emitStmt(o.literal(undefined).toStmt())).toEqual('undefined;');
|
||||
expect(emitStmt(o.variable('a', null).isBlank().toStmt())).toEqual('(a == null);');
|
||||
});
|
||||
|
||||
it('should support external identifiers', () => {
|
||||
expect(emitStmt(o.importExpr(sameModuleIdentifier).toStmt())).toEqual('someLocalId;');
|
||||
expect(emitStmt(o.importExpr(externalModuleIdentifier).toStmt()))
|
||||
.toEqual(`const i0 = require("/somePackage/someOtherPath"); i0.someExternalId;`);
|
||||
});
|
||||
|
||||
it('should support operators', () => {
|
||||
const lhs = o.variable('lhs');
|
||||
const rhs = o.variable('rhs');
|
||||
expect(emitStmt(someVar.cast(o.INT_TYPE).toStmt())).toEqual('someVar;');
|
||||
expect(emitStmt(o.not(someVar).toStmt())).toEqual('!someVar;');
|
||||
expect(emitStmt(o.assertNotNull(someVar).toStmt())).toEqual('someVar;');
|
||||
expect(emitStmt(someVar.conditional(o.variable('trueCase'), o.variable('falseCase')).toStmt()))
|
||||
.toEqual('(someVar ? trueCase : falseCase);');
|
||||
expect(emitStmt(someVar.conditional(o.variable('trueCase'), o.variable('falseCase'))
|
||||
.conditional(o.variable('trueCase'), o.variable('falseCase'))
|
||||
.toStmt()))
|
||||
.toEqual('((someVar ? trueCase : falseCase) ? trueCase : falseCase);');
|
||||
|
||||
expect(emitStmt(lhs.equals(rhs).toStmt())).toEqual('(lhs == rhs);');
|
||||
expect(emitStmt(lhs.notEquals(rhs).toStmt())).toEqual('(lhs != rhs);');
|
||||
expect(emitStmt(lhs.identical(rhs).toStmt())).toEqual('(lhs === rhs);');
|
||||
expect(emitStmt(lhs.notIdentical(rhs).toStmt())).toEqual('(lhs !== rhs);');
|
||||
expect(emitStmt(lhs.minus(rhs).toStmt())).toEqual('(lhs - rhs);');
|
||||
expect(emitStmt(lhs.plus(rhs).toStmt())).toEqual('(lhs + rhs);');
|
||||
expect(emitStmt(lhs.divide(rhs).toStmt())).toEqual('(lhs / rhs);');
|
||||
expect(emitStmt(lhs.multiply(rhs).toStmt())).toEqual('(lhs * rhs);');
|
||||
expect(emitStmt(lhs.plus(rhs).multiply(rhs).toStmt())).toEqual('((lhs + rhs) * rhs);');
|
||||
expect(emitStmt(lhs.modulo(rhs).toStmt())).toEqual('(lhs % rhs);');
|
||||
expect(emitStmt(lhs.and(rhs).toStmt())).toEqual('(lhs && rhs);');
|
||||
expect(emitStmt(lhs.or(rhs).toStmt())).toEqual('(lhs || rhs);');
|
||||
expect(emitStmt(lhs.lower(rhs).toStmt())).toEqual('(lhs < rhs);');
|
||||
expect(emitStmt(lhs.lowerEquals(rhs).toStmt())).toEqual('(lhs <= rhs);');
|
||||
expect(emitStmt(lhs.bigger(rhs).toStmt())).toEqual('(lhs > rhs);');
|
||||
expect(emitStmt(lhs.biggerEquals(rhs).toStmt())).toEqual('(lhs >= rhs);');
|
||||
});
|
||||
|
||||
it('should support function expressions', () => {
|
||||
expect(emitStmt(o.fn([], []).toStmt())).toEqual(`(function () { });`);
|
||||
expect(emitStmt(o.fn([], [new o.ReturnStatement(o.literal(1))], o.INT_TYPE).toStmt()))
|
||||
.toEqual(`(function () { return 1; });`);
|
||||
expect(emitStmt(o.fn([new o.FnParam('param1', o.INT_TYPE)], []).toStmt()))
|
||||
.toEqual(`(function (param1) { });`);
|
||||
});
|
||||
|
||||
it('should support function statements', () => {
|
||||
expect(emitStmt(new o.DeclareFunctionStmt('someFn', [], []))).toEqual('function someFn() { }');
|
||||
expect(emitStmt(new o.DeclareFunctionStmt('someFn', [], [], null, [o.StmtModifier.Exported])))
|
||||
.toEqual(`function someFn() { } exports.someFn = someFn;`);
|
||||
expect(emitStmt(new o.DeclareFunctionStmt(
|
||||
'someFn', [], [new o.ReturnStatement(o.literal(1))], o.INT_TYPE)))
|
||||
.toEqual(`function someFn() { return 1; }`);
|
||||
expect(emitStmt(new o.DeclareFunctionStmt('someFn', [new o.FnParam('param1', o.INT_TYPE)], [])))
|
||||
.toEqual(`function someFn(param1) { }`);
|
||||
});
|
||||
|
||||
describe('comments', () => {
|
||||
it('should support a preamble, which is wrapped as a multi-line comment with no trimming or padding',
|
||||
() => {
|
||||
expect(emitStmt(o.variable('a').toStmt(), Format.Raw, '*\n * SomePreamble\n '))
|
||||
.toBe('/**\n * SomePreamble\n */\na;');
|
||||
});
|
||||
|
||||
it('should support singleline comments', () => {
|
||||
expect(emitStmt(
|
||||
new o.ReturnStatement(o.literal(1), null, [o.leadingComment(' a\n b', false)]),
|
||||
Format.Raw))
|
||||
.toBe('// a\n// b\nreturn 1;');
|
||||
});
|
||||
|
||||
it('should support multiline comments', () => {
|
||||
expect(emitStmt(
|
||||
new o.ReturnStatement(
|
||||
o.literal(1), null, [o.leadingComment('Multiline comment', true)]),
|
||||
Format.Raw))
|
||||
.toBe('/* Multiline comment */\nreturn 1;');
|
||||
expect(emitStmt(
|
||||
new o.ReturnStatement(
|
||||
o.literal(1), null, [o.leadingComment(`Multiline\ncomment`, true)]),
|
||||
Format.Raw))
|
||||
.toBe(`/* Multiline\ncomment */\nreturn 1;`);
|
||||
});
|
||||
|
||||
describe('JSDoc comments', () => {
|
||||
it('should be supported', () => {
|
||||
expect(emitStmt(
|
||||
new o.ReturnStatement(
|
||||
o.literal(1), null, [o.jsDocComment([{text: 'Intro comment'}])]),
|
||||
Format.Raw))
|
||||
.toBe(`/**\n * Intro comment\n */\nreturn 1;`);
|
||||
expect(emitStmt(
|
||||
new o.ReturnStatement(
|
||||
o.literal(1), null,
|
||||
[o.jsDocComment([{tagName: o.JSDocTagName.Desc, text: 'description'}])]),
|
||||
Format.Raw))
|
||||
.toBe(`/**\n * @desc description\n */\nreturn 1;`);
|
||||
expect(emitStmt(
|
||||
new o.ReturnStatement(
|
||||
o.literal(1), null, [o.jsDocComment([
|
||||
{text: 'Intro comment'},
|
||||
{tagName: o.JSDocTagName.Desc, text: 'description'},
|
||||
{tagName: o.JSDocTagName.Id, text: '{number} identifier 123'},
|
||||
])]),
|
||||
Format.Raw))
|
||||
.toBe(
|
||||
`/**\n * Intro comment\n * @desc description\n * @id {number} identifier 123\n */\nreturn 1;`);
|
||||
});
|
||||
|
||||
it('should escape @ in the text', () => {
|
||||
expect(emitStmt(
|
||||
new o.ReturnStatement(
|
||||
o.literal(1), null, [o.jsDocComment([{text: 'email@google.com'}])]),
|
||||
Format.Raw))
|
||||
.toBe(`/**\n * email\\@google.com\n */\nreturn 1;`);
|
||||
});
|
||||
|
||||
it('should not allow /* and */ in the text', () => {
|
||||
expect(
|
||||
() => emitStmt(new o.ReturnStatement(
|
||||
o.literal(1), null, [o.jsDocComment([{text: 'some text /* */'}])])))
|
||||
.toThrowError(`JSDoc text cannot contain "/*" and "*/"`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should support if stmt', () => {
|
||||
const trueCase = o.variable('trueCase').callFn([]).toStmt();
|
||||
const falseCase = o.variable('falseCase').callFn([]).toStmt();
|
||||
expect(emitStmt(new o.IfStmt(o.variable('cond'), [trueCase])))
|
||||
.toEqual('if (cond) { trueCase(); }');
|
||||
expect(emitStmt(new o.IfStmt(o.variable('cond'), [trueCase], [falseCase])))
|
||||
.toEqual('if (cond) { trueCase(); } else { falseCase(); }');
|
||||
});
|
||||
|
||||
it('should support try/catch', () => {
|
||||
const bodyStmt = o.variable('body').callFn([]).toStmt();
|
||||
const catchStmt = o.variable('catchFn').callFn([o.CATCH_ERROR_VAR, o.CATCH_STACK_VAR]).toStmt();
|
||||
expect(emitStmt(new o.TryCatchStmt([bodyStmt], [catchStmt])))
|
||||
.toEqual(
|
||||
`try { body(); } catch (error) { var stack = error.stack; catchFn(error, stack); }`);
|
||||
});
|
||||
|
||||
it('should support support throwing', () => {
|
||||
expect(emitStmt(new o.ThrowStmt(someVar))).toEqual('throw someVar;');
|
||||
});
|
||||
|
||||
describe('classes', () => {
|
||||
let callSomeMethod: o.Statement;
|
||||
|
||||
beforeEach(() => {
|
||||
callSomeMethod = o.THIS_EXPR.prop('someMethod').callFn([]).toStmt();
|
||||
});
|
||||
|
||||
|
||||
it('should support declaring classes', () => {
|
||||
expect(emitStmt(new o.ClassStmt('SomeClass', null!, [], [], null!, [])))
|
||||
.toEqual('class SomeClass { }');
|
||||
expect(emitStmt(new o.ClassStmt('SomeClass', null!, [], [], null!, [], [
|
||||
o.StmtModifier.Exported
|
||||
]))).toEqual('class SomeClass { } exports.SomeClass = SomeClass;');
|
||||
expect(
|
||||
emitStmt(new o.ClassStmt('SomeClass', o.variable('SomeSuperClass'), [], [], null!, [])))
|
||||
.toEqual('class SomeClass extends SomeSuperClass { }');
|
||||
});
|
||||
|
||||
it('should support declaring constructors', () => {
|
||||
const superCall = o.SUPER_EXPR.callFn([o.variable('someParam')]).toStmt();
|
||||
expect(emitStmt(
|
||||
new o.ClassStmt('SomeClass', null!, [], [], new o.ClassMethod(null!, [], []), [])))
|
||||
.toEqual(`class SomeClass { constructor() { } }`);
|
||||
expect(emitStmt(new o.ClassStmt(
|
||||
'SomeClass', null!, [], [],
|
||||
new o.ClassMethod(null!, [new o.FnParam('someParam', o.INT_TYPE)], []), [])))
|
||||
.toEqual(`class SomeClass { constructor(someParam) { } }`);
|
||||
expect(emitStmt(new o.ClassStmt(
|
||||
'SomeClass', null!, [], [], new o.ClassMethod(null!, [], [superCall]), [])))
|
||||
.toEqual(`class SomeClass { constructor() { super(someParam); } }`);
|
||||
expect(emitStmt(new o.ClassStmt(
|
||||
'SomeClass', null!, [], [], new o.ClassMethod(null!, [], [callSomeMethod]), [])))
|
||||
.toEqual(`class SomeClass { constructor() { this.someMethod(); } }`);
|
||||
});
|
||||
|
||||
it('should support declaring fields', () => {
|
||||
expect(emitStmt(new o.ClassStmt(
|
||||
'SomeClass', null!, [new o.ClassField('someField')], [], null!, [])))
|
||||
.toEqual(`class SomeClass { constructor() { this.someField = null; } }`);
|
||||
expect(emitStmt(new o.ClassStmt(
|
||||
'SomeClass', null!, [new o.ClassField('someField', o.INT_TYPE)], [], null!, [])))
|
||||
.toEqual(`class SomeClass { constructor() { this.someField = null; } }`);
|
||||
expect(emitStmt(new o.ClassStmt(
|
||||
'SomeClass', null!,
|
||||
[new o.ClassField('someField', o.INT_TYPE, [o.StmtModifier.Private])], [], null!,
|
||||
[])))
|
||||
.toEqual(`class SomeClass { constructor() { this.someField = null; } }`);
|
||||
});
|
||||
|
||||
it('should support declaring getters', () => {
|
||||
expect(emitStmt(new o.ClassStmt(
|
||||
'SomeClass', null!, [], [new o.ClassGetter('someGetter', [])], null!, [])))
|
||||
.toEqual(`class SomeClass { get someGetter() { } }`);
|
||||
expect(emitStmt(new o.ClassStmt(
|
||||
'SomeClass', null!, [], [new o.ClassGetter('someGetter', [], o.INT_TYPE)], null!,
|
||||
[])))
|
||||
.toEqual(`class SomeClass { get someGetter() { } }`);
|
||||
expect(emitStmt(new o.ClassStmt(
|
||||
'SomeClass', null!, [], [new o.ClassGetter('someGetter', [callSomeMethod])], null!,
|
||||
[])))
|
||||
.toEqual(`class SomeClass { get someGetter() { this.someMethod(); } }`);
|
||||
expect(
|
||||
emitStmt(new o.ClassStmt(
|
||||
'SomeClass', null!, [],
|
||||
[new o.ClassGetter('someGetter', [], null!, [o.StmtModifier.Private])], null!, [])))
|
||||
.toEqual(`class SomeClass { get someGetter() { } }`);
|
||||
});
|
||||
|
||||
it('should support methods', () => {
|
||||
expect(emitStmt(new o.ClassStmt('SomeClass', null!, [], [], null!, [
|
||||
new o.ClassMethod('someMethod', [], [])
|
||||
]))).toEqual(`class SomeClass { someMethod() { } }`);
|
||||
expect(emitStmt(new o.ClassStmt('SomeClass', null!, [], [], null!, [
|
||||
new o.ClassMethod('someMethod', [], [], o.INT_TYPE)
|
||||
]))).toEqual(`class SomeClass { someMethod() { } }`);
|
||||
expect(emitStmt(new o.ClassStmt('SomeClass', null!, [], [], null!, [
|
||||
new o.ClassMethod('someMethod', [new o.FnParam('someParam', o.INT_TYPE)], [])
|
||||
]))).toEqual(`class SomeClass { someMethod(someParam) { } }`);
|
||||
expect(emitStmt(new o.ClassStmt('SomeClass', null!, [], [], null!, [
|
||||
new o.ClassMethod('someMethod', [], [callSomeMethod])
|
||||
]))).toEqual(`class SomeClass { someMethod() { this.someMethod(); } }`);
|
||||
});
|
||||
});
|
||||
|
||||
it('should support builtin types', () => {
|
||||
const writeVarExpr = o.variable('a').set(o.NULL_EXPR);
|
||||
expect(emitStmt(writeVarExpr.toDeclStmt(o.DYNAMIC_TYPE))).toEqual('var a = null;');
|
||||
expect(emitStmt(writeVarExpr.toDeclStmt(o.BOOL_TYPE))).toEqual('var a = null;');
|
||||
expect(emitStmt(writeVarExpr.toDeclStmt(o.INT_TYPE))).toEqual('var a = null;');
|
||||
expect(emitStmt(writeVarExpr.toDeclStmt(o.NUMBER_TYPE))).toEqual('var a = null;');
|
||||
expect(emitStmt(writeVarExpr.toDeclStmt(o.STRING_TYPE))).toEqual('var a = null;');
|
||||
expect(emitStmt(writeVarExpr.toDeclStmt(o.FUNCTION_TYPE))).toEqual('var a = null;');
|
||||
});
|
||||
|
||||
it('should support external types', () => {
|
||||
const writeVarExpr = o.variable('a').set(o.NULL_EXPR);
|
||||
expect(emitStmt(writeVarExpr.toDeclStmt(o.importType(sameModuleIdentifier))))
|
||||
.toEqual('var a = null;');
|
||||
expect(emitStmt(writeVarExpr.toDeclStmt(o.importType(externalModuleIdentifier))))
|
||||
.toEqual(`var a = null;`);
|
||||
});
|
||||
|
||||
it('should support expression types', () => {
|
||||
expect(emitStmt(o.variable('a').set(o.NULL_EXPR).toDeclStmt(o.expressionType(o.variable('b')))))
|
||||
.toEqual('var a = null;');
|
||||
});
|
||||
|
||||
it('should support expressions with type parameters', () => {
|
||||
expect(emitStmt(o.variable('a')
|
||||
.set(o.NULL_EXPR)
|
||||
.toDeclStmt(o.importType(externalModuleIdentifier, [o.STRING_TYPE]))))
|
||||
.toEqual(`var a = null;`);
|
||||
});
|
||||
|
||||
it('should support combined types', () => {
|
||||
const writeVarExpr = o.variable('a').set(o.NULL_EXPR);
|
||||
expect(emitStmt(writeVarExpr.toDeclStmt(new o.ArrayType(null!)))).toEqual('var a = null;');
|
||||
expect(emitStmt(writeVarExpr.toDeclStmt(new o.ArrayType(o.INT_TYPE)))).toEqual('var a = null;');
|
||||
|
||||
expect(emitStmt(writeVarExpr.toDeclStmt(new o.MapType(null)))).toEqual('var a = null;');
|
||||
expect(emitStmt(writeVarExpr.toDeclStmt(new o.MapType(o.INT_TYPE)))).toEqual('var a = null;');
|
||||
});
|
||||
|
||||
describe('source maps', () => {
|
||||
function emitStmt(stmt: o.Statement|o.Statement[], preamble?: string): string {
|
||||
const stmts = Array.isArray(stmt) ? stmt : [stmt];
|
||||
|
||||
const program = ts.createProgram(
|
||||
[someGenFileName], {
|
||||
module: ts.ModuleKind.CommonJS,
|
||||
target: ts.ScriptTarget.ES2017,
|
||||
sourceMap: true,
|
||||
inlineSourceMap: true,
|
||||
inlineSources: true,
|
||||
},
|
||||
host);
|
||||
const moduleSourceFile = program.getSourceFile(someGenFileName);
|
||||
const transformers: ts.CustomTransformers = {
|
||||
before: [context => {
|
||||
return sourceFile => {
|
||||
const [newSourceFile] = emitter.updateSourceFile(sourceFile, stmts, preamble);
|
||||
return newSourceFile;
|
||||
};
|
||||
}]
|
||||
};
|
||||
let result: string = '';
|
||||
const emitResult = program.emit(
|
||||
moduleSourceFile, (fileName, data, writeByteOrderMark, onError, sourceFiles) => {
|
||||
if (fileName.startsWith(someGenFilePath)) {
|
||||
result = data;
|
||||
}
|
||||
}, undefined, undefined, transformers);
|
||||
return result;
|
||||
}
|
||||
|
||||
function mappingItemsOf(text: string) {
|
||||
// find the source map:
|
||||
const sourceMapMatch = /sourceMappingURL\=data\:application\/json;base64,(.*)$/.exec(text);
|
||||
const sourceMapBase64 = sourceMapMatch![1];
|
||||
const sourceMapBuffer = Buffer.from(sourceMapBase64, 'base64');
|
||||
const sourceMapText = sourceMapBuffer.toString('utf8');
|
||||
const sourceMapParsed = JSON.parse(sourceMapText) as unknown;
|
||||
const consumer = new sourceMap.SourceMapConsumer(sourceMapParsed);
|
||||
const mappings: any[] = [];
|
||||
consumer.eachMapping((mapping: any) => {
|
||||
mappings.push(mapping);
|
||||
});
|
||||
return mappings;
|
||||
}
|
||||
|
||||
it('should produce a source map that maps back to the source', () => {
|
||||
const statement = someVar.set(o.literal(1)).toDeclStmt();
|
||||
const text = '<my-comp> a = 1 </my-comp>';
|
||||
const sourceName = '/some/file.html';
|
||||
const sourceUrl = '../some/file.html';
|
||||
const file = new ParseSourceFile(text, sourceName);
|
||||
const start = new ParseLocation(file, 0, 0, 0);
|
||||
const end = new ParseLocation(file, text.length, 0, text.length);
|
||||
statement.sourceSpan = new ParseSourceSpan(start, end);
|
||||
|
||||
const result = emitStmt(statement);
|
||||
const mappings = mappingItemsOf(result);
|
||||
|
||||
expect(mappings).toEqual([
|
||||
{
|
||||
source: sourceUrl,
|
||||
generatedLine: 3,
|
||||
generatedColumn: 0,
|
||||
originalLine: 1,
|
||||
originalColumn: 0,
|
||||
name: null! // TODO: Review use of `!` here (#19904)
|
||||
},
|
||||
{
|
||||
source: sourceUrl,
|
||||
generatedLine: 3,
|
||||
generatedColumn: 16,
|
||||
originalLine: 1,
|
||||
originalColumn: 26,
|
||||
name: null! // TODO: Review use of `!` here (#19904)
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it('should produce a mapping per range instead of a mapping per node', () => {
|
||||
const text = '<my-comp> a = 1 </my-comp>';
|
||||
const sourceName = '/some/file.html';
|
||||
const sourceUrl = '../some/file.html';
|
||||
const file = new ParseSourceFile(text, sourceName);
|
||||
const start = new ParseLocation(file, 0, 0, 0);
|
||||
const end = new ParseLocation(file, text.length, 0, text.length);
|
||||
const stmt = (loc: number) => {
|
||||
const start = new ParseLocation(file, loc, 0, loc);
|
||||
const end = new ParseLocation(file, loc + 1, 0, loc + 1);
|
||||
const span = new ParseSourceSpan(start, end);
|
||||
return someVar
|
||||
.set(new o.BinaryOperatorExpr(
|
||||
o.BinaryOperator.Plus, o.literal(loc, null, span), o.literal(loc, null, span), null,
|
||||
span))
|
||||
.toDeclStmt();
|
||||
};
|
||||
const stmts = [1, 2, 3, 4, 5, 6].map(stmt);
|
||||
const result = emitStmt(stmts);
|
||||
const mappings = mappingItemsOf(result);
|
||||
|
||||
// The span is used in three different nodes but should only be emitted at most twice
|
||||
// (once for the start and once for the end of a span).
|
||||
const maxDup = Math.max(
|
||||
...Array.from(countsOfDuplicatesMap(mappings.map(m => m.originalColumn)).values()));
|
||||
expect(maxDup <= 2).toBeTruthy('A redundant range was emitted');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function countsOfDuplicatesMap<T>(a: T[]): Map<T, number> {
|
||||
const result = new Map<T, number>();
|
||||
for (const item of a) {
|
||||
result.set(item, (result.get(item) || 0) + 1);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const FILES: Directory = {
|
||||
somePackage: {'someGenFile.ts': `export var a: number;`}
|
||||
};
|
||||
|
||||
const enum Format {
|
||||
Raw,
|
||||
Flat
|
||||
}
|
||||
|
||||
function normalizeResult(result: string, format: Format): string {
|
||||
// Remove TypeScript prefixes
|
||||
let res = result.replace('"use strict";', ' ')
|
||||
.replace('exports.__esModule = true;', ' ')
|
||||
.replace('Object.defineProperty(exports, "__esModule", { value: true });', ' ');
|
||||
|
||||
// Remove hoisted initial export assignments. These were added in TS 3.9:
|
||||
// https://github.com/Microsoft/TypeScript/commit/c6c2c4c8d5aa0947de16f484b8c16fb0eab1c48f
|
||||
res = res.replace(/^exports\.\S+ = void 0;$/gm, '');
|
||||
|
||||
// Remove new lines
|
||||
// Squish adjacent spaces
|
||||
if (format === Format.Flat) {
|
||||
return res.replace(/\n/g, ' ').replace(/ +/g, ' ').replace(/^ /g, '').replace(/ $/g, '');
|
||||
}
|
||||
|
||||
// Remove prefix and postfix spaces
|
||||
return res.trim();
|
||||
}
|
||||
|
|
@ -1,791 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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
|
||||
*/
|
||||
/// <reference types="node" />
|
||||
import * as ng from '@angular/compiler-cli';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import ts from 'typescript';
|
||||
|
||||
import {formatDiagnostics} from '../../src/perform_compile';
|
||||
import {CompilerHost, EmitFlags, LazyRoute} from '../../src/transformers/api';
|
||||
import {createSrcToOutPathMapper, resetTempProgramHandlerForTest, setTempProgramHandlerForTest} from '../../src/transformers/program';
|
||||
import {StructureIsReused, tsStructureIsReused} from '../../src/transformers/util';
|
||||
import {expectNoDiagnosticsInProgram, setup, stripAnsi, TestSupport} from '../test_support';
|
||||
|
||||
describe('ng program', () => {
|
||||
let testSupport: TestSupport;
|
||||
let errorSpy: jasmine.Spy&((s: string) => void);
|
||||
|
||||
beforeEach(() => {
|
||||
errorSpy = jasmine.createSpy('consoleError').and.callFake(console.error);
|
||||
testSupport = setup();
|
||||
});
|
||||
|
||||
function createModuleAndCompSource(prefix: string, template: string = prefix + 'template') {
|
||||
const templateEntry =
|
||||
template.endsWith('.html') ? `templateUrl: '${template}'` : `template: \`${template}\``;
|
||||
return `
|
||||
import {Component, NgModule} from '@angular/core';
|
||||
|
||||
@Component({selector: '${prefix}', ${templateEntry}})
|
||||
export class ${prefix}Comp {}
|
||||
|
||||
@NgModule({declarations: [${prefix}Comp]})
|
||||
export class ${prefix}Module {}
|
||||
`;
|
||||
}
|
||||
|
||||
function compileLib(libName: string) {
|
||||
testSupport.writeFiles({
|
||||
[`${libName}_src/index.ts`]: createModuleAndCompSource(libName),
|
||||
});
|
||||
const options = testSupport.createCompilerOptions();
|
||||
const program = ng.createProgram({
|
||||
rootNames: [path.resolve(testSupport.basePath, `${libName}_src/index.ts`)],
|
||||
options,
|
||||
host: ng.createCompilerHost({options}),
|
||||
});
|
||||
expectNoDiagnosticsInProgram(options, program);
|
||||
fs.symlinkSync(
|
||||
path.resolve(testSupport.basePath, 'built', `${libName}_src`),
|
||||
path.resolve(testSupport.basePath, 'node_modules', libName), 'dir');
|
||||
program.emit({emitFlags: ng.EmitFlags.DTS | ng.EmitFlags.JS | ng.EmitFlags.Metadata});
|
||||
}
|
||||
|
||||
function compile(
|
||||
oldProgram?: ng.Program, overrideOptions?: ng.CompilerOptions, rootNames?: string[],
|
||||
host?: CompilerHost): {program: ng.Program, emitResult: ts.EmitResult} {
|
||||
const options = testSupport.createCompilerOptions(overrideOptions);
|
||||
if (!rootNames) {
|
||||
rootNames = [path.resolve(testSupport.basePath, 'src/index.ts')];
|
||||
}
|
||||
if (!host) {
|
||||
host = ng.createCompilerHost({options});
|
||||
}
|
||||
const program = ng.createProgram({
|
||||
rootNames: rootNames,
|
||||
options,
|
||||
host,
|
||||
oldProgram,
|
||||
});
|
||||
expectNoDiagnosticsInProgram(options, program);
|
||||
const emitResult = program.emit();
|
||||
return {emitResult, program};
|
||||
}
|
||||
|
||||
function createWatchModeHost(): ng.CompilerHost {
|
||||
const options = testSupport.createCompilerOptions();
|
||||
const host = ng.createCompilerHost({options});
|
||||
|
||||
const originalGetSourceFile = host.getSourceFile;
|
||||
const cache = new Map<string, ts.SourceFile>();
|
||||
host.getSourceFile = function(fileName: string, languageVersion: ts.ScriptTarget):
|
||||
ts.SourceFile|
|
||||
undefined {
|
||||
const sf = originalGetSourceFile.call(host, fileName, languageVersion);
|
||||
if (sf) {
|
||||
if (cache.has(sf.fileName)) {
|
||||
const oldSf = cache.get(sf.fileName)!;
|
||||
if (oldSf.getFullText() === sf.getFullText()) {
|
||||
return oldSf;
|
||||
}
|
||||
}
|
||||
cache.set(sf.fileName, sf);
|
||||
}
|
||||
return sf;
|
||||
};
|
||||
return host;
|
||||
}
|
||||
|
||||
function resolveFiles(rootNames: string[]) {
|
||||
const preOptions = testSupport.createCompilerOptions();
|
||||
const preHost = ts.createCompilerHost(preOptions);
|
||||
// don't resolve symlinks
|
||||
preHost.realpath = (f) => f;
|
||||
const preProgram = ts.createProgram(rootNames, preOptions, preHost);
|
||||
return preProgram.getSourceFiles().map(sf => sf.fileName);
|
||||
}
|
||||
|
||||
describe('reuse of old program', () => {
|
||||
it('should reuse generated code for libraries from old programs', () => {
|
||||
compileLib('lib');
|
||||
testSupport.writeFiles({
|
||||
'src/main.ts': createModuleAndCompSource('main'),
|
||||
'src/index.ts': `
|
||||
export * from './main';
|
||||
export * from 'lib/index';
|
||||
`
|
||||
});
|
||||
const p1 = compile().program;
|
||||
expect(p1.getTsProgram().getSourceFiles().some(
|
||||
sf => /node_modules\/lib\/.*\.ngfactory\.ts$/.test(sf.fileName)))
|
||||
.toBe(true);
|
||||
expect(p1.getTsProgram().getSourceFiles().some(
|
||||
sf => /node_modules\/lib2\/.*\.ngfactory.*$/.test(sf.fileName)))
|
||||
.toBe(false);
|
||||
const p2 = compile(p1).program;
|
||||
expect(p2.getTsProgram().getSourceFiles().some(
|
||||
sf => /node_modules\/lib\/.*\.ngfactory.*$/.test(sf.fileName)))
|
||||
.toBe(false);
|
||||
expect(p2.getTsProgram().getSourceFiles().some(
|
||||
sf => /node_modules\/lib2\/.*\.ngfactory.*$/.test(sf.fileName)))
|
||||
.toBe(false);
|
||||
|
||||
// import a library for which we didn't generate code before
|
||||
compileLib('lib2');
|
||||
testSupport.writeFiles({
|
||||
'src/index.ts': `
|
||||
export * from './main';
|
||||
export * from 'lib/index';
|
||||
export * from 'lib2/index';
|
||||
`,
|
||||
});
|
||||
const p3 = compile(p2).program;
|
||||
expect(p3.getTsProgram().getSourceFiles().some(
|
||||
sf => /node_modules\/lib\/.*\.ngfactory.*$/.test(sf.fileName)))
|
||||
.toBe(false);
|
||||
expect(p3.getTsProgram().getSourceFiles().some(
|
||||
sf => /node_modules\/lib2\/.*\.ngfactory\.ts$/.test(sf.fileName)))
|
||||
.toBe(true);
|
||||
|
||||
const p4 = compile(p3).program;
|
||||
expect(p4.getTsProgram().getSourceFiles().some(
|
||||
sf => /node_modules\/lib\/.*\.ngfactory.*$/.test(sf.fileName)))
|
||||
.toBe(false);
|
||||
expect(p4.getTsProgram().getSourceFiles().some(
|
||||
sf => /node_modules\/lib2\/.*\.ngfactory.*$/.test(sf.fileName)))
|
||||
.toBe(false);
|
||||
});
|
||||
|
||||
// Note: this is the case for watch mode with declaration:false
|
||||
it('should reuse generated code from libraries from old programs with declaration:false',
|
||||
() => {
|
||||
compileLib('lib');
|
||||
|
||||
testSupport.writeFiles({
|
||||
'src/main.ts': createModuleAndCompSource('main'),
|
||||
'src/index.ts': `
|
||||
export * from './main';
|
||||
export * from 'lib/index';
|
||||
`
|
||||
});
|
||||
const p1 = compile(undefined, {declaration: false}).program;
|
||||
expect(p1.getTsProgram().getSourceFiles().some(
|
||||
sf => /node_modules\/lib\/.*\.ngfactory\.ts$/.test(sf.fileName)))
|
||||
.toBe(true);
|
||||
expect(p1.getTsProgram().getSourceFiles().some(
|
||||
sf => /node_modules\/lib2\/.*\.ngfactory.*$/.test(sf.fileName)))
|
||||
.toBe(false);
|
||||
const p2 = compile(p1, {declaration: false}).program;
|
||||
expect(p2.getTsProgram().getSourceFiles().some(
|
||||
sf => /node_modules\/lib\/.*\.ngfactory.*$/.test(sf.fileName)))
|
||||
.toBe(false);
|
||||
expect(p2.getTsProgram().getSourceFiles().some(
|
||||
sf => /node_modules\/lib2\/.*\.ngfactory.*$/.test(sf.fileName)))
|
||||
.toBe(false);
|
||||
});
|
||||
|
||||
it('should only emit changed files', () => {
|
||||
testSupport.writeFiles({
|
||||
'src/index.ts': createModuleAndCompSource('comp', 'index.html'),
|
||||
'src/index.html': `Start`
|
||||
});
|
||||
const options: ng.CompilerOptions = {declaration: false};
|
||||
const host = ng.createCompilerHost({options});
|
||||
const originalGetSourceFile = host.getSourceFile;
|
||||
const fileCache = new Map<string, ts.SourceFile>();
|
||||
host.getSourceFile = (fileName: string, languageVersion: ts.ScriptTarget) => {
|
||||
if (fileCache.has(fileName)) {
|
||||
return fileCache.get(fileName);
|
||||
}
|
||||
const sf = originalGetSourceFile.call(host, fileName, languageVersion);
|
||||
if (sf !== undefined) {
|
||||
fileCache.set(fileName, sf);
|
||||
}
|
||||
return sf;
|
||||
};
|
||||
|
||||
const written = new Map<string, string>();
|
||||
host.writeFile = (fileName: string, data: string) => written.set(fileName, data);
|
||||
|
||||
// compile libraries
|
||||
const p1 = compile(undefined, options, undefined, host).program;
|
||||
|
||||
// compile without libraries
|
||||
const p2 = compile(p1, options, undefined, host).program;
|
||||
expect(written.has(path.posix.join(testSupport.basePath, 'built/src/index.js'))).toBe(true);
|
||||
let ngFactoryContent =
|
||||
written.get(path.posix.join(testSupport.basePath, 'built/src/index.ngfactory.js'));
|
||||
expect(ngFactoryContent).toMatch(/Start/);
|
||||
|
||||
// no change -> no emit
|
||||
written.clear();
|
||||
const p3 = compile(p2, options, undefined, host).program;
|
||||
expect(written.size).toBe(0);
|
||||
|
||||
// change a user file
|
||||
written.clear();
|
||||
fileCache.delete(path.posix.join(testSupport.basePath, 'src/index.ts'));
|
||||
const p4 = compile(p3, options, undefined, host).program;
|
||||
expect(written.size).toBe(1);
|
||||
expect(written.has(path.posix.join(testSupport.basePath, 'built/src/index.js'))).toBe(true);
|
||||
|
||||
// change a file that is input to generated files
|
||||
written.clear();
|
||||
testSupport.writeFiles({'src/index.html': 'Hello'});
|
||||
const p5 = compile(p4, options, undefined, host).program;
|
||||
expect(written.size).toBe(1);
|
||||
ngFactoryContent =
|
||||
written.get(path.posix.join(testSupport.basePath, 'built/src/index.ngfactory.js'));
|
||||
expect(ngFactoryContent).toMatch(/Hello/);
|
||||
|
||||
// change a file and create an intermediate program that is not emitted
|
||||
written.clear();
|
||||
fileCache.delete(path.posix.join(testSupport.basePath, 'src/index.ts'));
|
||||
const p6 = ng.createProgram({
|
||||
rootNames: [path.posix.join(testSupport.basePath, 'src/index.ts')],
|
||||
options: testSupport.createCompilerOptions(options),
|
||||
host,
|
||||
oldProgram: p5
|
||||
});
|
||||
const p7 = compile(p6, options, undefined, host).program;
|
||||
expect(written.size).toBe(1);
|
||||
});
|
||||
|
||||
it('should set emitSkipped to false for full and incremental emit', () => {
|
||||
testSupport.writeFiles({
|
||||
'src/index.ts': createModuleAndCompSource('main'),
|
||||
});
|
||||
const {emitResult: emitResult1, program: p1} = compile();
|
||||
expect(emitResult1.emitSkipped).toBe(false);
|
||||
const {emitResult: emitResult2, program: p2} = compile(p1);
|
||||
expect(emitResult2.emitSkipped).toBe(false);
|
||||
const {emitResult: emitResult3, program: p3} = compile(p2);
|
||||
expect(emitResult3.emitSkipped).toBe(false);
|
||||
});
|
||||
|
||||
it('should store library summaries on emit', () => {
|
||||
compileLib('lib');
|
||||
testSupport.writeFiles({
|
||||
'src/main.ts': createModuleAndCompSource('main'),
|
||||
'src/index.ts': `
|
||||
export * from './main';
|
||||
export * from 'lib/index';
|
||||
`
|
||||
});
|
||||
const p1 = compile().program;
|
||||
expect(Array.from(p1.getLibrarySummaries().values())
|
||||
.some(sf => /node_modules\/lib\/index\.ngfactory\.d\.ts$/.test(sf.fileName)))
|
||||
.toBe(true);
|
||||
expect(Array.from(p1.getLibrarySummaries().values())
|
||||
.some(sf => /node_modules\/lib\/index\.ngsummary\.json$/.test(sf.fileName)))
|
||||
.toBe(true);
|
||||
expect(Array.from(p1.getLibrarySummaries().values())
|
||||
.some(sf => /node_modules\/lib\/index\.d\.ts$/.test(sf.fileName)))
|
||||
.toBe(true);
|
||||
|
||||
expect(Array.from(p1.getLibrarySummaries().values())
|
||||
.some(sf => /src\/main.*$/.test(sf.fileName)))
|
||||
.toBe(false);
|
||||
});
|
||||
|
||||
describe(
|
||||
'verify that program structure is reused within tsc in order to speed up incremental compilation',
|
||||
() => {
|
||||
afterEach(resetTempProgramHandlerForTest);
|
||||
|
||||
function captureStructureReuse(compile: () => void): StructureIsReused|null {
|
||||
let structureReuse: StructureIsReused|null = null;
|
||||
setTempProgramHandlerForTest(program => {
|
||||
structureReuse = tsStructureIsReused(program);
|
||||
});
|
||||
compile();
|
||||
return structureReuse;
|
||||
}
|
||||
|
||||
it('should reuse the old ts program completely if nothing changed', () => {
|
||||
testSupport.writeFiles({'src/index.ts': createModuleAndCompSource('main')});
|
||||
const host = createWatchModeHost();
|
||||
// Note: the second compile drops factories for library files,
|
||||
// and therefore changes the structure again
|
||||
const p1 = compile(undefined, undefined, undefined, host).program;
|
||||
const p2 = compile(p1, undefined, undefined, host).program;
|
||||
const structureReuse =
|
||||
captureStructureReuse(() => compile(p2, undefined, undefined, host));
|
||||
expect(structureReuse).toBe(StructureIsReused.Completely);
|
||||
});
|
||||
|
||||
it('should reuse the old ts program completely if a template or a ts file changed',
|
||||
() => {
|
||||
const host = createWatchModeHost();
|
||||
testSupport.writeFiles({
|
||||
'src/main.ts': createModuleAndCompSource('main', 'main.html'),
|
||||
'src/main.html': `Some template`,
|
||||
'src/util.ts': `export const x = 1`,
|
||||
'src/index.ts': `
|
||||
export * from './main';
|
||||
export * from './util';
|
||||
`
|
||||
});
|
||||
// Note: the second compile drops factories for library files,
|
||||
// and therefore changes the structure again
|
||||
const p1 = compile(undefined, undefined, undefined, host).program;
|
||||
const p2 = compile(p1, undefined, undefined, host).program;
|
||||
testSupport.writeFiles({
|
||||
'src/main.html': `Another template`,
|
||||
'src/util.ts': `export const x = 2`,
|
||||
});
|
||||
const structureReuse =
|
||||
captureStructureReuse(() => compile(p2, undefined, undefined, host));
|
||||
expect(structureReuse).toBe(StructureIsReused.Completely);
|
||||
});
|
||||
|
||||
it('should not reuse the old ts program if an import changed', () => {
|
||||
const host = createWatchModeHost();
|
||||
testSupport.writeFiles({
|
||||
'src/main.ts': createModuleAndCompSource('main'),
|
||||
'src/util.ts': `export const x = 1`,
|
||||
'src/index.ts': `
|
||||
export * from './main';
|
||||
export * from './util';
|
||||
`
|
||||
});
|
||||
// Note: the second compile drops factories for library files,
|
||||
// and therefore changes the structure again
|
||||
const p1 = compile(undefined, undefined, undefined, host).program;
|
||||
const p2 = compile(p1, undefined, undefined, host).program;
|
||||
testSupport.writeFiles(
|
||||
{'src/util.ts': `import {Injectable} from '@angular/core'; export const x = 1;`});
|
||||
const structureReuse =
|
||||
captureStructureReuse(() => compile(p2, undefined, undefined, host));
|
||||
expect(structureReuse).toBe(StructureIsReused.SafeModules);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should not typecheck templates if skipTemplateCodegen is set but fullTemplateTypeCheck is not',
|
||||
() => {
|
||||
testSupport.writeFiles({
|
||||
'src/main.ts': `
|
||||
import {NgModule} from '@angular/core';
|
||||
|
||||
@NgModule((() => {if (1==1) return null as any;}) as any)
|
||||
export class SomeClassWithInvalidMetadata {}
|
||||
`,
|
||||
});
|
||||
const options = testSupport.createCompilerOptions({skipTemplateCodegen: true});
|
||||
const host = ng.createCompilerHost({options});
|
||||
const program = ng.createProgram(
|
||||
{rootNames: [path.resolve(testSupport.basePath, 'src/main.ts')], options, host});
|
||||
expectNoDiagnosticsInProgram(options, program);
|
||||
const emitResult = program.emit({emitFlags: EmitFlags.All});
|
||||
expect(emitResult.diagnostics.length).toBe(0);
|
||||
|
||||
testSupport.shouldExist('built/src/main.metadata.json');
|
||||
});
|
||||
|
||||
it('should typecheck templates if skipTemplateCodegen and fullTemplateTypeCheck is set', () => {
|
||||
testSupport.writeFiles({
|
||||
'src/main.ts': createModuleAndCompSource('main', `{{nonExistent}}`),
|
||||
});
|
||||
const options = testSupport.createCompilerOptions({
|
||||
skipTemplateCodegen: true,
|
||||
fullTemplateTypeCheck: true,
|
||||
});
|
||||
const host = ng.createCompilerHost({options});
|
||||
const program = ng.createProgram(
|
||||
{rootNames: [path.resolve(testSupport.basePath, 'src/main.ts')], options, host});
|
||||
const diags = program.getNgSemanticDiagnostics();
|
||||
expect(diags.length).toBe(1);
|
||||
expect(diags[0].messageText).toBe(`Property 'nonExistent' does not exist on type 'mainComp'.`);
|
||||
});
|
||||
|
||||
it('should be able to use asynchronously loaded resources', (done) => {
|
||||
testSupport.writeFiles({
|
||||
'src/main.ts': createModuleAndCompSource('main', 'main.html'),
|
||||
// Note: we need to be able to resolve the template synchronously,
|
||||
// only the content is delivered asynchronously.
|
||||
'src/main.html': '',
|
||||
});
|
||||
const options = testSupport.createCompilerOptions();
|
||||
const host = ng.createCompilerHost({options});
|
||||
host.readResource = () => Promise.resolve('Hello world!');
|
||||
const program = ng.createProgram(
|
||||
{rootNames: [path.resolve(testSupport.basePath, 'src/main.ts')], options, host});
|
||||
program.loadNgStructureAsync().then(() => {
|
||||
program.emit();
|
||||
const ngFactoryPath = path.resolve(testSupport.basePath, 'built/src/main.ngfactory.js');
|
||||
const factory = fs.readFileSync(ngFactoryPath, 'utf8');
|
||||
expect(factory).toContain('Hello world!');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should work with noResolve', () => {
|
||||
// create a temporary ts program to get the list of all files from angular...
|
||||
testSupport.writeFiles({
|
||||
'src/main.ts': createModuleAndCompSource('main'),
|
||||
});
|
||||
const allRootNames = resolveFiles([path.resolve(testSupport.basePath, 'src/main.ts')]);
|
||||
|
||||
// now do the actual test with noResolve
|
||||
const program = compile(undefined, {noResolve: true}, allRootNames);
|
||||
|
||||
testSupport.shouldExist('built/src/main.ngfactory.js');
|
||||
testSupport.shouldExist('built/src/main.ngfactory.d.ts');
|
||||
});
|
||||
|
||||
it('should work with tsx files', () => {
|
||||
// create a temporary ts program to get the list of all files from angular...
|
||||
testSupport.writeFiles({
|
||||
'src/main.tsx': createModuleAndCompSource('main'),
|
||||
});
|
||||
const allRootNames = resolveFiles([path.resolve(testSupport.basePath, 'src/main.tsx')]);
|
||||
|
||||
const program = compile(undefined, {jsx: ts.JsxEmit.React}, allRootNames);
|
||||
|
||||
testSupport.shouldExist('built/src/main.js');
|
||||
testSupport.shouldExist('built/src/main.d.ts');
|
||||
testSupport.shouldExist('built/src/main.ngfactory.js');
|
||||
testSupport.shouldExist('built/src/main.ngfactory.d.ts');
|
||||
testSupport.shouldExist('built/src/main.ngsummary.json');
|
||||
});
|
||||
|
||||
it('should emit also empty generated files depending on the options', () => {
|
||||
testSupport.writeFiles({
|
||||
'src/main.ts': `
|
||||
import {Component, NgModule} from '@angular/core';
|
||||
|
||||
@Component({selector: 'main', template: '', styleUrls: ['main.css']})
|
||||
export class MainComp {}
|
||||
|
||||
@NgModule({declarations: [MainComp]})
|
||||
export class MainModule {}
|
||||
`,
|
||||
'src/main.css': ``,
|
||||
'src/util.ts': 'export const x = 1;',
|
||||
'src/index.ts': `
|
||||
export * from './util';
|
||||
export * from './main';
|
||||
`,
|
||||
});
|
||||
const options = testSupport.createCompilerOptions({
|
||||
allowEmptyCodegenFiles: true,
|
||||
enableSummariesForJit: true,
|
||||
});
|
||||
const host = ng.createCompilerHost({options});
|
||||
const written = new Map < string, {
|
||||
original: ReadonlyArray<ts.SourceFile>|undefined;
|
||||
data: string;
|
||||
}
|
||||
> ();
|
||||
|
||||
host.writeFile =
|
||||
(fileName: string, data: string, writeByteOrderMark: boolean,
|
||||
onError: ((message: string) => void)|undefined,
|
||||
sourceFiles?: ReadonlyArray<ts.SourceFile>) => {
|
||||
written.set(fileName, {original: sourceFiles, data});
|
||||
};
|
||||
const program = ng.createProgram(
|
||||
{rootNames: [path.resolve(testSupport.basePath, 'src/index.ts')], options, host});
|
||||
program.emit();
|
||||
|
||||
const enum ShouldBe { Empty, EmptyExport, NoneEmpty }
|
||||
function assertGenFile(
|
||||
fileName: string, checks: {originalFileName: string, shouldBe: ShouldBe}) {
|
||||
const writeData = written.get(path.posix.join(testSupport.basePath, fileName));
|
||||
expect(writeData).toBeTruthy();
|
||||
expect(
|
||||
writeData!.original!.some(
|
||||
sf => sf.fileName === path.posix.join(testSupport.basePath, checks.originalFileName)))
|
||||
.toBe(true);
|
||||
switch (checks.shouldBe) {
|
||||
case ShouldBe.Empty:
|
||||
expect(writeData!.data).toMatch(/^(\s*\/\*([^*]|\*[^\/])*\*\/\s*)?$/);
|
||||
break;
|
||||
case ShouldBe.EmptyExport:
|
||||
expect(writeData!.data)
|
||||
.toMatch(/^((\s*\/\*([^*]|\*[^\/])*\*\/\s*)|(\s*export\s*{\s*};\s*))$/m);
|
||||
break;
|
||||
case ShouldBe.NoneEmpty:
|
||||
expect(writeData!.data).not.toBe('');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
assertGenFile(
|
||||
'built/src/util.ngfactory.js',
|
||||
{originalFileName: 'src/util.ts', shouldBe: ShouldBe.EmptyExport});
|
||||
assertGenFile(
|
||||
'built/src/util.ngfactory.d.ts',
|
||||
{originalFileName: 'src/util.ts', shouldBe: ShouldBe.EmptyExport});
|
||||
assertGenFile(
|
||||
'built/src/util.ngsummary.js',
|
||||
{originalFileName: 'src/util.ts', shouldBe: ShouldBe.EmptyExport});
|
||||
assertGenFile(
|
||||
'built/src/util.ngsummary.d.ts',
|
||||
{originalFileName: 'src/util.ts', shouldBe: ShouldBe.EmptyExport});
|
||||
assertGenFile(
|
||||
'built/src/util.ngsummary.json',
|
||||
{originalFileName: 'src/util.ts', shouldBe: ShouldBe.NoneEmpty});
|
||||
|
||||
// Note: we always fill non shim and shim style files as they might
|
||||
// be shared by component with and without ViewEncapsulation.
|
||||
assertGenFile(
|
||||
'built/src/main.css.ngstyle.js',
|
||||
{originalFileName: 'src/main.ts', shouldBe: ShouldBe.NoneEmpty});
|
||||
assertGenFile(
|
||||
'built/src/main.css.ngstyle.d.ts',
|
||||
{originalFileName: 'src/main.ts', shouldBe: ShouldBe.EmptyExport});
|
||||
// Note: this file is not empty as we actually generated code for it
|
||||
assertGenFile(
|
||||
'built/src/main.css.shim.ngstyle.js',
|
||||
{originalFileName: 'src/main.ts', shouldBe: ShouldBe.NoneEmpty});
|
||||
assertGenFile(
|
||||
'built/src/main.css.shim.ngstyle.d.ts',
|
||||
{originalFileName: 'src/main.ts', shouldBe: ShouldBe.EmptyExport});
|
||||
});
|
||||
|
||||
it('should not emit /// references in .d.ts files', () => {
|
||||
testSupport.writeFiles({
|
||||
'src/main.ts': createModuleAndCompSource('main'),
|
||||
});
|
||||
compile(undefined, {declaration: true}, [path.resolve(testSupport.basePath, 'src/main.ts')]);
|
||||
|
||||
const dts =
|
||||
fs.readFileSync(path.resolve(testSupport.basePath, 'built', 'src', 'main.d.ts')).toString();
|
||||
expect(dts).toMatch('export declare class');
|
||||
expect(dts).not.toMatch('///');
|
||||
});
|
||||
|
||||
it('should not emit generated files whose sources are outside of the rootDir', () => {
|
||||
testSupport.writeFiles({
|
||||
'src/main.ts': createModuleAndCompSource('main'),
|
||||
'src/index.ts': `
|
||||
export * from './main';
|
||||
`
|
||||
});
|
||||
const options =
|
||||
testSupport.createCompilerOptions({rootDir: path.resolve(testSupport.basePath, 'src')});
|
||||
const host = ng.createCompilerHost({options});
|
||||
const writtenFileNames: string[] = [];
|
||||
const oldWriteFile = host.writeFile;
|
||||
host.writeFile = (fileName, data, writeByteOrderMark, onError, sourceFiles) => {
|
||||
writtenFileNames.push(fileName);
|
||||
oldWriteFile(fileName, data, writeByteOrderMark, onError, sourceFiles);
|
||||
};
|
||||
|
||||
compile(/*oldProgram*/ undefined, options, /*rootNames*/ undefined, host);
|
||||
|
||||
// no emit for files from node_modules as they are outside of rootDir
|
||||
expect(writtenFileNames.some(f => /node_modules/.test(f))).toBe(false);
|
||||
|
||||
// emit all gen files for files under src/
|
||||
testSupport.shouldExist('built/main.js');
|
||||
testSupport.shouldExist('built/main.d.ts');
|
||||
testSupport.shouldExist('built/main.ngfactory.js');
|
||||
testSupport.shouldExist('built/main.ngfactory.d.ts');
|
||||
testSupport.shouldExist('built/main.ngsummary.json');
|
||||
});
|
||||
|
||||
describe('createSrcToOutPathMapper', () => {
|
||||
it('should return identity mapping if no outDir is present', () => {
|
||||
const mapper = createSrcToOutPathMapper(undefined, undefined, undefined, path.posix);
|
||||
expect(mapper('/tmp/b/y.js')).toBe('/tmp/b/y.js');
|
||||
});
|
||||
|
||||
it('should return identity mapping if first src and out fileName have same dir', () => {
|
||||
const mapper = createSrcToOutPathMapper('/tmp', '/tmp/a/x.ts', '/tmp/a/x.js', path.posix);
|
||||
expect(mapper('/tmp/b/y.js')).toBe('/tmp/b/y.js');
|
||||
});
|
||||
|
||||
it('should adjust the filename if the outDir is inside of the rootDir', () => {
|
||||
const mapper =
|
||||
createSrcToOutPathMapper('/tmp/out', '/tmp/a/x.ts', '/tmp/out/a/x.js', path.posix);
|
||||
expect(mapper('/tmp/b/y.js')).toBe('/tmp/out/b/y.js');
|
||||
});
|
||||
|
||||
it('should adjust the filename if the outDir is outside of the rootDir', () => {
|
||||
const mapper = createSrcToOutPathMapper('/out', '/tmp/a/x.ts', '/out/a/x.js', path.posix);
|
||||
expect(mapper('/tmp/b/y.js')).toBe('/out/b/y.js');
|
||||
});
|
||||
|
||||
it('should adjust the filename if the common prefix of sampleSrc and sampleOut is outside of outDir',
|
||||
() => {
|
||||
const mapper = createSrcToOutPathMapper(
|
||||
'/dist/common', '/src/common/x.ts', '/dist/common/x.js', path.posix);
|
||||
expect(mapper('/src/common/y.js')).toBe('/dist/common/y.js');
|
||||
});
|
||||
|
||||
it('should work on windows with normalized paths', () => {
|
||||
const mapper =
|
||||
createSrcToOutPathMapper('c:/tmp/out', 'c:/tmp/a/x.ts', 'c:/tmp/out/a/x.js', path.win32);
|
||||
expect(mapper('c:/tmp/b/y.js')).toBe('c:/tmp/out/b/y.js');
|
||||
});
|
||||
|
||||
it('should work on windows with non-normalized paths', () => {
|
||||
const mapper = createSrcToOutPathMapper(
|
||||
'c:\\tmp\\out', 'c:\\tmp\\a\\x.ts', 'c:\\tmp\\out\\a\\x.js', path.win32);
|
||||
expect(mapper('c:\\tmp\\b\\y.js')).toBe('c:/tmp/out/b/y.js');
|
||||
});
|
||||
});
|
||||
|
||||
it('should report errors for ts and ng errors on emit with noEmitOnError=true', () => {
|
||||
testSupport.writeFiles({
|
||||
'src/main.ts': `
|
||||
import {Component, NgModule} from '@angular/core';
|
||||
|
||||
// Ts error
|
||||
let x: string = 1;
|
||||
|
||||
// Ng error
|
||||
@Component({selector: 'comp', templateUrl: './main.html'})
|
||||
export class MyComp {}
|
||||
|
||||
@NgModule({declarations: [MyComp]})
|
||||
export class MyModule {}
|
||||
`,
|
||||
'src/main.html': '{{nonExistent}}'
|
||||
});
|
||||
const options = testSupport.createCompilerOptions({noEmitOnError: true});
|
||||
const host = ng.createCompilerHost({options});
|
||||
const program1 = ng.createProgram(
|
||||
{rootNames: [path.resolve(testSupport.basePath, 'src/main.ts')], options, host});
|
||||
const errorDiags =
|
||||
program1.emit().diagnostics.filter(d => d.category === ts.DiagnosticCategory.Error);
|
||||
expect(stripAnsi(formatDiagnostics(errorDiags)))
|
||||
.toContain(
|
||||
`src/main.ts:5:13 - error TS2322: Type 'number' is not assignable to type 'string'.`);
|
||||
expect(stripAnsi(formatDiagnostics(errorDiags)))
|
||||
.toContain(
|
||||
`src/main.html:1:1 - error TS100: Property 'nonExistent' does not exist on type 'MyComp'.`);
|
||||
});
|
||||
|
||||
it('should not report emit errors with noEmitOnError=false', () => {
|
||||
testSupport.writeFiles({
|
||||
'src/main.ts': `
|
||||
@NgModule()
|
||||
`
|
||||
});
|
||||
const options = testSupport.createCompilerOptions({noEmitOnError: false});
|
||||
const host = ng.createCompilerHost({options});
|
||||
const program1 = ng.createProgram(
|
||||
{rootNames: [path.resolve(testSupport.basePath, 'src/main.ts')], options, host});
|
||||
expect(program1.emit().diagnostics.length).toBe(0);
|
||||
});
|
||||
|
||||
describe('errors', () => {
|
||||
const fileWithStructuralError = `
|
||||
import {NgModule} from '@angular/core';
|
||||
|
||||
@NgModule(() => (1===1 ? null as any : null as any))
|
||||
export class MyModule {}
|
||||
`;
|
||||
const fileWithGoodContent = `
|
||||
import {NgModule} from '@angular/core';
|
||||
|
||||
@NgModule()
|
||||
export class MyModule {}
|
||||
`;
|
||||
|
||||
it('should not throw on structural errors but collect them', () => {
|
||||
testSupport.write('src/index.ts', fileWithStructuralError);
|
||||
|
||||
const options = testSupport.createCompilerOptions();
|
||||
const host = ng.createCompilerHost({options});
|
||||
const program = ng.createProgram(
|
||||
{rootNames: [path.resolve(testSupport.basePath, 'src/index.ts')], options, host});
|
||||
|
||||
const structuralErrors = program.getNgStructuralDiagnostics();
|
||||
expect(structuralErrors.length).toBe(1);
|
||||
expect(structuralErrors[0].messageText).toContain('Function expressions are not supported');
|
||||
});
|
||||
|
||||
it('should not throw on structural errors but collect them (loadNgStructureAsync)', (done) => {
|
||||
testSupport.write('src/index.ts', fileWithStructuralError);
|
||||
|
||||
const options = testSupport.createCompilerOptions();
|
||||
const host = ng.createCompilerHost({options});
|
||||
const program = ng.createProgram(
|
||||
{rootNames: [path.resolve(testSupport.basePath, 'src/index.ts')], options, host});
|
||||
program.loadNgStructureAsync().then(() => {
|
||||
const structuralErrors = program.getNgStructuralDiagnostics();
|
||||
expect(structuralErrors.length).toBe(1);
|
||||
expect(structuralErrors[0].messageText).toContain('Function expressions are not supported');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should include non-formatted errors (e.g. invalid templateUrl)', () => {
|
||||
testSupport.write('src/index.ts', `
|
||||
import {Component, NgModule} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'my-component',
|
||||
templateUrl: 'template.html', // invalid template url
|
||||
})
|
||||
export class MyComponent {}
|
||||
|
||||
@NgModule({
|
||||
declarations: [MyComponent]
|
||||
})
|
||||
export class MyModule {}
|
||||
`);
|
||||
|
||||
const options = testSupport.createCompilerOptions();
|
||||
const host = ng.createCompilerHost({options});
|
||||
const program = ng.createProgram({
|
||||
rootNames: [path.resolve(testSupport.basePath, 'src/index.ts')],
|
||||
options,
|
||||
host,
|
||||
});
|
||||
|
||||
const structuralErrors = program.getNgStructuralDiagnostics();
|
||||
expect(structuralErrors.length).toBe(1);
|
||||
expect(structuralErrors[0].messageText).toContain('Couldn\'t resolve resource template.html');
|
||||
});
|
||||
|
||||
it('should be able report structural errors with noResolve:true and generateCodeForLibraries:false ' +
|
||||
'even if getSourceFile throws for non existent files',
|
||||
() => {
|
||||
testSupport.write('src/index.ts', fileWithGoodContent);
|
||||
|
||||
// compile angular and produce .ngsummary.json / ngfactory.d.ts files
|
||||
compile();
|
||||
|
||||
testSupport.write('src/ok.ts', fileWithGoodContent);
|
||||
testSupport.write('src/error.ts', fileWithStructuralError);
|
||||
|
||||
// Make sure the ok.ts file is before the error.ts file,
|
||||
// so we added a .ngfactory.ts file for it.
|
||||
const allRootNames = resolveFiles(
|
||||
['src/ok.ts', 'src/error.ts'].map(fn => path.resolve(testSupport.basePath, fn)));
|
||||
|
||||
const options = testSupport.createCompilerOptions({
|
||||
noResolve: true,
|
||||
generateCodeForLibraries: false,
|
||||
});
|
||||
const host = ng.createCompilerHost({options});
|
||||
const originalGetSourceFile = host.getSourceFile;
|
||||
host.getSourceFile =
|
||||
(fileName: string, languageVersion: ts.ScriptTarget,
|
||||
onError?: ((message: string) => void)|undefined): ts.SourceFile|undefined => {
|
||||
// We should never try to load .ngfactory.ts files
|
||||
if (fileName.match(/\.ngfactory\.ts$/)) {
|
||||
throw new Error(`Non existent ngfactory file: ` + fileName);
|
||||
}
|
||||
return originalGetSourceFile.call(host, fileName, languageVersion, onError);
|
||||
};
|
||||
const program = ng.createProgram({rootNames: allRootNames, options, host});
|
||||
const structuralErrors = program.getNgStructuralDiagnostics();
|
||||
expect(structuralErrors.length).toBe(1);
|
||||
expect(structuralErrors[0].messageText)
|
||||
.toContain('Function expressions are not supported');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 {ClassField, ClassMethod, ClassStmt, PartialModule, Statement, StmtModifier} from '@angular/compiler';
|
||||
import ts from 'typescript';
|
||||
|
||||
import {isClassMetadata, MetadataCollector} from '../../src/metadata/index';
|
||||
import {MetadataCache} from '../../src/transformers/metadata_cache';
|
||||
import {PartialModuleMetadataTransformer} from '../../src/transformers/r3_metadata_transform';
|
||||
|
||||
describe('r3_transform_spec', () => {
|
||||
it('should add a static method to collected metadata', () => {
|
||||
const fileName = '/some/directory/someFileName.ts';
|
||||
const className = 'SomeClass';
|
||||
const newFieldName = 'newStaticField';
|
||||
const source = `
|
||||
export class ${className} {
|
||||
myMethod(): void {}
|
||||
}
|
||||
`;
|
||||
|
||||
const sourceFile =
|
||||
ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, /* setParentNodes */ true);
|
||||
const partialModule: PartialModule = {
|
||||
fileName,
|
||||
statements: [new ClassStmt(
|
||||
className, /* parent */ null, /* fields */[new ClassField(
|
||||
/* name */ newFieldName, /* type */ null, /* modifiers */[StmtModifier.Static])],
|
||||
/* getters */[],
|
||||
/* constructorMethod */ new ClassMethod(/* name */ null, /* params */[], /* body */[]),
|
||||
/* methods */[])]
|
||||
};
|
||||
|
||||
const cache = new MetadataCache(
|
||||
new MetadataCollector(), /* strict */ true,
|
||||
[new PartialModuleMetadataTransformer([partialModule])]);
|
||||
const metadata = cache.getMetadata(sourceFile);
|
||||
expect(metadata).toBeDefined('Expected metadata from test source file');
|
||||
if (metadata) {
|
||||
const classData = metadata.metadata[className];
|
||||
expect(classData && isClassMetadata(classData))
|
||||
.toBeDefined(`Expected metadata to contain data for "${className}"`);
|
||||
if (classData && isClassMetadata(classData)) {
|
||||
const statics = classData.statics;
|
||||
expect(statics).toBeDefined(`Expected "${className}" metadata to contain statics`);
|
||||
if (statics) {
|
||||
expect(statics[newFieldName]).toEqual({}, 'Expected new field to recorded as a function');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -1,164 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 {PartialModule} from '@angular/compiler';
|
||||
import * as o from '@angular/compiler/src/output/output_ast';
|
||||
import ts from 'typescript';
|
||||
|
||||
import {getAngularClassTransformerFactory} from '../../src/transformers/r3_transform';
|
||||
import {Directory, MockAotContext, MockCompilerHost} from '../mocks';
|
||||
|
||||
const someGenFilePath = '/somePackage/someGenFile';
|
||||
const someGenFileName = someGenFilePath + '.ts';
|
||||
|
||||
describe('r3_transform_spec', () => {
|
||||
let context: MockAotContext;
|
||||
let host: MockCompilerHost;
|
||||
|
||||
beforeEach(() => {
|
||||
context = new MockAotContext('/', FILES);
|
||||
host = new MockCompilerHost(context);
|
||||
});
|
||||
|
||||
it('should be able to generate a simple identity function', () => {
|
||||
expect(emitStaticMethod(new o.ReturnStatement(o.variable('v')), ['v']))
|
||||
.toContain('static someMethod(v) { return v; }');
|
||||
});
|
||||
|
||||
it('should be able to generate a static field declaration', () => {
|
||||
expect(emitStaticField(o.literal(10))).toContain('SomeClass.someField = 10');
|
||||
});
|
||||
|
||||
it('should be able to import a symbol', () => {
|
||||
expect(emitStaticMethod(new o.ReturnStatement(
|
||||
o.importExpr(new o.ExternalReference('@angular/core', 'Component')))))
|
||||
.toContain('static someMethod() { return i0.Component; } }');
|
||||
});
|
||||
|
||||
it('should be able to modify multiple classes in the same module', () => {
|
||||
const result = emit(getAngularClassTransformerFactory(
|
||||
[{
|
||||
fileName: someGenFileName,
|
||||
statements: [
|
||||
classMethod(new o.ReturnStatement(o.variable('v')), ['v'], 'someMethod', 'SomeClass'),
|
||||
classMethod(
|
||||
new o.ReturnStatement(o.variable('v')), ['v'], 'someOtherMethod', 'SomeOtherClass')
|
||||
]
|
||||
}],
|
||||
false));
|
||||
expect(result).toContain('static someMethod(v) { return v; }');
|
||||
expect(result).toContain('static someOtherMethod(v) { return v; }');
|
||||
});
|
||||
|
||||
it('should insert imports after existing imports', () => {
|
||||
context = context.override({
|
||||
somePackage: {
|
||||
'someGenFile.ts': `
|
||||
import {Component} from '@angular/core';
|
||||
|
||||
@Component({selector: 'some-class', template: 'hello!'})
|
||||
export class SomeClass {}
|
||||
|
||||
export class SomeOtherClass {}
|
||||
`
|
||||
}
|
||||
});
|
||||
host = new MockCompilerHost(context);
|
||||
|
||||
expect(emitStaticMethod(new o.ReturnStatement(
|
||||
o.importExpr(new o.ExternalReference('@angular/core', 'Component')))))
|
||||
.toContain('const core_1 = require("@angular/core"); const i0 = require("@angular/core");');
|
||||
});
|
||||
|
||||
function emit(factory: ts.TransformerFactory<ts.SourceFile>): string {
|
||||
let result: string = '';
|
||||
const program = ts.createProgram(
|
||||
[someGenFileName], {module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2017}, host);
|
||||
const moduleSourceFile = program.getSourceFile(someGenFileName);
|
||||
const transformers: ts.CustomTransformers = {before: [factory]};
|
||||
const emitResult = program.emit(
|
||||
moduleSourceFile, (fileName, data, writeByteOrderMark, onError, sourceFiles) => {
|
||||
if (fileName.startsWith(someGenFilePath)) {
|
||||
result = data;
|
||||
}
|
||||
}, undefined, undefined, transformers);
|
||||
return normalizeResult(result);
|
||||
}
|
||||
|
||||
function emitStaticMethod(
|
||||
stmt: o.Statement|o.Statement[], parameters: string[] = [], methodName: string = 'someMethod',
|
||||
className: string = 'SomeClass'): string {
|
||||
const module: PartialModule = {
|
||||
fileName: someGenFileName,
|
||||
statements: [classMethod(stmt, parameters, methodName, className)]
|
||||
};
|
||||
return emit(getAngularClassTransformerFactory([module], false));
|
||||
}
|
||||
|
||||
function emitStaticField(
|
||||
initializer: o.Expression, fieldName: string = 'someField',
|
||||
className: string = 'SomeClass'): string {
|
||||
const module: PartialModule = {
|
||||
fileName: someGenFileName,
|
||||
statements: [classField(initializer, fieldName, className)]
|
||||
};
|
||||
return emit(getAngularClassTransformerFactory([module], false));
|
||||
}
|
||||
});
|
||||
|
||||
const FILES: Directory = {
|
||||
somePackage: {
|
||||
'someGenFile.ts': `
|
||||
|
||||
export class SomeClass {}
|
||||
|
||||
export class SomeOtherClass {}
|
||||
`
|
||||
}
|
||||
};
|
||||
|
||||
function classMethod(
|
||||
stmt: o.Statement|o.Statement[], parameters: string[] = [], methodName: string = 'someMethod',
|
||||
className: string = 'SomeClass'): o.ClassStmt {
|
||||
const statements = Array.isArray(stmt) ? stmt : [stmt];
|
||||
return new o.ClassStmt(
|
||||
/* name */ className,
|
||||
/* parent */ null,
|
||||
/* fields */[],
|
||||
/* getters */[],
|
||||
/* constructorMethod */ new o.ClassMethod(null, [], []),
|
||||
/* methods */[new o.ClassMethod(
|
||||
methodName, parameters.map(name => new o.FnParam(name)), statements, null,
|
||||
[o.StmtModifier.Static])]);
|
||||
}
|
||||
|
||||
function classField(
|
||||
initializer: o.Expression, fieldName: string = 'someField',
|
||||
className: string = 'SomeClass'): o.ClassStmt {
|
||||
return new o.ClassStmt(
|
||||
/* name */ className,
|
||||
/* parent */ null,
|
||||
/* fields */[new o.ClassField(fieldName, null, [o.StmtModifier.Static], initializer)],
|
||||
/* getters */[],
|
||||
/* constructorMethod */ new o.ClassMethod(null, [], []),
|
||||
/* methods */[]);
|
||||
}
|
||||
|
||||
function normalizeResult(result: string): string {
|
||||
// Remove TypeScript prefixes
|
||||
// Remove new lines
|
||||
// Squish adjacent spaces
|
||||
// Remove prefix and postfix spaces
|
||||
return result.replace('"use strict";', ' ')
|
||||
.replace('exports.__esModule = true;', ' ')
|
||||
.replace('Object.defineProperty(exports, "__esModule", { value: true });', ' ')
|
||||
.replace(/\n/g, ' ')
|
||||
.replace(/ +/g, ' ')
|
||||
.replace(/^ /g, '')
|
||||
.replace(/ $/g, '');
|
||||
}
|
||||
|
|
@ -7,39 +7,18 @@ NODE_ONLY = [
|
|||
"aot/**/*.ts",
|
||||
]
|
||||
|
||||
UTILS = [
|
||||
"aot/test_util.ts",
|
||||
]
|
||||
|
||||
circular_dependency_test(
|
||||
name = "circular_deps_test",
|
||||
entry_point = "angular/packages/compiler/index.js",
|
||||
deps = ["//packages/compiler"],
|
||||
)
|
||||
|
||||
ts_library(
|
||||
name = "test_utils",
|
||||
testonly = True,
|
||||
srcs = UTILS,
|
||||
visibility = [
|
||||
"//packages/compiler-cli/test:__subpackages__",
|
||||
"//packages/compiler/test:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
"//packages:types",
|
||||
"//packages/compiler",
|
||||
"//packages/compiler-cli",
|
||||
"//packages/compiler-cli/src/ngtsc/testing",
|
||||
"@npm//typescript",
|
||||
],
|
||||
)
|
||||
|
||||
ts_library(
|
||||
name = "test_lib",
|
||||
testonly = True,
|
||||
srcs = glob(
|
||||
["**/*.ts"],
|
||||
exclude = NODE_ONLY + UTILS,
|
||||
exclude = NODE_ONLY,
|
||||
),
|
||||
deps = [
|
||||
"//packages:types",
|
||||
|
|
@ -47,13 +26,14 @@ ts_library(
|
|||
"//packages/compiler",
|
||||
"//packages/compiler/test/expression_parser/utils",
|
||||
"//packages/compiler/test/ml_parser/util",
|
||||
"//packages/compiler/testing",
|
||||
"//packages/core",
|
||||
"//packages/core/src/compiler",
|
||||
"//packages/core/testing",
|
||||
"//packages/platform-browser",
|
||||
"//packages/platform-browser-dynamic",
|
||||
"//packages/platform-browser/testing",
|
||||
"@npm//base64-js",
|
||||
"@npm//source-map",
|
||||
],
|
||||
)
|
||||
|
||||
|
|
@ -62,15 +42,11 @@ ts_library(
|
|||
testonly = True,
|
||||
srcs = glob(
|
||||
NODE_ONLY,
|
||||
exclude = UTILS,
|
||||
),
|
||||
deps = [
|
||||
":test_lib",
|
||||
":test_utils",
|
||||
"//packages/compiler",
|
||||
"//packages/compiler-cli",
|
||||
"//packages/compiler/test/expression_parser/utils",
|
||||
"//packages/compiler/testing",
|
||||
"//packages/core",
|
||||
"@npm//typescript",
|
||||
],
|
||||
|
|
@ -79,14 +55,6 @@ ts_library(
|
|||
jasmine_node_test(
|
||||
name = "test",
|
||||
bootstrap = ["//tools/testing:node_es5"],
|
||||
data = [
|
||||
"@npm//@angular/animations-12",
|
||||
"@npm//@angular/core-12",
|
||||
],
|
||||
tags = [
|
||||
# Disabled as these tests pertain to the old ngc compilation and are not relevant in Ivy.
|
||||
"view-engine-only",
|
||||
],
|
||||
deps = [
|
||||
":test_lib",
|
||||
":test_node_only_lib",
|
||||
|
|
@ -97,10 +65,6 @@ jasmine_node_test(
|
|||
|
||||
karma_web_test_suite(
|
||||
name = "test_web",
|
||||
tags = [
|
||||
# Disabled as these tests pertain to the old ngc compilation and are not relevant in Ivy.
|
||||
"view-engine-only",
|
||||
],
|
||||
deps = [
|
||||
":test_lib",
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
Tests in this directory are excluded from running in the browser and only run in node.
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,463 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 {AotCompiler, AotCompilerHost, AotCompilerOptions, CompileSummaryKind, GeneratedFile, toTypeScript} from '@angular/compiler';
|
||||
|
||||
import {compile, MockDirectory, setup} from './test_util';
|
||||
|
||||
describe('aot summaries for jit', () => {
|
||||
let angularFiles = setup();
|
||||
let angularSummaryFiles: MockDirectory;
|
||||
|
||||
beforeEach(() => {
|
||||
angularSummaryFiles = compile(angularFiles, {useSummaries: false, emit: true}).outDir;
|
||||
});
|
||||
|
||||
function compileApp(
|
||||
rootDir: MockDirectory, options: {useSummaries?: boolean}&AotCompilerOptions = {}):
|
||||
{genFiles: GeneratedFile[], outDir: MockDirectory} {
|
||||
return compile(
|
||||
[rootDir, options.useSummaries ? angularSummaryFiles : angularFiles],
|
||||
{...options, enableSummariesForJit: true});
|
||||
}
|
||||
|
||||
it('should create @Injectable summaries', () => {
|
||||
const appDir = {
|
||||
'app.module.ts': `
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
export class Dep {}
|
||||
|
||||
@Injectable()
|
||||
export class MyService {
|
||||
constructor(d: Dep) {}
|
||||
}
|
||||
`
|
||||
};
|
||||
const rootDir = {'app': appDir};
|
||||
|
||||
const genFile =
|
||||
compileApp(rootDir).genFiles.find(f => f.genFileUrl === '/app/app.module.ngsummary.ts')!;
|
||||
const genSource = toTypeScript(genFile);
|
||||
|
||||
expect(genSource).toContain(`import * as i0 from '/app/app.module'`);
|
||||
expect(genSource).toContain('export function MyServiceNgSummary()');
|
||||
// Note: CompileSummaryKind.Injectable = 3
|
||||
expect(genSource).toMatch(/summaryKind:3,\s*type:\{\s*reference:i0.MyService/);
|
||||
expect(genSource).toContain('token:{identifier:{reference:i0.Dep}}');
|
||||
});
|
||||
|
||||
it('should create @Pipe summaries', () => {
|
||||
const appDir = {
|
||||
'app.module.ts': `
|
||||
import { Pipe, NgModule } from '@angular/core';
|
||||
|
||||
export class Dep {}
|
||||
|
||||
@Pipe({name: 'myPipe'})
|
||||
export class MyPipe {
|
||||
constructor(d: Dep) {}
|
||||
}
|
||||
|
||||
@NgModule({declarations: [MyPipe]})
|
||||
export class MyModule {}
|
||||
`
|
||||
};
|
||||
const rootDir = {'app': appDir};
|
||||
|
||||
const genFile =
|
||||
compileApp(rootDir).genFiles.find(f => f.genFileUrl === '/app/app.module.ngsummary.ts')!;
|
||||
const genSource = toTypeScript(genFile);
|
||||
|
||||
expect(genSource).toContain(`import * as i0 from '/app/app.module'`);
|
||||
expect(genSource).toContain('export function MyPipeNgSummary()');
|
||||
// Note: CompileSummaryKind.Pipe = 1
|
||||
expect(genSource).toMatch(/summaryKind:0,\s*type:\{\s*reference:i0.MyPipe/);
|
||||
expect(genSource).toContain('token:{identifier:{reference:i0.Dep}}');
|
||||
});
|
||||
|
||||
it('should create @Directive summaries', () => {
|
||||
const appDir = {
|
||||
'app.module.ts': `
|
||||
import { Directive, NgModule } from '@angular/core';
|
||||
|
||||
export class Dep {}
|
||||
|
||||
@Directive({selector: '[myDir]'})
|
||||
export class MyDirective {
|
||||
constructor(a: Dep) {}
|
||||
}
|
||||
|
||||
@NgModule({declarations: [MyDirective]})
|
||||
export class MyModule {}
|
||||
`
|
||||
};
|
||||
const rootDir = {'app': appDir};
|
||||
|
||||
const genFile =
|
||||
compileApp(rootDir).genFiles.find(f => f.genFileUrl === '/app/app.module.ngsummary.ts')!;
|
||||
const genSource = toTypeScript(genFile);
|
||||
|
||||
expect(genSource).toContain(`import * as i0 from '/app/app.module'`);
|
||||
expect(genSource).toContain('export function MyDirectiveNgSummary()');
|
||||
// Note: CompileSummaryKind.Directive = 1
|
||||
expect(genSource).toMatch(/summaryKind:1,\s*type:\{\s*reference:i0.MyDirective/);
|
||||
expect(genSource).toContain('token:{identifier:{reference:i0.Dep}}');
|
||||
});
|
||||
|
||||
it('should create @NgModule summaries', () => {
|
||||
const appDir = {
|
||||
'app.module.ts': `
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
export class Dep {}
|
||||
|
||||
@NgModule()
|
||||
export class MyModule {
|
||||
constructor(d: Dep) {}
|
||||
}
|
||||
`
|
||||
};
|
||||
const rootDir = {'app': appDir};
|
||||
|
||||
const genFile =
|
||||
compileApp(rootDir).genFiles.find(f => f.genFileUrl === '/app/app.module.ngsummary.ts')!;
|
||||
const genSource = toTypeScript(genFile);
|
||||
|
||||
expect(genSource).toContain(`import * as i0 from '/app/app.module'`);
|
||||
expect(genSource).toContain('export function MyModuleNgSummary()');
|
||||
// Note: CompileSummaryKind.NgModule = 2
|
||||
expect(genSource).toMatch(/summaryKind:2,\s*type:\{\s*reference:i0.MyModule/);
|
||||
expect(genSource).toContain('token:{identifier:{reference:i0.Dep}}');
|
||||
});
|
||||
|
||||
it('should embed useClass provider summaries in @Directive summaries', () => {
|
||||
const appDir = {
|
||||
'app.service.ts': `
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
export class Dep {}
|
||||
|
||||
@Injectable()
|
||||
export class MyService {
|
||||
constructor(d: Dep) {}
|
||||
}
|
||||
`,
|
||||
'app.module.ts': `
|
||||
import { Directive, NgModule } from '@angular/core';
|
||||
import { MyService } from './app.service';
|
||||
|
||||
@Directive({
|
||||
selector: '[myDir]',
|
||||
providers: [MyService]
|
||||
})
|
||||
export class MyDirective {}
|
||||
|
||||
@NgModule({declarations: [MyDirective]})
|
||||
export class MyModule {}
|
||||
`
|
||||
};
|
||||
const rootDir = {'app': appDir};
|
||||
|
||||
const genFile =
|
||||
compileApp(rootDir).genFiles.find(f => f.genFileUrl === '/app/app.module.ngsummary.ts')!;
|
||||
const genSource = toTypeScript(genFile);
|
||||
|
||||
expect(genSource).toMatch(/useClass:\{\s*reference:i1.MyService/);
|
||||
// Note: CompileSummaryKind.Injectable = 3
|
||||
expect(genSource).toMatch(/summaryKind:3,\s*type:\{\s*reference:i1.MyService/);
|
||||
expect(genSource).toContain('token:{identifier:{reference:i1.Dep}}');
|
||||
});
|
||||
|
||||
it('should embed useClass provider summaries into @NgModule summaries', () => {
|
||||
const appDir = {
|
||||
'app.service.ts': `
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
export class Dep {}
|
||||
|
||||
@Injectable()
|
||||
export class MyService {
|
||||
constructor(d: Dep) {}
|
||||
}
|
||||
`,
|
||||
'app.module.ts': `
|
||||
import { NgModule } from '@angular/core';
|
||||
import { MyService } from './app.service';
|
||||
|
||||
@NgModule({
|
||||
providers: [MyService]
|
||||
})
|
||||
export class MyModule {}
|
||||
`
|
||||
};
|
||||
const rootDir = {'app': appDir};
|
||||
|
||||
const genFile =
|
||||
compileApp(rootDir).genFiles.find(f => f.genFileUrl === '/app/app.module.ngsummary.ts')!;
|
||||
const genSource = toTypeScript(genFile);
|
||||
|
||||
expect(genSource).toMatch(/useClass:\{\s*reference:i1.MyService/);
|
||||
// Note: CompileSummaryKind.Injectable = 3
|
||||
expect(genSource).toMatch(/summaryKind:3,\s*type:\{\s*reference:i1.MyService/);
|
||||
expect(genSource).toContain('token:{identifier:{reference:i1.Dep}}');
|
||||
});
|
||||
|
||||
it('should reference declared @Directive and @Pipe summaries in @NgModule summaries', () => {
|
||||
const appDir = {
|
||||
'app.module.ts': `
|
||||
import { Directive, Pipe, NgModule } from '@angular/core';
|
||||
|
||||
@Directive({selector: '[myDir]'})
|
||||
export class MyDirective {}
|
||||
|
||||
@Pipe({name: 'myPipe'})
|
||||
export class MyPipe {}
|
||||
|
||||
@NgModule({declarations: [MyDirective, MyPipe]})
|
||||
export class MyModule {}
|
||||
`
|
||||
};
|
||||
const rootDir = {'app': appDir};
|
||||
|
||||
const genFile =
|
||||
compileApp(rootDir).genFiles.find(f => f.genFileUrl === '/app/app.module.ngsummary.ts')!;
|
||||
const genSource = toTypeScript(genFile);
|
||||
|
||||
expect(genSource).toMatch(
|
||||
/export function MyModuleNgSummary()[^;]*,\s*MyDirectiveNgSummary,\s*MyPipeNgSummary\s*\]\s*;/);
|
||||
});
|
||||
|
||||
it('should reference imported @NgModule summaries in @NgModule summaries', () => {
|
||||
const appDir = {
|
||||
'app.module.ts': `
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
@NgModule()
|
||||
export class MyImportedModule {}
|
||||
|
||||
@NgModule({imports: [MyImportedModule]})
|
||||
export class MyModule {}
|
||||
`
|
||||
};
|
||||
const rootDir = {'app': appDir};
|
||||
|
||||
const genFile =
|
||||
compileApp(rootDir).genFiles.find(f => f.genFileUrl === '/app/app.module.ngsummary.ts')!;
|
||||
const genSource = toTypeScript(genFile);
|
||||
|
||||
expect(genSource).toMatch(
|
||||
/export function MyModuleNgSummary()[^;]*,\s*MyImportedModuleNgSummary\s*\]\s*;/);
|
||||
});
|
||||
|
||||
it('should create and use reexports for imported NgModules ' +
|
||||
'across compilation units if symbol re-exports are enabled',
|
||||
() => {
|
||||
const lib1In = {
|
||||
'lib1': {
|
||||
'module.ts': `
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
@NgModule()
|
||||
export class Lib1Module {}
|
||||
`,
|
||||
'reexport.ts': `
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
@NgModule()
|
||||
export class ReexportModule {}
|
||||
|
||||
export const reexports: any[] = [ ReexportModule ];
|
||||
`,
|
||||
}
|
||||
};
|
||||
const {outDir: lib2In, genFiles: lib1Gen} = compileApp(lib1In, {
|
||||
useSummaries: true,
|
||||
createExternalSymbolFactoryReexports: true,
|
||||
});
|
||||
|
||||
lib2In['lib2'] = {
|
||||
'module.ts': `
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Lib1Module } from '../lib1/module';
|
||||
|
||||
@NgModule({
|
||||
imports: [Lib1Module]
|
||||
})
|
||||
export class Lib2Module {}
|
||||
`,
|
||||
'reexport.ts': `
|
||||
import { reexports as reexports_lib1 } from '../lib1/reexport';
|
||||
export const reexports: any[] = [ reexports_lib1 ];
|
||||
`,
|
||||
};
|
||||
const {outDir: lib3In, genFiles: lib2Gen} = compileApp(lib2In, {
|
||||
useSummaries: true,
|
||||
createExternalSymbolFactoryReexports: true,
|
||||
});
|
||||
|
||||
const lib2ModuleNgSummary = lib2Gen.find(f => f.genFileUrl === '/lib2/module.ngsummary.ts')!;
|
||||
const lib2ReexportNgSummary =
|
||||
lib2Gen.find(f => f.genFileUrl === '/lib2/reexport.ngsummary.ts')!;
|
||||
|
||||
// ngsummaries should add reexports for imported NgModules from a direct dependency
|
||||
expect(toTypeScript(lib2ModuleNgSummary))
|
||||
.toContain(
|
||||
`export {Lib1ModuleNgSummary as Lib1Module_1NgSummary} from '/lib1/module.ngsummary'`);
|
||||
// ngsummaries should add reexports for reexported values from a direct dependency
|
||||
expect(toTypeScript(lib2ReexportNgSummary))
|
||||
.toContain(
|
||||
`export {ReexportModuleNgSummary as ReexportModule_2NgSummary} from '/lib1/reexport.ngsummary'`);
|
||||
|
||||
lib3In['lib3'] = {
|
||||
'module.ts': `
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Lib2Module } from '../lib2/module';
|
||||
import { reexports } from '../lib2/reexport';
|
||||
|
||||
@NgModule({
|
||||
imports: [Lib2Module, reexports]
|
||||
})
|
||||
export class Lib3Module {}
|
||||
`,
|
||||
'reexport.ts': `
|
||||
import { reexports as reexports_lib2 } from '../lib2/reexport';
|
||||
export const reexports: any[] = [ reexports_lib2 ];
|
||||
`,
|
||||
};
|
||||
|
||||
const lib3Gen = compileApp(lib3In, {
|
||||
useSummaries: true,
|
||||
createExternalSymbolFactoryReexports: true
|
||||
}).genFiles;
|
||||
const lib3ModuleNgSummary = lib3Gen.find(f => f.genFileUrl === '/lib3/module.ngsummary.ts')!;
|
||||
const lib3ReexportNgSummary =
|
||||
lib3Gen.find(f => f.genFileUrl === '/lib3/reexport.ngsummary.ts')!;
|
||||
|
||||
// ngsummary.ts files should use the reexported values from direct and deep deps
|
||||
const lib3ModuleNgSummarySource = toTypeScript(lib3ModuleNgSummary);
|
||||
expect(lib3ModuleNgSummarySource).toContain(`import * as i4 from '/lib2/module.ngsummary'`);
|
||||
expect(lib3ModuleNgSummarySource)
|
||||
.toContain(`import * as i5 from '/lib2/reexport.ngsummary'`);
|
||||
expect(lib3ModuleNgSummarySource)
|
||||
.toMatch(
|
||||
/export function Lib3ModuleNgSummary()[^;]*,\s*i4.Lib1Module_1NgSummary,\s*i4.Lib2ModuleNgSummary,\s*i5.ReexportModule_2NgSummary\s*\]\s*;/);
|
||||
|
||||
// ngsummaries should add reexports for imported NgModules from a deep dependency
|
||||
expect(lib3ModuleNgSummarySource)
|
||||
.toContain(
|
||||
`export {Lib1Module_1NgSummary as Lib1Module_1NgSummary,Lib2ModuleNgSummary as Lib2Module_2NgSummary} from '/lib2/module.ngsummary'`);
|
||||
// ngsummaries should add reexports for reexported values from a deep dependency
|
||||
expect(toTypeScript(lib3ReexportNgSummary))
|
||||
.toContain(
|
||||
`export {ReexportModule_2NgSummary as ReexportModule_3NgSummary} from '/lib2/reexport.ngsummary'`);
|
||||
});
|
||||
|
||||
it('should not create reexports for external symbols imported by NgModules', () => {
|
||||
const lib1In = {
|
||||
'lib1': {
|
||||
'module.ts': `
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
@NgModule()
|
||||
export class Lib1Module {}`,
|
||||
'reexport.ts': `
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
@NgModule()
|
||||
export class ReexportModule {}
|
||||
|
||||
export const reexports: any[] = [ ReexportModule ];`,
|
||||
}
|
||||
};
|
||||
const {outDir: lib1Out} = compileApp(lib1In, {useSummaries: true});
|
||||
|
||||
const lib2In = {
|
||||
...lib1Out,
|
||||
'lib2': {
|
||||
'module.ts': `
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Lib1Module } from '../lib1/module';
|
||||
|
||||
@NgModule({
|
||||
imports: [Lib1Module]
|
||||
})
|
||||
export class Lib2Module {}`,
|
||||
'reexport.ts': `
|
||||
import { reexports as reexports_lib1 } from '../lib1/reexport';
|
||||
export const reexports: any[] = [ reexports_lib1 ];`,
|
||||
},
|
||||
};
|
||||
|
||||
const {outDir: lib2Out, genFiles: lib2Gen} = compileApp(lib2In, {useSummaries: true});
|
||||
|
||||
const lib2ModuleNgSummary = lib2Gen.find(f => f.genFileUrl === '/lib2/module.ngsummary.ts')!;
|
||||
const lib2ReexportNgSummary =
|
||||
lib2Gen.find(f => f.genFileUrl === '/lib2/reexport.ngsummary.ts')!;
|
||||
|
||||
// ngsummaries should not add reexports by default for imported NgModules from a direct
|
||||
// dependency
|
||||
expect(toTypeScript(lib2ModuleNgSummary))
|
||||
.toContain(
|
||||
`export {Lib1ModuleNgSummary as Lib1ModuleNgSummary} from '/lib1/module.ngsummary'`);
|
||||
// ngsummaries should not add reexports by default for reexported values from a direct
|
||||
// dependency.
|
||||
expect(toTypeScript(lib2ReexportNgSummary))
|
||||
.toContain(
|
||||
`export {ReexportModuleNgSummary as ReexportModuleNgSummary} from '/lib1/reexport.ngsummary'`);
|
||||
|
||||
const lib3In = {
|
||||
...lib1Out,
|
||||
...lib2Out,
|
||||
'lib3': {
|
||||
'module.ts': `
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Lib2Module } from '../lib2/module';
|
||||
import { reexports } from '../lib2/reexport';
|
||||
|
||||
@NgModule({
|
||||
imports: [Lib2Module, reexports]
|
||||
})
|
||||
export class Lib3Module {}
|
||||
`,
|
||||
'reexport.ts': `
|
||||
import { reexports as reexports_lib2 } from '../lib2/reexport';
|
||||
export const reexports: any[] = [ reexports_lib2 ];
|
||||
`,
|
||||
},
|
||||
};
|
||||
|
||||
const lib3Gen = compileApp(lib3In, {useSummaries: true}).genFiles;
|
||||
const lib3ModuleNgSummary = lib3Gen.find(f => f.genFileUrl === '/lib3/module.ngsummary.ts')!;
|
||||
const lib3ReexportNgSummary =
|
||||
lib3Gen.find(f => f.genFileUrl === '/lib3/reexport.ngsummary.ts')!;
|
||||
|
||||
// ngsummary.ts files should use the external symbols which are manually re-exported from
|
||||
// "lib2" from their original symbol location. With re-exported external symbols this would
|
||||
// be different because there would be no *direct* dependency on "lib1" at all.
|
||||
const lib3ModuleNgSummarySource = toTypeScript(lib3ModuleNgSummary);
|
||||
expect(lib3ModuleNgSummarySource).toContain(`import * as i1 from '/lib1/module';`);
|
||||
expect(lib3ModuleNgSummarySource).toContain(`import * as i3 from '/lib1/reexport';`);
|
||||
expect(lib3ModuleNgSummarySource)
|
||||
.toMatch(/export function Lib3ModuleNgSummary\(\).*modules:\[{reference:i1\.Lib1Module,/s);
|
||||
expect(lib3ModuleNgSummarySource)
|
||||
.toMatch(/export function Lib3ModuleNgSummary\(\).*reference:i3\.ReexportModule,/s);
|
||||
// ngsummaries should re-export all used summaries directly. With external symbol re-exports
|
||||
// enabled, the "lib1" summaries would be re-exported through "lib2" in order to avoid
|
||||
// a *direct* dependency on "lib1".
|
||||
expect(lib3ModuleNgSummarySource)
|
||||
.toContain(
|
||||
`export {Lib1ModuleNgSummary as Lib1ModuleNgSummary} from '/lib1/module.ngsummary';`);
|
||||
expect(lib3ModuleNgSummarySource)
|
||||
.toContain(
|
||||
`export {ReexportModuleNgSummary as ReexportModuleNgSummary} from '/lib1/reexport.ngsummary';`);
|
||||
expect(toTypeScript(lib3ReexportNgSummary))
|
||||
.toContain(
|
||||
`export {ReexportModuleNgSummary as ReexportModuleNgSummary} from '/lib1/reexport.ngsummary';`);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 {compile, expectNoDiagnostics, MockDirectory, setup} from './test_util';
|
||||
|
||||
describe('regressions', () => {
|
||||
let angularFiles = setup();
|
||||
|
||||
it('should compile components with empty templates', () => {
|
||||
const appDir = {
|
||||
'app.module.ts': `
|
||||
import { Component, NgModule } from '@angular/core';
|
||||
|
||||
@Component({template: ''})
|
||||
export class EmptyComp {}
|
||||
|
||||
@NgModule({declarations: [EmptyComp]})
|
||||
export class MyModule {}
|
||||
`
|
||||
};
|
||||
const rootDir = {'app': appDir};
|
||||
const {genFiles} = compile(
|
||||
[rootDir, angularFiles], {postCompile: expectNoDiagnostics},
|
||||
{noUnusedLocals: true, noUnusedParameters: true});
|
||||
expect(genFiles.find((f) => f.genFileUrl === '/app/app.module.ngfactory.ts')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,603 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 {StaticSymbol, StaticSymbolCache, StaticSymbolResolver, StaticSymbolResolverHost, Summary, SummaryResolver} from '@angular/compiler';
|
||||
import {CollectorOptions, METADATA_VERSION} from '@angular/compiler-cli';
|
||||
import {MetadataCollector} from '@angular/compiler-cli/src/metadata/collector';
|
||||
import ts from 'typescript';
|
||||
|
||||
// This matches .ts files but not .d.ts files.
|
||||
const TS_EXT = /(^.|(?!\.d)..)\.ts$/;
|
||||
|
||||
describe('StaticSymbolResolver', () => {
|
||||
let host: StaticSymbolResolverHost;
|
||||
let symbolResolver: StaticSymbolResolver;
|
||||
let symbolCache: StaticSymbolCache;
|
||||
|
||||
beforeEach(() => {
|
||||
symbolCache = new StaticSymbolCache();
|
||||
});
|
||||
|
||||
function init(
|
||||
testData: {[key: string]: any} = DEFAULT_TEST_DATA, summaries: Summary<StaticSymbol>[] = [],
|
||||
summaryImportAs: {symbol: StaticSymbol, importAs: StaticSymbol}[] = []) {
|
||||
host = new MockStaticSymbolResolverHost(testData);
|
||||
symbolResolver = new StaticSymbolResolver(
|
||||
host, symbolCache, new MockSummaryResolver(summaries, summaryImportAs));
|
||||
}
|
||||
|
||||
beforeEach(() => init());
|
||||
|
||||
it('should throw an exception for unsupported metadata versions', () => {
|
||||
expect(
|
||||
() => symbolResolver.resolveSymbol(
|
||||
symbolResolver.getSymbolByModule('src/version-error', 'e')))
|
||||
.toThrow(new Error(
|
||||
`Metadata version mismatch for module /tmp/src/version-error.d.ts, found version 100, expected ${
|
||||
METADATA_VERSION}`));
|
||||
});
|
||||
|
||||
it('should throw an exception for version 2 metadata', () => {
|
||||
expect(
|
||||
() => symbolResolver.resolveSymbol(
|
||||
symbolResolver.getSymbolByModule('src/version-2-error', 'e')))
|
||||
.toThrowError(
|
||||
'Unsupported metadata version 2 for module /tmp/src/version-2-error.d.ts. This module should be compiled with a newer version of ngc');
|
||||
});
|
||||
|
||||
it('should be produce the same symbol if asked twice', () => {
|
||||
const foo1 = symbolResolver.getStaticSymbol('main.ts', 'foo');
|
||||
const foo2 = symbolResolver.getStaticSymbol('main.ts', 'foo');
|
||||
expect(foo1).toBe(foo2);
|
||||
});
|
||||
|
||||
it('should be able to produce a symbol for a module with no file', () => {
|
||||
expect(symbolResolver.getStaticSymbol('angularjs', 'SomeAngularSymbol')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should be able to split the metadata per symbol', () => {
|
||||
init({
|
||||
'/tmp/src/test.ts': `
|
||||
export var a = 1;
|
||||
export var b = 2;
|
||||
`
|
||||
});
|
||||
expect(symbolResolver.resolveSymbol(symbolResolver.getStaticSymbol('/tmp/src/test.ts', 'a'))
|
||||
.metadata)
|
||||
.toBe(1);
|
||||
expect(symbolResolver.resolveSymbol(symbolResolver.getStaticSymbol('/tmp/src/test.ts', 'b'))
|
||||
.metadata)
|
||||
.toBe(2);
|
||||
});
|
||||
|
||||
it('should be able to resolve static symbols with members', () => {
|
||||
init({
|
||||
'/tmp/src/test.ts': `
|
||||
export {exportedObj} from './export';
|
||||
|
||||
export var obj = {a: 1};
|
||||
export class SomeClass {
|
||||
static someField = 2;
|
||||
}
|
||||
`,
|
||||
'/tmp/src/export.ts': `
|
||||
export var exportedObj = {};
|
||||
`,
|
||||
});
|
||||
expect(symbolResolver
|
||||
.resolveSymbol(symbolResolver.getStaticSymbol('/tmp/src/test.ts', 'obj', ['a']))
|
||||
.metadata)
|
||||
.toBe(1);
|
||||
expect(symbolResolver
|
||||
.resolveSymbol(
|
||||
symbolResolver.getStaticSymbol('/tmp/src/test.ts', 'SomeClass', ['someField']))
|
||||
.metadata)
|
||||
.toBe(2);
|
||||
expect(symbolResolver
|
||||
.resolveSymbol(symbolResolver.getStaticSymbol(
|
||||
'/tmp/src/test.ts', 'exportedObj', ['someMember']))
|
||||
.metadata)
|
||||
.toBe(symbolResolver.getStaticSymbol('/tmp/src/export.ts', 'exportedObj', ['someMember']));
|
||||
});
|
||||
|
||||
it('should not explore re-exports of the same module', () => {
|
||||
init({
|
||||
'/tmp/src/test.ts': `
|
||||
export * from './test';
|
||||
|
||||
export const testValue = 10;
|
||||
`,
|
||||
});
|
||||
|
||||
const symbols = symbolResolver.getSymbolsOf('/tmp/src/test.ts');
|
||||
expect(symbols).toEqual([symbolResolver.getStaticSymbol('/tmp/src/test.ts', 'testValue')]);
|
||||
});
|
||||
|
||||
it('should use summaries in resolveSymbol and prefer them over regular metadata', () => {
|
||||
const symbolA = symbolCache.get('/test.ts', 'a');
|
||||
const symbolB = symbolCache.get('/test.ts', 'b');
|
||||
const symbolC = symbolCache.get('/test.ts', 'c');
|
||||
init({'/test.ts': 'export var a = 2; export var b = 2; export var c = 2;'}, [
|
||||
{symbol: symbolA, metadata: 1},
|
||||
{symbol: symbolB, metadata: 1},
|
||||
]);
|
||||
// reading the metadata of a symbol without a summary first,
|
||||
// to test whether summaries are still preferred after this.
|
||||
expect(symbolResolver.resolveSymbol(symbolC).metadata).toBe(2);
|
||||
expect(symbolResolver.resolveSymbol(symbolA).metadata).toBe(1);
|
||||
expect(symbolResolver.resolveSymbol(symbolB).metadata).toBe(1);
|
||||
});
|
||||
|
||||
it('should be able to get all exported symbols of a file', () => {
|
||||
expect(symbolResolver.getSymbolsOf('/tmp/src/reexport/src/origin1.d.ts')).toEqual([
|
||||
symbolResolver.getStaticSymbol('/tmp/src/reexport/src/origin1.d.ts', 'One'),
|
||||
symbolResolver.getStaticSymbol('/tmp/src/reexport/src/origin1.d.ts', 'Two'),
|
||||
symbolResolver.getStaticSymbol('/tmp/src/reexport/src/origin1.d.ts', 'Three'),
|
||||
symbolResolver.getStaticSymbol('/tmp/src/reexport/src/origin1.d.ts', 'Six'),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should be able to get all reexported symbols of a file', () => {
|
||||
expect(symbolResolver.getSymbolsOf('/tmp/src/reexport/reexport.d.ts')).toEqual([
|
||||
symbolResolver.getStaticSymbol('/tmp/src/reexport/reexport.d.ts', 'One'),
|
||||
symbolResolver.getStaticSymbol('/tmp/src/reexport/reexport.d.ts', 'Two'),
|
||||
symbolResolver.getStaticSymbol('/tmp/src/reexport/reexport.d.ts', 'Four'),
|
||||
symbolResolver.getStaticSymbol('/tmp/src/reexport/reexport.d.ts', 'Six'),
|
||||
symbolResolver.getStaticSymbol('/tmp/src/reexport/reexport.d.ts', 'Five'),
|
||||
symbolResolver.getStaticSymbol('/tmp/src/reexport/reexport.d.ts', 'Thirty'),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should read the exported symbols of a file from the summary and ignore exports in the source',
|
||||
() => {
|
||||
init(
|
||||
{'/test.ts': 'export var b = 2'},
|
||||
[{symbol: symbolCache.get('/test.ts', 'a'), metadata: 1}]);
|
||||
expect(symbolResolver.getSymbolsOf('/test.ts')).toEqual([symbolCache.get('/test.ts', 'a')]);
|
||||
});
|
||||
|
||||
describe('importAs', () => {
|
||||
it('should calculate importAs relationship for non source files without summaries', () => {
|
||||
init(
|
||||
{
|
||||
'/test.d.ts': [{
|
||||
'__symbolic': 'module',
|
||||
'version': METADATA_VERSION,
|
||||
'metadata': {
|
||||
'a': {'__symbolic': 'reference', 'name': 'b', 'module': './test2'},
|
||||
}
|
||||
}],
|
||||
'/test2.d.ts': [{
|
||||
'__symbolic': 'module',
|
||||
'version': METADATA_VERSION,
|
||||
'metadata': {
|
||||
'b': {'__symbolic': 'reference', 'name': 'c', 'module': './test3'},
|
||||
}
|
||||
}]
|
||||
},
|
||||
[]);
|
||||
symbolResolver.getSymbolsOf('/test.d.ts');
|
||||
symbolResolver.getSymbolsOf('/test2.d.ts');
|
||||
|
||||
expect(symbolResolver.getImportAs(symbolCache.get('/test2.d.ts', 'b')))
|
||||
.toBe(symbolCache.get('/test.d.ts', 'a'));
|
||||
expect(symbolResolver.getImportAs(symbolCache.get('/test3.d.ts', 'c')))
|
||||
.toBe(symbolCache.get('/test.d.ts', 'a'));
|
||||
});
|
||||
|
||||
it('should calculate importAs relationship for non source files with summaries', () => {
|
||||
init(
|
||||
{
|
||||
'/test.ts': `
|
||||
export {a} from './test2';
|
||||
`
|
||||
},
|
||||
[], [{
|
||||
symbol: symbolCache.get('/test2.d.ts', 'a'),
|
||||
importAs: symbolCache.get('/test3.d.ts', 'b')
|
||||
}]);
|
||||
symbolResolver.getSymbolsOf('/test.ts');
|
||||
|
||||
expect(symbolResolver.getImportAs(symbolCache.get('/test2.d.ts', 'a')))
|
||||
.toBe(symbolCache.get('/test3.d.ts', 'b'));
|
||||
});
|
||||
|
||||
it('should ignore summaries for inputAs if requested', () => {
|
||||
init(
|
||||
{
|
||||
'/test.ts': `
|
||||
export {a} from './test2';
|
||||
`
|
||||
},
|
||||
[], [{
|
||||
symbol: symbolCache.get('/test2.d.ts', 'a'),
|
||||
importAs: symbolCache.get('/test3.d.ts', 'b')
|
||||
}]);
|
||||
|
||||
symbolResolver.getSymbolsOf('/test.ts');
|
||||
|
||||
expect(
|
||||
symbolResolver.getImportAs(symbolCache.get('/test2.d.ts', 'a'), /* useSummaries */ false))
|
||||
.toBeUndefined();
|
||||
});
|
||||
|
||||
it('should calculate importAs for symbols with members based on importAs for symbols without',
|
||||
() => {
|
||||
init(
|
||||
{
|
||||
'/test.ts': `
|
||||
export {a} from './test2';
|
||||
`
|
||||
},
|
||||
[], [{
|
||||
symbol: symbolCache.get('/test2.d.ts', 'a'),
|
||||
importAs: symbolCache.get('/test3.d.ts', 'b')
|
||||
}]);
|
||||
symbolResolver.getSymbolsOf('/test.ts');
|
||||
|
||||
expect(symbolResolver.getImportAs(symbolCache.get('/test2.d.ts', 'a', ['someMember'])))
|
||||
.toBe(symbolCache.get('/test3.d.ts', 'b', ['someMember']));
|
||||
});
|
||||
});
|
||||
|
||||
it('should replace references by StaticSymbols', () => {
|
||||
init({
|
||||
'/test.ts': `
|
||||
import {b, y} from './test2';
|
||||
export var a = b;
|
||||
export var x = [y];
|
||||
|
||||
export function simpleFn(fnArg) {
|
||||
return [a, y, fnArg];
|
||||
}
|
||||
`,
|
||||
'/test2.ts': `
|
||||
export var b;
|
||||
export var y;
|
||||
`
|
||||
});
|
||||
expect(symbolResolver.resolveSymbol(symbolCache.get('/test.ts', 'a')).metadata)
|
||||
.toEqual(symbolCache.get('/test2.ts', 'b'));
|
||||
expect(symbolResolver.resolveSymbol(symbolCache.get('/test.ts', 'x')).metadata).toEqual([{
|
||||
__symbolic: 'resolved',
|
||||
symbol: symbolCache.get('/test2.ts', 'y'),
|
||||
line: 3,
|
||||
character: 24,
|
||||
fileName: '/test.ts'
|
||||
}]);
|
||||
expect(symbolResolver.resolveSymbol(symbolCache.get('/test.ts', 'simpleFn')).metadata).toEqual({
|
||||
__symbolic: 'function',
|
||||
parameters: ['fnArg'],
|
||||
value: [
|
||||
symbolCache.get('/test.ts', 'a'), {
|
||||
__symbolic: 'resolved',
|
||||
symbol: symbolCache.get('/test2.ts', 'y'),
|
||||
line: 6,
|
||||
character: 21,
|
||||
fileName: '/test.ts'
|
||||
},
|
||||
{__symbolic: 'reference', name: 'fnArg'}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('should ignore module references without a name', () => {
|
||||
init({
|
||||
'/test.ts': `
|
||||
import Default from './test2';
|
||||
export {Default};
|
||||
`
|
||||
});
|
||||
|
||||
expect(symbolResolver.resolveSymbol(symbolCache.get('/test.ts', 'Default')).metadata)
|
||||
.toBeFalsy();
|
||||
});
|
||||
|
||||
it('should fill references to ambient symbols with undefined', () => {
|
||||
init({
|
||||
'/test.ts': `
|
||||
export var y = 1;
|
||||
export var z = [window, z];
|
||||
`
|
||||
});
|
||||
|
||||
expect(symbolResolver.resolveSymbol(symbolCache.get('/test.ts', 'z')).metadata).toEqual([
|
||||
undefined, symbolCache.get('/test.ts', 'z')
|
||||
]);
|
||||
});
|
||||
|
||||
it('should allow to use symbols with __', () => {
|
||||
init({
|
||||
'/test.ts': `
|
||||
export {__a__ as __b__} from './test2';
|
||||
import {__c__} from './test2';
|
||||
|
||||
export var __x__ = 1;
|
||||
export var __y__ = __c__;
|
||||
`
|
||||
});
|
||||
|
||||
expect(symbolResolver.resolveSymbol(symbolCache.get('/test.ts', '__x__')).metadata).toBe(1);
|
||||
expect(symbolResolver.resolveSymbol(symbolCache.get('/test.ts', '__y__')).metadata)
|
||||
.toBe(symbolCache.get('/test2.d.ts', '__c__'));
|
||||
expect(symbolResolver.resolveSymbol(symbolCache.get('/test.ts', '__b__')).metadata)
|
||||
.toBe(symbolCache.get('/test2.d.ts', '__a__'));
|
||||
|
||||
expect(symbolResolver.getSymbolsOf('/test.ts')).toEqual([
|
||||
symbolCache.get('/test.ts', '__b__'),
|
||||
symbolCache.get('/test.ts', '__x__'),
|
||||
symbolCache.get('/test.ts', '__y__'),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should only use the arity for classes from libraries without summaries', () => {
|
||||
init({
|
||||
'/test.d.ts': [{
|
||||
'__symbolic': 'module',
|
||||
'version': METADATA_VERSION,
|
||||
'metadata': {
|
||||
'AParam': {__symbolic: 'class'},
|
||||
'AClass': {
|
||||
__symbolic: 'class',
|
||||
arity: 1,
|
||||
members: {
|
||||
__ctor__: [
|
||||
{__symbolic: 'constructor', parameters: [symbolCache.get('/test.d.ts', 'AParam')]}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}]
|
||||
});
|
||||
|
||||
expect(symbolResolver.resolveSymbol(symbolCache.get('/test.d.ts', 'AClass')).metadata)
|
||||
.toEqual({__symbolic: 'class', arity: 1});
|
||||
});
|
||||
|
||||
it('should be able to trace a named export', () => {
|
||||
const symbol = symbolResolver
|
||||
.resolveSymbol(symbolResolver.getSymbolByModule(
|
||||
'./reexport/reexport', 'One', '/tmp/src/main.ts'))
|
||||
.metadata;
|
||||
expect(symbol.name).toEqual('One');
|
||||
expect(symbol.filePath).toEqual('/tmp/src/reexport/src/origin1.d.ts');
|
||||
});
|
||||
|
||||
it('should be able to trace a renamed export', () => {
|
||||
const symbol = symbolResolver
|
||||
.resolveSymbol(symbolResolver.getSymbolByModule(
|
||||
'./reexport/reexport', 'Four', '/tmp/src/main.ts'))
|
||||
.metadata;
|
||||
expect(symbol.name).toEqual('Three');
|
||||
expect(symbol.filePath).toEqual('/tmp/src/reexport/src/origin1.d.ts');
|
||||
});
|
||||
|
||||
it('should be able to trace an export * export', () => {
|
||||
const symbol = symbolResolver
|
||||
.resolveSymbol(symbolResolver.getSymbolByModule(
|
||||
'./reexport/reexport', 'Five', '/tmp/src/main.ts'))
|
||||
.metadata;
|
||||
expect(symbol.name).toEqual('Five');
|
||||
expect(symbol.filePath).toEqual('/tmp/src/reexport/src/origin5.d.ts');
|
||||
});
|
||||
|
||||
it('should be able to trace a multi-level re-export', () => {
|
||||
const symbol1 = symbolResolver
|
||||
.resolveSymbol(symbolResolver.getSymbolByModule(
|
||||
'./reexport/reexport', 'Thirty', '/tmp/src/main.ts'))
|
||||
.metadata;
|
||||
expect(symbol1.name).toEqual('Thirty');
|
||||
expect(symbol1.filePath).toEqual('/tmp/src/reexport/src/reexport2.d.ts');
|
||||
const symbol2 = symbolResolver.resolveSymbol(symbol1).metadata;
|
||||
expect(symbol2.name).toEqual('Thirty');
|
||||
expect(symbol2.filePath).toEqual('/tmp/src/reexport/src/origin30.d.ts');
|
||||
});
|
||||
|
||||
it('should prefer names in the file over reexports', () => {
|
||||
const metadata = symbolResolver
|
||||
.resolveSymbol(symbolResolver.getSymbolByModule(
|
||||
'./reexport/reexport', 'Six', '/tmp/src/main.ts'))
|
||||
.metadata;
|
||||
expect(metadata.__symbolic).toBe('class');
|
||||
});
|
||||
|
||||
it('should cache tracing a named export', () => {
|
||||
const moduleNameToFileNameSpy = spyOn(host, 'moduleNameToFileName').and.callThrough();
|
||||
const getMetadataForSpy = spyOn(host, 'getMetadataFor').and.callThrough();
|
||||
symbolResolver.resolveSymbol(
|
||||
symbolResolver.getSymbolByModule('./reexport/reexport', 'One', '/tmp/src/main.ts'));
|
||||
moduleNameToFileNameSpy.calls.reset();
|
||||
getMetadataForSpy.calls.reset();
|
||||
|
||||
const symbol = symbolResolver
|
||||
.resolveSymbol(symbolResolver.getSymbolByModule(
|
||||
'./reexport/reexport', 'One', '/tmp/src/main.ts'))
|
||||
.metadata;
|
||||
expect(moduleNameToFileNameSpy.calls.count()).toBe(1);
|
||||
expect(getMetadataForSpy.calls.count()).toBe(0);
|
||||
expect(symbol.name).toEqual('One');
|
||||
expect(symbol.filePath).toEqual('/tmp/src/reexport/src/origin1.d.ts');
|
||||
});
|
||||
});
|
||||
|
||||
export class MockSummaryResolver implements SummaryResolver<StaticSymbol> {
|
||||
constructor(private summaries: Summary<StaticSymbol>[] = [], private importAs: {
|
||||
symbol: StaticSymbol,
|
||||
importAs: StaticSymbol
|
||||
}[] = []) {}
|
||||
addSummary(summary: Summary<StaticSymbol>) {
|
||||
this.summaries.push(summary);
|
||||
}
|
||||
resolveSummary(reference: StaticSymbol): Summary<StaticSymbol> {
|
||||
return this.summaries.find(summary => summary.symbol === reference)!;
|
||||
}
|
||||
getSymbolsOf(filePath: string): StaticSymbol[]|null {
|
||||
const symbols = this.summaries.filter(summary => summary.symbol.filePath === filePath)
|
||||
.map(summary => summary.symbol);
|
||||
return symbols.length ? symbols : null;
|
||||
}
|
||||
getImportAs(symbol: StaticSymbol): StaticSymbol {
|
||||
const entry = this.importAs.find(entry => entry.symbol === symbol);
|
||||
return entry ? entry.importAs : undefined!;
|
||||
}
|
||||
getKnownModuleName(fileName: string): string|null {
|
||||
return null;
|
||||
}
|
||||
isLibraryFile(filePath: string): boolean {
|
||||
return filePath.endsWith('.d.ts');
|
||||
}
|
||||
toSummaryFileName(filePath: string): string {
|
||||
return filePath.replace(/(\.d)?\.ts$/, '.d.ts');
|
||||
}
|
||||
fromSummaryFileName(filePath: string): string {
|
||||
return filePath;
|
||||
}
|
||||
}
|
||||
|
||||
export class MockStaticSymbolResolverHost implements StaticSymbolResolverHost {
|
||||
private collector: MetadataCollector;
|
||||
|
||||
constructor(private data: {[key: string]: any}, collectorOptions?: CollectorOptions) {
|
||||
this.collector = new MetadataCollector(collectorOptions);
|
||||
}
|
||||
|
||||
// In tests, assume that symbols are not re-exported
|
||||
moduleNameToFileName(modulePath: string, containingFile?: string): string {
|
||||
function splitPath(path: string): string[] {
|
||||
return path.split(/\/|\\/g);
|
||||
}
|
||||
|
||||
function resolvePath(pathParts: string[]): string {
|
||||
const result: string[] = [];
|
||||
pathParts.forEach((part, index) => {
|
||||
switch (part) {
|
||||
case '':
|
||||
case '.':
|
||||
if (index > 0) return;
|
||||
break;
|
||||
case '..':
|
||||
if (index > 0 && result.length != 0) result.pop();
|
||||
return;
|
||||
}
|
||||
result.push(part);
|
||||
});
|
||||
return result.join('/');
|
||||
}
|
||||
|
||||
function pathTo(from: string, to: string): string {
|
||||
let result = to;
|
||||
if (to.startsWith('.')) {
|
||||
const fromParts = splitPath(from);
|
||||
fromParts.pop(); // remove the file name.
|
||||
const toParts = splitPath(to);
|
||||
result = resolvePath(fromParts.concat(toParts));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
if (modulePath.indexOf('.') === 0) {
|
||||
const baseName = pathTo(containingFile!, modulePath);
|
||||
const tsName = baseName + '.ts';
|
||||
if (this._getMetadataFor(tsName)) {
|
||||
return tsName;
|
||||
}
|
||||
return baseName + '.d.ts';
|
||||
}
|
||||
if (modulePath == 'unresolved') {
|
||||
return undefined!;
|
||||
}
|
||||
return '/tmp/' + modulePath + '.d.ts';
|
||||
}
|
||||
|
||||
getMetadataFor(moduleId: string): any {
|
||||
return this._getMetadataFor(moduleId);
|
||||
}
|
||||
|
||||
getOutputName(filePath: string): string {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
private _getMetadataFor(filePath: string): any {
|
||||
if (this.data[filePath] && filePath.match(TS_EXT)) {
|
||||
const text = this.data[filePath];
|
||||
if (typeof text === 'string') {
|
||||
const sf = ts.createSourceFile(
|
||||
filePath, this.data[filePath], ts.ScriptTarget.ES5, /* setParentNodes */ true);
|
||||
const diagnostics: ts.Diagnostic[] = (<any>sf).parseDiagnostics;
|
||||
if (diagnostics && diagnostics.length) {
|
||||
const errors = diagnostics
|
||||
.map(d => {
|
||||
const {line, character} =
|
||||
ts.getLineAndCharacterOfPosition(d.file!, d.start!);
|
||||
return `(${line}:${character}): ${d.messageText}`;
|
||||
})
|
||||
.join('\n');
|
||||
throw Error(`Error encountered during parse of file ${filePath}\n${errors}`);
|
||||
}
|
||||
return [this.collector.getMetadata(sf)];
|
||||
}
|
||||
}
|
||||
const result = this.data[filePath];
|
||||
if (result) {
|
||||
return Array.isArray(result) ? result : [result];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_TEST_DATA: {[key: string]: any} = {
|
||||
'/tmp/src/version-error.d.ts': {'__symbolic': 'module', 'version': 100, metadata: {e: 's'}},
|
||||
'/tmp/src/version-2-error.d.ts': {'__symbolic': 'module', 'version': 2, metadata: {e: 's'}},
|
||||
'/tmp/src/reexport/reexport.d.ts': {
|
||||
__symbolic: 'module',
|
||||
version: METADATA_VERSION,
|
||||
metadata: {
|
||||
Six: {__symbolic: 'class'},
|
||||
},
|
||||
exports: [
|
||||
{from: './src/origin1', export: ['One', 'Two', {name: 'Three', as: 'Four'}, 'Six']},
|
||||
{from: './src/origin5'}, {from: './src/reexport2'}
|
||||
]
|
||||
},
|
||||
'/tmp/src/reexport/src/origin1.d.ts': {
|
||||
__symbolic: 'module',
|
||||
version: METADATA_VERSION,
|
||||
metadata: {
|
||||
One: {__symbolic: 'class'},
|
||||
Two: {__symbolic: 'class'},
|
||||
Three: {__symbolic: 'class'},
|
||||
Six: {__symbolic: 'class'},
|
||||
},
|
||||
},
|
||||
'/tmp/src/reexport/src/origin5.d.ts': {
|
||||
__symbolic: 'module',
|
||||
version: METADATA_VERSION,
|
||||
metadata: {
|
||||
Five: {__symbolic: 'class'},
|
||||
},
|
||||
},
|
||||
'/tmp/src/reexport/src/origin30.d.ts': {
|
||||
__symbolic: 'module',
|
||||
version: METADATA_VERSION,
|
||||
metadata: {
|
||||
Thirty: {__symbolic: 'class'},
|
||||
},
|
||||
},
|
||||
'/tmp/src/reexport/src/originNone.d.ts': {
|
||||
__symbolic: 'module',
|
||||
version: METADATA_VERSION,
|
||||
metadata: {},
|
||||
},
|
||||
'/tmp/src/reexport/src/reexport2.d.ts': {
|
||||
__symbolic: 'module',
|
||||
version: METADATA_VERSION,
|
||||
metadata: {},
|
||||
exports: [{from: './originNone'}, {from: './origin30'}]
|
||||
}
|
||||
};
|
||||
|
|
@ -1,145 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 {AotSummaryResolver, AotSummaryResolverHost, ResolvedStaticSymbol, StaticSymbolCache, StaticSymbolResolver} from '@angular/compiler';
|
||||
import {serializeSummaries} from '@angular/compiler/src/aot/summary_serializer';
|
||||
import {ConstantPool, OutputContext} from '@angular/compiler/src/constant_pool';
|
||||
import * as o from '@angular/compiler/src/output/output_ast';
|
||||
import * as path from 'path';
|
||||
|
||||
import {MockStaticSymbolResolverHost, MockSummaryResolver} from './static_symbol_resolver_spec';
|
||||
|
||||
const EXT = /(\.d)?\.ts$/;
|
||||
|
||||
{
|
||||
describe('AotSummaryResolver', () => {
|
||||
let summaryResolver: AotSummaryResolver;
|
||||
let symbolCache: StaticSymbolCache;
|
||||
let host: MockAotSummaryResolverHost;
|
||||
|
||||
beforeEach(() => {
|
||||
symbolCache = new StaticSymbolCache();
|
||||
});
|
||||
|
||||
function init(summaries: {[filePath: string]: string} = {}) {
|
||||
host = new MockAotSummaryResolverHost(summaries);
|
||||
summaryResolver = new AotSummaryResolver(host, symbolCache);
|
||||
}
|
||||
|
||||
function serialize(
|
||||
symbols: ResolvedStaticSymbol[], enableExternalSymbolReexports = false): string {
|
||||
// Note: Don't use the top level host / summaryResolver as they might not be created yet
|
||||
const mockSummaryResolver = new MockSummaryResolver([]);
|
||||
const symbolResolver = new StaticSymbolResolver(
|
||||
new MockStaticSymbolResolverHost({}), symbolCache, mockSummaryResolver);
|
||||
return serializeSummaries(
|
||||
'someFile.ts', createMockOutputContext(), mockSummaryResolver, symbolResolver,
|
||||
symbols, [], enableExternalSymbolReexports)
|
||||
.json;
|
||||
}
|
||||
|
||||
it('should load serialized summary files', () => {
|
||||
const asymbol = symbolCache.get('/a.d.ts', 'a');
|
||||
init({'/a.ngsummary.json': serialize([{symbol: asymbol, metadata: 1}])});
|
||||
expect(summaryResolver.resolveSummary(asymbol)).toEqual({symbol: asymbol, metadata: 1});
|
||||
});
|
||||
|
||||
it('should not load summaries for source files', () => {
|
||||
init({});
|
||||
spyOn(host, 'loadSummary').and.callThrough();
|
||||
|
||||
expect(summaryResolver.resolveSummary(symbolCache.get('/a.ts', 'a'))).toBeFalsy();
|
||||
expect(host.loadSummary).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should cache summaries', () => {
|
||||
const asymbol = symbolCache.get('/a.d.ts', 'a');
|
||||
init({'/a.ngsummary.json': serialize([{symbol: asymbol, metadata: 1}])});
|
||||
expect(summaryResolver.resolveSummary(asymbol)).toBe(summaryResolver.resolveSummary(asymbol));
|
||||
});
|
||||
|
||||
it('should return all symbols in a summary', () => {
|
||||
const asymbol = symbolCache.get('/a.d.ts', 'a');
|
||||
init({'/a.ngsummary.json': serialize([{symbol: asymbol, metadata: 1}])});
|
||||
expect(summaryResolver.getSymbolsOf('/a.d.ts')).toEqual([asymbol]);
|
||||
});
|
||||
|
||||
it('should fill importAs for deep symbols if external symbol re-exports are enabled', () => {
|
||||
const libSymbol = symbolCache.get('/lib.d.ts', 'Lib');
|
||||
const srcSymbol = symbolCache.get('/src.ts', 'Src');
|
||||
init({
|
||||
'/src.ngsummary.json':
|
||||
serialize([{symbol: srcSymbol, metadata: 1}, {symbol: libSymbol, metadata: 2}], true)
|
||||
});
|
||||
summaryResolver.getSymbolsOf('/src.d.ts');
|
||||
|
||||
expect(summaryResolver.getImportAs(symbolCache.get('/src.d.ts', 'Src'))).toBeFalsy();
|
||||
expect(summaryResolver.getImportAs(libSymbol))
|
||||
.toBe(symbolCache.get('/src.ngfactory.d.ts', 'Lib_1'));
|
||||
});
|
||||
|
||||
describe('isLibraryFile', () => {
|
||||
it('should use host.isSourceFile to calculate the result', () => {
|
||||
init();
|
||||
expect(summaryResolver.isLibraryFile('someFile.ts')).toBe(false);
|
||||
expect(summaryResolver.isLibraryFile('someFile.d.ts')).toBe(true);
|
||||
});
|
||||
|
||||
it('should calculate the result for generated files based on the result for non generated files',
|
||||
() => {
|
||||
init();
|
||||
spyOn(host, 'isSourceFile').and.callThrough();
|
||||
expect(summaryResolver.isLibraryFile('someFile.ngfactory.ts')).toBe(false);
|
||||
expect(host.isSourceFile).toHaveBeenCalledWith('someFile.ts');
|
||||
});
|
||||
});
|
||||
|
||||
describe('regression', () => {
|
||||
// #18170
|
||||
it('should support resolving symbol with members ', () => {
|
||||
init();
|
||||
expect(summaryResolver.resolveSummary(symbolCache.get('/src.d.ts', 'Src', ['One', 'Two'])))
|
||||
.toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export class MockAotSummaryResolverHost implements AotSummaryResolverHost {
|
||||
constructor(private summaries: {[fileName: string]: string}) {}
|
||||
|
||||
fileNameToModuleName(fileName: string): string {
|
||||
return './' + path.basename(fileName).replace(EXT, '');
|
||||
}
|
||||
|
||||
toSummaryFileName(sourceFileName: string): string {
|
||||
return sourceFileName.replace(EXT, '') + '.d.ts';
|
||||
}
|
||||
|
||||
fromSummaryFileName(filePath: string): string {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
isSourceFile(filePath: string) {
|
||||
return !filePath.endsWith('.d.ts');
|
||||
}
|
||||
|
||||
loadSummary(filePath: string): string {
|
||||
return this.summaries[filePath];
|
||||
}
|
||||
}
|
||||
|
||||
export function createMockOutputContext(): OutputContext {
|
||||
return {
|
||||
statements: [],
|
||||
genFilePath: 'someGenFilePath',
|
||||
importExpr: () => o.NULL_EXPR,
|
||||
constantPool: new ConstantPool()
|
||||
};
|
||||
}
|
||||
|
|
@ -1,525 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 {AotSummaryResolver, AotSummaryResolverHost, CompileSummaryKind, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, StaticSymbolResolverHost} from '@angular/compiler';
|
||||
import {METADATA_VERSION} from '@angular/compiler-cli';
|
||||
import {deserializeSummaries, serializeSummaries} from '@angular/compiler/src/aot/summary_serializer';
|
||||
import {summaryFileName} from '@angular/compiler/src/aot/util';
|
||||
|
||||
import {MockStaticSymbolResolverHost} from './static_symbol_resolver_spec';
|
||||
import {createMockOutputContext, MockAotSummaryResolverHost} from './summary_resolver_spec';
|
||||
|
||||
|
||||
{
|
||||
describe('summary serializer', () => {
|
||||
let summaryResolver: AotSummaryResolver;
|
||||
let symbolResolver: StaticSymbolResolver;
|
||||
let symbolCache: StaticSymbolCache;
|
||||
let host: MockAotSummaryResolverHost;
|
||||
|
||||
beforeEach(() => {
|
||||
symbolCache = new StaticSymbolCache();
|
||||
});
|
||||
|
||||
function init(
|
||||
summaries: {[filePath: string]: string} = {}, metadata: {[key: string]: any} = {}) {
|
||||
host = new MockAotSummaryResolverHost(summaries);
|
||||
summaryResolver = new AotSummaryResolver(host, symbolCache);
|
||||
symbolResolver = new StaticSymbolResolver(
|
||||
new MockStaticSymbolResolverHost(metadata), symbolCache, summaryResolver);
|
||||
}
|
||||
|
||||
describe('summaryFileName', () => {
|
||||
it('should add .ngsummary.json to the filename', () => {
|
||||
init();
|
||||
expect(summaryFileName('a.ts')).toBe('a.ngsummary.json');
|
||||
expect(summaryFileName('a.d.ts')).toBe('a.ngsummary.json');
|
||||
expect(summaryFileName('a.js')).toBe('a.ngsummary.json');
|
||||
});
|
||||
});
|
||||
|
||||
it('should serialize various data correctly', () => {
|
||||
init();
|
||||
const serializedData = serializeSummaries(
|
||||
'someFile.ts', createMockOutputContext(), summaryResolver, symbolResolver,
|
||||
[
|
||||
{
|
||||
symbol: symbolCache.get('/tmp/some_values.ts', 'Values'),
|
||||
metadata: {
|
||||
aNumber: 1,
|
||||
aString: 'hello',
|
||||
anArray: [1, 2],
|
||||
aStaticSymbol: symbolCache.get('/tmp/some_symbol.ts', 'someName'),
|
||||
aStaticSymbolWithMembers:
|
||||
symbolCache.get('/tmp/some_symbol.ts', 'someName', ['someMember']),
|
||||
}
|
||||
},
|
||||
{
|
||||
symbol: symbolCache.get('/tmp/some_service.ts', 'SomeService'),
|
||||
metadata: {
|
||||
__symbolic: 'class',
|
||||
members: {'aMethod': {__symbolic: 'function'}},
|
||||
statics: {aStatic: true},
|
||||
decorators: ['aDecoratorData']
|
||||
}
|
||||
}
|
||||
],
|
||||
[{
|
||||
summary: {
|
||||
summaryKind: CompileSummaryKind.Injectable,
|
||||
type: {
|
||||
reference: symbolCache.get('/tmp/some_service.ts', 'SomeService'),
|
||||
}
|
||||
} as any,
|
||||
metadata: null as any
|
||||
}]);
|
||||
|
||||
|
||||
const summaries =
|
||||
deserializeSummaries(symbolCache, summaryResolver, 'someFile.d.ts', serializedData.json)
|
||||
.summaries;
|
||||
expect(summaries.length).toBe(2);
|
||||
|
||||
// Note: change from .ts to .d.ts is expected
|
||||
expect(summaries[0].symbol).toBe(symbolCache.get('/tmp/some_values.d.ts', 'Values'));
|
||||
expect(summaries[0].metadata).toEqual({
|
||||
aNumber: 1,
|
||||
aString: 'hello',
|
||||
anArray: [1, 2],
|
||||
aStaticSymbol: symbolCache.get('/tmp/some_symbol.d.ts', 'someName'),
|
||||
aStaticSymbolWithMembers:
|
||||
symbolCache.get('/tmp/some_symbol.d.ts', 'someName', ['someMember'])
|
||||
});
|
||||
|
||||
expect(summaries[1].symbol).toBe(symbolCache.get('/tmp/some_service.d.ts', 'SomeService'));
|
||||
// serialization should drop class decorators
|
||||
expect(summaries[1].metadata).toEqual({
|
||||
__symbolic: 'class',
|
||||
members: {aMethod: {__symbolic: 'function'}},
|
||||
statics: {aStatic: true}
|
||||
});
|
||||
expect(summaries[1].type!.type.reference)
|
||||
.toBe(symbolCache.get('/tmp/some_service.d.ts', 'SomeService'));
|
||||
});
|
||||
|
||||
it('should automatically add exported directives / pipes of NgModules that are not source files',
|
||||
() => {
|
||||
init();
|
||||
const externalSerialized = serializeSummaries(
|
||||
'someFile.ts', createMockOutputContext(), summaryResolver, symbolResolver,
|
||||
[
|
||||
{symbol: symbolCache.get('/tmp/external.ts', 'SomeExternalPipe'), metadata: null},
|
||||
{symbol: symbolCache.get('/tmp/external.ts', 'SomeExternalDir'), metadata: null},
|
||||
],
|
||||
[
|
||||
{
|
||||
summary: {
|
||||
summaryKind: CompileSummaryKind.Pipe,
|
||||
type: {
|
||||
reference: symbolCache.get('/tmp/external.ts', 'SomeExternalPipe'),
|
||||
}
|
||||
} as any,
|
||||
metadata: null as any
|
||||
},
|
||||
{
|
||||
summary: {
|
||||
summaryKind: CompileSummaryKind.Directive,
|
||||
type: {
|
||||
reference: symbolCache.get('/tmp/external.ts', 'SomeExternalDir'),
|
||||
},
|
||||
providers: [],
|
||||
viewProviders: [],
|
||||
} as any,
|
||||
metadata: null as any
|
||||
}
|
||||
]);
|
||||
init({
|
||||
'/tmp/external.ngsummary.json': externalSerialized.json,
|
||||
});
|
||||
|
||||
const serialized = serializeSummaries(
|
||||
'someFile.ts', createMockOutputContext(), summaryResolver, symbolResolver,
|
||||
[
|
||||
{symbol: symbolCache.get('/tmp/some_module.ts', 'SomeModule'), metadata: null},
|
||||
],
|
||||
[{
|
||||
summary: <any>{
|
||||
summaryKind: CompileSummaryKind.NgModule,
|
||||
type: {reference: symbolCache.get('/tmp/some_module.ts', 'SomeModule')},
|
||||
exportedPipes: [
|
||||
{reference: symbolCache.get('/tmp/some_pipe.ts', 'SomePipe')},
|
||||
{reference: symbolCache.get('/tmp/external.d.ts', 'SomeExternalPipe')}
|
||||
],
|
||||
exportedDirectives: [
|
||||
{reference: symbolCache.get('/tmp/some_dir.ts', 'SomeDir')},
|
||||
{reference: symbolCache.get('/tmp/external.d.ts', 'SomeExternalDir')}
|
||||
],
|
||||
providers: [],
|
||||
modules: [],
|
||||
},
|
||||
metadata: null as any
|
||||
}]);
|
||||
const summaries =
|
||||
deserializeSummaries(symbolCache, summaryResolver, 'someFile.d.ts', serialized.json)
|
||||
.summaries;
|
||||
init({
|
||||
'/tmp/some_module.ngsummary.json': serialized.json,
|
||||
});
|
||||
|
||||
const serializedReexport = serializeSummaries(
|
||||
'someFile.ts', createMockOutputContext(), summaryResolver, symbolResolver,
|
||||
[
|
||||
{
|
||||
symbol: symbolCache.get('/tmp/some_reexport.ts', 'ReexportModule'),
|
||||
metadata: symbolCache.get('/tmp/some_module.d.ts', 'SomeModule')
|
||||
},
|
||||
],
|
||||
[]);
|
||||
|
||||
expect(summaries.length).toBe(3);
|
||||
expect(summaries[0].symbol).toBe(symbolCache.get('/tmp/some_module.d.ts', 'SomeModule'));
|
||||
expect(summaries[1].symbol).toBe(symbolCache.get('/tmp/external.d.ts', 'SomeExternalDir'));
|
||||
expect(summaries[2].symbol)
|
||||
.toBe(symbolCache.get('/tmp/external.d.ts', 'SomeExternalPipe'));
|
||||
|
||||
const reexportSummaries =
|
||||
deserializeSummaries(
|
||||
symbolCache, summaryResolver, 'someFile.d.ts', serializedReexport.json)
|
||||
.summaries;
|
||||
expect(reexportSummaries.length).toBe(4);
|
||||
expect(reexportSummaries[0].symbol)
|
||||
.toBe(symbolCache.get('/tmp/some_reexport.d.ts', 'ReexportModule'));
|
||||
expect(reexportSummaries[1].symbol)
|
||||
.toBe(symbolCache.get('/tmp/some_module.d.ts', 'SomeModule'));
|
||||
expect(reexportSummaries[2].symbol)
|
||||
.toBe(symbolCache.get('/tmp/external.d.ts', 'SomeExternalDir'));
|
||||
expect(reexportSummaries[3].symbol)
|
||||
.toBe(symbolCache.get('/tmp/external.d.ts', 'SomeExternalPipe'));
|
||||
});
|
||||
|
||||
it('should automatically add the metadata of referenced symbols that are not in the source files',
|
||||
() => {
|
||||
init();
|
||||
const externalSerialized = serializeSummaries(
|
||||
'someFile.ts', createMockOutputContext(), summaryResolver, symbolResolver,
|
||||
[
|
||||
{
|
||||
symbol: symbolCache.get('/tmp/external.ts', 'PROVIDERS'),
|
||||
metadata: [symbolCache.get('/tmp/external_svc.ts', 'SomeService')]
|
||||
},
|
||||
{
|
||||
symbol: symbolCache.get('/tmp/external_svc.ts', 'SomeService'),
|
||||
metadata: {__symbolic: 'class'}
|
||||
},
|
||||
// Note: This is an important usecase when using ng1 and ng2 together via
|
||||
// goog.module.
|
||||
// In these cases, users write the following to get a referrable symbol in metadata
|
||||
// collection:
|
||||
// import UsernameService from 'goog:somePackage.UsernameService';
|
||||
// export {UsernameService};
|
||||
{
|
||||
symbol: symbolCache.get('/tmp/external.ts', 'ReexportNonExistent'),
|
||||
metadata: symbolCache.get('/tmp/external.ts', 'NonExistent'),
|
||||
}
|
||||
],
|
||||
[{
|
||||
summary: {
|
||||
summaryKind: CompileSummaryKind.Injectable,
|
||||
type: {
|
||||
reference: symbolCache.get('/tmp/external_svc.ts', 'SomeService'),
|
||||
}
|
||||
} as any,
|
||||
metadata: null as any
|
||||
}]);
|
||||
init(
|
||||
{
|
||||
'/tmp/external.ngsummary.json': externalSerialized.json,
|
||||
},
|
||||
{
|
||||
'/tmp/local.ts': `
|
||||
export var local = 'a';
|
||||
`,
|
||||
'/tmp/non_summary.d.ts':
|
||||
{__symbolic: 'module', version: METADATA_VERSION, metadata: {'external': 'b'}}
|
||||
});
|
||||
const serialized = serializeSummaries(
|
||||
'someFile.ts', createMockOutputContext(), summaryResolver, symbolResolver, [{
|
||||
symbol: symbolCache.get('/tmp/test.ts', 'main'),
|
||||
metadata: {
|
||||
local: symbolCache.get('/tmp/local.ts', 'local'),
|
||||
external: symbolCache.get('/tmp/external.d.ts', 'PROVIDERS'),
|
||||
externalNonSummary: symbolCache.get('/tmp/non_summary.d.ts', 'external'),
|
||||
reexportNonExistent: symbolCache.get('/tmp/external.ts', 'ReexportNonExistent'),
|
||||
}
|
||||
}],
|
||||
[]);
|
||||
|
||||
const summaries =
|
||||
deserializeSummaries(symbolCache, summaryResolver, 'someFile.d.ts', serialized.json)
|
||||
.summaries;
|
||||
// Note: local should not show up!
|
||||
expect(summaries.length).toBe(4);
|
||||
expect(summaries[0].symbol).toBe(symbolCache.get('/tmp/test.d.ts', 'main'));
|
||||
expect(summaries[0].metadata).toEqual({
|
||||
local: symbolCache.get('/tmp/local.d.ts', 'local'),
|
||||
external: symbolCache.get('/tmp/external.d.ts', 'PROVIDERS'),
|
||||
externalNonSummary: symbolCache.get('/tmp/non_summary.d.ts', 'external'),
|
||||
reexportNonExistent: symbolCache.get('/tmp/external.d.ts', 'ReexportNonExistent'),
|
||||
});
|
||||
expect(summaries[1].symbol).toBe(symbolCache.get('/tmp/external.d.ts', 'PROVIDERS'));
|
||||
expect(summaries[1].metadata).toEqual([symbolCache.get(
|
||||
'/tmp/external_svc.d.ts', 'SomeService')]);
|
||||
// SomService is a transitive dep, but should have been serialized as well.
|
||||
expect(summaries[2].symbol).toBe(symbolCache.get('/tmp/external_svc.d.ts', 'SomeService'));
|
||||
expect(summaries[2].type!.type.reference)
|
||||
.toBe(symbolCache.get('/tmp/external_svc.d.ts', 'SomeService'));
|
||||
// there was no summary for non_summary, but it should have
|
||||
// been serialized as well.
|
||||
expect(summaries[3].symbol).toBe(symbolCache.get('/tmp/non_summary.d.ts', 'external'));
|
||||
expect(summaries[3].metadata).toEqual('b');
|
||||
});
|
||||
|
||||
it('should resolve reexported values in libraries', () => {
|
||||
init();
|
||||
const externalSerialized = serializeSummaries(
|
||||
'someFile.ts', createMockOutputContext(), summaryResolver, symbolResolver,
|
||||
[
|
||||
{symbol: symbolCache.get('/tmp/external.ts', 'value'), metadata: 'someString'},
|
||||
{
|
||||
symbol: symbolCache.get('/tmp/external.ts', 'reexportValue'),
|
||||
metadata: symbolCache.get('/tmp/external.ts', 'value')
|
||||
},
|
||||
],
|
||||
[]);
|
||||
init({
|
||||
'/tmp/external.ngsummary.json': externalSerialized.json,
|
||||
});
|
||||
const serialized = serializeSummaries(
|
||||
'someFile.ts', createMockOutputContext(), summaryResolver, symbolResolver,
|
||||
[
|
||||
{
|
||||
symbol: symbolCache.get('/tmp/test.ts', 'mainValue'),
|
||||
metadata: symbolCache.get('/tmp/external.d.ts', 'reexportValue'),
|
||||
},
|
||||
],
|
||||
[]);
|
||||
|
||||
const summaries =
|
||||
deserializeSummaries(symbolCache, summaryResolver, 'someFile.d.ts', serialized.json)
|
||||
.summaries;
|
||||
expect(summaries.length).toBe(2);
|
||||
expect(summaries[0].symbol).toBe(symbolCache.get('/tmp/test.d.ts', 'mainValue'));
|
||||
expect(summaries[0].metadata).toBe(symbolCache.get('/tmp/external.d.ts', 'value'));
|
||||
expect(summaries[1].symbol).toBe(symbolCache.get('/tmp/external.d.ts', 'value'));
|
||||
expect(summaries[1].metadata).toBe('someString');
|
||||
});
|
||||
|
||||
it('should use existing reexports for "importAs" for symbols of libraries', () => {
|
||||
init();
|
||||
const externalSerialized = serializeSummaries(
|
||||
'someFile.ts', createMockOutputContext(), summaryResolver, symbolResolver,
|
||||
[
|
||||
{symbol: symbolCache.get('/tmp/external.ts', 'value'), metadata: 'aValue'},
|
||||
{
|
||||
symbol: symbolCache.get('/tmp/external.ts', 'reexportValue'),
|
||||
metadata: symbolCache.get('/tmp/external.ts', 'value')
|
||||
},
|
||||
],
|
||||
[]);
|
||||
expect(externalSerialized.exportAs).toEqual([]);
|
||||
init({
|
||||
'/tmp/external.ngsummary.json': externalSerialized.json,
|
||||
});
|
||||
const serialized = serializeSummaries(
|
||||
'someFile.ts', createMockOutputContext(), summaryResolver, symbolResolver, [{
|
||||
symbol: symbolCache.get('/tmp/test.ts', 'mainValue'),
|
||||
metadata: symbolCache.get('/tmp/external.d.ts', 'reexportValue'),
|
||||
}],
|
||||
[]);
|
||||
expect(serialized.exportAs).toEqual([]);
|
||||
const importAs =
|
||||
deserializeSummaries(symbolCache, summaryResolver, 'someFile.d.ts', serialized.json)
|
||||
.importAs;
|
||||
expect(importAs).toEqual([{
|
||||
symbol: symbolCache.get('/tmp/external.d.ts', 'value'),
|
||||
importAs: symbolCache.get('/tmp/test.d.ts', 'mainValue'),
|
||||
}]);
|
||||
});
|
||||
|
||||
describe('with resolved symbols', () => {
|
||||
it('should be able to serialize a call', () => {
|
||||
init();
|
||||
const serialized = serializeSummaries(
|
||||
'someFile.ts', createMockOutputContext(), summaryResolver, symbolResolver, [{
|
||||
symbol: symbolCache.get('/tmp/test.ts', 'main'),
|
||||
metadata: {
|
||||
__symbolic: 'call',
|
||||
expression:
|
||||
{__symbolic: 'resolved', symbol: symbolCache.get('/tmp/test2.ts', 'ref')}
|
||||
}
|
||||
}],
|
||||
[]);
|
||||
expect(serialized.json).not.toContain('error');
|
||||
});
|
||||
|
||||
it('should be able to serialize a call to a method', () => {
|
||||
init();
|
||||
const serialized = serializeSummaries(
|
||||
'someFile.ts', createMockOutputContext(), summaryResolver, symbolResolver, [{
|
||||
symbol: symbolCache.get('/tmp/test.ts', 'main'),
|
||||
metadata: {
|
||||
__symbolic: 'call',
|
||||
expression: {
|
||||
__symbolic: 'select',
|
||||
expression:
|
||||
{__symbolic: 'resolved', symbol: symbolCache.get('/tmp/test2.ts', 'ref')},
|
||||
name: 'foo'
|
||||
}
|
||||
}
|
||||
}],
|
||||
[]);
|
||||
expect(serialized.json).not.toContain('error');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('symbol re-exports enabled', () => {
|
||||
it('should not create "importAs" names for ctor arguments which are types of reexported classes in libraries',
|
||||
() => {
|
||||
init();
|
||||
const externalSerialized = serializeSummaries(
|
||||
'someFile.ts', createMockOutputContext(), summaryResolver, symbolResolver,
|
||||
[
|
||||
{
|
||||
symbol: symbolCache.get('/tmp/external.ts', 'type'),
|
||||
metadata: {__symbolic: 'interface'}
|
||||
},
|
||||
{
|
||||
symbol: symbolCache.get('/tmp/external.ts', 'value'),
|
||||
metadata: {__symbolic: 'class'}
|
||||
},
|
||||
{
|
||||
symbol: symbolCache.get('/tmp/external.ts', 'reexportClass'),
|
||||
metadata: {
|
||||
__symbolic: 'class',
|
||||
'members': {
|
||||
'__ctor__': [{
|
||||
'__symbolic': 'constructor',
|
||||
'parameters': [
|
||||
symbolCache.get('/tmp/external.ts', 'type'),
|
||||
symbolCache.get('/tmp/external.ts', 'value'),
|
||||
]
|
||||
}]
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
],
|
||||
[], true);
|
||||
expect(externalSerialized.exportAs).toEqual([]);
|
||||
init({
|
||||
'/tmp/external.ngsummary.json': externalSerialized.json,
|
||||
});
|
||||
const serialized = serializeSummaries(
|
||||
'someFile.ts', createMockOutputContext(), summaryResolver, symbolResolver, [{
|
||||
symbol: symbolCache.get('/tmp/test.ts', 'mainClass'),
|
||||
metadata: symbolCache.get('/tmp/external.d.ts', 'reexportClass'),
|
||||
}],
|
||||
[], true);
|
||||
const importAs =
|
||||
deserializeSummaries(symbolCache, summaryResolver, 'someFile.d.ts', serialized.json)
|
||||
.importAs;
|
||||
expect(importAs).toEqual([
|
||||
{
|
||||
symbol: symbolCache.get('/tmp/external.d.ts', 'reexportClass'),
|
||||
importAs: symbolCache.get('/tmp/test.d.ts', 'mainClass'),
|
||||
},
|
||||
{
|
||||
symbol: symbolCache.get('/tmp/external.d.ts', 'value'),
|
||||
importAs: symbolCache.get('someFile.ngfactory.d.ts', 'value_3'),
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it('should create reexports in the ngfactory for symbols of libraries', () => {
|
||||
init();
|
||||
const serialized = serializeSummaries(
|
||||
'someFile.ts', createMockOutputContext(), summaryResolver, symbolResolver, [{
|
||||
symbol: symbolCache.get('/tmp/test.ts', 'main'),
|
||||
metadata: [
|
||||
symbolCache.get('/tmp/external.d.ts', 'lib'),
|
||||
symbolCache.get('/tmp/external.d.ts', 'lib', ['someMember']),
|
||||
]
|
||||
}],
|
||||
[], true);
|
||||
// Note: no entry for the symbol with members!
|
||||
expect(serialized.exportAs).toEqual([
|
||||
{symbol: symbolCache.get('/tmp/external.d.ts', 'lib'), exportAs: 'lib_1'}
|
||||
]);
|
||||
|
||||
const deserialized =
|
||||
deserializeSummaries(symbolCache, summaryResolver, 'someFile.d.ts', serialized.json);
|
||||
// Note: no entry for the symbol with members!
|
||||
expect(deserialized.importAs).toEqual([{
|
||||
symbol: symbolCache.get('/tmp/external.d.ts', 'lib'),
|
||||
importAs: symbolCache.get('someFile.ngfactory.d.ts', 'lib_1')
|
||||
}]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should use existing reexports for "importAs" for symbols of libraries', () => {
|
||||
init();
|
||||
const externalSerialized = serializeSummaries(
|
||||
'someFile.ts', createMockOutputContext(), summaryResolver, symbolResolver,
|
||||
[
|
||||
{symbol: symbolCache.get('/tmp/external.ts', 'value'), metadata: 'aValue'},
|
||||
{
|
||||
symbol: symbolCache.get('/tmp/external.ts', 'reexportValue'),
|
||||
metadata: symbolCache.get('/tmp/external.ts', 'value')
|
||||
},
|
||||
],
|
||||
[], false);
|
||||
expect(externalSerialized.exportAs).toEqual([]);
|
||||
init({
|
||||
'/tmp/external.ngsummary.json': externalSerialized.json,
|
||||
});
|
||||
const serialized = serializeSummaries(
|
||||
'someFile.ts', createMockOutputContext(), summaryResolver, symbolResolver, [{
|
||||
symbol: symbolCache.get('/tmp/test.ts', 'mainValue'),
|
||||
metadata: symbolCache.get('/tmp/external.d.ts', 'reexportValue'),
|
||||
}],
|
||||
[]);
|
||||
expect(serialized.exportAs).toEqual([]);
|
||||
const importAs =
|
||||
deserializeSummaries(symbolCache, summaryResolver, 'someFile.d.ts', serialized.json)
|
||||
.importAs;
|
||||
expect(importAs).toEqual([{
|
||||
symbol: symbolCache.get('/tmp/external.d.ts', 'value'),
|
||||
importAs: symbolCache.get('/tmp/test.d.ts', 'mainValue'),
|
||||
}]);
|
||||
});
|
||||
|
||||
it('should not create reexports in the ngfactory for external symbols', () => {
|
||||
init();
|
||||
const serialized = serializeSummaries(
|
||||
'someFile.ts', createMockOutputContext(), summaryResolver, symbolResolver, [{
|
||||
symbol: symbolCache.get('/tmp/test.ts', 'main'),
|
||||
metadata: [
|
||||
symbolCache.get('/tmp/external.d.ts', 'lib'),
|
||||
symbolCache.get('/tmp/external.d.ts', 'lib', ['someMember']),
|
||||
]
|
||||
}],
|
||||
[], false);
|
||||
expect(serialized.exportAs.length).toBe(0, 'Expected no external symbols to be re-exported.');
|
||||
const deserialized =
|
||||
deserializeSummaries(symbolCache, summaryResolver, 'someFile.d.ts', serialized.json);
|
||||
expect(deserialized.importAs.length)
|
||||
.toBe(0, 'Expected no symbols that can be imported from a re-exported location');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -1,890 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 {AotCompilerHost, AotCompilerOptions, createAotCompiler, GeneratedFile, toTypeScript} from '@angular/compiler';
|
||||
import {MetadataBundlerHost} from '@angular/compiler-cli/src/metadata/bundler';
|
||||
import {MetadataCollector} from '@angular/compiler-cli/src/metadata/collector';
|
||||
import {ModuleMetadata} from '@angular/compiler-cli/src/metadata/index';
|
||||
import {getCachedSourceFile} from '@angular/compiler-cli/src/ngtsc/testing';
|
||||
import {newArray} from '@angular/compiler/src/util';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import ts from 'typescript';
|
||||
|
||||
export interface MetadataProvider {
|
||||
getMetadata(source: ts.SourceFile): ModuleMetadata|undefined;
|
||||
}
|
||||
|
||||
let nodeModulesPath: string;
|
||||
let angularSourcePath: string;
|
||||
let rootPath: string;
|
||||
|
||||
calcPathsOnDisc();
|
||||
|
||||
export type MockFileOrDirectory = string|MockDirectory;
|
||||
|
||||
export type MockDirectory = {
|
||||
[name: string]: MockFileOrDirectory|undefined;
|
||||
};
|
||||
|
||||
export function isDirectory(data: MockFileOrDirectory|undefined): data is MockDirectory {
|
||||
return typeof data !== 'string';
|
||||
}
|
||||
|
||||
const rxjs = /\/rxjs\//;
|
||||
export const settings: ts.CompilerOptions = {
|
||||
target: ts.ScriptTarget.ES5,
|
||||
declaration: true,
|
||||
module: ts.ModuleKind.CommonJS,
|
||||
moduleResolution: ts.ModuleResolutionKind.NodeJs,
|
||||
emitDecoratorMetadata: true,
|
||||
experimentalDecorators: true,
|
||||
removeComments: false,
|
||||
noImplicitAny: false,
|
||||
skipLibCheck: true,
|
||||
strictNullChecks: true,
|
||||
lib: ['lib.es2015.d.ts', 'lib.dom.d.ts'],
|
||||
types: []
|
||||
};
|
||||
|
||||
export interface EmitterOptions {
|
||||
emitMetadata: boolean;
|
||||
mockData?: MockDirectory;
|
||||
context?: Map<string, string>;
|
||||
}
|
||||
|
||||
function calcPathsOnDisc() {
|
||||
const moduleFilename = module.filename.replace(/\\/g, '/');
|
||||
const distIndex = moduleFilename.indexOf('/dist/all');
|
||||
if (distIndex >= 0) {
|
||||
rootPath = moduleFilename.substr(0, distIndex);
|
||||
nodeModulesPath = path.join(rootPath, 'node_modules');
|
||||
angularSourcePath = path.join(rootPath, 'packages');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class EmittingCompilerHost implements ts.CompilerHost {
|
||||
private addedFiles = new Map<string, string>();
|
||||
private writtenFiles = new Map<string, string>();
|
||||
private scriptNames: string[];
|
||||
private root = '/';
|
||||
private collector = new MetadataCollector();
|
||||
private cachedAddedDirectories: Set<string>|undefined;
|
||||
|
||||
constructor(scriptNames: string[], private options: EmitterOptions) {
|
||||
// Rewrite references to scripts with '@angular' to its corresponding location in
|
||||
// the source tree.
|
||||
this.scriptNames = scriptNames.map(f => this.effectiveName(f));
|
||||
this.root = rootPath || this.root;
|
||||
if (options.context) {
|
||||
this.addedFiles = mergeMaps(options.context);
|
||||
}
|
||||
}
|
||||
|
||||
public writtenAngularFiles(target = new Map<string, string>()): Map<string, string> {
|
||||
this.written.forEach((value, key) => {
|
||||
const path = `/node_modules/@angular${key.substring(angularSourcePath.length)}`;
|
||||
target.set(path, value);
|
||||
});
|
||||
return target;
|
||||
}
|
||||
|
||||
public addScript(fileName: string, content: string) {
|
||||
const scriptName = this.effectiveName(fileName);
|
||||
this.addedFiles.set(scriptName, content);
|
||||
this.cachedAddedDirectories = undefined;
|
||||
this.scriptNames.push(scriptName);
|
||||
}
|
||||
|
||||
public override(fileName: string, content: string) {
|
||||
const scriptName = this.effectiveName(fileName);
|
||||
this.addedFiles.set(scriptName, content);
|
||||
this.cachedAddedDirectories = undefined;
|
||||
}
|
||||
|
||||
public addFiles(map: Map<string, string>) {
|
||||
for (const [name, content] of Array.from(map.entries())) {
|
||||
this.addedFiles.set(name, content);
|
||||
}
|
||||
}
|
||||
|
||||
public addWrittenFile(fileName: string, content: string) {
|
||||
this.writtenFiles.set(this.effectiveName(fileName), content);
|
||||
}
|
||||
|
||||
public getWrittenFiles(): {name: string, content: string}[] {
|
||||
return Array.from(this.writtenFiles).map(f => ({name: f[0], content: f[1]}));
|
||||
}
|
||||
|
||||
public get scripts(): string[] {
|
||||
return this.scriptNames;
|
||||
}
|
||||
|
||||
public get written(): Map<string, string> {
|
||||
return this.writtenFiles;
|
||||
}
|
||||
|
||||
public effectiveName(fileName: string): string {
|
||||
const prefix = '@angular/';
|
||||
return angularSourcePath && fileName.startsWith(prefix) ?
|
||||
path.join(angularSourcePath, fileName.substr(prefix.length)) :
|
||||
fileName;
|
||||
}
|
||||
|
||||
// ts.ModuleResolutionHost
|
||||
fileExists(fileName: string): boolean {
|
||||
return this.addedFiles.has(fileName) || open(fileName, this.options.mockData) != null ||
|
||||
fs.existsSync(fileName);
|
||||
}
|
||||
|
||||
readFile(fileName: string): string {
|
||||
const result = this.addedFiles.get(fileName) || open(fileName, this.options.mockData);
|
||||
if (result) return result;
|
||||
|
||||
let basename = path.basename(fileName);
|
||||
if (/^lib.*\.d\.ts$/.test(basename)) {
|
||||
let libPath = ts.getDefaultLibFilePath(settings);
|
||||
return fs.readFileSync(path.join(path.dirname(libPath), basename), 'utf8');
|
||||
}
|
||||
return fs.readFileSync(fileName, 'utf8');
|
||||
}
|
||||
|
||||
directoryExists(directoryName: string): boolean {
|
||||
return directoryExists(directoryName, this.options.mockData) ||
|
||||
this.getAddedDirectories().has(directoryName) ||
|
||||
(fs.existsSync(directoryName) && fs.statSync(directoryName).isDirectory());
|
||||
}
|
||||
|
||||
getCurrentDirectory(): string {
|
||||
return this.root;
|
||||
}
|
||||
|
||||
getDirectories(dir: string): string[] {
|
||||
const result = open(dir, this.options.mockData);
|
||||
if (result && typeof result !== 'string') {
|
||||
return Object.keys(result);
|
||||
}
|
||||
return fs.readdirSync(dir).filter(p => {
|
||||
const name = path.join(dir, p);
|
||||
const stat = fs.statSync(name);
|
||||
return stat && stat.isDirectory();
|
||||
});
|
||||
}
|
||||
|
||||
// ts.CompilerHost
|
||||
getSourceFile(
|
||||
fileName: string, languageVersion: ts.ScriptTarget,
|
||||
onError?: (message: string) => void): ts.SourceFile {
|
||||
const content = this.readFile(fileName);
|
||||
if (content) {
|
||||
const cachedSf = getCachedSourceFile(fileName, () => content);
|
||||
if (cachedSf !== null) {
|
||||
return cachedSf;
|
||||
}
|
||||
return ts.createSourceFile(fileName, content, languageVersion, /* setParentNodes */ true);
|
||||
}
|
||||
throw new Error(`File not found '${fileName}'.`);
|
||||
}
|
||||
|
||||
getDefaultLibFileName(options: ts.CompilerOptions): string {
|
||||
return 'lib.d.ts';
|
||||
}
|
||||
|
||||
writeFile: ts.WriteFileCallback =
|
||||
(fileName: string, data: string, writeByteOrderMark: boolean,
|
||||
onError?: (message: string) => void, sourceFiles?: ReadonlyArray<ts.SourceFile>) => {
|
||||
this.addWrittenFile(fileName, data);
|
||||
if (this.options.emitMetadata && sourceFiles && sourceFiles.length && DTS.test(fileName)) {
|
||||
const metadataFilePath = fileName.replace(DTS, '.metadata.json');
|
||||
const metadata = this.collector.getMetadata(sourceFiles[0]);
|
||||
if (metadata) {
|
||||
this.addWrittenFile(metadataFilePath, JSON.stringify(metadata));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getCanonicalFileName(fileName: string): string {
|
||||
return fileName;
|
||||
}
|
||||
useCaseSensitiveFileNames(): boolean {
|
||||
return false;
|
||||
}
|
||||
getNewLine(): string {
|
||||
return '\n';
|
||||
}
|
||||
|
||||
private getAddedDirectories(): Set<string> {
|
||||
let result = this.cachedAddedDirectories;
|
||||
if (!result) {
|
||||
const newCache = new Set<string>();
|
||||
const addFile = (fileName: string) => {
|
||||
const directory = fileName.substr(0, fileName.lastIndexOf('/'));
|
||||
if (!newCache.has(directory)) {
|
||||
newCache.add(directory);
|
||||
addFile(directory);
|
||||
}
|
||||
};
|
||||
Array.from(this.addedFiles.keys()).forEach(addFile);
|
||||
this.cachedAddedDirectories = result = newCache;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export class MockCompilerHost implements ts.CompilerHost {
|
||||
scriptNames: string[];
|
||||
|
||||
public overrides = new Map<string, string>();
|
||||
public writtenFiles = new Map<string, string>();
|
||||
private sourceFiles = new Map<string, ts.SourceFile>();
|
||||
private assumeExists = new Set<string>();
|
||||
private traces: string[] = [];
|
||||
|
||||
constructor(scriptNames: string[], private data: MockDirectory) {
|
||||
this.scriptNames = [...scriptNames];
|
||||
}
|
||||
|
||||
// Test API
|
||||
override(fileName: string, content: string) {
|
||||
if (content) {
|
||||
this.overrides.set(fileName, content);
|
||||
} else {
|
||||
this.overrides.delete(fileName);
|
||||
}
|
||||
this.sourceFiles.delete(fileName);
|
||||
}
|
||||
|
||||
addScript(fileName: string, content: string) {
|
||||
this.overrides.set(fileName, content);
|
||||
this.scriptNames.push(fileName);
|
||||
this.sourceFiles.delete(fileName);
|
||||
}
|
||||
|
||||
assumeFileExists(fileName: string) {
|
||||
this.assumeExists.add(fileName);
|
||||
}
|
||||
|
||||
remove(files: string[]) {
|
||||
// Remove the files from the list of scripts.
|
||||
const fileSet = new Set(files);
|
||||
this.scriptNames = this.scriptNames.filter(f => fileSet.has(f));
|
||||
|
||||
// Remove files from written files
|
||||
files.forEach(f => this.writtenFiles.delete(f));
|
||||
}
|
||||
|
||||
// ts.ModuleResolutionHost
|
||||
fileExists(fileName: string): boolean {
|
||||
if (this.overrides.has(fileName) || this.writtenFiles.has(fileName) ||
|
||||
this.assumeExists.has(fileName)) {
|
||||
return true;
|
||||
}
|
||||
const effectiveName = this.getEffectiveName(fileName);
|
||||
if (effectiveName == fileName) {
|
||||
return open(fileName, this.data) != null;
|
||||
}
|
||||
if (fileName.match(rxjs)) {
|
||||
return fs.existsSync(effectiveName);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
readFile(fileName: string): string {
|
||||
return this.getFileContent(fileName)!;
|
||||
}
|
||||
|
||||
trace(s: string): void {
|
||||
this.traces.push(s);
|
||||
}
|
||||
|
||||
getCurrentDirectory(): string {
|
||||
return '/';
|
||||
}
|
||||
|
||||
getDirectories(dir: string): string[] {
|
||||
const effectiveName = this.getEffectiveName(dir);
|
||||
if (effectiveName === dir) {
|
||||
const data = find(dir, this.data);
|
||||
if (isDirectory(data)) {
|
||||
return Object.keys(data).filter(k => isDirectory(data[k]));
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// ts.CompilerHost
|
||||
getSourceFile(
|
||||
fileName: string, languageVersion: ts.ScriptTarget,
|
||||
onError?: (message: string) => void): ts.SourceFile|undefined {
|
||||
let result = this.sourceFiles.get(fileName);
|
||||
if (!result) {
|
||||
const content = this.getFileContent(fileName);
|
||||
const cachedSf = getCachedSourceFile(fileName, () => content);
|
||||
if (cachedSf !== null) {
|
||||
return cachedSf;
|
||||
}
|
||||
if (content) {
|
||||
result = ts.createSourceFile(fileName, content, languageVersion);
|
||||
this.sourceFiles.set(fileName, result);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
getDefaultLibFileName(options: ts.CompilerOptions): string {
|
||||
return 'lib.d.ts';
|
||||
}
|
||||
|
||||
writeFile: ts.WriteFileCallback =
|
||||
(fileName: string, data: string, writeByteOrderMark: boolean) => {
|
||||
this.writtenFiles.set(fileName, data);
|
||||
this.sourceFiles.delete(fileName);
|
||||
}
|
||||
|
||||
getCanonicalFileName(fileName: string): string {
|
||||
return fileName;
|
||||
}
|
||||
useCaseSensitiveFileNames(): boolean {
|
||||
return false;
|
||||
}
|
||||
getNewLine(): string {
|
||||
return '\n';
|
||||
}
|
||||
|
||||
// Private methods
|
||||
private getFileContent(fileName: string): string|undefined {
|
||||
if (this.overrides.has(fileName)) {
|
||||
return this.overrides.get(fileName);
|
||||
}
|
||||
if (this.writtenFiles.has(fileName)) {
|
||||
return this.writtenFiles.get(fileName);
|
||||
}
|
||||
let basename = path.basename(fileName);
|
||||
if (/^lib.*\.d\.ts$/.test(basename)) {
|
||||
let libPath = ts.getDefaultLibFilePath(settings);
|
||||
return fs.readFileSync(path.join(path.dirname(libPath), basename), 'utf8');
|
||||
}
|
||||
let effectiveName = this.getEffectiveName(fileName);
|
||||
if (effectiveName === fileName) {
|
||||
return open(fileName, this.data);
|
||||
}
|
||||
if (fileName.match(rxjs) && fs.existsSync(fileName)) {
|
||||
return fs.readFileSync(fileName, 'utf8');
|
||||
}
|
||||
}
|
||||
|
||||
private getEffectiveName(name: string): string {
|
||||
const node_modules = 'node_modules';
|
||||
const rxjs = '/rxjs';
|
||||
if (name.startsWith('/' + node_modules)) {
|
||||
if (nodeModulesPath && name.startsWith('/' + node_modules + rxjs)) {
|
||||
return path.join(nodeModulesPath, name.substr(node_modules.length + 1));
|
||||
}
|
||||
}
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
const EXT = /(\.ts|\.d\.ts|\.js|\.jsx|\.tsx)$/;
|
||||
const DTS = /\.d\.ts$/;
|
||||
const GENERATED_FILES = /\.ngfactory\.ts$|\.ngstyle\.ts$/;
|
||||
|
||||
export class MockAotCompilerHost implements AotCompilerHost {
|
||||
private metadataVisible: boolean = true;
|
||||
private dtsAreSource: boolean = true;
|
||||
private resolveModuleNameHost: ts.ModuleResolutionHost;
|
||||
|
||||
constructor(
|
||||
private tsHost: MockCompilerHost,
|
||||
private metadataProvider: MetadataProvider = new MetadataCollector()) {
|
||||
this.resolveModuleNameHost = Object.create(tsHost);
|
||||
this.resolveModuleNameHost.fileExists = (fileName) => {
|
||||
fileName = stripNgResourceSuffix(fileName);
|
||||
return tsHost.fileExists(fileName);
|
||||
};
|
||||
}
|
||||
|
||||
hideMetadata() {
|
||||
this.metadataVisible = false;
|
||||
}
|
||||
|
||||
tsFilesOnly() {
|
||||
this.dtsAreSource = false;
|
||||
}
|
||||
|
||||
// StaticSymbolResolverHost
|
||||
getMetadataFor(modulePath: string): {[key: string]: any}[]|undefined {
|
||||
if (!this.tsHost.fileExists(modulePath)) {
|
||||
return undefined;
|
||||
}
|
||||
if (DTS.test(modulePath)) {
|
||||
if (this.metadataVisible) {
|
||||
const metadataPath = modulePath.replace(DTS, '.metadata.json');
|
||||
if (this.tsHost.fileExists(metadataPath)) {
|
||||
let result = JSON.parse(this.tsHost.readFile(metadataPath)) as {[key: string]: any}[];
|
||||
return Array.isArray(result) ? result : [result];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const sf = this.tsHost.getSourceFile(modulePath, ts.ScriptTarget.Latest);
|
||||
const metadata = sf && this.metadataProvider.getMetadata(sf);
|
||||
return metadata ? [metadata] : [];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
moduleNameToFileName(moduleName: string, containingFile: string): string|null {
|
||||
if (!containingFile || !containingFile.length) {
|
||||
if (moduleName.indexOf('.') === 0) {
|
||||
throw new Error('Resolution of relative paths requires a containing file.');
|
||||
}
|
||||
// Any containing file gives the same result for absolute imports
|
||||
containingFile = path.join('/', 'index.ts');
|
||||
}
|
||||
moduleName = moduleName.replace(EXT, '');
|
||||
const resolved = ts.resolveModuleName(
|
||||
moduleName, containingFile.replace(/\\/g, '/'),
|
||||
{baseDir: '/', genDir: '/'}, this.resolveModuleNameHost)
|
||||
.resolvedModule;
|
||||
return resolved ? resolved.resolvedFileName : null;
|
||||
}
|
||||
|
||||
getOutputName(filePath: string) {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
resourceNameToFileName(resourceName: string, containingFile: string) {
|
||||
// Note: we convert package paths into relative paths to be compatible with the the
|
||||
// previous implementation of UrlResolver.
|
||||
if (resourceName && resourceName.charAt(0) !== '.' && !path.isAbsolute(resourceName)) {
|
||||
resourceName = `./${resourceName}`;
|
||||
}
|
||||
const filePathWithNgResource =
|
||||
this.moduleNameToFileName(addNgResourceSuffix(resourceName), containingFile);
|
||||
return filePathWithNgResource ? stripNgResourceSuffix(filePathWithNgResource) : null;
|
||||
}
|
||||
|
||||
// AotSummaryResolverHost
|
||||
loadSummary(filePath: string): string|null {
|
||||
return this.tsHost.readFile(filePath);
|
||||
}
|
||||
|
||||
isSourceFile(sourceFilePath: string): boolean {
|
||||
return !GENERATED_FILES.test(sourceFilePath) &&
|
||||
(this.dtsAreSource || !DTS.test(sourceFilePath));
|
||||
}
|
||||
|
||||
toSummaryFileName(filePath: string): string {
|
||||
return filePath.replace(EXT, '') + '.d.ts';
|
||||
}
|
||||
|
||||
fromSummaryFileName(filePath: string): string {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
// AotCompilerHost
|
||||
fileNameToModuleName(importedFile: string, containingFile: string): string {
|
||||
return importedFile.replace(EXT, '');
|
||||
}
|
||||
|
||||
loadResource(path: string): string {
|
||||
if (this.tsHost.fileExists(path)) {
|
||||
return this.tsHost.readFile(path);
|
||||
} else {
|
||||
throw new Error(`Resource ${path} not found.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class MockMetadataBundlerHost implements MetadataBundlerHost {
|
||||
private collector = new MetadataCollector();
|
||||
|
||||
constructor(private host: ts.CompilerHost) {}
|
||||
|
||||
getMetadataFor(moduleName: string): ModuleMetadata|undefined {
|
||||
const source = this.host.getSourceFile(moduleName + '.ts', ts.ScriptTarget.Latest);
|
||||
return source && this.collector.getMetadata(source);
|
||||
}
|
||||
}
|
||||
|
||||
function find(fileName: string, data: MockFileOrDirectory|undefined): MockFileOrDirectory|
|
||||
undefined {
|
||||
if (!data) return undefined;
|
||||
const names = fileName.split('/');
|
||||
if (names.length && !names[0].length) names.shift();
|
||||
let current: MockFileOrDirectory|undefined = data;
|
||||
for (const name of names) {
|
||||
if (typeof current !== 'object') {
|
||||
return undefined;
|
||||
}
|
||||
current = current[name];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function open(fileName: string, data: MockFileOrDirectory|undefined): string|undefined {
|
||||
let result = find(fileName, data);
|
||||
if (typeof result === 'string') {
|
||||
return result;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function directoryExists(dirname: string, data: MockFileOrDirectory|undefined): boolean {
|
||||
let result = find(dirname, data);
|
||||
return !!result && typeof result !== 'string';
|
||||
}
|
||||
|
||||
export type MockFileArray = {
|
||||
fileName: string,
|
||||
content: string
|
||||
}[];
|
||||
|
||||
export type MockData = MockDirectory|Map<string, string>|(MockDirectory|Map<string, string>)[];
|
||||
|
||||
export function toMockFileArray(data: MockData, target: MockFileArray = []): MockFileArray {
|
||||
if (data instanceof Map) {
|
||||
mapToMockFileArray(data, target);
|
||||
} else if (Array.isArray(data)) {
|
||||
data.forEach(entry => toMockFileArray(entry, target));
|
||||
} else {
|
||||
mockDirToFileArray(data, '', target);
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
function mockDirToFileArray(dir: MockDirectory, path: string, target: MockFileArray) {
|
||||
Object.keys(dir).forEach((localFileName) => {
|
||||
const value = dir[localFileName]!;
|
||||
const fileName = `${path}/${localFileName}`;
|
||||
if (typeof value === 'string') {
|
||||
target.push({fileName, content: value});
|
||||
} else {
|
||||
mockDirToFileArray(value, fileName, target);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function mapToMockFileArray(files: Map<string, string>, target: MockFileArray) {
|
||||
files.forEach((content, fileName) => {
|
||||
target.push({fileName, content});
|
||||
});
|
||||
}
|
||||
|
||||
export function arrayToMockMap(arr: MockFileArray): Map<string, string> {
|
||||
const map = new Map<string, string>();
|
||||
arr.forEach(({fileName, content}) => {
|
||||
map.set(fileName, content);
|
||||
});
|
||||
return map;
|
||||
}
|
||||
|
||||
export function arrayToMockDir(arr: MockFileArray): MockDirectory {
|
||||
const rootDir: MockDirectory = {};
|
||||
arr.forEach(({fileName, content}) => {
|
||||
let pathParts = fileName.split('/');
|
||||
// trim trailing slash
|
||||
let startIndex = pathParts[0] ? 0 : 1;
|
||||
// get/create the directory
|
||||
let currentDir = rootDir;
|
||||
for (let i = startIndex; i < pathParts.length - 1; i++) {
|
||||
const pathPart = pathParts[i];
|
||||
let localDir = <MockDirectory>currentDir[pathPart];
|
||||
if (!localDir) {
|
||||
currentDir[pathPart] = localDir = {};
|
||||
}
|
||||
currentDir = localDir;
|
||||
}
|
||||
// write the file
|
||||
currentDir[pathParts[pathParts.length - 1]] = content;
|
||||
});
|
||||
return rootDir;
|
||||
}
|
||||
|
||||
const minCoreIndex = `
|
||||
export * from './src/application_module';
|
||||
export * from './src/change_detection';
|
||||
export * from './src/metadata';
|
||||
export * from './src/di/metadata';
|
||||
export * from './src/di/injectable';
|
||||
export * from './src/di/injector';
|
||||
export * from './src/di/injection_token';
|
||||
export * from './src/linker';
|
||||
export * from './src/render';
|
||||
export * from './src/codegen_private_exports';
|
||||
`;
|
||||
|
||||
function readBazelWrittenFilesFrom(
|
||||
bazelPackageRoot: string, packageName: string, map: Map<string, string>,
|
||||
skip: (name: string, fullName: string) => boolean = () => false) {
|
||||
function processDirectory(dir: string, dest: string) {
|
||||
const entries = fs.readdirSync(dir);
|
||||
for (const name of entries) {
|
||||
const fullName = path.posix.join(dir, name);
|
||||
const destName = path.posix.join(dest, name);
|
||||
const stat = fs.statSync(fullName);
|
||||
if (!skip(name, fullName)) {
|
||||
if (stat.isDirectory()) {
|
||||
processDirectory(fullName, destName);
|
||||
} else {
|
||||
const content = fs.readFileSync(fullName, 'utf8');
|
||||
map.set(destName, content);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
processDirectory(bazelPackageRoot, path.posix.join('/node_modules/@angular', packageName));
|
||||
// todo: check why we always need an index.d.ts
|
||||
if (fs.existsSync(path.join(bazelPackageRoot, `${packageName}.d.ts`))) {
|
||||
const content = fs.readFileSync(path.join(bazelPackageRoot, `${packageName}.d.ts`), 'utf8');
|
||||
map.set(path.posix.join('/node_modules/@angular', packageName, 'index.d.ts'), content);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Consider adding //packages/${
|
||||
packageName} as a data dependency in the BUILD.bazel rule for the failing test`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
export function isInBazel(): boolean {
|
||||
return process.env.TEST_SRCDIR != null;
|
||||
}
|
||||
|
||||
export function setup(options: {
|
||||
compileAngular: boolean,
|
||||
compileFakeCore?: boolean, compileAnimations: boolean,
|
||||
compileCommon?: boolean
|
||||
} = {
|
||||
compileAngular: true,
|
||||
compileAnimations: true,
|
||||
}) {
|
||||
let angularFiles = new Map<string, string>();
|
||||
|
||||
beforeAll(() => {
|
||||
const sources = process.env.TEST_SRCDIR;
|
||||
if (sources) {
|
||||
// If running under bazel then we get the compiled version of the files from the bazel package
|
||||
// output.
|
||||
const bundles = new Set([
|
||||
'bundles', 'esm2015', 'esm5', 'testing', 'testing.d.ts', 'testing.metadata.json', 'browser',
|
||||
'browser.d.ts'
|
||||
]);
|
||||
const skipDirs = (name: string) => bundles.has(name);
|
||||
if (options.compileAngular) {
|
||||
// If this fails please add //packages/core:npm_package as a test data dependency.
|
||||
readBazelWrittenFilesFrom(
|
||||
resolveNpmTreeArtifact('npm/node_modules/@angular/core-12'), 'core', angularFiles,
|
||||
skipDirs);
|
||||
}
|
||||
if (options.compileAnimations) {
|
||||
// If this fails please add //packages/animations:npm_package as a test data dependency.
|
||||
readBazelWrittenFilesFrom(
|
||||
resolveNpmTreeArtifact('npm/node_modules/@angular/animations-12'), 'animations',
|
||||
angularFiles, skipDirs);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.compileAngular) {
|
||||
const emittingHost = new EmittingCompilerHost([], {emitMetadata: true});
|
||||
emittingHost.addScript('@angular/core/index.ts', minCoreIndex);
|
||||
const emittingProgram = ts.createProgram(emittingHost.scripts, settings, emittingHost);
|
||||
emittingProgram.emit();
|
||||
emittingHost.writtenAngularFiles(angularFiles);
|
||||
}
|
||||
if (options.compileAnimations) {
|
||||
const emittingHost =
|
||||
new EmittingCompilerHost(['@angular/animations/index.ts'], {emitMetadata: true});
|
||||
const emittingProgram = ts.createProgram(emittingHost.scripts, settings, emittingHost);
|
||||
emittingProgram.emit();
|
||||
emittingHost.writtenAngularFiles(angularFiles);
|
||||
}
|
||||
});
|
||||
|
||||
return angularFiles;
|
||||
}
|
||||
|
||||
export function expectNoDiagnostics(program: ts.Program) {
|
||||
function fileInfo(diagnostic: ts.Diagnostic): string {
|
||||
if (diagnostic.file) {
|
||||
return `${diagnostic.file.fileName}(${diagnostic.start}): `;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function chars(len: number, ch: string): string {
|
||||
return newArray(len, ch).join('');
|
||||
}
|
||||
|
||||
function lineNoOf(offset: number, text: string): number {
|
||||
let result = 1;
|
||||
for (let i = 0; i < offset; i++) {
|
||||
if (text[i] == '\n') result++;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function lineInfo(diagnostic: ts.Diagnostic): string {
|
||||
if (diagnostic.file) {
|
||||
const start = diagnostic.start!;
|
||||
let end = diagnostic.start! + diagnostic.length!;
|
||||
const source = diagnostic.file.text;
|
||||
let lineStart = start;
|
||||
let lineEnd = end;
|
||||
while (lineStart > 0 && source[lineStart] != '\n') lineStart--;
|
||||
if (lineStart < start) lineStart++;
|
||||
while (lineEnd < source.length && source[lineEnd] != '\n') lineEnd++;
|
||||
let line = source.substring(lineStart, lineEnd);
|
||||
const lineIndex = line.indexOf('/n');
|
||||
if (lineIndex > 0) {
|
||||
line = line.substr(0, lineIndex);
|
||||
end = start + lineIndex;
|
||||
}
|
||||
const lineNo = lineNoOf(start, source) + ': ';
|
||||
return '\n' + lineNo + line + '\n' + chars(start - lineStart + lineNo.length, ' ') +
|
||||
chars(end - start, '^');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function expectNoDiagnostics(diagnostics: ReadonlyArray<ts.Diagnostic>) {
|
||||
if (diagnostics && diagnostics.length) {
|
||||
throw new Error(
|
||||
'Errors from TypeScript:\n' +
|
||||
diagnostics
|
||||
.map(
|
||||
d => `${fileInfo(d)}${ts.flattenDiagnosticMessageText(d.messageText, '\n')}${
|
||||
lineInfo(d)}`)
|
||||
.join(' \n'));
|
||||
}
|
||||
}
|
||||
expectNoDiagnostics(program.getOptionsDiagnostics());
|
||||
expectNoDiagnostics(program.getSyntacticDiagnostics());
|
||||
expectNoDiagnostics(program.getSemanticDiagnostics());
|
||||
}
|
||||
|
||||
export function isSource(fileName: string): boolean {
|
||||
return !isDts(fileName) && /\.ts$/.test(fileName);
|
||||
}
|
||||
|
||||
function isDts(fileName: string): boolean {
|
||||
return /\.d.ts$/.test(fileName);
|
||||
}
|
||||
|
||||
function isSourceOrDts(fileName: string): boolean {
|
||||
return /\.ts$/.test(fileName) && !/(ngfactory|ngstyle|ngsummary).d.ts$/.test(fileName);
|
||||
}
|
||||
|
||||
function resolveNpmTreeArtifact(manifestPath: string, resolveFile = 'package.json') {
|
||||
return path.dirname(require.resolve(path.posix.join(manifestPath, resolveFile)));
|
||||
}
|
||||
|
||||
export function compile(
|
||||
rootDirs: MockData, options: {
|
||||
emit?: boolean,
|
||||
useSummaries?: boolean,
|
||||
preCompile?: (program: ts.Program) => void,
|
||||
postCompile?: (program: ts.Program) => void,
|
||||
}&AotCompilerOptions = {},
|
||||
tsOptions: ts.CompilerOptions = {}): {genFiles: GeneratedFile[], outDir: MockDirectory} {
|
||||
// when using summaries, always emit so the next step can use the results.
|
||||
const emit = options.emit || options.useSummaries;
|
||||
const preCompile = options.preCompile || (() => {});
|
||||
const postCompile = options.postCompile || expectNoDiagnostics;
|
||||
const rootDirArr = toMockFileArray(rootDirs);
|
||||
const scriptNames = rootDirArr.map(entry => entry.fileName)
|
||||
.filter(options.useSummaries ? isSource : isSourceOrDts);
|
||||
|
||||
const host = new MockCompilerHost(scriptNames, arrayToMockDir(rootDirArr));
|
||||
const aotHost = new MockAotCompilerHost(host);
|
||||
if (options.useSummaries) {
|
||||
aotHost.hideMetadata();
|
||||
aotHost.tsFilesOnly();
|
||||
}
|
||||
const tsSettings = {...settings, ...tsOptions};
|
||||
const program = ts.createProgram([...host.scriptNames], tsSettings, host);
|
||||
preCompile(program);
|
||||
const {compiler, reflector} = createAotCompiler(aotHost, options, (err) => {
|
||||
throw err;
|
||||
});
|
||||
const analyzedModules =
|
||||
compiler.analyzeModulesSync(program.getSourceFiles().map(sf => sf.fileName));
|
||||
const genFiles = compiler.emitAllImpls(analyzedModules);
|
||||
genFiles.forEach((file) => {
|
||||
const source = file.source || toTypeScript(file);
|
||||
if (isSource(file.genFileUrl)) {
|
||||
host.addScript(file.genFileUrl, source);
|
||||
} else {
|
||||
host.override(file.genFileUrl, source);
|
||||
}
|
||||
});
|
||||
const newProgram = ts.createProgram([...host.scriptNames], tsSettings, host);
|
||||
postCompile(newProgram);
|
||||
if (emit) {
|
||||
newProgram.emit();
|
||||
}
|
||||
let outDir: MockDirectory = {};
|
||||
if (emit) {
|
||||
const dtsFilesWithGenFiles = new Set<string>(genFiles.map(gf => gf.srcFileUrl).filter(isDts));
|
||||
outDir =
|
||||
arrayToMockDir(toMockFileArray([host.writtenFiles, host.overrides])
|
||||
.filter((entry) => !isSource(entry.fileName))
|
||||
.concat(rootDirArr.filter(e => dtsFilesWithGenFiles.has(e.fileName))));
|
||||
}
|
||||
return {genFiles, outDir};
|
||||
}
|
||||
|
||||
function stripNgResourceSuffix(fileName: string): string {
|
||||
return fileName.replace(/\.\$ngresource\$.*/, '');
|
||||
}
|
||||
|
||||
function addNgResourceSuffix(fileName: string): string {
|
||||
return `${fileName}.$ngresource$`;
|
||||
}
|
||||
|
||||
function extractFileNames(directory: MockDirectory): string[] {
|
||||
const result: string[] = [];
|
||||
const scan = (directory: MockDirectory, prefix: string) => {
|
||||
for (let name of Object.getOwnPropertyNames(directory)) {
|
||||
const entry = directory[name];
|
||||
const fileName = `${prefix}/${name}`;
|
||||
if (typeof entry === 'string') {
|
||||
result.push(fileName);
|
||||
} else if (entry) {
|
||||
scan(entry, fileName);
|
||||
}
|
||||
}
|
||||
};
|
||||
scan(directory, '');
|
||||
return result;
|
||||
}
|
||||
|
||||
export function emitLibrary(
|
||||
context: Map<string, string>, mockData: MockDirectory,
|
||||
scriptFiles?: string[]): Map<string, string> {
|
||||
const emittingHost = new EmittingCompilerHost(
|
||||
scriptFiles || extractFileNames(mockData), {emitMetadata: true, mockData, context});
|
||||
const emittingProgram = ts.createProgram(emittingHost.scripts, settings, emittingHost);
|
||||
expectNoDiagnostics(emittingProgram);
|
||||
emittingProgram.emit();
|
||||
return emittingHost.written;
|
||||
}
|
||||
|
||||
export function mergeMaps<K, V>(...maps: Map<K, V>[]): Map<K, V> {
|
||||
const result = new Map<K, V>();
|
||||
|
||||
for (const map of maps) {
|
||||
for (const [key, value] of Array.from(map.entries())) {
|
||||
result.set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 {MissingTranslationStrategy} from '@angular/core';
|
||||
import {CompilerConfig, preserveWhitespacesDefault} from '../src/config';
|
||||
|
||||
{
|
||||
describe('compiler config', () => {
|
||||
it('should set missing translation strategy', () => {
|
||||
const config = new CompilerConfig({missingTranslation: MissingTranslationStrategy.Error});
|
||||
expect(config.missingTranslation).toEqual(MissingTranslationStrategy.Error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('preserveWhitespacesDefault', () => {
|
||||
it('should return the default `false` setting when no preserveWhitespacesOption are provided',
|
||||
() => {
|
||||
expect(preserveWhitespacesDefault(null)).toEqual(false);
|
||||
});
|
||||
it('should return the preserveWhitespacesOption when provided as a parameter', () => {
|
||||
expect(preserveWhitespacesDefault(true)).toEqual(true);
|
||||
expect(preserveWhitespacesDefault(false)).toEqual(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -1,206 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 {core as compilerCore} from '@angular/compiler';
|
||||
import * as core from '@angular/core';
|
||||
|
||||
{
|
||||
describe('compiler core', () => {
|
||||
it('Attribute should be equal', () => {
|
||||
typeExtends<compilerCore.Attribute, core.Attribute>();
|
||||
typeExtends<core.Attribute, compilerCore.Attribute>();
|
||||
compareRuntimeShape(new core.Attribute('someName'), compilerCore.createAttribute('someName'));
|
||||
});
|
||||
|
||||
it('Inject should be equal', () => {
|
||||
typeExtends<compilerCore.Inject, core.Inject>();
|
||||
typeExtends<core.Inject, compilerCore.Inject>();
|
||||
compareRuntimeShape(new core.Inject('someName'), compilerCore.createInject('someName'));
|
||||
});
|
||||
|
||||
it('Query should be equal', () => {
|
||||
typeExtends<compilerCore.Query, core.Query>();
|
||||
typeExtends<core.Query, compilerCore.Query>();
|
||||
compareRuntimeShape(
|
||||
new core.ContentChild('someSelector'), compilerCore.createContentChild('someSelector'));
|
||||
compareRuntimeShape(
|
||||
new core.ContentChild('someSelector', {read: 'someRead'}),
|
||||
compilerCore.createContentChild('someSelector', {read: 'someRead'}));
|
||||
compareRuntimeShape(
|
||||
new core.ContentChildren('someSelector'),
|
||||
compilerCore.createContentChildren('someSelector'));
|
||||
compareRuntimeShape(
|
||||
new core.ContentChildren('someSelector', {read: 'someRead', descendants: false}),
|
||||
compilerCore.createContentChildren(
|
||||
'someSelector', {read: 'someRead', descendants: false}));
|
||||
compareRuntimeShape(
|
||||
new core.ViewChild('someSelector'), compilerCore.createViewChild('someSelector'));
|
||||
compareRuntimeShape(
|
||||
new core.ViewChild('someSelector', {read: 'someRead'}),
|
||||
compilerCore.createViewChild('someSelector', {read: 'someRead'}));
|
||||
compareRuntimeShape(
|
||||
new core.ViewChildren('someSelector'), compilerCore.createViewChildren('someSelector'));
|
||||
compareRuntimeShape(
|
||||
new core.ViewChildren('someSelector', {read: 'someRead'}),
|
||||
compilerCore.createViewChildren('someSelector', {read: 'someRead'}));
|
||||
});
|
||||
|
||||
it('Directive should be equal', () => {
|
||||
typeExtends<compilerCore.Directive, core.Directive>();
|
||||
typeExtends<core.Directive, compilerCore.Directive>();
|
||||
compareRuntimeShape(new core.Directive({}), compilerCore.createDirective({}));
|
||||
});
|
||||
|
||||
it('Component should be equal', () => {
|
||||
typeExtends<compilerCore.Component, core.Component>();
|
||||
typeExtends<core.Component, compilerCore.Component>();
|
||||
compareRuntimeShape(new core.Component({}), compilerCore.createComponent({}));
|
||||
});
|
||||
|
||||
it('Pipe should be equal', () => {
|
||||
typeExtends<compilerCore.Pipe, core.Pipe>();
|
||||
typeExtends<core.Pipe, compilerCore.Pipe>();
|
||||
compareRuntimeShape(
|
||||
new core.Pipe({name: 'someName'}), compilerCore.createPipe({name: 'someName'}));
|
||||
});
|
||||
|
||||
it('NgModule should be equal', () => {
|
||||
typeExtends<compilerCore.NgModule, core.NgModule>();
|
||||
typeExtends<core.NgModule, compilerCore.NgModule>();
|
||||
compareRuntimeShape(new core.NgModule({}), compilerCore.createNgModule({}));
|
||||
});
|
||||
|
||||
it('marker metadata should be equal', () => {
|
||||
compareRuntimeShape(new core.Injectable(), compilerCore.createInjectable());
|
||||
compareRuntimeShape(new core.Optional(), compilerCore.createOptional());
|
||||
compareRuntimeShape(new core.Self(), compilerCore.createSelf());
|
||||
compareRuntimeShape(new core.SkipSelf(), compilerCore.createSkipSelf());
|
||||
compareRuntimeShape(new core.Host(), compilerCore.createHost());
|
||||
});
|
||||
|
||||
it('InjectionToken should be equal', () => {
|
||||
compareRuntimeShape(
|
||||
new core.InjectionToken('someName'), compilerCore.createInjectionToken('someName'));
|
||||
});
|
||||
|
||||
it('non const enums should be equal', () => {
|
||||
typeExtends<compilerCore.ViewEncapsulation, core.ViewEncapsulation>();
|
||||
typeExtends<core.ViewEncapsulation, compilerCore.ViewEncapsulation>();
|
||||
|
||||
typeExtends<compilerCore.ChangeDetectionStrategy, core.ChangeDetectionStrategy>();
|
||||
typeExtends<core.ChangeDetectionStrategy, compilerCore.ChangeDetectionStrategy>();
|
||||
|
||||
typeExtends<compilerCore.SecurityContext, core.SecurityContext>();
|
||||
typeExtends<core.SecurityContext, compilerCore.SecurityContext>();
|
||||
|
||||
typeExtends<compilerCore.MissingTranslationStrategy, core.MissingTranslationStrategy>();
|
||||
typeExtends<core.MissingTranslationStrategy, compilerCore.MissingTranslationStrategy>();
|
||||
});
|
||||
|
||||
it('const enums should be equal', () => {
|
||||
const expectToBe = (val1: any, val2: any) => expect(val1).toBe(val2);
|
||||
|
||||
expectToBe(compilerCore.NodeFlags.None, core.ɵNodeFlags.None);
|
||||
expectToBe(compilerCore.NodeFlags.TypeElement, core.ɵNodeFlags.TypeElement);
|
||||
expectToBe(compilerCore.NodeFlags.TypeText, core.ɵNodeFlags.TypeText);
|
||||
expectToBe(compilerCore.NodeFlags.ProjectedTemplate, core.ɵNodeFlags.ProjectedTemplate);
|
||||
expectToBe(compilerCore.NodeFlags.CatRenderNode, core.ɵNodeFlags.CatRenderNode);
|
||||
expectToBe(compilerCore.NodeFlags.TypeNgContent, core.ɵNodeFlags.TypeNgContent);
|
||||
expectToBe(compilerCore.NodeFlags.TypePipe, core.ɵNodeFlags.TypePipe);
|
||||
expectToBe(compilerCore.NodeFlags.TypePureArray, core.ɵNodeFlags.TypePureArray);
|
||||
expectToBe(compilerCore.NodeFlags.TypePureObject, core.ɵNodeFlags.TypePureObject);
|
||||
expectToBe(compilerCore.NodeFlags.TypePurePipe, core.ɵNodeFlags.TypePurePipe);
|
||||
expectToBe(compilerCore.NodeFlags.CatPureExpression, core.ɵNodeFlags.CatPureExpression);
|
||||
expectToBe(compilerCore.NodeFlags.TypeValueProvider, core.ɵNodeFlags.TypeValueProvider);
|
||||
expectToBe(compilerCore.NodeFlags.TypeClassProvider, core.ɵNodeFlags.TypeClassProvider);
|
||||
expectToBe(compilerCore.NodeFlags.TypeFactoryProvider, core.ɵNodeFlags.TypeFactoryProvider);
|
||||
expectToBe(
|
||||
compilerCore.NodeFlags.TypeUseExistingProvider, core.ɵNodeFlags.TypeUseExistingProvider);
|
||||
expectToBe(compilerCore.NodeFlags.LazyProvider, core.ɵNodeFlags.LazyProvider);
|
||||
expectToBe(compilerCore.NodeFlags.PrivateProvider, core.ɵNodeFlags.PrivateProvider);
|
||||
expectToBe(compilerCore.NodeFlags.TypeDirective, core.ɵNodeFlags.TypeDirective);
|
||||
expectToBe(compilerCore.NodeFlags.Component, core.ɵNodeFlags.Component);
|
||||
expectToBe(
|
||||
compilerCore.NodeFlags.CatProviderNoDirective, core.ɵNodeFlags.CatProviderNoDirective);
|
||||
expectToBe(compilerCore.NodeFlags.CatProvider, core.ɵNodeFlags.CatProvider);
|
||||
expectToBe(compilerCore.NodeFlags.OnInit, core.ɵNodeFlags.OnInit);
|
||||
expectToBe(compilerCore.NodeFlags.OnDestroy, core.ɵNodeFlags.OnDestroy);
|
||||
expectToBe(compilerCore.NodeFlags.DoCheck, core.ɵNodeFlags.DoCheck);
|
||||
expectToBe(compilerCore.NodeFlags.OnChanges, core.ɵNodeFlags.OnChanges);
|
||||
expectToBe(compilerCore.NodeFlags.AfterContentInit, core.ɵNodeFlags.AfterContentInit);
|
||||
expectToBe(compilerCore.NodeFlags.AfterContentChecked, core.ɵNodeFlags.AfterContentChecked);
|
||||
expectToBe(compilerCore.NodeFlags.AfterViewInit, core.ɵNodeFlags.AfterViewInit);
|
||||
expectToBe(compilerCore.NodeFlags.AfterViewChecked, core.ɵNodeFlags.AfterViewChecked);
|
||||
expectToBe(compilerCore.NodeFlags.EmbeddedViews, core.ɵNodeFlags.EmbeddedViews);
|
||||
expectToBe(compilerCore.NodeFlags.ComponentView, core.ɵNodeFlags.ComponentView);
|
||||
expectToBe(compilerCore.NodeFlags.TypeContentQuery, core.ɵNodeFlags.TypeContentQuery);
|
||||
expectToBe(compilerCore.NodeFlags.TypeViewQuery, core.ɵNodeFlags.TypeViewQuery);
|
||||
expectToBe(compilerCore.NodeFlags.StaticQuery, core.ɵNodeFlags.StaticQuery);
|
||||
expectToBe(compilerCore.NodeFlags.DynamicQuery, core.ɵNodeFlags.DynamicQuery);
|
||||
expectToBe(compilerCore.NodeFlags.CatQuery, core.ɵNodeFlags.CatQuery);
|
||||
expectToBe(compilerCore.NodeFlags.Types, core.ɵNodeFlags.Types);
|
||||
|
||||
expectToBe(compilerCore.DepFlags.None, core.ɵDepFlags.None);
|
||||
expectToBe(compilerCore.DepFlags.SkipSelf, core.ɵDepFlags.SkipSelf);
|
||||
expectToBe(compilerCore.DepFlags.Optional, core.ɵDepFlags.Optional);
|
||||
expectToBe(compilerCore.DepFlags.Value, core.ɵDepFlags.Value);
|
||||
|
||||
expectToBe(compilerCore.InjectFlags.Default, core.InjectFlags.Default);
|
||||
expectToBe(compilerCore.InjectFlags.SkipSelf, core.InjectFlags.SkipSelf);
|
||||
expectToBe(compilerCore.InjectFlags.Self, core.InjectFlags.Self);
|
||||
expectToBe(compilerCore.InjectFlags.Host, core.InjectFlags.Host);
|
||||
expectToBe(compilerCore.InjectFlags.Optional, core.InjectFlags.Optional);
|
||||
|
||||
expectToBe(compilerCore.ArgumentType.Inline, core.ɵArgumentType.Inline);
|
||||
expectToBe(compilerCore.ArgumentType.Dynamic, core.ɵArgumentType.Dynamic);
|
||||
|
||||
expectToBe(
|
||||
compilerCore.BindingFlags.TypeElementAttribute, core.ɵBindingFlags.TypeElementAttribute);
|
||||
expectToBe(compilerCore.BindingFlags.TypeElementClass, core.ɵBindingFlags.TypeElementClass);
|
||||
expectToBe(compilerCore.BindingFlags.TypeElementStyle, core.ɵBindingFlags.TypeElementStyle);
|
||||
expectToBe(compilerCore.BindingFlags.TypeProperty, core.ɵBindingFlags.TypeProperty);
|
||||
expectToBe(compilerCore.BindingFlags.SyntheticProperty, core.ɵBindingFlags.SyntheticProperty);
|
||||
expectToBe(
|
||||
compilerCore.BindingFlags.SyntheticHostProperty,
|
||||
core.ɵBindingFlags.SyntheticHostProperty);
|
||||
expectToBe(
|
||||
compilerCore.BindingFlags.CatSyntheticProperty, core.ɵBindingFlags.CatSyntheticProperty);
|
||||
expectToBe(compilerCore.BindingFlags.Types, core.ɵBindingFlags.Types);
|
||||
|
||||
expectToBe(compilerCore.QueryBindingType.First, core.ɵQueryBindingType.First);
|
||||
expectToBe(compilerCore.QueryBindingType.All, core.ɵQueryBindingType.All);
|
||||
|
||||
expectToBe(compilerCore.QueryValueType.ElementRef, core.ɵQueryValueType.ElementRef);
|
||||
expectToBe(compilerCore.QueryValueType.RenderElement, core.ɵQueryValueType.RenderElement);
|
||||
expectToBe(compilerCore.QueryValueType.TemplateRef, core.ɵQueryValueType.TemplateRef);
|
||||
expectToBe(
|
||||
compilerCore.QueryValueType.ViewContainerRef, core.ɵQueryValueType.ViewContainerRef);
|
||||
expectToBe(compilerCore.QueryValueType.Provider, core.ɵQueryValueType.Provider);
|
||||
|
||||
expectToBe(compilerCore.ViewFlags.None, core.ɵViewFlags.None);
|
||||
expectToBe(compilerCore.ViewFlags.OnPush, core.ɵViewFlags.OnPush);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function compareRuntimeShape(a: any, b: any) {
|
||||
const keys = metadataKeys(a);
|
||||
expect(keys).toEqual(metadataKeys(b));
|
||||
keys.forEach(key => {
|
||||
expect(a[key]).toBe(b[key]);
|
||||
});
|
||||
// Need to check 'ngMetadataName' separately, as this is
|
||||
// on the prototype in @angular/core, but a regular property in @angular/compiler.
|
||||
expect(a.ngMetadataName).toBe(b.ngMetadataName);
|
||||
}
|
||||
|
||||
function metadataKeys(a: any): string[] {
|
||||
return Object.keys(a).filter(prop => prop !== 'ngMetadataName' && !prop.startsWith('_')).sort();
|
||||
}
|
||||
|
||||
function typeExtends<T1 extends T2, T2>() {}
|
||||
|
|
@ -1,141 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 {hasLifecycleHook as hasLifecycleHookImpl, LifecycleHooks as Hooks} from '@angular/compiler/src/lifecycle_reflector';
|
||||
import {SimpleChanges} from '@angular/core';
|
||||
import {JitReflector} from '@angular/platform-browser-dynamic/src/compiler_reflector';
|
||||
|
||||
function hasLifecycleHook(hook: Hooks, directive: any): boolean {
|
||||
return hasLifecycleHookImpl(new JitReflector(), hook, directive);
|
||||
}
|
||||
|
||||
{
|
||||
describe('Create Directive', () => {
|
||||
describe('lifecycle', () => {
|
||||
describe('ngOnChanges', () => {
|
||||
it('should be true when the directive has the ngOnChanges method', () => {
|
||||
expect(hasLifecycleHook(Hooks.OnChanges, DirectiveWithOnChangesMethod)).toBe(true);
|
||||
});
|
||||
|
||||
it('should be false otherwise', () => {
|
||||
expect(hasLifecycleHook(Hooks.OnChanges, DirectiveNoHooks)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ngOnDestroy', () => {
|
||||
it('should be true when the directive has the ngOnDestroy method', () => {
|
||||
expect(hasLifecycleHook(Hooks.OnDestroy, DirectiveWithOnDestroyMethod)).toBe(true);
|
||||
});
|
||||
|
||||
it('should be false otherwise', () => {
|
||||
expect(hasLifecycleHook(Hooks.OnDestroy, DirectiveNoHooks)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ngOnInit', () => {
|
||||
it('should be true when the directive has the ngOnInit method', () => {
|
||||
expect(hasLifecycleHook(Hooks.OnInit, DirectiveWithOnInitMethod)).toBe(true);
|
||||
});
|
||||
|
||||
it('should be false otherwise', () => {
|
||||
expect(hasLifecycleHook(Hooks.OnInit, DirectiveNoHooks)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ngDoCheck', () => {
|
||||
it('should be true when the directive has the ngDoCheck method', () => {
|
||||
expect(hasLifecycleHook(Hooks.DoCheck, DirectiveWithOnCheckMethod)).toBe(true);
|
||||
});
|
||||
|
||||
it('should be false otherwise', () => {
|
||||
expect(hasLifecycleHook(Hooks.DoCheck, DirectiveNoHooks)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ngAfterContentInit', () => {
|
||||
it('should be true when the directive has the ngAfterContentInit method', () => {
|
||||
expect(hasLifecycleHook(Hooks.AfterContentInit, DirectiveWithAfterContentInitMethod))
|
||||
.toBe(true);
|
||||
});
|
||||
|
||||
it('should be false otherwise', () => {
|
||||
expect(hasLifecycleHook(Hooks.AfterContentInit, DirectiveNoHooks)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ngAfterContentChecked', () => {
|
||||
it('should be true when the directive has the ngAfterContentChecked method', () => {
|
||||
expect(
|
||||
hasLifecycleHook(Hooks.AfterContentChecked, DirectiveWithAfterContentCheckedMethod))
|
||||
.toBe(true);
|
||||
});
|
||||
|
||||
it('should be false otherwise', () => {
|
||||
expect(hasLifecycleHook(Hooks.AfterContentChecked, DirectiveNoHooks)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('ngAfterViewInit', () => {
|
||||
it('should be true when the directive has the ngAfterViewInit method', () => {
|
||||
expect(hasLifecycleHook(Hooks.AfterViewInit, DirectiveWithAfterViewInitMethod))
|
||||
.toBe(true);
|
||||
});
|
||||
|
||||
it('should be false otherwise', () => {
|
||||
expect(hasLifecycleHook(Hooks.AfterViewInit, DirectiveNoHooks)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ngAfterViewChecked', () => {
|
||||
it('should be true when the directive has the ngAfterViewChecked method', () => {
|
||||
expect(hasLifecycleHook(Hooks.AfterViewChecked, DirectiveWithAfterViewCheckedMethod))
|
||||
.toBe(true);
|
||||
});
|
||||
|
||||
it('should be false otherwise', () => {
|
||||
expect(hasLifecycleHook(Hooks.AfterViewChecked, DirectiveNoHooks)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class DirectiveNoHooks {}
|
||||
|
||||
class DirectiveWithOnChangesMethod {
|
||||
ngOnChanges(_: SimpleChanges) {}
|
||||
}
|
||||
|
||||
class DirectiveWithOnInitMethod {
|
||||
ngOnInit() {}
|
||||
}
|
||||
|
||||
class DirectiveWithOnCheckMethod {
|
||||
ngDoCheck() {}
|
||||
}
|
||||
|
||||
class DirectiveWithOnDestroyMethod {
|
||||
ngOnDestroy() {}
|
||||
}
|
||||
|
||||
class DirectiveWithAfterContentInitMethod {
|
||||
ngAfterContentInit() {}
|
||||
}
|
||||
|
||||
class DirectiveWithAfterContentCheckedMethod {
|
||||
ngAfterContentChecked() {}
|
||||
}
|
||||
|
||||
class DirectiveWithAfterViewInitMethod {
|
||||
ngAfterViewInit() {}
|
||||
}
|
||||
|
||||
class DirectiveWithAfterViewCheckedMethod {
|
||||
ngAfterViewChecked() {}
|
||||
}
|
||||
|
|
@ -1,403 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 {CompileStylesheetMetadata, CompileTemplateMetadata} from '@angular/compiler/src/compile_metadata';
|
||||
import {CompilerConfig, preserveWhitespacesDefault} from '@angular/compiler/src/config';
|
||||
import {DirectiveNormalizer} from '@angular/compiler/src/directive_normalizer';
|
||||
import {ResourceLoader} from '@angular/compiler/src/resource_loader';
|
||||
import {ViewEncapsulation} from '@angular/core/src/metadata/view';
|
||||
import {inject, TestBed} from '@angular/core/testing';
|
||||
|
||||
import {noUndefined} from '../src/util';
|
||||
|
||||
import {TEST_COMPILER_PROVIDERS} from './test_bindings';
|
||||
|
||||
const SOME_MODULE_URL = 'package:some/module/a.js';
|
||||
const SOME_HTTP_MODULE_URL = 'http://some/module/a.js';
|
||||
|
||||
function normalizeTemplate(normalizer: DirectiveNormalizer, o: {
|
||||
moduleUrl?: string;
|
||||
template?: string | null;
|
||||
templateUrl?: string | null;
|
||||
styles?: string[];
|
||||
styleUrls?: string[];
|
||||
interpolation?: [string, string] | null;
|
||||
encapsulation?: ViewEncapsulation | null;
|
||||
animations?: any[];
|
||||
preserveWhitespaces?: boolean | null;
|
||||
}) {
|
||||
return normalizer.normalizeTemplate({
|
||||
ngModuleType: null,
|
||||
componentType: SomeComp,
|
||||
moduleUrl: noUndefined(o.moduleUrl || SOME_MODULE_URL),
|
||||
template: noUndefined(o.template),
|
||||
templateUrl: noUndefined(o.templateUrl),
|
||||
styles: noUndefined(o.styles),
|
||||
styleUrls: noUndefined(o.styleUrls),
|
||||
interpolation: noUndefined(o.interpolation),
|
||||
encapsulation: noUndefined(o.encapsulation),
|
||||
animations: noUndefined(o.animations),
|
||||
preserveWhitespaces: noUndefined(o.preserveWhitespaces),
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
describe('DirectiveNormalizer', () => {
|
||||
let resourceLoaderSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
resourceLoaderSpy =
|
||||
jasmine.createSpy('get').and.callFake((url: string) => `resource(${url})`);
|
||||
const resourceLoader = {get: resourceLoaderSpy};
|
||||
TestBed.configureCompiler({
|
||||
providers: [...TEST_COMPILER_PROVIDERS, {provide: ResourceLoader, useValue: resourceLoader}]
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeTemplate', () => {
|
||||
it('should throw if no template was specified',
|
||||
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
|
||||
expect(() => normalizeTemplate(normalizer, {}))
|
||||
.toThrowError('No template specified for component SomeComp');
|
||||
}));
|
||||
it('should throw if template is not a string',
|
||||
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
|
||||
expect(() => normalizeTemplate(normalizer, {template: <any> {}}))
|
||||
.toThrowError('The template specified for component SomeComp is not a string');
|
||||
}));
|
||||
it('should throw if templateUrl is not a string',
|
||||
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
|
||||
expect(() => normalizeTemplate(normalizer, {templateUrl: <any> {}}))
|
||||
.toThrowError('The templateUrl specified for component SomeComp is not a string');
|
||||
}));
|
||||
it('should throw if both template and templateUrl are defined',
|
||||
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
|
||||
expect(() => normalizeTemplate(normalizer, {
|
||||
template: '',
|
||||
templateUrl: '',
|
||||
}))
|
||||
.toThrowError(`'SomeComp' component cannot define both template and templateUrl`);
|
||||
}));
|
||||
it('should throw if preserveWhitespaces is not a boolean',
|
||||
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
|
||||
expect(() => normalizeTemplate(normalizer, {
|
||||
template: '',
|
||||
preserveWhitespaces: <any>'WRONG',
|
||||
}))
|
||||
.toThrowError(
|
||||
'The preserveWhitespaces option for component SomeComp must be a boolean');
|
||||
}));
|
||||
});
|
||||
|
||||
describe('inline template', () => {
|
||||
it('should store the template',
|
||||
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
|
||||
const template = <CompileTemplateMetadata>normalizeTemplate(normalizer, {
|
||||
template: 'a',
|
||||
});
|
||||
expect(template.template).toEqual('a');
|
||||
expect(template.templateUrl).toEqual('package:some/module/a.js');
|
||||
expect(template.isInline).toBe(true);
|
||||
}));
|
||||
|
||||
it('should resolve styles on the annotation against the moduleUrl',
|
||||
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
|
||||
const template = <CompileTemplateMetadata>normalizeTemplate(
|
||||
normalizer, {template: '', styleUrls: ['test.css']});
|
||||
expect(template.styleUrls).toEqual(['package:some/module/test.css']);
|
||||
}));
|
||||
|
||||
it('should resolve styles in the template against the moduleUrl and add them to the styles',
|
||||
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
|
||||
const template = <CompileTemplateMetadata>normalizeTemplate(normalizer, {
|
||||
template: '<style>template @import test.css</style>',
|
||||
styles: ['direct'],
|
||||
});
|
||||
expect(template.styles).toEqual([
|
||||
'direct', 'template ', 'resource(package:some/module/test.css)'
|
||||
]);
|
||||
}));
|
||||
|
||||
it('should use ViewEncapsulation.Emulated by default',
|
||||
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
|
||||
const template = <CompileTemplateMetadata>normalizeTemplate(
|
||||
normalizer, {template: '', styleUrls: ['test.css']});
|
||||
expect(template.encapsulation).toEqual(ViewEncapsulation.Emulated);
|
||||
}));
|
||||
|
||||
it('should use default encapsulation provided by CompilerConfig',
|
||||
inject(
|
||||
[CompilerConfig, DirectiveNormalizer],
|
||||
(config: CompilerConfig, normalizer: DirectiveNormalizer) => {
|
||||
config.defaultEncapsulation = ViewEncapsulation.None;
|
||||
const template = <CompileTemplateMetadata>normalizeTemplate(
|
||||
normalizer, {template: '', styleUrls: ['test.css']});
|
||||
expect(template.encapsulation).toEqual(ViewEncapsulation.None);
|
||||
}));
|
||||
});
|
||||
|
||||
it('should load a template from a url that is resolved against moduleUrl',
|
||||
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
|
||||
const template = <CompileTemplateMetadata>normalizeTemplate(
|
||||
normalizer, {templateUrl: 'sometplurl.html', styleUrls: ['test.css']});
|
||||
expect(template.template).toEqual('resource(package:some/module/sometplurl.html)');
|
||||
expect(template.templateUrl).toEqual('package:some/module/sometplurl.html');
|
||||
expect(template.isInline).toBe(false);
|
||||
}));
|
||||
|
||||
it('should resolve styles on the annotation against the moduleUrl',
|
||||
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
|
||||
const template = <CompileTemplateMetadata>normalizeTemplate(
|
||||
normalizer, {templateUrl: 'tpl/sometplurl.html', styleUrls: ['test.css']});
|
||||
expect(template.styleUrls).toEqual(['package:some/module/test.css']);
|
||||
}));
|
||||
|
||||
it('should resolve styles in the template against the templateUrl and add them to the styles',
|
||||
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
|
||||
resourceLoaderSpy.and.callFake((url: string) => {
|
||||
switch (url) {
|
||||
case 'package:some/module/tpl/sometplurl.html':
|
||||
return '<style>template @import test.css</style>';
|
||||
default:
|
||||
return `resource(${url})`;
|
||||
}
|
||||
});
|
||||
const template = <CompileTemplateMetadata>normalizeTemplate(
|
||||
normalizer, {templateUrl: 'tpl/sometplurl.html', styles: ['direct']});
|
||||
expect(template.styles).toEqual([
|
||||
'direct', 'template ', 'resource(package:some/module/tpl/test.css)'
|
||||
]);
|
||||
}));
|
||||
|
||||
describe('externalStylesheets', () => {
|
||||
it('should load an external stylesheet',
|
||||
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
|
||||
const template = <CompileTemplateMetadata>normalizeTemplate(
|
||||
normalizer, {template: '', styleUrls: ['package:some/module/test.css']});
|
||||
expect(template.externalStylesheets.length).toBe(1);
|
||||
expect(template.externalStylesheets[0]).toEqual(new CompileStylesheetMetadata({
|
||||
moduleUrl: 'package:some/module/test.css',
|
||||
styles: ['resource(package:some/module/test.css)'],
|
||||
}));
|
||||
}));
|
||||
|
||||
it('should load stylesheets referenced by external stylesheets and inline them',
|
||||
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
|
||||
resourceLoaderSpy.and.callFake((url: string) => {
|
||||
switch (url) {
|
||||
case 'package:some/module/test.css':
|
||||
return 'a@import "test2.css"';
|
||||
case 'package:some/module/test2.css':
|
||||
return 'b';
|
||||
default:
|
||||
throw new Error(`Unexpected url ${url}`);
|
||||
}
|
||||
});
|
||||
const template = <CompileTemplateMetadata>normalizeTemplate(normalizer, {
|
||||
template: '',
|
||||
styleUrls: ['package:some/module/test.css'],
|
||||
});
|
||||
expect(template.externalStylesheets.length).toBe(1);
|
||||
expect(template.externalStylesheets[0])
|
||||
.toEqual(new CompileStylesheetMetadata(
|
||||
{moduleUrl: 'package:some/module/test.css', styles: ['a', 'b'], styleUrls: []}));
|
||||
}));
|
||||
});
|
||||
|
||||
describe('caching', () => {
|
||||
it('should work for templateUrl',
|
||||
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
|
||||
const prenormMeta = {
|
||||
templateUrl: 'cmp.html',
|
||||
};
|
||||
const template1 = <CompileTemplateMetadata>normalizeTemplate(normalizer, prenormMeta);
|
||||
const template2 = <CompileTemplateMetadata>normalizeTemplate(normalizer, prenormMeta);
|
||||
expect(template1.template).toEqual('resource(package:some/module/cmp.html)');
|
||||
expect(template2.template).toEqual('resource(package:some/module/cmp.html)');
|
||||
|
||||
expect(resourceLoaderSpy).toHaveBeenCalledTimes(1);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('normalizeLoadedTemplate', () => {
|
||||
it('should store the viewEncapsulation in the result',
|
||||
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
|
||||
const viewEncapsulation = ViewEncapsulation.ShadowDom;
|
||||
const template = <CompileTemplateMetadata>normalizeTemplate(normalizer, {
|
||||
encapsulation: viewEncapsulation,
|
||||
template: '',
|
||||
});
|
||||
expect(template.encapsulation).toBe(viewEncapsulation);
|
||||
}));
|
||||
|
||||
it('should use preserveWhitespaces setting from compiler config if none provided',
|
||||
inject(
|
||||
[DirectiveNormalizer, CompilerConfig],
|
||||
(normalizer: DirectiveNormalizer, config: CompilerConfig) => {
|
||||
const template =
|
||||
<CompileTemplateMetadata>normalizeTemplate(normalizer, {template: ''});
|
||||
expect(template.preserveWhitespaces).toBe(config.preserveWhitespaces);
|
||||
}));
|
||||
|
||||
it('should store the preserveWhitespaces=false in the result',
|
||||
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
|
||||
const template = <CompileTemplateMetadata>normalizeTemplate(
|
||||
normalizer, {preserveWhitespaces: false, template: ''});
|
||||
expect(template.preserveWhitespaces).toBe(false);
|
||||
}));
|
||||
|
||||
it('should store the preserveWhitespaces=true in the result',
|
||||
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
|
||||
const template = <CompileTemplateMetadata>normalizeTemplate(
|
||||
normalizer, {preserveWhitespaces: true, template: ''});
|
||||
expect(template.preserveWhitespaces).toBe(true);
|
||||
}));
|
||||
|
||||
it('should keep the template as html',
|
||||
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
|
||||
const template = <CompileTemplateMetadata>normalizeTemplate(normalizer, {
|
||||
template: 'a',
|
||||
});
|
||||
expect(template.template).toEqual('a');
|
||||
}));
|
||||
|
||||
it('should collect ngContent',
|
||||
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
|
||||
const template = <CompileTemplateMetadata>normalizeTemplate(normalizer, {
|
||||
template: '<ng-content select="a"></ng-content>',
|
||||
});
|
||||
expect(template.ngContentSelectors).toEqual(['a']);
|
||||
}));
|
||||
|
||||
it('should normalize ngContent wildcard selector',
|
||||
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
|
||||
const template = <CompileTemplateMetadata>normalizeTemplate(normalizer, {
|
||||
template:
|
||||
'<ng-content></ng-content><ng-content select></ng-content><ng-content select="*"></ng-content>',
|
||||
});
|
||||
expect(template.ngContentSelectors).toEqual(['*', '*', '*']);
|
||||
}));
|
||||
|
||||
it('should collect top level styles in the template',
|
||||
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
|
||||
const template = <CompileTemplateMetadata>normalizeTemplate(normalizer, {
|
||||
template: '<style>a</style>',
|
||||
});
|
||||
expect(template.styles).toEqual(['a']);
|
||||
}));
|
||||
|
||||
it('should collect styles inside elements',
|
||||
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
|
||||
const template = <CompileTemplateMetadata>normalizeTemplate(normalizer, {
|
||||
template: '<div><style>a</style></div>',
|
||||
});
|
||||
expect(template.styles).toEqual(['a']);
|
||||
}));
|
||||
|
||||
it('should collect styleUrls in the template and add them to the styles',
|
||||
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
|
||||
const template = <CompileTemplateMetadata>normalizeTemplate(normalizer, {
|
||||
template: '<link rel="stylesheet" href="aUrl">',
|
||||
});
|
||||
expect(template.styles).toEqual(['resource(package:some/module/aUrl)']);
|
||||
expect(template.styleUrls).toEqual([]);
|
||||
}));
|
||||
|
||||
it('should collect styleUrls in elements and add them to the styles',
|
||||
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
|
||||
const template = <CompileTemplateMetadata>normalizeTemplate(normalizer, {
|
||||
template: '<div><link rel="stylesheet" href="aUrl"></div>',
|
||||
});
|
||||
expect(template.styles).toEqual(['resource(package:some/module/aUrl)']);
|
||||
expect(template.styleUrls).toEqual([]);
|
||||
}));
|
||||
|
||||
it('should ignore link elements with non stylesheet rel attribute',
|
||||
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
|
||||
const template = <CompileTemplateMetadata>normalizeTemplate(normalizer, {
|
||||
template: '<link href="b" rel="a">',
|
||||
});
|
||||
expect(template.styleUrls).toEqual([]);
|
||||
}));
|
||||
|
||||
it('should ignore link elements with absolute urls but non package: scheme',
|
||||
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
|
||||
const template = <CompileTemplateMetadata>normalizeTemplate(normalizer, {
|
||||
template: '<link href="http://some/external.css" rel="stylesheet">',
|
||||
});
|
||||
expect(template.styleUrls).toEqual([]);
|
||||
}));
|
||||
|
||||
it('should extract @import style urls and add them to the styles',
|
||||
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
|
||||
const template = <CompileTemplateMetadata>normalizeTemplate(normalizer, {
|
||||
styles: ['@import "test.css";'],
|
||||
template: '',
|
||||
});
|
||||
expect(template.styles).toEqual(['', 'resource(package:some/module/test.css)']);
|
||||
expect(template.styleUrls).toEqual([]);
|
||||
}));
|
||||
|
||||
it('should not resolve relative urls in inline styles',
|
||||
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
|
||||
const template = <CompileTemplateMetadata>normalizeTemplate(normalizer, {
|
||||
styles: ['.foo{background-image: url(\'double.jpg\');'],
|
||||
template: '',
|
||||
});
|
||||
expect(template.styles).toEqual(['.foo{background-image: url(\'double.jpg\');']);
|
||||
}));
|
||||
|
||||
it('should resolve relative style urls in styleUrls',
|
||||
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
|
||||
const template = <CompileTemplateMetadata>normalizeTemplate(normalizer, {
|
||||
styleUrls: ['test.css'],
|
||||
template: '',
|
||||
});
|
||||
expect(template.styles).toEqual([]);
|
||||
expect(template.styleUrls).toEqual(['package:some/module/test.css']);
|
||||
}));
|
||||
|
||||
it('should resolve relative style urls in styleUrls with http directive url',
|
||||
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
|
||||
const template = <CompileTemplateMetadata>normalizeTemplate(normalizer, {
|
||||
moduleUrl: SOME_HTTP_MODULE_URL,
|
||||
styleUrls: ['test.css'],
|
||||
template: '',
|
||||
});
|
||||
expect(template.styles).toEqual([]);
|
||||
expect(template.styleUrls).toEqual(['http://some/module/test.css']);
|
||||
}));
|
||||
|
||||
it('should normalize ViewEncapsulation.Emulated to ViewEncapsulation.None if there are no styles nor stylesheets',
|
||||
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
|
||||
const template = <CompileTemplateMetadata>normalizeTemplate(normalizer, {
|
||||
encapsulation: ViewEncapsulation.Emulated,
|
||||
template: '',
|
||||
});
|
||||
expect(template.encapsulation).toEqual(ViewEncapsulation.None);
|
||||
}));
|
||||
|
||||
it('should ignore ng-content in elements with ngNonBindable',
|
||||
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
|
||||
const template = <CompileTemplateMetadata>normalizeTemplate(normalizer, {
|
||||
template: '<div ngNonBindable><ng-content select="a"></ng-content></div>',
|
||||
});
|
||||
expect(template.ngContentSelectors).toEqual([]);
|
||||
}));
|
||||
|
||||
it('should still collect <style> in elements with ngNonBindable',
|
||||
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
|
||||
const template = <CompileTemplateMetadata>normalizeTemplate(normalizer, {
|
||||
template: '<div ngNonBindable><style>div {color:red}</style></div>',
|
||||
});
|
||||
expect(template.styles).toEqual(['div {color:red}']);
|
||||
}));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class SomeComp {}
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 {Component, Directive, Injector} from '@angular/core';
|
||||
import {inject, TestBed} from '@angular/core/testing';
|
||||
import {JitReflector} from '@angular/platform-browser-dynamic/src/compiler_reflector';
|
||||
|
||||
import {MockDirectiveResolver} from '../testing';
|
||||
|
||||
{
|
||||
describe('MockDirectiveResolver', () => {
|
||||
let dirResolver: MockDirectiveResolver;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule(
|
||||
{declarations: [SomeDirective, SomeOtherDirective, SomeComponent]});
|
||||
});
|
||||
|
||||
beforeEach(inject([Injector], (injector: Injector) => {
|
||||
dirResolver = new MockDirectiveResolver(new JitReflector());
|
||||
}));
|
||||
|
||||
describe('Directive overriding', () => {
|
||||
it('should fallback to the default DirectiveResolver when templates are not overridden',
|
||||
() => {
|
||||
const ngModule = dirResolver.resolve(SomeComponent);
|
||||
expect(ngModule.selector).toEqual('cmp');
|
||||
});
|
||||
|
||||
it('should allow overriding the @Directive', () => {
|
||||
dirResolver.setDirective(SomeComponent, new Component({selector: 'someOtherSelector'}));
|
||||
const metadata = dirResolver.resolve(SomeComponent);
|
||||
expect(metadata.selector).toEqual('someOtherSelector');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Directive({selector: 'some-directive'})
|
||||
class SomeDirective {
|
||||
}
|
||||
|
||||
@Component({selector: 'cmp', template: 'template'})
|
||||
class SomeComponent {
|
||||
}
|
||||
|
||||
@Directive({selector: 'some-other-directive'})
|
||||
class SomeOtherDirective {
|
||||
}
|
||||
|
|
@ -1,451 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 {core} from '@angular/compiler';
|
||||
import {DirectiveResolver} from '@angular/compiler/src/directive_resolver';
|
||||
import {Component, ContentChild, ContentChildren, Directive, HostBinding, HostListener, Input, Output, ViewChild, ViewChildren} from '@angular/core/src/metadata';
|
||||
import {JitReflector} from '@angular/platform-browser-dynamic/src/compiler_reflector';
|
||||
|
||||
@Directive({selector: 'someDirective'})
|
||||
class SomeDirective {
|
||||
}
|
||||
|
||||
@Directive({selector: 'someDirective', inputs: ['c']})
|
||||
class SomeDirectiveWithInputs {
|
||||
@Input() a: any;
|
||||
@Input('renamed') b: any;
|
||||
c: any;
|
||||
}
|
||||
|
||||
@Directive({selector: 'someDirective', outputs: ['c']})
|
||||
class SomeDirectiveWithOutputs {
|
||||
@Output() a: any;
|
||||
@Output('renamed') b: any;
|
||||
c: any;
|
||||
}
|
||||
|
||||
@Directive({selector: 'someDirective'})
|
||||
class SomeDirectiveWithSetterProps {
|
||||
@Input('renamed')
|
||||
set a(value: any) {
|
||||
}
|
||||
}
|
||||
|
||||
@Directive({selector: 'someDirective'})
|
||||
class SomeDirectiveWithGetterOutputs {
|
||||
@Output('renamed')
|
||||
get a(): any {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Directive({selector: 'someDirective', host: {'[c]': 'c'}})
|
||||
class SomeDirectiveWithHostBindings {
|
||||
@HostBinding() a: any;
|
||||
@HostBinding('renamed') b: any;
|
||||
c: any;
|
||||
}
|
||||
|
||||
@Directive({selector: 'someDirective', host: {'(c)': 'onC()'}})
|
||||
class SomeDirectiveWithHostListeners {
|
||||
@HostListener('a')
|
||||
onA() {
|
||||
}
|
||||
@HostListener('b', ['$event.value'])
|
||||
onB(value: any) {
|
||||
}
|
||||
}
|
||||
|
||||
@Directive({selector: 'someDirective', queries: {'cs': new ContentChildren('c')}})
|
||||
class SomeDirectiveWithContentChildren {
|
||||
@ContentChildren('a') as: any;
|
||||
c: any;
|
||||
}
|
||||
|
||||
@Directive({selector: 'someDirective', queries: {'cs': new ViewChildren('c')}})
|
||||
class SomeDirectiveWithViewChildren {
|
||||
@ViewChildren('a') as: any;
|
||||
c: any;
|
||||
}
|
||||
|
||||
@Directive({selector: 'someDirective', queries: {'c': new ContentChild('c')}})
|
||||
class SomeDirectiveWithContentChild {
|
||||
@ContentChild('a') a: any;
|
||||
c: any;
|
||||
}
|
||||
|
||||
@Directive({selector: 'someDirective', queries: {'c': new ViewChild('c')}})
|
||||
class SomeDirectiveWithViewChild {
|
||||
@ViewChild('a') a: any;
|
||||
c: any;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'sample',
|
||||
template: 'some template',
|
||||
styles: ['some styles'],
|
||||
preserveWhitespaces: true
|
||||
})
|
||||
class ComponentWithTemplate {
|
||||
}
|
||||
|
||||
@Directive({
|
||||
selector: 'someDirective',
|
||||
host: {'[decorator]': 'decorator'},
|
||||
inputs: ['decorator'],
|
||||
})
|
||||
class SomeDirectiveWithSameHostBindingAndInput {
|
||||
@Input() @HostBinding() prop: any;
|
||||
}
|
||||
|
||||
@Directive({selector: 'someDirective'})
|
||||
class SomeDirectiveWithMalformedHostBinding1 {
|
||||
@HostBinding('(a)')
|
||||
onA() {
|
||||
}
|
||||
}
|
||||
|
||||
@Directive({selector: 'someDirective'})
|
||||
class SomeDirectiveWithMalformedHostBinding2 {
|
||||
@HostBinding('[a]')
|
||||
onA() {
|
||||
}
|
||||
}
|
||||
|
||||
class SomeDirectiveWithoutMetadata {}
|
||||
|
||||
{
|
||||
describe('DirectiveResolver', () => {
|
||||
let resolver: DirectiveResolver;
|
||||
|
||||
beforeEach(() => {
|
||||
resolver = new DirectiveResolver(new JitReflector());
|
||||
});
|
||||
|
||||
it('should read out the Directive metadata', () => {
|
||||
const directiveMetadata = resolver.resolve(SomeDirective);
|
||||
expect(directiveMetadata).toEqual(core.createDirective({
|
||||
selector: 'someDirective',
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
host: {},
|
||||
queries: {},
|
||||
guards: {},
|
||||
exportAs: undefined,
|
||||
providers: undefined
|
||||
}));
|
||||
});
|
||||
|
||||
it('should throw if not matching metadata is found', () => {
|
||||
expect(() => {
|
||||
resolver.resolve(SomeDirectiveWithoutMetadata);
|
||||
}).toThrowError('No Directive annotation found on SomeDirectiveWithoutMetadata');
|
||||
});
|
||||
|
||||
it('should support inheriting the Directive metadata', function() {
|
||||
@Directive({selector: 'p'})
|
||||
class Parent {
|
||||
}
|
||||
|
||||
class ChildNoDecorator extends Parent {}
|
||||
|
||||
@Directive({selector: 'c'})
|
||||
class ChildWithDecorator extends Parent {
|
||||
}
|
||||
|
||||
expect(resolver.resolve(ChildNoDecorator)).toEqual(core.createDirective({
|
||||
selector: 'p',
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
host: {},
|
||||
queries: {},
|
||||
guards: {},
|
||||
exportAs: undefined,
|
||||
providers: undefined
|
||||
}));
|
||||
|
||||
expect(resolver.resolve(ChildWithDecorator)).toEqual(core.createDirective({
|
||||
selector: 'c',
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
host: {},
|
||||
queries: {},
|
||||
guards: {},
|
||||
exportAs: undefined,
|
||||
providers: undefined
|
||||
}));
|
||||
});
|
||||
|
||||
describe('inputs', () => {
|
||||
it('should append directive inputs', () => {
|
||||
const directiveMetadata = resolver.resolve(SomeDirectiveWithInputs);
|
||||
expect(directiveMetadata.inputs).toEqual(['c', 'a', 'b: renamed']);
|
||||
});
|
||||
|
||||
it('should work with getters and setters', () => {
|
||||
const directiveMetadata = resolver.resolve(SomeDirectiveWithSetterProps);
|
||||
expect(directiveMetadata.inputs).toEqual(['a: renamed']);
|
||||
});
|
||||
|
||||
it('should remove duplicate inputs', () => {
|
||||
@Directive({selector: 'someDirective', inputs: ['a', 'a']})
|
||||
class SomeDirectiveWithDuplicateInputs {
|
||||
}
|
||||
|
||||
const directiveMetadata = resolver.resolve(SomeDirectiveWithDuplicateInputs);
|
||||
expect(directiveMetadata.inputs).toEqual(['a']);
|
||||
});
|
||||
|
||||
it('should use the last input if duplicate inputs (with rename)', () => {
|
||||
@Directive({selector: 'someDirective', inputs: ['a', 'localA: a']})
|
||||
class SomeDirectiveWithDuplicateInputs {
|
||||
}
|
||||
|
||||
const directiveMetadata = resolver.resolve(SomeDirectiveWithDuplicateInputs);
|
||||
expect(directiveMetadata.inputs).toEqual(['localA: a']);
|
||||
});
|
||||
|
||||
it('should prefer @Input over @Directive.inputs', () => {
|
||||
@Directive({selector: 'someDirective', inputs: ['a']})
|
||||
class SomeDirectiveWithDuplicateInputs {
|
||||
@Input('a') propA: any;
|
||||
}
|
||||
const directiveMetadata = resolver.resolve(SomeDirectiveWithDuplicateInputs);
|
||||
expect(directiveMetadata.inputs).toEqual(['propA: a']);
|
||||
});
|
||||
|
||||
it('should support inheriting inputs', () => {
|
||||
@Directive({selector: 'p'})
|
||||
class Parent {
|
||||
@Input() p1: any;
|
||||
@Input('p21') p2: any;
|
||||
}
|
||||
|
||||
class Child extends Parent {
|
||||
@Input('p22') override p2: any;
|
||||
@Input() p3: any;
|
||||
}
|
||||
|
||||
const directiveMetadata = resolver.resolve(Child);
|
||||
expect(directiveMetadata.inputs).toEqual(['p1', 'p2: p22', 'p3']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('outputs', () => {
|
||||
it('should append directive outputs', () => {
|
||||
const directiveMetadata = resolver.resolve(SomeDirectiveWithOutputs);
|
||||
expect(directiveMetadata.outputs).toEqual(['c', 'a', 'b: renamed']);
|
||||
});
|
||||
|
||||
it('should work with getters and setters', () => {
|
||||
const directiveMetadata = resolver.resolve(SomeDirectiveWithGetterOutputs);
|
||||
expect(directiveMetadata.outputs).toEqual(['a: renamed']);
|
||||
});
|
||||
|
||||
it('should remove duplicate outputs', () => {
|
||||
@Directive({selector: 'someDirective', outputs: ['a', 'a']})
|
||||
class SomeDirectiveWithDuplicateOutputs {
|
||||
}
|
||||
|
||||
const directiveMetadata = resolver.resolve(SomeDirectiveWithDuplicateOutputs);
|
||||
expect(directiveMetadata.outputs).toEqual(['a']);
|
||||
});
|
||||
|
||||
it('should use the last output if duplicate outputs (with rename)', () => {
|
||||
@Directive({selector: 'someDirective', outputs: ['a', 'localA: a']})
|
||||
class SomeDirectiveWithDuplicateOutputs {
|
||||
}
|
||||
|
||||
const directiveMetadata = resolver.resolve(SomeDirectiveWithDuplicateOutputs);
|
||||
expect(directiveMetadata.outputs).toEqual(['localA: a']);
|
||||
});
|
||||
|
||||
it('should prefer @Output over @Directive.outputs', () => {
|
||||
@Directive({selector: 'someDirective', outputs: ['a']})
|
||||
class SomeDirectiveWithDuplicateOutputs {
|
||||
@Output('a') propA: any;
|
||||
}
|
||||
const directiveMetadata = resolver.resolve(SomeDirectiveWithDuplicateOutputs);
|
||||
expect(directiveMetadata.outputs).toEqual(['propA: a']);
|
||||
});
|
||||
|
||||
it('should support inheriting outputs', () => {
|
||||
@Directive({selector: 'p'})
|
||||
class Parent {
|
||||
@Output() p1: any;
|
||||
@Output('p21') p2: any;
|
||||
}
|
||||
|
||||
class Child extends Parent {
|
||||
@Output('p22') override p2: any;
|
||||
@Output() p3: any;
|
||||
}
|
||||
|
||||
const directiveMetadata = resolver.resolve(Child);
|
||||
expect(directiveMetadata.outputs).toEqual(['p1', 'p2: p22', 'p3']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('host', () => {
|
||||
it('should append host bindings', () => {
|
||||
const directiveMetadata = resolver.resolve(SomeDirectiveWithHostBindings);
|
||||
expect(directiveMetadata.host).toEqual({'[c]': 'c', '[a]': 'a', '[renamed]': 'b'});
|
||||
});
|
||||
|
||||
it('should append host binding and input on the same property', () => {
|
||||
const directiveMetadata = resolver.resolve(SomeDirectiveWithSameHostBindingAndInput);
|
||||
expect(directiveMetadata.host).toEqual({'[decorator]': 'decorator', '[prop]': 'prop'});
|
||||
expect(directiveMetadata.inputs).toEqual(['decorator', 'prop']);
|
||||
});
|
||||
|
||||
it('should append host listeners', () => {
|
||||
const directiveMetadata = resolver.resolve(SomeDirectiveWithHostListeners);
|
||||
expect(directiveMetadata.host)
|
||||
.toEqual({'(c)': 'onC()', '(a)': 'onA()', '(b)': 'onB($event.value)'});
|
||||
});
|
||||
|
||||
it('should throw when @HostBinding name starts with "("', () => {
|
||||
expect(() => resolver.resolve(SomeDirectiveWithMalformedHostBinding1))
|
||||
.toThrowError('@HostBinding can not bind to events. Use @HostListener instead.');
|
||||
});
|
||||
|
||||
it('should throw when @HostBinding name starts with "["', () => {
|
||||
expect(() => resolver.resolve(SomeDirectiveWithMalformedHostBinding2))
|
||||
.toThrowError(
|
||||
`@HostBinding parameter should be a property name, 'class.<name>', or 'attr.<name>'.`);
|
||||
});
|
||||
|
||||
it('should support inheriting host bindings', () => {
|
||||
@Directive({selector: 'p'})
|
||||
class Parent {
|
||||
@HostBinding() p1: any;
|
||||
@HostBinding('p21') p2: any;
|
||||
}
|
||||
|
||||
class Child extends Parent {
|
||||
@HostBinding('p22') override p2: any;
|
||||
@HostBinding() p3: any;
|
||||
}
|
||||
|
||||
const directiveMetadata = resolver.resolve(Child);
|
||||
expect(directiveMetadata.host)
|
||||
.toEqual({'[p1]': 'p1', '[p21]': 'p2', '[p22]': 'p2', '[p3]': 'p3'});
|
||||
});
|
||||
|
||||
it('should support inheriting host listeners', () => {
|
||||
@Directive({selector: 'p'})
|
||||
class Parent {
|
||||
@HostListener('p1')
|
||||
p1() {
|
||||
}
|
||||
@HostListener('p21')
|
||||
p2() {
|
||||
}
|
||||
}
|
||||
|
||||
class Child extends Parent {
|
||||
@HostListener('p22')
|
||||
override p2() {
|
||||
}
|
||||
@HostListener('p3')
|
||||
p3() {
|
||||
}
|
||||
}
|
||||
|
||||
const directiveMetadata = resolver.resolve(Child);
|
||||
expect(directiveMetadata.host)
|
||||
.toEqual({'(p1)': 'p1()', '(p21)': 'p2()', '(p22)': 'p2()', '(p3)': 'p3()'});
|
||||
});
|
||||
|
||||
it('should combine host bindings and listeners during inheritance', () => {
|
||||
@Directive({selector: 'p'})
|
||||
class Parent {
|
||||
@HostListener('p11')
|
||||
@HostListener('p12')
|
||||
p1() {
|
||||
}
|
||||
|
||||
@HostBinding('p21') @HostBinding('p22') p2: any;
|
||||
}
|
||||
|
||||
class Child extends Parent {
|
||||
@HostListener('c1')
|
||||
override p1() {
|
||||
}
|
||||
|
||||
@HostBinding('c2') override p2: any;
|
||||
}
|
||||
|
||||
const directiveMetadata = resolver.resolve(Child);
|
||||
expect(directiveMetadata.host).toEqual({
|
||||
'(p11)': 'p1()',
|
||||
'(p12)': 'p1()',
|
||||
'(c1)': 'p1()',
|
||||
'[p21]': 'p2',
|
||||
'[p22]': 'p2',
|
||||
'[c2]': 'p2'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('queries', () => {
|
||||
it('should append ContentChildren', () => {
|
||||
const directiveMetadata = resolver.resolve(SomeDirectiveWithContentChildren);
|
||||
expect(directiveMetadata.queries)
|
||||
.toEqual({'cs': new ContentChildren('c'), 'as': new ContentChildren('a')});
|
||||
});
|
||||
|
||||
it('should append ViewChildren', () => {
|
||||
const directiveMetadata = resolver.resolve(SomeDirectiveWithViewChildren);
|
||||
expect(directiveMetadata.queries)
|
||||
.toEqual({'cs': new ViewChildren('c'), 'as': new ViewChildren('a')});
|
||||
});
|
||||
|
||||
it('should append ContentChild', () => {
|
||||
const directiveMetadata = resolver.resolve(SomeDirectiveWithContentChild);
|
||||
expect(directiveMetadata.queries)
|
||||
.toEqual({'c': new ContentChild('c'), 'a': new ContentChild('a')});
|
||||
});
|
||||
|
||||
it('should append ViewChild', () => {
|
||||
const directiveMetadata = resolver.resolve(SomeDirectiveWithViewChild);
|
||||
expect(directiveMetadata.queries)
|
||||
.toEqual({'c': new ViewChild('c'), 'a': new ViewChild('a')});
|
||||
});
|
||||
|
||||
it('should support inheriting queries', () => {
|
||||
@Directive({selector: 'p'})
|
||||
class Parent {
|
||||
@ContentChild('p1') p1: any;
|
||||
@ContentChild('p21') p2: any;
|
||||
}
|
||||
|
||||
class Child extends Parent {
|
||||
@ContentChild('p22') override p2: any;
|
||||
@ContentChild('p3') p3: any;
|
||||
}
|
||||
|
||||
const directiveMetadata = resolver.resolve(Child);
|
||||
expect(directiveMetadata.queries).toEqual({
|
||||
'p1': new ContentChild('p1'),
|
||||
'p2': new ContentChild('p22'),
|
||||
'p3': new ContentChild('p3')
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Component', () => {
|
||||
it('should read out the template related metadata from the Component metadata', () => {
|
||||
const compMetadata: Component = resolver.resolve(ComponentWithTemplate);
|
||||
expect(compMetadata.template).toEqual('some template');
|
||||
expect(compMetadata.styles).toEqual(['some styles']);
|
||||
expect(compMetadata.preserveWhitespaces).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -8,7 +8,6 @@ ts_library(
|
|||
"//packages:types",
|
||||
"//packages/compiler",
|
||||
"//packages/compiler/test/expression_parser/utils",
|
||||
"//packages/compiler/testing",
|
||||
"//packages/platform-browser/testing",
|
||||
],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ import {expect} from '@angular/platform-browser/testing/src/matchers';
|
|||
|
||||
import {configureCompiler, createComponent, HTML, serializeTranslations, validateHtml} from './integration_common';
|
||||
|
||||
describe('i18n XLIFF integration spec', () => {
|
||||
// TODO(alxhub): figure out if this test is still relevant.
|
||||
xdescribe('i18n XLIFF integration spec', () => {
|
||||
describe('(with LF line endings)', () => {
|
||||
beforeEach(waitForAsync(
|
||||
() => configureCompiler(XLIFF2_TOMERGE + LF_LINE_ENDING_XLIFF2_TOMERGE, 'xlf2')));
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ import {expect} from '@angular/platform-browser/testing/src/matchers';
|
|||
|
||||
import {configureCompiler, createComponent, HTML, serializeTranslations, validateHtml} from './integration_common';
|
||||
|
||||
describe('i18n XLIFF integration spec', () => {
|
||||
// TODO(alxhub): figure out if this test is still relevant.
|
||||
xdescribe('i18n XLIFF integration spec', () => {
|
||||
describe('(with LF line endings)', () => {
|
||||
beforeEach(
|
||||
waitForAsync(() => configureCompiler(XLIFF_TOMERGE + LF_LINE_ENDING_XLIFF_TOMERGE, 'xlf')));
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ import {expect} from '@angular/platform-browser/testing/src/matchers';
|
|||
|
||||
import {configureCompiler, createComponent, HTML, serializeTranslations, validateHtml} from './integration_common';
|
||||
|
||||
describe('i18n XMB/XTB integration spec', () => {
|
||||
// TODO(alxhub): figure out if this test is still relevant.
|
||||
xdescribe('i18n XMB/XTB integration spec', () => {
|
||||
describe('(with LF line endings)', () => {
|
||||
beforeEach(waitForAsync(() => configureCompiler(XTB + LF_LINE_ENDING_XTB, 'xtb')));
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 {Component} from '@angular/core';
|
||||
|
||||
@Component({styles: <any>('foo'), template: ''})
|
||||
export class MalformedStylesComponent {
|
||||
}
|
||||
|
|
@ -1,554 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 {LIFECYCLE_HOOKS_VALUES, LifecycleHooks} from '@angular/compiler/src/lifecycle_reflector';
|
||||
import {AfterContentChecked, AfterContentInit, AfterViewChecked, AfterViewInit, ChangeDetectionStrategy, Component, Directive, DoCheck, Injectable, NgModule, OnChanges, OnDestroy, OnInit, Pipe, SimpleChanges, ViewEncapsulation, ɵstringify as stringify} from '@angular/core';
|
||||
import {inject, TestBed, waitForAsync} from '@angular/core/testing';
|
||||
|
||||
import {CompileDiDependencyMetadata} from '../src/compile_metadata';
|
||||
import {CompileMetadataResolver} from '../src/metadata_resolver';
|
||||
import {identifierName} from '../src/parse_util';
|
||||
import {ResourceLoader} from '../src/resource_loader';
|
||||
import {MockResourceLoader} from '../testing/src/resource_loader_mock';
|
||||
|
||||
import {MalformedStylesComponent} from './metadata_resolver_fixture';
|
||||
import {TEST_COMPILER_PROVIDERS} from './test_bindings';
|
||||
|
||||
{
|
||||
describe('CompileMetadataResolver', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureCompiler({providers: TEST_COMPILER_PROVIDERS});
|
||||
});
|
||||
|
||||
it('should throw on the getDirectiveMetadata/getPipeMetadata methods if the module has not been loaded yet',
|
||||
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
|
||||
@NgModule({})
|
||||
class SomeModule {
|
||||
}
|
||||
|
||||
@Pipe({name: 'pipe'})
|
||||
class SomePipe {
|
||||
}
|
||||
|
||||
expect(() => resolver.getDirectiveMetadata(ComponentWithEverythingInline))
|
||||
.toThrowError(/Illegal state/);
|
||||
expect(() => resolver.getPipeMetadata(SomePipe)).toThrowError(/Illegal state/);
|
||||
}));
|
||||
|
||||
it('should read metadata in sync for components with inline resources',
|
||||
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
|
||||
@NgModule({declarations: [ComponentWithEverythingInline]})
|
||||
class SomeModule {
|
||||
}
|
||||
resolver.loadNgModuleDirectiveAndPipeMetadata(SomeModule, true);
|
||||
|
||||
const meta = resolver.getDirectiveMetadata(ComponentWithEverythingInline);
|
||||
expect(meta.selector).toEqual('someSelector');
|
||||
expect(meta.exportAs).toEqual('someExportAs');
|
||||
expect(meta.isComponent).toBe(true);
|
||||
expect(meta.type.reference).toBe(ComponentWithEverythingInline);
|
||||
expect(identifierName(meta.type)).toEqual(stringify(ComponentWithEverythingInline));
|
||||
expect(meta.type.lifecycleHooks).toEqual(LIFECYCLE_HOOKS_VALUES);
|
||||
expect(meta.changeDetection).toBe(ChangeDetectionStrategy.Default);
|
||||
expect(meta.inputs).toEqual({'someProp': 'someProp'});
|
||||
expect(meta.outputs).toEqual({'someEvent': 'someEvent'});
|
||||
expect(meta.hostListeners).toEqual({'someHostListener': 'someHostListenerExpr'});
|
||||
expect(meta.hostProperties).toEqual({'someHostProp': 'someHostPropExpr'});
|
||||
expect(meta.hostAttributes).toEqual({'someHostAttr': 'someHostAttrValue'});
|
||||
expect(meta.template !.encapsulation).toBe(ViewEncapsulation.Emulated);
|
||||
expect(meta.template !.styles).toEqual(['someStyle']);
|
||||
expect(meta.template !.template).toEqual('someTemplate');
|
||||
expect(meta.template !.interpolation).toEqual(['{{', '}}']);
|
||||
}));
|
||||
|
||||
it('should throw when reading metadata for component with external resources when sync=true is passed',
|
||||
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
|
||||
@NgModule({declarations: [ComponentWithExternalResources]})
|
||||
class SomeModule {
|
||||
}
|
||||
|
||||
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(SomeModule, true))
|
||||
.toThrowError(`Can't compile synchronously as ${
|
||||
stringify(ComponentWithExternalResources)} is still being loaded!`);
|
||||
}));
|
||||
|
||||
it('should read external metadata when sync=false',
|
||||
waitForAsync(inject(
|
||||
[CompileMetadataResolver, ResourceLoader],
|
||||
(resolver: CompileMetadataResolver, resourceLoader: MockResourceLoader) => {
|
||||
@NgModule({declarations: [ComponentWithExternalResources]})
|
||||
class SomeModule {
|
||||
}
|
||||
|
||||
resourceLoader.when('someTemplateUrl', 'someTemplate');
|
||||
resolver.loadNgModuleDirectiveAndPipeMetadata(SomeModule, false).then(() => {
|
||||
const meta = resolver.getDirectiveMetadata(ComponentWithExternalResources);
|
||||
expect(meta.selector).toEqual('someSelector');
|
||||
expect(meta.template !.styleUrls).toEqual(['someStyleUrl']);
|
||||
expect(meta.template !.templateUrl).toEqual('someTemplateUrl');
|
||||
expect(meta.template !.template).toEqual('someTemplate');
|
||||
});
|
||||
resourceLoader.flush();
|
||||
})));
|
||||
|
||||
it('should use `./` as base url for templates during runtime compilation if no moduleId is given',
|
||||
waitForAsync(inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
|
||||
@Component({selector: 'someComponent', templateUrl: 'someUrl'})
|
||||
class ComponentWithoutModuleId {
|
||||
}
|
||||
|
||||
|
||||
@NgModule({declarations: [ComponentWithoutModuleId]})
|
||||
class SomeModule {
|
||||
}
|
||||
|
||||
resolver.loadNgModuleDirectiveAndPipeMetadata(SomeModule, false).then(() => {
|
||||
const value: string =
|
||||
resolver.getDirectiveMetadata(ComponentWithoutModuleId).template !.templateUrl!;
|
||||
const expectedEndValue = './someUrl';
|
||||
expect(value.endsWith(expectedEndValue)).toBe(true);
|
||||
});
|
||||
})));
|
||||
|
||||
it('should throw when the moduleId is not a string',
|
||||
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
|
||||
@NgModule({declarations: [ComponentWithInvalidModuleId]})
|
||||
class SomeModule {
|
||||
}
|
||||
|
||||
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(SomeModule, true))
|
||||
.toThrowError(
|
||||
`moduleId should be a string in "ComponentWithInvalidModuleId". See` +
|
||||
` https://goo.gl/wIDDiL for more information.\n` +
|
||||
`If you're using Webpack you should inline the template and the styles, see` +
|
||||
` https://goo.gl/X2J8zc.`);
|
||||
}));
|
||||
|
||||
|
||||
it('should throw when metadata is incorrectly typed',
|
||||
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
|
||||
@NgModule({declarations: [MalformedStylesComponent]})
|
||||
class SomeModule {
|
||||
}
|
||||
|
||||
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(SomeModule, true))
|
||||
.toThrowError(`Expected 'styles' to be an array of strings.`);
|
||||
}));
|
||||
|
||||
it('should throw with descriptive error message when a module imports itself',
|
||||
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
|
||||
@NgModule({imports: [SomeModule]})
|
||||
class SomeModule {
|
||||
}
|
||||
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(SomeModule, true))
|
||||
.toThrowError(`'SomeModule' module can't import itself`);
|
||||
}));
|
||||
|
||||
it('should throw with descriptive error message when provider token can not be resolved',
|
||||
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
|
||||
@NgModule({declarations: [MyBrokenComp1]})
|
||||
class SomeModule {
|
||||
}
|
||||
|
||||
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(SomeModule, true))
|
||||
.toThrowError(`Can't resolve all parameters for MyBrokenComp1: (?).`);
|
||||
}));
|
||||
|
||||
it('should throw with descriptive error message when a directive is passed to imports',
|
||||
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
|
||||
@NgModule({imports: [ComponentWithoutModuleId]})
|
||||
class ModuleWithImportedComponent {
|
||||
}
|
||||
expect(
|
||||
() => resolver.loadNgModuleDirectiveAndPipeMetadata(ModuleWithImportedComponent, true))
|
||||
.toThrowError(
|
||||
`Unexpected directive 'ComponentWithoutModuleId' imported by the module 'ModuleWithImportedComponent'. Please add a @NgModule annotation.`);
|
||||
}));
|
||||
|
||||
it('should throw with descriptive error message when a pipe is passed to imports',
|
||||
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
|
||||
@Pipe({name: 'somePipe'})
|
||||
class SomePipe {
|
||||
}
|
||||
@NgModule({imports: [SomePipe]})
|
||||
class ModuleWithImportedPipe {
|
||||
}
|
||||
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(ModuleWithImportedPipe, true))
|
||||
.toThrowError(
|
||||
`Unexpected pipe 'SomePipe' imported by the module 'ModuleWithImportedPipe'. Please add a @NgModule annotation.`);
|
||||
}));
|
||||
|
||||
it('should throw with descriptive error message when a module is passed to declarations',
|
||||
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
|
||||
@NgModule({})
|
||||
class SomeModule {
|
||||
}
|
||||
@NgModule({declarations: [SomeModule]})
|
||||
class ModuleWithDeclaredModule {
|
||||
}
|
||||
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(ModuleWithDeclaredModule, true))
|
||||
.toThrowError(
|
||||
`Unexpected module 'SomeModule' declared by the module 'ModuleWithDeclaredModule'. Please add a @Pipe/@Directive/@Component annotation.`);
|
||||
}));
|
||||
|
||||
it('should throw with descriptive error message when a declared pipe is missing annotation',
|
||||
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
|
||||
class SomePipe {}
|
||||
@NgModule({declarations: [SomePipe]})
|
||||
class ModuleWithDeclaredModule {
|
||||
}
|
||||
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(ModuleWithDeclaredModule, true))
|
||||
.toThrowError(
|
||||
`Unexpected value 'SomePipe' declared by the module 'ModuleWithDeclaredModule'. Please add a @Pipe/@Directive/@Component annotation.`);
|
||||
}));
|
||||
|
||||
it('should throw with descriptive error message when an imported module is missing annotation',
|
||||
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
|
||||
class SomeModule {}
|
||||
@NgModule({imports: [SomeModule]})
|
||||
class ModuleWithImportedModule {
|
||||
}
|
||||
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(ModuleWithImportedModule, true))
|
||||
.toThrowError(
|
||||
`Unexpected value 'SomeModule' imported by the module 'ModuleWithImportedModule'. Please add a @NgModule annotation.`);
|
||||
}));
|
||||
|
||||
it('should throw with descriptive error message when null is passed to declarations',
|
||||
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
|
||||
@NgModule({declarations: [null!]})
|
||||
class ModuleWithNullDeclared {
|
||||
}
|
||||
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(ModuleWithNullDeclared, true))
|
||||
.toThrowError(
|
||||
`Unexpected value 'null' declared by the module 'ModuleWithNullDeclared'`);
|
||||
}));
|
||||
|
||||
it('should throw with descriptive error message when null is passed to imports',
|
||||
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
|
||||
@NgModule({imports: [null!]})
|
||||
class ModuleWithNullImported {
|
||||
}
|
||||
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(ModuleWithNullImported, true))
|
||||
.toThrowError(
|
||||
`Unexpected value 'null' imported by the module 'ModuleWithNullImported'`);
|
||||
}));
|
||||
|
||||
|
||||
it('should throw with descriptive error message when a param token of a dependency is undefined',
|
||||
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
|
||||
@NgModule({declarations: [MyBrokenComp2]})
|
||||
class SomeModule {
|
||||
}
|
||||
|
||||
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(SomeModule, true))
|
||||
.toThrowError(`Can't resolve all parameters for NonAnnotatedService: (?).`);
|
||||
}));
|
||||
|
||||
it('should throw with descriptive error message when encounter invalid provider',
|
||||
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
|
||||
@NgModule({providers: [{provide: SimpleService, useClass: undefined!}]})
|
||||
class SomeModule {
|
||||
}
|
||||
|
||||
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(SomeModule, true))
|
||||
.toThrowError(/Invalid provider for SimpleService. useClass cannot be undefined./);
|
||||
}));
|
||||
|
||||
it('should throw with descriptive error message when provider is undefined',
|
||||
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
|
||||
@NgModule({providers: [undefined!]})
|
||||
class SomeModule {
|
||||
}
|
||||
|
||||
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(SomeModule, true))
|
||||
.toThrowError(/Encountered undefined provider!/);
|
||||
}));
|
||||
|
||||
it('should throw with descriptive error message when one of providers is not present',
|
||||
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
|
||||
@NgModule({declarations: [MyBrokenComp3]})
|
||||
class SomeModule {
|
||||
}
|
||||
|
||||
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(SomeModule, true))
|
||||
.toThrowError(
|
||||
`Invalid providers for "MyBrokenComp3" - only instances of Provider and Type are allowed, got: [SimpleService, ?null?, ...]`);
|
||||
}));
|
||||
|
||||
it('should throw with descriptive error message when one of viewProviders is not present',
|
||||
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
|
||||
@NgModule({declarations: [MyBrokenComp4]})
|
||||
class SomeModule {
|
||||
}
|
||||
|
||||
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(SomeModule, true))
|
||||
.toThrowError(
|
||||
`Invalid viewProviders for "MyBrokenComp4" - only instances of Provider and Type are allowed, got: [?null?, ...]`);
|
||||
}));
|
||||
|
||||
it('should throw with descriptive error message when null or undefined is passed to module bootstrap',
|
||||
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
|
||||
@NgModule({bootstrap: [null!]})
|
||||
class ModuleWithNullBootstrap {
|
||||
}
|
||||
@NgModule({bootstrap: [undefined!]})
|
||||
class ModuleWithUndefinedBootstrap {
|
||||
}
|
||||
|
||||
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(ModuleWithNullBootstrap, true))
|
||||
.toThrowError(
|
||||
`Unexpected value 'null' used in the bootstrap property of module 'ModuleWithNullBootstrap'`);
|
||||
expect(
|
||||
() =>
|
||||
resolver.loadNgModuleDirectiveAndPipeMetadata(ModuleWithUndefinedBootstrap, true))
|
||||
.toThrowError(
|
||||
`Unexpected value 'undefined' used in the bootstrap property of module 'ModuleWithUndefinedBootstrap'`);
|
||||
}));
|
||||
|
||||
it('should throw an error when the interpolation config has invalid symbols',
|
||||
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
|
||||
@NgModule({declarations: [ComponentWithInvalidInterpolation1]})
|
||||
class Module1 {
|
||||
}
|
||||
|
||||
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(Module1, true))
|
||||
.toThrowError(`[' ', ' '] contains unusable interpolation symbol.`);
|
||||
|
||||
@NgModule({declarations: [ComponentWithInvalidInterpolation2]})
|
||||
class Module2 {
|
||||
}
|
||||
|
||||
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(Module2, true))
|
||||
.toThrowError(`['{', '}'] contains unusable interpolation symbol.`);
|
||||
|
||||
@NgModule({declarations: [ComponentWithInvalidInterpolation3]})
|
||||
class Module3 {
|
||||
}
|
||||
|
||||
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(Module3, true))
|
||||
.toThrowError(`['<%', '%>'] contains unusable interpolation symbol.`);
|
||||
|
||||
@NgModule({declarations: [ComponentWithInvalidInterpolation4]})
|
||||
class Module4 {
|
||||
}
|
||||
|
||||
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(Module4, true))
|
||||
.toThrowError(`['&#', '}}'] contains unusable interpolation symbol.`);
|
||||
|
||||
@NgModule({declarations: [ComponentWithInvalidInterpolation5]})
|
||||
class Module5 {
|
||||
}
|
||||
|
||||
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(Module5, true))
|
||||
.toThrowError(`['{', '}}'] contains unusable interpolation symbol.`);
|
||||
}));
|
||||
|
||||
it(`should throw an error when a Pipe is added to module's bootstrap list`,
|
||||
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
|
||||
@Pipe({name: 'pipe'})
|
||||
class MyPipe {
|
||||
}
|
||||
|
||||
@NgModule({declarations: [MyPipe], bootstrap: [MyPipe]})
|
||||
class ModuleWithPipeInBootstrap {
|
||||
}
|
||||
|
||||
expect(() => resolver.getNgModuleMetadata(ModuleWithPipeInBootstrap))
|
||||
.toThrowError(`MyPipe cannot be used as an entry component.`);
|
||||
}));
|
||||
|
||||
it(`should throw an error when a Service is added to module's bootstrap list`,
|
||||
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
|
||||
@NgModule({declarations: [], bootstrap: [SimpleService]})
|
||||
class ModuleWithServiceInBootstrap {
|
||||
}
|
||||
|
||||
expect(() => resolver.getNgModuleMetadata(ModuleWithServiceInBootstrap))
|
||||
.toThrowError(`SimpleService cannot be used as an entry component.`);
|
||||
}));
|
||||
|
||||
it('should generate an error when a dependency could not be resolved',
|
||||
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
|
||||
// Override the errorCollector so that error gets collected instead of
|
||||
// being thrown.
|
||||
(resolver as any)._errorCollector = (error: Error, type?: any) => {
|
||||
expect(error.message).toBe(`Can't resolve all parameters for MyComponent: (?).`);
|
||||
};
|
||||
|
||||
@Component({template: ''})
|
||||
class MyComponent {
|
||||
// @ts-ignore UserService is a non-existent class.
|
||||
constructor(service: UserService) {}
|
||||
}
|
||||
|
||||
@NgModule({declarations: [MyComponent]})
|
||||
class AppModule {
|
||||
}
|
||||
|
||||
const moduleMetadata = resolver.getNgModuleMetadata(AppModule)!;
|
||||
expect(moduleMetadata).toBeTruthy();
|
||||
expect(moduleMetadata.declaredDirectives.length).toBe(1);
|
||||
const directive = moduleMetadata.declaredDirectives[0];
|
||||
const directiveMetadata = resolver.getNonNormalizedDirectiveMetadata(directive.reference)!;
|
||||
expect(directiveMetadata).toBeTruthy();
|
||||
const {metadata} = directiveMetadata;
|
||||
const diDeps: CompileDiDependencyMetadata[] = metadata.type.diDeps;
|
||||
// 'null' does not conform to the shape of `CompileDiDependencyMetadata`
|
||||
expect(diDeps.every(d => d !== null)).toBe(true);
|
||||
}));
|
||||
|
||||
it(`should throw an error when a Directive is added to module's bootstrap list`,
|
||||
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
|
||||
@Directive({selector: 'directive'})
|
||||
class MyDirective {
|
||||
}
|
||||
|
||||
@NgModule({declarations: [], bootstrap: [MyDirective]})
|
||||
class ModuleWithDirectiveInBootstrap {
|
||||
}
|
||||
|
||||
expect(() => resolver.getNgModuleMetadata(ModuleWithDirectiveInBootstrap))
|
||||
.toThrowError(`MyDirective cannot be used as an entry component.`);
|
||||
}));
|
||||
|
||||
it(`should not throw an error when a Component is added to module's bootstrap list`,
|
||||
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
|
||||
@Component({template: ''})
|
||||
class MyComp {
|
||||
}
|
||||
|
||||
@NgModule({declarations: [MyComp], bootstrap: [MyComp]})
|
||||
class ModuleWithComponentInBootstrap {
|
||||
}
|
||||
|
||||
expect(() => resolver.getNgModuleMetadata(ModuleWithComponentInBootstrap)).not.toThrow();
|
||||
}));
|
||||
|
||||
// #20049
|
||||
it('should throw a reasonable message when an invalid import is given',
|
||||
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
|
||||
@NgModule({imports: [{ngModule: true as any}]})
|
||||
class InvalidModule {
|
||||
}
|
||||
|
||||
expect(() => {
|
||||
resolver.getNgModuleMetadata(InvalidModule);
|
||||
})
|
||||
.toThrowError(
|
||||
`Unexpected value '[object Object]' imported by the module 'InvalidModule'. Please add a @NgModule annotation.`);
|
||||
}));
|
||||
});
|
||||
|
||||
it('should dedupe declarations in NgModule',
|
||||
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
|
||||
@Component({template: ''})
|
||||
class MyComp {
|
||||
}
|
||||
|
||||
@NgModule({declarations: [MyComp, MyComp]})
|
||||
class MyModule {
|
||||
}
|
||||
|
||||
const modMeta = resolver.getNgModuleMetadata(MyModule)!;
|
||||
expect(modMeta.declaredDirectives.length).toBe(1);
|
||||
expect(modMeta.declaredDirectives[0].reference).toBe(MyComp);
|
||||
}));
|
||||
}
|
||||
|
||||
@Component({selector: 'someComponent', template: ''})
|
||||
class ComponentWithoutModuleId {
|
||||
}
|
||||
|
||||
@Component({selector: 'someComponent', template: '', moduleId: <any>0})
|
||||
class ComponentWithInvalidModuleId {
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'someSelector',
|
||||
templateUrl: 'someTemplateUrl',
|
||||
styleUrls: ['someStyleUrl'],
|
||||
})
|
||||
class ComponentWithExternalResources {
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'someSelector',
|
||||
inputs: ['someProp'],
|
||||
outputs: ['someEvent'],
|
||||
host: {
|
||||
'[someHostProp]': 'someHostPropExpr',
|
||||
'(someHostListener)': 'someHostListenerExpr',
|
||||
'someHostAttr': 'someHostAttrValue'
|
||||
},
|
||||
exportAs: 'someExportAs',
|
||||
moduleId: 'someModuleId',
|
||||
changeDetection: ChangeDetectionStrategy.Default,
|
||||
template: 'someTemplate',
|
||||
encapsulation: ViewEncapsulation.Emulated,
|
||||
styles: ['someStyle'],
|
||||
interpolation: ['{{', '}}']
|
||||
})
|
||||
class ComponentWithEverythingInline implements OnChanges, OnInit, DoCheck, OnDestroy,
|
||||
AfterContentInit, AfterContentChecked, AfterViewInit,
|
||||
AfterViewChecked {
|
||||
ngOnChanges(changes: SimpleChanges): void {}
|
||||
ngOnInit(): void {}
|
||||
ngDoCheck(): void {}
|
||||
ngOnDestroy(): void {}
|
||||
ngAfterContentInit(): void {}
|
||||
ngAfterContentChecked(): void {}
|
||||
ngAfterViewInit(): void {}
|
||||
ngAfterViewChecked(): void {}
|
||||
}
|
||||
|
||||
@Component({selector: 'my-broken-comp', template: ''})
|
||||
class MyBrokenComp1 {
|
||||
constructor(public dependency: any) {}
|
||||
}
|
||||
|
||||
class NonAnnotatedService {
|
||||
constructor(dep: any) {}
|
||||
}
|
||||
|
||||
@Component({selector: 'my-broken-comp', template: '', providers: [NonAnnotatedService]})
|
||||
class MyBrokenComp2 {
|
||||
constructor(dependency: NonAnnotatedService) {}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
class SimpleService {
|
||||
}
|
||||
|
||||
@Component({selector: 'my-broken-comp', template: '', providers: [SimpleService, null!, [null]]})
|
||||
class MyBrokenComp3 {
|
||||
}
|
||||
|
||||
@Component(
|
||||
{selector: 'my-broken-comp', template: '', viewProviders: [null!, SimpleService, [null]]})
|
||||
class MyBrokenComp4 {
|
||||
}
|
||||
|
||||
@Component({selector: 'someSelector', template: '', interpolation: [' ', ' ']})
|
||||
class ComponentWithInvalidInterpolation1 {
|
||||
}
|
||||
|
||||
@Component({selector: 'someSelector', template: '', interpolation: ['{', '}']})
|
||||
class ComponentWithInvalidInterpolation2 {
|
||||
}
|
||||
|
||||
@Component({selector: 'someSelector', template: '', interpolation: ['<%', '%>']})
|
||||
class ComponentWithInvalidInterpolation3 {
|
||||
}
|
||||
|
||||
@Component({selector: 'someSelector', template: '', interpolation: ['&#', '}}']})
|
||||
class ComponentWithInvalidInterpolation4 {
|
||||
}
|
||||
|
||||
@Component({selector: 'someSelector', template: '', interpolation: ['{', '}}']})
|
||||
class ComponentWithInvalidInterpolation5 {
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 {NgModule} from '@angular/core';
|
||||
import {JitReflector} from '@angular/platform-browser-dynamic/src/compiler_reflector';
|
||||
|
||||
import {MockNgModuleResolver} from '../testing';
|
||||
|
||||
{
|
||||
describe('MockNgModuleResolver', () => {
|
||||
let ngModuleResolver: MockNgModuleResolver;
|
||||
|
||||
beforeEach(() => {
|
||||
ngModuleResolver = new MockNgModuleResolver(new JitReflector());
|
||||
});
|
||||
|
||||
describe('NgModule overriding', () => {
|
||||
it('should fallback to the default NgModuleResolver when templates are not overridden',
|
||||
() => {
|
||||
const ngModule = ngModuleResolver.resolve(SomeNgModule);
|
||||
expect(ngModule.declarations).toEqual([SomeDirective]);
|
||||
});
|
||||
|
||||
it('should allow overriding the @NgModule', () => {
|
||||
ngModuleResolver.setNgModule(
|
||||
SomeNgModule, new NgModule({declarations: [SomeOtherDirective]}));
|
||||
const ngModule = ngModuleResolver.resolve(SomeNgModule);
|
||||
expect(ngModule.declarations).toEqual([SomeOtherDirective]);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class SomeDirective {}
|
||||
|
||||
class SomeOtherDirective {}
|
||||
|
||||
@NgModule({declarations: [SomeDirective]})
|
||||
class SomeNgModule {
|
||||
}
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 {NgModuleResolver} from '@angular/compiler/src/ng_module_resolver';
|
||||
import {Component, Directive, Injectable, NgModule, ɵstringify as stringify} from '@angular/core';
|
||||
import {JitReflector} from '@angular/platform-browser-dynamic/src/compiler_reflector';
|
||||
|
||||
@Directive()
|
||||
class SomeClass1 {
|
||||
}
|
||||
|
||||
@NgModule()
|
||||
class SomeClass2 {
|
||||
}
|
||||
|
||||
@NgModule()
|
||||
class SomeClass3 {
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
class SomeClass4 {
|
||||
}
|
||||
|
||||
@Component({template: ''})
|
||||
class SomeClass5 {
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [SomeClass1],
|
||||
imports: [SomeClass2],
|
||||
exports: [SomeClass3],
|
||||
providers: [SomeClass4],
|
||||
entryComponents: [SomeClass5]
|
||||
})
|
||||
class SomeModule {
|
||||
}
|
||||
|
||||
class SimpleClass {}
|
||||
|
||||
{
|
||||
describe('NgModuleResolver', () => {
|
||||
let resolver: NgModuleResolver;
|
||||
|
||||
beforeEach(() => {
|
||||
resolver = new NgModuleResolver(new JitReflector());
|
||||
});
|
||||
|
||||
it('should read out the metadata from the class', () => {
|
||||
const moduleMetadata = resolver.resolve(SomeModule);
|
||||
expect(moduleMetadata).toEqual(new NgModule({
|
||||
declarations: [SomeClass1],
|
||||
imports: [SomeClass2],
|
||||
exports: [SomeClass3],
|
||||
providers: [SomeClass4],
|
||||
entryComponents: [SomeClass5]
|
||||
}));
|
||||
});
|
||||
|
||||
it('should throw when simple class has no NgModule decorator', () => {
|
||||
expect(() => resolver.resolve(SimpleClass))
|
||||
.toThrowError(`No NgModule metadata found for '${stringify(SimpleClass)}'.`);
|
||||
});
|
||||
|
||||
it('should support inheriting the metadata', function() {
|
||||
@NgModule({id: 'p'})
|
||||
class Parent {
|
||||
}
|
||||
|
||||
class ChildNoDecorator extends Parent {}
|
||||
|
||||
@NgModule({id: 'c'})
|
||||
class ChildWithDecorator extends Parent {
|
||||
}
|
||||
|
||||
expect(resolver.resolve(ChildNoDecorator)).toEqual(new NgModule({id: 'p'}));
|
||||
|
||||
expect(resolver.resolve(ChildWithDecorator)).toEqual(new NgModule({id: 'c'}));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -9,7 +9,8 @@
|
|||
import {ParseLocation, ParseSourceFile, ParseSourceSpan} from '@angular/compiler';
|
||||
import {EmitterVisitorContext} from '@angular/compiler/src/output/abstract_emitter';
|
||||
import {SourceMap} from '@angular/compiler/src/output/source_map';
|
||||
import {extractSourceMap, originalPositionFor} from '@angular/compiler/testing/src/output/source_map_util';
|
||||
|
||||
import {extractSourceMap, originalPositionFor} from './source_map_util';
|
||||
|
||||
{
|
||||
describe('AbstractEmitter', () => {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import * as o from '@angular/compiler/src/output/output_ast';
|
|||
import {SourceMap} from '@angular/compiler/src/output/source_map';
|
||||
import {ParseLocation, ParseSourceFile, ParseSourceSpan} from '@angular/compiler/src/parse_util';
|
||||
|
||||
import {extractSourceMap, originalPositionFor} from '@angular/compiler/testing/src/output/source_map_util';
|
||||
import {extractSourceMap, originalPositionFor} from './source_map_util';
|
||||
|
||||
const someGenFilePath = 'somePackage/someGenFile';
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ export function extractSourceMap(source: string): SourceMap|null {
|
|||
if (idx == -1) return null;
|
||||
const smComment = source.slice(idx).split('\n', 2)[1].trim();
|
||||
const smB64 = smComment.split('sourceMappingURL=data:application/json;base64,')[1];
|
||||
return smB64 ? JSON.parse(decodeB64String(smB64)) : null;
|
||||
return smB64 ? JSON.parse(decodeB64String(smB64)) as SourceMap : null;
|
||||
}
|
||||
|
||||
function decodeB64String(s: string): string {
|
||||
|
|
@ -13,7 +13,7 @@ import {SourceMap} from '@angular/compiler/src/output/source_map';
|
|||
import {TypeScriptEmitter} from '@angular/compiler/src/output/ts_emitter';
|
||||
import {ParseSourceSpan} from '@angular/compiler/src/parse_util';
|
||||
|
||||
import {extractSourceMap, originalPositionFor} from '@angular/compiler/testing/src/output/source_map_util';
|
||||
import {extractSourceMap, originalPositionFor} from './source_map_util';
|
||||
|
||||
const someGenFilePath = 'somePackage/someGenFile';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 {ParseError, ParseErrorLevel, ParseLocation, ParseSourceFile, ParseSourceSpan} from '../src/parse_util';
|
||||
|
||||
{
|
||||
describe('ParseError', () => {
|
||||
it('should reflect the level in the message', () => {
|
||||
const file = new ParseSourceFile(`foo\nbar\nfoo`, 'url');
|
||||
const start = new ParseLocation(file, 4, 1, 0);
|
||||
const end = new ParseLocation(file, 6, 1, 2);
|
||||
const span = new ParseSourceSpan(start, end);
|
||||
|
||||
const fatal = new ParseError(span, 'fatal', ParseErrorLevel.ERROR);
|
||||
expect(fatal.toString()).toEqual('fatal ("foo\n[ERROR ->]bar\nfoo"): url@1:0');
|
||||
|
||||
const warning = new ParseError(span, 'warning', ParseErrorLevel.WARNING);
|
||||
expect(warning.toString()).toEqual('warning ("foo\n[WARNING ->]bar\nfoo"): url@1:0');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 {Injector, Pipe} from '@angular/core';
|
||||
import {inject} from '@angular/core/testing';
|
||||
import {JitReflector} from '@angular/platform-browser-dynamic/src/compiler_reflector';
|
||||
|
||||
import {MockPipeResolver} from '../testing';
|
||||
|
||||
{
|
||||
describe('MockPipeResolver', () => {
|
||||
let pipeResolver: MockPipeResolver;
|
||||
|
||||
beforeEach(inject([Injector], (injector: Injector) => {
|
||||
pipeResolver = new MockPipeResolver(new JitReflector());
|
||||
}));
|
||||
|
||||
describe('Pipe overriding', () => {
|
||||
it('should fallback to the default PipeResolver when templates are not overridden', () => {
|
||||
const pipe = pipeResolver.resolve(SomePipe);
|
||||
expect(pipe.name).toEqual('somePipe');
|
||||
});
|
||||
|
||||
it('should allow overriding the @Pipe', () => {
|
||||
pipeResolver.setPipe(SomePipe, new Pipe({name: 'someOtherName'}));
|
||||
const pipe = pipeResolver.resolve(SomePipe);
|
||||
expect(pipe.name).toEqual('someOtherName');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Pipe({name: 'somePipe'})
|
||||
class SomePipe {
|
||||
}
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 {PipeResolver} from '@angular/compiler/src/pipe_resolver';
|
||||
import {ɵstringify as stringify} from '@angular/core';
|
||||
import {Pipe} from '@angular/core/src/metadata';
|
||||
import {JitReflector} from '@angular/platform-browser-dynamic/src/compiler_reflector';
|
||||
|
||||
@Pipe({name: 'somePipe', pure: true})
|
||||
class SomePipe {
|
||||
}
|
||||
|
||||
class SimpleClass {}
|
||||
|
||||
{
|
||||
describe('PipeResolver', () => {
|
||||
let resolver: PipeResolver;
|
||||
|
||||
beforeEach(() => {
|
||||
resolver = new PipeResolver(new JitReflector());
|
||||
});
|
||||
|
||||
it('should read out the metadata from the class', () => {
|
||||
const moduleMetadata = resolver.resolve(SomePipe);
|
||||
expect(moduleMetadata).toEqual(new Pipe({name: 'somePipe', pure: true}));
|
||||
});
|
||||
|
||||
it('should throw when simple class has no pipe decorator', () => {
|
||||
expect(() => resolver.resolve(SimpleClass))
|
||||
.toThrowError(`No Pipe decorator found on ${stringify(SimpleClass)}`);
|
||||
});
|
||||
|
||||
it('should support inheriting the metadata', function() {
|
||||
@Pipe({name: 'p'})
|
||||
class Parent {
|
||||
}
|
||||
|
||||
class ChildNoDecorator extends Parent {}
|
||||
|
||||
@Pipe({name: 'c'})
|
||||
class ChildWithDecorator extends Parent {
|
||||
}
|
||||
|
||||
expect(resolver.resolve(ChildNoDecorator)).toEqual(new Pipe({name: 'p'}));
|
||||
|
||||
expect(resolver.resolve(ChildWithDecorator)).toEqual(new Pipe({name: 'c'}));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -9,9 +9,7 @@ ts_library(
|
|||
deps = [
|
||||
"//packages:types",
|
||||
"//packages/compiler",
|
||||
"//packages/compiler/test:test_utils",
|
||||
"//packages/compiler/test/expression_parser/utils",
|
||||
"//packages/compiler/testing",
|
||||
"//packages/core",
|
||||
],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -18,8 +18,70 @@ import * as a from '../../../src/render3/r3_ast';
|
|||
import {htmlAstToRender3Ast, Render3ParseResult} from '../../../src/render3/r3_template_transform';
|
||||
import {I18nMetaVisitor} from '../../../src/render3/view/i18n/meta';
|
||||
import {LEADING_TRIVIA_CHARS} from '../../../src/render3/view/template';
|
||||
import {ElementSchemaRegistry} from '../../../src/schema/element_schema_registry';
|
||||
import {BindingParser} from '../../../src/template_parser/binding_parser';
|
||||
import {MockSchemaRegistry} from '../../../testing';
|
||||
|
||||
class MockSchemaRegistry implements ElementSchemaRegistry {
|
||||
constructor(
|
||||
public existingProperties: {[key: string]: boolean},
|
||||
public attrPropMapping: {[key: string]: string},
|
||||
public existingElements: {[key: string]: boolean}, public invalidProperties: Array<string>,
|
||||
public invalidAttributes: Array<string>) {}
|
||||
|
||||
hasProperty(tagName: string, property: string, schemas: any[]): boolean {
|
||||
const value = this.existingProperties[property];
|
||||
return value === void 0 ? true : value;
|
||||
}
|
||||
|
||||
hasElement(tagName: string, schemaMetas: any[]): boolean {
|
||||
const value = this.existingElements[tagName.toLowerCase()];
|
||||
return value === void 0 ? true : value;
|
||||
}
|
||||
|
||||
allKnownElementNames(): string[] {
|
||||
return Object.keys(this.existingElements);
|
||||
}
|
||||
|
||||
securityContext(selector: string, property: string, isAttribute: boolean): any {
|
||||
return 0;
|
||||
}
|
||||
|
||||
getMappedPropName(attrName: string): string {
|
||||
return this.attrPropMapping[attrName] || attrName;
|
||||
}
|
||||
|
||||
getDefaultComponentElementName(): string {
|
||||
return 'ng-component';
|
||||
}
|
||||
|
||||
validateProperty(name: string): {error: boolean, msg?: string} {
|
||||
if (this.invalidProperties.indexOf(name) > -1) {
|
||||
return {error: true, msg: `Binding to property '${name}' is disallowed for security reasons`};
|
||||
} else {
|
||||
return {error: false};
|
||||
}
|
||||
}
|
||||
|
||||
validateAttribute(name: string): {error: boolean, msg?: string} {
|
||||
if (this.invalidAttributes.indexOf(name) > -1) {
|
||||
return {
|
||||
error: true,
|
||||
msg: `Binding to attribute '${name}' is disallowed for security reasons`
|
||||
};
|
||||
} else {
|
||||
return {error: false};
|
||||
}
|
||||
}
|
||||
|
||||
normalizeAnimationStyleProperty(propName: string): string {
|
||||
return propName;
|
||||
}
|
||||
normalizeAnimationStyleValue(camelCaseProp: string, userProvidedProp: string, val: string|number):
|
||||
{error: string, value: string} {
|
||||
return {error: null!, value: val.toString()};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function findExpression(tmpl: a.Node[], expr: string): e.AST|null {
|
||||
const res = tmpl.reduce((found, node) => {
|
||||
|
|
|
|||
|
|
@ -1,118 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 {MockResourceLoader} from '@angular/compiler/testing/src/resource_loader_mock';
|
||||
|
||||
{
|
||||
describe('MockResourceLoader', () => {
|
||||
let resourceLoader: MockResourceLoader;
|
||||
|
||||
beforeEach(() => {
|
||||
resourceLoader = new MockResourceLoader();
|
||||
});
|
||||
|
||||
function expectResponse(
|
||||
request: Promise<string>, url: string, response: string, done: () => void = null!) {
|
||||
function onResponse(text: string): string {
|
||||
if (response === null) {
|
||||
throw `Unexpected response ${url} -> ${text}`;
|
||||
} else {
|
||||
expect(text).toEqual(response);
|
||||
if (done != null) done();
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
function onError(error: string): string {
|
||||
if (response !== null) {
|
||||
throw `Unexpected error ${url}`;
|
||||
} else {
|
||||
expect(error).toEqual(`Failed to load ${url}`);
|
||||
if (done != null) done();
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
request.then(onResponse, onError);
|
||||
}
|
||||
|
||||
it('should return a response from the definitions', done => {
|
||||
const url = '/foo';
|
||||
const response = 'bar';
|
||||
resourceLoader.when(url, response);
|
||||
expectResponse(resourceLoader.get(url), url, response, () => done());
|
||||
resourceLoader.flush();
|
||||
});
|
||||
|
||||
it('should return an error from the definitions', done => {
|
||||
const url = '/foo';
|
||||
const response: string = null!;
|
||||
resourceLoader.when(url, response);
|
||||
expectResponse(resourceLoader.get(url), url, response, () => done());
|
||||
resourceLoader.flush();
|
||||
});
|
||||
|
||||
it('should return a response from the expectations', done => {
|
||||
const url = '/foo';
|
||||
const response = 'bar';
|
||||
resourceLoader.expect(url, response);
|
||||
expectResponse(resourceLoader.get(url), url, response, () => done());
|
||||
resourceLoader.flush();
|
||||
});
|
||||
|
||||
it('should return an error from the expectations', done => {
|
||||
const url = '/foo';
|
||||
const response: string = null!;
|
||||
resourceLoader.expect(url, response);
|
||||
expectResponse(resourceLoader.get(url), url, response, () => done());
|
||||
resourceLoader.flush();
|
||||
});
|
||||
|
||||
it('should not reuse expectations', () => {
|
||||
const url = '/foo';
|
||||
const response = 'bar';
|
||||
resourceLoader.expect(url, response);
|
||||
resourceLoader.get(url);
|
||||
resourceLoader.get(url);
|
||||
expect(() => {
|
||||
resourceLoader.flush();
|
||||
}).toThrowError('Unexpected request /foo');
|
||||
});
|
||||
|
||||
it('should return expectations before definitions', done => {
|
||||
const url = '/foo';
|
||||
resourceLoader.when(url, 'when');
|
||||
resourceLoader.expect(url, 'expect');
|
||||
expectResponse(resourceLoader.get(url), url, 'expect');
|
||||
expectResponse(resourceLoader.get(url), url, 'when', () => done());
|
||||
resourceLoader.flush();
|
||||
});
|
||||
|
||||
it('should throw when there is no definitions or expectations', () => {
|
||||
resourceLoader.get('/foo');
|
||||
expect(() => {
|
||||
resourceLoader.flush();
|
||||
}).toThrowError('Unexpected request /foo');
|
||||
});
|
||||
|
||||
it('should throw when flush is called without any pending requests', () => {
|
||||
expect(() => {
|
||||
resourceLoader.flush();
|
||||
}).toThrowError('No pending requests to flush');
|
||||
});
|
||||
|
||||
it('should throw on unsatisfied expectations', () => {
|
||||
resourceLoader.expect('/foo', 'bar');
|
||||
resourceLoader.when('/bar', 'foo');
|
||||
resourceLoader.get('/bar');
|
||||
expect(() => {
|
||||
resourceLoader.flush();
|
||||
}).toThrowError('Unsatisfied requests: /foo');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -1,171 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 {DirectiveResolver, ResourceLoader} from '@angular/compiler';
|
||||
import {Compiler, Component, Injector, NgModule, NgModuleFactory, ɵstringify as stringify} from '@angular/core';
|
||||
import {fakeAsync, inject, TestBed, tick, waitForAsync} from '@angular/core/testing';
|
||||
import {expect} from '@angular/platform-browser/testing/src/matchers';
|
||||
|
||||
import {MockDirectiveResolver} from '../testing';
|
||||
|
||||
@Component({selector: 'child-cmp'})
|
||||
class ChildComp {
|
||||
}
|
||||
|
||||
@Component({selector: 'some-cmp', template: 'someComp'})
|
||||
class SomeComp {
|
||||
}
|
||||
|
||||
@Component({selector: 'some-cmp', templateUrl: './someTpl'})
|
||||
class SomeCompWithUrlTemplate {
|
||||
}
|
||||
|
||||
{
|
||||
describe('RuntimeCompiler', () => {
|
||||
describe('compilerComponentSync', () => {
|
||||
describe('never resolving loader', () => {
|
||||
class StubResourceLoader {
|
||||
get(url: string) {
|
||||
return new Promise(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureCompiler(
|
||||
{providers: [{provide: ResourceLoader, useClass: StubResourceLoader, deps: []}]});
|
||||
});
|
||||
|
||||
it('should throw when using a templateUrl that has not been compiled before',
|
||||
waitForAsync(() => {
|
||||
TestBed.configureTestingModule({declarations: [SomeCompWithUrlTemplate]});
|
||||
TestBed.compileComponents().then(() => {
|
||||
expect(() => TestBed.createComponent(SomeCompWithUrlTemplate))
|
||||
.toThrowError(`Can't compile synchronously as ${
|
||||
stringify(SomeCompWithUrlTemplate)} is still being loaded!`);
|
||||
});
|
||||
}));
|
||||
|
||||
it('should throw when using a templateUrl in a nested component that has not been compiled before',
|
||||
() => {
|
||||
TestBed.configureTestingModule({declarations: [SomeComp, ChildComp]});
|
||||
TestBed.overrideComponent(ChildComp, {set: {templateUrl: '/someTpl.html'}});
|
||||
TestBed.overrideComponent(SomeComp, {set: {template: '<child-cmp></child-cmp>'}});
|
||||
TestBed.compileComponents().then(() => {
|
||||
expect(() => TestBed.createComponent(SomeComp))
|
||||
.toThrowError(`Can't compile synchronously as ${
|
||||
stringify(ChildComp)} is still being loaded!`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolving loader', () => {
|
||||
class StubResourceLoader {
|
||||
get(url: string) {
|
||||
return Promise.resolve('hello');
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureCompiler(
|
||||
{providers: [{provide: ResourceLoader, useClass: StubResourceLoader, deps: []}]});
|
||||
});
|
||||
|
||||
it('should allow to use templateUrl components that have been loaded before',
|
||||
waitForAsync(() => {
|
||||
TestBed.configureTestingModule({declarations: [SomeCompWithUrlTemplate]});
|
||||
TestBed.compileComponents().then(() => {
|
||||
const fixture = TestBed.createComponent(SomeCompWithUrlTemplate);
|
||||
expect(fixture.nativeElement).toHaveText('hello');
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('RuntimeCompiler', () => {
|
||||
let compiler: Compiler;
|
||||
let resourceLoader: {get: jasmine.Spy};
|
||||
let dirResolver: MockDirectiveResolver;
|
||||
|
||||
beforeEach(() => {
|
||||
resourceLoader = jasmine.createSpyObj('ResourceLoader', ['get']);
|
||||
TestBed.configureCompiler({providers: [{provide: ResourceLoader, useValue: resourceLoader}]});
|
||||
});
|
||||
|
||||
beforeEach(fakeAsync(inject(
|
||||
[Compiler, ResourceLoader, DirectiveResolver, Injector],
|
||||
(_compiler: Compiler, _resourceLoader: any, _dirResolver: MockDirectiveResolver) => {
|
||||
compiler = _compiler;
|
||||
resourceLoader = _resourceLoader;
|
||||
dirResolver = _dirResolver;
|
||||
})));
|
||||
|
||||
describe('compileModuleAsync', () => {
|
||||
it('should allow to use templateUrl components', fakeAsync(() => {
|
||||
@NgModule({
|
||||
declarations: [SomeCompWithUrlTemplate],
|
||||
entryComponents: [SomeCompWithUrlTemplate]
|
||||
})
|
||||
class SomeModule {
|
||||
}
|
||||
|
||||
resourceLoader.get.and.callFake(() => Promise.resolve('hello'));
|
||||
let ngModuleFactory: NgModuleFactory<any> = undefined!;
|
||||
compiler.compileModuleAsync(SomeModule).then((f) => ngModuleFactory = f);
|
||||
tick();
|
||||
expect(ngModuleFactory.moduleType).toBe(SomeModule);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('compileModuleSync', () => {
|
||||
it('should throw when using a templateUrl that has not been compiled before', () => {
|
||||
@NgModule(
|
||||
{declarations: [SomeCompWithUrlTemplate], entryComponents: [SomeCompWithUrlTemplate]})
|
||||
class SomeModule {
|
||||
}
|
||||
|
||||
resourceLoader.get.and.callFake(() => Promise.resolve(''));
|
||||
expect(() => compiler.compileModuleSync(SomeModule))
|
||||
.toThrowError(`Can't compile synchronously as ${
|
||||
stringify(SomeCompWithUrlTemplate)} is still being loaded!`);
|
||||
});
|
||||
|
||||
it('should throw when using a templateUrl in a nested component that has not been compiled before',
|
||||
() => {
|
||||
@NgModule({declarations: [SomeComp, ChildComp], entryComponents: [SomeComp]})
|
||||
class SomeModule {
|
||||
}
|
||||
|
||||
resourceLoader.get.and.callFake(() => Promise.resolve(''));
|
||||
dirResolver.setDirective(SomeComp, new Component({selector: 'some-cmp', template: ''}));
|
||||
dirResolver.setDirective(
|
||||
ChildComp, new Component({selector: 'child-cmp', templateUrl: '/someTpl.html'}));
|
||||
expect(() => compiler.compileModuleSync(SomeModule))
|
||||
.toThrowError(
|
||||
`Can't compile synchronously as ${stringify(ChildComp)} is still being loaded!`);
|
||||
});
|
||||
|
||||
it('should allow to use templateUrl components that have been loaded before',
|
||||
fakeAsync(() => {
|
||||
@NgModule({
|
||||
declarations: [SomeCompWithUrlTemplate],
|
||||
entryComponents: [SomeCompWithUrlTemplate]
|
||||
})
|
||||
class SomeModule {
|
||||
}
|
||||
|
||||
resourceLoader.get.and.callFake(() => Promise.resolve('hello'));
|
||||
compiler.compileModuleAsync(SomeModule);
|
||||
tick();
|
||||
|
||||
const ngModuleFactory = compiler.compileModuleSync(SomeModule);
|
||||
expect(ngModuleFactory).toBeTruthy();
|
||||
}));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -6,11 +6,7 @@ ts_library(
|
|||
srcs = glob(["**/*.ts"]),
|
||||
deps = [
|
||||
"//packages:types",
|
||||
"//packages/common",
|
||||
"//packages/compiler",
|
||||
"//packages/compiler/testing",
|
||||
"//packages/core/testing",
|
||||
"//packages/platform-browser",
|
||||
"//packages/platform-browser/testing",
|
||||
],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {ɵgetDOM as getDOM} from '@angular/common';
|
||||
import {CssSelector, SelectorMatcher} from '@angular/compiler/src/selector';
|
||||
import {el} from '@angular/platform-browser/testing/src/browser_util';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,62 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 {SecurityContext} from '@angular/core';
|
||||
import {inject} from '@angular/core/testing';
|
||||
|
||||
import {ElementSchemaRegistry} from '../../src/schema/element_schema_registry';
|
||||
import {calcPossibleSecurityContexts} from '../../src/template_parser/binding_parser';
|
||||
|
||||
{
|
||||
describe('BindingParser', () => {
|
||||
let registry: ElementSchemaRegistry;
|
||||
|
||||
beforeEach(inject([ElementSchemaRegistry], (_registry: ElementSchemaRegistry) => {
|
||||
registry = _registry;
|
||||
}));
|
||||
|
||||
describe('possibleSecurityContexts', () => {
|
||||
function hrefSecurityContexts(selector: string) {
|
||||
return calcPossibleSecurityContexts(registry, selector, 'href', false);
|
||||
}
|
||||
|
||||
it('should return a single security context if the selector as an element name', () => {
|
||||
expect(hrefSecurityContexts('a')).toEqual([SecurityContext.URL]);
|
||||
});
|
||||
|
||||
it('should return the possible security contexts if the selector has no element name', () => {
|
||||
expect(hrefSecurityContexts('[myDir]')).toEqual([
|
||||
SecurityContext.NONE, SecurityContext.URL, SecurityContext.RESOURCE_URL
|
||||
]);
|
||||
});
|
||||
|
||||
it('should exclude possible elements via :not', () => {
|
||||
expect(hrefSecurityContexts('[myDir]:not(link):not(base)')).toEqual([
|
||||
SecurityContext.NONE, SecurityContext.URL
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not exclude possible narrowed elements via :not', () => {
|
||||
expect(hrefSecurityContexts('[myDir]:not(link.someClass):not(base.someClass)')).toEqual([
|
||||
SecurityContext.NONE, SecurityContext.URL, SecurityContext.RESOURCE_URL
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return SecurityContext.NONE if there are no possible elements', () => {
|
||||
expect(hrefSecurityContexts('img:not(img)')).toEqual([SecurityContext.NONE]);
|
||||
});
|
||||
|
||||
it('should return the union of the possible security contexts if multiple selectors are specified',
|
||||
() => {
|
||||
expect(calcPossibleSecurityContexts(registry, 'a,link', 'href', false)).toEqual([
|
||||
SecurityContext.URL, SecurityContext.RESOURCE_URL
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -1,367 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 {AbsoluteSourceSpan, CompileDirectiveSummary, CompilePipeMetadata, CompilePipeSummary, SchemaMetadata} from '@angular/compiler';
|
||||
import {TemplateAst} from '@angular/compiler/src/template_parser/template_ast';
|
||||
import {TemplateParser} from '@angular/compiler/src/template_parser/template_parser';
|
||||
import {inject} from '@angular/core/testing';
|
||||
|
||||
import {humanizeExpressionSource} from './util/expression';
|
||||
import {compileDirectiveMetadataCreate, compileTemplateMetadata, createTypeMeta} from './util/metadata';
|
||||
|
||||
describe('expression AST absolute source spans', () => {
|
||||
const fakeTemplate = compileTemplateMetadata({animations: []});
|
||||
const fakeComponent = compileDirectiveMetadataCreate({
|
||||
isHost: false,
|
||||
selector: 'app-fake',
|
||||
template: fakeTemplate,
|
||||
type: createTypeMeta({reference: {filePath: 'fake-path', name: 'FakeComponent'}}),
|
||||
isComponent: true
|
||||
});
|
||||
const ngIf = compileDirectiveMetadataCreate({
|
||||
selector: '[ngIf]',
|
||||
template: fakeTemplate,
|
||||
type: createTypeMeta({reference: {filePath: 'fake-path', name: 'NgIf'}}),
|
||||
inputs: ['ngIf']
|
||||
}).toSummary();
|
||||
let parse: (
|
||||
template: string, directives?: CompileDirectiveSummary[], pipes?: CompilePipeSummary[],
|
||||
schemas?: SchemaMetadata[], preserveWhitespaces?: boolean) => TemplateAst[];
|
||||
|
||||
beforeEach(inject([TemplateParser], (parser: TemplateParser) => {
|
||||
parse =
|
||||
(template: string, directives: CompileDirectiveSummary[] = [],
|
||||
pipes: CompilePipeSummary[]|null = null, schemas: SchemaMetadata[] = [],
|
||||
preserveWhitespaces = true): TemplateAst[] => {
|
||||
if (pipes === null) {
|
||||
pipes = [];
|
||||
}
|
||||
return parser
|
||||
.parse(
|
||||
fakeComponent, template, directives, pipes, schemas, 'TestComponent',
|
||||
preserveWhitespaces)
|
||||
.template;
|
||||
};
|
||||
}));
|
||||
|
||||
it('should provide absolute offsets of an expression in a bound text', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{foo}}</div>'))).toContain([
|
||||
'{{ foo }}', new AbsoluteSourceSpan(5, 12)
|
||||
]);
|
||||
});
|
||||
|
||||
it('should provide absolute offsets of an expression in a bound event', () => {
|
||||
expect(humanizeExpressionSource(parse('<div (click)="foo();bar();"></div>'))).toContain([
|
||||
'foo(); bar();', new AbsoluteSourceSpan(14, 26)
|
||||
]);
|
||||
|
||||
expect(humanizeExpressionSource(parse('<div on-click="foo();bar();"></div>'))).toContain([
|
||||
'foo(); bar();', new AbsoluteSourceSpan(15, 27)
|
||||
]);
|
||||
});
|
||||
|
||||
it('should provide absolute offsets of an expression in a bound attribute', () => {
|
||||
expect(humanizeExpressionSource(parse('<input [disabled]="condition ? true : false" />')))
|
||||
.toContain(['condition ? true : false', new AbsoluteSourceSpan(19, 43)]);
|
||||
|
||||
expect(humanizeExpressionSource(parse('<input bind-disabled="condition ? true : false" />')))
|
||||
.toContain(['condition ? true : false', new AbsoluteSourceSpan(22, 46)]);
|
||||
});
|
||||
|
||||
it('should provide absolute offsets of an expression in a template attribute', () => {
|
||||
const ngTemplate =
|
||||
compileDirectiveMetadataCreate({
|
||||
selector: 'ng-template',
|
||||
type: createTypeMeta({reference: {filePath: 'fake-path', name: 'OnTemplate'}})
|
||||
}).toSummary();
|
||||
|
||||
expect(humanizeExpressionSource(parse('<div *ngIf="value"></div>', [ngIf, ngTemplate])))
|
||||
.toContain(['value', new AbsoluteSourceSpan(12, 17)]);
|
||||
});
|
||||
|
||||
describe('binary expression', () => {
|
||||
it('should provide absolute offsets of a binary expression', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{1 + 2}}<div>'))).toContain([
|
||||
'1 + 2', new AbsoluteSourceSpan(7, 12)
|
||||
]);
|
||||
});
|
||||
|
||||
it('should provide absolute offsets of expressions in a binary expression', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{1 + 2}}<div>')))
|
||||
.toEqual(jasmine.arrayContaining([
|
||||
['1', new AbsoluteSourceSpan(7, 8)],
|
||||
['2', new AbsoluteSourceSpan(11, 12)],
|
||||
]));
|
||||
});
|
||||
});
|
||||
|
||||
describe('conditional', () => {
|
||||
it('should provide absolute offsets of a conditional', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{bool ? 1 : 0}}<div>'))).toContain([
|
||||
'bool ? 1 : 0', new AbsoluteSourceSpan(7, 19)
|
||||
]);
|
||||
});
|
||||
|
||||
it('should provide absolute offsets of expressions in a conditional', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{bool ? 1 : 0}}<div>')))
|
||||
.toEqual(jasmine.arrayContaining([
|
||||
['bool', new AbsoluteSourceSpan(7, 11)],
|
||||
['1', new AbsoluteSourceSpan(14, 15)],
|
||||
['0', new AbsoluteSourceSpan(18, 19)],
|
||||
]));
|
||||
});
|
||||
});
|
||||
|
||||
describe('chain', () => {
|
||||
it('should provide absolute offsets of a chain', () => {
|
||||
expect(humanizeExpressionSource(parse('<div (click)="a(); b();"><div>'))).toContain([
|
||||
'a(); b();', new AbsoluteSourceSpan(14, 23)
|
||||
]);
|
||||
});
|
||||
|
||||
it('should provide absolute offsets of expressions in a chain', () => {
|
||||
expect(humanizeExpressionSource(parse('<div (click)="a(); b();"><div>')))
|
||||
.toEqual(jasmine.arrayContaining([
|
||||
['a()', new AbsoluteSourceSpan(14, 17)],
|
||||
['b()', new AbsoluteSourceSpan(19, 22)],
|
||||
]));
|
||||
});
|
||||
});
|
||||
|
||||
describe('function call', () => {
|
||||
it('should provide absolute offsets of a function call', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{fn()()}}<div>'))).toContain([
|
||||
'fn()()', new AbsoluteSourceSpan(7, 13)
|
||||
]);
|
||||
});
|
||||
|
||||
it('should provide absolute offsets of expressions in a function call', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{fn()(param)}}<div>'))).toContain([
|
||||
'param', new AbsoluteSourceSpan(12, 17)
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should provide absolute offsets of an implicit receiver', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{a.b}}<div>'))).toContain([
|
||||
'', new AbsoluteSourceSpan(7, 7)
|
||||
]);
|
||||
});
|
||||
|
||||
describe('interpolation', () => {
|
||||
it('should provide absolute offsets of an interpolation', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{1 + foo.length}}<div>'))).toContain([
|
||||
'{{ 1 + foo.length }}', new AbsoluteSourceSpan(5, 23)
|
||||
]);
|
||||
});
|
||||
|
||||
it('should provide absolute offsets of expressions in an interpolation', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{1 + 2}}<div>')))
|
||||
.toEqual(jasmine.arrayContaining([
|
||||
['1', new AbsoluteSourceSpan(7, 8)],
|
||||
['2', new AbsoluteSourceSpan(11, 12)],
|
||||
]));
|
||||
});
|
||||
});
|
||||
|
||||
describe('keyed read', () => {
|
||||
it('should provide absolute offsets of a keyed read', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{obj[key]}}<div>'))).toContain([
|
||||
'obj[key]', new AbsoluteSourceSpan(7, 15)
|
||||
]);
|
||||
});
|
||||
|
||||
it('should provide absolute offsets of expressions in a keyed read', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{obj[key]}}<div>'))).toContain([
|
||||
'key', new AbsoluteSourceSpan(11, 14)
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('keyed write', () => {
|
||||
it('should provide absolute offsets of a keyed write', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{obj[key] = 0}}<div>'))).toContain([
|
||||
'obj[key] = 0', new AbsoluteSourceSpan(7, 19)
|
||||
]);
|
||||
});
|
||||
|
||||
it('should provide absolute offsets of expressions in a keyed write', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{obj[key] = 0}}<div>')))
|
||||
.toEqual(jasmine.arrayContaining([
|
||||
['key', new AbsoluteSourceSpan(11, 14)],
|
||||
['0', new AbsoluteSourceSpan(18, 19)],
|
||||
]));
|
||||
});
|
||||
});
|
||||
|
||||
it('should provide absolute offsets of a literal primitive', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{100}}<div>'))).toContain([
|
||||
'100', new AbsoluteSourceSpan(7, 10)
|
||||
]);
|
||||
});
|
||||
|
||||
describe('literal array', () => {
|
||||
it('should provide absolute offsets of a literal array', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{[0, 1, 2]}}<div>'))).toContain([
|
||||
'[0, 1, 2]', new AbsoluteSourceSpan(7, 16)
|
||||
]);
|
||||
});
|
||||
|
||||
it('should provide absolute offsets of expressions in a literal array', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{[0, 1, 2]}}<div>')))
|
||||
.toEqual(jasmine.arrayContaining([
|
||||
['0', new AbsoluteSourceSpan(8, 9)],
|
||||
['1', new AbsoluteSourceSpan(11, 12)],
|
||||
['2', new AbsoluteSourceSpan(14, 15)],
|
||||
]));
|
||||
});
|
||||
});
|
||||
|
||||
describe('literal map', () => {
|
||||
it('should provide absolute offsets of a literal map', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{ {a: 0} }}<div>'))).toContain([
|
||||
'{a: 0}', new AbsoluteSourceSpan(8, 14)
|
||||
]);
|
||||
});
|
||||
|
||||
it('should provide absolute offsets of expressions in a literal map', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{ {a: 0} }}<div>')))
|
||||
.toEqual(jasmine.arrayContaining([
|
||||
['0', new AbsoluteSourceSpan(12, 13)],
|
||||
]));
|
||||
});
|
||||
});
|
||||
|
||||
describe('method call', () => {
|
||||
it('should provide absolute offsets of a method call', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{method()}}</div>'))).toContain([
|
||||
'method()', new AbsoluteSourceSpan(7, 15)
|
||||
]);
|
||||
});
|
||||
|
||||
it('should provide absolute offsets of expressions in a method call', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{method(param)}}<div>'))).toContain([
|
||||
'param', new AbsoluteSourceSpan(14, 19)
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('non-null assert', () => {
|
||||
it('should provide absolute offsets of a non-null assert', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{prop!}}</div>'))).toContain([
|
||||
'prop!', new AbsoluteSourceSpan(7, 12)
|
||||
]);
|
||||
});
|
||||
|
||||
it('should provide absolute offsets of expressions in a non-null assert', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{prop!}}<div>'))).toContain([
|
||||
'prop', new AbsoluteSourceSpan(7, 11)
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pipe', () => {
|
||||
const testPipe = new CompilePipeMetadata({
|
||||
name: 'test',
|
||||
type: createTypeMeta({reference: {filePath: 'fake-path', name: 'TestPipe'}}),
|
||||
pure: false
|
||||
}).toSummary();
|
||||
|
||||
it('should provide absolute offsets of a pipe', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{prop | test}}<div>', [], [testPipe])))
|
||||
.toContain(['(prop | test)', new AbsoluteSourceSpan(7, 18)]);
|
||||
});
|
||||
|
||||
it('should provide absolute offsets expressions in a pipe', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{prop | test}}<div>', [], [testPipe])))
|
||||
.toContain(['prop', new AbsoluteSourceSpan(7, 11)]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('property read', () => {
|
||||
it('should provide absolute offsets of a property read', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{prop.obj}}<div>'))).toContain([
|
||||
'prop.obj', new AbsoluteSourceSpan(7, 15)
|
||||
]);
|
||||
});
|
||||
|
||||
it('should provide absolute offsets of expressions in a property read', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{prop.obj}}<div>'))).toContain([
|
||||
'prop', new AbsoluteSourceSpan(7, 11)
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('property write', () => {
|
||||
it('should provide absolute offsets of a property write', () => {
|
||||
expect(humanizeExpressionSource(parse('<div (click)="prop = 0"></div>'))).toContain([
|
||||
'prop = 0', new AbsoluteSourceSpan(14, 22)
|
||||
]);
|
||||
});
|
||||
|
||||
it('should provide absolute offsets of an accessed property write', () => {
|
||||
expect(humanizeExpressionSource(parse('<div (click)="prop.inner = 0"></div>'))).toContain([
|
||||
'prop.inner = 0', new AbsoluteSourceSpan(14, 28)
|
||||
]);
|
||||
});
|
||||
|
||||
it('should provide absolute offsets of expressions in a property write', () => {
|
||||
expect(humanizeExpressionSource(parse('<div (click)="prop = 0"></div>'))).toContain([
|
||||
'0', new AbsoluteSourceSpan(21, 22)
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('"not" prefix', () => {
|
||||
it('should provide absolute offsets of a "not" prefix', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{!prop}}</div>'))).toContain([
|
||||
'!prop', new AbsoluteSourceSpan(7, 12)
|
||||
]);
|
||||
});
|
||||
|
||||
it('should provide absolute offsets of expressions in a "not" prefix', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{!prop}}<div>'))).toContain([
|
||||
'prop', new AbsoluteSourceSpan(8, 12)
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('safe method call', () => {
|
||||
it('should provide absolute offsets of a safe method call', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{prop?.safe()}}<div>'))).toContain([
|
||||
'prop?.safe()', new AbsoluteSourceSpan(7, 19)
|
||||
]);
|
||||
});
|
||||
|
||||
it('should provide absolute offsets of expressions in safe method call', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{prop?.safe()}}<div>'))).toContain([
|
||||
'prop', new AbsoluteSourceSpan(7, 11)
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('safe property read', () => {
|
||||
it('should provide absolute offsets of a safe property read', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{prop?.safe}}<div>'))).toContain([
|
||||
'prop?.safe', new AbsoluteSourceSpan(7, 17)
|
||||
]);
|
||||
});
|
||||
|
||||
it('should provide absolute offsets of expressions in safe property read', () => {
|
||||
expect(humanizeExpressionSource(parse('<div>{{prop?.safe}}<div>'))).toContain([
|
||||
'prop', new AbsoluteSourceSpan(7, 11)
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should provide absolute offsets of a quote', () => {
|
||||
expect(humanizeExpressionSource(parse('<div [class.some-class]="a:b"></div>'))).toContain([
|
||||
'a:b', new AbsoluteSourceSpan(25, 28)
|
||||
]);
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,54 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 {inject} from '../../../core/testing';
|
||||
import {Element} from '../../src/ml_parser/ast';
|
||||
import {HtmlParser} from '../../src/ml_parser/html_parser';
|
||||
import {PreparsedElement, PreparsedElementType, preparseElement} from '../../src/template_parser/template_preparser';
|
||||
|
||||
{
|
||||
describe('preparseElement', () => {
|
||||
let htmlParser: HtmlParser;
|
||||
beforeEach(inject([HtmlParser], (_htmlParser: HtmlParser) => {
|
||||
htmlParser = _htmlParser;
|
||||
}));
|
||||
|
||||
function preparse(html: string): PreparsedElement {
|
||||
return preparseElement(htmlParser.parse(html, 'TestComp').rootNodes[0] as Element);
|
||||
}
|
||||
|
||||
it('should detect script elements', inject([HtmlParser], (htmlParser: HtmlParser) => {
|
||||
expect(preparse('<script>').type).toBe(PreparsedElementType.SCRIPT);
|
||||
}));
|
||||
|
||||
it('should detect style elements', inject([HtmlParser], (htmlParser: HtmlParser) => {
|
||||
expect(preparse('<style>').type).toBe(PreparsedElementType.STYLE);
|
||||
}));
|
||||
|
||||
it('should detect stylesheet elements', inject([HtmlParser], (htmlParser: HtmlParser) => {
|
||||
expect(preparse('<link rel="stylesheet">').type).toBe(PreparsedElementType.STYLESHEET);
|
||||
expect(preparse('<link rel="stylesheet" href="someUrl">').hrefAttr).toEqual('someUrl');
|
||||
expect(preparse('<link rel="someRel">').type).toBe(PreparsedElementType.OTHER);
|
||||
}));
|
||||
|
||||
it('should detect ng-content elements', inject([HtmlParser], (htmlParser: HtmlParser) => {
|
||||
expect(preparse('<ng-content>').type).toBe(PreparsedElementType.NG_CONTENT);
|
||||
}));
|
||||
|
||||
it('should normalize ng-content.select attribute',
|
||||
inject([HtmlParser], (htmlParser: HtmlParser) => {
|
||||
expect(preparse('<ng-content>').selectAttr).toEqual('*');
|
||||
expect(preparse('<ng-content select>').selectAttr).toEqual('*');
|
||||
expect(preparse('<ng-content select="*">').selectAttr).toEqual('*');
|
||||
}));
|
||||
|
||||
it('should extract ngProjectAs value', () => {
|
||||
expect(preparse('<p ngProjectAs="el[attr].class"></p>').projectAs).toEqual('el[attr].class');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -1,167 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 {AbsoluteSourceSpan} from '@angular/compiler';
|
||||
import * as e from '../../../src/expression_parser/ast';
|
||||
import * as t from '../../../src/template_parser/template_ast';
|
||||
import {unparse} from '../../expression_parser/utils/unparser';
|
||||
|
||||
type HumanizedExpressionSource = [string, AbsoluteSourceSpan];
|
||||
class ExpressionSourceHumanizer extends e.RecursiveAstVisitor implements t.TemplateAstVisitor {
|
||||
result: HumanizedExpressionSource[] = [];
|
||||
|
||||
private recordAst(ast: e.AST) {
|
||||
this.result.push([unparse(ast), ast.sourceSpan]);
|
||||
}
|
||||
|
||||
// This method is defined to reconcile the type of ExpressionSourceHumanizer
|
||||
// since both RecursiveAstVisitor and TemplateAstVisitor define the visit()
|
||||
// method in their interfaces.
|
||||
override visit(node: e.AST|t.TemplateAst, context?: any) {
|
||||
node.visit(this, context);
|
||||
}
|
||||
|
||||
visitASTWithSource(ast: e.ASTWithSource) {
|
||||
this.recordAst(ast);
|
||||
this.visitAll([ast.ast], null);
|
||||
}
|
||||
override visitUnary(ast: e.Unary) {
|
||||
this.recordAst(ast);
|
||||
super.visitUnary(ast, null);
|
||||
}
|
||||
override visitBinary(ast: e.Binary) {
|
||||
this.recordAst(ast);
|
||||
super.visitBinary(ast, null);
|
||||
}
|
||||
override visitChain(ast: e.Chain) {
|
||||
this.recordAst(ast);
|
||||
super.visitChain(ast, null);
|
||||
}
|
||||
override visitConditional(ast: e.Conditional) {
|
||||
this.recordAst(ast);
|
||||
super.visitConditional(ast, null);
|
||||
}
|
||||
override visitImplicitReceiver(ast: e.ImplicitReceiver) {
|
||||
this.recordAst(ast);
|
||||
super.visitImplicitReceiver(ast, null);
|
||||
}
|
||||
override visitInterpolation(ast: e.Interpolation) {
|
||||
this.recordAst(ast);
|
||||
super.visitInterpolation(ast, null);
|
||||
}
|
||||
override visitKeyedRead(ast: e.KeyedRead) {
|
||||
this.recordAst(ast);
|
||||
super.visitKeyedRead(ast, null);
|
||||
}
|
||||
override visitKeyedWrite(ast: e.KeyedWrite) {
|
||||
this.recordAst(ast);
|
||||
super.visitKeyedWrite(ast, null);
|
||||
}
|
||||
override visitLiteralPrimitive(ast: e.LiteralPrimitive) {
|
||||
this.recordAst(ast);
|
||||
super.visitLiteralPrimitive(ast, null);
|
||||
}
|
||||
override visitLiteralArray(ast: e.LiteralArray) {
|
||||
this.recordAst(ast);
|
||||
super.visitLiteralArray(ast, null);
|
||||
}
|
||||
override visitLiteralMap(ast: e.LiteralMap) {
|
||||
this.recordAst(ast);
|
||||
super.visitLiteralMap(ast, null);
|
||||
}
|
||||
override visitNonNullAssert(ast: e.NonNullAssert) {
|
||||
this.recordAst(ast);
|
||||
super.visitNonNullAssert(ast, null);
|
||||
}
|
||||
override visitPipe(ast: e.BindingPipe) {
|
||||
this.recordAst(ast);
|
||||
super.visitPipe(ast, null);
|
||||
}
|
||||
override visitPrefixNot(ast: e.PrefixNot) {
|
||||
this.recordAst(ast);
|
||||
super.visitPrefixNot(ast, null);
|
||||
}
|
||||
override visitPropertyRead(ast: e.PropertyRead) {
|
||||
this.recordAst(ast);
|
||||
super.visitPropertyRead(ast, null);
|
||||
}
|
||||
override visitPropertyWrite(ast: e.PropertyWrite) {
|
||||
this.recordAst(ast);
|
||||
super.visitPropertyWrite(ast, null);
|
||||
}
|
||||
override visitSafePropertyRead(ast: e.SafePropertyRead) {
|
||||
this.recordAst(ast);
|
||||
super.visitSafePropertyRead(ast, null);
|
||||
}
|
||||
override visitQuote(ast: e.Quote) {
|
||||
this.recordAst(ast);
|
||||
super.visitQuote(ast, null);
|
||||
}
|
||||
override visitSafeKeyedRead(ast: e.SafeKeyedRead) {
|
||||
this.recordAst(ast);
|
||||
super.visitSafeKeyedRead(ast, null);
|
||||
}
|
||||
override visitCall(ast: e.Call) {
|
||||
this.recordAst(ast);
|
||||
super.visitCall(ast, null);
|
||||
}
|
||||
|
||||
visitNgContent(ast: t.NgContentAst) {}
|
||||
visitEmbeddedTemplate(ast: t.EmbeddedTemplateAst) {
|
||||
t.templateVisitAll(this, ast.attrs);
|
||||
t.templateVisitAll(this, ast.children);
|
||||
t.templateVisitAll(this, ast.directives);
|
||||
t.templateVisitAll(this, ast.outputs);
|
||||
t.templateVisitAll(this, ast.providers);
|
||||
t.templateVisitAll(this, ast.references);
|
||||
t.templateVisitAll(this, ast.variables);
|
||||
}
|
||||
visitElement(ast: t.ElementAst) {
|
||||
t.templateVisitAll(this, ast.attrs);
|
||||
t.templateVisitAll(this, ast.children);
|
||||
t.templateVisitAll(this, ast.directives);
|
||||
t.templateVisitAll(this, ast.inputs);
|
||||
t.templateVisitAll(this, ast.outputs);
|
||||
t.templateVisitAll(this, ast.providers);
|
||||
t.templateVisitAll(this, ast.references);
|
||||
}
|
||||
visitReference(ast: t.ReferenceAst) {}
|
||||
visitVariable(ast: t.VariableAst) {}
|
||||
visitEvent(ast: t.BoundEventAst) {
|
||||
ast.handler.visit(this);
|
||||
}
|
||||
visitElementProperty(ast: t.BoundElementPropertyAst) {
|
||||
ast.value.visit(this);
|
||||
}
|
||||
visitAttr(ast: t.AttrAst) {}
|
||||
visitBoundText(ast: t.BoundTextAst) {
|
||||
ast.value.visit(this);
|
||||
}
|
||||
visitText(ast: t.TextAst) {}
|
||||
visitDirective(ast: t.DirectiveAst) {
|
||||
t.templateVisitAll(this, ast.hostEvents);
|
||||
t.templateVisitAll(this, ast.hostProperties);
|
||||
t.templateVisitAll(this, ast.inputs);
|
||||
}
|
||||
visitDirectiveProperty(ast: t.BoundDirectivePropertyAst) {
|
||||
ast.value.visit(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Humanizes expression AST source spans in a template by returning an array of tuples
|
||||
* [unparsed AST, AST source span]
|
||||
* for each expression in the template.
|
||||
* @param templateAsts template AST to humanize
|
||||
*/
|
||||
export function humanizeExpressionSource(templateAsts: t.TemplateAst[]):
|
||||
HumanizedExpressionSource[] {
|
||||
const humanizer = new ExpressionSourceHumanizer();
|
||||
t.templateVisitAll(humanizer, templateAsts);
|
||||
return humanizer.result;
|
||||
}
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 {CompileDirectiveMetadata, CompileEntryComponentMetadata, CompileProviderMetadata, CompileQueryMetadata, CompileStylesheetMetadata, CompileTemplateMetadata, CompileTypeMetadata, preserveWhitespacesDefault, ProxyClass, StaticSymbol} from '@angular/compiler';
|
||||
import {ChangeDetectionStrategy, RendererType2, ViewEncapsulation} from '@angular/core';
|
||||
|
||||
import {noUndefined} from '../../../src/util';
|
||||
|
||||
export function createTypeMeta({reference, diDeps}: {reference: any, diDeps?: any[]}):
|
||||
CompileTypeMetadata {
|
||||
return {reference: reference, diDeps: diDeps || [], lifecycleHooks: []};
|
||||
}
|
||||
|
||||
export function compileDirectiveMetadataCreate({
|
||||
isHost,
|
||||
type,
|
||||
isComponent,
|
||||
selector,
|
||||
exportAs,
|
||||
inputs,
|
||||
outputs,
|
||||
host,
|
||||
providers,
|
||||
viewProviders,
|
||||
queries,
|
||||
guards,
|
||||
viewQueries,
|
||||
entryComponents,
|
||||
template,
|
||||
componentViewType,
|
||||
rendererType
|
||||
}: Partial<Parameters<typeof CompileDirectiveMetadata.create>[0]>) {
|
||||
return CompileDirectiveMetadata.create({
|
||||
isHost: !!isHost,
|
||||
type: noUndefined(type)!,
|
||||
isComponent: !!isComponent,
|
||||
selector: noUndefined(selector),
|
||||
exportAs: noUndefined(exportAs),
|
||||
changeDetection: null,
|
||||
inputs: inputs || [],
|
||||
outputs: outputs || [],
|
||||
host: host || {},
|
||||
providers: providers || [],
|
||||
viewProviders: viewProviders || [],
|
||||
queries: queries || [],
|
||||
guards: guards || {},
|
||||
viewQueries: viewQueries || [],
|
||||
entryComponents: entryComponents || [],
|
||||
template: noUndefined(template)!,
|
||||
componentViewType: noUndefined(componentViewType),
|
||||
rendererType: noUndefined(rendererType),
|
||||
componentFactory: null,
|
||||
});
|
||||
}
|
||||
|
||||
export function compileTemplateMetadata({
|
||||
encapsulation,
|
||||
template,
|
||||
templateUrl,
|
||||
styles,
|
||||
styleUrls,
|
||||
externalStylesheets,
|
||||
animations,
|
||||
ngContentSelectors,
|
||||
interpolation,
|
||||
isInline,
|
||||
preserveWhitespaces
|
||||
}: Partial<CompileTemplateMetadata>): CompileTemplateMetadata {
|
||||
return new CompileTemplateMetadata({
|
||||
encapsulation: noUndefined(encapsulation),
|
||||
template: noUndefined(template),
|
||||
templateUrl: noUndefined(templateUrl),
|
||||
htmlAst: null,
|
||||
styles: styles || [],
|
||||
styleUrls: styleUrls || [],
|
||||
externalStylesheets: externalStylesheets || [],
|
||||
animations: animations || [],
|
||||
ngContentSelectors: ngContentSelectors || [],
|
||||
interpolation: noUndefined(interpolation),
|
||||
isInline: !!isInline,
|
||||
preserveWhitespaces: preserveWhitespacesDefault(noUndefined(preserveWhitespaces)),
|
||||
});
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 {ElementSchemaRegistry, ResourceLoader, UrlResolver} from '@angular/compiler';
|
||||
import {MockResourceLoader} from '@angular/compiler/testing/src/resource_loader_mock';
|
||||
import {MockSchemaRegistry} from '@angular/compiler/testing/src/schema_registry_mock';
|
||||
import {Provider} from '@angular/core';
|
||||
|
||||
export function createUrlResolverWithoutPackagePrefix(): UrlResolver {
|
||||
return new UrlResolver();
|
||||
}
|
||||
|
||||
// This provider is put here just so that we can access it from multiple
|
||||
// internal test packages.
|
||||
// TODO: get rid of it or move to a separate @angular/internal_testing package
|
||||
export const TEST_COMPILER_PROVIDERS: Provider[] = [
|
||||
{provide: ElementSchemaRegistry, useValue: new MockSchemaRegistry({}, {}, {}, [], [])},
|
||||
{provide: ResourceLoader, useClass: MockResourceLoader, deps: []},
|
||||
{provide: UrlResolver, useFactory: createUrlResolverWithoutPackagePrefix, deps: []}
|
||||
];
|
||||
|
|
@ -93,10 +93,12 @@ import {inject} from '@angular/core/testing';
|
|||
expect(resolver.resolve(null!, 'some/dir/file.txt')).toEqual('some/dir/file.txt');
|
||||
});
|
||||
|
||||
it('should contain a default value of "/" when nothing is provided',
|
||||
inject([UrlResolver], (resolver: UrlResolver) => {
|
||||
expect(resolver.resolve(null!, 'package:file')).toEqual('/file');
|
||||
}));
|
||||
// TODO(alxhub): figure out if this test is still relevant, as it fails in Ivy
|
||||
// since UrlResolver is not part of TestBed.
|
||||
xit('should contain a default value of "/" when nothing is provided',
|
||||
inject([UrlResolver], (resolver: UrlResolver) => {
|
||||
expect(resolver.resolve(null!, 'package:file')).toEqual('/file');
|
||||
}));
|
||||
|
||||
it('should resolve a package value when present within the baseurl', () => {
|
||||
resolver = new UrlResolver('/my_special_dir');
|
||||
|
|
|
|||
|
|
@ -1,34 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 {CompileReflector, core, DirectiveResolver} from '@angular/compiler';
|
||||
|
||||
/**
|
||||
* An implementation of {@link DirectiveResolver} that allows overriding
|
||||
* various properties of directives.
|
||||
*/
|
||||
export class MockDirectiveResolver extends DirectiveResolver {
|
||||
private _directives = new Map<core.Type, core.Directive>();
|
||||
|
||||
constructor(reflector: CompileReflector) {
|
||||
super(reflector);
|
||||
}
|
||||
|
||||
override resolve(type: core.Type): core.Directive;
|
||||
override resolve(type: core.Type, throwIfNotFound: true): core.Directive;
|
||||
override resolve(type: core.Type, throwIfNotFound: boolean): core.Directive|null;
|
||||
override resolve(type: core.Type, throwIfNotFound = true): core.Directive|null {
|
||||
return this._directives.get(type) || super.resolve(type, throwIfNotFound);
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the {@link core.Directive} for a directive.
|
||||
*/
|
||||
setDirective(type: core.Type, metadata: core.Directive): void {
|
||||
this._directives.set(type, metadata);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 {CompileReflector, core, NgModuleResolver} from '@angular/compiler';
|
||||
|
||||
export class MockNgModuleResolver extends NgModuleResolver {
|
||||
private _ngModules = new Map<core.Type, core.NgModule>();
|
||||
|
||||
constructor(reflector: CompileReflector) {
|
||||
super(reflector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the {@link NgModule} for a module.
|
||||
*/
|
||||
setNgModule(type: core.Type, metadata: core.NgModule): void {
|
||||
this._ngModules.set(type, metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link NgModule} for a module:
|
||||
* - Set the {@link NgModule} to the overridden view when it exists or fallback to the
|
||||
* default
|
||||
* `NgModuleResolver`, see `setNgModule`.
|
||||
*/
|
||||
override resolve(type: core.Type, throwIfNotFound = true): core.NgModule {
|
||||
return this._ngModules.get(type) || super.resolve(type, throwIfNotFound)!;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 {CompileReflector, core, PipeResolver} from '@angular/compiler';
|
||||
|
||||
export class MockPipeResolver extends PipeResolver {
|
||||
private _pipes = new Map<core.Type, core.Pipe>();
|
||||
|
||||
constructor(refector: CompileReflector) {
|
||||
super(refector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the {@link Pipe} for a pipe.
|
||||
*/
|
||||
setPipe(type: core.Type, metadata: core.Pipe): void {
|
||||
this._pipes.set(type, metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link Pipe} for a pipe:
|
||||
* - Set the {@link Pipe} to the overridden view when it exists or fallback to the
|
||||
* default
|
||||
* `PipeResolver`, see `setPipe`.
|
||||
*/
|
||||
override resolve(type: core.Type, throwIfNotFound = true): core.Pipe {
|
||||
let metadata = this._pipes.get(type);
|
||||
if (!metadata) {
|
||||
metadata = super.resolve(type, throwIfNotFound)!;
|
||||
}
|
||||
return metadata;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 {core, ElementSchemaRegistry} from '@angular/compiler';
|
||||
|
||||
export class MockSchemaRegistry implements ElementSchemaRegistry {
|
||||
constructor(
|
||||
public existingProperties: {[key: string]: boolean},
|
||||
public attrPropMapping: {[key: string]: string},
|
||||
public existingElements: {[key: string]: boolean}, public invalidProperties: Array<string>,
|
||||
public invalidAttributes: Array<string>) {}
|
||||
|
||||
hasProperty(tagName: string, property: string, schemas: core.SchemaMetadata[]): boolean {
|
||||
const value = this.existingProperties[property];
|
||||
return value === void 0 ? true : value;
|
||||
}
|
||||
|
||||
hasElement(tagName: string, schemaMetas: core.SchemaMetadata[]): boolean {
|
||||
const value = this.existingElements[tagName.toLowerCase()];
|
||||
return value === void 0 ? true : value;
|
||||
}
|
||||
|
||||
allKnownElementNames(): string[] {
|
||||
return Object.keys(this.existingElements);
|
||||
}
|
||||
|
||||
securityContext(selector: string, property: string, isAttribute: boolean): core.SecurityContext {
|
||||
return core.SecurityContext.NONE;
|
||||
}
|
||||
|
||||
getMappedPropName(attrName: string): string {
|
||||
return this.attrPropMapping[attrName] || attrName;
|
||||
}
|
||||
|
||||
getDefaultComponentElementName(): string {
|
||||
return 'ng-component';
|
||||
}
|
||||
|
||||
validateProperty(name: string): {error: boolean, msg?: string} {
|
||||
if (this.invalidProperties.indexOf(name) > -1) {
|
||||
return {error: true, msg: `Binding to property '${name}' is disallowed for security reasons`};
|
||||
} else {
|
||||
return {error: false};
|
||||
}
|
||||
}
|
||||
|
||||
validateAttribute(name: string): {error: boolean, msg?: string} {
|
||||
if (this.invalidAttributes.indexOf(name) > -1) {
|
||||
return {
|
||||
error: true,
|
||||
msg: `Binding to attribute '${name}' is disallowed for security reasons`
|
||||
};
|
||||
} else {
|
||||
return {error: false};
|
||||
}
|
||||
}
|
||||
|
||||
normalizeAnimationStyleProperty(propName: string): string {
|
||||
return propName;
|
||||
}
|
||||
normalizeAnimationStyleValue(camelCaseProp: string, userProvidedProp: string, val: string|number):
|
||||
{error: string, value: string} {
|
||||
return {error: null!, value: val.toString()};
|
||||
}
|
||||
}
|
||||
|
|
@ -21,8 +21,10 @@
|
|||
* </p>
|
||||
* </div>
|
||||
*/
|
||||
export * from './resource_loader_mock';
|
||||
export * from './schema_registry_mock';
|
||||
export * from './directive_resolver_mock';
|
||||
export * from './ng_module_resolver_mock';
|
||||
export * from './pipe_resolver_mock';
|
||||
|
||||
// TODO(alxhub): @angular/compiler/testing is unused in Ivy. A placeholder
|
||||
// is left here because the components repo still depends on this package
|
||||
// directly. It will be cleaned up at a later date.
|
||||
//
|
||||
// This export ensures this package is not empty.
|
||||
export const unusedExport = true;
|
||||
|
|
|
|||
|
|
@ -32,12 +32,27 @@ genrule(
|
|||
tools = ["@npm//typescript/bin:tsc"],
|
||||
)
|
||||
|
||||
UTILS = [
|
||||
"linker/source_map_util.ts",
|
||||
]
|
||||
|
||||
ts_library(
|
||||
name = "test_utils",
|
||||
testonly = True,
|
||||
srcs = UTILS,
|
||||
deps = [
|
||||
"//packages/compiler",
|
||||
"@npm//base64-js",
|
||||
"@npm//source-map",
|
||||
],
|
||||
)
|
||||
|
||||
ts_library(
|
||||
name = "test_lib",
|
||||
testonly = True,
|
||||
srcs = glob(
|
||||
["**/*.ts"],
|
||||
exclude = [
|
||||
exclude = UTILS + [
|
||||
"**/*_node_only_spec.ts",
|
||||
"reflection/es2015_inheritance_fixture.ts",
|
||||
],
|
||||
|
|
@ -45,13 +60,13 @@ ts_library(
|
|||
# Visible to //:saucelabs_unit_tests_poc target
|
||||
visibility = ["//:__pkg__"],
|
||||
deps = [
|
||||
":test_utils",
|
||||
"//packages/animations",
|
||||
"//packages/animations/browser",
|
||||
"//packages/animations/browser/testing",
|
||||
"//packages/common",
|
||||
"//packages/common/locales",
|
||||
"//packages/compiler",
|
||||
"//packages/compiler/testing",
|
||||
"//packages/core",
|
||||
"//packages/core/src/di/interface",
|
||||
"//packages/core/src/interface",
|
||||
|
|
@ -74,11 +89,14 @@ ts_library(
|
|||
ts_library(
|
||||
name = "test_node_only_lib",
|
||||
testonly = True,
|
||||
srcs = glob(["**/*_node_only_spec.ts"]),
|
||||
srcs = glob(
|
||||
["**/*_node_only_spec.ts"],
|
||||
exclude = UTILS,
|
||||
),
|
||||
deps = [
|
||||
":test_lib",
|
||||
":test_utils",
|
||||
"//packages/compiler",
|
||||
"//packages/compiler/testing",
|
||||
"//packages/core",
|
||||
"//packages/core/src/compiler",
|
||||
"//packages/core/testing",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package(default_visibility = ["//visibility:private"])
|
||||
|
||||
load("//tools:defaults.bzl", "jasmine_node_test", "karma_web_test_suite", "ts_library")
|
||||
|
||||
package(default_visibility = ["//visibility:private"])
|
||||
|
||||
ts_library(
|
||||
name = "acceptance_lib",
|
||||
testonly = True,
|
||||
|
|
@ -17,7 +17,6 @@ ts_library(
|
|||
"//packages/common",
|
||||
"//packages/common/locales",
|
||||
"//packages/compiler",
|
||||
"//packages/compiler/testing",
|
||||
"//packages/core",
|
||||
"//packages/core/src/util",
|
||||
"//packages/core/test/render3:matchers",
|
||||
|
|
|
|||
|
|
@ -27,10 +27,10 @@ js_size_tracking_test(
|
|||
src = "angular/packages/core/test/bundling/core_all/bundle.min.js",
|
||||
angular_ivy_enabled = "True",
|
||||
data = [
|
||||
"bundle.golden_size_map_ivy.json",
|
||||
"bundle.golden_size_map.json",
|
||||
":bundle",
|
||||
],
|
||||
golden_file = "angular/packages/core/test/bundling/core_all/bundle.golden_size_map_ivy.json",
|
||||
golden_file = "angular/packages/core/test/bundling/core_all/bundle.golden_size_map.json",
|
||||
max_byte_diff = 250,
|
||||
max_percentage_diff = 15,
|
||||
source_map = "angular/packages/core/test/bundling/core_all/bundle.min.js.map",
|
||||
|
|
@ -38,24 +38,3 @@ js_size_tracking_test(
|
|||
"manual",
|
||||
],
|
||||
)
|
||||
|
||||
js_size_tracking_test(
|
||||
name = "size_test_view_engine",
|
||||
src = "angular/packages/core/test/bundling/core_all/bundle.min.js",
|
||||
# Ensures that this target runs with `--config=view-engine`. This is
|
||||
# necessary because we don't run this test on CI currently, but if we run it manually,
|
||||
# we need to ensure that it runs with View Engine for proper size comparisons.
|
||||
angular_ivy_enabled = "False",
|
||||
data = [
|
||||
"bundle.golden_size_map_view_engine.json",
|
||||
":bundle",
|
||||
],
|
||||
golden_file = "angular/packages/core/test/bundling/core_all/bundle.golden_size_map_view_engine.json",
|
||||
max_byte_diff = 250,
|
||||
max_percentage_diff = 15,
|
||||
source_map = "angular/packages/core/test/bundling/core_all/bundle.min.js.map",
|
||||
tags = [
|
||||
"manual",
|
||||
"view-engine-only",
|
||||
],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,366 +0,0 @@
|
|||
{
|
||||
"unmapped": 21331,
|
||||
"files": {
|
||||
"size": 274151,
|
||||
"@angular/": {
|
||||
"size": 241140,
|
||||
"core/": {
|
||||
"size": 241140,
|
||||
"src/": {
|
||||
"size": 241126,
|
||||
"application_init.ts": 539,
|
||||
"application_module.ts": 531,
|
||||
"application_ref.ts": 6867,
|
||||
"application_tokens.ts": 302,
|
||||
"change_detection/": {
|
||||
"size": 13979,
|
||||
"change_detection.ts": 46,
|
||||
"change_detection_util.ts": 813,
|
||||
"change_detector_ref.ts": 163,
|
||||
"constants.ts": 311,
|
||||
"differs/": {
|
||||
"size": 12646,
|
||||
"default_iterable_differ.ts": 7551,
|
||||
"default_keyvalue_differ.ts": 3846,
|
||||
"iterable_differs.ts": 654,
|
||||
"keyvalue_differs.ts": 595
|
||||
}
|
||||
},
|
||||
"compiler/": {
|
||||
"size": 442,
|
||||
"compiler_facade.ts": 442
|
||||
},
|
||||
"console.ts": 140,
|
||||
"debug/": {
|
||||
"size": 6955,
|
||||
"debug_node.ts": 6955
|
||||
},
|
||||
"di/": {
|
||||
"size": 21250,
|
||||
"forward_ref.ts": 229,
|
||||
"injectable.ts": 207,
|
||||
"injection_token.ts": 336,
|
||||
"injector.ts": 3375,
|
||||
"injector_compatibility.ts": 1879,
|
||||
"interface/": {
|
||||
"size": 761,
|
||||
"defs.ts": 623,
|
||||
"injector.ts": 138
|
||||
},
|
||||
"jit/": {
|
||||
"size": 2127,
|
||||
"environment.ts": 227,
|
||||
"injectable.ts": 801,
|
||||
"util.ts": 1099
|
||||
},
|
||||
"metadata.ts": 163,
|
||||
"r3_injector.ts": 4593,
|
||||
"reflective_errors.ts": 1376,
|
||||
"reflective_injector.ts": 2940,
|
||||
"reflective_key.ts": 589,
|
||||
"reflective_provider.ts": 2001,
|
||||
"scope.ts": 32,
|
||||
"util.ts": 642
|
||||
},
|
||||
"error_handler.ts": 444,
|
||||
"errors.ts": 171,
|
||||
"event_emitter.ts": 943,
|
||||
"i18n/": {
|
||||
"size": 2266,
|
||||
"locale_data.ts": 740,
|
||||
"locale_data_api.ts": 247,
|
||||
"locale_en.ts": 929,
|
||||
"localization.ts": 182,
|
||||
"tokens.ts": 168
|
||||
},
|
||||
"interface/": {
|
||||
"size": 224,
|
||||
"simple_change.ts": 170,
|
||||
"type.ts": 54
|
||||
},
|
||||
"ivy_switch.ts": 4,
|
||||
"linker/": {
|
||||
"size": 4981,
|
||||
"compiler.ts": 809,
|
||||
"component_factory.ts": 82,
|
||||
"component_factory_resolver.ts": 994,
|
||||
"element_ref.ts": 116,
|
||||
"ng_module_factory.ts": 78,
|
||||
"ng_module_factory_loader.ts": 218,
|
||||
"ng_module_factory_registration.ts": 436,
|
||||
"query_list.ts": 1040,
|
||||
"system_js_ng_module_factory_loader.ts": 918,
|
||||
"template_ref.ts": 94,
|
||||
"view_container_ref.ts": 94,
|
||||
"view_ref.ts": 102
|
||||
},
|
||||
"metadata/": {
|
||||
"size": 2425,
|
||||
"di.ts": 546,
|
||||
"directives.ts": 600,
|
||||
"ng_module.ts": 251,
|
||||
"resource_loading.ts": 857,
|
||||
"schema.ts": 51,
|
||||
"view.ts": 120
|
||||
},
|
||||
"platform_core_providers.ts": 118,
|
||||
"profile/": {
|
||||
"size": 442,
|
||||
"profile.ts": 170,
|
||||
"wtf_impl.ts": 272
|
||||
},
|
||||
"reflection/": {
|
||||
"size": 4878,
|
||||
"reflection.ts": 16,
|
||||
"reflection_capabilities.ts": 3677,
|
||||
"reflector.ts": 1185
|
||||
},
|
||||
"render/": {
|
||||
"size": 472,
|
||||
"api.ts": 472
|
||||
},
|
||||
"render3/": {
|
||||
"size": 98145,
|
||||
"bindings.ts": 308,
|
||||
"component.ts": 1254,
|
||||
"component_ref.ts": 2311,
|
||||
"context_discovery.ts": 2098,
|
||||
"definition.ts": 2565,
|
||||
"di.ts": 3517,
|
||||
"di_setup.ts": 1576,
|
||||
"empty.ts": 16,
|
||||
"errors.ts": 472,
|
||||
"features/": {
|
||||
"size": 2573,
|
||||
"inherit_definition_feature.ts": 1889,
|
||||
"ng_onchanges_feature.ts": 571,
|
||||
"providers_feature.ts": 113
|
||||
},
|
||||
"fields.ts": 187,
|
||||
"hooks.ts": 1748,
|
||||
"i18n.ts": 8980,
|
||||
"instructions/": {
|
||||
"size": 24631,
|
||||
"advance.ts": 231,
|
||||
"alloc_host_vars.ts": 290,
|
||||
"attribute.ts": 78,
|
||||
"attribute_interpolation.ts": 1003,
|
||||
"change_detection.ts": 62,
|
||||
"class_map_interpolation.ts": 675,
|
||||
"container.ts": 945,
|
||||
"di.ts": 130,
|
||||
"element.ts": 877,
|
||||
"element_container.ts": 451,
|
||||
"embedded_view.ts": 655,
|
||||
"get_current_view.ts": 26,
|
||||
"host_property.ts": 165,
|
||||
"interpolation.ts": 1194,
|
||||
"listener.ts": 1431,
|
||||
"lview_debug.ts": 685,
|
||||
"next_context.ts": 44,
|
||||
"projection.ts": 517,
|
||||
"property.ts": 78,
|
||||
"property_interpolation.ts": 1008,
|
||||
"shared.ts": 10199,
|
||||
"storage.ts": 136,
|
||||
"style_prop_interpolation.ts": 837,
|
||||
"styling.ts": 1870,
|
||||
"text.ts": 116,
|
||||
"text_interpolation.ts": 928
|
||||
},
|
||||
"interfaces/": {
|
||||
"size": 909,
|
||||
"container.ts": 34,
|
||||
"context.ts": 23,
|
||||
"i18n.ts": 48,
|
||||
"injector.ts": 227,
|
||||
"renderer.ts": 165,
|
||||
"type_checks.ts": 302,
|
||||
"view.ts": 110
|
||||
},
|
||||
"jit/": {
|
||||
"size": 10595,
|
||||
"directive.ts": 3998,
|
||||
"environment.ts": 3364,
|
||||
"module.ts": 2764,
|
||||
"pipe.ts": 469
|
||||
},
|
||||
"metadata.ts": 392,
|
||||
"ng_module_ref.ts": 966,
|
||||
"node_manipulation.ts": 4148,
|
||||
"node_selector_matcher.ts": 1650,
|
||||
"node_util.ts": 222,
|
||||
"pipe.ts": 1034,
|
||||
"pure_function.ts": 1274,
|
||||
"query.ts": 5226,
|
||||
"state.ts": 1311,
|
||||
"styling/": {
|
||||
"size": 5655,
|
||||
"bindings.ts": 3235,
|
||||
"map_based_bindings.ts": 941,
|
||||
"state.ts": 403,
|
||||
"styling_debug.ts": 1076
|
||||
},
|
||||
"tokens.ts": 10,
|
||||
"util/": {
|
||||
"size": 6561,
|
||||
"attrs_utils.ts": 374,
|
||||
"discovery_utils.ts": 1984,
|
||||
"global_utils.ts": 330,
|
||||
"injector_utils.ts": 150,
|
||||
"misc_utils.ts": 575,
|
||||
"styling_utils.ts": 2381,
|
||||
"view_traversal_utils.ts": 228,
|
||||
"view_utils.ts": 539
|
||||
},
|
||||
"view_engine_compatibility.ts": 3713,
|
||||
"view_engine_compatibility_prebound.ts": 180,
|
||||
"view_ref.ts": 2063
|
||||
},
|
||||
"sanitization/": {
|
||||
"size": 10193,
|
||||
"bypass.ts": 1241,
|
||||
"html_sanitizer.ts": 4491,
|
||||
"inert_body.ts": 2066,
|
||||
"sanitization.ts": 1065,
|
||||
"sanitizer.ts": 121,
|
||||
"security.ts": 160,
|
||||
"style_sanitizer.ts": 576,
|
||||
"url_sanitizer.ts": 473
|
||||
},
|
||||
"testability/": {
|
||||
"size": 3154,
|
||||
"testability.ts": 3154
|
||||
},
|
||||
"util/": {
|
||||
"size": 4294,
|
||||
"array_utils.ts": 467,
|
||||
"assert.ts": 116,
|
||||
"closure.ts": 37,
|
||||
"comparison.ts": 90,
|
||||
"decorators.ts": 1606,
|
||||
"errors.ts": 164,
|
||||
"global.ts": 243,
|
||||
"is_dev_mode.ts": 138,
|
||||
"lang.ts": 109,
|
||||
"microtask.ts": 180,
|
||||
"ng_reflect.ts": 330,
|
||||
"noop.ts": 68,
|
||||
"property.ts": 201,
|
||||
"stringify.ts": 290,
|
||||
"symbol.ts": 255
|
||||
},
|
||||
"version.ts": 179,
|
||||
"view/": {
|
||||
"size": 54087,
|
||||
"element.ts": 3697,
|
||||
"entrypoint.ts": 698,
|
||||
"errors.ts": 642,
|
||||
"ng_content.ts": 440,
|
||||
"ng_module.ts": 2384,
|
||||
"provider.ts": 5331,
|
||||
"pure_expression.ts": 2195,
|
||||
"query.ts": 2378,
|
||||
"refs.ts": 8739,
|
||||
"services.ts": 11374,
|
||||
"text.ts": 1525,
|
||||
"types.ts": 768,
|
||||
"util.ts": 4652,
|
||||
"view.ts": 8133,
|
||||
"view_attach.ts": 1131
|
||||
},
|
||||
"zone/": {
|
||||
"size": 2701,
|
||||
"ng_zone.ts": 2701
|
||||
}
|
||||
},
|
||||
"test/": {
|
||||
"size": 14,
|
||||
"bundling/": {
|
||||
"size": 14,
|
||||
"core_all/": {
|
||||
"size": 14,
|
||||
"index.ts": 14
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"external/": {
|
||||
"size": 11680,
|
||||
"npm/": {
|
||||
"size": 11680,
|
||||
"node_modules/": {
|
||||
"size": 11680,
|
||||
"rxjs/": {
|
||||
"size": 10200,
|
||||
"src/": {
|
||||
"size": 10200,
|
||||
"internal/": {
|
||||
"size": 10200,
|
||||
"InnerSubscriber.ts": 247,
|
||||
"Notification.ts": 15,
|
||||
"Observable.ts": 622,
|
||||
"Observer.ts": 78,
|
||||
"OuterSubscriber.ts": 80,
|
||||
"Subject.ts": 1102,
|
||||
"SubjectSubscription.ts": 268,
|
||||
"Subscriber.ts": 2066,
|
||||
"Subscription.ts": 783,
|
||||
"config.ts": 32,
|
||||
"observable/": {
|
||||
"size": 2105,
|
||||
"ConnectableObservable.ts": 978,
|
||||
"from.ts": 67,
|
||||
"fromArray.ts": 132,
|
||||
"fromIterable.ts": 224,
|
||||
"fromObservable.ts": 315,
|
||||
"fromPromise.ts": 217,
|
||||
"merge.ts": 172
|
||||
},
|
||||
"operators/": {
|
||||
"size": 1651,
|
||||
"map.ts": 235,
|
||||
"mergeAll.ts": 22,
|
||||
"mergeMap.ts": 712,
|
||||
"multicast.ts": 264,
|
||||
"refCount.ts": 400,
|
||||
"share.ts": 14,
|
||||
"windowToggle.ts": 4
|
||||
},
|
||||
"symbol/": {
|
||||
"size": 103,
|
||||
"iterator.ts": 55,
|
||||
"rxSubscriber.ts": 48
|
||||
},
|
||||
"util/": {
|
||||
"size": 1048,
|
||||
"EmptyError.ts": 6,
|
||||
"ObjectUnsubscribedError.ts": 137,
|
||||
"UnsubscriptionError.ts": 243,
|
||||
"canReportError.ts": 56,
|
||||
"isInteropObservable.ts": 14,
|
||||
"isIterable.ts": 14,
|
||||
"isObject.ts": 3,
|
||||
"isScheduler.ts": 5,
|
||||
"pipe.ts": 32,
|
||||
"subscribeTo.ts": 181,
|
||||
"subscribeToArray.ts": 9,
|
||||
"subscribeToIterable.ts": 126,
|
||||
"subscribeToObservable.ts": 48,
|
||||
"subscribeToPromise.ts": 58,
|
||||
"subscribeToResult.ts": 65,
|
||||
"toSubscriber.ts": 51
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tslib/": {
|
||||
"size": 1480,
|
||||
"tslib.es6.js": 1480
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,6 @@
|
|||
*/
|
||||
|
||||
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, Injectable, Input, OnChanges, OnDestroy, OnInit, Output, Pipe, PipeTransform, Provider, RendererFactory2, RendererType2, SimpleChange, SimpleChanges, TemplateRef, Type, ViewChild, ViewContainerRef} from '@angular/core';
|
||||
import {ComponentFixture, fakeAsync, TestBed} from '@angular/core/testing';
|
||||
import {By} from '@angular/platform-browser/src/dom/debug/by';
|
||||
|
|
@ -15,6 +14,8 @@ import {isTextNode} from '@angular/platform-browser/testing/src/browser_util';
|
|||
import {expect} from '@angular/platform-browser/testing/src/matchers';
|
||||
import {ivyEnabled, modifiedInIvy, onlyInIvy} from '@angular/private/testing';
|
||||
|
||||
import {MockResourceLoader} from './resource_loader_mock';
|
||||
|
||||
export function createUrlResolverWithoutPackagePrefix(): UrlResolver {
|
||||
return new UrlResolver();
|
||||
}
|
||||
|
|
@ -1329,7 +1330,7 @@ describe(`ChangeDetection`, () => {
|
|||
// TODO(issue/24571): remove '!'.
|
||||
@ViewChild('vc', {read: ViewContainerRef, static: true}) vc!: ViewContainerRef;
|
||||
// TODO(issue/24571): remove '!'.
|
||||
@ViewChild(TemplateRef, {static: true}) template !: TemplateRef<any>;
|
||||
@ViewChild(TemplateRef, {static: true}) template!: TemplateRef<any>;
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [Comp]});
|
||||
|
|
|
|||
|
|
@ -1,294 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 {ResourceLoader} from '@angular/compiler';
|
||||
import {CompileMetadataResolver} from '@angular/compiler/src/metadata_resolver';
|
||||
import {MockResourceLoader} from '@angular/compiler/testing/src/resource_loader_mock';
|
||||
import {Component, Directive, Injectable, NgModule, OnDestroy, Pipe} from '@angular/core';
|
||||
import {getTestBed, TestBed, waitForAsync} from '@angular/core/testing';
|
||||
import {expect} from '@angular/platform-browser/testing/src/matchers';
|
||||
import {obsoleteInIvy} from '@angular/private/testing';
|
||||
|
||||
{
|
||||
obsoleteInIvy('Summaries are not used/supported/able to be produced in Ivy. See FW-838.')
|
||||
.describe('Jit Summaries', () => {
|
||||
let instances: Map<any, Base>;
|
||||
let summaries: () => any[];
|
||||
|
||||
class SomeDep {}
|
||||
|
||||
class Base {
|
||||
static annotations: any[];
|
||||
static parameters: any[][];
|
||||
|
||||
constructor(public dep: SomeDep) {
|
||||
instances.set(Object.getPrototypeOf(this).constructor, this);
|
||||
}
|
||||
}
|
||||
|
||||
function expectInstanceCreated(type: any) {
|
||||
const instance = instances.get(type)!;
|
||||
expect(instance).toBeDefined();
|
||||
expect(instance.dep instanceof SomeDep).toBe(true);
|
||||
}
|
||||
|
||||
class SomeModule extends Base {}
|
||||
|
||||
class SomePrivateComponent extends Base {}
|
||||
|
||||
class SomePublicComponent extends Base {}
|
||||
|
||||
class SomeDirective extends Base {}
|
||||
|
||||
class SomePipe extends Base {
|
||||
transform(value: any) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
class SomeService extends Base {}
|
||||
|
||||
// Move back into the it which needs it after https://github.com/angular/tsickle/issues/547
|
||||
// is
|
||||
// fixed.
|
||||
@Component({template: '<div someDir>{{1 | somePipe}}</div>'})
|
||||
class TestComp3 {
|
||||
constructor(service: SomeService) {}
|
||||
}
|
||||
|
||||
@Component({template: ''})
|
||||
class TestCompErrorOnDestroy implements OnDestroy {
|
||||
ngOnDestroy() {}
|
||||
}
|
||||
|
||||
function resetTestEnvironmentWithSummaries(summaries?: () => any[]) {
|
||||
const {platform, ngModule} = getTestBed();
|
||||
TestBed.resetTestEnvironment();
|
||||
TestBed.initTestEnvironment(ngModule, platform, summaries);
|
||||
}
|
||||
|
||||
function createSummaries() {
|
||||
const resourceLoader = new MockResourceLoader();
|
||||
|
||||
setMetadata(resourceLoader);
|
||||
|
||||
TestBed.configureCompiler(
|
||||
{providers: [{provide: ResourceLoader, useValue: resourceLoader}]});
|
||||
TestBed.configureTestingModule({imports: [SomeModule], providers: [SomeDep]});
|
||||
|
||||
let summariesPromise = TestBed.compileComponents().then(() => {
|
||||
const metadataResolver = TestBed.inject(CompileMetadataResolver);
|
||||
const summaries = [
|
||||
metadataResolver.getNgModuleSummary(SomeModule),
|
||||
// test nesting via closures, as we use this in the generated code too.
|
||||
() =>
|
||||
[metadataResolver.getDirectiveSummary(SomePublicComponent),
|
||||
metadataResolver.getDirectiveSummary(SomePrivateComponent),
|
||||
],
|
||||
metadataResolver.getDirectiveSummary(SomeDirective),
|
||||
metadataResolver.getPipeSummary(SomePipe),
|
||||
metadataResolver.getInjectableSummary(SomeService)
|
||||
];
|
||||
clearMetadata();
|
||||
TestBed.resetTestingModule();
|
||||
return () => summaries;
|
||||
});
|
||||
|
||||
resourceLoader.flush();
|
||||
return summariesPromise;
|
||||
}
|
||||
|
||||
function setMetadata(resourceLoader: MockResourceLoader) {
|
||||
Base.parameters = [[SomeDep]];
|
||||
|
||||
SomeModule.annotations = [new NgModule({
|
||||
declarations: [SomePublicComponent, SomePrivateComponent, SomeDirective, SomePipe],
|
||||
exports: [SomeDirective, SomePipe, SomePublicComponent],
|
||||
providers: [SomeService]
|
||||
})];
|
||||
|
||||
SomePublicComponent.annotations = [new Component({templateUrl: 'somePublicUrl.html'})];
|
||||
resourceLoader.expect('somePublicUrl.html', `Hello public world!`);
|
||||
|
||||
SomePrivateComponent.annotations = [new Component({templateUrl: 'somePrivateUrl.html'})];
|
||||
resourceLoader.expect('somePrivateUrl.html', `Hello private world!`);
|
||||
|
||||
SomeDirective.annotations = [new Directive({selector: '[someDir]'})];
|
||||
|
||||
SomePipe.annotations = [new Pipe({name: 'somePipe'})];
|
||||
|
||||
SomeService.annotations = [new Injectable()];
|
||||
}
|
||||
|
||||
function clearMetadata() {
|
||||
Base.parameters = [];
|
||||
SomeModule.annotations = [];
|
||||
SomePublicComponent.annotations = [];
|
||||
SomePrivateComponent.annotations = [];
|
||||
SomeDirective.annotations = [];
|
||||
SomePipe.annotations = [];
|
||||
SomeService.annotations = [];
|
||||
}
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
instances = new Map<any, any>();
|
||||
createSummaries().then(s => summaries = s);
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
resetTestEnvironmentWithSummaries();
|
||||
});
|
||||
|
||||
it('should use directive metadata from summaries', () => {
|
||||
resetTestEnvironmentWithSummaries(summaries);
|
||||
|
||||
@Component({template: '<div someDir></div>'})
|
||||
class TestComp {
|
||||
}
|
||||
|
||||
TestBed
|
||||
.configureTestingModule(
|
||||
{providers: [SomeDep], declarations: [TestComp, SomeDirective]})
|
||||
.createComponent(TestComp);
|
||||
expectInstanceCreated(SomeDirective);
|
||||
});
|
||||
|
||||
|
||||
it('should use pipe metadata from summaries', () => {
|
||||
resetTestEnvironmentWithSummaries(summaries);
|
||||
|
||||
@Component({template: '{{1 | somePipe}}'})
|
||||
class TestComp {
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({providers: [SomeDep], declarations: [TestComp, SomePipe]})
|
||||
.createComponent(TestComp);
|
||||
expectInstanceCreated(SomePipe);
|
||||
});
|
||||
|
||||
it('should use Service metadata from summaries', () => {
|
||||
resetTestEnvironmentWithSummaries(summaries);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [SomeService, SomeDep],
|
||||
});
|
||||
TestBed.inject(SomeService);
|
||||
expectInstanceCreated(SomeService);
|
||||
});
|
||||
|
||||
it('should use NgModule metadata from summaries', () => {
|
||||
resetTestEnvironmentWithSummaries(summaries);
|
||||
|
||||
TestBed
|
||||
.configureTestingModule(
|
||||
{providers: [SomeDep], declarations: [TestComp3], imports: [SomeModule]})
|
||||
.createComponent(TestComp3);
|
||||
|
||||
expectInstanceCreated(SomeModule);
|
||||
expectInstanceCreated(SomeDirective);
|
||||
expectInstanceCreated(SomePipe);
|
||||
expectInstanceCreated(SomeService);
|
||||
});
|
||||
|
||||
it('should allow to create private components from imported NgModule summaries', () => {
|
||||
resetTestEnvironmentWithSummaries(summaries);
|
||||
|
||||
TestBed.configureTestingModule({providers: [SomeDep], imports: [SomeModule]})
|
||||
.createComponent(SomePrivateComponent);
|
||||
expectInstanceCreated(SomePrivateComponent);
|
||||
});
|
||||
|
||||
it('should throw when trying to mock a type with a summary', () => {
|
||||
resetTestEnvironmentWithSummaries(summaries);
|
||||
|
||||
TestBed.resetTestingModule();
|
||||
expect(
|
||||
() => TestBed.overrideComponent(SomePrivateComponent, {add: {}}).compileComponents())
|
||||
.toThrowError(
|
||||
'SomePrivateComponent was AOT compiled, so its metadata cannot be changed.');
|
||||
TestBed.resetTestingModule();
|
||||
expect(() => TestBed.overrideDirective(SomeDirective, {add: {}}).compileComponents())
|
||||
.toThrowError('SomeDirective was AOT compiled, so its metadata cannot be changed.');
|
||||
TestBed.resetTestingModule();
|
||||
expect(() => TestBed.overridePipe(SomePipe, {add: {name: 'test'}}).compileComponents())
|
||||
.toThrowError('SomePipe was AOT compiled, so its metadata cannot be changed.');
|
||||
TestBed.resetTestingModule();
|
||||
expect(() => TestBed.overrideModule(SomeModule, {add: {}}).compileComponents())
|
||||
.toThrowError('SomeModule was AOT compiled, so its metadata cannot be changed.');
|
||||
});
|
||||
|
||||
it('should return stack trace and component data on resetTestingModule when error is thrown',
|
||||
() => {
|
||||
resetTestEnvironmentWithSummaries();
|
||||
|
||||
const fixture =
|
||||
TestBed.configureTestingModule({declarations: [TestCompErrorOnDestroy]})
|
||||
.createComponent<TestCompErrorOnDestroy>(TestCompErrorOnDestroy);
|
||||
|
||||
const expectedError = 'Error from ngOnDestroy';
|
||||
|
||||
const component: TestCompErrorOnDestroy = fixture.componentInstance;
|
||||
|
||||
spyOn(console, 'error');
|
||||
spyOn(component, 'ngOnDestroy').and.throwError(expectedError);
|
||||
|
||||
const expectedObject = {
|
||||
stacktrace: new Error(expectedError),
|
||||
component,
|
||||
};
|
||||
|
||||
expect(() => TestBed.resetTestingModule())
|
||||
.toThrowError('1 component threw errors during cleanup');
|
||||
expect(console.error)
|
||||
.toHaveBeenCalledWith('Error during cleanup of component', expectedObject);
|
||||
});
|
||||
|
||||
it('should allow to add summaries via configureTestingModule', () => {
|
||||
resetTestEnvironmentWithSummaries();
|
||||
|
||||
@Component({template: '<div someDir></div>'})
|
||||
class TestComp {
|
||||
}
|
||||
|
||||
TestBed
|
||||
.configureTestingModule({
|
||||
providers: [SomeDep],
|
||||
declarations: [TestComp, SomeDirective],
|
||||
aotSummaries: summaries
|
||||
})
|
||||
.createComponent(TestComp);
|
||||
expectInstanceCreated(SomeDirective);
|
||||
});
|
||||
|
||||
it('should allow to override a provider', () => {
|
||||
resetTestEnvironmentWithSummaries(summaries);
|
||||
|
||||
const overwrittenValue = {};
|
||||
|
||||
const fixture =
|
||||
TestBed.overrideProvider(SomeDep, {useFactory: () => overwrittenValue, deps: []})
|
||||
.configureTestingModule({providers: [SomeDep], imports: [SomeModule]})
|
||||
.createComponent<SomePublicComponent>(SomePublicComponent);
|
||||
|
||||
expect(fixture.componentInstance.dep).toBe(overwrittenValue);
|
||||
});
|
||||
|
||||
it('should allow to override a template', () => {
|
||||
resetTestEnvironmentWithSummaries(summaries);
|
||||
|
||||
TestBed.overrideTemplateUsingTestingModule(SomePublicComponent, 'overwritten');
|
||||
|
||||
const fixture =
|
||||
TestBed.configureTestingModule({providers: [SomeDep], imports: [SomeModule]})
|
||||
.createComponent(SomePublicComponent);
|
||||
expectInstanceCreated(SomePublicComponent);
|
||||
|
||||
expect(fixture.nativeElement).toHaveText('overwritten');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -10,8 +10,6 @@ import {ResourceLoader, SourceMap} from '@angular/compiler';
|
|||
import {CompilerFacadeImpl} from '@angular/compiler/src/jit_compiler_facade';
|
||||
import {JitEvaluator} from '@angular/compiler/src/output/output_jit';
|
||||
import {escapeRegExp} from '@angular/compiler/src/util';
|
||||
import {extractSourceMap, originalPositionFor} from '@angular/compiler/testing/src/output/source_map_util';
|
||||
import {MockResourceLoader} from '@angular/compiler/testing/src/resource_loader_mock';
|
||||
import {Attribute, Component, Directive, ErrorHandler, ɵglobal} from '@angular/core';
|
||||
import {CompilerFacade, ExportedCompilerFacade} from '@angular/core/src/compiler/compiler_facade';
|
||||
import {getErrorLogger} from '@angular/core/src/errors';
|
||||
|
|
@ -19,6 +17,9 @@ import {resolveComponentResources} from '@angular/core/src/metadata/resource_loa
|
|||
import {fakeAsync, TestBed, tick} from '@angular/core/testing';
|
||||
import {modifiedInIvy, onlyInIvy} from '@angular/private/testing';
|
||||
|
||||
import {MockResourceLoader} from './resource_loader_mock';
|
||||
import {extractSourceMap, originalPositionFor} from './source_map_util';
|
||||
|
||||
describe('jit source mapping', () => {
|
||||
let resourceLoader: MockResourceLoader;
|
||||
let jitEvaluator: MockJitEvaluator;
|
||||
|
|
|
|||
40
packages/core/test/linker/source_map_util.ts
Normal file
40
packages/core/test/linker/source_map_util.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 {SourceMap} from '@angular/compiler';
|
||||
import b64 from 'base64-js';
|
||||
import {SourceMapConsumer} from 'source-map';
|
||||
|
||||
export interface SourceLocation {
|
||||
line: number;
|
||||
column: number;
|
||||
source: string;
|
||||
}
|
||||
|
||||
export function originalPositionFor(
|
||||
sourceMap: SourceMap, genPosition: {line: number, column: number}): SourceLocation {
|
||||
// Note: The `SourceMap` type from the compiler is different to `RawSourceMap`
|
||||
// from the `source-map` package, but the method we rely on works as expected.
|
||||
const smc = new SourceMapConsumer(sourceMap as any);
|
||||
// Note: We don't return the original object as it also contains a `name` property
|
||||
// which is always null and we don't want to include that in our assertions...
|
||||
const {line, column, source} = smc.originalPositionFor(genPosition);
|
||||
return {line, column, source};
|
||||
}
|
||||
|
||||
export function extractSourceMap(source: string): SourceMap|null {
|
||||
let idx = source.lastIndexOf('\n//#');
|
||||
if (idx == -1) return null;
|
||||
const smComment = source.slice(idx).split('\n', 2)[1].trim();
|
||||
const smB64 = smComment.split('sourceMappingURL=data:application/json;base64,')[1];
|
||||
return smB64 ? JSON.parse(decodeB64String(smB64)) as SourceMap : null;
|
||||
}
|
||||
|
||||
function decodeB64String(s: string): string {
|
||||
return b64.toByteArray(s).reduce((s: string, c: number) => s + String.fromCharCode(c), '');
|
||||
}
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
load("//tools:defaults.bzl", "jasmine_node_test", "karma_web_test_suite", "ts_library")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
ts_library(
|
||||
name = "view_lib",
|
||||
testonly = True,
|
||||
srcs = glob(
|
||||
["**/*.ts"],
|
||||
),
|
||||
deps = [
|
||||
"//packages/common",
|
||||
"//packages/core",
|
||||
"//packages/core/src/di/interface",
|
||||
"//packages/core/src/interface",
|
||||
"//packages/core/src/util",
|
||||
"//packages/core/testing",
|
||||
"//packages/platform-browser",
|
||||
"//packages/private/testing",
|
||||
],
|
||||
)
|
||||
|
||||
ts_library(
|
||||
name = "view_node_only_lib",
|
||||
testonly = True,
|
||||
srcs = glob(["**/*_node_only_spec.ts"]),
|
||||
deps = [
|
||||
":view_lib",
|
||||
"//packages/core",
|
||||
"//packages/core/testing",
|
||||
"//packages/private/testing",
|
||||
],
|
||||
)
|
||||
|
||||
jasmine_node_test(
|
||||
name = "view",
|
||||
bootstrap = ["//tools/testing:node_es5"],
|
||||
tags = [
|
||||
"view-engine-only",
|
||||
],
|
||||
deps = [
|
||||
":view_lib",
|
||||
":view_node_only_lib",
|
||||
],
|
||||
)
|
||||
|
||||
karma_web_test_suite(
|
||||
name = "view_web",
|
||||
tags = [
|
||||
"view-engine-only",
|
||||
],
|
||||
deps = [
|
||||
":view_lib",
|
||||
],
|
||||
)
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 {ɵgetDOM as getDOM} from '@angular/common';
|
||||
import {getDebugNode} from '@angular/core';
|
||||
import {anchorDef, asElementData, elementDef, NodeFlags} from '@angular/core/src/view/index';
|
||||
|
||||
import {compViewDef, createAndGetRootNodes} from './helper';
|
||||
|
||||
{
|
||||
describe(`View Anchor`, () => {
|
||||
describe('create', () => {
|
||||
it('should create anchor nodes without parents', () => {
|
||||
const rootNodes = createAndGetRootNodes(compViewDef([
|
||||
anchorDef(NodeFlags.None, null, null, 0)
|
||||
])).rootNodes;
|
||||
expect(rootNodes.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should create views with multiple root anchor nodes', () => {
|
||||
const rootNodes =
|
||||
createAndGetRootNodes(compViewDef([
|
||||
anchorDef(NodeFlags.None, null, null, 0), anchorDef(NodeFlags.None, null, null, 0)
|
||||
])).rootNodes;
|
||||
expect(rootNodes.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should create anchor nodes with parents', () => {
|
||||
const rootNodes = createAndGetRootNodes(compViewDef([
|
||||
elementDef(0, NodeFlags.None, null, null, 1, 'div'),
|
||||
anchorDef(NodeFlags.None, null, null, 0),
|
||||
])).rootNodes;
|
||||
expect(rootNodes[0].childNodes.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should add debug information to the renderer', () => {
|
||||
const someContext = {};
|
||||
const {view, rootNodes} = createAndGetRootNodes(
|
||||
compViewDef([anchorDef(NodeFlags.None, null, null, 0)]), someContext);
|
||||
expect(getDebugNode(rootNodes[0])!.nativeNode).toBe(asElementData(view, 0).renderElement);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -1,353 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 {ɵgetDOM as getDOM} from '@angular/common';
|
||||
import {SecurityContext} from '@angular/core';
|
||||
import {ArgumentType, asElementData, BindingFlags, directiveDef, elementDef, NodeCheckFn, NodeFlags, rootRenderNodes, Services, ViewData, ViewFlags, ViewState} from '@angular/core/src/view/index';
|
||||
|
||||
import {callMostRecentEventListenerHandler, compViewDef, createAndGetRootNodes, createRootView, isBrowser, recordNodeToRemove} from './helper';
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* We map addEventListener to the Zones internal name. This is because we want to be fast
|
||||
* and bypass the zone bookkeeping. We know that we can do the bookkeeping faster.
|
||||
*/
|
||||
const addEventListener = 'addEventListener';
|
||||
|
||||
{
|
||||
describe(`Component Views`, () => {
|
||||
it('should create and attach component views', () => {
|
||||
let instance: AComp = undefined!;
|
||||
class AComp {
|
||||
constructor() {
|
||||
instance = this;
|
||||
}
|
||||
}
|
||||
|
||||
const {view, rootNodes} = createAndGetRootNodes(compViewDef([
|
||||
elementDef(
|
||||
0, NodeFlags.None, null, null, 1, 'div', null, null, null, null,
|
||||
() => compViewDef([
|
||||
elementDef(0, NodeFlags.None, null, null, 0, 'span'),
|
||||
])),
|
||||
directiveDef(1, NodeFlags.Component, null, 0, AComp, []),
|
||||
]));
|
||||
|
||||
const compView = asElementData(view, 0).componentView;
|
||||
|
||||
expect(compView.context).toBe(instance);
|
||||
expect(compView.component).toBe(instance);
|
||||
|
||||
const compRootEl = rootNodes[0].childNodes[0];
|
||||
expect(compRootEl.nodeName.toLowerCase()).toBe('span');
|
||||
});
|
||||
|
||||
if (isBrowser()) {
|
||||
describe('root views', () => {
|
||||
let rootNode: HTMLElement;
|
||||
beforeEach(() => {
|
||||
rootNode = document.createElement('root');
|
||||
document.body.appendChild(rootNode);
|
||||
recordNodeToRemove(rootNode);
|
||||
});
|
||||
|
||||
it('should select root elements based on a selector', () => {
|
||||
const view = createRootView(
|
||||
compViewDef([
|
||||
elementDef(0, NodeFlags.None, null, null, 0, 'div'),
|
||||
]),
|
||||
{}, [], 'root');
|
||||
const rootNodes = rootRenderNodes(view);
|
||||
expect(rootNodes).toEqual([rootNode]);
|
||||
});
|
||||
|
||||
it('should select root elements based on a node', () => {
|
||||
const view = createRootView(
|
||||
compViewDef([
|
||||
elementDef(0, NodeFlags.None, null, null, 0, 'div'),
|
||||
]),
|
||||
{}, [], rootNode);
|
||||
const rootNodes = rootRenderNodes(view);
|
||||
expect(rootNodes).toEqual([rootNode]);
|
||||
});
|
||||
|
||||
it('should set attributes on the root node', () => {
|
||||
createRootView(
|
||||
compViewDef([
|
||||
elementDef(0, NodeFlags.None, null, null, 0, 'div', [['a', 'b']]),
|
||||
]),
|
||||
{}, [], rootNode);
|
||||
expect(rootNode.getAttribute('a')).toBe('b');
|
||||
});
|
||||
|
||||
it('should clear the content of the root node', () => {
|
||||
rootNode.appendChild(document.createElement('div'));
|
||||
createRootView(
|
||||
compViewDef([
|
||||
elementDef(0, NodeFlags.None, null, null, 0, 'div', [['a', 'b']]),
|
||||
]),
|
||||
{}, [], rootNode);
|
||||
expect(rootNode.childNodes.length).toBe(0);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe('data binding', () => {
|
||||
it('should dirty check component views', () => {
|
||||
let value: any;
|
||||
class AComp {
|
||||
a: any;
|
||||
}
|
||||
|
||||
const update =
|
||||
jasmine.createSpy('updater').and.callFake((check: NodeCheckFn, view: ViewData) => {
|
||||
check(view, 0, ArgumentType.Inline, value);
|
||||
});
|
||||
|
||||
const {view, rootNodes} = createAndGetRootNodes(compViewDef([
|
||||
elementDef(
|
||||
0, NodeFlags.None, null, null, 1, 'div', null, null, null, null,
|
||||
() => compViewDef(
|
||||
[
|
||||
elementDef(
|
||||
0, NodeFlags.None, null, null, 0, 'span', null,
|
||||
[[BindingFlags.TypeElementAttribute, 'a', SecurityContext.NONE]]),
|
||||
],
|
||||
null, update)),
|
||||
directiveDef(1, NodeFlags.Component, null, 0, AComp, []),
|
||||
]));
|
||||
const compView = asElementData(view, 0).componentView;
|
||||
|
||||
value = 'v1';
|
||||
Services.checkAndUpdateView(view);
|
||||
|
||||
expect(update.calls.mostRecent().args[1]).toBe(compView);
|
||||
|
||||
update.calls.reset();
|
||||
Services.checkNoChangesView(view);
|
||||
|
||||
expect(update.calls.mostRecent().args[1]).toBe(compView);
|
||||
|
||||
value = 'v2';
|
||||
expect(() => Services.checkNoChangesView(view))
|
||||
.toThrowError(
|
||||
`ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'a: v1'. Current value: 'a: v2'.`);
|
||||
});
|
||||
|
||||
// fixes https://github.com/angular/angular/issues/21788
|
||||
it('report the binding name when an expression changes after it has been checked', () => {
|
||||
let value: any;
|
||||
class AComp {}
|
||||
|
||||
const update =
|
||||
jasmine.createSpy('updater').and.callFake((check: NodeCheckFn, view: ViewData) => {
|
||||
check(view, 0, ArgumentType.Inline, 'const', 'const', value);
|
||||
});
|
||||
|
||||
const {view, rootNodes} = createAndGetRootNodes(
|
||||
compViewDef([
|
||||
elementDef(0, NodeFlags.None, null, null, 1, 'div', null, null, null, null, () => compViewDef([
|
||||
elementDef(0, NodeFlags.None, null, null, 0, 'span', null, [
|
||||
[BindingFlags.TypeElementAttribute, 'p1', SecurityContext.NONE],
|
||||
[BindingFlags.TypeElementAttribute, 'p2', SecurityContext.NONE],
|
||||
[BindingFlags.TypeElementAttribute, 'p3', SecurityContext.NONE],
|
||||
]),
|
||||
], null, update)
|
||||
),
|
||||
directiveDef(1, NodeFlags.Component, null, 0, AComp, []),
|
||||
]));
|
||||
|
||||
value = 'v1';
|
||||
Services.checkAndUpdateView(view);
|
||||
value = 'v2';
|
||||
expect(() => Services.checkNoChangesView(view))
|
||||
.toThrowError(
|
||||
`ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'p3: v1'. Current value: 'p3: v2'.`);
|
||||
});
|
||||
|
||||
it('should support detaching and attaching component views for dirty checking', () => {
|
||||
class AComp {
|
||||
a: any;
|
||||
}
|
||||
|
||||
const update = jasmine.createSpy('updater');
|
||||
|
||||
const {view, rootNodes} = createAndGetRootNodes(compViewDef([
|
||||
elementDef(
|
||||
0, NodeFlags.None, null, null, 1, 'div', null, null, null, null,
|
||||
() => compViewDef(
|
||||
[
|
||||
elementDef(0, NodeFlags.None, null, null, 0, 'span'),
|
||||
],
|
||||
update)),
|
||||
directiveDef(1, NodeFlags.Component, null, 0, AComp, [], null, null),
|
||||
]));
|
||||
|
||||
const compView = asElementData(view, 0).componentView;
|
||||
|
||||
Services.checkAndUpdateView(view);
|
||||
update.calls.reset();
|
||||
|
||||
compView.state &= ~ViewState.ChecksEnabled;
|
||||
Services.checkAndUpdateView(view);
|
||||
expect(update).not.toHaveBeenCalled();
|
||||
|
||||
compView.state |= ViewState.ChecksEnabled;
|
||||
Services.checkAndUpdateView(view);
|
||||
expect(update).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
if (isBrowser()) {
|
||||
it('should support OnPush components', () => {
|
||||
let compInputValue: any;
|
||||
class AComp {
|
||||
a: any;
|
||||
}
|
||||
|
||||
const update = jasmine.createSpy('updater');
|
||||
|
||||
const addListenerSpy = spyOn(HTMLElement.prototype, addEventListener).and.callThrough();
|
||||
|
||||
const {view} = createAndGetRootNodes(compViewDef(
|
||||
[
|
||||
elementDef(
|
||||
0, NodeFlags.None, null, null, 1, 'div', null, null, null, null,
|
||||
() => {
|
||||
return compViewDef(
|
||||
[
|
||||
elementDef(
|
||||
0, NodeFlags.None, null, null, 0, 'span', null, null,
|
||||
[[null!, 'click']]),
|
||||
],
|
||||
update, null, ViewFlags.OnPush);
|
||||
}),
|
||||
directiveDef(1, NodeFlags.Component, null, 0, AComp, [], {a: [0, 'a']}),
|
||||
],
|
||||
(check, view) => {
|
||||
check(view, 1, ArgumentType.Inline, compInputValue);
|
||||
}));
|
||||
|
||||
Services.checkAndUpdateView(view);
|
||||
|
||||
// auto detach
|
||||
update.calls.reset();
|
||||
Services.checkAndUpdateView(view);
|
||||
expect(update).not.toHaveBeenCalled();
|
||||
|
||||
// auto attach on input changes
|
||||
update.calls.reset();
|
||||
compInputValue = 'v1';
|
||||
Services.checkAndUpdateView(view);
|
||||
expect(update).toHaveBeenCalled();
|
||||
|
||||
// auto detach
|
||||
update.calls.reset();
|
||||
Services.checkAndUpdateView(view);
|
||||
expect(update).not.toHaveBeenCalled();
|
||||
|
||||
// auto attach on events
|
||||
callMostRecentEventListenerHandler(addListenerSpy, 'SomeEvent');
|
||||
update.calls.reset();
|
||||
Services.checkAndUpdateView(view);
|
||||
expect(update).toHaveBeenCalled();
|
||||
|
||||
// auto detach
|
||||
update.calls.reset();
|
||||
Services.checkAndUpdateView(view);
|
||||
expect(update).not.toHaveBeenCalled();
|
||||
});
|
||||
}
|
||||
|
||||
it('should not stop dirty checking views that threw errors in change detection', () => {
|
||||
class AComp {
|
||||
a: any;
|
||||
}
|
||||
|
||||
const update = jasmine.createSpy('updater');
|
||||
|
||||
const {view, rootNodes} = createAndGetRootNodes(compViewDef([
|
||||
elementDef(
|
||||
0, NodeFlags.None, null, null, 1, 'div', null, null, null, null,
|
||||
() => compViewDef(
|
||||
[elementDef(
|
||||
0, NodeFlags.None, null, null, 0, 'span', null,
|
||||
[[BindingFlags.TypeElementAttribute, 'a', SecurityContext.NONE]])],
|
||||
null, update)),
|
||||
directiveDef(
|
||||
1,
|
||||
NodeFlags.Component,
|
||||
null,
|
||||
0,
|
||||
AComp,
|
||||
[],
|
||||
null,
|
||||
null,
|
||||
),
|
||||
]));
|
||||
|
||||
update.and.callFake((check: NodeCheckFn, view: ViewData) => {
|
||||
throw new Error('Test');
|
||||
});
|
||||
expect(() => Services.checkAndUpdateView(view)).toThrowError('Test');
|
||||
expect(update).toHaveBeenCalled();
|
||||
|
||||
update.calls.reset();
|
||||
expect(() => Services.checkAndUpdateView(view)).toThrowError('Test');
|
||||
expect(update).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('destroy', () => {
|
||||
it('should destroy component views', () => {
|
||||
const log: string[] = [];
|
||||
|
||||
class AComp {}
|
||||
|
||||
class ChildProvider {
|
||||
ngOnDestroy() {
|
||||
log.push('ngOnDestroy');
|
||||
}
|
||||
}
|
||||
|
||||
const {view, rootNodes} = createAndGetRootNodes(compViewDef([
|
||||
elementDef(
|
||||
0, NodeFlags.None, null, null, 1, 'div', null, null, null, null,
|
||||
() => compViewDef([
|
||||
elementDef(0, NodeFlags.None, null, null, 1, 'span'),
|
||||
directiveDef(1, NodeFlags.OnDestroy, null, 0, ChildProvider, [])
|
||||
])),
|
||||
directiveDef(
|
||||
1,
|
||||
NodeFlags.Component,
|
||||
null,
|
||||
0,
|
||||
AComp,
|
||||
[],
|
||||
null,
|
||||
null,
|
||||
),
|
||||
]));
|
||||
|
||||
Services.destroyView(view);
|
||||
|
||||
expect(log).toEqual(['ngOnDestroy']);
|
||||
});
|
||||
|
||||
it('should throw on dirty checking destroyed views', () => {
|
||||
const {view, rootNodes} = createAndGetRootNodes(
|
||||
compViewDef([elementDef(0, NodeFlags.None, null, null, 0, 'div')]));
|
||||
|
||||
Services.destroyView(view);
|
||||
|
||||
expect(() => Services.checkAndUpdateView(view))
|
||||
.toThrowError('ViewDestroyedError: Attempt to use a destroyed view: detectChanges');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -1,299 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 {ɵgetDOM as getDOM} from '@angular/common';
|
||||
import {ErrorHandler, getDebugNode, SecurityContext} from '@angular/core';
|
||||
import {getDebugContext} from '@angular/core/src/errors';
|
||||
import {asElementData, BindingFlags, elementDef, NodeFlags, Services, ViewData, ViewDefinition} from '@angular/core/src/view/index';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
|
||||
import {ARG_TYPE_VALUES, callMostRecentEventListenerHandler, checkNodeInlineOrDynamic, compViewDef, createAndGetRootNodes, isBrowser, recordNodeToRemove} from './helper';
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* We map addEventListener to the Zones internal name. This is because we want to be fast
|
||||
* and bypass the zone bookkeeping. We know that we can do the bookkeeping faster.
|
||||
*/
|
||||
const addEventListener = 'addEventListener';
|
||||
const removeEventListener = 'removeEventListener';
|
||||
|
||||
{
|
||||
describe(`View Elements`, () => {
|
||||
describe('create', () => {
|
||||
it('should create elements without parents', () => {
|
||||
const rootNodes = createAndGetRootNodes(compViewDef([
|
||||
elementDef(0, NodeFlags.None, null, null, 0, 'span')
|
||||
])).rootNodes;
|
||||
expect(rootNodes.length).toBe(1);
|
||||
expect(rootNodes[0].nodeName.toLowerCase()).toBe('span');
|
||||
});
|
||||
|
||||
it('should create views with multiple root elements', () => {
|
||||
const rootNodes = createAndGetRootNodes(compViewDef([
|
||||
elementDef(0, NodeFlags.None, null, null, 0, 'span'),
|
||||
elementDef(1, NodeFlags.None, null, null, 0, 'span'),
|
||||
])).rootNodes;
|
||||
expect(rootNodes.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should create elements with parents', () => {
|
||||
const rootNodes = createAndGetRootNodes(compViewDef([
|
||||
elementDef(0, NodeFlags.None, null, null, 1, 'div'),
|
||||
elementDef(1, NodeFlags.None, null, null, 0, 'span'),
|
||||
])).rootNodes;
|
||||
expect(rootNodes.length).toBe(1);
|
||||
const spanEl = rootNodes[0].childNodes[0];
|
||||
expect(spanEl.nodeName.toLowerCase()).toBe('span');
|
||||
});
|
||||
|
||||
it('should set fixed attributes', () => {
|
||||
const rootNodes = createAndGetRootNodes(compViewDef([
|
||||
elementDef(0, NodeFlags.None, null, null, 0, 'div', [['title', 'a']]),
|
||||
])).rootNodes;
|
||||
expect(rootNodes.length).toBe(1);
|
||||
expect(rootNodes[0].getAttribute('title')).toBe('a');
|
||||
});
|
||||
|
||||
it('should add debug information to the renderer', () => {
|
||||
const someContext = {};
|
||||
const {view, rootNodes} = createAndGetRootNodes(
|
||||
compViewDef([elementDef(0, NodeFlags.None, null, null, 0, 'div')]), someContext);
|
||||
expect(getDebugNode(rootNodes[0])!.nativeNode).toBe(asElementData(view, 0).renderElement);
|
||||
});
|
||||
});
|
||||
|
||||
describe('change properties', () => {
|
||||
ARG_TYPE_VALUES.forEach((inlineDynamic) => {
|
||||
it(`should update via strategy ${inlineDynamic}`, () => {
|
||||
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
|
||||
[
|
||||
elementDef(
|
||||
0, NodeFlags.None, null, null, 0, 'input', null,
|
||||
[
|
||||
[BindingFlags.TypeProperty, 'title', SecurityContext.NONE],
|
||||
[BindingFlags.TypeProperty, 'value', SecurityContext.NONE],
|
||||
]),
|
||||
],
|
||||
null, (check, view) => {
|
||||
checkNodeInlineOrDynamic(check, view, 0, inlineDynamic, ['v1', 'v2']);
|
||||
}));
|
||||
|
||||
Services.checkAndUpdateView(view);
|
||||
|
||||
const el = rootNodes[0];
|
||||
expect(el.title).toBe('v1');
|
||||
expect(el.value).toBe('v2');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('change attributes', () => {
|
||||
ARG_TYPE_VALUES.forEach((inlineDynamic) => {
|
||||
it(`should update via strategy ${inlineDynamic}`, () => {
|
||||
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
|
||||
[
|
||||
elementDef(
|
||||
0, NodeFlags.None, null, null, 0, 'div', null,
|
||||
[
|
||||
[BindingFlags.TypeElementAttribute, 'a1', SecurityContext.NONE],
|
||||
[BindingFlags.TypeElementAttribute, 'a2', SecurityContext.NONE],
|
||||
]),
|
||||
],
|
||||
null, (check, view) => {
|
||||
checkNodeInlineOrDynamic(check, view, 0, inlineDynamic, ['v1', 'v2']);
|
||||
}));
|
||||
|
||||
Services.checkAndUpdateView(view);
|
||||
|
||||
const el = rootNodes[0];
|
||||
expect(el.getAttribute('a1')).toBe('v1');
|
||||
expect(el.getAttribute('a2')).toBe('v2');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('change classes', () => {
|
||||
ARG_TYPE_VALUES.forEach((inlineDynamic) => {
|
||||
it(`should update via strategy ${inlineDynamic}`, () => {
|
||||
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
|
||||
[
|
||||
elementDef(
|
||||
0, NodeFlags.None, null, null, 0, 'div', null,
|
||||
[
|
||||
[BindingFlags.TypeElementClass, 'c1', null],
|
||||
[BindingFlags.TypeElementClass, 'c2', null],
|
||||
]),
|
||||
],
|
||||
(check, view) => {
|
||||
checkNodeInlineOrDynamic(check, view, 0, inlineDynamic, [true, true]);
|
||||
}));
|
||||
|
||||
Services.checkAndUpdateView(view);
|
||||
|
||||
const el = rootNodes[0];
|
||||
expect(el.classList.contains('c1')).toBeTruthy();
|
||||
expect(el.classList.contains('c2')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('change styles', () => {
|
||||
ARG_TYPE_VALUES.forEach((inlineDynamic) => {
|
||||
it(`should update via strategy ${inlineDynamic}`, () => {
|
||||
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
|
||||
[
|
||||
elementDef(
|
||||
0, NodeFlags.None, null, null, 0, 'div', null,
|
||||
[
|
||||
[BindingFlags.TypeElementStyle, 'width', 'px'],
|
||||
[BindingFlags.TypeElementStyle, 'color', null],
|
||||
]),
|
||||
],
|
||||
null, (check, view) => {
|
||||
checkNodeInlineOrDynamic(check, view, 0, inlineDynamic, [10, 'red']);
|
||||
}));
|
||||
|
||||
Services.checkAndUpdateView(view);
|
||||
|
||||
const el = rootNodes[0];
|
||||
expect(el.style['width']).toBe('10px');
|
||||
expect(el.style['color']).toBe('red');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
if (isBrowser()) {
|
||||
describe('listen to DOM events', () => {
|
||||
function createAndAttachAndGetRootNodes(viewDef: ViewDefinition):
|
||||
{rootNodes: any[], view: ViewData} {
|
||||
const result = createAndGetRootNodes(viewDef);
|
||||
// Note: We need to append the node to the document.body, otherwise `click` events
|
||||
// won't work in IE.
|
||||
result.rootNodes.forEach((node) => {
|
||||
document.body.appendChild(node);
|
||||
recordNodeToRemove(node);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
it('should listen to DOM events', () => {
|
||||
const handleEventSpy = jasmine.createSpy('handleEvent');
|
||||
const removeListenerSpy =
|
||||
spyOn(HTMLElement.prototype, removeEventListener).and.callThrough();
|
||||
const {view, rootNodes} = createAndAttachAndGetRootNodes(compViewDef([elementDef(
|
||||
0, NodeFlags.None, null, null, 0, 'button', null, null, [[null!, 'click']],
|
||||
handleEventSpy)]));
|
||||
|
||||
rootNodes[0].click();
|
||||
|
||||
expect(handleEventSpy).toHaveBeenCalled();
|
||||
let handleEventArgs = handleEventSpy.calls.mostRecent().args;
|
||||
expect(handleEventArgs[0]).toBe(view);
|
||||
expect(handleEventArgs[1]).toBe('click');
|
||||
expect(handleEventArgs[2]).toBeTruthy();
|
||||
|
||||
Services.destroyView(view);
|
||||
|
||||
expect(removeListenerSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should listen to window events', () => {
|
||||
const handleEventSpy = jasmine.createSpy('handleEvent');
|
||||
const addListenerSpy = spyOn(window, addEventListener);
|
||||
const removeListenerSpy = spyOn(window, removeEventListener);
|
||||
const {view, rootNodes} = createAndAttachAndGetRootNodes(compViewDef([elementDef(
|
||||
0, NodeFlags.None, null, null, 0, 'button', null, null, [['window', 'windowClick']],
|
||||
handleEventSpy)]));
|
||||
|
||||
expect(addListenerSpy).toHaveBeenCalled();
|
||||
expect(addListenerSpy.calls.mostRecent().args[0]).toBe('windowClick');
|
||||
callMostRecentEventListenerHandler(addListenerSpy, {name: 'windowClick'});
|
||||
|
||||
expect(handleEventSpy).toHaveBeenCalled();
|
||||
const handleEventArgs = handleEventSpy.calls.mostRecent().args;
|
||||
expect(handleEventArgs[0]).toBe(view);
|
||||
expect(handleEventArgs[1]).toBe('window:windowClick');
|
||||
expect(handleEventArgs[2]).toBeTruthy();
|
||||
|
||||
Services.destroyView(view);
|
||||
|
||||
expect(removeListenerSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should listen to document events', () => {
|
||||
const handleEventSpy = jasmine.createSpy('handleEvent');
|
||||
const addListenerSpy = spyOn(document, addEventListener);
|
||||
const removeListenerSpy = spyOn(document, removeEventListener);
|
||||
const {view, rootNodes} = createAndAttachAndGetRootNodes(compViewDef([elementDef(
|
||||
0, NodeFlags.None, null, null, 0, 'button', null, null,
|
||||
[['document', 'documentClick']], handleEventSpy)]));
|
||||
|
||||
expect(addListenerSpy).toHaveBeenCalled();
|
||||
expect(addListenerSpy.calls.mostRecent().args[0]).toBe('documentClick');
|
||||
callMostRecentEventListenerHandler(addListenerSpy, {name: 'windowClick'});
|
||||
|
||||
expect(handleEventSpy).toHaveBeenCalled();
|
||||
const handleEventArgs = handleEventSpy.calls.mostRecent().args;
|
||||
expect(handleEventArgs[0]).toBe(view);
|
||||
expect(handleEventArgs[1]).toBe('document:documentClick');
|
||||
expect(handleEventArgs[2]).toBeTruthy();
|
||||
|
||||
Services.destroyView(view);
|
||||
|
||||
expect(removeListenerSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should preventDefault only if the handler returns false', () => {
|
||||
let eventHandlerResult: any;
|
||||
let preventDefaultSpy: jasmine.Spy = undefined!;
|
||||
|
||||
const {view, rootNodes} = createAndAttachAndGetRootNodes(compViewDef([elementDef(
|
||||
0, NodeFlags.None, null, null, 0, 'button', null, null, [[null!, 'click']],
|
||||
(view, eventName, event) => {
|
||||
preventDefaultSpy = spyOn(event, 'preventDefault').and.callThrough();
|
||||
return eventHandlerResult;
|
||||
})]));
|
||||
|
||||
eventHandlerResult = undefined;
|
||||
rootNodes[0].click();
|
||||
expect(preventDefaultSpy).not.toHaveBeenCalled();
|
||||
|
||||
eventHandlerResult = true;
|
||||
rootNodes[0].click();
|
||||
expect(preventDefaultSpy).not.toHaveBeenCalled();
|
||||
|
||||
eventHandlerResult = 'someString';
|
||||
rootNodes[0].click();
|
||||
expect(preventDefaultSpy).not.toHaveBeenCalled();
|
||||
|
||||
eventHandlerResult = false;
|
||||
rootNodes[0].click();
|
||||
expect(preventDefaultSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should report debug info on event errors', () => {
|
||||
const handleErrorSpy = spyOn(TestBed.inject(ErrorHandler), 'handleError');
|
||||
const addListenerSpy = spyOn(HTMLElement.prototype, addEventListener).and.callThrough();
|
||||
const {view, rootNodes} = createAndAttachAndGetRootNodes(compViewDef([elementDef(
|
||||
0, NodeFlags.None, null, null, 0, 'button', null, null, [[null!, 'click']], () => {
|
||||
throw new Error('Test');
|
||||
})]));
|
||||
|
||||
callMostRecentEventListenerHandler(addListenerSpy, 'SomeEvent');
|
||||
const err = handleErrorSpy.calls.mostRecent().args[0];
|
||||
expect(err).toBeTruthy();
|
||||
expect(err.message).toBe('Test');
|
||||
const debugCtx = getDebugContext(err);
|
||||
expect(debugCtx.view).toBe(view);
|
||||
expect(debugCtx.nodeIndex).toBe(0);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -1,175 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 {ɵgetDOM as getDOM} from '@angular/common';
|
||||
import {SecurityContext} from '@angular/core';
|
||||
import {anchorDef, ArgumentType, asElementData, attachEmbeddedView, BindingFlags, detachEmbeddedView, directiveDef, elementDef, moveEmbeddedView, NodeCheckFn, NodeFlags, rootRenderNodes, Services, ViewData} from '@angular/core/src/view/index';
|
||||
|
||||
import {compViewDef, compViewDefFactory, createAndGetRootNodes, createEmbeddedView} from './helper';
|
||||
|
||||
{
|
||||
describe(`Embedded Views`, () => {
|
||||
it('should create embedded views with the right context', () => {
|
||||
const parentContext = {};
|
||||
const childContext = {};
|
||||
|
||||
const {view: parentView} = createAndGetRootNodes(
|
||||
compViewDef([
|
||||
elementDef(0, NodeFlags.None, null, null, 1, 'div'),
|
||||
anchorDef(
|
||||
NodeFlags.EmbeddedViews, null, null, 0, null,
|
||||
compViewDefFactory([elementDef(0, NodeFlags.None, null, null, 0, 'span')])),
|
||||
]),
|
||||
parentContext);
|
||||
|
||||
const childView = createEmbeddedView(parentView, parentView.def.nodes[1], childContext);
|
||||
expect(childView.component).toBe(parentContext);
|
||||
expect(childView.context).toBe(childContext);
|
||||
});
|
||||
|
||||
it('should attach and detach embedded views', () => {
|
||||
const {view: parentView, rootNodes} = createAndGetRootNodes(compViewDef([
|
||||
elementDef(0, NodeFlags.None, null, null, 2, 'div'),
|
||||
anchorDef(NodeFlags.EmbeddedViews, null, null, 0, null, compViewDefFactory([
|
||||
elementDef(0, NodeFlags.None, null, null, 0, 'span', [['name', 'child0']])
|
||||
])),
|
||||
anchorDef(NodeFlags.None, null, null, 0, null, compViewDefFactory([
|
||||
elementDef(0, NodeFlags.None, null, null, 0, 'span', [['name', 'child1']])
|
||||
]))
|
||||
]));
|
||||
const viewContainerData = asElementData(parentView, 1);
|
||||
const rf = parentView.root.rendererFactory;
|
||||
|
||||
const childView0 = createEmbeddedView(parentView, parentView.def.nodes[1]);
|
||||
const childView1 = createEmbeddedView(parentView, parentView.def.nodes[2]);
|
||||
|
||||
attachEmbeddedView(parentView, viewContainerData, 0, childView0);
|
||||
attachEmbeddedView(parentView, viewContainerData, 1, childView1);
|
||||
|
||||
// 2 anchors + 2 elements
|
||||
const rootChildren = rootNodes[0].childNodes;
|
||||
expect(rootChildren.length).toBe(4);
|
||||
expect(rootChildren[1].getAttribute('name')).toBe('child0');
|
||||
expect(rootChildren[2].getAttribute('name')).toBe('child1');
|
||||
|
||||
rf.begin!();
|
||||
detachEmbeddedView(viewContainerData, 1);
|
||||
detachEmbeddedView(viewContainerData, 0);
|
||||
rf.end!();
|
||||
|
||||
expect(rootNodes[0].childNodes.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should move embedded views', () => {
|
||||
const {view: parentView, rootNodes} = createAndGetRootNodes(compViewDef([
|
||||
elementDef(0, NodeFlags.None, null, null, 2, 'div'),
|
||||
anchorDef(NodeFlags.EmbeddedViews, null, null, 0, null, compViewDefFactory([
|
||||
elementDef(0, NodeFlags.None, null, null, 0, 'span', [['name', 'child0']])
|
||||
])),
|
||||
anchorDef(NodeFlags.None, null, null, 0, null, compViewDefFactory([
|
||||
elementDef(0, NodeFlags.None, null, null, 0, 'span', [['name', 'child1']])
|
||||
]))
|
||||
]));
|
||||
const viewContainerData = asElementData(parentView, 1);
|
||||
|
||||
const childView0 = createEmbeddedView(parentView, parentView.def.nodes[1]);
|
||||
const childView1 = createEmbeddedView(parentView, parentView.def.nodes[2]);
|
||||
|
||||
attachEmbeddedView(parentView, viewContainerData, 0, childView0);
|
||||
attachEmbeddedView(parentView, viewContainerData, 1, childView1);
|
||||
|
||||
moveEmbeddedView(viewContainerData, 0, 1);
|
||||
|
||||
expect(viewContainerData.viewContainer!._embeddedViews).toEqual([childView1, childView0]);
|
||||
// 2 anchors + 2 elements
|
||||
const rootChildren = rootNodes[0].childNodes;
|
||||
expect(rootChildren.length).toBe(4);
|
||||
expect(rootChildren[1].getAttribute('name')).toBe('child1');
|
||||
expect(rootChildren[2].getAttribute('name')).toBe('child0');
|
||||
});
|
||||
|
||||
it('should include embedded views in root nodes', () => {
|
||||
const {view: parentView} = createAndGetRootNodes(compViewDef([
|
||||
anchorDef(NodeFlags.EmbeddedViews, null, null, 0, null, compViewDefFactory([
|
||||
elementDef(0, NodeFlags.None, null, null, 0, 'span', [['name', 'child0']])
|
||||
])),
|
||||
elementDef(1, NodeFlags.None, null, null, 0, 'span', [['name', 'after']])
|
||||
]));
|
||||
|
||||
const childView0 = createEmbeddedView(parentView, parentView.def.nodes[0]);
|
||||
attachEmbeddedView(parentView, asElementData(parentView, 0), 0, childView0);
|
||||
|
||||
const rootNodes = rootRenderNodes(parentView);
|
||||
expect(rootNodes.length).toBe(3);
|
||||
expect(rootNodes[1].getAttribute('name')).toBe('child0');
|
||||
expect(rootNodes[2].getAttribute('name')).toBe('after');
|
||||
});
|
||||
|
||||
it('should dirty check embedded views', () => {
|
||||
let childValue = 'v1';
|
||||
const update =
|
||||
jasmine.createSpy('updater').and.callFake((check: NodeCheckFn, view: ViewData) => {
|
||||
check(view, 0, ArgumentType.Inline, childValue);
|
||||
});
|
||||
|
||||
const {view: parentView, rootNodes} = createAndGetRootNodes(compViewDef([
|
||||
elementDef(0, NodeFlags.None, null, null, 1, 'div'),
|
||||
anchorDef(
|
||||
NodeFlags.EmbeddedViews, null, null, 0, null,
|
||||
compViewDefFactory(
|
||||
[elementDef(
|
||||
0, NodeFlags.None, null, null, 0, 'span', null,
|
||||
[[BindingFlags.TypeElementAttribute, 'name', SecurityContext.NONE]])],
|
||||
update))
|
||||
]));
|
||||
|
||||
const childView0 = createEmbeddedView(parentView, parentView.def.nodes[1]);
|
||||
|
||||
attachEmbeddedView(parentView, asElementData(parentView, 1), 0, childView0);
|
||||
|
||||
Services.checkAndUpdateView(parentView);
|
||||
|
||||
expect(update.calls.mostRecent().args[1]).toBe(childView0);
|
||||
|
||||
update.calls.reset();
|
||||
Services.checkNoChangesView(parentView);
|
||||
|
||||
expect(update.calls.mostRecent().args[1]).toBe(childView0);
|
||||
|
||||
childValue = 'v2';
|
||||
expect(() => Services.checkNoChangesView(parentView))
|
||||
.toThrowError(
|
||||
`ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'name: v1'. Current value: 'name: v2'.`);
|
||||
});
|
||||
|
||||
it('should destroy embedded views', () => {
|
||||
const log: string[] = [];
|
||||
|
||||
class ChildProvider {
|
||||
ngOnDestroy() {
|
||||
log.push('ngOnDestroy');
|
||||
}
|
||||
}
|
||||
|
||||
const {view: parentView} = createAndGetRootNodes(compViewDef([
|
||||
elementDef(0, NodeFlags.None, null, null, 1, 'div'),
|
||||
anchorDef(NodeFlags.EmbeddedViews, null, null, 0, null, compViewDefFactory([
|
||||
elementDef(0, NodeFlags.None, null, null, 1, 'span'),
|
||||
directiveDef(1, NodeFlags.OnDestroy, null, 0, ChildProvider, [])
|
||||
]))
|
||||
]));
|
||||
|
||||
const childView0 = createEmbeddedView(parentView, parentView.def.nodes[1]);
|
||||
|
||||
attachEmbeddedView(parentView, asElementData(parentView, 1), 0, childView0);
|
||||
Services.destroyView(parentView);
|
||||
|
||||
expect(log).toEqual(['ngOnDestroy']);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -1,103 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 {ɵgetDOM as getDOM} from '@angular/common';
|
||||
import {Injector, NgModuleRef} from '@angular/core';
|
||||
import {ArgumentType, initServicesIfNeeded, NodeCheckFn, NodeDef, rootRenderNodes, Services, ViewData, viewDef, ViewDefinition, ViewDefinitionFactory, ViewFlags, ViewUpdateFn} from '@angular/core/src/view/index';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
|
||||
export function isBrowser() {
|
||||
return getDOM().supportsDOMEvents;
|
||||
}
|
||||
|
||||
export const ARG_TYPE_VALUES = [ArgumentType.Inline, ArgumentType.Dynamic];
|
||||
|
||||
export function checkNodeInlineOrDynamic(
|
||||
check: NodeCheckFn, view: ViewData, nodeIndex: number, argType: ArgumentType,
|
||||
values: any[]): any {
|
||||
switch (argType) {
|
||||
case ArgumentType.Inline:
|
||||
return (<any>check)(view, nodeIndex, argType, ...values);
|
||||
case ArgumentType.Dynamic:
|
||||
return check(view, nodeIndex, argType, values);
|
||||
}
|
||||
}
|
||||
|
||||
export function createRootView(
|
||||
def: ViewDefinition, context?: any, projectableNodes?: any[][],
|
||||
rootSelectorOrNode?: any): ViewData {
|
||||
initServicesIfNeeded();
|
||||
return Services.createRootView(
|
||||
TestBed.inject(Injector), projectableNodes || [], rootSelectorOrNode, def,
|
||||
TestBed.inject(NgModuleRef), context);
|
||||
}
|
||||
|
||||
export function createEmbeddedView(parent: ViewData, anchorDef: NodeDef, context?: any): ViewData {
|
||||
return Services.createEmbeddedView(parent, anchorDef, anchorDef.element!.template !, context);
|
||||
}
|
||||
|
||||
export function compViewDef(
|
||||
nodes: NodeDef[], updateDirectives?: null|ViewUpdateFn, updateRenderer?: null|ViewUpdateFn,
|
||||
viewFlags: ViewFlags = ViewFlags.None): ViewDefinition {
|
||||
const def = viewDef(viewFlags, nodes, updateDirectives, updateRenderer);
|
||||
|
||||
def.nodes.forEach((node, index) => {
|
||||
if (node.nodeIndex !== index) {
|
||||
throw new Error('nodeIndex should be the same as the index of the node');
|
||||
}
|
||||
|
||||
// This check should be removed when we start reordering nodes at runtime
|
||||
if (node.checkIndex > -1 && node.checkIndex !== node.nodeIndex) {
|
||||
throw new Error(`nodeIndex and checkIndex should be the same, got ${node.nodeIndex} !== ${
|
||||
node.checkIndex}`);
|
||||
}
|
||||
});
|
||||
|
||||
return def;
|
||||
}
|
||||
|
||||
export function compViewDefFactory(
|
||||
nodes: NodeDef[], updateDirectives?: null|ViewUpdateFn, updateRenderer?: null|ViewUpdateFn,
|
||||
viewFlags: ViewFlags = ViewFlags.None): ViewDefinitionFactory {
|
||||
return () => compViewDef(nodes, updateDirectives, updateRenderer, viewFlags);
|
||||
}
|
||||
|
||||
export function createAndGetRootNodes(
|
||||
viewDef: ViewDefinition, ctx?: any): {rootNodes: any[], view: ViewData} {
|
||||
const view = createRootView(viewDef, ctx);
|
||||
const rootNodes = rootRenderNodes(view);
|
||||
return {rootNodes, view};
|
||||
}
|
||||
|
||||
let removeNodes: Node[];
|
||||
|
||||
beforeEach(() => {
|
||||
removeNodes = [];
|
||||
});
|
||||
afterEach(() => {
|
||||
removeNodes.forEach((node) => getDOM().remove(node));
|
||||
});
|
||||
|
||||
export function recordNodeToRemove(node: Node) {
|
||||
removeNodes.push(node);
|
||||
}
|
||||
|
||||
export function callMostRecentEventListenerHandler(spy: any, params: any) {
|
||||
const mostRecent = spy.calls.mostRecent();
|
||||
if (!mostRecent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const obj = mostRecent.object;
|
||||
const args = mostRecent.args;
|
||||
|
||||
const eventName = args[0];
|
||||
const handler = args[1];
|
||||
|
||||
handler && handler.apply(obj, [{type: eventName}]);
|
||||
}
|
||||
|
|
@ -1,137 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 {ɵgetDOM as getDOM} from '@angular/common';
|
||||
import {TemplateRef, ViewContainerRef} from '@angular/core';
|
||||
import {anchorDef, asElementData, asTextData, attachEmbeddedView, detachEmbeddedView, directiveDef, elementDef, ngContentDef, NodeDef, NodeFlags, rootRenderNodes, textDef, ViewData, ViewDefinition} from '@angular/core/src/view/index';
|
||||
|
||||
import {compViewDef, compViewDefFactory, createEmbeddedView, createRootView, isBrowser} from './helper';
|
||||
|
||||
{
|
||||
describe(`View NgContent`, () => {
|
||||
function hostElDef(
|
||||
checkIndex: number, contentNodes: NodeDef[], viewNodes: NodeDef[]): NodeDef[] {
|
||||
class AComp {}
|
||||
|
||||
const aCompViewDef = compViewDef(viewNodes);
|
||||
|
||||
return [
|
||||
elementDef(
|
||||
checkIndex, NodeFlags.None, null, null, 1 + contentNodes.length, 'acomp', null, null,
|
||||
null, null, () => aCompViewDef),
|
||||
directiveDef(checkIndex + 1, NodeFlags.Component, null, 0, AComp, []), ...contentNodes
|
||||
];
|
||||
}
|
||||
|
||||
function createAndGetRootNodes(
|
||||
viewDef: ViewDefinition, ctx?: any): {rootNodes: any[], view: ViewData} {
|
||||
const view = createRootView(viewDef, ctx || {});
|
||||
const rootNodes = rootRenderNodes(view);
|
||||
return {rootNodes, view};
|
||||
}
|
||||
|
||||
it('should create ng-content nodes without parents', () => {
|
||||
const {view, rootNodes} = createAndGetRootNodes(
|
||||
compViewDef(hostElDef(0, [textDef(2, 0, ['a'])], [ngContentDef(null, 0)])));
|
||||
|
||||
expect(rootNodes[0].firstChild).toBe(asTextData(view, 2).renderText);
|
||||
});
|
||||
|
||||
it('should create views with multiple root ng-content nodes', () => {
|
||||
const {view, rootNodes} = createAndGetRootNodes(compViewDef(hostElDef(
|
||||
0, [textDef(2, 0, ['a']), textDef(3, 1, ['b'])],
|
||||
[ngContentDef(null, 0), ngContentDef(null, 1)])));
|
||||
|
||||
expect(rootNodes[0].childNodes[0]).toBe(asTextData(view, 2).renderText);
|
||||
expect(rootNodes[0].childNodes[1]).toBe(asTextData(view, 3).renderText);
|
||||
});
|
||||
|
||||
it('should create ng-content nodes with parents', () => {
|
||||
const {view, rootNodes} = createAndGetRootNodes(compViewDef(hostElDef(
|
||||
0, [textDef(2, 0, ['a'])],
|
||||
[elementDef(0, NodeFlags.None, null, null, 1, 'div'), ngContentDef(null, 0)])));
|
||||
|
||||
expect(rootNodes[0].firstChild.firstChild).toBe(asTextData(view, 2).renderText);
|
||||
});
|
||||
|
||||
it('should reproject ng-content nodes', () => {
|
||||
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
|
||||
hostElDef(0, [textDef(2, 0, ['a'])], hostElDef(0, [ngContentDef(0, 0)], [
|
||||
elementDef(0, NodeFlags.None, null, null, 1, 'span'), ngContentDef(null, 0)
|
||||
]))));
|
||||
expect(rootNodes[0].firstChild.firstChild.firstChild).toBe(asTextData(view, 2).renderText);
|
||||
});
|
||||
|
||||
it('should project already attached embedded views', () => {
|
||||
class CreateViewService {
|
||||
constructor(templateRef: TemplateRef<any>, viewContainerRef: ViewContainerRef) {
|
||||
viewContainerRef.createEmbeddedView(templateRef);
|
||||
}
|
||||
}
|
||||
|
||||
const {view, rootNodes} = createAndGetRootNodes(compViewDef(hostElDef(
|
||||
0,
|
||||
[
|
||||
anchorDef(NodeFlags.EmbeddedViews, null, 0, 1, null, compViewDefFactory([textDef(
|
||||
0, null, ['a'])])),
|
||||
directiveDef(
|
||||
3, NodeFlags.None, null, 0, CreateViewService, [TemplateRef, ViewContainerRef]),
|
||||
],
|
||||
[
|
||||
elementDef(0, NodeFlags.None, null, null, 1, 'div'),
|
||||
ngContentDef(null, 0),
|
||||
])));
|
||||
|
||||
const anchor = asElementData(view, 2);
|
||||
const child = rootNodes[0].firstChild;
|
||||
expect(child.childNodes[0]).toBe(anchor.renderElement);
|
||||
const embeddedView = anchor.viewContainer!._embeddedViews[0];
|
||||
expect(child.childNodes[1]).toBe(asTextData(embeddedView, 0).renderText);
|
||||
});
|
||||
|
||||
it('should include projected nodes when attaching / detaching embedded views', () => {
|
||||
const {view, rootNodes} =
|
||||
createAndGetRootNodes(compViewDef(hostElDef(0, [textDef(2, 0, ['a'])], [
|
||||
elementDef(0, NodeFlags.None, null, null, 1, 'div'),
|
||||
anchorDef(NodeFlags.EmbeddedViews, null, 0, 0, null, compViewDefFactory([
|
||||
ngContentDef(null, 0),
|
||||
// The anchor would be added by the compiler after the ngContent
|
||||
anchorDef(NodeFlags.None, null, null, 0),
|
||||
])),
|
||||
])));
|
||||
|
||||
const componentView = asElementData(view, 0).componentView;
|
||||
const rf = componentView.root.rendererFactory;
|
||||
const view0 = createEmbeddedView(componentView, componentView.def.nodes[1]);
|
||||
|
||||
attachEmbeddedView(view, asElementData(componentView, 1), 0, view0);
|
||||
let child = rootNodes[0].firstChild;
|
||||
expect(child.childNodes.length).toBe(3);
|
||||
expect(child.childNodes[1]).toBe(asTextData(view, 2).renderText);
|
||||
|
||||
rf.begin!();
|
||||
detachEmbeddedView(asElementData(componentView, 1), 0);
|
||||
rf.end!();
|
||||
child = rootNodes[0].firstChild;
|
||||
expect(child.childNodes.length).toBe(1);
|
||||
});
|
||||
|
||||
if (isBrowser()) {
|
||||
it('should use root projectable nodes', () => {
|
||||
const projectableNodes = [[document.createTextNode('a')], [document.createTextNode('b')]];
|
||||
const view = createRootView(
|
||||
compViewDef(hostElDef(0, [], [ngContentDef(null, 0), ngContentDef(null, 1)])), {},
|
||||
projectableNodes);
|
||||
const rootNodes = rootRenderNodes(view);
|
||||
|
||||
expect(rootNodes[0].childNodes[0]).toBe(projectableNodes[0][0]);
|
||||
expect(rootNodes[0].childNodes[1]).toBe(projectableNodes[1][0]);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -1,312 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 {NgModuleRef, ɵINJECTOR_SCOPE as INJECTOR_SCOPE} from '@angular/core';
|
||||
import {inject, InjectFlags} from '@angular/core/src/di';
|
||||
import {Injector} from '@angular/core/src/di/injector';
|
||||
import {INJECTOR} from '@angular/core/src/di/injector_token';
|
||||
import {ɵɵdefineInjectable} from '@angular/core/src/di/interface/defs';
|
||||
import {NgModuleDefinition, NgModuleProviderDef, NodeFlags} from '@angular/core/src/view';
|
||||
import {moduleDef} from '@angular/core/src/view/ng_module';
|
||||
import {createNgModuleRef} from '@angular/core/src/view/refs';
|
||||
import {tokenKey} from '@angular/core/src/view/util';
|
||||
|
||||
class Foo {}
|
||||
|
||||
class MyModule {}
|
||||
|
||||
class MyChildModule {}
|
||||
|
||||
class NotMyModule {}
|
||||
|
||||
class Bar {
|
||||
static ɵprov = ɵɵdefineInjectable({
|
||||
token: Bar,
|
||||
factory: () => new Bar(),
|
||||
providedIn: MyModule,
|
||||
});
|
||||
}
|
||||
|
||||
class Baz {
|
||||
static ɵprov = ɵɵdefineInjectable({
|
||||
token: Baz,
|
||||
factory: () => new Baz(),
|
||||
providedIn: NotMyModule,
|
||||
});
|
||||
}
|
||||
|
||||
class HasNormalDep {
|
||||
constructor(public foo: Foo) {}
|
||||
|
||||
static ɵprov = ɵɵdefineInjectable({
|
||||
token: HasNormalDep,
|
||||
factory: () => new HasNormalDep(inject(Foo)),
|
||||
providedIn: MyModule,
|
||||
});
|
||||
}
|
||||
|
||||
class HasDefinedDep {
|
||||
constructor(public bar: Bar) {}
|
||||
|
||||
static ɵprov = ɵɵdefineInjectable({
|
||||
token: HasDefinedDep,
|
||||
factory: () => new HasDefinedDep(inject(Bar)),
|
||||
providedIn: MyModule,
|
||||
});
|
||||
}
|
||||
|
||||
class HasOptionalDep {
|
||||
constructor(public baz: Baz|null) {}
|
||||
|
||||
static ɵprov = ɵɵdefineInjectable({
|
||||
token: HasOptionalDep,
|
||||
factory: () => new HasOptionalDep(inject(Baz, InjectFlags.Optional)),
|
||||
providedIn: MyModule,
|
||||
});
|
||||
}
|
||||
|
||||
class ChildDep {
|
||||
static ɵprov = ɵɵdefineInjectable({
|
||||
token: ChildDep,
|
||||
factory: () => new ChildDep(),
|
||||
providedIn: MyChildModule,
|
||||
});
|
||||
}
|
||||
|
||||
class FromChildWithOptionalDep {
|
||||
constructor(public baz: Baz|null) {}
|
||||
static ɵprov = ɵɵdefineInjectable({
|
||||
token: FromChildWithOptionalDep,
|
||||
factory: () => new FromChildWithOptionalDep(inject(Baz, InjectFlags.Default)),
|
||||
providedIn: MyChildModule,
|
||||
});
|
||||
}
|
||||
|
||||
class FromChildWithSkipSelfDep {
|
||||
constructor(
|
||||
public skipSelfChildDep: ChildDep|null, public selfChildDep: ChildDep|null,
|
||||
public optionalSelfBar: Bar|null) {}
|
||||
static ɵprov = ɵɵdefineInjectable({
|
||||
token: FromChildWithSkipSelfDep,
|
||||
factory: () => new FromChildWithSkipSelfDep(
|
||||
inject(ChildDep, InjectFlags.SkipSelf|InjectFlags.Optional),
|
||||
inject(ChildDep, InjectFlags.Self),
|
||||
inject(Bar, InjectFlags.Self|InjectFlags.Optional),
|
||||
),
|
||||
providedIn: MyChildModule,
|
||||
});
|
||||
}
|
||||
|
||||
class UsesInject {
|
||||
constructor() {
|
||||
inject(INJECTOR);
|
||||
}
|
||||
}
|
||||
|
||||
function makeProviders(classes: any[], modules: any[]): NgModuleDefinition {
|
||||
const providers = classes.map((token, index) => ({
|
||||
index,
|
||||
deps: [],
|
||||
flags: NodeFlags.TypeClassProvider | NodeFlags.LazyProvider,
|
||||
token,
|
||||
value: token,
|
||||
}));
|
||||
return makeModule(modules, providers);
|
||||
}
|
||||
|
||||
function makeFactoryProviders(
|
||||
factories: {token: any, factory: Function}[], modules: any[]): NgModuleDefinition {
|
||||
const providers = factories.map((factory, index) => ({
|
||||
index,
|
||||
deps: [],
|
||||
flags: NodeFlags.TypeFactoryProvider | NodeFlags.LazyProvider,
|
||||
token: factory.token,
|
||||
value: factory.factory,
|
||||
}));
|
||||
return makeModule(modules, providers);
|
||||
}
|
||||
|
||||
function makeModule(modules: any[], providers: NgModuleProviderDef[]): NgModuleDefinition {
|
||||
const providersByKey: {[key: string]: NgModuleProviderDef} = {};
|
||||
providers.forEach(provider => providersByKey[tokenKey(provider.token)] = provider);
|
||||
return {factory: null, providers, providersByKey, modules, scope: 'root'};
|
||||
}
|
||||
|
||||
describe('NgModuleRef_ injector', () => {
|
||||
let ref: NgModuleRef<any>;
|
||||
let childRef: NgModuleRef<any>;
|
||||
beforeEach(() => {
|
||||
ref = createNgModuleRef(
|
||||
MyModule, Injector.NULL, [], makeProviders([MyModule, Foo, UsesInject], [MyModule]));
|
||||
childRef = createNgModuleRef(
|
||||
MyChildModule, ref.injector, [], makeProviders([MyChildModule], [MyChildModule]));
|
||||
});
|
||||
|
||||
it('injects a provided value', () => {
|
||||
expect(ref.injector.get(Foo) instanceof Foo).toBeTruthy();
|
||||
});
|
||||
|
||||
it('injects an injectable value', () => {
|
||||
expect(ref.injector.get(Bar) instanceof Bar).toBeTruthy();
|
||||
});
|
||||
|
||||
it('caches injectable values', () => {
|
||||
expect(ref.injector.get(Bar)).toBe(ref.injector.get(Bar));
|
||||
});
|
||||
|
||||
it('injects provided deps properly', () => {
|
||||
const instance = ref.injector.get(HasNormalDep);
|
||||
expect(instance instanceof HasNormalDep).toBeTruthy();
|
||||
expect(instance.foo).toBe(ref.injector.get(Foo));
|
||||
});
|
||||
|
||||
it('injects defined deps properly', () => {
|
||||
const instance = ref.injector.get(HasDefinedDep);
|
||||
expect(instance instanceof HasDefinedDep).toBeTruthy();
|
||||
expect(instance.bar).toBe(ref.injector.get(Bar));
|
||||
});
|
||||
|
||||
it('injects optional deps properly', () => {
|
||||
const instance = ref.injector.get(HasOptionalDep);
|
||||
expect(instance instanceof HasOptionalDep).toBeTruthy();
|
||||
expect(instance.baz).toBeNull();
|
||||
});
|
||||
|
||||
it('injects skip-self and self deps across injectors properly', () => {
|
||||
const instance = childRef.injector.get(FromChildWithSkipSelfDep);
|
||||
expect(instance instanceof FromChildWithSkipSelfDep).toBeTruthy();
|
||||
expect(instance.skipSelfChildDep).toBeNull();
|
||||
expect(instance.selfChildDep instanceof ChildDep).toBeTruthy();
|
||||
expect(instance.optionalSelfBar).toBeNull();
|
||||
});
|
||||
|
||||
it('does not inject something not scoped to the module', () => {
|
||||
expect(ref.injector.get(Baz, null)).toBeNull();
|
||||
});
|
||||
|
||||
it('injects with the current injector always set', () => {
|
||||
expect(() => ref.injector.get(UsesInject)).not.toThrow();
|
||||
});
|
||||
|
||||
it('calls ngOnDestroy on services created via factory', () => {
|
||||
class Module {}
|
||||
|
||||
class Service {
|
||||
static destroyed = 0;
|
||||
ngOnDestroy(): void {
|
||||
Service.destroyed++;
|
||||
}
|
||||
}
|
||||
|
||||
const ref = createNgModuleRef(
|
||||
Module, Injector.NULL, [],
|
||||
makeFactoryProviders(
|
||||
[{
|
||||
token: Service,
|
||||
factory: () => new Service(),
|
||||
}],
|
||||
[Module]));
|
||||
|
||||
expect(ref.injector.get(Service)).toBeDefined();
|
||||
expect(Service.destroyed).toBe(0);
|
||||
ref.destroy();
|
||||
expect(Service.destroyed).toBe(1);
|
||||
});
|
||||
|
||||
it('calls ngOnDestroy on scoped providers', () => {
|
||||
class Module {}
|
||||
|
||||
class Service {
|
||||
static destroyed = 0;
|
||||
|
||||
ngOnDestroy(): void {
|
||||
Service.destroyed++;
|
||||
}
|
||||
|
||||
static ɵprov = ɵɵdefineInjectable({
|
||||
token: Service,
|
||||
factory: () => new Service(),
|
||||
providedIn: 'root',
|
||||
});
|
||||
}
|
||||
|
||||
const ref = createNgModuleRef(Module, Injector.NULL, [], makeFactoryProviders([], [Module]));
|
||||
|
||||
expect(ref.injector.get(Service)).toBeDefined();
|
||||
expect(Service.destroyed).toBe(0);
|
||||
ref.destroy();
|
||||
expect(Service.destroyed).toBe(1);
|
||||
});
|
||||
|
||||
it('only calls ngOnDestroy once per instance', () => {
|
||||
class Module {}
|
||||
|
||||
class Service {
|
||||
static destroyed = 0;
|
||||
ngOnDestroy(): void {
|
||||
Service.destroyed++;
|
||||
}
|
||||
}
|
||||
|
||||
class OtherToken {}
|
||||
|
||||
const instance = new Service();
|
||||
const ref = createNgModuleRef(
|
||||
Module, Injector.NULL, [],
|
||||
makeFactoryProviders(
|
||||
[
|
||||
{
|
||||
token: Service,
|
||||
factory: () => instance,
|
||||
},
|
||||
{
|
||||
token: OtherToken,
|
||||
factory: () => instance,
|
||||
}
|
||||
],
|
||||
[Module]));
|
||||
|
||||
expect(ref.injector.get(Service)).toBe(instance);
|
||||
expect(ref.injector.get(OtherToken)).toBe(instance);
|
||||
expect(Service.destroyed).toBe(0);
|
||||
ref.destroy();
|
||||
expect(Service.destroyed).toBe(1);
|
||||
});
|
||||
|
||||
describe('moduleDef', () => {
|
||||
function createProvider(token: any, value: any) {
|
||||
return {
|
||||
index: 0,
|
||||
flags: NodeFlags.TypeValueProvider | NodeFlags.LazyProvider,
|
||||
deps: [],
|
||||
token,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
it('sets scope to `root` when INJECTOR_SCOPE is `root`', () => {
|
||||
const def = moduleDef([createProvider(INJECTOR_SCOPE, 'root')]);
|
||||
expect(def.scope).toBe('root');
|
||||
});
|
||||
|
||||
it('sets scope to `platform` when INJECTOR_SCOPE is `platform`', () => {
|
||||
const def = moduleDef([createProvider(INJECTOR_SCOPE, 'platform')]);
|
||||
expect(def.scope).toBe('platform');
|
||||
});
|
||||
|
||||
it('sets scope to `null` when INJECTOR_SCOPE is absent', () => {
|
||||
const def = moduleDef([]);
|
||||
expect(def.scope).toBe(null);
|
||||
});
|
||||
|
||||
it('sets scope to `null` when INJECTOR_SCOPE is `null`', () => {
|
||||
const def = moduleDef([createProvider(INJECTOR_SCOPE, null)]);
|
||||
expect(def.scope).toBe(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,581 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 {ɵgetDOM as getDOM} from '@angular/common';
|
||||
import {AfterContentChecked, AfterContentInit, AfterViewChecked, AfterViewInit, ChangeDetectorRef, DoCheck, ElementRef, ErrorHandler, EventEmitter, Injector, OnChanges, OnDestroy, OnInit, Renderer2, SimpleChange, TemplateRef, ViewContainerRef,} from '@angular/core';
|
||||
import {getDebugContext} from '@angular/core/src/errors';
|
||||
import {anchorDef, ArgumentType, asElementData, DepFlags, directiveDef, elementDef, NodeFlags, providerDef, Services, textDef} from '@angular/core/src/view/index';
|
||||
import {TestBed, withModule} from '@angular/core/testing';
|
||||
import {ivyEnabled} from '@angular/private/testing';
|
||||
|
||||
import {ARG_TYPE_VALUES, checkNodeInlineOrDynamic, compViewDef, compViewDefFactory, createAndGetRootNodes, createRootView} from './helper';
|
||||
|
||||
{
|
||||
describe(`View Providers`, () => {
|
||||
describe('create', () => {
|
||||
let instance: SomeService;
|
||||
|
||||
class SomeService {
|
||||
constructor(public dep: any) {
|
||||
instance = this;
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
instance = null!;
|
||||
});
|
||||
|
||||
it('should create providers eagerly', () => {
|
||||
createAndGetRootNodes(compViewDef([
|
||||
elementDef(0, NodeFlags.None, null, null, 1, 'span'),
|
||||
directiveDef(1, NodeFlags.None, null, 0, SomeService, [])
|
||||
]));
|
||||
|
||||
expect(instance instanceof SomeService).toBe(true);
|
||||
});
|
||||
|
||||
it('should create providers lazily', () => {
|
||||
let lazy: LazyService = undefined!;
|
||||
class LazyService {
|
||||
constructor() {
|
||||
lazy = this;
|
||||
}
|
||||
}
|
||||
|
||||
createAndGetRootNodes(compViewDef([
|
||||
elementDef(0, NodeFlags.None, null, null, 2, 'span'),
|
||||
providerDef(
|
||||
NodeFlags.TypeClassProvider | NodeFlags.LazyProvider, null, LazyService, LazyService,
|
||||
[]),
|
||||
directiveDef(2, NodeFlags.None, null, 0, SomeService, [Injector])
|
||||
]));
|
||||
|
||||
expect(lazy).toBeUndefined();
|
||||
instance.dep.get(LazyService);
|
||||
expect(lazy instanceof LazyService).toBe(true);
|
||||
});
|
||||
|
||||
it('should create value providers', () => {
|
||||
createAndGetRootNodes(compViewDef([
|
||||
elementDef(0, NodeFlags.None, null, null, 2, 'span'),
|
||||
providerDef(NodeFlags.TypeValueProvider, null, 'someToken', 'someValue', []),
|
||||
directiveDef(2, NodeFlags.None, null, 0, SomeService, ['someToken']),
|
||||
]));
|
||||
|
||||
expect(instance.dep).toBe('someValue');
|
||||
});
|
||||
|
||||
it('should create factory providers', () => {
|
||||
function someFactory() {
|
||||
return 'someValue';
|
||||
}
|
||||
|
||||
createAndGetRootNodes(compViewDef([
|
||||
elementDef(0, NodeFlags.None, null, null, 2, 'span'),
|
||||
providerDef(NodeFlags.TypeFactoryProvider, null, 'someToken', someFactory, []),
|
||||
directiveDef(2, NodeFlags.None, null, 0, SomeService, ['someToken']),
|
||||
]));
|
||||
|
||||
expect(instance.dep).toBe('someValue');
|
||||
});
|
||||
|
||||
it('should create useExisting providers', () => {
|
||||
createAndGetRootNodes(compViewDef([
|
||||
elementDef(0, NodeFlags.None, null, null, 3, 'span'),
|
||||
providerDef(NodeFlags.TypeValueProvider, null, 'someExistingToken', 'someValue', []),
|
||||
providerDef(
|
||||
NodeFlags.TypeUseExistingProvider, null, 'someToken', null, ['someExistingToken']),
|
||||
directiveDef(3, NodeFlags.None, null, 0, SomeService, ['someToken']),
|
||||
]));
|
||||
|
||||
expect(instance.dep).toBe('someValue');
|
||||
});
|
||||
|
||||
it('should add a DebugContext to errors in provider factories', () => {
|
||||
class SomeService {
|
||||
constructor() {
|
||||
throw new Error('Test');
|
||||
}
|
||||
}
|
||||
|
||||
let err: any;
|
||||
try {
|
||||
createRootView(
|
||||
compViewDef([
|
||||
elementDef(
|
||||
0, NodeFlags.None, null, null, 1, 'div', null, null, null, null,
|
||||
() => compViewDef([textDef(0, null, ['a'])])),
|
||||
directiveDef(1, NodeFlags.Component, null, 0, SomeService, [])
|
||||
]),
|
||||
TestBed.inject(Injector), [], getDOM().createElement('div'));
|
||||
} catch (e) {
|
||||
err = e;
|
||||
}
|
||||
expect(err).toBeTruthy();
|
||||
expect(err.message).toBe('Test');
|
||||
const debugCtx = getDebugContext(err);
|
||||
expect(debugCtx.view).toBeTruthy();
|
||||
expect(debugCtx.nodeIndex).toBe(1);
|
||||
});
|
||||
|
||||
describe('deps', () => {
|
||||
class Dep {}
|
||||
|
||||
it('should inject deps from the same element', () => {
|
||||
createAndGetRootNodes(compViewDef([
|
||||
elementDef(0, NodeFlags.None, null, null, 2, 'span'),
|
||||
directiveDef(1, NodeFlags.None, null, 0, Dep, []),
|
||||
directiveDef(2, NodeFlags.None, null, 0, SomeService, [Dep])
|
||||
]));
|
||||
|
||||
expect(instance.dep instanceof Dep).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should inject deps from a parent element', () => {
|
||||
createAndGetRootNodes(compViewDef([
|
||||
elementDef(0, NodeFlags.None, null, null, 3, 'span'),
|
||||
directiveDef(1, NodeFlags.None, null, 0, Dep, []),
|
||||
elementDef(2, NodeFlags.None, null, null, 1, 'span'),
|
||||
directiveDef(3, NodeFlags.None, null, 0, SomeService, [Dep])
|
||||
]));
|
||||
|
||||
expect(instance.dep instanceof Dep).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not inject deps from sibling root elements', () => {
|
||||
const rootElNodes = [
|
||||
elementDef(0, NodeFlags.None, null, null, 1, 'span'),
|
||||
directiveDef(1, NodeFlags.None, null, 0, Dep, []),
|
||||
elementDef(2, NodeFlags.None, null, null, 1, 'span'),
|
||||
directiveDef(3, NodeFlags.None, null, 0, SomeService, [Dep]),
|
||||
];
|
||||
|
||||
expect(() => createAndGetRootNodes(compViewDef(rootElNodes)))
|
||||
.toThrowError(
|
||||
`${
|
||||
ivyEnabled ?
|
||||
'R3InjectorError' :
|
||||
'StaticInjectorError'}(DynamicTestModule)[SomeService -> Dep]: \n` +
|
||||
' StaticInjectorError(Platform: core)[SomeService -> Dep]: \n' +
|
||||
' NullInjectorError: No provider for Dep!');
|
||||
|
||||
const nonRootElNodes = [
|
||||
elementDef(0, NodeFlags.None, null, null, 4, 'span'),
|
||||
elementDef(1, NodeFlags.None, null, null, 1, 'span'),
|
||||
directiveDef(2, NodeFlags.None, null, 0, Dep, []),
|
||||
elementDef(3, NodeFlags.None, null, null, 1, 'span'),
|
||||
directiveDef(4, NodeFlags.None, null, 0, SomeService, [Dep]),
|
||||
];
|
||||
|
||||
expect(() => createAndGetRootNodes(compViewDef(nonRootElNodes)))
|
||||
.toThrowError(
|
||||
`${
|
||||
ivyEnabled ?
|
||||
'R3InjectorError' :
|
||||
'StaticInjectorError'}(DynamicTestModule)[SomeService -> Dep]: \n` +
|
||||
' StaticInjectorError(Platform: core)[SomeService -> Dep]: \n' +
|
||||
' NullInjectorError: No provider for Dep!');
|
||||
});
|
||||
|
||||
it('should inject from a parent element in a parent view', () => {
|
||||
createAndGetRootNodes(compViewDef([
|
||||
elementDef(
|
||||
0, NodeFlags.None, null, null, 1, 'div', null, null, null, null,
|
||||
() => compViewDef([
|
||||
elementDef(0, NodeFlags.None, null, null, 1, 'span'),
|
||||
directiveDef(1, NodeFlags.None, null, 0, SomeService, [Dep])
|
||||
])),
|
||||
directiveDef(1, NodeFlags.Component, null, 0, Dep, []),
|
||||
]));
|
||||
|
||||
expect(instance.dep instanceof Dep).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should throw for missing dependencies', () => {
|
||||
expect(() => createAndGetRootNodes(compViewDef([
|
||||
elementDef(0, NodeFlags.None, null, null, 1, 'span'),
|
||||
directiveDef(1, NodeFlags.None, null, 0, SomeService, ['nonExistingDep'])
|
||||
])))
|
||||
.toThrowError(
|
||||
`${
|
||||
ivyEnabled ? 'R3InjectorError' :
|
||||
'StaticInjectorError'}(DynamicTestModule)[nonExistingDep]: \n` +
|
||||
' StaticInjectorError(Platform: core)[nonExistingDep]: \n' +
|
||||
' NullInjectorError: No provider for nonExistingDep!');
|
||||
});
|
||||
|
||||
it('should use null for optional missing dependencies', () => {
|
||||
createAndGetRootNodes(compViewDef([
|
||||
elementDef(0, NodeFlags.None, null, null, 1, 'span'),
|
||||
directiveDef(
|
||||
1, NodeFlags.None, null, 0, SomeService, [[DepFlags.Optional, 'nonExistingDep']])
|
||||
]));
|
||||
expect(instance.dep).toBe(null);
|
||||
});
|
||||
|
||||
it('should skip the current element when using SkipSelf', () => {
|
||||
createAndGetRootNodes(compViewDef([
|
||||
elementDef(0, NodeFlags.None, null, null, 4, 'span'),
|
||||
providerDef(NodeFlags.TypeValueProvider, null, 'someToken', 'someParentValue', []),
|
||||
elementDef(2, NodeFlags.None, null, null, 2, 'span'),
|
||||
providerDef(NodeFlags.TypeValueProvider, null, 'someToken', 'someValue', []),
|
||||
directiveDef(
|
||||
4, NodeFlags.None, null, 0, SomeService, [[DepFlags.SkipSelf, 'someToken']])
|
||||
]));
|
||||
expect(instance.dep).toBe('someParentValue');
|
||||
});
|
||||
|
||||
it('should ask the root injector',
|
||||
withModule({providers: [{provide: 'rootDep', useValue: 'rootValue'}]}, () => {
|
||||
createAndGetRootNodes(compViewDef([
|
||||
elementDef(0, NodeFlags.None, null, null, 1, 'span'),
|
||||
directiveDef(1, NodeFlags.None, null, 0, SomeService, ['rootDep'])
|
||||
]));
|
||||
|
||||
expect(instance.dep).toBe('rootValue');
|
||||
}));
|
||||
|
||||
describe('builtin tokens', () => {
|
||||
it('should inject ViewContainerRef', () => {
|
||||
createAndGetRootNodes(compViewDef([
|
||||
anchorDef(NodeFlags.EmbeddedViews, null, null, 1),
|
||||
directiveDef(1, NodeFlags.None, null, 0, SomeService, [ViewContainerRef]),
|
||||
]));
|
||||
|
||||
expect(instance.dep.createEmbeddedView).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should inject TemplateRef', () => {
|
||||
createAndGetRootNodes(compViewDef([
|
||||
anchorDef(NodeFlags.None, null, null, 1, null, compViewDefFactory([anchorDef(
|
||||
NodeFlags.None, null, null, 0)])),
|
||||
directiveDef(1, NodeFlags.None, null, 0, SomeService, [TemplateRef]),
|
||||
]));
|
||||
|
||||
expect(instance.dep.createEmbeddedView).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should inject ElementRef', () => {
|
||||
const {view} = createAndGetRootNodes(compViewDef([
|
||||
elementDef(0, NodeFlags.None, null, null, 1, 'span'),
|
||||
directiveDef(1, NodeFlags.None, null, 0, SomeService, [ElementRef]),
|
||||
]));
|
||||
|
||||
expect(instance.dep.nativeElement).toBe(asElementData(view, 0).renderElement);
|
||||
});
|
||||
|
||||
it('should inject Injector', () => {
|
||||
const {view} = createAndGetRootNodes(compViewDef([
|
||||
elementDef(0, NodeFlags.None, null, null, 1, 'span'),
|
||||
directiveDef(1, NodeFlags.None, null, 0, SomeService, [Injector]),
|
||||
]));
|
||||
|
||||
expect(instance.dep.get(SomeService)).toBe(instance);
|
||||
});
|
||||
|
||||
it('should inject ChangeDetectorRef for non component providers', () => {
|
||||
const {view} = createAndGetRootNodes(compViewDef([
|
||||
elementDef(0, NodeFlags.None, null, null, 1, 'span'),
|
||||
directiveDef(1, NodeFlags.None, null, 0, SomeService, [ChangeDetectorRef])
|
||||
]));
|
||||
|
||||
expect(instance.dep._view).toBe(view);
|
||||
});
|
||||
|
||||
it('should inject ChangeDetectorRef for component providers', () => {
|
||||
const {view, rootNodes} = createAndGetRootNodes(compViewDef([
|
||||
elementDef(
|
||||
0, NodeFlags.None, null, null, 1, 'div', null, null, null, null,
|
||||
() => compViewDef([
|
||||
elementDef(0, NodeFlags.None, null, null, 0, 'span'),
|
||||
])),
|
||||
directiveDef(1, NodeFlags.Component, null, 0, SomeService, [ChangeDetectorRef]),
|
||||
]));
|
||||
|
||||
const compView = asElementData(view, 0).componentView;
|
||||
expect(instance.dep._view).toBe(compView);
|
||||
});
|
||||
|
||||
it('should inject Renderer2', () => {
|
||||
createAndGetRootNodes(compViewDef([
|
||||
elementDef(
|
||||
0, NodeFlags.None, null, null, 1, 'span', null, null, null, null,
|
||||
() => compViewDef([anchorDef(NodeFlags.None, null, null, 0)])),
|
||||
directiveDef(1, NodeFlags.Component, null, 0, SomeService, [Renderer2])
|
||||
]));
|
||||
|
||||
expect(instance.dep.createElement).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('data binding', () => {
|
||||
ARG_TYPE_VALUES.forEach((inlineDynamic) => {
|
||||
it(`should update via strategy ${inlineDynamic}`, () => {
|
||||
let instance: SomeService = undefined!;
|
||||
|
||||
class SomeService {
|
||||
a: any;
|
||||
b: any;
|
||||
constructor() {
|
||||
instance = this;
|
||||
}
|
||||
}
|
||||
|
||||
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
|
||||
[
|
||||
elementDef(0, NodeFlags.None, null, null, 1, 'span'),
|
||||
directiveDef(
|
||||
1, NodeFlags.None, null, 0, SomeService, [], {a: [0, 'a'], b: [1, 'b']})
|
||||
],
|
||||
(check, view) => {
|
||||
checkNodeInlineOrDynamic(check, view, 1, inlineDynamic, ['v1', 'v2']);
|
||||
}));
|
||||
|
||||
Services.checkAndUpdateView(view);
|
||||
|
||||
expect(instance.a).toBe('v1');
|
||||
expect(instance.b).toBe('v2');
|
||||
|
||||
const el = rootNodes[0];
|
||||
expect(el.getAttribute('ng-reflect-a')).toBe('v1');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('outputs', () => {
|
||||
it('should listen to provider events', () => {
|
||||
let emitter = new EventEmitter<any>();
|
||||
let unsubscribeSpy: any;
|
||||
|
||||
class SomeService {
|
||||
emitter = {
|
||||
subscribe: (callback: any) => {
|
||||
const subscription = emitter.subscribe(callback);
|
||||
unsubscribeSpy = spyOn(subscription, 'unsubscribe').and.callThrough();
|
||||
return subscription;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleEvent = jasmine.createSpy('handleEvent');
|
||||
|
||||
const {view, rootNodes} = createAndGetRootNodes(compViewDef([
|
||||
elementDef(0, NodeFlags.None, null, null, 1, 'span', null, null, null, handleEvent),
|
||||
directiveDef(
|
||||
1, NodeFlags.None, null, 0, SomeService, [], null, {emitter: 'someEventName'})
|
||||
]));
|
||||
|
||||
emitter.emit('someEventInstance');
|
||||
expect(handleEvent).toHaveBeenCalledWith(view, 'someEventName', 'someEventInstance');
|
||||
|
||||
Services.destroyView(view);
|
||||
expect(unsubscribeSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should report debug info on event errors', () => {
|
||||
const handleErrorSpy = spyOn(TestBed.inject(ErrorHandler), 'handleError');
|
||||
let emitter = new EventEmitter<any>();
|
||||
|
||||
class SomeService {
|
||||
emitter = emitter;
|
||||
}
|
||||
|
||||
const {view, rootNodes} = createAndGetRootNodes(compViewDef([
|
||||
elementDef(
|
||||
0, NodeFlags.None, null, null, 1, 'span', null, null, null,
|
||||
() => {
|
||||
throw new Error('Test');
|
||||
}),
|
||||
directiveDef(
|
||||
1, NodeFlags.None, null, 0, SomeService, [], null, {emitter: 'someEventName'})
|
||||
]));
|
||||
|
||||
emitter.emit('someEventInstance');
|
||||
const err = handleErrorSpy.calls.mostRecent().args[0];
|
||||
expect(err).toBeTruthy();
|
||||
const debugCtx = getDebugContext(err);
|
||||
expect(debugCtx.view).toBe(view);
|
||||
// events are emitted with the index of the element, not the index of the provider.
|
||||
expect(debugCtx.nodeIndex).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('lifecycle hooks', () => {
|
||||
it('should call the lifecycle hooks in the right order', () => {
|
||||
let instanceCount = 0;
|
||||
let log: string[] = [];
|
||||
|
||||
class SomeService implements OnInit, DoCheck, OnChanges, AfterContentInit,
|
||||
AfterContentChecked, AfterViewInit, AfterViewChecked,
|
||||
OnDestroy {
|
||||
id: number;
|
||||
a: any;
|
||||
ngOnInit() {
|
||||
log.push(`${this.id}_ngOnInit`);
|
||||
}
|
||||
ngDoCheck() {
|
||||
log.push(`${this.id}_ngDoCheck`);
|
||||
}
|
||||
ngOnChanges() {
|
||||
log.push(`${this.id}_ngOnChanges`);
|
||||
}
|
||||
ngAfterContentInit() {
|
||||
log.push(`${this.id}_ngAfterContentInit`);
|
||||
}
|
||||
ngAfterContentChecked() {
|
||||
log.push(`${this.id}_ngAfterContentChecked`);
|
||||
}
|
||||
ngAfterViewInit() {
|
||||
log.push(`${this.id}_ngAfterViewInit`);
|
||||
}
|
||||
ngAfterViewChecked() {
|
||||
log.push(`${this.id}_ngAfterViewChecked`);
|
||||
}
|
||||
ngOnDestroy() {
|
||||
log.push(`${this.id}_ngOnDestroy`);
|
||||
}
|
||||
constructor() {
|
||||
this.id = instanceCount++;
|
||||
}
|
||||
}
|
||||
|
||||
const allFlags = NodeFlags.OnInit | NodeFlags.DoCheck | NodeFlags.OnChanges |
|
||||
NodeFlags.AfterContentInit | NodeFlags.AfterContentChecked | NodeFlags.AfterViewInit |
|
||||
NodeFlags.AfterViewChecked | NodeFlags.OnDestroy;
|
||||
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
|
||||
[
|
||||
elementDef(0, NodeFlags.None, null, null, 3, 'span'),
|
||||
directiveDef(1, allFlags, null, 0, SomeService, [], {a: [0, 'a']}),
|
||||
elementDef(2, NodeFlags.None, null, null, 1, 'span'),
|
||||
directiveDef(3, allFlags, null, 0, SomeService, [], {a: [0, 'a']})
|
||||
],
|
||||
(check, view) => {
|
||||
check(view, 1, ArgumentType.Inline, 'someValue');
|
||||
check(view, 3, ArgumentType.Inline, 'someValue');
|
||||
}));
|
||||
|
||||
Services.checkAndUpdateView(view);
|
||||
|
||||
// Note: After... hooks are called bottom up.
|
||||
expect(log).toEqual([
|
||||
'0_ngOnChanges',
|
||||
'0_ngOnInit',
|
||||
'0_ngDoCheck',
|
||||
'1_ngOnChanges',
|
||||
'1_ngOnInit',
|
||||
'1_ngDoCheck',
|
||||
'1_ngAfterContentInit',
|
||||
'1_ngAfterContentChecked',
|
||||
'0_ngAfterContentInit',
|
||||
'0_ngAfterContentChecked',
|
||||
'1_ngAfterViewInit',
|
||||
'1_ngAfterViewChecked',
|
||||
'0_ngAfterViewInit',
|
||||
'0_ngAfterViewChecked',
|
||||
]);
|
||||
|
||||
log = [];
|
||||
Services.checkAndUpdateView(view);
|
||||
|
||||
// Note: After... hooks are called bottom up.
|
||||
expect(log).toEqual([
|
||||
'0_ngDoCheck', '1_ngDoCheck', '1_ngAfterContentChecked', '0_ngAfterContentChecked',
|
||||
'1_ngAfterViewChecked', '0_ngAfterViewChecked'
|
||||
]);
|
||||
|
||||
log = [];
|
||||
Services.destroyView(view);
|
||||
|
||||
// Note: ngOnDestroy ist called bottom up.
|
||||
expect(log).toEqual(['1_ngOnDestroy', '0_ngOnDestroy']);
|
||||
});
|
||||
|
||||
it('should call ngOnChanges with the changed values and the non minified names', () => {
|
||||
let changesLog: SimpleChange[] = [];
|
||||
let currValue = 'v1';
|
||||
|
||||
class SomeService implements OnChanges {
|
||||
a: any;
|
||||
ngOnChanges(changes: {[name: string]: SimpleChange}) {
|
||||
changesLog.push(changes['nonMinifiedA']);
|
||||
}
|
||||
}
|
||||
|
||||
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
|
||||
[
|
||||
elementDef(0, NodeFlags.None, null, null, 1, 'span'),
|
||||
directiveDef(
|
||||
1, NodeFlags.OnChanges, null, 0, SomeService, [], {a: [0, 'nonMinifiedA']})
|
||||
],
|
||||
(check, view) => {
|
||||
check(view, 1, ArgumentType.Inline, currValue);
|
||||
}));
|
||||
|
||||
Services.checkAndUpdateView(view);
|
||||
expect(changesLog).toEqual([new SimpleChange(undefined, 'v1', true)]);
|
||||
|
||||
currValue = 'v2';
|
||||
changesLog = [];
|
||||
Services.checkAndUpdateView(view);
|
||||
expect(changesLog).toEqual([new SimpleChange('v1', 'v2', false)]);
|
||||
});
|
||||
|
||||
it('should add a DebugContext to errors in provider afterXXX lifecycles', () => {
|
||||
class SomeService implements AfterContentChecked {
|
||||
ngAfterContentChecked() {
|
||||
throw new Error('Test');
|
||||
}
|
||||
}
|
||||
|
||||
const {view, rootNodes} = createAndGetRootNodes(compViewDef([
|
||||
elementDef(0, NodeFlags.None, null, null, 1, 'span'),
|
||||
directiveDef(1, NodeFlags.AfterContentChecked, null, 0, SomeService, [], {a: [0, 'a']}),
|
||||
]));
|
||||
|
||||
let err: any;
|
||||
try {
|
||||
Services.checkAndUpdateView(view);
|
||||
} catch (e) {
|
||||
err = e;
|
||||
}
|
||||
expect(err).toBeTruthy();
|
||||
expect(err.message).toBe('Test');
|
||||
const debugCtx = getDebugContext(err);
|
||||
expect(debugCtx.view).toBe(view);
|
||||
expect(debugCtx.nodeIndex).toBe(1);
|
||||
});
|
||||
|
||||
it('should add a DebugContext to errors inServices.destroyView', () => {
|
||||
class SomeService implements OnDestroy {
|
||||
ngOnDestroy() {
|
||||
throw new Error('Test');
|
||||
}
|
||||
}
|
||||
|
||||
const {view, rootNodes} = createAndGetRootNodes(compViewDef([
|
||||
elementDef(0, NodeFlags.None, null, null, 1, 'span'),
|
||||
directiveDef(1, NodeFlags.OnDestroy, null, 0, SomeService, [], {a: [0, 'a']}),
|
||||
]));
|
||||
|
||||
let err: any;
|
||||
try {
|
||||
Services.destroyView(view);
|
||||
} catch (e) {
|
||||
err = e;
|
||||
}
|
||||
expect(err).toBeTruthy();
|
||||
expect(err.message).toBe('Test');
|
||||
const debugCtx = getDebugContext(err);
|
||||
expect(debugCtx.view).toBe(view);
|
||||
expect(debugCtx.nodeIndex).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -1,136 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 {PipeTransform} from '@angular/core';
|
||||
import {asProviderData, directiveDef, elementDef, NodeFlags, nodeValue, pipeDef, pureArrayDef, pureObjectDef, purePipeDef, Services} from '@angular/core/src/view/index';
|
||||
|
||||
import {ARG_TYPE_VALUES, checkNodeInlineOrDynamic, compViewDef, createAndGetRootNodes} from './helper';
|
||||
|
||||
{
|
||||
describe(`View Pure Expressions`, () => {
|
||||
class Service {
|
||||
data: any;
|
||||
}
|
||||
|
||||
describe('pure arrays', () => {
|
||||
ARG_TYPE_VALUES.forEach((inlineDynamic) => {
|
||||
it(`should update via strategy ${inlineDynamic}`, () => {
|
||||
let values: any[];
|
||||
|
||||
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
|
||||
[
|
||||
elementDef(0, NodeFlags.None, null, null, 2, 'span'),
|
||||
pureArrayDef(1, 2),
|
||||
directiveDef(2, NodeFlags.None, null, 0, Service, [], {data: [0, 'data']}),
|
||||
],
|
||||
(check, view) => {
|
||||
const pureValue = checkNodeInlineOrDynamic(check, view, 1, inlineDynamic, values);
|
||||
checkNodeInlineOrDynamic(check, view, 2, inlineDynamic, [pureValue]);
|
||||
}));
|
||||
const service = asProviderData(view, 2).instance;
|
||||
|
||||
values = [1, 2];
|
||||
Services.checkAndUpdateView(view);
|
||||
const arr0 = service.data;
|
||||
expect(arr0).toEqual([1, 2]);
|
||||
|
||||
// instance should not change
|
||||
// if the values don't change
|
||||
Services.checkAndUpdateView(view);
|
||||
expect(service.data).toBe(arr0);
|
||||
|
||||
values = [3, 2];
|
||||
Services.checkAndUpdateView(view);
|
||||
const arr1 = service.data;
|
||||
expect(arr1).not.toBe(arr0);
|
||||
expect(arr1).toEqual([3, 2]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('pure objects', () => {
|
||||
ARG_TYPE_VALUES.forEach((inlineDynamic) => {
|
||||
it(`should update via strategy ${inlineDynamic}`, () => {
|
||||
let values: any[];
|
||||
|
||||
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
|
||||
[
|
||||
elementDef(0, NodeFlags.None, null, null, 2, 'span'),
|
||||
pureObjectDef(1, {a: 0, b: 1}),
|
||||
directiveDef(2, NodeFlags.None, null, 0, Service, [], {data: [0, 'data']})
|
||||
],
|
||||
(check, view) => {
|
||||
const pureValue = checkNodeInlineOrDynamic(check, view, 1, inlineDynamic, values);
|
||||
checkNodeInlineOrDynamic(check, view, 2, inlineDynamic, [pureValue]);
|
||||
}));
|
||||
const service = asProviderData(view, 2).instance;
|
||||
|
||||
values = [1, 2];
|
||||
Services.checkAndUpdateView(view);
|
||||
const obj0 = service.data;
|
||||
expect(obj0).toEqual({a: 1, b: 2});
|
||||
|
||||
// instance should not change
|
||||
// if the values don't change
|
||||
Services.checkAndUpdateView(view);
|
||||
expect(service.data).toBe(obj0);
|
||||
|
||||
values = [3, 2];
|
||||
Services.checkAndUpdateView(view);
|
||||
const obj1 = service.data;
|
||||
expect(obj1).not.toBe(obj0);
|
||||
expect(obj1).toEqual({a: 3, b: 2});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('pure pipes', () => {
|
||||
ARG_TYPE_VALUES.forEach((inlineDynamic) => {
|
||||
it(`should update via strategy ${inlineDynamic}`, () => {
|
||||
class SomePipe implements PipeTransform {
|
||||
transform(v1: any, v2: any) {
|
||||
return [v1 + 10, v2 + 20];
|
||||
}
|
||||
}
|
||||
|
||||
let values: any[];
|
||||
|
||||
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
|
||||
[
|
||||
elementDef(0, NodeFlags.None, null!, null!, 3, 'span'),
|
||||
pipeDef(NodeFlags.None, SomePipe, []),
|
||||
purePipeDef(2, 2),
|
||||
directiveDef(3, NodeFlags.None, null, 0, Service, [], {data: [0, 'data']}),
|
||||
],
|
||||
(check, view) => {
|
||||
const pureValue = checkNodeInlineOrDynamic(
|
||||
check, view, 2, inlineDynamic, [nodeValue(view, 1)].concat(values));
|
||||
checkNodeInlineOrDynamic(check, view, 3, inlineDynamic, [pureValue]);
|
||||
}));
|
||||
const service = asProviderData(view, 3).instance;
|
||||
|
||||
values = [1, 2];
|
||||
Services.checkAndUpdateView(view);
|
||||
const obj0 = service.data;
|
||||
expect(obj0).toEqual([11, 22]);
|
||||
|
||||
// instance should not change
|
||||
// if the values don't change
|
||||
Services.checkAndUpdateView(view);
|
||||
expect(service.data).toBe(obj0);
|
||||
|
||||
values = [3, 2];
|
||||
Services.checkAndUpdateView(view);
|
||||
const obj1 = service.data;
|
||||
expect(obj1).not.toBe(obj0);
|
||||
expect(obj1).toEqual([13, 22]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -1,400 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 {ElementRef, QueryList, TemplateRef, ViewContainerRef} from '@angular/core';
|
||||
import {getDebugContext} from '@angular/core/src/errors';
|
||||
import {anchorDef, asElementData, asProviderData, attachEmbeddedView, detachEmbeddedView, directiveDef, elementDef, NodeDef, NodeFlags, QueryBindingType, queryDef, QueryValueType, Services} from '@angular/core/src/view/index';
|
||||
|
||||
import {compViewDef, compViewDefFactory, createAndGetRootNodes, createEmbeddedView} from './helper';
|
||||
|
||||
{
|
||||
describe(`Query Views`, () => {
|
||||
const someQueryId = 1;
|
||||
|
||||
class AService {}
|
||||
|
||||
class QueryService {
|
||||
// TODO(issue/24571): remove '!'.
|
||||
a!: QueryList<AService>;
|
||||
}
|
||||
|
||||
function contentQueryProviders(checkIndex: number) {
|
||||
return [
|
||||
directiveDef(checkIndex, NodeFlags.None, null, 1, QueryService, []),
|
||||
queryDef(
|
||||
NodeFlags.TypeContentQuery | NodeFlags.DynamicQuery, someQueryId,
|
||||
{'a': QueryBindingType.All})
|
||||
];
|
||||
}
|
||||
|
||||
const cQPLength = contentQueryProviders(0).length;
|
||||
|
||||
// nodes first checkIndex should be 1 (to account for the `queryDef`
|
||||
function compViewQueryProviders(checkIndex: number, extraChildCount: number, nodes: NodeDef[]) {
|
||||
return [
|
||||
elementDef(
|
||||
checkIndex, NodeFlags.None, null, null, 1 + extraChildCount, 'div', null, null, null,
|
||||
null, () => compViewDef([
|
||||
queryDef(
|
||||
NodeFlags.TypeViewQuery | NodeFlags.DynamicQuery, someQueryId,
|
||||
{'a': QueryBindingType.All}),
|
||||
...nodes
|
||||
])),
|
||||
directiveDef(
|
||||
checkIndex + 1,
|
||||
NodeFlags.Component,
|
||||
null!,
|
||||
0,
|
||||
QueryService,
|
||||
[],
|
||||
null!,
|
||||
null!,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
const cVQLength = compViewQueryProviders(0, 0, []).length;
|
||||
|
||||
|
||||
function aServiceProvider(checkIndex: number) {
|
||||
return directiveDef(
|
||||
checkIndex, NodeFlags.None, [[someQueryId, QueryValueType.Provider]], 0, AService, []);
|
||||
}
|
||||
|
||||
describe('content queries', () => {
|
||||
it('should query providers on the same element and child elements', () => {
|
||||
const {view} = createAndGetRootNodes(compViewDef([
|
||||
elementDef(0, NodeFlags.None, null, null, 5, 'div'),
|
||||
...contentQueryProviders(1),
|
||||
aServiceProvider(1 + cQPLength),
|
||||
elementDef(2 + cQPLength, NodeFlags.None, null, null, 1, 'div'),
|
||||
aServiceProvider(3 + cQPLength),
|
||||
]));
|
||||
|
||||
const qs: QueryService = asProviderData(view, 1).instance;
|
||||
expect(qs.a).toBeUndefined();
|
||||
|
||||
Services.checkAndUpdateView(view);
|
||||
|
||||
const as = qs.a.toArray();
|
||||
expect(as.length).toBe(2);
|
||||
expect(as [0]).toBe(asProviderData(view, 3).instance);
|
||||
expect(as [1]).toBe(asProviderData(view, 5).instance);
|
||||
});
|
||||
|
||||
it('should not query providers on sibling or parent elements', () => {
|
||||
const {view} = createAndGetRootNodes(compViewDef([
|
||||
elementDef(0, NodeFlags.None, null, null, 6, 'div'),
|
||||
aServiceProvider(1),
|
||||
elementDef(2, NodeFlags.None, null, null, 2, 'div'),
|
||||
...contentQueryProviders(3),
|
||||
elementDef(3 + cQPLength, NodeFlags.None, null, null, 1, 'div'),
|
||||
aServiceProvider(4 + cQPLength),
|
||||
]));
|
||||
|
||||
Services.checkAndUpdateView(view);
|
||||
|
||||
const qs: QueryService = asProviderData(view, 3).instance;
|
||||
expect(qs.a.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('view queries', () => {
|
||||
it('should query providers in the view', () => {
|
||||
const {view} = createAndGetRootNodes(compViewDef([
|
||||
...compViewQueryProviders(
|
||||
0, 0,
|
||||
[
|
||||
elementDef(1, NodeFlags.None, null, null, 1, 'span'),
|
||||
aServiceProvider(2),
|
||||
]),
|
||||
]));
|
||||
|
||||
Services.checkAndUpdateView(view);
|
||||
|
||||
const comp: QueryService = asProviderData(view, 1).instance;
|
||||
const compView = asElementData(view, 0).componentView;
|
||||
expect(comp.a.length).toBe(1);
|
||||
expect(comp.a.first).toBe(asProviderData(compView, 2).instance);
|
||||
});
|
||||
|
||||
it('should not query providers on the host element', () => {
|
||||
const {view} = createAndGetRootNodes(compViewDef([
|
||||
...compViewQueryProviders(0, 1, [elementDef(1, NodeFlags.None, null, null, 0, 'span')]),
|
||||
aServiceProvider(cVQLength),
|
||||
]));
|
||||
|
||||
Services.checkAndUpdateView(view);
|
||||
const comp: QueryService = asProviderData(view, 1).instance;
|
||||
expect(comp.a.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('embedded views', () => {
|
||||
it('should query providers in embedded views', () => {
|
||||
const {view} = createAndGetRootNodes(compViewDef([
|
||||
elementDef(0, NodeFlags.None, null, null, 5, 'div'),
|
||||
...contentQueryProviders(1),
|
||||
anchorDef(NodeFlags.EmbeddedViews, null, null, 2, null, compViewDefFactory([
|
||||
elementDef(0, NodeFlags.None, null, null, 1, 'div'),
|
||||
aServiceProvider(1),
|
||||
])),
|
||||
...contentQueryProviders(2 + cQPLength),
|
||||
]));
|
||||
|
||||
const childView = createEmbeddedView(view, view.def.nodes[3]);
|
||||
attachEmbeddedView(view, asElementData(view, 3), 0, childView);
|
||||
Services.checkAndUpdateView(view);
|
||||
|
||||
// queries on parent elements of anchors
|
||||
const qs1: QueryService = asProviderData(view, 1).instance;
|
||||
expect(qs1.a.length).toBe(1);
|
||||
expect(qs1.a.first instanceof AService).toBe(true);
|
||||
|
||||
// queries on the anchor
|
||||
const qs2: QueryService = asProviderData(view, 4).instance;
|
||||
expect(qs2.a.length).toBe(1);
|
||||
expect(qs2.a.first instanceof AService).toBe(true);
|
||||
});
|
||||
|
||||
it('should query providers in embedded views only at the template declaration', () => {
|
||||
const {view} = createAndGetRootNodes(compViewDef([
|
||||
elementDef(0, NodeFlags.None, null, null, 3, 'div'),
|
||||
...contentQueryProviders(1),
|
||||
anchorDef(NodeFlags.EmbeddedViews, null, null, 0, null, compViewDefFactory([
|
||||
elementDef(0, NodeFlags.None, null, null, 1, 'div'),
|
||||
aServiceProvider(1),
|
||||
])),
|
||||
elementDef(2 + cQPLength, NodeFlags.None, null, null, 3, 'div'),
|
||||
...contentQueryProviders(3 + cQPLength),
|
||||
anchorDef(NodeFlags.EmbeddedViews, null, null, 0),
|
||||
]));
|
||||
|
||||
const childView = createEmbeddedView(view, view.def.nodes[3]);
|
||||
// attach at a different place than the one where the template was defined
|
||||
attachEmbeddedView(view, asElementData(view, 7), 0, childView);
|
||||
|
||||
Services.checkAndUpdateView(view);
|
||||
|
||||
// query on the declaration place
|
||||
const qs1: QueryService = asProviderData(view, 1).instance;
|
||||
expect(qs1.a.length).toBe(1);
|
||||
expect(qs1.a.first instanceof AService).toBe(true);
|
||||
|
||||
// query on the attach place
|
||||
const qs2: QueryService = asProviderData(view, 5).instance;
|
||||
expect(qs2.a.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should update content queries if embedded views are added or removed', () => {
|
||||
const {view} = createAndGetRootNodes(compViewDef([
|
||||
elementDef(0, NodeFlags.None, null, null, 3, 'div'),
|
||||
...contentQueryProviders(1),
|
||||
anchorDef(NodeFlags.EmbeddedViews, null, null, 0, null, compViewDefFactory([
|
||||
elementDef(0, NodeFlags.None, null, null, 1, 'div'),
|
||||
aServiceProvider(1),
|
||||
])),
|
||||
]));
|
||||
|
||||
Services.checkAndUpdateView(view);
|
||||
|
||||
const qs: QueryService = asProviderData(view, 1).instance;
|
||||
expect(qs.a.length).toBe(0);
|
||||
|
||||
const childView = createEmbeddedView(view, view.def.nodes[3]);
|
||||
attachEmbeddedView(view, asElementData(view, 3), 0, childView);
|
||||
Services.checkAndUpdateView(view);
|
||||
|
||||
expect(qs.a.length).toBe(1);
|
||||
|
||||
detachEmbeddedView(asElementData(view, 3), 0);
|
||||
|
||||
Services.checkAndUpdateView(view);
|
||||
|
||||
expect(qs.a.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should update view queries if embedded views are added or removed', () => {
|
||||
const {view} = createAndGetRootNodes(compViewDef([
|
||||
...compViewQueryProviders(
|
||||
0, 0,
|
||||
[
|
||||
anchorDef(NodeFlags.EmbeddedViews, null, null, 0, null, compViewDefFactory([
|
||||
elementDef(0, NodeFlags.None, null, null, 1, 'div'),
|
||||
aServiceProvider(1),
|
||||
])),
|
||||
]),
|
||||
]));
|
||||
|
||||
Services.checkAndUpdateView(view);
|
||||
|
||||
const comp: QueryService = asProviderData(view, 1).instance;
|
||||
expect(comp.a.length).toBe(0);
|
||||
|
||||
const compView = asElementData(view, 0).componentView;
|
||||
const childView = createEmbeddedView(compView, compView.def.nodes[1]);
|
||||
attachEmbeddedView(view, asElementData(compView, 1), 0, childView);
|
||||
Services.checkAndUpdateView(view);
|
||||
|
||||
expect(comp.a.length).toBe(1);
|
||||
|
||||
detachEmbeddedView(asElementData(compView, 1), 0);
|
||||
Services.checkAndUpdateView(view);
|
||||
|
||||
expect(comp.a.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('QueryBindingType', () => {
|
||||
it('should query all matches', () => {
|
||||
class QueryService {
|
||||
// TODO(issue/24571): remove '!'.
|
||||
a!: QueryList<AService>;
|
||||
}
|
||||
|
||||
const {view} = createAndGetRootNodes(compViewDef([
|
||||
elementDef(0, NodeFlags.None, null, null, 4, 'div'),
|
||||
directiveDef(1, NodeFlags.None, null, 1, QueryService, []),
|
||||
queryDef(
|
||||
NodeFlags.TypeContentQuery | NodeFlags.DynamicQuery, someQueryId,
|
||||
{'a': QueryBindingType.All}),
|
||||
aServiceProvider(3),
|
||||
aServiceProvider(4),
|
||||
]));
|
||||
|
||||
Services.checkAndUpdateView(view);
|
||||
|
||||
const qs: QueryService = asProviderData(view, 1).instance;
|
||||
expect(qs.a instanceof QueryList).toBeTruthy();
|
||||
expect(qs.a.toArray()).toEqual([
|
||||
asProviderData(view, 3).instance,
|
||||
asProviderData(view, 4).instance,
|
||||
]);
|
||||
});
|
||||
|
||||
it('should query the first match', () => {
|
||||
class QueryService {
|
||||
// TODO(issue/24571): remove '!'.
|
||||
a!: AService;
|
||||
}
|
||||
|
||||
const {view} = createAndGetRootNodes(compViewDef([
|
||||
elementDef(0, NodeFlags.None, null, null, 4, 'div'),
|
||||
directiveDef(1, NodeFlags.None, null, 1, QueryService, []),
|
||||
queryDef(
|
||||
NodeFlags.TypeContentQuery | NodeFlags.DynamicQuery, someQueryId,
|
||||
{'a': QueryBindingType.First}),
|
||||
aServiceProvider(3),
|
||||
aServiceProvider(4),
|
||||
]));
|
||||
|
||||
Services.checkAndUpdateView(view);
|
||||
|
||||
const qs: QueryService = asProviderData(view, 1).instance;
|
||||
expect(qs.a).toBe(asProviderData(view, 3).instance);
|
||||
});
|
||||
});
|
||||
|
||||
describe('query builtins', () => {
|
||||
it('should query ElementRef', () => {
|
||||
class QueryService {
|
||||
// TODO(issue/24571): remove '!'.
|
||||
a!: ElementRef;
|
||||
}
|
||||
|
||||
const {view} = createAndGetRootNodes(compViewDef([
|
||||
elementDef(0, NodeFlags.None, [[someQueryId, QueryValueType.ElementRef]], null, 2, 'div'),
|
||||
directiveDef(1, NodeFlags.None, null, 1, QueryService, []),
|
||||
queryDef(
|
||||
NodeFlags.TypeContentQuery | NodeFlags.DynamicQuery, someQueryId,
|
||||
{'a': QueryBindingType.First}),
|
||||
]));
|
||||
|
||||
Services.checkAndUpdateView(view);
|
||||
|
||||
const qs: QueryService = asProviderData(view, 1).instance;
|
||||
expect(qs.a.nativeElement).toBe(asElementData(view, 0).renderElement);
|
||||
});
|
||||
|
||||
it('should query TemplateRef', () => {
|
||||
class QueryService {
|
||||
// TODO(issue/24571): remove '!'.
|
||||
a!: TemplateRef<any>;
|
||||
}
|
||||
|
||||
const {view} = createAndGetRootNodes(compViewDef([
|
||||
anchorDef(
|
||||
NodeFlags.None, [[someQueryId, QueryValueType.TemplateRef]], null, 2, null,
|
||||
compViewDefFactory([anchorDef(NodeFlags.None, null, null, 0)])),
|
||||
directiveDef(1, NodeFlags.None, null, 1, QueryService, []),
|
||||
queryDef(
|
||||
NodeFlags.TypeContentQuery | NodeFlags.DynamicQuery, someQueryId,
|
||||
{'a': QueryBindingType.First}),
|
||||
]));
|
||||
|
||||
Services.checkAndUpdateView(view);
|
||||
|
||||
const qs: QueryService = asProviderData(view, 1).instance;
|
||||
expect(qs.a.createEmbeddedView).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should query ViewContainerRef', () => {
|
||||
class QueryService {
|
||||
// TODO(issue/24571): remove '!'.
|
||||
a!: ViewContainerRef;
|
||||
}
|
||||
|
||||
const {view} = createAndGetRootNodes(compViewDef([
|
||||
anchorDef(
|
||||
NodeFlags.EmbeddedViews, [[someQueryId, QueryValueType.ViewContainerRef]], null, 2),
|
||||
directiveDef(1, NodeFlags.None, null, 1, QueryService, []),
|
||||
queryDef(
|
||||
NodeFlags.TypeContentQuery | NodeFlags.DynamicQuery, someQueryId,
|
||||
{'a': QueryBindingType.First}),
|
||||
]));
|
||||
|
||||
Services.checkAndUpdateView(view);
|
||||
|
||||
const qs: QueryService = asProviderData(view, 1).instance;
|
||||
expect(qs.a.createEmbeddedView).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('general binding behavior', () => {
|
||||
it('should report debug info on binding errors', () => {
|
||||
class QueryService {
|
||||
set a(value: any) {
|
||||
throw new Error('Test');
|
||||
}
|
||||
}
|
||||
|
||||
const {view} = createAndGetRootNodes(compViewDef([
|
||||
elementDef(0, NodeFlags.None, null, null, 3, 'div'),
|
||||
directiveDef(1, NodeFlags.None, null, 1, QueryService, []),
|
||||
queryDef(
|
||||
NodeFlags.TypeContentQuery | NodeFlags.DynamicQuery, someQueryId,
|
||||
{'a': QueryBindingType.All}),
|
||||
aServiceProvider(3),
|
||||
]));
|
||||
|
||||
|
||||
let err: any;
|
||||
try {
|
||||
Services.checkAndUpdateView(view);
|
||||
} catch (e) {
|
||||
err = e;
|
||||
}
|
||||
expect(err).toBeTruthy();
|
||||
expect(err.message).toBe('Test');
|
||||
const debugCtx = getDebugContext(err);
|
||||
expect(debugCtx.view).toBe(view);
|
||||
expect(debugCtx.nodeIndex).toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 {asElementData, asTextData, DebugContext, directiveDef, elementDef, NodeFlags, QueryValueType, Services, textDef} from '@angular/core/src/view/index';
|
||||
|
||||
import {compViewDef, createAndGetRootNodes} from './helper';
|
||||
|
||||
{
|
||||
describe('View Services', () => {
|
||||
describe('DebugContext', () => {
|
||||
class AComp {}
|
||||
|
||||
class AService {}
|
||||
|
||||
function createViewWithData() {
|
||||
const {view} = createAndGetRootNodes(compViewDef([
|
||||
elementDef(
|
||||
0, NodeFlags.None, null, null, 1, 'div', null, null, null, null,
|
||||
() => compViewDef([
|
||||
elementDef(
|
||||
0, NodeFlags.None, [['ref', QueryValueType.ElementRef]], null, 2, 'span'),
|
||||
directiveDef(1, NodeFlags.None, null, 0, AService, []), textDef(2, null, ['a'])
|
||||
])),
|
||||
directiveDef(1, NodeFlags.Component, null, 0, AComp, []),
|
||||
]));
|
||||
return view;
|
||||
}
|
||||
|
||||
it('should provide data for elements', () => {
|
||||
const view = createViewWithData();
|
||||
const compView = asElementData(view, 0).componentView;
|
||||
|
||||
const debugCtx = Services.createDebugContext(compView, 0);
|
||||
|
||||
expect(debugCtx.componentRenderElement).toBe(asElementData(view, 0).renderElement);
|
||||
expect(debugCtx.renderNode).toBe(asElementData(compView, 0).renderElement);
|
||||
expect(debugCtx.injector.get(AComp)).toBe(compView.component);
|
||||
expect(debugCtx.component).toBe(compView.component);
|
||||
expect(debugCtx.context).toBe(compView.context);
|
||||
expect(debugCtx.providerTokens).toEqual([AService]);
|
||||
expect(debugCtx.references['ref'].nativeElement)
|
||||
.toBe(asElementData(compView, 0).renderElement);
|
||||
});
|
||||
|
||||
it('should provide data for text nodes', () => {
|
||||
const view = createViewWithData();
|
||||
const compView = asElementData(view, 0).componentView;
|
||||
|
||||
const debugCtx = Services.createDebugContext(compView, 2);
|
||||
|
||||
expect(debugCtx.componentRenderElement).toBe(asElementData(view, 0).renderElement);
|
||||
expect(debugCtx.renderNode).toBe(asTextData(compView, 2).renderText);
|
||||
expect(debugCtx.injector.get(AComp)).toBe(compView.component);
|
||||
expect(debugCtx.component).toBe(compView.component);
|
||||
expect(debugCtx.context).toBe(compView.context);
|
||||
});
|
||||
|
||||
it('should provide data for other nodes based on the nearest element parent', () => {
|
||||
const view = createViewWithData();
|
||||
const compView = asElementData(view, 0).componentView;
|
||||
|
||||
const debugCtx = Services.createDebugContext(compView, 1);
|
||||
|
||||
expect(debugCtx.renderNode).toBe(asElementData(compView, 0).renderElement);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* 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 {ɵgetDOM as getDOM} from '@angular/common';
|
||||
import {getDebugNode} from '@angular/core';
|
||||
import {asTextData, elementDef, NodeFlags, Services, textDef} from '@angular/core/src/view/index';
|
||||
|
||||
import {ARG_TYPE_VALUES, checkNodeInlineOrDynamic, compViewDef, createAndGetRootNodes} from './helper';
|
||||
|
||||
{
|
||||
describe(`View Text`, () => {
|
||||
describe('create', () => {
|
||||
it('should create text nodes without parents', () => {
|
||||
const rootNodes = createAndGetRootNodes(compViewDef([textDef(0, null, ['a'])])).rootNodes;
|
||||
expect(rootNodes.length).toBe(1);
|
||||
expect(rootNodes[0].textContent).toBe('a');
|
||||
});
|
||||
|
||||
it('should create views with multiple root text nodes', () => {
|
||||
const rootNodes = createAndGetRootNodes(compViewDef([
|
||||
textDef(0, null, ['a']),
|
||||
textDef(1, null, ['b']),
|
||||
])).rootNodes;
|
||||
expect(rootNodes.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should create text nodes with parents', () => {
|
||||
const rootNodes = createAndGetRootNodes(compViewDef([
|
||||
elementDef(0, NodeFlags.None, null, null, 1, 'div'),
|
||||
textDef(1, null, ['a']),
|
||||
])).rootNodes;
|
||||
expect(rootNodes.length).toBe(1);
|
||||
const textNode = rootNodes[0].firstChild;
|
||||
expect(textNode.textContent).toBe('a');
|
||||
});
|
||||
|
||||
it('should add debug information to the renderer', () => {
|
||||
const someContext = {};
|
||||
const {view, rootNodes} =
|
||||
createAndGetRootNodes(compViewDef([textDef(0, null, ['a'])]), someContext);
|
||||
expect(getDebugNode(rootNodes[0])!.nativeNode).toBe(asTextData(view, 0).renderText);
|
||||
});
|
||||
});
|
||||
|
||||
describe('change text', () => {
|
||||
ARG_TYPE_VALUES.forEach((inlineDynamic) => {
|
||||
it(`should update via strategy ${inlineDynamic}`, () => {
|
||||
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
|
||||
[
|
||||
textDef(0, null, ['0', '1', '2']),
|
||||
],
|
||||
null!, (check, view) => {
|
||||
checkNodeInlineOrDynamic(check, view, 0, inlineDynamic, ['a', 'b']);
|
||||
}));
|
||||
|
||||
Services.checkAndUpdateView(view);
|
||||
|
||||
expect(rootNodes[0].textContent).toBe('0a1b2');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue