From f867314f73fb6a85166beb08ad7c065b404b0ece Mon Sep 17 00:00:00 2001 From: Tobias Bosch Date: Wed, 6 Jan 2016 08:55:19 -0800 Subject: [PATCH] feat(test_component_builder): allow to override components Note: This also works with precompiled templates, i.e. with tests that use transformers. Closes #6276 --- .../platform/testing/browser_static.ts | 4 +- modules/angular2/platform/testing/server.ts | 4 +- .../src/compiler/proto_view_compiler.ts | 6 +- .../src/compiler/template_compiler.ts | 16 ++-- .../angular2/src/compiler/view_compiler.ts | 19 ++++- .../src/core/application_common_providers.ts | 3 +- modules/angular2/src/core/linker/view.ts | 3 +- .../angular2/src/core/linker/view_listener.ts | 13 ++++ .../angular2/src/core/linker/view_manager.ts | 15 +++- .../src/testing/test_component_builder.ts | 77 ++++++++++++++++--- .../test/compiler/runtime_compiler_spec.ts | 2 +- .../test/compiler/template_compiler_spec.ts | 1 + .../test/core/linker/compiler_spec.ts | 2 +- .../test/core/linker/integration_spec.ts | 9 +-- .../testing/test_component_builder_spec.ts | 39 +++++++++- 15 files changed, 171 insertions(+), 42 deletions(-) diff --git a/modules/angular2/platform/testing/browser_static.ts b/modules/angular2/platform/testing/browser_static.ts index 1f4192134e0e..1c026ad4c5a3 100644 --- a/modules/angular2/platform/testing/browser_static.ts +++ b/modules/angular2/platform/testing/browser_static.ts @@ -21,7 +21,7 @@ import {MockNgZone} from 'angular2/src/mock/ng_zone_mock'; import {XHRImpl} from "angular2/src/platform/browser/xhr_impl"; import {XHR} from 'angular2/compiler'; -import {TestComponentBuilder} from 'angular2/src/testing/test_component_builder'; +import {TEST_COMPONENT_BUILDER_PROVIDERS} from 'angular2/src/testing/test_component_builder'; import {BrowserDetection} from 'angular2/src/testing/utils'; @@ -52,7 +52,7 @@ export const ADDITIONAL_TEST_BROWSER_PROVIDERS: Array typeRef(directiveType.type)); + var expressions = directives.map(directiveType => codeGenType(directiveType.type)); return `[${expressions.join(',')}]`; } function codeGenTypesArray(types: CompileTypeMetadata[]): string { - var expressions = types.map(typeRef); + var expressions = types.map(codeGenType); return `[${expressions.join(',')}]`; } @@ -382,7 +382,7 @@ function codeGenViewType(value: ViewType): string { } } -function typeRef(type: CompileTypeMetadata): string { +export function codeGenType(type: CompileTypeMetadata): string { return `${moduleRef(type.moduleUrl)}${type.name}`; } diff --git a/modules/angular2/src/compiler/template_compiler.ts b/modules/angular2/src/compiler/template_compiler.ts index 04a33eadbd3b..3880b41127d3 100644 --- a/modules/angular2/src/compiler/template_compiler.ts +++ b/modules/angular2/src/compiler/template_compiler.ts @@ -127,8 +127,12 @@ export class TemplateCompiler { this._compileComponentRuntime(hostCacheKey, hostMeta, [compMeta], [], []); } return this._compiledTemplateDone.get(hostCacheKey) - .then((compiledTemplate: CompiledTemplate) => - new HostViewFactory(compMeta.selector, compiledTemplate.viewFactory)); + .then((hostCompiledTemplate) => { + return this._compiledTemplateDone.get(type).then((componentCompiledTemplate) => { + return new HostViewFactory(compMeta.selector, hostCompiledTemplate.viewFactory, + componentCompiledTemplate.viewFactory); + }); + }); } clearCache() { @@ -146,15 +150,15 @@ export class TemplateCompiler { components.forEach(componentWithDirs => { var compMeta = componentWithDirs.component; assertComponent(compMeta); - this._compileComponentCodeGen(compMeta, componentWithDirs.directives, componentWithDirs.pipes, - declarations); + var componentViewFactoryExpression = this._compileComponentCodeGen( + compMeta, componentWithDirs.directives, componentWithDirs.pipes, declarations); if (compMeta.dynamicLoadable) { var hostMeta = createHostComponentMeta(compMeta.type, compMeta.selector); - var viewFactoryExpression = + var hostViewFactoryExpression = this._compileComponentCodeGen(hostMeta, [compMeta], [], declarations); var constructionKeyword = IS_DART ? 'const' : 'new'; var compiledTemplateExpr = - `${constructionKeyword} ${APP_VIEW_MODULE_REF}HostViewFactory('${compMeta.selector}',${viewFactoryExpression})`; + `${constructionKeyword} ${APP_VIEW_MODULE_REF}HostViewFactory('${compMeta.selector}',${hostViewFactoryExpression},${componentViewFactoryExpression})`; var varName = codeGenHostViewFactoryName(compMeta.type); declarations.push(`${codeGenExportVariable(varName)}${compiledTemplateExpr};`); } diff --git a/modules/angular2/src/compiler/view_compiler.ts b/modules/angular2/src/compiler/view_compiler.ts index 2f6ec19697e0..3f6c71393660 100644 --- a/modules/angular2/src/compiler/view_compiler.ts +++ b/modules/angular2/src/compiler/view_compiler.ts @@ -5,7 +5,8 @@ import { isString, StringWrapper, IS_DART, - CONST_EXPR + CONST_EXPR, + assertionsEnabled } from 'angular2/src/facade/lang'; import {SetWrapper, StringMapWrapper, ListWrapper} from 'angular2/src/facade/collection'; import { @@ -58,7 +59,8 @@ import { APP_EL_MODULE_REF, METADATA_MODULE_REF, CompileProtoView, - CompileProtoElement + CompileProtoElement, + codeGenType } from './proto_view_compiler'; export const VIEW_JIT_IMPORTS = CONST_EXPR({ @@ -227,15 +229,20 @@ class CodeGenViewFactory implements ViewFactory { appEl: Expression, component: CompileDirectiveMetadata, contentNodesByNgContentIndex: Expression[][], targetStatements: Statement[]) { + var viewFactoryExpr = this.componentViewFactory(component); var codeGenContentNodes; if (this.component.type.isHost) { codeGenContentNodes = `${view.expression}.projectableNodes`; } else { codeGenContentNodes = `[${contentNodesByNgContentIndex.map( nodes => codeGenFlatArray(nodes) ).join(',')}]`; + if (assertionsEnabled()) { + viewFactoryExpr = + `viewManager.getComponentViewFactory(${codeGenType(component.type)}, ${viewFactoryExpr})`; + } } targetStatements.push(new Statement( - `${this.componentViewFactory(component)}(${renderer.expression}, ${viewManager.expression}, ${appEl.expression}, ${codeGenContentNodes}, null, null, null);`)); + `${viewFactoryExpr}(${renderer.expression}, ${viewManager.expression}, ${appEl.expression}, ${codeGenContentNodes}, null, null, null);`)); } getProjectedNodes(projectableNodes: Expression, ngContentIndex: number): Expression { @@ -366,6 +373,7 @@ class RuntimeViewFactory implements ViewFactory { contentNodesByNgContentIndex: Array>, targetStatements: any[]) { var flattenedContentNodes; + var viewFactory = this.componentViewFactory(component); if (this.component.type.isHost) { flattenedContentNodes = appView.projectableNodes; } else { @@ -373,8 +381,11 @@ class RuntimeViewFactory implements ViewFactory { for (var i = 0; i < contentNodesByNgContentIndex.length; i++) { flattenedContentNodes[i] = flattenArray(contentNodesByNgContentIndex[i], []); } + if (assertionsEnabled()) { + viewFactory = viewManager.getComponentViewFactory(component.type.runtime, viewFactory); + } } - this.componentViewFactory(component)(renderer, viewManager, appEl, flattenedContentNodes); + viewFactory(renderer, viewManager, appEl, flattenedContentNodes); } getProjectedNodes(projectableNodes: any[][], ngContentIndex: number): any[] { diff --git a/modules/angular2/src/core/application_common_providers.ts b/modules/angular2/src/core/application_common_providers.ts index bb75bf517e5e..717e312a80a2 100644 --- a/modules/angular2/src/core/application_common_providers.ts +++ b/modules/angular2/src/core/application_common_providers.ts @@ -15,7 +15,7 @@ import {ResolvedMetadataCache} from 'angular2/src/core/linker/resolved_metadata_ import {AppViewManager} from './linker/view_manager'; import {AppViewManager_} from "./linker/view_manager"; import {ViewResolver} from './linker/view_resolver'; -import {AppViewListener} from './linker/view_listener'; +import {AppViewListener, ViewFactoryProxy} from './linker/view_listener'; import {DirectiveResolver} from './linker/directive_resolver'; import {PipeResolver} from './linker/pipe_resolver'; import {Compiler} from './linker/compiler'; @@ -33,6 +33,7 @@ export const APPLICATION_COMMON_PROVIDERS: Array = CONS ResolvedMetadataCache, new Provider(AppViewManager, {useClass: AppViewManager_}), AppViewListener, + ViewFactoryProxy, ViewResolver, new Provider(IterableDiffers, {useValue: defaultIterableDiffers}), new Provider(KeyValueDiffers, {useValue: defaultKeyValueDiffers}), diff --git a/modules/angular2/src/core/linker/view.ts b/modules/angular2/src/core/linker/view.ts index 6c1503261390..b2e9692f83fc 100644 --- a/modules/angular2/src/core/linker/view.ts +++ b/modules/angular2/src/core/linker/view.ts @@ -304,7 +304,8 @@ export class AppProtoView { @CONST() export class HostViewFactory { - constructor(public selector: string, public viewFactory: Function) {} + constructor(public selector: string, public viewFactory: Function, + public componentViewFactory: Function) {} } export function flattenNestedViewRenderNodes(nodes: any[]): any[] { diff --git a/modules/angular2/src/core/linker/view_listener.ts b/modules/angular2/src/core/linker/view_listener.ts index 7bc0f9988ba5..faa2ade91b26 100644 --- a/modules/angular2/src/core/linker/view_listener.ts +++ b/modules/angular2/src/core/linker/view_listener.ts @@ -1,4 +1,5 @@ import {Injectable} from 'angular2/src/core/di'; +import {Type} from 'angular2/src/facade/lang'; import * as viewModule from './view'; /** @@ -9,3 +10,15 @@ export class AppViewListener { onViewCreated(view: viewModule.AppView) {} onViewDestroyed(view: viewModule.AppView) {} } + +/** + * Proxy that allows to intercept component view factories. + * This also works for precompiled templates, if they were + * generated in development mode. + */ +@Injectable() +export class ViewFactoryProxy { + getComponentViewFactory(component: Type, originalViewFactory: Function): Function { + return originalViewFactory; + } +} diff --git a/modules/angular2/src/core/linker/view_manager.ts b/modules/angular2/src/core/linker/view_manager.ts index d2efabaeab3c..6ea02c7f3d9a 100644 --- a/modules/angular2/src/core/linker/view_manager.ts +++ b/modules/angular2/src/core/linker/view_manager.ts @@ -4,9 +4,11 @@ import { Provider, Injectable, ResolvedProvider, - forwardRef + forwardRef, + OpaqueToken, + Optional } from 'angular2/src/core/di'; -import {isPresent, isBlank, isArray} from 'angular2/src/facade/lang'; +import {isPresent, isBlank, isArray, Type, CONST_EXPR} from 'angular2/src/facade/lang'; import {ListWrapper, StringMapWrapper} from 'angular2/src/facade/collection'; import {BaseException} from 'angular2/src/facade/exceptions'; import {AppView, HostViewFactory, flattenNestedViewRenderNodes} from './view'; @@ -22,7 +24,7 @@ import { } from './view_ref'; import {ViewContainerRef} from './view_container_ref'; import {TemplateRef, TemplateRef_} from './template_ref'; -import {AppViewListener} from './view_listener'; +import {AppViewListener, ViewFactoryProxy} from './view_listener'; import {RootRenderer, RenderComponentType} from 'angular2/src/core/render/api'; import {wtfCreateScope, wtfLeave, WtfScopeFn} from '../profile/profile'; import {APP_ID} from 'angular2/src/core/application_tokens'; @@ -186,7 +188,7 @@ export class AppViewManager_ extends AppViewManager { private _nextCompTypeId: number = 0; constructor(private _renderer: RootRenderer, private _viewListener: AppViewListener, - @Inject(APP_ID) private _appId: string) { + private _viewFactoryProxy: ViewFactoryProxy, @Inject(APP_ID) private _appId: string) { super(); } @@ -328,6 +330,11 @@ export class AppViewManager_ extends AppViewManager { styles); } + /** @internal */ + getComponentViewFactory(component: Type, originalViewFactory: Function): Function { + return this._viewFactoryProxy.getComponentViewFactory(component, originalViewFactory); + } + private _attachViewToContainer(view: AppView, vcAppElement: AppElement, viewIndex: number) { if (view.proto.type === ViewType.COMPONENT) { throw new BaseException(`Component views can't be moved!`); diff --git a/modules/angular2/src/testing/test_component_builder.ts b/modules/angular2/src/testing/test_component_builder.ts index a09645cbdca3..f4efeb60ac67 100644 --- a/modules/angular2/src/testing/test_component_builder.ts +++ b/modules/angular2/src/testing/test_component_builder.ts @@ -8,14 +8,17 @@ import { ViewMetadata, EmbeddedViewRef, ViewResolver, - provide + provide, + Provider } from 'angular2/core'; -import {Type, isPresent, isBlank} from 'angular2/src/facade/lang'; -import {Promise} from 'angular2/src/facade/async'; +import {Type, isPresent, isBlank, CONST_EXPR} from 'angular2/src/facade/lang'; +import {Promise, PromiseWrapper} from 'angular2/src/facade/async'; import {ListWrapper, MapWrapper} from 'angular2/src/facade/collection'; +import {Compiler, Compiler_} from 'angular2/src/core/linker/compiler'; +import {ViewFactoryProxy} from 'angular2/src/core/linker/view_listener'; -import {ViewRef_} from 'angular2/src/core/linker/view_ref'; +import {ViewRef_, HostViewFactoryRef_} from 'angular2/src/core/linker/view_ref'; import {AppView} from 'angular2/src/core/linker/view'; import {el} from './utils'; @@ -25,7 +28,6 @@ import {DOM} from 'angular2/src/platform/dom/dom_adapter'; import {DebugElement_} from 'angular2/src/core/debug/debug_element'; - /** * Fixture for debugging and testing a component. */ @@ -82,6 +84,20 @@ export class ComponentFixture_ extends ComponentFixture { var _nextRootElementId = 0; +@Injectable() +export class TestViewFactoryProxy implements ViewFactoryProxy { + private _componentFactoryOverrides: Map = new Map(); + + getComponentViewFactory(component: Type, originalViewFactory: Function): Function { + var override = this._componentFactoryOverrides.get(component); + return isPresent(override) ? override : originalViewFactory; + } + + setComponentViewFactory(component: Type, viewFactory: Function) { + this._componentFactoryOverrides.set(component, viewFactory); + } +} + /** * Builds a ComponentFixture for use in component level tests. */ @@ -97,7 +113,8 @@ export class TestComponentBuilder { _viewBindingsOverrides = new Map(); /** @internal */ _viewOverrides = new Map(); - + /** @internal */ + _componentOverrides = new Map(); constructor(private _injector: Injector) {} @@ -107,6 +124,23 @@ export class TestComponentBuilder { clone._viewOverrides = MapWrapper.clone(this._viewOverrides); clone._directiveOverrides = MapWrapper.clone(this._directiveOverrides); clone._templateOverrides = MapWrapper.clone(this._templateOverrides); + clone._componentOverrides = MapWrapper.clone(this._componentOverrides); + return clone; + } + + /** + * Overrides a component with another component. + * This also works with precompiled templates if they were generated + * in development mode. + * + * @param {Type} original component + * @param {Type} mock component + * + * @return {TestComponentBuilder} + */ + overrideComponent(componentType: Type, mockType: Type): TestComponentBuilder { + var clone = this._clone(); + clone._componentOverrides.set(componentType, mockType); return clone; } @@ -246,10 +280,31 @@ export class TestComponentBuilder { DOM.remove(oldRoots[i]); } DOM.appendChild(doc.body, rootEl); - - - return this._injector.get(DynamicComponentLoader) - .loadAsRoot(rootComponentType, `#${rootElId}`, this._injector) - .then((componentRef) => { return new ComponentFixture_(componentRef); }); + var originalCompTypes = []; + var mockHostViewFactoryPromises = []; + var compiler: Compiler_ = this._injector.get(Compiler); + var viewFactoryProxy: TestViewFactoryProxy = this._injector.get(TestViewFactoryProxy); + this._componentOverrides.forEach((mockCompType, originalCompType) => { + originalCompTypes.push(originalCompType); + mockHostViewFactoryPromises.push(compiler.compileInHost(mockCompType)); + }); + return PromiseWrapper.all(mockHostViewFactoryPromises) + .then((mockHostViewFactories: HostViewFactoryRef_[]) => { + for (var i = 0; i < mockHostViewFactories.length; i++) { + var originalCompType = originalCompTypes[i]; + viewFactoryProxy.setComponentViewFactory( + originalCompType, + mockHostViewFactories[i].internalHostViewFactory.componentViewFactory); + } + return this._injector.get(DynamicComponentLoader) + .loadAsRoot(rootComponentType, `#${rootElId}`, this._injector) + .then((componentRef) => { return new ComponentFixture_(componentRef); }); + }); } } + +export const TEST_COMPONENT_BUILDER_PROVIDERS = CONST_EXPR([ + TestViewFactoryProxy, + CONST_EXPR(new Provider(ViewFactoryProxy, {useExisting: TestViewFactoryProxy})), + TestComponentBuilder +]); diff --git a/modules/angular2/test/compiler/runtime_compiler_spec.ts b/modules/angular2/test/compiler/runtime_compiler_spec.ts index 5d1496cafd34..33aced7c2c1e 100644 --- a/modules/angular2/test/compiler/runtime_compiler_spec.ts +++ b/modules/angular2/test/compiler/runtime_compiler_spec.ts @@ -28,7 +28,7 @@ export function main() { beforeEachProviders(() => { templateCompilerSpy = new SpyTemplateCompiler(); - someHostViewFactory = new HostViewFactory(null, null); + someHostViewFactory = new HostViewFactory(null, null, null); templateCompilerSpy.spy('compileHostComponentRuntime') .andReturn(PromiseWrapper.resolve(someHostViewFactory)); return [provide(TemplateCompiler, {useValue: templateCompilerSpy})]; diff --git a/modules/angular2/test/compiler/template_compiler_spec.ts b/modules/angular2/test/compiler/template_compiler_spec.ts index fce620019e40..3dff2c3d3e97 100644 --- a/modules/angular2/test/compiler/template_compiler_spec.ts +++ b/modules/angular2/test/compiler/template_compiler_spec.ts @@ -473,6 +473,7 @@ export function humanizeViewFactory( cachedResults = new Map(); } var viewManager = new SpyAppViewManager(); + viewManager.spy('getComponentViewFactory').andCallFake((component, viewFactory) => viewFactory); viewManager.spy('createRenderComponentType') .andCallFake((encapsulation: ViewEncapsulation, styles: Array) => { return new RenderComponentType('someId', encapsulation, styles); diff --git a/modules/angular2/test/core/linker/compiler_spec.ts b/modules/angular2/test/core/linker/compiler_spec.ts index 480c0bd3493b..9ae97793480b 100644 --- a/modules/angular2/test/core/linker/compiler_spec.ts +++ b/modules/angular2/test/core/linker/compiler_spec.ts @@ -26,7 +26,7 @@ export function main() { beforeEachProviders(() => [provide(Compiler, {useClass: Compiler_})]); beforeEach(inject([Compiler], (_compiler) => { - someHostViewFactory = new HostViewFactory(null, null); + someHostViewFactory = new HostViewFactory(null, null, null); reflector.registerType(SomeComponent, new ReflectionInfo([someHostViewFactory])); })); diff --git a/modules/angular2/test/core/linker/integration_spec.ts b/modules/angular2/test/core/linker/integration_spec.ts index 353fb068da0f..95fd59e27317 100644 --- a/modules/angular2/test/core/linker/integration_spec.ts +++ b/modules/angular2/test/core/linker/integration_spec.ts @@ -1322,14 +1322,13 @@ function declareTests() { })); it('should report a meaningful error when a component is missing view annotation', - inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { - try { - tcb.createAsync(ComponentWithoutView); - } catch (e) { + inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => { + PromiseWrapper.catchError(tcb.createAsync(ComponentWithoutView), (e) => { expect(e.message) .toContain(`must have either 'template', 'templateUrl', or '@View' set.`); + async.done(); return null; - } + }); })); it('should report a meaningful error when a directive is null', diff --git a/modules/angular2/test/testing/test_component_builder_spec.ts b/modules/angular2/test/testing/test_component_builder_spec.ts index 333d6ca46b3f..ad77cd9cdc5c 100644 --- a/modules/angular2/test/testing/test_component_builder_spec.ts +++ b/modules/angular2/test/testing/test_component_builder_spec.ts @@ -17,6 +17,9 @@ import { import {Injectable, provide} from 'angular2/core'; import {NgIf} from 'angular2/common'; import {Directive, Component, View, ViewMetadata} from 'angular2/src/core/metadata'; +import {IS_DART} from 'angular2/src/facade/lang'; + +import {ChangeDetectorGenConfig} from 'angular2/src/core/change_detection/change_detection'; @Component({selector: 'child-comp'}) @View({template: `Original {{childBinding}}`, directives: []}) @@ -90,8 +93,29 @@ class TestViewBindingsComp { constructor(private fancyService: FancyService) {} } - export function main() { + if (IS_DART) { + declareTests(); + } else { + describe('no jit compiler', () => { + beforeEachProviders(() => [ + provide(ChangeDetectorGenConfig, + {useValue: new ChangeDetectorGenConfig(true, false, false)}) + ]); + declareTests(); + }); + + describe('jit compiler', () => { + beforeEachProviders(() => [ + provide(ChangeDetectorGenConfig, + {useValue: new ChangeDetectorGenConfig(true, false, true)}) + ]); + declareTests(); + }); + } +} + +export function declareTests() { describe('test component builder', function() { it('should instantiate a component with valid DOM', inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => { @@ -119,6 +143,19 @@ export function main() { }); })); + it('should override a component', + inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => { + + tcb.overrideComponent(ChildComp, MockChildComp) + .createAsync(ParentComp) + .then((componentFixture) => { + componentFixture.detectChanges(); + expect(componentFixture.nativeElement).toHaveText('Parent(Mock)'); + + async.done(); + }); + })); + it('should override a template', inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {