diff --git a/build.sh b/build.sh index 269d6719b2..722b1fc2ea 100755 --- a/build.sh +++ b/build.sh @@ -8,8 +8,8 @@ source ${currentDir}/scripts/ci/_travis-fold.sh # TODO(i): wrap into subshell, so that we don't pollute CWD, but not yet to minimize diff collision with Jason cd ${currentDir} -PACKAGES=(core - compiler +PACKAGES=(compiler + core common animations platform-browser @@ -27,7 +27,8 @@ PACKAGES=(core service-worker elements) -TSC_PACKAGES=(compiler-cli +TSC_PACKAGES=(compiler + compiler-cli language-service benchpress) @@ -239,7 +240,13 @@ compilePackage() { # For TSC_PACKAGES items if containsElement "${3}" "${TSC_PACKAGES[@]}"; then echo "====== [${3}]: COMPILING: ${TSC} -p ${1}/tsconfig-build.json" + local package_name=$(basename "${2}") $TSC -p ${1}/tsconfig-build.json + if [[ "${3}" = "compiler" ]]; then + if [[ "${package_name}" = "testing" ]]; then + echo "$(cat ${LICENSE_BANNER}) ${N} export * from './${package_name}/${package_name}'" > ${2}/../${package_name}.d.ts + fi + fi else echo "====== [${3}]: COMPILING: ${NGC} -p ${1}/tsconfig-build.json" local package_name=$(basename "${2}") diff --git a/integration/bazel/BUILD.bazel b/integration/bazel/BUILD.bazel index 3307866a21..fe695525e1 100644 --- a/integration/bazel/BUILD.bazel +++ b/integration/bazel/BUILD.bazel @@ -24,9 +24,8 @@ filegroup( ANGULAR_TESTING = [ "node_modules/@angular/*/bundles/*-testing.umd.js", - # We use AOT, so the compiler and the dynamic platform-browser should be - # visible only in tests - "node_modules/@angular/compiler/bundles/*.umd.js", + # We use AOT, so the dynamic platform-browser should be visible only in tests + # NOTE that we still need to include the compiler because the core depends on it "node_modules/@angular/platform-browser-dynamic/bundles/*.umd.js", ] diff --git a/integration/hello_world__closure/closure.conf b/integration/hello_world__closure/closure.conf index 0f4c7d7713..1198d0c870 100644 --- a/integration/hello_world__closure/closure.conf +++ b/integration/hello_world__closure/closure.conf @@ -19,6 +19,9 @@ node_modules/zone.js/dist/zone_externs.js --js node_modules/rxjs/operators/package.json --js node_modules/rxjs/_esm2015/operators/index.js +--js node_modules/@angular/compiler/package.json +--js node_modules/@angular/compiler/fesm2015/compiler.js + --js node_modules/@angular/core/package.json --js node_modules/@angular/core/fesm2015/core.js --js node_modules/@angular/core/src/testability/testability.externs.js diff --git a/integration/i18n/closure.conf b/integration/i18n/closure.conf index 89c0cd7732..6cba1da94a 100644 --- a/integration/i18n/closure.conf +++ b/integration/i18n/closure.conf @@ -17,6 +17,9 @@ node_modules/zone.js/dist/zone_externs.js --module_resolution=node --package_json_entry_names es2015 +--js node_modules/@angular/compiler/package.json +--js node_modules/@angular/compiler/fesm2015/compiler.js + --js node_modules/@angular/core/package.json --js node_modules/@angular/core/fesm2015/core.js --js node_modules/@angular/core/src/testability/testability.externs.js diff --git a/integration/i18n/package.json b/integration/i18n/package.json index a607ff4ef9..8672de9ccb 100644 --- a/integration/i18n/package.json +++ b/integration/i18n/package.json @@ -9,6 +9,7 @@ "@angular/compiler-cli": "file:../../dist/packages-dist/compiler-cli", "@angular/core": "file:../../dist/packages-dist/core", "@angular/platform-browser": "file:../../dist/packages-dist/platform-browser", + "@angular/platform-browser-dynamic": "file:../../dist/packages-dist/platform-browser-dynamic", "@angular/platform-server": "file:../../dist/packages-dist/platform-server", "google-closure-compiler": "git+https://github.com/alexeagle/closure-compiler.git#packagejson.dist", "rxjs": "file:../../node_modules/rxjs", @@ -30,4 +31,4 @@ "preprotractor": "tsc -p e2e", "protractor": "protractor e2e/protractor.config.js" } -} +} \ No newline at end of file diff --git a/integration/i18n/yarn.lock b/integration/i18n/yarn.lock index 3316e73583..8bfbdd926a 100644 --- a/integration/i18n/yarn.lock +++ b/integration/i18n/yarn.lock @@ -3,40 +3,48 @@ "@angular/animations@file:../../dist/packages-dist/animations": - version "6.0.0-beta.7-8203e0365a" + version "7.0.0-beta.1-d2d510089c" dependencies: tslib "^1.9.0" "@angular/common@file:../../dist/packages-dist/common": - version "6.0.0-beta.7-8203e0365a" + version "7.0.0-beta.1-d2d510089c" dependencies: tslib "^1.9.0" "@angular/compiler-cli@file:../../dist/packages-dist/compiler-cli": - version "6.0.0-beta.7-8203e0365a" + version "7.0.0-beta.1-d2d510089c" dependencies: chokidar "^1.4.2" + convert-source-map "^1.5.1" + magic-string "^0.25.0" minimist "^1.2.0" reflect-metadata "^0.1.2" - tsickle "^0.27.2" + source-map "^0.6.1" + tsickle "^0.32.1" "@angular/compiler@file:../../dist/packages-dist/compiler": - version "6.0.0-beta.7-8203e0365a" + version "7.0.0-beta.1-d2d510089c" dependencies: tslib "^1.9.0" "@angular/core@file:../../dist/packages-dist/core": - version "6.0.0-beta.7-8203e0365a" + version "7.0.0-beta.1-d2d510089c" + dependencies: + tslib "^1.9.0" + +"@angular/platform-browser-dynamic@file:../../dist/packages-dist/platform-browser-dynamic": + version "7.0.0-beta.1-d2d510089c" dependencies: tslib "^1.9.0" "@angular/platform-browser@file:../../dist/packages-dist/platform-browser": - version "6.0.0-beta.7-8203e0365a" + version "7.0.0-beta.1-d2d510089c" dependencies: tslib "^1.9.0" "@angular/platform-server@file:../../dist/packages-dist/platform-server": - version "6.0.0-beta.7-8203e0365a" + version "7.0.0-beta.1-d2d510089c" dependencies: domino "^2.0.1" tslib "^1.9.0" @@ -510,6 +518,10 @@ console-control-strings@^1.0.0, console-control-strings@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" +convert-source-map@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.1.tgz#b8278097b9bc229365de5c62cf5fcaed8b5599e5" + cookie@0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" @@ -612,6 +624,10 @@ dev-ip@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/dev-ip/-/dev-ip-1.0.1.tgz#a76a3ed1855be7a012bb8ac16cb80f3c00dc28f0" +diff@^3.2.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" + domino@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/domino/-/domino-2.0.1.tgz#9e1d63215d0fe8dcb8202bff07effa1a216db504" @@ -1203,6 +1219,12 @@ jasmine-core@~2.8.0: version "2.8.0" resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-2.8.0.tgz#bcc979ae1f9fd05701e45e52e65d3a5d63f1a24e" +jasmine-diff@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/jasmine-diff/-/jasmine-diff-0.1.3.tgz#93ccc2dcc41028c5ddd4606558074839f2deeaa8" + dependencies: + diff "^3.2.0" + jasmine@^2.5.3: version "2.8.0" resolved "https://registry.yarnpkg.com/jasmine/-/jasmine-2.8.0.tgz#6b089c0a11576b1f16df11b80146d91d4e8b8a3e" @@ -1319,6 +1341,12 @@ lodash@^4.11.1, lodash@^4.5.1: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" +magic-string@^0.25.0: + version "0.25.0" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.0.tgz#1f3696f9931ff0a1ed4c132250529e19cad6759b" + dependencies: + sourcemap-codec "^1.4.1" + micromatch@2.3.11, micromatch@^2.1.5: version "2.3.11" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565" @@ -1835,7 +1863,7 @@ rx@4.1.0: resolved "https://registry.yarnpkg.com/rx/-/rx-4.1.0.tgz#a5f13ff79ef3b740fe30aa803fb09f98805d4782" "rxjs@file:../../node_modules/rxjs": - version "6.0.0-alpha.4" + version "6.0.0" dependencies: tslib "^1.9.0" @@ -2016,10 +2044,14 @@ source-map@^0.5.1, source-map@^0.5.6: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" -source-map@^0.6.0: +source-map@^0.6.0, source-map@^0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" +sourcemap-codec@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.1.tgz#c8fd92d91889e902a07aee392bdd2c5863958ba2" + spawn-command@^0.0.2-1: version "0.0.2" resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2.tgz#9544e1a43ca045f8531aac1a48cb29bdae62338e" @@ -2180,10 +2212,11 @@ tree-kill@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.0.tgz#5846786237b4239014f05db156b643212d4c6f36" -tsickle@^0.27.2: - version "0.27.2" - resolved "https://registry.yarnpkg.com/tsickle/-/tsickle-0.27.2.tgz#f33d46d046f73dd5c155a37922e422816e878736" +tsickle@^0.32.1: + version "0.32.1" + resolved "https://registry.yarnpkg.com/tsickle/-/tsickle-0.32.1.tgz#f16e94ba80b32fc9ebe320dc94fbc2ca7f3521a5" dependencies: + jasmine-diff "^0.1.3" minimist "^1.2.0" mkdirp "^0.5.1" source-map "^0.6.0" @@ -2204,7 +2237,7 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0: resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" "typescript@file:../../node_modules/typescript": - version "2.7.2" + version "2.9.2" ua-parser-js@0.7.12: version "0.7.12" @@ -2424,4 +2457,4 @@ yeast@0.1.2: resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419" "zone.js@file:../../node_modules/zone.js": - version "0.8.20" + version "0.8.26" diff --git a/packages/compiler/src/lifecycle_reflector.ts b/packages/compiler/src/lifecycle_reflector.ts index 738e699176..769a350455 100644 --- a/packages/compiler/src/lifecycle_reflector.ts +++ b/packages/compiler/src/lifecycle_reflector.ts @@ -52,5 +52,12 @@ function getHookName(hook: LifecycleHooks): string { return 'ngAfterViewInit'; case LifecycleHooks.AfterViewChecked: return 'ngAfterViewChecked'; + default: + // This default case is not needed by TypeScript compiler, as the switch is exhaustive. + // However Closure Compiler does not understand that and reports an error in typed mode. + // The `throw new Error` below works around the problem, and the unexpected: never variable + // makes sure tsc still checks this code is unreachable. + const unexpected: never = hook; + throw new Error(`unexpected ${unexpected}`); } } diff --git a/packages/compiler/src/metadata_resolver.ts b/packages/compiler/src/metadata_resolver.ts index d43556f56d..af1c0f1a9d 100644 --- a/packages/compiler/src/metadata_resolver.ts +++ b/packages/compiler/src/metadata_resolver.ts @@ -115,16 +115,13 @@ export class CompileMetadataResolver { return this.getGeneratedClass(dirType, cpl.hostViewClassName(dirType)); } - getHostComponentType(dirType: any): StaticSymbol|Type { + getHostComponentType(dirType: any): StaticSymbol|cpl.ProxyClass { const name = `${cpl.identifierName({reference: dirType})}_Host`; if (dirType instanceof StaticSymbol) { return this._staticSymbolCache.get(dirType.filePath, name); - } else { - const HostClass = function HostClass() {}; - HostClass.overriddenName = name; - - return HostClass; } + + return this._createProxyClass(dirType, name); } private getRendererType(dirType: any): StaticSymbol|object { diff --git a/packages/compiler/src/view_compiler/view_compiler.ts b/packages/compiler/src/view_compiler/view_compiler.ts index 47299e6541..3fbe01806f 100644 --- a/packages/compiler/src/view_compiler/view_compiler.ts +++ b/packages/compiler/src/view_compiler/view_compiler.ts @@ -940,7 +940,8 @@ function needsAdditionalRootNode(astNodes: TemplateAst[]): boolean { function elementBindingDef(inputAst: BoundElementPropertyAst, dirAst: DirectiveAst): o.Expression { - switch (inputAst.type) { + const inputType = inputAst.type; + switch (inputType) { case PropertyBindingType.Attribute: return o.literalArr([ o.literal(BindingFlags.TypeElementAttribute), o.literal(inputAst.name), @@ -965,6 +966,13 @@ function elementBindingDef(inputAst: BoundElementPropertyAst, dirAst: DirectiveA return o.literalArr([ o.literal(BindingFlags.TypeElementStyle), o.literal(inputAst.name), o.literal(inputAst.unit) ]); + default: + // This default case is not needed by TypeScript compiler, as the switch is exhaustive. + // However Closure Compiler does not understand that and reports an error in typed mode. + // The `throw new Error` below works around the problem, and the unexpected: never variable + // makes sure tsc still checks this code is unreachable. + const unexpected: never = inputType; + throw new Error(`unexpected ${unexpected}`); } } diff --git a/packages/core/src/core_render3_private_export.ts b/packages/core/src/core_render3_private_export.ts index c520218a8a..53a1cf3c5e 100644 --- a/packages/core/src/core_render3_private_export.ts +++ b/packages/core/src/core_render3_private_export.ts @@ -15,6 +15,8 @@ export { detectChanges as ɵdetectChanges, renderComponent as ɵrenderComponent, ComponentType as ɵComponentType, + ComponentFactory as ɵRender3ComponentFactory, + ComponentRef as ɵRender3ComponentRef, DirectiveType as ɵDirectiveType, RenderFlags as ɵRenderFlags, directiveInject as ɵdirectiveInject, @@ -29,6 +31,7 @@ export { InheritDefinitionFeature as ɵInheritDefinitionFeature, NgOnChangesFeature as ɵNgOnChangesFeature, NgModuleType as ɵNgModuleType, + NgModuleRef as ɵRender3NgModuleRef, CssSelectorList as ɵCssSelectorList, markDirty as ɵmarkDirty, NgModuleFactory as ɵNgModuleFactory, @@ -95,6 +98,7 @@ export { ld as ɵld, Pp as ɵPp, ComponentDef as ɵComponentDef, + ComponentDefInternal as ɵComponentDefInternal, DirectiveDef as ɵDirectiveDef, PipeDef as ɵPipeDef, whenRendered as ɵwhenRendered, @@ -112,14 +116,38 @@ export { iM as ɵiM, I18nInstruction as ɵI18nInstruction, I18nExpInstruction as ɵI18nExpInstruction, + WRAP_RENDERER_FACTORY2 as ɵWRAP_RENDERER_FACTORY2, + Render3DebugRendererFactory2 as ɵRender3DebugRendererFactory2, } from './render3/index'; -export {NgModuleDef as ɵNgModuleDef} from './metadata/ng_module'; + + +export { + compileNgModuleDefs as ɵcompileNgModuleDefs, + patchComponentDefWithScope as ɵpatchComponentDefWithScope, +} from './render3/jit/module'; + +export { + compileComponent as ɵcompileComponent, + compileDirective as ɵcompileDirective, +} from './render3/jit/directive'; + +export { + compilePipe as ɵcompilePipe, +} from './render3/jit/pipe'; + +export { + NgModuleDef as ɵNgModuleDef, + NgModuleDefInternal as ɵNgModuleDefInternal, + NgModuleTransitiveScopes as ɵNgModuleTransitiveScopes, +} from './metadata/ng_module'; + export { sanitizeHtml as ɵsanitizeHtml, sanitizeStyle as ɵsanitizeStyle, sanitizeUrl as ɵsanitizeUrl, sanitizeResourceUrl as ɵsanitizeResourceUrl, } from './sanitization/sanitization'; + export { bypassSanitizationTrustHtml as ɵbypassSanitizationTrustHtml, bypassSanitizationTrustStyle as ɵbypassSanitizationTrustStyle, @@ -127,4 +155,4 @@ export { bypassSanitizationTrustUrl as ɵbypassSanitizationTrustUrl, bypassSanitizationTrustResourceUrl as ɵbypassSanitizationTrustResourceUrl, } from './sanitization/bypass'; -// clang-format on +// clang-format on \ No newline at end of file diff --git a/packages/core/src/render3/component_ref.ts b/packages/core/src/render3/component_ref.ts index 34c0b98e15..4b6a52ee60 100644 --- a/packages/core/src/render3/component_ref.ts +++ b/packages/core/src/render3/component_ref.ts @@ -6,12 +6,12 @@ * found in the LICENSE file at https://angular.io/license */ -import {ChangeDetectorRef} from '../change_detection/change_detector_ref'; +import {ChangeDetectorRef as ViewEngine_ChangeDetectorRef} from '../change_detection/change_detector_ref'; import {InjectionToken} from '../di/injection_token'; import {Injector, inject} from '../di/injector'; import {ComponentFactory as viewEngine_ComponentFactory, ComponentRef as viewEngine_ComponentRef} from '../linker/component_factory'; import {ComponentFactoryResolver as viewEngine_ComponentFactoryResolver} from '../linker/component_factory_resolver'; -import {ElementRef} from '../linker/element_ref'; +import {ElementRef as viewEngine_ElementRef} from '../linker/element_ref'; import {NgModuleRef as viewEngine_NgModuleRef} from '../linker/ng_module_factory'; import {RendererFactory2} from '../render/api'; import {Type} from '../type'; @@ -21,7 +21,7 @@ import {LifecycleHooksFeature, createRootContext} from './component'; import {baseDirectiveCreate, createLNode, createLViewData, createTView, elementCreate, enterView, hostElement, initChangeDetectorIfExisting, locateHostElement, renderEmbeddedTemplate} from './instructions'; import {ComponentDefInternal, ComponentType, RenderFlags} from './interfaces/definition'; import {LElementNode, TNode, TNodeType} from './interfaces/node'; -import {RElement, domRendererFactory3} from './interfaces/renderer'; +import {RElement, RendererFactory3, domRendererFactory3} from './interfaces/renderer'; import {CONTEXT, FLAGS, INJECTOR, LViewData, LViewFlags, RootContext, TVIEW} from './interfaces/view'; import {RootViewRef, ViewRef} from './view_ref'; @@ -55,8 +55,20 @@ export const ROOT_CONTEXT = new InjectionToken( * A change detection scheduler token for {@link RootContext}. This token is the default value used * for the default `RootContext` found in the {@link ROOT_CONTEXT} token. */ -export const SCHEDULER = new InjectionToken<((fn: () => void) => void)>( - 'SCHEDULER_TOKEN', {providedIn: 'root', factory: () => requestAnimationFrame.bind(window)}); +export const SCHEDULER = new InjectionToken<((fn: () => void) => void)>('SCHEDULER_TOKEN', { + providedIn: 'root', + factory: () => { + const useRaf = typeof requestAnimationFrame !== 'undefined' && typeof window !== 'undefined'; + return useRaf ? requestAnimationFrame.bind(window) : setTimeout; + }, +}); + +/** + * A function used to wrap the `RendererFactory2`. + * Used in tests to change the `RendererFactory2` into a `DebugRendererFactory2`. + */ +export const WRAP_RENDERER_FACTORY2 = + new InjectionToken<(rf: RendererFactory2) => RendererFactory2>('WRAP_RENDERER_FACTORY2'); /** * Render3 implementation of {@link viewEngine_ComponentFactory}. @@ -65,9 +77,11 @@ export class ComponentFactory extends viewEngine_ComponentFactory { selector: string; componentType: Type; ngContentSelectors: string[]; + get inputs(): {propName: string; templateName: string;}[] { return toRefArray(this.componentDef.inputs); } + get outputs(): {propName: string; templateName: string;}[] { return toRefArray(this.componentDef.outputs); } @@ -84,8 +98,15 @@ export class ComponentFactory extends viewEngine_ComponentFactory { ngModule?: viewEngine_NgModuleRef|undefined): viewEngine_ComponentRef { const isInternalRootView = rootSelectorOrNode === undefined; - const rendererFactory = - ngModule ? ngModule.injector.get(RendererFactory2) : domRendererFactory3; + let rendererFactory: RendererFactory3; + + if (ngModule) { + const wrapper = ngModule.injector.get(WRAP_RENDERER_FACTORY2, (v: RendererFactory2) => v); + rendererFactory = wrapper(ngModule.injector.get(RendererFactory2)) as RendererFactory3; + } else { + rendererFactory = domRendererFactory3; + } + const hostNode = isInternalRootView ? elementCreate(this.selector, rendererFactory.createRenderer(null, this.componentDef)) : locateHostElement(rendererFactory, rootSelectorOrNode); @@ -116,11 +137,10 @@ export class ComponentFactory extends viewEngine_ComponentFactory { elementNode = hostElement(componentTag, hostNode, this.componentDef); // Create directive instance with factory() and store at index 0 in directives array - rootContext.components.push( - component = baseDirectiveCreate(0, this.componentDef.factory(), this.componentDef) as T); + component = baseDirectiveCreate(0, this.componentDef.factory(), this.componentDef); + rootContext.components.push(component); initChangeDetectorIfExisting(elementNode.nodeInjector, component, elementNode.data !); (elementNode.data as LViewData)[CONTEXT] = component; - // TODO: should LifecycleHooksFeature and other host features be generated by the compiler and // executed here? // Angular 5 reference: https://stackblitz.com/edit/lifecycle-hooks-vcref @@ -177,11 +197,11 @@ export class ComponentFactory extends viewEngine_ComponentFactory { */ export class ComponentRef extends viewEngine_ComponentRef { destroyCbs: (() => void)[]|null = []; - location: ElementRef; + location: viewEngine_ElementRef; injector: Injector; instance: T; hostView: ViewRef; - changeDetectorRef: ChangeDetectorRef; + changeDetectorRef: ViewEngine_ChangeDetectorRef; componentType: Type; constructor( @@ -192,7 +212,7 @@ export class ComponentRef extends viewEngine_ComponentRef { this.hostView = this.changeDetectorRef = new RootViewRef(rootView); this.hostView._lViewNode = createLNode(-1, TNodeType.View, null, null, null, rootView); this.injector = injector; - this.location = new ElementRef(hostNode); + this.location = new viewEngine_ElementRef(hostNode); this.componentType = componentType; } @@ -205,4 +225,4 @@ export class ComponentRef extends viewEngine_ComponentRef { ngDevMode && assertDefined(this.destroyCbs, 'NgModule already destroyed'); this.destroyCbs !.push(callback); } -} +} \ No newline at end of file diff --git a/packages/core/src/render3/debug.ts b/packages/core/src/render3/debug.ts new file mode 100644 index 0000000000..aa0a29f10b --- /dev/null +++ b/packages/core/src/render3/debug.ts @@ -0,0 +1,115 @@ +/** + * @license + * Copyright Google Inc. 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} from '../di/injector'; +import {Renderer2, RendererType2} from '../render/api'; +import {DebugContext} from '../view'; +import {DebugRenderer2, DebugRendererFactory2} from '../view/services'; + +import * as di from './di'; +import {NG_HOST_SYMBOL, _getViewData} from './instructions'; +import {LElementNode} from './interfaces/node'; +import {CONTEXT, DIRECTIVES, LViewData, TVIEW} from './interfaces/view'; + +/** + * Adapts the DebugRendererFactory2 to create a DebugRenderer2 specific for IVY. + * + * The created DebugRenderer know how to create a Debug Context specific to IVY. + */ +export class Render3DebugRendererFactory2 extends DebugRendererFactory2 { + createRenderer(element: any, renderData: RendererType2|null): Renderer2 { + const renderer = super.createRenderer(element, renderData) as DebugRenderer2; + renderer.debugContextFactory = () => new Render3DebugContext(_getViewData()); + return renderer; + } +} + +/** + * Stores context information about view nodes. + * + * Used in tests to retrieve information those nodes. + */ +class Render3DebugContext implements DebugContext { + readonly nodeIndex: number|null; + + constructor(private viewData: LViewData) { + // The LNode will be created next and appended to viewData + this.nodeIndex = viewData ? viewData.length : null; + } + + get view(): any { return this.viewData; } + + get injector(): Injector { + if (this.nodeIndex !== null) { + const lElementNode: LElementNode = this.view[this.nodeIndex]; + const nodeInjector = lElementNode.nodeInjector; + + if (nodeInjector) { + return new di.NodeInjector(nodeInjector); + } + } + return Injector.NULL; + } + + get component(): any { + // TODO(vicb): why/when + if (this.nodeIndex === null) { + return null; + } + + const tView = this.view[TVIEW]; + const components: number[]|null = tView.components; + + return (components && components.indexOf(this.nodeIndex) == -1) ? + null : + this.view[this.nodeIndex].data[CONTEXT]; + } + + // TODO(vicb): add view providers when supported + get providerTokens(): any[] { + const matchedDirectives: any[] = []; + + // TODO(vicb): why/when + if (this.nodeIndex === null) { + return matchedDirectives; + } + + const directives = this.view[DIRECTIVES]; + + if (directives) { + const currentNode = this.view[this.nodeIndex]; + for (let dirIndex = 0; dirIndex < directives.length; dirIndex++) { + const directive = directives[dirIndex]; + if (directive[NG_HOST_SYMBOL] === currentNode) { + matchedDirectives.push(directive.constructor); + } + } + } + return matchedDirectives; + } + + get references(): {[key: string]: any} { + // TODO(vicb): implement retrieving references + throw new Error('Not implemented yet in ivy'); + } + + get context(): any { + if (this.nodeIndex === null) { + return null; + } + const lNode = this.view[this.nodeIndex]; + return lNode.view[CONTEXT]; + } + + get componentRenderElement(): any { throw new Error('Not implemented in ivy'); } + + get renderNode(): any { throw new Error('Not implemented in ivy'); } + + // TODO(vicb): check previous implementation + logError(console: Console, ...values: any[]): void { console.error(...values); } +} \ No newline at end of file diff --git a/packages/core/src/render3/di.ts b/packages/core/src/render3/di.ts index 4fbefe234e..a539e0972b 100644 --- a/packages/core/src/render3/di.ts +++ b/packages/core/src/render3/di.ts @@ -605,7 +605,7 @@ export function getOrCreateContainerRef(di: LInjector): viewEngine.ViewContainer return di.viewContainerRef; } -class NodeInjector implements Injector { +export class NodeInjector implements Injector { constructor(private _lInjector: LInjector) {} get(token: any): any { diff --git a/packages/core/src/render3/features/ng_onchanges_feature.ts b/packages/core/src/render3/features/ng_onchanges_feature.ts index 09259a9eb5..75b4147f8f 100644 --- a/packages/core/src/render3/features/ng_onchanges_feature.ts +++ b/packages/core/src/render3/features/ng_onchanges_feature.ts @@ -89,7 +89,9 @@ export function NgOnChangesFeature(definition: DirectiveDefInternal): void } if (setter) setter.call(this, value); - } + }, + // Make the property configurable in dev mode to allow overriding in tests + configurable: !!ngDevMode }); } } diff --git a/packages/core/src/render3/index.ts b/packages/core/src/render3/index.ts index 27b380c78c..59ac8becd1 100644 --- a/packages/core/src/render3/index.ts +++ b/packages/core/src/render3/index.ts @@ -13,13 +13,12 @@ import {NgOnChangesFeature} from './features/ng_onchanges_feature'; import {PublicFeature} from './features/public_feature'; import {ComponentDef, ComponentDefInternal, ComponentTemplate, ComponentType, DirectiveDef, DirectiveDefFlags, DirectiveDefInternal, DirectiveType, PipeDef} from './interfaces/definition'; -export {ComponentFactory, ComponentFactoryResolver, ComponentRef} from './component_ref'; +export {ComponentFactory, ComponentFactoryResolver, ComponentRef, WRAP_RENDERER_FACTORY2} from './component_ref'; +export {Render3DebugRendererFactory2} from './debug'; export {QUERY_READ_CONTAINER_REF, QUERY_READ_ELEMENT_REF, QUERY_READ_FROM_NODE, QUERY_READ_TEMPLATE_REF, directiveInject, getFactoryOf, getInheritedFactory, injectAttribute, injectChangeDetectorRef, injectComponentFactoryResolver, injectElementRef, injectTemplateRef, injectViewContainerRef} from './di'; export {RenderFlags} from './interfaces/definition'; export {CssSelectorList} from './interfaces/projection'; - - // Naming scheme: // - Capital letters are for creating things: T(Text), E(Element), D(Directive), V(View), // C(Container), L(Listener) diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index ee19081c1c..ab2ee5d083 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -30,8 +30,6 @@ import {StylingContext, allocStylingContext, createStylingContextTemplate, rende import {assertDataInRangeInternal, isDifferent, loadElementInternal, loadInternal, stringify} from './util'; import {ViewRef} from './view_ref'; - - /** * Directive (D) sets a property on all component instances using this constant as a key and the * component's host node (LElement) as the value. This is used in methods like detectChanges to @@ -126,16 +124,6 @@ export function getCurrentView(): OpaqueViewState { return viewData as any as OpaqueViewState; } -/** - * Internal function that returns the current LViewData instance. - * - * The getCurrentView() instruction should be used for anything public. - */ -export function _getViewData(): LViewData { - // top level variables should not be exported for performance reasons (PERF_NOTES.md) - return viewData; -} - /** * Restores `contextViewData` to the given OpaqueViewState instance. * @@ -207,6 +195,16 @@ export function getCreationMode(): boolean { */ let viewData: LViewData; +/** + * Internal function that returns the current LViewData instance. + * + * The getCurrentView() instruction should be used for anything public. + */ +export function _getViewData(): LViewData { + // top level variables should not be exported for performance reasons (PERF_NOTES.md) + return viewData; +} + /** * The last viewData retrieved by nextContext(). * Allows building nextContext() and reference() calls. diff --git a/packages/core/src/render3/jit/directive.ts b/packages/core/src/render3/jit/directive.ts index 210784fb11..8b7f3444fb 100644 --- a/packages/core/src/render3/jit/directive.ts +++ b/packages/core/src/render3/jit/directive.ts @@ -10,13 +10,12 @@ import {ConstantPool, R3DirectiveMetadata, WrappedNodeExpr, compileComponentFrom import {Component, Directive, HostBinding, HostListener, Input, Output} from '../../metadata/directives'; import {componentNeedsResolution, maybeQueueResolutionOfComponentResources} from '../../metadata/resource_loading'; -import {ReflectionCapabilities} from '../../reflection/reflection_capabilities'; import {Type} from '../../type'; import {stringify} from '../../util'; import {angularCoreEnv} from './environment'; import {NG_COMPONENT_DEF, NG_DIRECTIVE_DEF} from './fields'; -import {patchComponentDefWithScope} from './module'; +import {patchComponentDefWithScope, transitiveScopesFor} from './module'; import {getReflect, reflectDependencies} from './util'; type StringMap = { @@ -33,12 +32,12 @@ type StringMap = { * until the global queue has been resolved with a call to `resolveComponentResources`. */ export function compileComponent(type: Type, metadata: Component): void { - let def: any = null; + let ngComponentDef: any = null; // Metadata may have resources which need to be resolved. maybeQueueResolutionOfComponentResources(metadata); Object.defineProperty(type, NG_COMPONENT_DEF, { get: () => { - if (def === null) { + if (ngComponentDef === null) { if (componentNeedsResolution(metadata)) { const error = [`Component '${stringify(type)}' is not resolved:`]; if (metadata.templateUrl) { @@ -78,7 +77,7 @@ export function compileComponent(type: Type, metadata: Component): void { constantPool, makeBindingParser()); const preStatements = [...constantPool.statements, ...res.statements]; - def = jitExpression( + ngComponentDef = jitExpression( res.expression, angularCoreEnv, `ng://${type.name}/ngComponentDef.js`, preStatements); // If component compilation is async, then the @NgModule annotation which declares the @@ -86,11 +85,14 @@ export function compileComponent(type: Type, metadata: Component): void { // allows the component to patch itself with directiveDefs from the module after it finishes // compiling. if (hasSelectorScope(type)) { - patchComponentDefWithScope(def, type.ngSelectorScope); + const scopes = transitiveScopesFor(type.ngSelectorScope); + patchComponentDefWithScope(ngComponentDef, scopes); } } - return def; + return ngComponentDef; }, + // Make the property configurable in dev mode to allow overriding in tests + configurable: !!ngDevMode, }); } @@ -107,23 +109,24 @@ function hasSelectorScope(component: Type): component is Type& * will resolve when compilation completes and the directive becomes usable. */ export function compileDirective(type: Type, directive: Directive): void { - let def: any = null; + let ngDirectiveDef: any = null; Object.defineProperty(type, NG_DIRECTIVE_DEF, { get: () => { - if (def === null) { + if (ngDirectiveDef === null) { const constantPool = new ConstantPool(); const sourceMapUrl = `ng://${type && type.name}/ngDirectiveDef.js`; const res = compileR3Directive( directiveMetadata(type, directive), constantPool, makeBindingParser()); const preStatements = [...constantPool.statements, ...res.statements]; - def = jitExpression(res.expression, angularCoreEnv, sourceMapUrl, preStatements); + ngDirectiveDef = jitExpression(res.expression, angularCoreEnv, sourceMapUrl, preStatements); } - return def; + return ngDirectiveDef; }, + // Make the property configurable in dev mode to allow overriding in tests + configurable: !!ngDevMode, }); } - export function extendsDirectlyFromObject(type: Type): boolean { return Object.getPrototypeOf(type.prototype) === Object.prototype; } diff --git a/packages/core/src/render3/jit/module.ts b/packages/core/src/render3/jit/module.ts index 8f1eda973b..e432d81a7a 100644 --- a/packages/core/src/render3/jit/module.ts +++ b/packages/core/src/render3/jit/module.ts @@ -18,15 +18,28 @@ import {reflectDependencies} from './util'; const EMPTY_ARRAY: Type[] = []; -export function compileNgModule(type: Type, ngModule: NgModule): void { +/** + * Compiles a module in JIT mode. + * + * This function automatically gets called when a class has a `@NgModule` decorator. + */ +export function compileNgModule(moduleType: Type, ngModule: NgModule): void { + compileNgModuleDefs(moduleType, ngModule); + setScopeOnDeclaredComponents(moduleType, ngModule); +} + +/** + * Compiles and adds the `ngModuleDef` and `ngInjectorDef` properties to the module class. + */ +export function compileNgModuleDefs(moduleType: Type, ngModule: NgModule): void { const declarations: Type[] = flatten(ngModule.declarations || EMPTY_ARRAY); let ngModuleDef: any = null; - Object.defineProperty(type, NG_MODULE_DEF, { + Object.defineProperty(moduleType, NG_MODULE_DEF, { get: () => { if (ngModuleDef === null) { const meta: R3NgModuleMetadata = { - type: wrap(type), + type: wrap(moduleType), bootstrap: flatten(ngModule.bootstrap || EMPTY_ARRAY).map(wrap), declarations: declarations.map(wrapReference), imports: flatten(ngModule.imports || EMPTY_ARRAY) @@ -38,21 +51,23 @@ export function compileNgModule(type: Type, ngModule: NgModule): void { emitInline: true, }; const res = compileR3NgModule(meta); - ngModuleDef = - jitExpression(res.expression, angularCoreEnv, `ng://${type.name}/ngModuleDef.js`, []); + ngModuleDef = jitExpression( + res.expression, angularCoreEnv, `ng://${moduleType.name}/ngModuleDef.js`, []); } return ngModuleDef; }, + // Make the property configurable in dev mode to allow overriding in tests + configurable: !!ngDevMode, }); let ngInjectorDef: any = null; - Object.defineProperty(type, NG_INJECTOR_DEF, { + Object.defineProperty(moduleType, NG_INJECTOR_DEF, { get: () => { if (ngInjectorDef === null) { const meta: R3InjectorMetadata = { - name: type.name, - type: wrap(type), - deps: reflectDependencies(type), + name: moduleType.name, + type: wrap(moduleType), + deps: reflectDependencies(moduleType), providers: new WrappedNodeExpr(ngModule.providers || EMPTY_ARRAY), imports: new WrappedNodeExpr([ ngModule.imports || EMPTY_ARRAY, @@ -61,25 +76,36 @@ export function compileNgModule(type: Type, ngModule: NgModule): void { }; const res = compileInjector(meta); ngInjectorDef = jitExpression( - res.expression, angularCoreEnv, `ng://${type.name}/ngInjectorDef.js`, res.statements); + res.expression, angularCoreEnv, `ng://${moduleType.name}/ngInjectorDef.js`, + res.statements); } return ngInjectorDef; }, + // Make the property configurable in dev mode to allow overriding in tests + configurable: !!ngDevMode, }); +} + +/** + * Some declared components may be compiled asynchronously, and thus may not have their + * ngComponentDef set yet. If this is the case, then a reference to the module is written into + * the `ngSelectorScope` property of the declared type. + */ +function setScopeOnDeclaredComponents(moduleType: Type, ngModule: NgModule) { + const declarations: Type[] = flatten(ngModule.declarations || EMPTY_ARRAY); + + const transitiveScopes = transitiveScopesFor(moduleType); declarations.forEach(declaration => { - // Some declared components may be compiled asynchronously, and thus may not have their - // ngComponentDef set yet. If this is the case, then a reference to the module is written into - // the `ngSelectorScope` property of the declared type. if (declaration.hasOwnProperty(NG_COMPONENT_DEF)) { // An `ngComponentDef` field exists - go ahead and patch the component directly. - patchComponentDefWithScope( - (declaration as Type& {ngComponentDef: ComponentDefInternal}).ngComponentDef, - type); + const component = declaration as Type& {ngComponentDef: ComponentDefInternal}; + const componentDef = component.ngComponentDef; + patchComponentDefWithScope(componentDef, transitiveScopes); } else if ( !declaration.hasOwnProperty(NG_DIRECTIVE_DEF) && !declaration.hasOwnProperty(NG_PIPE_DEF)) { // Set `ngSelectorScope` for future reference when the component compilation finishes. - (declaration as Type& {ngSelectorScope?: any}).ngSelectorScope = type; + (declaration as Type& {ngSelectorScope?: any}).ngSelectorScope = moduleType; } }); } @@ -88,13 +114,13 @@ export function compileNgModule(type: Type, ngModule: NgModule): void { * Patch the definition of a component with directives and pipes from the compilation scope of * a given module. */ -export function patchComponentDefWithScope( - componentDef: ComponentDefInternal, module: Type) { - componentDef.directiveDefs = () => Array.from(transitiveScopesFor(module).compilation.directives) +export function patchComponentDefWithScope( + componentDef: ComponentDefInternal, transitiveScopes: NgModuleTransitiveScopes) { + componentDef.directiveDefs = () => Array.from(transitiveScopes.compilation.directives) .map(dir => dir.ngDirectiveDef || dir.ngComponentDef) .filter(def => !!def); componentDef.pipeDefs = () => - Array.from(transitiveScopesFor(module).compilation.pipes).map(pipe => pipe.ngPipeDef); + Array.from(transitiveScopes.compilation.pipes).map(pipe => pipe.ngPipeDef); } /** @@ -104,7 +130,7 @@ export function patchComponentDefWithScope( * on modules with components that have not fully compiled yet, but the result should not be used * until they have. */ -function transitiveScopesFor(moduleType: Type): NgModuleTransitiveScopes { +export function transitiveScopesFor(moduleType: Type): NgModuleTransitiveScopes { if (!isNgModule(moduleType)) { throw new Error(`${moduleType.name} does not have an ngModuleDef`); } diff --git a/packages/core/src/render3/jit/pipe.ts b/packages/core/src/render3/jit/pipe.ts index a086f50e10..f5f724817d 100644 --- a/packages/core/src/render3/jit/pipe.ts +++ b/packages/core/src/render3/jit/pipe.ts @@ -17,10 +17,10 @@ import {NG_PIPE_DEF} from './fields'; import {reflectDependencies} from './util'; export function compilePipe(type: Type, meta: Pipe): void { - let def: any = null; + let ngPipeDef: any = null; Object.defineProperty(type, NG_PIPE_DEF, { get: () => { - if (def === null) { + if (ngPipeDef === null) { const sourceMapUrl = `ng://${stringify(type)}/ngPipeDef.js`; const name = type.name; @@ -32,9 +32,11 @@ export function compilePipe(type: Type, meta: Pipe): void { pure: meta.pure !== undefined ? meta.pure : true, }); - def = jitExpression(res.expression, angularCoreEnv, sourceMapUrl, res.statements); + ngPipeDef = jitExpression(res.expression, angularCoreEnv, sourceMapUrl, res.statements); } - return def; - } + return ngPipeDef; + }, + // Make the property configurable in dev mode to allow overriding in tests + configurable: !!ngDevMode, }); } diff --git a/packages/core/src/render3/ng_dev_mode.ts b/packages/core/src/render3/ng_dev_mode.ts index 90f48c8237..cdf56d98b6 100644 --- a/packages/core/src/render3/ng_dev_mode.ts +++ b/packages/core/src/render3/ng_dev_mode.ts @@ -6,7 +6,6 @@ * found in the LICENSE file at https://angular.io/license */ - declare global { const ngDevMode: null|NgDevModePerfCounters; interface NgDevModePerfCounters { @@ -33,8 +32,6 @@ declare global { } } - - declare let global: any; // NOTE: The order here matters: Checking window, then global, then self is important. diff --git a/packages/core/src/view/services.ts b/packages/core/src/view/services.ts index 5a077aef70..c3eabf9ee7 100644 --- a/packages/core/src/view/services.ts +++ b/packages/core/src/view/services.ts @@ -663,8 +663,7 @@ export function getCurrentDebugContext(): DebugContext|null { return _currentView ? new DebugContext_(_currentView, _currentNodeIndex) : null; } - -class DebugRendererFactory2 implements RendererFactory2 { +export class DebugRendererFactory2 implements RendererFactory2 { constructor(private delegate: RendererFactory2) {} createRenderer(element: any, renderData: RendererType2|null): Renderer2 { @@ -690,9 +689,21 @@ class DebugRendererFactory2 implements RendererFactory2 { } } - -class DebugRenderer2 implements Renderer2 { +export class DebugRenderer2 implements Renderer2 { readonly data: {[key: string]: any}; + + /** + * Factory function used to create a `DebugContext` when a node is created. + * + * The `DebugContext` allows to retrieve information about the nodes that are useful in tests. + * + * The factory is configurable so that the `DebugRenderer2` could instantiate either a View Engine + * or a Render context. + */ + debugContextFactory: () => DebugContext | null = getCurrentDebugContext; + + private get debugContext() { return this.debugContextFactory(); } + constructor(private delegate: Renderer2) { this.data = this.delegate.data; } destroyNode(node: any) { @@ -706,7 +717,7 @@ class DebugRenderer2 implements Renderer2 { createElement(name: string, namespace?: string): any { const el = this.delegate.createElement(name, namespace); - const debugCtx = getCurrentDebugContext(); + const debugCtx = this.debugContext; if (debugCtx) { const debugEl = new DebugElement(el, null, debugCtx); debugEl.name = name; @@ -717,7 +728,7 @@ class DebugRenderer2 implements Renderer2 { createComment(value: string): any { const comment = this.delegate.createComment(value); - const debugCtx = getCurrentDebugContext(); + const debugCtx = this.debugContext; if (debugCtx) { indexDebugNode(new DebugNode(comment, null, debugCtx)); } @@ -726,7 +737,7 @@ class DebugRenderer2 implements Renderer2 { createText(value: string): any { const text = this.delegate.createText(value); - const debugCtx = getCurrentDebugContext(); + const debugCtx = this.debugContext; if (debugCtx) { indexDebugNode(new DebugNode(text, null, debugCtx)); } @@ -764,7 +775,7 @@ class DebugRenderer2 implements Renderer2 { selectRootElement(selectorOrNode: string|any): any { const el = this.delegate.selectRootElement(selectorOrNode); - const debugCtx = getCurrentDebugContext(); + const debugCtx = this.debugContext; if (debugCtx) { indexDebugNode(new DebugElement(el, null, debugCtx)); } diff --git a/packages/core/test/application_ref_integration_spec.ts b/packages/core/test/application_ref_integration_spec.ts index cc13822f5c..5b6eb9df3e 100644 --- a/packages/core/test/application_ref_integration_spec.ts +++ b/packages/core/test/application_ref_integration_spec.ts @@ -6,44 +6,33 @@ * found in the LICENSE file at https://angular.io/license */ -import {ApplicationModule, ApplicationRef, DoCheck, InjectFlags, InjectorType, Input, OnInit, PlatformRef, TestabilityRegistry, Type, defineInjector, inject, ɵE as elementStart, ɵNgModuleDef as NgModuleDef, ɵRenderFlags as RenderFlags, ɵT as text, ɵdefineComponent as defineComponent, ɵe as elementEnd, ɵi1 as interpolation1, ɵt as textBinding} from '@angular/core'; +import {ApplicationRef, Component, DoCheck, NgModule, OnInit, TestabilityRegistry, ɵivyEnabled as ivyEnabled} from '@angular/core'; import {getTestBed} from '@angular/core/testing'; -import {BrowserModule, EVENT_MANAGER_PLUGINS, platformBrowser} from '@angular/platform-browser'; +import {BrowserModule} from '@angular/platform-browser'; import {withBody} from '@angular/private/testing'; -import {BROWSER_MODULE_PROVIDERS} from '../../platform-browser/src/browser'; -import {APPLICATION_MODULE_PROVIDERS} from '../src/application_module'; import {NgModuleFactory} from '../src/render3/ng_module_ref'; -describe('ApplicationRef bootstrap', () => { - class HelloWorldComponent implements OnInit, DoCheck { +ivyEnabled && describe('ApplicationRef bootstrap', () => { + @Component({ + selector: 'hello-world', + template: '
Hello {{ name }}
', + }) + class HelloWorldComponent implements OnInit, + DoCheck { log: string[] = []; name = 'World'; - static ngComponentDef = defineComponent({ - type: HelloWorldComponent, - selectors: [['hello-world']], - factory: () => new HelloWorldComponent(), - template: function(rf: RenderFlags, ctx: HelloWorldComponent): void { - if (rf & RenderFlags.Create) { - elementStart(0, 'div'); - text(1); - elementEnd(); - } - if (rf & RenderFlags.Update) { - textBinding(1, interpolation1('Hello ', ctx.name, '')); - } - } - }); ngOnInit(): void { this.log.push('OnInit'); } - ngDoCheck(): void { this.log.push('DoCheck'); } } + @NgModule({ + declarations: [HelloWorldComponent], + bootstrap: [HelloWorldComponent], + imports: [BrowserModule], + }) class MyAppModule { - static ngInjectorDef = - defineInjector({factory: () => new MyAppModule(), imports: [BrowserModule]}); - static ngModuleDef = defineNgModule({bootstrap: [HelloWorldComponent]}); } it('should bootstrap hello world', withBody('', async() => { @@ -66,29 +55,3 @@ describe('ApplicationRef bootstrap', () => { })); }); - -///////////////////////////////////////////////////////// - -// These go away when Compiler is ready - -(BrowserModule as any as InjectorType).ngInjectorDef = defineInjector({ - factory: function BrowserModule_Factory() { - return new BrowserModule(inject(BrowserModule, InjectFlags.Optional | InjectFlags.SkipSelf)); - }, - imports: [ApplicationModule], - providers: BROWSER_MODULE_PROVIDERS -}); - -(ApplicationModule as any as InjectorType).ngInjectorDef = defineInjector({ - factory: function ApplicationModule_Factory() { - return new ApplicationModule(inject(ApplicationRef)); - }, - providers: APPLICATION_MODULE_PROVIDERS -}); - -export function defineNgModule({bootstrap}: {bootstrap?: Type[]}): - NgModuleDef { - return ({ bootstrap: bootstrap || [], } as any); -} - -///////////////////////////////////////////////////////// diff --git a/packages/core/test/test_bed_spec.ts b/packages/core/test/test_bed_spec.ts new file mode 100644 index 0000000000..fa6671668b --- /dev/null +++ b/packages/core/test/test_bed_spec.ts @@ -0,0 +1,149 @@ +/** + * @license + * Copyright Google Inc. 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, Inject, InjectionToken, NgModule, Optional} from '@angular/core'; +import {TestBed, getTestBed} from '@angular/core/testing/src/test_bed'; +import {By} from '@angular/platform-browser'; +import {expect} from '@angular/platform-browser/testing/src/matchers'; + +const NAME = new InjectionToken('name'); + +// -- module: HWModule +@Component({ + selector: 'hello-world', + template: '', +}) +export class HelloWorld { +} + +// -- module: Greeting +@Component({ + selector: 'greeting-cmp', + template: 'Hello {{ name }}', +}) +export class GreetingCmp { + name: string; + + constructor(@Inject(NAME) @Optional() name: string) { this.name = name || 'nobody!'; } +} + +@NgModule({ + declarations: [GreetingCmp], + exports: [GreetingCmp], +}) +export class GreetingModule { +} + +@Component({selector: 'simple-cmp', template: 'simple'}) +export class SimpleCmp { +} + +@NgModule({ + declarations: [HelloWorld, SimpleCmp], + imports: [GreetingModule], + providers: [ + {provide: NAME, useValue: 'World!'}, + ] +}) +export class HelloWorldModule { +} + +describe('TestBed', () => { + beforeEach(() => { + getTestBed().resetTestingModule(); + TestBed.configureTestingModule({imports: [HelloWorldModule]}); + }); + + it('should compile and render a component', () => { + const hello = TestBed.createComponent(HelloWorld); + hello.detectChanges(); + + expect(hello.nativeElement).toHaveText('Hello World!'); + }); + + it('should give access to the component instance', () => { + const hello = TestBed.createComponent(HelloWorld); + hello.detectChanges(); + + expect(hello.componentInstance).toBeAnInstanceOf(HelloWorld); + }); + + it('should give the ability to query by css', () => { + const hello = TestBed.createComponent(HelloWorld); + hello.detectChanges(); + + const greetingByCss = hello.debugElement.query(By.css('greeting-cmp')); + expect(greetingByCss.nativeElement).toHaveText('Hello World!'); + expect(greetingByCss.componentInstance).toBeAnInstanceOf(GreetingCmp); + }); + + it('should give the ability to trigger the change detection', () => { + const hello = TestBed.createComponent(HelloWorld); + + hello.detectChanges(); + const greetingByCss = hello.debugElement.query(By.css('greeting-cmp')); + expect(greetingByCss.nativeElement).toHaveText('Hello World!'); + + greetingByCss.componentInstance.name = 'TestBed!'; + hello.detectChanges(); + expect(greetingByCss.nativeElement).toHaveText('Hello TestBed!'); + }); + + + it('should give access to the node injector', () => { + const hello = TestBed.createComponent(HelloWorld); + hello.detectChanges(); + const injector = hello.debugElement.query(By.css('greeting-cmp')).injector; + + // from the node injector + const helloInjected = injector.get(HelloWorld); + expect(helloInjected).toBe(hello.componentInstance); + + // from the module injector + const nameInjected = injector.get(NAME); + expect(nameInjected).toEqual('World!'); + }); + + it('should give the ability to query by directive', () => { + const hello = TestBed.createComponent(HelloWorld); + hello.detectChanges(); + + const greetingByDirective = hello.debugElement.query(By.directive(GreetingCmp)); + expect(greetingByDirective.componentInstance).toBeAnInstanceOf(GreetingCmp); + }); + + + it('allow to override a template', () => { + // use original template when there is no override + let hello = TestBed.createComponent(HelloWorld); + hello.detectChanges(); + expect(hello.nativeElement).toHaveText('Hello World!'); + + // override the template + getTestBed().resetTestingModule(); + TestBed.configureTestingModule({imports: [HelloWorldModule]}); + TestBed.overrideComponent(GreetingCmp, {set: {template: `Bonjour {{ name }}`}}); + hello = TestBed.createComponent(HelloWorld); + hello.detectChanges(); + expect(hello.nativeElement).toHaveText('Bonjour World!'); + + // restore the original template by calling `.resetTestingModule()` + getTestBed().resetTestingModule(); + TestBed.configureTestingModule({imports: [HelloWorldModule]}); + hello = TestBed.createComponent(HelloWorld); + hello.detectChanges(); + expect(hello.nativeElement).toHaveText('Hello World!'); + }); + + it('allow to override a provider', () => { + TestBed.overrideProvider(NAME, {useValue: 'injected World !'}); + const hello = TestBed.createComponent(HelloWorld); + hello.detectChanges(); + expect(hello.nativeElement).toHaveText('Hello injected World !'); + }); +}); diff --git a/packages/core/testing/rollup.config.js b/packages/core/testing/rollup.config.js index 55cb635adb..7166f4aa49 100644 --- a/packages/core/testing/rollup.config.js +++ b/packages/core/testing/rollup.config.js @@ -11,6 +11,7 @@ const sourcemaps = require('rollup-plugin-sourcemaps'); const globals = { '@angular/core': 'ng.core', + '@angular/compiler': 'ng.compiler', 'rxjs': 'rxjs', }; diff --git a/packages/core/testing/src/metadata_overrider.ts b/packages/core/testing/src/metadata_overrider.ts new file mode 100644 index 0000000000..a2c6581889 --- /dev/null +++ b/packages/core/testing/src/metadata_overrider.ts @@ -0,0 +1,131 @@ +/** + * @license + * Copyright Google Inc. 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 {ɵstringify as stringify} from '@angular/core'; +import {MetadataOverride} from './metadata_override'; + +type StringMap = { + [key: string]: any +}; + +let _nextReferenceId = 0; + +export class MetadataOverrider { + private _references = new Map(); + /** + * Creates a new instance for the given metadata class + * based on an old instance and overrides. + */ + overrideMetadata( + metadataClass: {new (options: T): C;}, oldMetadata: C, override: MetadataOverride): C { + const props: StringMap = {}; + if (oldMetadata) { + _valueProps(oldMetadata).forEach((prop) => props[prop] = (oldMetadata)[prop]); + } + + if (override.set) { + if (override.remove || override.add) { + throw new Error(`Cannot set and add/remove ${stringify(metadataClass)} at the same time!`); + } + setMetadata(props, override.set); + } + if (override.remove) { + removeMetadata(props, override.remove, this._references); + } + if (override.add) { + addMetadata(props, override.add); + } + return new metadataClass(props); + } +} + +function removeMetadata(metadata: StringMap, remove: any, references: Map) { + const removeObjects = new Set(); + for (const prop in remove) { + const removeValue = remove[prop]; + if (removeValue instanceof Array) { + removeValue.forEach( + (value: any) => { removeObjects.add(_propHashKey(prop, value, references)); }); + } else { + removeObjects.add(_propHashKey(prop, removeValue, references)); + } + } + + for (const prop in metadata) { + const propValue = metadata[prop]; + if (propValue instanceof Array) { + metadata[prop] = propValue.filter( + (value: any) => !removeObjects.has(_propHashKey(prop, value, references))); + } else { + if (removeObjects.has(_propHashKey(prop, propValue, references))) { + metadata[prop] = undefined; + } + } + } +} + +function addMetadata(metadata: StringMap, add: any) { + for (const prop in add) { + const addValue = add[prop]; + const propValue = metadata[prop]; + if (propValue != null && propValue instanceof Array) { + metadata[prop] = propValue.concat(addValue); + } else { + metadata[prop] = addValue; + } + } +} + +function setMetadata(metadata: StringMap, set: any) { + for (const prop in set) { + metadata[prop] = set[prop]; + } +} + +function _propHashKey(propName: any, propValue: any, references: Map): string { + const replacer = (key: any, value: any) => { + if (typeof value === 'function') { + value = _serializeReference(value, references); + } + return value; + }; + + return `${propName}:${JSON.stringify(propValue, replacer)}`; +} + +function _serializeReference(ref: any, references: Map): string { + let id = references.get(ref); + if (!id) { + id = `${stringify(ref)}${_nextReferenceId++}`; + references.set(ref, id); + } + return id; +} + + +function _valueProps(obj: any): string[] { + const props: string[] = []; + // regular public props + Object.keys(obj).forEach((prop) => { + if (!prop.startsWith('_')) { + props.push(prop); + } + }); + + // getters + let proto = obj; + while (proto = Object.getPrototypeOf(proto)) { + Object.keys(proto).forEach((protoProp) => { + const desc = Object.getOwnPropertyDescriptor(proto, protoProp); + if (!protoProp.startsWith('_') && desc && 'get' in desc) { + props.push(protoProp); + } + }); + } + return props; +} diff --git a/packages/core/testing/src/r3_test_bed.ts b/packages/core/testing/src/r3_test_bed.ts new file mode 100644 index 0000000000..2ae720fa84 --- /dev/null +++ b/packages/core/testing/src/r3_test_bed.ts @@ -0,0 +1,604 @@ +/** + * @license + * Copyright Google Inc. 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, NgModule, Pipe, PlatformRef, Provider, RendererFactory2, SchemaMetadata, Type, ɵNgModuleDefInternal as NgModuleDefInternal, ɵNgModuleTransitiveScopes as NgModuleTransitiveScopes, ɵRender3ComponentFactory as ComponentFactory, ɵRender3DebugRendererFactory2 as Render3DebugRendererFactory2, ɵRender3NgModuleRef as NgModuleRef, ɵWRAP_RENDERER_FACTORY2 as WRAP_RENDERER_FACTORY2, ɵcompileComponent as compileComponent, ɵcompileDirective as compileDirective, ɵcompileNgModuleDefs as compileNgModuleDefs, ɵcompilePipe as compilePipe, ɵpatchComponentDefWithScope as patchComponentDefWithScope, ɵstringify as stringify} from '@angular/core'; + +import {ComponentFixture} from './component_fixture'; +import {MetadataOverride} from './metadata_override'; +import {ComponentResolver, DirectiveResolver, NgModuleResolver, PipeResolver, Resolver} from './resolvers'; +import {ComponentFixtureAutoDetect, TestComponentRenderer, TestModuleMetadata} from './test_bed_common'; + +let _nextRootElementId = 0; + +/** + * @description + * Configures and initializes environment for unit testing and provides methods for + * creating components and services in unit tests. + * + * TestBed is the primary api for writing unit tests for Angular applications and libraries. + * + * Note: Use `TestBed` in tests. It will be set to either `TestBedViewEngine` or `TestBedRender3` + * according to the compiler used. + */ +export class TestBedRender3 { + /** + * Initialize the environment for testing with a compiler factory, a PlatformRef, and an + * angular module. These are common to every test in the suite. + * + * This may only be called once, to set up the common providers for the current test + * suite on the current platform. If you absolutely need to change the providers, + * first use `resetTestEnvironment`. + * + * Test modules and platforms for individual platforms are available from + * '@angular//testing'. + * + * @experimental + */ + static initTestEnvironment( + ngModule: Type|Type[], platform: PlatformRef, + aotSummaries?: () => any[]): TestBedRender3 { + const testBed = _getTestBedRender3(); + testBed.initTestEnvironment(ngModule, platform, aotSummaries); + return testBed; + } + + /** + * Reset the providers for the test injector. + * + * @experimental + */ + static resetTestEnvironment(): void { _getTestBedRender3().resetTestEnvironment(); } + + static configureCompiler(config: {providers?: any[]; useJit?: boolean;}): typeof TestBedRender3 { + _getTestBedRender3().configureCompiler(config); + return TestBedRender3; + } + + /** + * Allows overriding default providers, directives, pipes, modules of the test injector, + * which are defined in test_injector.js + */ + static configureTestingModule(moduleDef: TestModuleMetadata): typeof TestBedRender3 { + _getTestBedRender3().configureTestingModule(moduleDef); + return TestBedRender3; + } + + /** + * Compile components with a `templateUrl` for the test's NgModule. + * It is necessary to call this function + * as fetching urls is asynchronous. + */ + static compileComponents(): Promise { return _getTestBedRender3().compileComponents(); } + + static overrideModule(ngModule: Type, override: MetadataOverride): + typeof TestBedRender3 { + _getTestBedRender3().overrideModule(ngModule, override); + return TestBedRender3; + } + + static overrideComponent(component: Type, override: MetadataOverride): + typeof TestBedRender3 { + _getTestBedRender3().overrideComponent(component, override); + return TestBedRender3; + } + + static overrideDirective(directive: Type, override: MetadataOverride): + typeof TestBedRender3 { + _getTestBedRender3().overrideDirective(directive, override); + return TestBedRender3; + } + + static overridePipe(pipe: Type, override: MetadataOverride): typeof TestBedRender3 { + _getTestBedRender3().overridePipe(pipe, override); + return TestBedRender3; + } + + static overrideTemplate(component: Type, template: string): typeof TestBedRender3 { + _getTestBedRender3().overrideComponent(component, {set: {template, templateUrl: null !}}); + return TestBedRender3; + } + + /** + * Overrides the template of the given component, compiling the template + * in the context of the TestingModule. + * + * Note: This works for JIT and AOTed components as well. + */ + static overrideTemplateUsingTestingModule(component: Type, template: string): + typeof TestBedRender3 { + _getTestBedRender3().overrideTemplateUsingTestingModule(component, template); + return TestBedRender3; + } + + overrideTemplateUsingTestingModule(component: Type, template: string): void { + throw new Error('Render3TestBed.overrideTemplateUsingTestingModule is not implemented yet'); + } + + static overrideProvider(token: any, provider: { + useFactory: Function, + deps: any[], + }): typeof TestBedRender3; + static overrideProvider(token: any, provider: {useValue: any;}): typeof TestBedRender3; + static overrideProvider(token: any, provider: { + useFactory?: Function, + useValue?: any, + deps?: any[], + }): typeof TestBedRender3 { + _getTestBedRender3().overrideProvider(token, provider); + return TestBedRender3; + } + + /** + * Overwrites all providers for the given token with the given provider definition. + * + * @deprecated as it makes all NgModules lazy. Introduced only for migrating off of it. + */ + static deprecatedOverrideProvider(token: any, provider: { + useFactory: Function, + deps: any[], + }): void; + static deprecatedOverrideProvider(token: any, provider: {useValue: any;}): void; + static deprecatedOverrideProvider(token: any, provider: { + useFactory?: Function, + useValue?: any, + deps?: any[], + }): typeof TestBedRender3 { + throw new Error('Render3TestBed.deprecatedOverrideProvider is not implemented'); + } + + static get(token: any, notFoundValue: any = Injector.THROW_IF_NOT_FOUND): any { + return _getTestBedRender3().get(token, notFoundValue); + } + + static createComponent(component: Type): ComponentFixture { + return _getTestBedRender3().createComponent(component); + } + + static resetTestingModule(): typeof TestBedRender3 { + _getTestBedRender3().resetTestingModule(); + return TestBedRender3; + } + + // Properties + + platform: PlatformRef = null !; + ngModule: Type|Type[] = null !; + + // metadata overrides + private _moduleOverrides: [Type, MetadataOverride][] = []; + private _componentOverrides: [Type, MetadataOverride][] = []; + private _directiveOverrides: [Type, MetadataOverride][] = []; + private _pipeOverrides: [Type, MetadataOverride][] = []; + private _providerOverrides: Provider[] = []; + private _rootProviderOverrides: Provider[] = []; + + // test module configuration + private _providers: Provider[] = []; + private _declarations: Array|any[]|any> = []; + private _imports: Array|any[]|any> = []; + private _schemas: Array = []; + + private _activeFixtures: ComponentFixture[] = []; + + private _moduleRef: NgModuleRef = null !; + + private _instantiated: boolean = false; + + /** + * Initialize the environment for testing with a compiler factory, a PlatformRef, and an + * angular module. These are common to every test in the suite. + * + * This may only be called once, to set up the common providers for the current test + * suite on the current platform. If you absolutely need to change the providers, + * first use `resetTestEnvironment`. + * + * Test modules and platforms for individual platforms are available from + * '@angular//testing'. + * + * @experimental + */ + initTestEnvironment( + ngModule: Type|Type[], platform: PlatformRef, aotSummaries?: () => any[]): void { + if (this.platform || this.ngModule) { + throw new Error('Cannot set base providers because it has already been called'); + } + this.platform = platform; + this.ngModule = ngModule; + } + + /** + * Reset the providers for the test injector. + * + * @experimental + */ + resetTestEnvironment(): void { + this.resetTestingModule(); + this.platform = null !; + this.ngModule = null !; + } + + resetTestingModule(): void { + // reset metadata overrides + this._moduleOverrides = []; + this._componentOverrides = []; + this._directiveOverrides = []; + this._pipeOverrides = []; + this._providerOverrides = []; + this._rootProviderOverrides = []; + + // reset test module config + this._providers = []; + this._declarations = []; + this._imports = []; + this._schemas = []; + this._moduleRef = null !; + + this._instantiated = false; + this._activeFixtures.forEach((fixture) => { + try { + fixture.destroy(); + } catch (e) { + console.error('Error during cleanup of component', { + component: fixture.componentInstance, + stacktrace: e, + }); + } + }); + this._activeFixtures = []; + } + + configureCompiler(config: {providers?: any[]; useJit?: boolean;}): void { + throw new Error('the Render3 compiler is not configurable !'); + } + + configureTestingModule(moduleDef: TestModuleMetadata): void { + this._assertNotInstantiated('R3TestBed.configureTestingModule', 'configure the test module'); + if (moduleDef.providers) { + this._providers.push(...moduleDef.providers); + } + if (moduleDef.declarations) { + this._declarations.push(...moduleDef.declarations); + } + if (moduleDef.imports) { + this._imports.push(...moduleDef.imports); + } + if (moduleDef.schemas) { + this._schemas.push(...moduleDef.schemas); + } + } + + // TODO(vicb): implement + compileComponents(): Promise { + throw new Error('Render3TestBed.compileComponents is not implemented yet'); + } + + get(token: any, notFoundValue: any = Injector.THROW_IF_NOT_FOUND): any { + this._initIfNeeded(); + if (token === TestBedRender3) { + return this; + } + return this._moduleRef.injector.get(token, notFoundValue); + } + + execute(tokens: any[], fn: Function, context?: any): any { + this._initIfNeeded(); + const params = tokens.map(t => this.get(t)); + return fn.apply(context, params); + } + + overrideModule(ngModule: Type, override: MetadataOverride): void { + this._assertNotInstantiated('overrideModule', 'override module metadata'); + this._moduleOverrides.push([ngModule, override]); + } + + overrideComponent(component: Type, override: MetadataOverride): void { + this._assertNotInstantiated('overrideComponent', 'override component metadata'); + this._componentOverrides.push([component, override]); + } + + overrideDirective(directive: Type, override: MetadataOverride): void { + this._assertNotInstantiated('overrideDirective', 'override directive metadata'); + this._directiveOverrides.push([directive, override]); + } + + overridePipe(pipe: Type, override: MetadataOverride): void { + this._assertNotInstantiated('overridePipe', 'override pipe metadata'); + this._pipeOverrides.push([pipe, override]); + } + + /** + * Overwrites all providers for the given token with the given provider definition. + */ + overrideProvider(token: any, provider: {useFactory?: Function, useValue?: any, deps?: any[]}): + void { + const isRoot = + (typeof token !== 'string' && token.ngInjectableDef && + token.ngInjectableDef.providedIn === 'root'); + const overrides = isRoot ? this._rootProviderOverrides : this._providerOverrides; + + if (provider.useFactory) { + overrides.push({provide: token, useFactory: provider.useFactory, deps: provider.deps || []}); + } else { + overrides.push({provide: token, useValue: provider.useValue}); + } + } + + /** + * Overwrites all providers for the given token with the given provider definition. + * + * @deprecated as it makes all NgModules lazy. Introduced only for migrating off of it. + */ + deprecatedOverrideProvider(token: any, provider: { + useFactory: Function, + deps: any[], + }): void; + deprecatedOverrideProvider(token: any, provider: {useValue: any;}): void; + deprecatedOverrideProvider( + token: any, provider: {useFactory?: Function, useValue?: any, deps?: any[]}): void { + throw new Error('No implemented in IVY'); + } + + createComponent(type: Type): ComponentFixture { + this._initIfNeeded(); + + const testComponentRenderer: TestComponentRenderer = this.get(TestComponentRenderer); + const rootElId = `root${_nextRootElementId++}`; + testComponentRenderer.insertRootElement(rootElId); + + const componentDef = (type as any).ngComponentDef; + + if (!componentDef) { + throw new Error( + `It looks like '${stringify(type)}' has not been IVY compiled - it has no 'ngComponentDef' field`); + } + + const componentFactory = new ComponentFactory(componentDef); + const componentRef = + componentFactory.create(Injector.NULL, [], `#${rootElId}`, this._moduleRef); + const autoDetect: boolean = this.get(ComponentFixtureAutoDetect, false); + const fixture = new ComponentFixture(componentRef, null, autoDetect); + this._activeFixtures.push(fixture); + return fixture; + } + + // internal methods + + private _initIfNeeded(): void { + if (this._instantiated) { + return; + } + + const resolvers = this._getResolvers(); + const testModuleType = this._createTestModule(); + + compileNgModule(testModuleType, resolvers); + + const parentInjector = this.platform.injector; + this._moduleRef = new NgModuleRef(testModuleType, parentInjector); + + this._instantiated = true; + } + + // creates resolvers taking overrides into account + private _getResolvers() { + const module = new NgModuleResolver(); + module.setOverrides(this._moduleOverrides); + + const component = new ComponentResolver(); + component.setOverrides(this._componentOverrides); + + const directive = new DirectiveResolver(); + directive.setOverrides(this._directiveOverrides); + + const pipe = new PipeResolver(); + pipe.setOverrides(this._pipeOverrides); + + return {module, component, directive, pipe}; + } + + private _assertNotInstantiated(methodName: string, methodDescription: string) { + if (this._instantiated) { + throw new Error( + `Cannot ${methodDescription} when the test module has already been instantiated. ` + + `Make sure you are not using \`inject\` before \`${methodName}\`.`); + } + } + + private _createTestModule(): Type { + const rootProviderOverrides = this._rootProviderOverrides; + + const rendererFactoryWrapper = { + provide: WRAP_RENDERER_FACTORY2, + useFactory: () => (rf: RendererFactory2) => new Render3DebugRendererFactory2(rf), + }; + + @NgModule({ + providers: [...rootProviderOverrides, rendererFactoryWrapper], + jit: true, + }) + class RootScopeModule { + } + + const providers = [...this._providers, ...this._providerOverrides]; + + const declarations = this._declarations; + const imports = [RootScopeModule, this.ngModule, this._imports]; + const schemas = this._schemas; + + @NgModule({providers, declarations, imports, schemas, jit: true}) + class DynamicTestModule { + } + + return DynamicTestModule; + } +} + +let testBed: TestBedRender3; + +export function _getTestBedRender3(): TestBedRender3 { + return testBed = testBed || new TestBedRender3(); +} + + +// Module compiler + +const EMPTY_ARRAY: Type[] = []; + +// Resolvers for Angular decorators +type Resolvers = { + module: Resolver, + component: Resolver, + directive: Resolver, + pipe: Resolver, +}; + +function compileNgModule(moduleType: Type, resolvers: Resolvers): void { + const ngModule = resolvers.module.resolve(moduleType); + + if (ngModule === null) { + throw new Error(`${stringify(moduleType)} has not @NgModule annotation`); + } + + compileNgModuleDefs(moduleType, ngModule); + + const declarations: Type[] = flatten(ngModule.declarations || EMPTY_ARRAY); + + const compiledComponents: Type[] = []; + + // Compile the components, directives and pipes declared by this module + declarations.forEach(declaration => { + const component = resolvers.component.resolve(declaration); + if (component) { + compileComponent(declaration, component); + compiledComponents.push(declaration); + return; + } + + const directive = resolvers.directive.resolve(declaration); + if (directive) { + compileDirective(declaration, directive); + return; + } + + const pipe = resolvers.pipe.resolve(declaration); + if (pipe) { + compilePipe(declaration, pipe); + return; + } + }); + + // Compile transitive modules, components, directives and pipes + const transitiveScope = transitiveScopesFor(moduleType, resolvers); + compiledComponents.forEach( + cmp => patchComponentDefWithScope((cmp as any).ngComponentDef, transitiveScope)); +} + +/** + * Compute the pair of transitive scopes (compilation scope and exported scope) for a given module. + * + * This operation is memoized and the result is cached on the module's definition. It can be called + * on modules with components that have not fully compiled yet, but the result should not be used + * until they have. + */ +function transitiveScopesFor( + moduleType: Type, resolvers: Resolvers): NgModuleTransitiveScopes { + if (!isNgModule(moduleType)) { + throw new Error(`${moduleType.name} does not have an ngModuleDef`); + } + const def = moduleType.ngModuleDef; + + if (def.transitiveCompileScopes !== null) { + return def.transitiveCompileScopes; + } + + const scopes: NgModuleTransitiveScopes = { + compilation: { + directives: new Set(), + pipes: new Set(), + }, + exported: { + directives: new Set(), + pipes: new Set(), + }, + }; + + def.declarations.forEach(declared => { + const declaredWithDefs = declared as Type& { ngPipeDef?: any; }; + + if (declaredWithDefs.ngPipeDef !== undefined) { + scopes.compilation.pipes.add(declared); + } else { + scopes.compilation.directives.add(declared); + } + }); + + def.imports.forEach((imported: Type) => { + const ngModule = resolvers.module.resolve(imported); + + if (ngModule === null) { + throw new Error(`Importing ${imported.name} which does not have an @ngModule`); + } else { + compileNgModule(imported, resolvers); + } + + // When this module imports another, the imported module's exported directives and pipes are + // added to the compilation scope of this module. + const importedScope = transitiveScopesFor(imported, resolvers); + importedScope.exported.directives.forEach(entry => scopes.compilation.directives.add(entry)); + importedScope.exported.pipes.forEach(entry => scopes.compilation.pipes.add(entry)); + }); + + def.exports.forEach((exported: Type) => { + const exportedTyped = exported as Type& { + // Components, Directives, NgModules, and Pipes can all be exported. + ngComponentDef?: any; + ngDirectiveDef?: any; + ngModuleDef?: NgModuleDefInternal; + ngPipeDef?: any; + }; + + // Either the type is a module, a pipe, or a component/directive (which may not have an + // ngComponentDef as it might be compiled asynchronously). + if (isNgModule(exportedTyped)) { + // When this module exports another, the exported module's exported directives and pipes are + // added to both the compilation and exported scopes of this module. + const exportedScope = transitiveScopesFor(exportedTyped, resolvers); + exportedScope.exported.directives.forEach(entry => { + scopes.compilation.directives.add(entry); + scopes.exported.directives.add(entry); + }); + exportedScope.exported.pipes.forEach(entry => { + scopes.compilation.pipes.add(entry); + scopes.exported.pipes.add(entry); + }); + } else if (exportedTyped.ngPipeDef !== undefined) { + scopes.exported.pipes.add(exportedTyped); + } else { + scopes.exported.directives.add(exportedTyped); + } + }); + + def.transitiveCompileScopes = scopes; + return scopes; +} + +function flatten(values: any[]): T[] { + const out: T[] = []; + values.forEach(value => { + if (Array.isArray(value)) { + out.push(...flatten(value)); + } else { + out.push(value); + } + }); + return out; +} + +function isNgModule(value: Type): value is Type&{ngModuleDef: NgModuleDefInternal} { + return (value as{ngModuleDef?: NgModuleDefInternal}).ngModuleDef !== undefined; +} diff --git a/packages/core/testing/src/resolvers.ts b/packages/core/testing/src/resolvers.ts new file mode 100644 index 0000000000..262d387d02 --- /dev/null +++ b/packages/core/testing/src/resolvers.ts @@ -0,0 +1,73 @@ +/** + * @license + * Copyright Google Inc. 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, NgModule, Pipe, Type, ɵReflectionCapabilities as ReflectionCapabilities} from '@angular/core'; + +import {MetadataOverride} from './metadata_override'; +import {MetadataOverrider} from './metadata_overrider'; + +const reflection = new ReflectionCapabilities(); + +/** + * Base interface to resolve `@Component`, `@Directive`, `@Pipe` and `@NgModule`. + */ +export interface Resolver { resolve(type: Type): T|null; } + +/** + * Allows to override ivy metadata for tests (via the `TestBed`). + */ +abstract class OverrideResolver implements Resolver { + private overrides = new Map, MetadataOverride>(); + private resolved = new Map, T|null>(); + + abstract get type(): any; + + setOverrides(overrides: Array<[Type, MetadataOverride]>) { + this.overrides.clear(); + overrides.forEach(([type, override]) => this.overrides.set(type, override)); + } + + getAnnotation(type: Type): T|null { + return reflection.annotations(type).find(a => a instanceof this.type) || null; + } + + resolve(type: Type): T|null { + let resolved = this.resolved.get(type) || null; + + if (!resolved) { + resolved = this.getAnnotation(type); + if (resolved) { + const override = this.overrides.get(type); + if (override) { + const overrider = new MetadataOverrider(); + resolved = overrider.overrideMetadata(this.type, resolved, override); + } + } + this.resolved.set(type, resolved); + } + + return resolved; + } +} + + +export class DirectiveResolver extends OverrideResolver { + get type() { return Directive; } +} + +export class ComponentResolver extends OverrideResolver { + get type() { return Component; } +} + +export class PipeResolver extends OverrideResolver { + get type() { return Pipe; } +} + +export class NgModuleResolver extends OverrideResolver { + get type() { return NgModule; } +} diff --git a/packages/core/testing/src/test_bed.ts b/packages/core/testing/src/test_bed.ts index 27c0cac61c..0043dfef91 100644 --- a/packages/core/testing/src/test_bed.ts +++ b/packages/core/testing/src/test_bed.ts @@ -6,56 +6,31 @@ * found in the LICENSE file at https://angular.io/license */ -import {ApplicationInitStatus, CompilerOptions, Component, Directive, InjectionToken, Injector, NgModule, NgModuleFactory, NgModuleRef, NgZone, Optional, Pipe, PlatformRef, Provider, SchemaMetadata, SkipSelf, StaticProvider, Type, ɵAPP_ROOT as APP_ROOT, ɵDepFlags as DepFlags, ɵNodeFlags as NodeFlags, ɵclearOverrides as clearOverrides, ɵgetComponentViewDefinitionFactory as getComponentViewDefinitionFactory, ɵoverrideComponentView as overrideComponentView, ɵoverrideProvider as overrideProvider, ɵstringify as stringify} from '@angular/core'; +import {ApplicationInitStatus, CompilerOptions, Component, Directive, Injector, NgModule, NgModuleFactory, NgModuleRef, NgZone, Optional, Pipe, PlatformRef, Provider, SchemaMetadata, SkipSelf, StaticProvider, Type, ɵAPP_ROOT as APP_ROOT, ɵDepFlags as DepFlags, ɵNodeFlags as NodeFlags, ɵclearOverrides as clearOverrides, ɵgetComponentViewDefinitionFactory as getComponentViewDefinitionFactory, ɵivyEnabled as ivyEnabled, ɵoverrideComponentView as overrideComponentView, ɵoverrideProvider as overrideProvider, ɵstringify as stringify} from '@angular/core'; import {AsyncTestCompleter} from './async_test_completer'; import {ComponentFixture} from './component_fixture'; import {MetadataOverride} from './metadata_override'; +import {TestBedRender3, _getTestBedRender3} from './r3_test_bed'; +import {ComponentFixtureAutoDetect, ComponentFixtureNoNgZone, TestComponentRenderer, TestModuleMetadata} from './test_bed_common'; import {TestingCompiler, TestingCompilerFactory} from './test_compiler'; const UNDEFINED = new Object(); -/** - * An abstract class for inserting the root test component element in a platform independent way. - * - * @experimental - */ -export class TestComponentRenderer { - insertRootElement(rootElementId: string) {} -} let _nextRootElementId = 0; -/** - * @experimental - */ -export const ComponentFixtureAutoDetect = - new InjectionToken('ComponentFixtureAutoDetect'); - -/** - * @experimental - */ -export const ComponentFixtureNoNgZone = new InjectionToken('ComponentFixtureNoNgZone'); - -/** - * @experimental - */ -export type TestModuleMetadata = { - providers?: any[], - declarations?: any[], - imports?: any[], - schemas?: Array, - aotSummaries?: () => any[], -}; - /** * @description * Configures and initializes environment for unit testing and provides methods for * creating components and services in unit tests. * - * TestBed is the primary api for writing unit tests for Angular applications and libraries. + * `TestBed` is the primary api for writing unit tests for Angular applications and libraries. + * + * Note: Use `TestBed` in tests. It will be set to either `TestBedViewEngine` or `TestBedRender3` + * according to the compiler used. */ -export class TestBed implements Injector { +export class TestBedViewEngine implements Injector { /** * Initialize the environment for testing with a compiler factory, a PlatformRef, and an * angular module. These are common to every test in the suite. @@ -70,8 +45,9 @@ export class TestBed implements Injector { * @experimental */ static initTestEnvironment( - ngModule: Type|Type[], platform: PlatformRef, aotSummaries?: () => any[]): TestBed { - const testBed = getTestBed(); + ngModule: Type|Type[], platform: PlatformRef, + aotSummaries?: () => any[]): TestBedViewEngine { + const testBed = _getTestBedViewEngine(); testBed.initTestEnvironment(ngModule, platform, aotSummaries); return testBed; } @@ -81,10 +57,10 @@ export class TestBed implements Injector { * * @experimental */ - static resetTestEnvironment() { getTestBed().resetTestEnvironment(); } + static resetTestEnvironment(): void { _getTestBedViewEngine().resetTestEnvironment(); } static resetTestingModule(): typeof TestBed { - getTestBed().resetTestingModule(); + _getTestBedViewEngine().resetTestingModule(); return TestBed; } @@ -93,7 +69,7 @@ export class TestBed implements Injector { * which are defined in test_injector.js */ static configureCompiler(config: {providers?: any[]; useJit?: boolean;}): typeof TestBed { - getTestBed().configureCompiler(config); + _getTestBedViewEngine().configureCompiler(config); return TestBed; } @@ -102,7 +78,7 @@ export class TestBed implements Injector { * which are defined in test_injector.js */ static configureTestingModule(moduleDef: TestModuleMetadata): typeof TestBed { - getTestBed().configureTestingModule(moduleDef); + _getTestBedViewEngine().configureTestingModule(moduleDef); return TestBed; } @@ -114,29 +90,29 @@ export class TestBed implements Injector { static compileComponents(): Promise { return getTestBed().compileComponents(); } static overrideModule(ngModule: Type, override: MetadataOverride): typeof TestBed { - getTestBed().overrideModule(ngModule, override); + _getTestBedViewEngine().overrideModule(ngModule, override); return TestBed; } static overrideComponent(component: Type, override: MetadataOverride): typeof TestBed { - getTestBed().overrideComponent(component, override); + _getTestBedViewEngine().overrideComponent(component, override); return TestBed; } static overrideDirective(directive: Type, override: MetadataOverride): typeof TestBed { - getTestBed().overrideDirective(directive, override); + _getTestBedViewEngine().overrideDirective(directive, override); return TestBed; } static overridePipe(pipe: Type, override: MetadataOverride): typeof TestBed { - getTestBed().overridePipe(pipe, override); + _getTestBedViewEngine().overridePipe(pipe, override); return TestBed; } static overrideTemplate(component: Type, template: string): typeof TestBed { - getTestBed().overrideComponent(component, {set: {template, templateUrl: null !}}); + _getTestBedViewEngine().overrideComponent(component, {set: {template, templateUrl: null !}}); return TestBed; } @@ -148,11 +124,10 @@ export class TestBed implements Injector { */ static overrideTemplateUsingTestingModule(component: Type, template: string): typeof TestBed { - getTestBed().overrideTemplateUsingTestingModule(component, template); + _getTestBedViewEngine().overrideTemplateUsingTestingModule(component, template); return TestBed; } - /** * Overwrites all providers for the given token with the given provider definition. * @@ -168,7 +143,7 @@ export class TestBed implements Injector { useValue?: any, deps?: any[], }): typeof TestBed { - getTestBed().overrideProvider(token, provider as any); + _getTestBedViewEngine().overrideProvider(token, provider as any); return TestBed; } @@ -187,16 +162,16 @@ export class TestBed implements Injector { useValue?: any, deps?: any[], }): typeof TestBed { - getTestBed().deprecatedOverrideProvider(token, provider as any); + _getTestBedViewEngine().deprecatedOverrideProvider(token, provider as any); return TestBed; } static get(token: any, notFoundValue: any = Injector.THROW_IF_NOT_FOUND) { - return getTestBed().get(token, notFoundValue); + return _getTestBedViewEngine().get(token, notFoundValue); } static createComponent(component: Type): ComponentFixture { - return getTestBed().createComponent(component); + return _getTestBedViewEngine().createComponent(component); } private _instantiated: boolean = false; @@ -243,7 +218,7 @@ export class TestBed implements Injector { * @experimental */ initTestEnvironment( - ngModule: Type|Type[], platform: PlatformRef, aotSummaries?: () => any[]) { + ngModule: Type|Type[], platform: PlatformRef, aotSummaries?: () => any[]): void { if (this.platform || this.ngModule) { throw new Error('Cannot set base providers because it has already been called'); } @@ -259,14 +234,14 @@ export class TestBed implements Injector { * * @experimental */ - resetTestEnvironment() { + resetTestEnvironment(): void { this.resetTestingModule(); this.platform = null !; this.ngModule = null !; this._testEnvAotSummaries = () => []; } - resetTestingModule() { + resetTestingModule(): void { clearOverrides(); this._aotSummaries = []; this._templateOverrides = []; @@ -300,12 +275,12 @@ export class TestBed implements Injector { this._activeFixtures = []; } - configureCompiler(config: {providers?: any[], useJit?: boolean}) { + configureCompiler(config: {providers?: any[], useJit?: boolean}): void { this._assertNotInstantiated('TestBed.configureCompiler', 'configure the compiler'); this._compilerOptions.push(config); } - configureTestingModule(moduleDef: TestModuleMetadata) { + configureTestingModule(moduleDef: TestModuleMetadata): void { this._assertNotInstantiated('TestBed.configureTestingModule', 'configure the test module'); if (moduleDef.providers) { this._providers.push(...moduleDef.providers); @@ -336,7 +311,7 @@ export class TestBed implements Injector { }); } - private _initIfNeeded() { + private _initIfNeeded(): void { if (this._instantiated) { return; } @@ -425,7 +400,7 @@ export class TestBed implements Injector { } } - get(token: any, notFoundValue: any = Injector.THROW_IF_NOT_FOUND) { + get(token: any, notFoundValue: any = Injector.THROW_IF_NOT_FOUND): any { this._initIfNeeded(); if (token === TestBed) { return this; @@ -574,13 +549,32 @@ export class TestBed implements Injector { } } -let _testBed: TestBed = null !; +/** + * @description + * Configures and initializes environment for unit testing and provides methods for + * creating components and services in unit tests. + * + * `TestBed` is the primary api for writing unit tests for Angular applications and libraries. + * + * Note: Use `TestBed` in tests. It will be set to either `TestBedViewEngine` or `TestBedRender3` + * according to the compiler used. + */ +export const TestBed = ivyEnabled ? TestBedRender3 : TestBedViewEngine; /** + * Returns a singleton of the applicable `TestBed`. + * + * It will be either an instance of `TestBedViewEngine` or `TestBedRender3`. + * * @experimental */ -export function getTestBed(): TestBed { - return _testBed = _testBed || new TestBed(); +export const getTestBed: () => TestBedRender3 | TestBedViewEngine = + ivyEnabled ? _getTestBedRender3 : _getTestBedViewEngine; + +let testBed: TestBedViewEngine; + +function _getTestBedViewEngine(): TestBedViewEngine { + return testBed = testBed || new TestBedViewEngine(); } /** diff --git a/packages/core/testing/src/test_bed_common.ts b/packages/core/testing/src/test_bed_common.ts new file mode 100644 index 0000000000..b590305987 --- /dev/null +++ b/packages/core/testing/src/test_bed_common.ts @@ -0,0 +1,40 @@ +/** + * @license + * Copyright Google Inc. 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 {InjectionToken, SchemaMetadata} from '@angular/core'; + +/** + * An abstract class for inserting the root test component element in a platform independent way. + * + * @experimental + */ +export class TestComponentRenderer { + insertRootElement(rootElementId: string) {} +} + +/** + * @experimental + */ +export const ComponentFixtureAutoDetect = + new InjectionToken('ComponentFixtureAutoDetect'); + +/** + * @experimental + */ +export const ComponentFixtureNoNgZone = new InjectionToken('ComponentFixtureNoNgZone'); + +/** + * @experimental + */ +export type TestModuleMetadata = { + providers?: any[], + declarations?: any[], + imports?: any[], + schemas?: Array, + aotSummaries?: () => any[], +}; diff --git a/packages/core/testing/src/testing.ts b/packages/core/testing/src/testing.ts index 9051a017e2..e16711c006 100644 --- a/packages/core/testing/src/testing.ts +++ b/packages/core/testing/src/testing.ts @@ -16,6 +16,9 @@ export * from './async'; export * from './component_fixture'; export * from './fake_async'; export * from './test_bed'; +export * from './test_bed_common'; +export * from './r3_test_bed'; export * from './before_each'; export * from './metadata_override'; +export * from './metadata_overrider'; export * from './private_export_testing'; diff --git a/packages/core/testing/tsconfig-build.json b/packages/core/testing/tsconfig-build.json index adc2efbeff..7b91f4043b 100644 --- a/packages/core/testing/tsconfig-build.json +++ b/packages/core/testing/tsconfig-build.json @@ -1,27 +1,31 @@ { "extends": "../tsconfig-build.json", - "compilerOptions": { "baseUrl": ".", "rootDir": "../", "paths": { - "rxjs/*": ["../../../node_modules/rxjs/*"], - "@angular/core": ["../../../dist/packages/core"] + "rxjs/*": [ + "../../../node_modules/rxjs/*" + ], + "@angular/core": [ + "../../../dist/packages/core" + ], + "@angular/compiler": [ + "../../../dist/packages/compiler" + ], }, "outDir": "../../../dist/packages/core" }, - "files": [ "public_api.ts", "../../../node_modules/zone.js/dist/zone.js.d.ts", "../../system.d.ts", "../../types.d.ts" ], - "angularCompilerOptions": { "strictMetadataEmit": false, "skipTemplateCodegen": true, "flatModuleOutFile": "testing.js", "flatModuleId": "@angular/core/testing" } -} +} \ No newline at end of file diff --git a/packages/core/tsconfig-build.json b/packages/core/tsconfig-build.json index ba15f687e8..a60be80d19 100644 --- a/packages/core/tsconfig-build.json +++ b/packages/core/tsconfig-build.json @@ -1,22 +1,26 @@ { "extends": "../tsconfig-build.json", - "compilerOptions": { "baseUrl": ".", "rootDir": ".", "paths": { - "rxjs/*": ["../../node_modules/rxjs/*"], - "@angular/core": ["."] + "rxjs/*": [ + "../../node_modules/rxjs/*" + ], + "@angular/core": [ + "." + ], + "@angular/compiler": [ + "../../dist/packages/compiler" + ] }, "outDir": "../../dist/packages/core" }, - "files": [ "public_api.ts", "../../node_modules/zone.js/dist/zone.js.d.ts", "../system.d.ts" ], - "angularCompilerOptions": { "annotateForClosureCompiler": true, "strictMetadataEmit": false, @@ -24,4 +28,4 @@ "flatModuleOutFile": "core.js", "flatModuleId": "@angular/core" } -} +} \ No newline at end of file diff --git a/packages/language-service/test/BUILD.bazel b/packages/language-service/test/BUILD.bazel index 89e4f08f24..81d9cd0811 100644 --- a/packages/language-service/test/BUILD.bazel +++ b/packages/language-service/test/BUILD.bazel @@ -18,6 +18,7 @@ jasmine_node_test( bootstrap = ["angular/tools/testing/init_node_spec.js"], data = [ "//packages/common:npm_package", + "//packages/compiler:npm_package", "//packages/core:npm_package", "//packages/forms:npm_package", ], diff --git a/packages/platform-browser-dynamic/testing/src/metadata_overrider.ts b/packages/platform-browser-dynamic/testing/src/metadata_overrider.ts index 6c5858fa2f..bc2c957942 100644 --- a/packages/platform-browser-dynamic/testing/src/metadata_overrider.ts +++ b/packages/platform-browser-dynamic/testing/src/metadata_overrider.ts @@ -6,126 +6,5 @@ * found in the LICENSE file at https://angular.io/license */ -import {ɵstringify as stringify} from '@angular/core'; -import {MetadataOverride} from '@angular/core/testing'; - -type StringMap = { - [key: string]: any -}; - -let _nextReferenceId = 0; - -export class MetadataOverrider { - private _references = new Map(); - /** - * Creates a new instance for the given metadata class - * based on an old instance and overrides. - */ - overrideMetadata( - metadataClass: {new (options: T): C;}, oldMetadata: C, override: MetadataOverride): C { - const props: StringMap = {}; - if (oldMetadata) { - _valueProps(oldMetadata).forEach((prop) => props[prop] = (oldMetadata)[prop]); - } - - if (override.set) { - if (override.remove || override.add) { - throw new Error(`Cannot set and add/remove ${stringify(metadataClass)} at the same time!`); - } - setMetadata(props, override.set); - } - if (override.remove) { - removeMetadata(props, override.remove, this._references); - } - if (override.add) { - addMetadata(props, override.add); - } - return new metadataClass(props); - } -} - -function removeMetadata(metadata: StringMap, remove: any, references: Map) { - const removeObjects = new Set(); - for (const prop in remove) { - const removeValue = remove[prop]; - if (removeValue instanceof Array) { - removeValue.forEach( - (value: any) => { removeObjects.add(_propHashKey(prop, value, references)); }); - } else { - removeObjects.add(_propHashKey(prop, removeValue, references)); - } - } - - for (const prop in metadata) { - const propValue = metadata[prop]; - if (propValue instanceof Array) { - metadata[prop] = propValue.filter( - (value: any) => !removeObjects.has(_propHashKey(prop, value, references))); - } else { - if (removeObjects.has(_propHashKey(prop, propValue, references))) { - metadata[prop] = undefined; - } - } - } -} - -function addMetadata(metadata: StringMap, add: any) { - for (const prop in add) { - const addValue = add[prop]; - const propValue = metadata[prop]; - if (propValue != null && propValue instanceof Array) { - metadata[prop] = propValue.concat(addValue); - } else { - metadata[prop] = addValue; - } - } -} - -function setMetadata(metadata: StringMap, set: any) { - for (const prop in set) { - metadata[prop] = set[prop]; - } -} - -function _propHashKey(propName: any, propValue: any, references: Map): string { - const replacer = (key: any, value: any) => { - if (typeof value === 'function') { - value = _serializeReference(value, references); - } - return value; - }; - - return `${propName}:${JSON.stringify(propValue, replacer)}`; -} - -function _serializeReference(ref: any, references: Map): string { - let id = references.get(ref); - if (!id) { - id = `${stringify(ref)}${_nextReferenceId++}`; - references.set(ref, id); - } - return id; -} - - -function _valueProps(obj: any): string[] { - const props: string[] = []; - // regular public props - Object.keys(obj).forEach((prop) => { - if (!prop.startsWith('_')) { - props.push(prop); - } - }); - - // getters - let proto = obj; - while (proto = Object.getPrototypeOf(proto)) { - Object.keys(proto).forEach((protoProp) => { - const desc = Object.getOwnPropertyDescriptor(proto, protoProp); - if (!protoProp.startsWith('_') && desc && 'get' in desc) { - props.push(protoProp); - } - }); - } - return props; -} +// `MetadataOverrider` has been moved to core to allow using it from the render3 TestBed +export {MetadataOverrider} from '@angular/core/testing'; diff --git a/packages/platform-browser-dynamic/testing/src/testing.ts b/packages/platform-browser-dynamic/testing/src/testing.ts index 89786fb396..d849378d81 100644 --- a/packages/platform-browser-dynamic/testing/src/testing.ts +++ b/packages/platform-browser-dynamic/testing/src/testing.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {NgModule, createPlatformFactory} from '@angular/core'; +import {NgModule, PlatformRef, StaticProvider, createPlatformFactory} from '@angular/core'; import {TestComponentRenderer} from '@angular/core/testing'; import {ɵINTERNAL_BROWSER_DYNAMIC_PLATFORM_PROVIDERS as INTERNAL_BROWSER_DYNAMIC_PLATFORM_PROVIDERS} from '@angular/platform-browser-dynamic'; import {BrowserTestingModule} from '@angular/platform-browser/testing'; diff --git a/scripts/ci/install.sh b/scripts/ci/install.sh index ef0f31023a..47bd8efb79 100755 --- a/scripts/ci/install.sh +++ b/scripts/ci/install.sh @@ -56,7 +56,7 @@ if [[ ${TRAVIS} && travisFoldStart "yarn-install.aio" ( # HACK (don't submit with this): Build Angular - ./build.sh --packages=core,elements --examples=false + ./build.sh --packages=compiler,core,elements --examples=false cd ${PROJECT_ROOT}/aio yarn install --frozen-lockfile --non-interactive diff --git a/tools/public_api_guard/core/testing.d.ts b/tools/public_api_guard/core/testing.d.ts index e91c3737b7..afd0340bb6 100644 --- a/tools/public_api_guard/core/testing.d.ts +++ b/tools/public_api_guard/core/testing.d.ts @@ -1,3 +1,5 @@ +export declare function _getTestBedRender3(): TestBedRender3; + export declare function async(fn: Function): (done: any) => any; export declare class ComponentFixture { @@ -37,7 +39,7 @@ export declare function flush(maxTurns?: number): number; export declare function flushMicrotasks(): void; /** @experimental */ -export declare function getTestBed(): TestBed; +export declare const getTestBed: () => TestBedRender3 | TestBedViewEngine; export declare function inject(tokens: any[], fn: Function): () => any; @@ -54,10 +56,83 @@ export declare type MetadataOverride = { set?: Partial; }; +export declare class MetadataOverrider { + overrideMetadata(metadataClass: { + new (options: T): C; + }, oldMetadata: C, override: MetadataOverride): C; +} + /** @experimental */ export declare function resetFakeAsyncZone(): void; -export declare class TestBed implements Injector { +export declare const TestBed: typeof TestBedRender3 | typeof TestBedViewEngine; + +export declare class TestBedRender3 { + ngModule: Type | Type[]; + platform: PlatformRef; + compileComponents(): Promise; + configureCompiler(config: { + providers?: any[]; + useJit?: boolean; + }): void; + configureTestingModule(moduleDef: TestModuleMetadata): void; + createComponent(type: Type): ComponentFixture; + deprecatedOverrideProvider(token: any, provider: { + useValue: any; + }): void; + /** @deprecated */ deprecatedOverrideProvider(token: any, provider: { + useFactory: Function; + deps: any[]; + }): void; + execute(tokens: any[], fn: Function, context?: any): any; + get(token: any, notFoundValue?: any): any; + /** @experimental */ initTestEnvironment(ngModule: Type | Type[], platform: PlatformRef, aotSummaries?: () => any[]): void; + overrideComponent(component: Type, override: MetadataOverride): void; + overrideDirective(directive: Type, override: MetadataOverride): void; + overrideModule(ngModule: Type, override: MetadataOverride): void; + overridePipe(pipe: Type, override: MetadataOverride): void; + overrideProvider(token: any, provider: { + useFactory?: Function; + useValue?: any; + deps?: any[]; + }): void; + overrideTemplateUsingTestingModule(component: Type, template: string): void; + /** @experimental */ resetTestEnvironment(): void; + resetTestingModule(): void; + static compileComponents(): Promise; + static configureCompiler(config: { + providers?: any[]; + useJit?: boolean; + }): typeof TestBedRender3; + static configureTestingModule(moduleDef: TestModuleMetadata): typeof TestBedRender3; + static createComponent(component: Type): ComponentFixture; + static deprecatedOverrideProvider(token: any, provider: { + useValue: any; + }): void; + /** @deprecated */ static deprecatedOverrideProvider(token: any, provider: { + useFactory: Function; + deps: any[]; + }): void; + static get(token: any, notFoundValue?: any): any; + /** @experimental */ static initTestEnvironment(ngModule: Type | Type[], platform: PlatformRef, aotSummaries?: () => any[]): TestBedRender3; + static overrideComponent(component: Type, override: MetadataOverride): typeof TestBedRender3; + static overrideDirective(directive: Type, override: MetadataOverride): typeof TestBedRender3; + static overrideModule(ngModule: Type, override: MetadataOverride): typeof TestBedRender3; + static overridePipe(pipe: Type, override: MetadataOverride): typeof TestBedRender3; + static overrideProvider(token: any, provider: { + useFactory: Function; + deps: any[]; + }): typeof TestBedRender3; + static overrideProvider(token: any, provider: { + useValue: any; + }): typeof TestBedRender3; + static overrideTemplate(component: Type, template: string): typeof TestBedRender3; + static overrideTemplateUsingTestingModule(component: Type, template: string): typeof TestBedRender3; + /** @experimental */ static resetTestEnvironment(): void; + static resetTestingModule(): typeof TestBedRender3; +} + +export declare class TestBedViewEngine implements Injector { ngModule: Type | Type[]; platform: PlatformRef; compileComponents(): Promise; @@ -106,7 +181,7 @@ export declare class TestBed implements Injector { deps: any[]; }): void; static get(token: any, notFoundValue?: any): any; - /** @experimental */ static initTestEnvironment(ngModule: Type | Type[], platform: PlatformRef, aotSummaries?: () => any[]): TestBed; + /** @experimental */ static initTestEnvironment(ngModule: Type | Type[], platform: PlatformRef, aotSummaries?: () => any[]): TestBedViewEngine; static overrideComponent(component: Type, override: MetadataOverride): typeof TestBed; static overrideDirective(directive: Type, override: MetadataOverride): typeof TestBed; static overrideModule(ngModule: Type, override: MetadataOverride): typeof TestBed;