From 39b92f7e546b8bdecc0e08c915d36717358c122b Mon Sep 17 00:00:00 2001 From: Tobias Bosch Date: Mon, 15 May 2017 13:12:10 -0700 Subject: [PATCH] feat: introduce `TestBed.overrideProvider` (#16725) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This allows to overwrite all providers for a token, not matter where they were defined. This can be used to test JIT and AOT’ed code in the same way. Design doc: https://docs.google.com/document/d/1VmTkz0EbEVSWfEEWEvQ5sXyQXSCvtMOw4t7pKU-jOwc/edit?usp=sharing --- packages/core/src/application_module.ts | 8 +- packages/core/src/core_private_export.ts | 1 + packages/core/src/view/entrypoint.ts | 50 ++++ packages/core/src/view/index.ts | 2 +- packages/core/src/view/ng_module.ts | 22 +- packages/core/src/view/provider.ts | 14 +- packages/core/src/view/refs.ts | 39 +-- packages/core/src/view/services.ts | 162 ++++++++++- packages/core/src/view/types.ts | 27 +- packages/core/src/view/util.ts | 16 +- packages/core/src/view/view.ts | 29 +- .../test/linker/ng_module_integration_spec.ts | 49 +++- .../linker/view_injector_integration_spec.ts | 47 +++ packages/core/test/view/embedded_view_spec.ts | 19 +- packages/core/test/view/helper.ts | 6 +- packages/core/test/view/ng_content_spec.ts | 4 +- packages/core/test/view/query_spec.ts | 10 +- packages/core/testing/src/test_bed.ts | 64 +++- .../test/testing_public_spec.ts | 273 +++++++++++++++++- tools/public_api_guard/core/testing.d.ts | 14 + 20 files changed, 735 insertions(+), 121 deletions(-) create mode 100644 packages/core/src/view/entrypoint.ts diff --git a/packages/core/src/application_module.ts b/packages/core/src/application_module.ts index f03e7f682d..5c4b359a5f 100644 --- a/packages/core/src/application_module.ts +++ b/packages/core/src/application_module.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {APP_INITIALIZER, ApplicationInitStatus} from './application_init'; +import {ApplicationInitStatus} from './application_init'; import {ApplicationRef, ApplicationRef_} from './application_ref'; import {APP_ID_RANDOM_PROVIDER} from './application_tokens'; import {IterableDiffers, KeyValueDiffers, defaultIterableDiffers, defaultKeyValueDiffers} from './change_detection/change_detection'; @@ -14,7 +14,6 @@ import {Inject, Optional, SkipSelf} from './di/metadata'; import {LOCALE_ID} from './i18n/tokens'; import {Compiler} from './linker/compiler'; import {NgModule} from './metadata'; -import {initServicesIfNeeded} from './view/index'; export function _iterableDiffersFactory() { return defaultIterableDiffers; @@ -28,10 +27,6 @@ export function _localeFactory(locale?: string): string { return locale || 'en-US'; } -export function _initViewEngine() { - initServicesIfNeeded(); -} - /** * This module includes the providers of @angular/core that are needed * to bootstrap components via `ApplicationRef`. @@ -52,7 +47,6 @@ export function _initViewEngine() { useFactory: _localeFactory, deps: [[new Inject(LOCALE_ID), new Optional(), new SkipSelf()]] }, - {provide: APP_INITIALIZER, useValue: _initViewEngine, multi: true}, ] }) export class ApplicationModule { diff --git a/packages/core/src/core_private_export.ts b/packages/core/src/core_private_export.ts index cffbcfa770..93917abdc1 100644 --- a/packages/core/src/core_private_export.ts +++ b/packages/core/src/core_private_export.ts @@ -26,4 +26,5 @@ export {DirectRenderer as ɵDirectRenderer, RenderDebugInfo as ɵRenderDebugInfo export {global as ɵglobal, looseIdentical as ɵlooseIdentical, stringify as ɵstringify} from './util'; export {makeDecorator as ɵmakeDecorator} from './util/decorators'; export {isObservable as ɵisObservable, isPromise as ɵisPromise} from './util/lang'; +export {clearProviderOverrides as ɵclearProviderOverrides, overrideProvider as ɵoverrideProvider} from './view/index'; export {NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR as ɵNOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR} from './view/provider'; diff --git a/packages/core/src/view/entrypoint.ts b/packages/core/src/view/entrypoint.ts new file mode 100644 index 0000000000..f0355dba4e --- /dev/null +++ b/packages/core/src/view/entrypoint.ts @@ -0,0 +1,50 @@ +/** + * @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 {NgModuleFactory, NgModuleRef} from '../linker/ng_module_factory'; +import {Type} from '../type'; + +import {initServicesIfNeeded} from './services'; +import {NgModuleDefinitionFactory, ProviderOverride, Services} from './types'; +import {resolveDefinition} from './util'; + +export function overrideProvider(override: ProviderOverride) { + initServicesIfNeeded(); + return Services.overrideProvider(override); +} + +export function clearProviderOverrides() { + initServicesIfNeeded(); + return Services.clearProviderOverrides(); +} + +// Attention: this function is called as top level function. +// Putting any logic in here will destroy closure tree shaking! +export function createNgModuleFactory( + ngModuleType: Type, bootstrapComponents: Type[], + defFactory: NgModuleDefinitionFactory): NgModuleFactory { + return new NgModuleFactory_(ngModuleType, bootstrapComponents, defFactory); +} + +class NgModuleFactory_ extends NgModuleFactory { + constructor( + public readonly moduleType: Type, private _bootstrapComponents: Type[], + private _ngModuleDefFactory: NgModuleDefinitionFactory) { + // Attention: this ctor is called as top level function. + // Putting any logic in here will destroy closure tree shaking! + super(); + } + + create(parentInjector: Injector|null): NgModuleRef { + initServicesIfNeeded(); + const def = resolveDefinition(this._ngModuleDefFactory); + return Services.createNgModuleRef( + this.moduleType, parentInjector || Injector.NULL, this._bootstrapComponents, def); + } +} diff --git a/packages/core/src/view/index.ts b/packages/core/src/view/index.ts index 9d15814d6e..4275d4490a 100644 --- a/packages/core/src/view/index.ts +++ b/packages/core/src/view/index.ts @@ -7,13 +7,13 @@ */ export {anchorDef, elementDef} from './element'; +export {clearProviderOverrides, createNgModuleFactory, overrideProvider} from './entrypoint'; export {ngContentDef} from './ng_content'; export {moduleDef, moduleProvideDef} from './ng_module'; export {directiveDef, pipeDef, providerDef} from './provider'; export {pureArrayDef, pureObjectDef, purePipeDef} from './pure_expression'; export {queryDef} from './query'; export {ViewRef_, createComponentFactory, getComponentViewDefinitionFactory, nodeValue} from './refs'; -export {createNgModuleFactory} from './refs'; export {initServicesIfNeeded} from './services'; export {textDef} from './text'; export {EMPTY_ARRAY, EMPTY_MAP, createRendererType2, elementEventFullName, inlineInterpolate, interpolate, rootRenderNodes, tokenKey, unwrapValue} from './util'; diff --git a/packages/core/src/view/ng_module.ts b/packages/core/src/view/ng_module.ts index d130b6767b..c7c464aa34 100644 --- a/packages/core/src/view/ng_module.ts +++ b/packages/core/src/view/ng_module.ts @@ -10,7 +10,7 @@ import {Injector, THROW_IF_NOT_FOUND} from '../di/injector'; import {NgModuleRef} from '../linker/ng_module_factory'; import {DepDef, DepFlags, NgModuleData, NgModuleDefinition, NgModuleDefinitionFactory, NgModuleProviderDef, NodeFlags} from './types'; -import {tokenKey} from './util'; +import {splitDepsDsl, tokenKey} from './util'; const NOT_CREATED = new Object(); @@ -20,17 +20,7 @@ const NgModuleRefTokenKey = tokenKey(NgModuleRef); export function moduleProvideDef( flags: NodeFlags, token: any, value: any, deps: ([DepFlags, any] | any)[]): NgModuleProviderDef { - const depDefs: DepDef[] = deps.map(value => { - let token: any; - let flags: DepFlags; - if (Array.isArray(value)) { - [flags, token] = value; - } else { - flags = DepFlags.None; - token = value; - } - return {flags, token, tokenKey: tokenKey(token)}; - }); + const depDefs = splitDepsDsl(deps); return { // will bet set by the module definition index: -1, @@ -97,16 +87,16 @@ function _createProviderInstance(ngModule: NgModuleData, providerDef: NgModulePr let injectable: any; switch (providerDef.flags & NodeFlags.Types) { case NodeFlags.TypeClassProvider: - injectable = _createClass(ngModule, providerDef !.value, providerDef !.deps); + injectable = _createClass(ngModule, providerDef.value, providerDef.deps); break; case NodeFlags.TypeFactoryProvider: - injectable = _callFactory(ngModule, providerDef !.value, providerDef !.deps); + injectable = _callFactory(ngModule, providerDef.value, providerDef.deps); break; case NodeFlags.TypeUseExistingProvider: - injectable = resolveNgModuleDep(ngModule, providerDef !.deps[0]); + injectable = resolveNgModuleDep(ngModule, providerDef.deps[0]); break; case NodeFlags.TypeValueProvider: - injectable = providerDef !.value; + injectable = providerDef.value; break; } return injectable; diff --git a/packages/core/src/view/provider.ts b/packages/core/src/view/provider.ts index 9ca128f44e..be71c31a1f 100644 --- a/packages/core/src/view/provider.ts +++ b/packages/core/src/view/provider.ts @@ -15,7 +15,7 @@ import {Renderer as RendererV1, Renderer2} from '../render/api'; import {createChangeDetectorRef, createInjector, createRendererV1} from './refs'; import {BindingDef, BindingFlags, DepDef, DepFlags, NodeDef, NodeFlags, OutputDef, OutputType, ProviderData, QueryValueType, Services, ViewData, ViewFlags, ViewState, asElementData, asProviderData} from './types'; -import {calcBindingFlags, checkBinding, dispatchEvent, isComponentView, splitMatchedQueriesDsl, tokenKey, viewParentEl} from './util'; +import {calcBindingFlags, checkBinding, dispatchEvent, isComponentView, splitDepsDsl, splitMatchedQueriesDsl, tokenKey, viewParentEl} from './util'; const RendererV1TokenKey = tokenKey(RendererV1); const Renderer2TokenKey = tokenKey(Renderer2); @@ -78,17 +78,7 @@ export function _def( bindings = []; } - const depDefs: DepDef[] = deps.map(value => { - let token: any; - let flags: DepFlags; - if (Array.isArray(value)) { - [flags, token] = value; - } else { - flags = DepFlags.None; - token = value; - } - return {flags, token, tokenKey: tokenKey(token)}; - }); + const depDefs = splitDepsDsl(deps); return { // will bet set by the view definition diff --git a/packages/core/src/view/refs.ts b/packages/core/src/view/refs.ts index f38e86f469..2318ca6f45 100644 --- a/packages/core/src/view/refs.ts +++ b/packages/core/src/view/refs.ts @@ -12,7 +12,7 @@ import {Injector} from '../di/injector'; import {ComponentFactory, ComponentRef} from '../linker/component_factory'; import {ComponentFactoryBoundToModule, ComponentFactoryResolver} from '../linker/component_factory_resolver'; import {ElementRef} from '../linker/element_ref'; -import {InternalNgModuleRef, NgModuleFactory, NgModuleRef} from '../linker/ng_module_factory'; +import {InternalNgModuleRef, NgModuleRef} from '../linker/ng_module_factory'; import {TemplateRef} from '../linker/template_ref'; import {ViewContainerRef} from '../linker/view_container_ref'; import {EmbeddedViewRef, InternalViewRef, ViewRef} from '../linker/view_ref'; @@ -22,7 +22,7 @@ import {stringify} from '../util'; import {VERSION} from '../version'; import {callNgModuleLifecycle, initNgModule, resolveNgModuleDep} from './ng_module'; -import {DepFlags, ElementData, NgModuleData, NgModuleDefinition, NgModuleDefinitionFactory, NodeDef, NodeFlags, Services, TemplateData, ViewContainerData, ViewData, ViewDefinitionFactory, ViewState, asElementData, asProviderData, asTextData} from './types'; +import {DepFlags, ElementData, NgModuleData, NgModuleDefinition, NodeDef, NodeFlags, Services, TemplateData, ViewContainerData, ViewData, ViewDefinitionFactory, ViewState, asElementData, asProviderData, asTextData} from './types'; import {markParentViewsForCheck, resolveDefinition, rootRenderNodes, splitNamespace, tokenKey, viewParentEl} from './util'; import {attachEmbeddedView, detachEmbeddedView, moveEmbeddedView, renderDetachView} from './view_attach'; @@ -43,14 +43,6 @@ export function getComponentViewDefinitionFactory(componentFactory: ComponentFac return (componentFactory as ComponentFactory_).viewDefFactory; } -// Attention: this function is called as top level function. -// Putting any logic in here will destroy closure tree shaking! -export function createNgModuleFactory( - ngModuleType: Type, bootstrapComponents: Type[], - defFactory: NgModuleDefinitionFactory): NgModuleFactory { - return new NgModuleFactory_(ngModuleType, bootstrapComponents, defFactory); -} - class ComponentFactory_ extends ComponentFactory { /** * @internal @@ -312,7 +304,8 @@ class TemplateRef_ extends TemplateRef implements TemplateData { constructor(private _parentView: ViewData, private _def: NodeDef) { super(); } createEmbeddedView(context: any): EmbeddedViewRef { - return new ViewRef_(Services.createEmbeddedView(this._parentView, this._def, context)); + return new ViewRef_(Services.createEmbeddedView( + this._parentView, this._def, this._def.element !.template !, context)); } get elementRef(): ElementRef { @@ -464,22 +457,10 @@ class RendererAdapter implements RendererV1 { } -class NgModuleFactory_ extends NgModuleFactory { - constructor( - private _moduleType: Type, private _bootstrapComponents: Type[], - private _ngModuleDefFactory: NgModuleDefinitionFactory, ) { - // Attention: this ctor is called as top level function. - // Putting any logic in here will destroy closure tree shaking! - super(); - } - - get moduleType(): Type { return this._moduleType; } - - create(parentInjector: Injector|null): NgModuleRef { - const def = resolveDefinition(this._ngModuleDefFactory); - return new NgModuleRef_( - this._moduleType, parentInjector || Injector.NULL, this._bootstrapComponents, def); - } +export function createNgModuleRef( + moduleType: Type, parent: Injector, bootstrapComponents: Type[], + def: NgModuleDefinition): NgModuleRef { + return new NgModuleRef_(moduleType, parent, bootstrapComponents, def); } class NgModuleRef_ implements NgModuleData, InternalNgModuleRef { @@ -488,8 +469,8 @@ class NgModuleRef_ implements NgModuleData, InternalNgModuleRef { public _providers: any[]; constructor( - private _moduleType: any, public _parent: Injector, public _bootstrapComponents: Type[], - public _def: NgModuleDefinition) { + private _moduleType: Type, public _parent: Injector, + public _bootstrapComponents: Type[], public _def: NgModuleDefinition) { initNgModule(this); } diff --git a/packages/core/src/view/services.ts b/packages/core/src/view/services.ts index d7aa1bf716..b33fcc0f01 100644 --- a/packages/core/src/view/services.ts +++ b/packages/core/src/view/services.ts @@ -13,14 +13,15 @@ import {ErrorHandler} from '../error_handler'; import {NgModuleRef} from '../linker/ng_module_factory'; import {Renderer2, RendererFactory2, RendererStyleFlags2, RendererType2} from '../render/api'; import {Sanitizer} from '../security'; +import {Type} from '../type'; import {isViewDebugError, viewDestroyedError, viewWrappedDebugError} from './errors'; import {resolveDep} from './provider'; import {dirtyParentQueries, getQueryValue} from './query'; -import {createInjector} from './refs'; -import {ArgumentType, BindingFlags, CheckType, DebugContext, ElementData, NodeDef, NodeFlags, NodeLogger, RootData, Services, ViewData, ViewDefinition, ViewState, asElementData, asPureExpressionData} from './types'; -import {NOOP, isComponentView, renderNode, viewParentEl} from './util'; -import {checkAndUpdateNode, checkAndUpdateView, checkNoChangesNode, checkNoChangesView, createEmbeddedView, createRootView, destroyView} from './view'; +import {createInjector, createNgModuleRef} from './refs'; +import {ArgumentType, BindingFlags, CheckType, DebugContext, DepDef, ElementData, NgModuleDefinition, NgModuleProviderDef, NodeDef, NodeFlags, NodeLogger, ProviderOverride, RootData, Services, ViewData, ViewDefinition, ViewState, asElementData, asPureExpressionData} from './types'; +import {NOOP, isComponentView, renderNode, splitDepsDsl, viewParentEl} from './util'; +import {checkAndUpdateNode, checkAndUpdateView, checkNoChangesNode, checkNoChangesView, createComponentView, createEmbeddedView, createRootView, destroyView} from './view'; let initialized = false; @@ -34,6 +35,10 @@ export function initServicesIfNeeded() { Services.setCurrentNode = services.setCurrentNode; Services.createRootView = services.createRootView; Services.createEmbeddedView = services.createEmbeddedView; + Services.createComponentView = services.createComponentView; + Services.createNgModuleRef = services.createNgModuleRef; + Services.overrideProvider = services.overrideProvider; + Services.clearProviderOverrides = services.clearProviderOverrides; Services.checkAndUpdateView = services.checkAndUpdateView; Services.checkNoChangesView = services.checkNoChangesView; Services.destroyView = services.destroyView; @@ -50,6 +55,10 @@ function createProdServices() { setCurrentNode: () => {}, createRootView: createProdRootView, createEmbeddedView: createEmbeddedView, + createComponentView: createComponentView, + createNgModuleRef: createNgModuleRef, + overrideProvider: NOOP, + clearProviderOverrides: NOOP, checkAndUpdateView: checkAndUpdateView, checkNoChangesView: checkNoChangesView, destroyView: destroyView, @@ -72,13 +81,17 @@ function createDebugServices() { setCurrentNode: debugSetCurrentNode, createRootView: debugCreateRootView, createEmbeddedView: debugCreateEmbeddedView, + createComponentView: debugCreateComponentView, + createNgModuleRef: debugCreateNgModuleRef, + overrideProvider: debugOverrideProvider, + clearProviderOverrides: debugClearProviderOverrides, checkAndUpdateView: debugCheckAndUpdateView, checkNoChangesView: debugCheckNoChangesView, destroyView: debugDestroyView, createDebugContext: (view: ViewData, nodeIndex: number) => new DebugContext_(view, nodeIndex), handleEvent: debugHandleEvent, updateDirectives: debugUpdateDirectives, - updateRenderer: debugUpdateRenderer + updateRenderer: debugUpdateRenderer, }; } @@ -98,7 +111,9 @@ function debugCreateRootView( const root = createRootData( elInjector, ngModule, new DebugRendererFactory2(rendererFactory), projectableNodes, rootSelectorOrNode); - return callWithDebugContext(DebugAction.create, createRootView, null, [root, def, context]); + const defWithOverride = applyProviderOverridesToView(def); + return callWithDebugContext( + DebugAction.create, createRootView, null, [root, defWithOverride, context]); } function createRootData( @@ -114,6 +129,136 @@ function createRootData( }; } +function debugCreateEmbeddedView( + parentView: ViewData, anchorDef: NodeDef, viewDef: ViewDefinition, context?: any): ViewData { + const defWithOverride = applyProviderOverridesToView(viewDef); + return callWithDebugContext( + DebugAction.create, createEmbeddedView, null, + [parentView, anchorDef, defWithOverride, context]); +} + +function debugCreateComponentView( + parentView: ViewData, nodeDef: NodeDef, viewDef: ViewDefinition, hostElement: any): ViewData { + const defWithOverride = applyProviderOverridesToView(viewDef); + return callWithDebugContext( + DebugAction.create, createComponentView, null, + [parentView, nodeDef, defWithOverride, hostElement]); +} + +function debugCreateNgModuleRef( + moduleType: Type, parentInjector: Injector, bootstrapComponents: Type[], + def: NgModuleDefinition): NgModuleRef { + const defWithOverride = applyProviderOverridesToNgModule(def); + return createNgModuleRef(moduleType, parentInjector, bootstrapComponents, defWithOverride); +} + +const providerOverrides = new Map(); + +function debugOverrideProvider(override: ProviderOverride) { + providerOverrides.set(override.token, override); +} + +function debugClearProviderOverrides() { + providerOverrides.clear(); +} + +// Notes about the algorithm: +// 1) Locate the providers of an element and check if one of them was overwritten +// 2) Change the providers of that element +// +// We only create new datastructures if we need to, to keep perf impact +// reasonable. +function applyProviderOverridesToView(def: ViewDefinition): ViewDefinition { + if (providerOverrides.size === 0) { + return def; + } + const elementIndicesWithOverwrittenProviders = findElementIndicesWithOverwrittenProviders(def); + if (elementIndicesWithOverwrittenProviders.length === 0) { + return def; + } + // clone the whole view definition, + // as it maintains references between the nodes that are hard to update. + def = def.factory !(() => NOOP); + for (let i = 0; i < elementIndicesWithOverwrittenProviders.length; i++) { + applyProviderOverridesToElement(def, elementIndicesWithOverwrittenProviders[i]); + } + return def; + + function findElementIndicesWithOverwrittenProviders(def: ViewDefinition): number[] { + const elIndicesWithOverwrittenProviders: number[] = []; + let lastElementDef: NodeDef|null = null; + for (let i = 0; i < def.nodes.length; i++) { + const nodeDef = def.nodes[i]; + if (nodeDef.flags & NodeFlags.TypeElement) { + lastElementDef = nodeDef; + } + if (lastElementDef && nodeDef.flags & NodeFlags.CatProviderNoDirective && + providerOverrides.has(nodeDef.provider !.token)) { + elIndicesWithOverwrittenProviders.push(lastElementDef !.index); + lastElementDef = null; + } + } + return elIndicesWithOverwrittenProviders; + } + + function applyProviderOverridesToElement(viewDef: ViewDefinition, elIndex: number) { + for (let i = elIndex + 1; i < viewDef.nodes.length; i++) { + const nodeDef = viewDef.nodes[i]; + if (nodeDef.flags & NodeFlags.TypeElement) { + // stop at the next element + return; + } + if (nodeDef.flags & NodeFlags.CatProviderNoDirective) { + // Make all providers lazy, so that we don't get into trouble + // with ordering problems of providers on the same element + nodeDef.flags |= NodeFlags.LazyProvider; + const provider = nodeDef.provider !; + const override = providerOverrides.get(provider.token); + if (override) { + nodeDef.flags = (nodeDef.flags & ~NodeFlags.CatProviderNoDirective) | override.flags; + provider.deps = splitDepsDsl(override.deps); + provider.value = override.value; + } + } + } + } +} + +// Notes about the algorithm: +// We only create new datastructures if we need to, to keep perf impact +// reasonable. +function applyProviderOverridesToNgModule(def: NgModuleDefinition): NgModuleDefinition { + if (providerOverrides.size === 0 || !hasOverrrides(def)) { + return def; + } + // clone the whole view definition, + // as it maintains references between the nodes that are hard to update. + def = def.factory !(() => NOOP); + applyProviderOverrides(def); + return def; + + function hasOverrrides(def: NgModuleDefinition): boolean { + return def.providers.some( + node => + !!(node.flags & NodeFlags.CatProviderNoDirective) && providerOverrides.has(node.token)); + } + + function applyProviderOverrides(def: NgModuleDefinition) { + for (let i = 0; i < def.providers.length; i++) { + const provider = def.providers[i]; + // Make all providers lazy, so that we don't get into trouble + // with ordering problems of providers on the same element + provider.flags |= NodeFlags.LazyProvider; + const override = providerOverrides.get(provider.token); + if (override) { + provider.flags = (provider.flags & ~NodeFlags.CatProviderNoDirective) | override.flags; + provider.deps = splitDepsDsl(override.deps); + provider.value = override.value; + } + } + } +} + function prodCheckAndUpdateNode( view: ViewData, nodeIndex: number, argStyle: ArgumentType, v0?: any, v1?: any, v2?: any, v3?: any, v4?: any, v5?: any, v6?: any, v7?: any, v8?: any, v9?: any): any { @@ -134,11 +279,6 @@ function prodCheckNoChangesNode( undefined; } -function debugCreateEmbeddedView(parent: ViewData, anchorDef: NodeDef, context?: any): ViewData { - return callWithDebugContext( - DebugAction.create, createEmbeddedView, null, [parent, anchorDef, context]); -} - function debugCheckAndUpdateView(view: ViewData) { return callWithDebugContext(DebugAction.detectChanges, checkAndUpdateView, null, [view]); } diff --git a/packages/core/src/view/types.ts b/packages/core/src/view/types.ts index c231d1d4c3..24026fb127 100644 --- a/packages/core/src/view/types.ts +++ b/packages/core/src/view/types.ts @@ -14,6 +14,7 @@ import {TemplateRef} from '../linker/template_ref'; import {ViewContainerRef} from '../linker/view_container_ref'; import {Renderer2, RendererFactory2, RendererType2} from '../render/api'; import {Sanitizer, SecurityContext} from '../security'; +import {Type} from '../type'; // ------------------------------------- // Defs @@ -169,8 +170,9 @@ export const enum NodeFlags { PrivateProvider = 1 << 13, TypeDirective = 1 << 14, Component = 1 << 15, - CatProvider = TypeValueProvider | TypeClassProvider | TypeFactoryProvider | - TypeUseExistingProvider | TypeDirective, + CatProviderNoDirective = + TypeValueProvider | TypeClassProvider | TypeFactoryProvider | TypeUseExistingProvider, + CatProvider = CatProviderNoDirective | TypeDirective, OnInit = 1 << 16, OnDestroy = 1 << 17, DoCheck = 1 << 18, @@ -495,12 +497,27 @@ export abstract class DebugContext { export const enum CheckType {CheckAndUpdate, CheckNoChanges} +export interface ProviderOverride { + token: any; + flags: NodeFlags; + value: any; + deps: ([DepFlags, any]|any)[]; +} + export interface Services { setCurrentNode(view: ViewData, nodeIndex: number): void; createRootView( injector: Injector, projectableNodes: any[][], rootSelectorOrNode: string|any, def: ViewDefinition, ngModule: NgModuleRef, context?: any): ViewData; - createEmbeddedView(parent: ViewData, anchorDef: NodeDef, context?: any): ViewData; + createEmbeddedView(parent: ViewData, anchorDef: NodeDef, viewDef: ViewDefinition, context?: any): + ViewData; + createComponentView( + parentView: ViewData, nodeDef: NodeDef, viewDef: ViewDefinition, hostElement: any): ViewData; + createNgModuleRef( + moduleType: Type, parent: Injector, bootstrapComponents: Type[], + def: NgModuleDefinition): NgModuleRef; + overrideProvider(override: ProviderOverride): void; + clearProviderOverrides(): void; checkAndUpdateView(view: ViewData): void; checkNoChangesView(view: ViewData): void; destroyView(view: ViewData): void; @@ -522,6 +539,10 @@ export const Services: Services = { setCurrentNode: undefined !, createRootView: undefined !, createEmbeddedView: undefined !, + createComponentView: undefined !, + createNgModuleRef: undefined !, + overrideProvider: undefined !, + clearProviderOverrides: undefined !, checkAndUpdateView: undefined !, checkNoChangesView: undefined !, destroyView: undefined !, diff --git a/packages/core/src/view/util.ts b/packages/core/src/view/util.ts index 7d0a1e2bbf..a10511389b 100644 --- a/packages/core/src/view/util.ts +++ b/packages/core/src/view/util.ts @@ -12,7 +12,7 @@ import {RendererType2} from '../render/api'; import {looseIdentical, stringify} from '../util'; import {expressionChangedAfterItHasBeenCheckedError} from './errors'; -import {BindingDef, BindingFlags, Definition, DefinitionFactory, ElementData, NodeDef, NodeFlags, QueryValueType, Services, ViewData, ViewDefinition, ViewDefinitionFactory, ViewFlags, ViewState, asElementData, asTextData} from './types'; +import {BindingDef, BindingFlags, Definition, DefinitionFactory, DepDef, DepFlags, ElementData, NodeDef, NodeFlags, QueryValueType, Services, ViewData, ViewDefinition, ViewDefinitionFactory, ViewFlags, ViewState, asElementData, asTextData} from './types'; export const NOOP: any = () => {}; @@ -203,6 +203,20 @@ export function splitMatchedQueriesDsl( return {matchedQueries, references, matchedQueryIds}; } +export function splitDepsDsl(deps: ([DepFlags, any] | any)[]): DepDef[] { + return deps.map(value => { + let token: any; + let flags: DepFlags; + if (Array.isArray(value)) { + [flags, token] = value; + } else { + flags = DepFlags.None; + token = value; + } + return {flags, token, tokenKey: tokenKey(token)}; + }); +} + export function getParentRenderElement(view: ViewData, renderHost: any, def: NodeDef): any { let renderParent = def.renderParent; if (renderParent) { diff --git a/packages/core/src/view/view.ts b/packages/core/src/view/view.ts index 9a5d288443..70ddaee437 100644 --- a/packages/core/src/view/view.ts +++ b/packages/core/src/view/view.ts @@ -184,11 +184,11 @@ function validateNode(parent: NodeDef | null, node: NodeDef, nodeCount: number) } } -export function createEmbeddedView(parent: ViewData, anchorDef: NodeDef, context?: any): ViewData { +export function createEmbeddedView( + parent: ViewData, anchorDef: NodeDef, viewDef: ViewDefinition, context?: any): ViewData { // embedded views are seen as siblings to the anchor, so we need // to get the parent of the anchor and use it as parentIndex. - const view = - createView(parent.root, parent.renderer, parent, anchorDef, anchorDef.element !.template !); + const view = createView(parent.root, parent.renderer, parent, anchorDef, viewDef); initView(view, parent.component, context); createViewNodes(view); return view; @@ -201,6 +201,19 @@ export function createRootView(root: RootData, def: ViewDefinition, context?: an return view; } +export function createComponentView( + parentView: ViewData, nodeDef: NodeDef, viewDef: ViewDefinition, hostElement: any): ViewData { + const rendererType = nodeDef.element !.componentRendererType; + let compRenderer: Renderer2; + if (!rendererType) { + compRenderer = parentView.root.renderer; + } else { + compRenderer = parentView.root.rendererFactory.createRenderer(hostElement, rendererType); + } + return createView( + parentView.root, compRenderer, parentView, nodeDef.element !.componentProvider, viewDef); +} + function createView( root: RootData, renderer: Renderer2, parent: ViewData | null, parentNodeDef: NodeDef | null, def: ViewDefinition): ViewData { @@ -241,15 +254,7 @@ function createViewNodes(view: ViewData) { let componentView: ViewData = undefined !; if (nodeDef.flags & NodeFlags.ComponentView) { const compViewDef = resolveDefinition(nodeDef.element !.componentView !); - const rendererType = nodeDef.element !.componentRendererType; - let compRenderer: Renderer2; - if (!rendererType) { - compRenderer = view.root.renderer; - } else { - compRenderer = view.root.rendererFactory.createRenderer(el, rendererType); - } - componentView = createView( - view.root, compRenderer, view, nodeDef.element !.componentProvider, compViewDef); + componentView = Services.createComponentView(view, nodeDef, compViewDef, el); } listenToElementOutputs(view, componentView, nodeDef, el); nodeData = { diff --git a/packages/core/test/linker/ng_module_integration_spec.ts b/packages/core/test/linker/ng_module_integration_spec.ts index f62ac13032..0991efe7dc 100644 --- a/packages/core/test/linker/ng_module_integration_spec.ts +++ b/packages/core/test/linker/ng_module_integration_spec.ts @@ -787,15 +787,46 @@ function declareTests({useJit}: {useJit: boolean}) { expect(child.get(Injector)).toBe(child); }); - it('should allow to inject lazy providers via Injector.get from an eager provider that is declared earlier', - () => { - @NgModule({providers: [{provide: 'a', useFactory: () => 'aValue'}]}) - class SomeModule { - public a: string; - constructor(injector: Injector) { this.a = injector.get('a'); } - } - expect(createModule(SomeModule).instance.a).toBe('aValue'); - }); + describe('injecting lazy providers into an eager provider via Injector.get', () => { + + it('should inject providers that were declared before it', () => { + @NgModule({ + providers: [ + {provide: 'lazy', useFactory: () => 'lazyValue'}, + { + provide: 'eager', + useFactory: (i: Injector) => `eagerValue: ${i.get('lazy')}`, + deps: [Injector] + }, + ] + }) + class MyModule { + // NgModule is eager, which makes all of its deps eager + constructor(@Inject('eager') eager: any) {} + } + + expect(createModule(MyModule).injector.get('eager')).toBe('eagerValue: lazyValue'); + }); + + it('should inject providers that were declared after it', () => { + @NgModule({ + providers: [ + { + provide: 'eager', + useFactory: (i: Injector) => `eagerValue: ${i.get('lazy')}`, + deps: [Injector] + }, + {provide: 'lazy', useFactory: () => 'lazyValue'}, + ] + }) + class MyModule { + // NgModule is eager, which makes all of its deps eager + constructor(@Inject('eager') eager: any) {} + } + + expect(createModule(MyModule).injector.get('eager')).toBe('eagerValue: lazyValue'); + }); + }); it('should throw when no provider defined', () => { const injector = createInjector([]); diff --git a/packages/core/test/linker/view_injector_integration_spec.ts b/packages/core/test/linker/view_injector_integration_spec.ts index bc261656e4..96e3f6605f 100644 --- a/packages/core/test/linker/view_injector_integration_spec.ts +++ b/packages/core/test/linker/view_injector_integration_spec.ts @@ -342,6 +342,53 @@ export function main() { expect(created).toBe(true); }); + describe('injecting lazy providers into an eager provider via Injector.get', () => { + + it('should inject providers that were declared before it', () => { + @Component({ + template: '', + providers: [ + {provide: 'lazy', useFactory: () => 'lazyValue'}, + { + provide: 'eager', + useFactory: (i: Injector) => `eagerValue: ${i.get('lazy')}`, + deps: [Injector] + }, + ] + }) + class MyComp { + // Component is eager, which makes all of its deps eager + constructor(@Inject('eager') eager: any) {} + } + + const ctx = + TestBed.configureTestingModule({declarations: [MyComp]}).createComponent(MyComp); + expect(ctx.debugElement.injector.get('eager')).toBe('eagerValue: lazyValue'); + }); + + it('should inject providers that were declared after it', () => { + @Component({ + template: '', + providers: [ + { + provide: 'eager', + useFactory: (i: Injector) => `eagerValue: ${i.get('lazy')}`, + deps: [Injector] + }, + {provide: 'lazy', useFactory: () => 'lazyValue'}, + ] + }) + class MyComp { + // Component is eager, which makes all of its deps eager + constructor(@Inject('eager') eager: any) {} + } + + const ctx = + TestBed.configureTestingModule({declarations: [MyComp]}).createComponent(MyComp); + expect(ctx.debugElement.injector.get('eager')).toBe('eagerValue: lazyValue'); + }); + }); + it('should allow injecting lazy providers via Injector.get from an eager provider that is declared earlier', () => { @Component({providers: [{provide: 'a', useFactory: () => 'aValue'}], template: ''}) diff --git a/packages/core/test/view/embedded_view_spec.ts b/packages/core/test/view/embedded_view_spec.ts index 59df21d1a1..fee7481e5c 100644 --- a/packages/core/test/view/embedded_view_spec.ts +++ b/packages/core/test/view/embedded_view_spec.ts @@ -11,7 +11,7 @@ import {ArgumentType, BindingFlags, NodeCheckFn, NodeDef, NodeFlags, RootData, S import {inject} from '@angular/core/testing'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; -import {createRootView, isBrowser} from './helper'; +import {createEmbeddedView, createRootView, isBrowser} from './helper'; export function main() { describe(`Embedded Views`, () => { @@ -45,8 +45,7 @@ export function main() { ]), parentContext); - const childView = - Services.createEmbeddedView(parentView, parentView.def.nodes[1], childContext); + const childView = createEmbeddedView(parentView, parentView.def.nodes[1], childContext); expect(childView.component).toBe(parentContext); expect(childView.context).toBe(childContext); }); @@ -64,8 +63,8 @@ export function main() { ])); const viewContainerData = asElementData(parentView, 1); - const childView0 = Services.createEmbeddedView(parentView, parentView.def.nodes[1]); - const childView1 = Services.createEmbeddedView(parentView, parentView.def.nodes[2]); + const childView0 = createEmbeddedView(parentView, parentView.def.nodes[1]); + const childView1 = createEmbeddedView(parentView, parentView.def.nodes[2]); attachEmbeddedView(parentView, viewContainerData, 0, childView0); attachEmbeddedView(parentView, viewContainerData, 1, childView1); @@ -95,8 +94,8 @@ export function main() { ])); const viewContainerData = asElementData(parentView, 1); - const childView0 = Services.createEmbeddedView(parentView, parentView.def.nodes[1]); - const childView1 = Services.createEmbeddedView(parentView, parentView.def.nodes[2]); + const childView0 = createEmbeddedView(parentView, parentView.def.nodes[1]); + const childView1 = createEmbeddedView(parentView, parentView.def.nodes[2]); attachEmbeddedView(parentView, viewContainerData, 0, childView0); attachEmbeddedView(parentView, viewContainerData, 1, childView1); @@ -119,7 +118,7 @@ export function main() { elementDef(NodeFlags.None, null !, null !, 0, 'span', [['name', 'after']]) ])); - const childView0 = Services.createEmbeddedView(parentView, parentView.def.nodes[0]); + const childView0 = createEmbeddedView(parentView, parentView.def.nodes[0]); attachEmbeddedView(parentView, asElementData(parentView, 0), 0, childView0); const rootNodes = rootRenderNodes(parentView); @@ -146,7 +145,7 @@ export function main() { update)) ])); - const childView0 = Services.createEmbeddedView(parentView, parentView.def.nodes[1]); + const childView0 = createEmbeddedView(parentView, parentView.def.nodes[1]); attachEmbeddedView(parentView, asElementData(parentView, 1), 0, childView0); @@ -180,7 +179,7 @@ export function main() { ])) ])); - const childView0 = Services.createEmbeddedView(parentView, parentView.def.nodes[1]); + const childView0 = createEmbeddedView(parentView, parentView.def.nodes[1]); attachEmbeddedView(parentView, asElementData(parentView, 1), 0, childView0); Services.destroyView(parentView); diff --git a/packages/core/test/view/helper.ts b/packages/core/test/view/helper.ts index 5c90a3a4f0..cdcf61a2f1 100644 --- a/packages/core/test/view/helper.ts +++ b/packages/core/test/view/helper.ts @@ -7,7 +7,7 @@ */ import {Injector, NgModuleRef, RootRenderer, Sanitizer} from '@angular/core'; -import {ArgumentType, NodeCheckFn, RootData, Services, ViewData, ViewDefinition, initServicesIfNeeded} from '@angular/core/src/view/index'; +import {ArgumentType, NodeCheckFn, NodeDef, RootData, Services, ViewData, ViewDefinition, initServicesIfNeeded} from '@angular/core/src/view/index'; import {TestBed} from '@angular/core/testing'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; @@ -37,6 +37,10 @@ export function createRootView( TestBed.get(NgModuleRef), context); } +export function createEmbeddedView(parent: ViewData, anchorDef: NodeDef, context?: any): ViewData { + return Services.createEmbeddedView(parent, anchorDef, anchorDef.element !.template !, context); +} + export let removeNodes: Node[]; beforeEach(() => { removeNodes = []; }); afterEach(() => { removeNodes.forEach((node) => getDOM().remove(node)); }); diff --git a/packages/core/test/view/ng_content_spec.ts b/packages/core/test/view/ng_content_spec.ts index 35a622a983..1c176b0dad 100644 --- a/packages/core/test/view/ng_content_spec.ts +++ b/packages/core/test/view/ng_content_spec.ts @@ -10,7 +10,7 @@ import {Injector, RenderComponentType, RootRenderer, Sanitizer, SecurityContext, import {DebugContext, NodeDef, NodeFlags, RootData, Services, ViewData, ViewDefinition, ViewDefinitionFactory, ViewFlags, ViewHandleEventFn, ViewUpdateFn, anchorDef, asElementData, asProviderData, asTextData, attachEmbeddedView, detachEmbeddedView, directiveDef, elementDef, ngContentDef, rootRenderNodes, textDef, viewDef} from '@angular/core/src/view/index'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; -import {createRootView, isBrowser} from './helper'; +import {createEmbeddedView, createRootView, isBrowser} from './helper'; export function main() { describe(`View NgContent`, () => { @@ -121,7 +121,7 @@ export function main() { ]))); const componentView = asElementData(view, 0).componentView; - const view0 = Services.createEmbeddedView(componentView, componentView.def.nodes[1]); + const view0 = createEmbeddedView(componentView, componentView.def.nodes[1]); attachEmbeddedView(view, asElementData(componentView, 1), 0, view0); expect(getDOM().childNodes(getDOM().firstChild(rootNodes[0])).length).toBe(3); diff --git a/packages/core/test/view/query_spec.ts b/packages/core/test/view/query_spec.ts index 9cd9bc92f0..4a5db92503 100644 --- a/packages/core/test/view/query_spec.ts +++ b/packages/core/test/view/query_spec.ts @@ -12,7 +12,7 @@ import {BindingFlags, DebugContext, NodeDef, NodeFlags, QueryBindingType, QueryV import {inject} from '@angular/core/testing'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; -import {createRootView} from './helper'; +import {createEmbeddedView, createRootView} from './helper'; export function main() { describe(`Query Views`, () => { @@ -155,7 +155,7 @@ export function main() { ...contentQueryProviders(), ])); - const childView = Services.createEmbeddedView(view, view.def.nodes[3]); + const childView = createEmbeddedView(view, view.def.nodes[3]); attachEmbeddedView(view, asElementData(view, 3), 0, childView); Services.checkAndUpdateView(view); @@ -183,7 +183,7 @@ export function main() { anchorDef(NodeFlags.EmbeddedViews, null !, null !, 0), ])); - const childView = Services.createEmbeddedView(view, view.def.nodes[3]); + const childView = createEmbeddedView(view, view.def.nodes[3]); // attach at a different place than the one where the template was defined attachEmbeddedView(view, asElementData(view, 7), 0, childView); @@ -214,7 +214,7 @@ export function main() { const qs: QueryService = asProviderData(view, 1).instance; expect(qs.a.length).toBe(0); - const childView = Services.createEmbeddedView(view, view.def.nodes[3]); + const childView = createEmbeddedView(view, view.def.nodes[3]); attachEmbeddedView(view, asElementData(view, 3), 0, childView); Services.checkAndUpdateView(view); @@ -245,7 +245,7 @@ export function main() { expect(comp.a.length).toBe(0); const compView = asElementData(view, 0).componentView; - const childView = Services.createEmbeddedView(compView, compView.def.nodes[1]); + const childView = createEmbeddedView(compView, compView.def.nodes[1]); attachEmbeddedView(view, asElementData(compView, 1), 0, childView); Services.checkAndUpdateView(view); diff --git a/packages/core/testing/src/test_bed.ts b/packages/core/testing/src/test_bed.ts index 677340e15e..306f0a3d46 100644 --- a/packages/core/testing/src/test_bed.ts +++ b/packages/core/testing/src/test_bed.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {CompilerOptions, Component, Directive, InjectionToken, Injector, ModuleWithComponentFactories, NgModule, NgModuleFactory, NgModuleRef, NgZone, Pipe, PlatformRef, Provider, ReflectiveInjector, SchemaMetadata, Type, ɵERROR_COMPONENT_TYPE, ɵstringify as stringify} from '@angular/core'; +import {CompilerOptions, Component, Directive, InjectionToken, Injector, ModuleWithComponentFactories, NgModule, NgModuleFactory, NgModuleRef, NgZone, Optional, Pipe, PlatformRef, Provider, ReflectiveInjector, SchemaMetadata, SkipSelf, Type, ɵDepFlags as DepFlags, ɵERROR_COMPONENT_TYPE, ɵNodeFlags as NodeFlags, ɵclearProviderOverrides as clearProviderOverrides, ɵoverrideProvider as overrideProvider, ɵstringify as stringify} from '@angular/core'; import {AsyncTestCompleter} from './async_test_completer'; import {ComponentFixture} from './component_fixture'; @@ -141,6 +141,24 @@ export class TestBed implements Injector { return TestBed; } + + /** + * Overwrites all providers for the given token with the given provider definition. + */ + static overrideProvider(token: any, provider: { + useFactory: Function, + deps: any[], + }): void; + static overrideProvider(token: any, provider: {useValue: any;}): void; + static overrideProvider(token: any, provider: { + useFactory?: Function, + useValue?: any, + deps?: any[], + }): typeof TestBed { + getTestBed().overrideProvider(token, provider as any); + return TestBed; + } + static get(token: any, notFoundValue: any = Injector.THROW_IF_NOT_FOUND) { return getTestBed().get(token, notFoundValue); } @@ -212,6 +230,7 @@ export class TestBed implements Injector { } resetTestingModule() { + clearProviderOverrides(); this._compiler = null !; this._moduleOverrides = []; this._componentOverrides = []; @@ -364,6 +383,49 @@ export class TestBed implements Injector { this._pipeOverrides.push([pipe, override]); } + /** + * Overwrites all providers for the given token with the given provider definition. + */ + overrideProvider(token: any, provider: { + useFactory: Function, + deps: any[], + }): void; + overrideProvider(token: any, provider: {useValue: any;}): void; + overrideProvider(token: any, provider: { + useFactory?: Function, + useValue?: any, + deps?: any[], + }): void { + let flags: NodeFlags = 0; + let value: any; + if (provider.useFactory) { + flags |= NodeFlags.TypeFactoryProvider; + value = provider.useFactory; + } else { + flags |= NodeFlags.TypeValueProvider; + value = provider.useValue; + } + const deps = (provider.deps || []).map((dep) => { + let depFlags: DepFlags = DepFlags.None; + let depToken: any; + if (Array.isArray(dep)) { + dep.forEach((entry: any) => { + if (entry instanceof Optional) { + depFlags |= DepFlags.Optional; + } else if (entry instanceof SkipSelf) { + depFlags |= DepFlags.SkipSelf; + } else { + depToken = entry; + } + }); + } else { + depToken = dep; + } + return [depFlags, depToken]; + }); + overrideProvider({token, flags, deps, value}); + } + createComponent(component: Type): ComponentFixture { this._initIfNeeded(); const componentFactory = this._compiler.getComponentFactory(component); diff --git a/packages/platform-browser/test/testing_public_spec.ts b/packages/platform-browser/test/testing_public_spec.ts index cb9d3e19db..cd97306f7e 100644 --- a/packages/platform-browser/test/testing_public_spec.ts +++ b/packages/platform-browser/test/testing_public_spec.ts @@ -7,7 +7,7 @@ */ import {CompilerConfig, ResourceLoader} from '@angular/compiler'; -import {CUSTOM_ELEMENTS_SCHEMA, Component, Directive, Injectable, Input, NgModule, Pipe, ɵstringify as stringify} from '@angular/core'; +import {CUSTOM_ELEMENTS_SCHEMA, Compiler, Component, Directive, Inject, Injectable, Injector, Input, NgModule, Optional, Pipe, SkipSelf, ɵstringify as stringify} from '@angular/core'; import {TestBed, async, fakeAsync, getTestBed, inject, tick, withModule} from '@angular/core/testing'; import {expect} from '@angular/platform-browser/testing/src/matchers'; @@ -401,6 +401,277 @@ export function main() { }); }); + describe('overriding providers', () => { + describe('in NgModules', () => { + it('should support useValue', () => { + TestBed.configureTestingModule({ + providers: [ + {provide: 'a', useValue: 'aValue'}, + ] + }); + TestBed.overrideProvider('a', {useValue: 'mockValue'}); + expect(TestBed.get('a')).toBe('mockValue'); + }); + + it('should support useFactory', () => { + TestBed.configureTestingModule({ + providers: [ + {provide: 'dep', useValue: 'depValue'}, + {provide: 'a', useValue: 'aValue'}, + ] + }); + TestBed.overrideProvider( + 'a', {useFactory: (dep: any) => `mockA: ${dep}`, deps: ['dep']}); + expect(TestBed.get('a')).toBe('mockA: depValue'); + }); + + it('should support @Optional without matches', () => { + TestBed.configureTestingModule({ + providers: [ + {provide: 'a', useValue: 'aValue'}, + ] + }); + TestBed.overrideProvider( + 'a', {useFactory: (dep: any) => `mockA: ${dep}`, deps: [[new Optional(), 'dep']]}); + expect(TestBed.get('a')).toBe('mockA: null'); + }); + + it('should support Optional with matches', () => { + TestBed.configureTestingModule({ + providers: [ + {provide: 'dep', useValue: 'depValue'}, + {provide: 'a', useValue: 'aValue'}, + ] + }); + TestBed.overrideProvider( + 'a', {useFactory: (dep: any) => `mockA: ${dep}`, deps: [[new Optional(), 'dep']]}); + expect(TestBed.get('a')).toBe('mockA: depValue'); + }); + + it('should support SkipSelf', () => { + @NgModule({ + providers: [ + {provide: 'a', useValue: 'aValue'}, + {provide: 'dep', useValue: 'depValue'}, + ] + }) + class MyModule { + } + + TestBed.overrideProvider( + 'a', {useFactory: (dep: any) => `mockA: ${dep}`, deps: [[new SkipSelf(), 'dep']]}); + TestBed.configureTestingModule( + {providers: [{provide: 'dep', useValue: 'parentDepValue'}]}); + + const compiler = TestBed.get(Compiler) as Compiler; + const modFactory = compiler.compileModuleSync(MyModule); + expect(modFactory.create(getTestBed()).injector.get('a')).toBe('mockA: parentDepValue'); + }); + + describe('injecting eager providers into an eager overwritten provider', () => { + @NgModule({ + providers: [ + {provide: 'a', useFactory: () => 'aValue'}, + {provide: 'b', useFactory: () => 'bValue'}, + ] + }) + class MyModule { + // NgModule is eager, which makes all of its deps eager + constructor(@Inject('a') a: any, @Inject('b') b: any) {} + } + + it('should inject providers that were declared before', () => { + TestBed.configureTestingModule({imports: [MyModule]}); + TestBed.overrideProvider( + 'b', {useFactory: (a: string) => `mockB: ${a}`, deps: ['a']}); + + expect(TestBed.get('b')).toBe('mockB: aValue'); + }); + + it('should inject providers that were declared afterwards', () => { + TestBed.configureTestingModule({imports: [MyModule]}); + TestBed.overrideProvider( + 'a', {useFactory: (b: string) => `mockA: ${b}`, deps: ['b']}); + + expect(TestBed.get('a')).toBe('mockA: bValue'); + }); + }); + }); + + describe('in Components', () => { + it('should support useValue', () => { + @Component({ + template: '', + providers: [ + {provide: 'a', useValue: 'aValue'}, + ] + }) + class MComp { + } + + TestBed.overrideProvider('a', {useValue: 'mockValue'}); + const ctx = + TestBed.configureTestingModule({declarations: [MComp]}).createComponent(MComp); + + expect(ctx.debugElement.injector.get('a')).toBe('mockValue'); + }); + + it('should support useFactory', () => { + @Component({ + template: '', + providers: [ + {provide: 'dep', useValue: 'depValue'}, + {provide: 'a', useValue: 'aValue'}, + ] + }) + class MyComp { + } + + TestBed.overrideProvider( + 'a', {useFactory: (dep: any) => `mockA: ${dep}`, deps: ['dep']}); + const ctx = + TestBed.configureTestingModule({declarations: [MyComp]}).createComponent(MyComp); + + expect(ctx.debugElement.injector.get('a')).toBe('mockA: depValue'); + }); + + it('should support @Optional without matches', () => { + @Component({ + template: '', + providers: [ + {provide: 'a', useValue: 'aValue'}, + ] + }) + class MyComp { + } + + TestBed.overrideProvider( + 'a', {useFactory: (dep: any) => `mockA: ${dep}`, deps: [[new Optional(), 'dep']]}); + const ctx = + TestBed.configureTestingModule({declarations: [MyComp]}).createComponent(MyComp); + + expect(ctx.debugElement.injector.get('a')).toBe('mockA: null'); + }); + + it('should support Optional with matches', () => { + @Component({ + template: '', + providers: [ + {provide: 'dep', useValue: 'depValue'}, + {provide: 'a', useValue: 'aValue'}, + ] + }) + class MyComp { + } + + TestBed.overrideProvider( + 'a', {useFactory: (dep: any) => `mockA: ${dep}`, deps: [[new Optional(), 'dep']]}); + const ctx = + TestBed.configureTestingModule({declarations: [MyComp]}).createComponent(MyComp); + + expect(ctx.debugElement.injector.get('a')).toBe('mockA: depValue'); + }); + + it('should support SkipSelf', () => { + @Directive({ + selector: '[myDir]', + providers: [ + {provide: 'a', useValue: 'aValue'}, + {provide: 'dep', useValue: 'depValue'}, + ] + }) + class MyDir { + } + + @Component({ + template: '
', + providers: [ + {provide: 'dep', useValue: 'parentDepValue'}, + ] + }) + class MyComp { + } + + TestBed.overrideProvider( + 'a', {useFactory: (dep: any) => `mockA: ${dep}`, deps: [[new SkipSelf(), 'dep']]}); + const ctx = TestBed.configureTestingModule({declarations: [MyComp, MyDir]}) + .createComponent(MyComp); + expect(ctx.debugElement.children[0].injector.get('a')).toBe('mockA: parentDepValue'); + }); + + it('should support multiple providers in a template', () => { + @Directive({ + selector: '[myDir1]', + providers: [ + {provide: 'a', useValue: 'aValue1'}, + ] + }) + class MyDir1 { + } + + @Directive({ + selector: '[myDir2]', + providers: [ + {provide: 'a', useValue: 'aValue2'}, + ] + }) + class MyDir2 { + } + + @Component({ + template: '
', + }) + class MyComp { + } + + TestBed.overrideProvider('a', {useValue: 'mockA'}); + const ctx = TestBed.configureTestingModule({declarations: [MyComp, MyDir1, MyDir2]}) + .createComponent(MyComp); + expect(ctx.debugElement.children[0].injector.get('a')).toBe('mockA'); + expect(ctx.debugElement.children[1].injector.get('a')).toBe('mockA'); + }); + + describe('injecting eager providers into an eager overwritten provider', () => { + @Component({ + template: '', + providers: [ + {provide: 'a', useFactory: () => 'aValue'}, + {provide: 'b', useFactory: () => 'bValue'}, + ] + }) + class MyComp { + // Component is eager, which makes all of its deps eager + constructor(@Inject('a') a: any, @Inject('b') b: any) {} + } + + it('should inject providers that were declared before it', () => { + TestBed.overrideProvider( + 'b', {useFactory: (a: string) => `mockB: ${a}`, deps: ['a']}); + const ctx = + TestBed.configureTestingModule({declarations: [MyComp]}).createComponent(MyComp); + + expect(ctx.debugElement.injector.get('b')).toBe('mockB: aValue'); + }); + + it('should inject providers that were declared after it', () => { + TestBed.overrideProvider( + 'a', {useFactory: (b: string) => `mockA: ${b}`, deps: ['b']}); + const ctx = + TestBed.configureTestingModule({declarations: [MyComp]}).createComponent(MyComp); + + expect(ctx.debugElement.injector.get('a')).toBe('mockA: bValue'); + }); + }); + }); + + it('should reset overrides when the testing modules is resetted', () => { + TestBed.overrideProvider('a', {useValue: 'mockValue'}); + TestBed.resetTestingModule(); + TestBed.configureTestingModule({providers: [{provide: 'a', useValue: 'aValue'}]}); + expect(TestBed.get('a')).toBe('aValue'); + }); + }); + describe('setting up the compiler', () => { describe('providers', () => { diff --git a/tools/public_api_guard/core/testing.d.ts b/tools/public_api_guard/core/testing.d.ts index b5768129db..39044a1646 100644 --- a/tools/public_api_guard/core/testing.d.ts +++ b/tools/public_api_guard/core/testing.d.ts @@ -75,6 +75,13 @@ export declare class TestBed implements Injector { overrideDirective(directive: Type, override: MetadataOverride): void; overrideModule(ngModule: Type, override: MetadataOverride): void; overridePipe(pipe: Type, override: MetadataOverride): void; + overrideProvider(token: any, provider: { + useFactory: Function; + deps: any[]; + }): void; + overrideProvider(token: any, provider: { + useValue: any; + }): void; /** @experimental */ resetTestEnvironment(): void; resetTestingModule(): void; static compileComponents(): Promise; @@ -90,6 +97,13 @@ export declare class TestBed implements Injector { static overrideDirective(directive: Type, override: MetadataOverride): typeof TestBed; static overrideModule(ngModule: Type, override: MetadataOverride): typeof TestBed; static overridePipe(pipe: Type, override: MetadataOverride): typeof TestBed; + static overrideProvider(token: any, provider: { + useFactory: Function; + deps: any[]; + }): void; + static overrideProvider(token: any, provider: { + useValue: any; + }): void; static overrideTemplate(component: Type, template: string): typeof TestBed; /** @experimental */ static resetTestEnvironment(): void; static resetTestingModule(): typeof TestBed;