From 438c2b31e44bd3ad58e48f35229e48941aadc618 Mon Sep 17 00:00:00 2001 From: Victor Berchet Date: Mon, 30 Mar 2015 16:37:33 +0200 Subject: [PATCH] test(TestBed): initial implementation --- modules/angular2/core.js | 1 + modules/angular2/src/core/application.js | 13 +- .../angular2/src/core/application_tokens.js | 7 + .../angular2/src/mock/vm_turn_zone_mock.js | 15 ++ modules/angular2/src/test_lib/lang_utils.dart | 8 ++ modules/angular2/src/test_lib/lang_utils.es6 | 9 ++ modules/angular2/src/test_lib/test_bed.js | 131 ++++++++++++++++++ .../angular2/src/test_lib/test_injector.js | 43 +++++- .../angular2/test/core/application_spec.js | 4 +- 9 files changed, 218 insertions(+), 13 deletions(-) create mode 100644 modules/angular2/src/core/application_tokens.js create mode 100644 modules/angular2/src/mock/vm_turn_zone_mock.js create mode 100644 modules/angular2/src/test_lib/lang_utils.dart create mode 100644 modules/angular2/src/test_lib/lang_utils.es6 create mode 100644 modules/angular2/src/test_lib/test_bed.js diff --git a/modules/angular2/core.js b/modules/angular2/core.js index 87419c5b70..cad9b91e40 100644 --- a/modules/angular2/core.js +++ b/modules/angular2/core.js @@ -2,6 +2,7 @@ export * from './src/core/annotations/visibility'; export * from './src/core/compiler/interfaces'; export * from './src/core/annotations/template'; export * from './src/core/application'; +export * from './src/core/application_tokens'; export * from './src/core/annotations/di'; export * from './src/core/compiler/compiler'; diff --git a/modules/angular2/src/core/application.js b/modules/angular2/src/core/application.js index fac037c01a..fd668ced1e 100644 --- a/modules/angular2/src/core/application.js +++ b/modules/angular2/src/core/application.js @@ -28,6 +28,13 @@ import {StyleInliner} from 'angular2/src/render/dom/shadow_dom/style_inliner'; import {Component} from 'angular2/src/core/annotations/annotations'; import {PrivateComponentLoader} from 'angular2/src/core/compiler/private_component_loader'; import {TestabilityRegistry, Testability} from 'angular2/src/core/testability/testability'; +import { + appViewToken, + appChangeDetectorToken, + appElementToken, + appComponentAnnotatedTypeToken, + appDocumentToken, +} from './application_tokens'; var _rootInjector: Injector; @@ -37,12 +44,6 @@ var _rootBindings = [ TestabilityRegistry ]; -export var appViewToken = new OpaqueToken('AppView'); -export var appChangeDetectorToken = new OpaqueToken('AppChangeDetector'); -export var appElementToken = new OpaqueToken('AppElement'); -export var appComponentAnnotatedTypeToken = new OpaqueToken('AppComponentAnnotatedType'); -export var appDocumentToken = new OpaqueToken('AppDocument'); - function _injectorBindings(appComponentType): List { return [ bind(appDocumentToken).toValue(DOM.defaultDoc()), diff --git a/modules/angular2/src/core/application_tokens.js b/modules/angular2/src/core/application_tokens.js new file mode 100644 index 0000000000..9f0004b1ef --- /dev/null +++ b/modules/angular2/src/core/application_tokens.js @@ -0,0 +1,7 @@ +import {OpaqueToken} from 'angular2/di'; + +export var appViewToken = new OpaqueToken('AppView'); +export var appChangeDetectorToken = new OpaqueToken('AppChangeDetector'); +export var appElementToken = new OpaqueToken('AppElement'); +export var appComponentAnnotatedTypeToken = new OpaqueToken('AppComponentAnnotatedType'); +export var appDocumentToken = new OpaqueToken('AppDocument'); diff --git a/modules/angular2/src/mock/vm_turn_zone_mock.js b/modules/angular2/src/mock/vm_turn_zone_mock.js new file mode 100644 index 0000000000..0dcd479caa --- /dev/null +++ b/modules/angular2/src/mock/vm_turn_zone_mock.js @@ -0,0 +1,15 @@ +import {VmTurnZone} from 'angular2/src/core/zone/vm_turn_zone'; + +export class MockVmTurnZone extends VmTurnZone { + constructor() { + super({enableLongStackTrace: false}); + } + + run(fn) { + fn(); + } + + runOutsideAngular(fn) { + fn(); + } +} diff --git a/modules/angular2/src/test_lib/lang_utils.dart b/modules/angular2/src/test_lib/lang_utils.dart new file mode 100644 index 0000000000..9717dc3e62 --- /dev/null +++ b/modules/angular2/src/test_lib/lang_utils.dart @@ -0,0 +1,8 @@ +import 'dart:mirrors'; + +Type getTypeOf(instance) => instance.runtimeType; + +dynamic instantiateType(Type type, [List params = const []]) { + var cm = reflectClass(type); + return cm.newInstance(new Symbol(''), params).reflectee; +} diff --git a/modules/angular2/src/test_lib/lang_utils.es6 b/modules/angular2/src/test_lib/lang_utils.es6 new file mode 100644 index 0000000000..b3c42ff193 --- /dev/null +++ b/modules/angular2/src/test_lib/lang_utils.es6 @@ -0,0 +1,9 @@ +export function getTypeOf(instance) { + return instance.constructor; +} + +export function instantiateType(type: Function, params: Array = []) { + var instance = Object.create(type.prototype); + instance.constructor.apply(instance, params); + return instance; +} diff --git a/modules/angular2/src/test_lib/test_bed.js b/modules/angular2/src/test_lib/test_bed.js new file mode 100644 index 0000000000..7de912ebcc --- /dev/null +++ b/modules/angular2/src/test_lib/test_bed.js @@ -0,0 +1,131 @@ +import {Injector} from 'angular2/di'; + +import {Type, isPresent, BaseException} from 'angular2/src/facade/lang'; +import {Promise} from 'angular2/src/facade/async'; +import {isBlank} from 'angular2/src/facade/lang'; +import {List} from 'angular2/src/facade/collection'; + +import {Template} from 'angular2/src/core/annotations/template'; + +import {TemplateResolver} from 'angular2/src/core/compiler/template_resolver'; +import {Compiler} from 'angular2/src/core/compiler/compiler'; +import {View} from 'angular2/src/core/compiler/view'; + +import {EventManager} from 'angular2/src/render/dom/events/event_manager'; + +import {queryView} from './utils'; +import {instantiateType, getTypeOf} from './lang_utils'; + +export class TestBed { + _injector: Injector; + + constructor(injector: Injector) { + this._injector = injector; + } + + /** + * Overrides the [Template] of a [Component]. + * + * @see setInlineTemplate() to only override the html + * + * @param {Type} component + * @param {Template} template + */ + overrideTemplate(component: Type, template: Template): void { + this._injector.get(TemplateResolver).setTemplate(component, template); + } + + /** + * Overrides only the html of a [Component]. + * All the other propoerties of the component's [Template] are preserved. + * + * @param {Type} component + * @param {string} html + */ + setInlineTemplate(component: Type, html: string): void { + this._injector.get(TemplateResolver).setInlineTemplate(component, html); + } + + /** + * Overrides the directives from the component [Template]. + * + * @param {Type} component + * @param {Type} from + * @param {Type} to + */ + overrideDirective(component: Type, from: Type, to: Type): void { + this._injector.get(TemplateResolver).overrideTemplateDirective(component, from, to); + } + + /** + * Creates a [View] for the given component. + * + * Only either a component or a context needs to be specified but both can be provided for + * advanced use cases (ie subclassing the context). + * + * @param {Type} component + * @param {*} context + * @param {string} html Use as the component template when specified (shortcut for setInlineTemplate) + * @return {Promise} + */ + createView(component: Type, + {context = null, html = null}: {context:any, html: string} = {}): Promise { + + if (isBlank(component) && isBlank(context)) { + throw new BaseException('You must specified at least a component or a context'); + } + + if (isBlank(component)) { + component = getTypeOf(context); + } else if (isBlank(context)) { + context = instantiateType(component); + } + + if (isPresent(html)) { + this.setInlineTemplate(component, html); + } + + return this._injector.get(Compiler).compile(component).then((pv) => { + var eventManager = this._injector.get(EventManager); + var view = pv.instantiate(null, eventManager); + view.hydrate(this._injector, null, null, context, null); + return new ViewProxy(view); + }); + } +} + +/** + * Proxy to [View] return by [TestBed.createView] which offers a high level API for tests. + */ +export class ViewProxy { + _view: View; + + constructor(view: View) { + this._view = view; + } + + get context(): any { + return this._view.context; + } + + get nodes(): List { + return this._view.nodes; + } + + detectChanges(): void { + this._view.changeDetector.detectChanges(); + } + + querySelector(selector) { + return queryView(this._view, selector); + } + + /** + * @returns {View} return the underlying [View]. + * + * Prefer using the other methods which hide implementation details. + */ + get rawView(): View { + return this._view; + } +} diff --git a/modules/angular2/src/test_lib/test_injector.js b/modules/angular2/src/test_lib/test_injector.js index f7bb1ce94b..7fe3f696b2 100644 --- a/modules/angular2/src/test_lib/test_injector.js +++ b/modules/angular2/src/test_lib/test_injector.js @@ -1,4 +1,5 @@ import {bind} from 'angular2/di'; + import {Compiler, CompilerCache} from 'angular2/src/core/compiler/compiler'; import {Reflector, reflector} from 'angular2/src/reflection/reflection'; import {Parser, Lexer, ChangeDetection, dynamicChangeDetection} from 'angular2/change_detection'; @@ -6,13 +7,25 @@ import {ExceptionHandler} from 'angular2/src/core/exception_handler'; import {TemplateLoader} from 'angular2/src/render/dom/compiler/template_loader'; import {TemplateResolver} from 'angular2/src/core/compiler/template_resolver'; import {DirectiveMetadataReader} from 'angular2/src/core/compiler/directive_metadata_reader'; -import {ShadowDomStrategy, NativeShadowDomStrategy} from 'angular2/src/core/compiler/shadow_dom_strategy'; +import {ShadowDomStrategy, EmulatedUnscopedShadowDomStrategy} from 'angular2/src/core/compiler/shadow_dom_strategy'; import {XHR} from 'angular2/src/services/xhr'; -import {XHRMock} from 'angular2/src/mock/xhr_mock'; import {ComponentUrlMapper} from 'angular2/src/core/compiler/component_url_mapper'; import {UrlResolver} from 'angular2/src/services/url_resolver'; import {StyleUrlResolver} from 'angular2/src/render/dom/shadow_dom/style_url_resolver'; import {StyleInliner} from 'angular2/src/render/dom/shadow_dom/style_inliner'; +import {VmTurnZone} from 'angular2/src/core/zone/vm_turn_zone'; + +import {DOM} from 'angular2/src/dom/dom_adapter'; + +import {appDocumentToken} from 'angular2/src/core/application_tokens'; + +import {EventManager, DomEventsPlugin} from 'angular2/src/render/dom/events/event_manager'; + +import {MockTemplateResolver} from 'angular2/src/mock/template_resolver_mock'; +import {XHRMock} from 'angular2/src/mock/xhr_mock'; +import {MockVmTurnZone} from 'angular2/src/mock/vm_turn_zone_mock'; + +import {TestBed} from './test_bed'; import {Injector} from 'angular2/di'; @@ -40,11 +53,23 @@ function _getRootBindings() { * @returns {*[]} */ function _getAppBindings() { + var appDoc; + + // The document is only available in browser environment + try { + appDoc = DOM.defaultDoc(); + } catch(e) { + appDoc = null; + } + return [ - bind(ShadowDomStrategy).toClass(NativeShadowDomStrategy), + bind(appDocumentToken).toValue(appDoc), + bind(ShadowDomStrategy).toFactory( + (styleUrlResolver, doc) => new EmulatedUnscopedShadowDomStrategy(styleUrlResolver, doc.head), + [StyleUrlResolver, appDocumentToken]), Compiler, CompilerCache, - TemplateResolver, + bind(TemplateResolver).toClass(MockTemplateResolver), bind(ChangeDetection).toValue(dynamicChangeDetection), TemplateLoader, DirectiveMetadataReader, @@ -55,7 +80,15 @@ function _getAppBindings() { ComponentUrlMapper, UrlResolver, StyleUrlResolver, - StyleInliner + StyleInliner, + TestBed, + bind(VmTurnZone).toClass(MockVmTurnZone), + bind(EventManager).toFactory((zone) => { + var plugins = [ + new DomEventsPlugin(), + ]; + return new EventManager(plugins, zone); + }, [VmTurnZone]), ]; } diff --git a/modules/angular2/test/core/application_spec.js b/modules/angular2/test/core/application_spec.js index 561eb38a00..9b52e04fa7 100644 --- a/modules/angular2/test/core/application_spec.js +++ b/modules/angular2/test/core/application_spec.js @@ -10,8 +10,8 @@ import { xdescribe, xit, } from 'angular2/test_lib'; -import {bootstrap, appDocumentToken, appElementToken} - from 'angular2/src/core/application'; +import {bootstrap} from 'angular2/src/core/application'; +import {appDocumentToken, appElementToken} from 'angular2/src/core/application_tokens'; import {Component, Decorator} from 'angular2/src/core/annotations/annotations'; import {DOM} from 'angular2/src/dom/dom_adapter'; import {ListWrapper} from 'angular2/src/facade/collection';