diff --git a/modules/angular2/debug.ts b/modules/angular2/debug.ts new file mode 100644 index 0000000000..5ad3f25f45 --- /dev/null +++ b/modules/angular2/debug.ts @@ -0,0 +1,2 @@ +export * from './src/debug/debug_element'; +export {inspectDomElement, ELEMENT_PROBE_CONFIG} from './src/debug/debug_element_view_listener'; diff --git a/modules/angular2/src/debug/debug_element.ts b/modules/angular2/src/debug/debug_element.ts new file mode 100644 index 0000000000..72f752ad34 --- /dev/null +++ b/modules/angular2/src/debug/debug_element.ts @@ -0,0 +1,203 @@ +import {Type, isPresent, BaseException, isBlank} from 'angular2/src/facade/lang'; +import {List, ListWrapper, MapWrapper} from 'angular2/src/facade/collection'; + +import {DOM} from 'angular2/src/dom/dom_adapter'; + +import {ElementInjector} from 'angular2/src/core/compiler/element_injector'; +import {AppView} from 'angular2/src/core/compiler/view'; +import {internalView} from 'angular2/src/core/compiler/view_ref'; +import {ElementRef} from 'angular2/src/core/compiler/element_ref'; + +import {resolveInternalDomView} from 'angular2/src/render/dom/view/view'; + +/** + * @exportedAs angular2/test + * + * An DebugElement contains information from the Angular compiler about an + * element and provides access to the corresponding ElementInjector and + * underlying dom Element, as well as a way to query for children. + */ +export class DebugElement { + _elementInjector: ElementInjector; + + constructor(private _parentView: AppView, private _boundElementIndex: number) { + this._elementInjector = this._parentView.elementInjectors[this._boundElementIndex]; + } + + static create(elementRef: ElementRef): DebugElement { + return new DebugElement(internalView(elementRef.parentView), elementRef.boundElementIndex); + } + + get componentInstance(): any { + if (!isPresent(this._elementInjector)) { + return null; + } + return this._elementInjector.getComponent(); + } + + get dynamicallyCreatedComponentInstance(): any { + if (!isPresent(this._elementInjector)) { + return null; + } + return this._elementInjector.getDynamicallyLoadedComponent(); + } + + get domElement(): any { + return resolveInternalDomView(this._parentView.render).boundElements[this._boundElementIndex]; + } + + getDirectiveInstance(directiveIndex: number): any { + return this._elementInjector.getDirectiveAtIndex(directiveIndex); + } + + /** + * Get child DebugElements from within the Light DOM. + * + * @return {List} + */ + get children(): List { + var thisElementBinder = this._parentView.proto.elementBinders[this._boundElementIndex]; + + return this._getChildElements(this._parentView, thisElementBinder.index); + } + + /** + * Get the root DebugElement children of a component. Returns an empty + * list if the current DebugElement is not a component root. + * + * @return {List} + */ + get componentViewChildren(): List { + var shadowView = this._parentView.componentChildViews[this._boundElementIndex]; + + if (!isPresent(shadowView)) { + // The current element is not a component. + return ListWrapper.create(); + } + + return this._getChildElements(shadowView, null); + } + + triggerEventHandler(eventName, eventObj): void { + this._parentView.triggerEventHandlers(eventName, eventObj, this._boundElementIndex); + } + + hasDirective(type: Type): boolean { + if (!isPresent(this._elementInjector)) { + return false; + } + return this._elementInjector.hasDirective(type); + } + + inject(type: Type): any { + if (!isPresent(this._elementInjector)) { + return null; + } + return this._elementInjector.get(type); + } + + /** + * Return the first descendant TestElememt matching the given predicate + * and scope. + * + * @param {Function: boolean} predicate + * @param {Scope} scope + * + * @return {DebugElement} + */ + query(predicate: Function, scope = Scope.all): DebugElement { + var results = this.queryAll(predicate, scope); + return results.length > 0 ? results[0] : null; + } + + /** + * Return descendant TestElememts matching the given predicate + * and scope. + * + * @param {Function: boolean} predicate + * @param {Scope} scope + * + * @return {List} + */ + queryAll(predicate: Function, scope = Scope.all): List { + var elementsInScope = scope(this); + + return ListWrapper.filter(elementsInScope, predicate); + } + + _getChildElements(view: AppView, parentBoundElementIndex: number): List { + var els = ListWrapper.create(); + var parentElementBinder = null; + if (isPresent(parentBoundElementIndex)) { + parentElementBinder = view.proto.elementBinders[parentBoundElementIndex]; + } + for (var i = 0; i < view.proto.elementBinders.length; ++i) { + var binder = view.proto.elementBinders[i]; + if (binder.parent == parentElementBinder) { + ListWrapper.push(els, new DebugElement(view, i)); + + var views = view.viewContainers[i]; + if (isPresent(views)) { + ListWrapper.forEach(views.views, (nextView) => { + els = ListWrapper.concat(els, this._getChildElements(nextView, null)); + }); + } + } + } + return els; + } +} + +export function inspectElement(elementRef: ElementRef): DebugElement { + return DebugElement.create(elementRef); +} + +/** + * @exportedAs angular2/test + */ +export class Scope { + static all(debugElement): List { + var scope = ListWrapper.create(); + ListWrapper.push(scope, debugElement); + + ListWrapper.forEach(debugElement.children, + (child) => { scope = ListWrapper.concat(scope, Scope.all(child)); }); + + ListWrapper.forEach(debugElement.componentViewChildren, + (child) => { scope = ListWrapper.concat(scope, Scope.all(child)); }); + + return scope; + } + static light(debugElement): List { + var scope = ListWrapper.create(); + ListWrapper.forEach(debugElement.children, (child) => { + ListWrapper.push(scope, child); + scope = ListWrapper.concat(scope, Scope.light(child)); + }); + return scope; + } + + static view(debugElement): List { + var scope = ListWrapper.create(); + + ListWrapper.forEach(debugElement.componentViewChildren, (child) => { + ListWrapper.push(scope, child); + scope = ListWrapper.concat(scope, Scope.light(child)); + }); + return scope; + } +} + +/** + * @exportedAs angular2/test + */ +export class By { + static all(): Function { return (debugElement) => true; } + + static css(selector: string): Function { + return (debugElement) => { return DOM.elementMatches(debugElement.domElement, selector); }; + } + static directive(type: Type): Function { + return (debugElement) => { return debugElement.hasDirective(type); }; + } +} diff --git a/modules/angular2/src/debug/debug_element_view_listener.ts b/modules/angular2/src/debug/debug_element_view_listener.ts new file mode 100644 index 0000000000..9b9c1bf19b --- /dev/null +++ b/modules/angular2/src/debug/debug_element_view_listener.ts @@ -0,0 +1,78 @@ +import { + CONST_EXPR, + isPresent, + NumberWrapper, + StringWrapper, + RegExpWrapper +} from 'angular2/src/facade/lang'; +import {MapWrapper, Map, ListWrapper, List} from 'angular2/src/facade/collection'; +import {Injectable, bind, Binding} from 'angular2/di'; +import {AppViewListener} from 'angular2/src/core/compiler/view_listener'; +import {AppView} from 'angular2/src/core/compiler/view'; +import {DOM} from 'angular2/src/dom/dom_adapter'; +import {resolveInternalDomView} from 'angular2/src/render/dom/view/view'; +import {DebugElement} from './debug_element'; + +const NG_ID_PROPERTY = 'ngid'; +const INSPECT_GLOBAL_NAME = 'ngProbe'; + +var NG_ID_SEPARATOR_RE = RegExpWrapper.create('#'); +var NG_ID_SEPARATOR = '#'; + +// Need to keep the views in a global Map so that multiple angular apps are supported +var _allIdsByView: Map = CONST_EXPR(MapWrapper.create()); +var _allViewsById: Map = CONST_EXPR(MapWrapper.create()); +var _nextId = 0; + +function _setElementId(element, indices: List) { + if (isPresent(element)) { + DOM.setData(element, NG_ID_PROPERTY, ListWrapper.join(indices, NG_ID_SEPARATOR)); + } +} + +function _getElementId(element): List { + var elId = DOM.getData(element, NG_ID_PROPERTY); + if (isPresent(elId)) { + return ListWrapper.map(StringWrapper.split(elId, NG_ID_SEPARATOR_RE), + (partStr) => NumberWrapper.parseInt(partStr, 10)); + } else { + return null; + } +} + +export function inspectDomElement(element): DebugElement { + var elId = _getElementId(element); + if (isPresent(elId)) { + var view = MapWrapper.get(_allViewsById, elId[0]); + if (isPresent(view)) { + return new DebugElement(view, elId[1]); + } + } + return null; +} + +@Injectable() +export class DebugElementViewListener implements AppViewListener { + constructor() { DOM.setGlobalVar(INSPECT_GLOBAL_NAME, inspectDomElement); } + + viewCreated(view: AppView) { + var viewId = _nextId++; + MapWrapper.set(_allViewsById, viewId, view); + MapWrapper.set(_allIdsByView, view, viewId); + var renderView = resolveInternalDomView(view.render); + for (var i = 0; i < renderView.boundElements.length; i++) { + _setElementId(renderView.boundElements[i], [viewId, i]); + } + } + + viewDestroyed(view: AppView) { + var viewId = MapWrapper.get(_allIdsByView, view); + MapWrapper.delete(_allIdsByView, view); + MapWrapper.delete(_allViewsById, viewId); + } +} + +export var ELEMENT_PROBE_CONFIG = [ + DebugElementViewListener, + bind(AppViewListener).toAlias(DebugElementViewListener), +]; diff --git a/modules/angular2/src/dom/browser_adapter.ts b/modules/angular2/src/dom/browser_adapter.ts index 87a5abc81e..99e79e4132 100644 --- a/modules/angular2/src/dom/browser_adapter.ts +++ b/modules/angular2/src/dom/browser_adapter.ts @@ -258,9 +258,7 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter { setData(element, name: string, value: string) { element.dataset[name] = value; } getData(element, name: string): string { return element.dataset[name]; } // TODO(tbosch): move this into a separate environment class once we have it - setGlobalVar(name: string, value: any) { - global[name] = value; - } + setGlobalVar(name: string, value: any) { global[name] = value; } } // based on urlUtils.js in AngularJS 1 diff --git a/modules/angular2/src/test_lib/test_component_builder.ts b/modules/angular2/src/test_lib/test_component_builder.ts index fe4e3c54b4..c6a2dc45f4 100644 --- a/modules/angular2/src/test_lib/test_component_builder.ts +++ b/modules/angular2/src/test_lib/test_component_builder.ts @@ -6,7 +6,6 @@ import {List, ListWrapper, MapWrapper} from 'angular2/src/facade/collection'; import {View} from 'angular2/src/core/annotations_impl/view'; -import {ElementInjector} from 'angular2/src/core/compiler/element_injector'; import {TemplateResolver} from 'angular2/src/core/compiler/template_resolver'; import {AppView} from 'angular2/src/core/compiler/view'; import {internalView} from 'angular2/src/core/compiler/view_ref'; @@ -14,161 +13,18 @@ import { DynamicComponentLoader, ComponentRef } from 'angular2/src/core/compiler/dynamic_component_loader'; -import {ElementRef} from 'angular2/src/core/compiler/element_ref'; import {el} from './utils'; import {DOCUMENT_TOKEN} from 'angular2/src/render/dom/dom_renderer'; import {DOM} from 'angular2/src/dom/dom_adapter'; -import {resolveInternalDomView} from 'angular2/src/render/dom/view/view'; - -/** - * @exportedAs angular2/test - * - * A TestElement contains information from the Angular compiler about an - * element and provides access to the corresponding ElementInjector and - * underlying dom Element, as well as a way to query for children. - */ -export class TestElement { - _elementInjector: ElementInjector; - - constructor(private _parentView: AppView, private _boundElementIndex: number) { - this._elementInjector = this._parentView.elementInjectors[this._boundElementIndex]; - } - - static create(elementRef: ElementRef): TestElement { - return new TestElement(internalView(elementRef.parentView), elementRef.boundElementIndex); - } - - get componentInstance(): any { - if (!isPresent(this._elementInjector)) { - return null; - } - return this._elementInjector.getComponent(); - } - - get dynamicallyCreatedComponentInstance(): any { - if (!isPresent(this._elementInjector)) { - return null; - } - return this._elementInjector.getDynamicallyLoadedComponent(); - } - - get domElement(): any { - return resolveInternalDomView(this._parentView.render).boundElements[this._boundElementIndex]; - } - - getDirectiveInstance(directiveIndex: number): any { - return this._elementInjector.getDirectiveAtIndex(directiveIndex); - } - - /** - * Get child TestElements from within the Light DOM. - * - * @return {List} - */ - get children(): List { - var thisElementBinder = this._parentView.proto.elementBinders[this._boundElementIndex]; - - return this._getChildElements(this._parentView, thisElementBinder.index); - } - - /** - * Get the root TestElement children of a component. Returns an empty - * list if the current TestElement is not a component root. - * - * @return {List} - */ - get componentViewChildren(): List { - var shadowView = this._parentView.componentChildViews[this._boundElementIndex]; - - if (!isPresent(shadowView)) { - // The current test element is not a component. - return ListWrapper.create(); - } - - return this._getChildElements(shadowView, null); - } - - triggerEventHandler(eventName, eventObj): void { - this._parentView.triggerEventHandlers(eventName, eventObj, this._boundElementIndex); - } - - hasDirective(type: Type): boolean { - if (!isPresent(this._elementInjector)) { - return false; - } - return this._elementInjector.hasDirective(type); - } - - inject(type: Type): any { - if (!isPresent(this._elementInjector)) { - return null; - } - return this._elementInjector.get(type); - } - - /** - * Return the first descendant TestElememt matching the given predicate - * and scope. - * - * @param {Function: boolean} predicate - * @param {Scope} scope - * - * @return {TestElement} - */ - query(predicate: Function, scope = Scope.all): TestElement { - var results = this.queryAll(predicate, scope); - return results.length > 0 ? results[0] : null; - } - - /** - * Return descendant TestElememts matching the given predicate - * and scope. - * - * @param {Function: boolean} predicate - * @param {Scope} scope - * - * @return {List} - */ - queryAll(predicate: Function, scope = Scope.all): List { - var elementsInScope = scope(this); - - return ListWrapper.filter(elementsInScope, predicate); - } - - _getChildElements(view: AppView, parentBoundElementIndex: number): List { - var els = ListWrapper.create(); - var parentElementBinder = null; - if (isPresent(parentBoundElementIndex)) { - parentElementBinder = view.proto.elementBinders[parentBoundElementIndex]; - } - for (var i = 0; i < view.proto.elementBinders.length; ++i) { - var binder = view.proto.elementBinders[i]; - if (binder.parent == parentElementBinder) { - ListWrapper.push(els, new TestElement(view, i)); - - var views = view.viewContainers[i]; - if (isPresent(views)) { - ListWrapper.forEach(views.views, (nextView) => { - els = ListWrapper.concat(els, this._getChildElements(nextView, null)); - }); - } - } - } - return els; - } -} - -export function inspectElement(elementRef: ElementRef): TestElement { - return TestElement.create(elementRef); -} +import {DebugElement} from 'angular2/src/debug/debug_element'; /** * @exportedAs angular2/test */ -export class RootTestComponent extends TestElement { +export class RootTestComponent extends DebugElement { _componentRef: ComponentRef; _componentParentView: AppView; @@ -187,55 +43,7 @@ export class RootTestComponent extends TestElement { destroy(): void { this._componentRef.dispose(); } } -/** - * @exportedAs angular2/test - */ -export class Scope { - static all(testElement): List { - var scope = ListWrapper.create(); - ListWrapper.push(scope, testElement); - - ListWrapper.forEach(testElement.children, - (child) => { scope = ListWrapper.concat(scope, Scope.all(child)); }); - - ListWrapper.forEach(testElement.componentViewChildren, - (child) => { scope = ListWrapper.concat(scope, Scope.all(child)); }); - - return scope; - } - static light(testElement): List { - var scope = ListWrapper.create(); - ListWrapper.forEach(testElement.children, (child) => { - ListWrapper.push(scope, child); - scope = ListWrapper.concat(scope, Scope.light(child)); - }); - return scope; - } - - static view(testElement): List { - var scope = ListWrapper.create(); - - ListWrapper.forEach(testElement.componentViewChildren, (child) => { - ListWrapper.push(scope, child); - scope = ListWrapper.concat(scope, Scope.light(child)); - }); - return scope; - } -} - -/** - * @exportedAs angular2/test - */ -export class By { - static all(): Function { return (testElement) => true; } - - static css(selector: string): Function { - return (testElement) => { return DOM.elementMatches(testElement.domElement, selector); }; - } - static directive(type: Type): Function { - return (testElement) => { return testElement.hasDirective(type); }; - } -} +var _nextRootElementId = 0; /** * @exportedAs angular2/test @@ -331,13 +139,14 @@ export class TestComponentBuilder { }); }); - var rootEl = el('
'); + var rootElId = `root${_nextRootElementId++}`; + var rootEl = el(`
`); var doc = this._injector.get(DOCUMENT_TOKEN); // TODO(juliemr): can/should this be optional? DOM.appendChild(doc.body, rootEl); return this._injector.get(DynamicComponentLoader) - .loadAsRoot(rootComponentType, '#root', this._injector) + .loadAsRoot(rootComponentType, `#${rootElId}`, this._injector) .then((componentRef) => { return new RootTestComponent(componentRef); }); } } diff --git a/modules/angular2/src/test_lib/test_injector.ts b/modules/angular2/src/test_lib/test_injector.ts index 516be0de2c..22b65f2744 100644 --- a/modules/angular2/src/test_lib/test_injector.ts +++ b/modules/angular2/src/test_lib/test_injector.ts @@ -45,6 +45,7 @@ import {FunctionWrapper, Type} from 'angular2/src/facade/lang'; import {AppViewPool, APP_VIEW_POOL_CAPACITY} from 'angular2/src/core/compiler/view_pool'; import {AppViewManager} from 'angular2/src/core/compiler/view_manager'; import {AppViewManagerUtils} from 'angular2/src/core/compiler/view_manager_utils'; +import {ELEMENT_PROBE_CONFIG} from 'angular2/debug'; import {ProtoViewFactory} from 'angular2/src/core/compiler/proto_view_factory'; import {RenderCompiler, Renderer} from 'angular2/src/render/api'; import {DomRenderer, DOCUMENT_TOKEN} from 'angular2/src/render/dom/dom_renderer'; @@ -80,7 +81,6 @@ function _getAppBindings() { } catch (e) { appDoc = null; } - return [ bind(DOCUMENT_TOKEN) .toValue(appDoc), @@ -96,6 +96,7 @@ function _getAppBindings() { AppViewPool, AppViewManager, AppViewManagerUtils, + ELEMENT_PROBE_CONFIG, bind(APP_VIEW_POOL_CAPACITY).toValue(500), Compiler, CompilerCache, diff --git a/modules/angular2/test/core/compiler/integration_spec.ts b/modules/angular2/test/core/compiler/integration_spec.ts index ca9eb57c87..9033808d0c 100644 --- a/modules/angular2/test/core/compiler/integration_spec.ts +++ b/modules/angular2/test/core/compiler/integration_spec.ts @@ -725,9 +725,7 @@ export function main() { var updateHost = injector.get(DirectiveUpdatingHostActions); ObservableWrapper.subscribe(updateHost.setAttr, (_) => { - expect(stringifyElement(domElement)) - .toEqual( - '
'); + expect(DOM.hasAttribute(domElement, 'update-host-actions')).toBe(true); async.done(); }); diff --git a/modules/angular2/test/debug/debug_element_spec.ts b/modules/angular2/test/debug/debug_element_spec.ts new file mode 100644 index 0000000000..d23fe39b32 --- /dev/null +++ b/modules/angular2/test/debug/debug_element_spec.ts @@ -0,0 +1,273 @@ +import { + AsyncTestCompleter, + beforeEach, + ddescribe, + xdescribe, + describe, + dispatchEvent, + expect, + iit, + inject, + IS_DARTIUM, + beforeEachBindings, + it, + xit, + TestComponentBuilder, + By, + Scope +} from 'angular2/test_lib'; + +import {DOM} from 'angular2/src/dom/dom_adapter'; + +import {List, ListWrapper} from 'angular2/src/facade/collection'; +import {PromiseWrapper, EventEmitter, ObservableWrapper} from 'angular2/src/facade/async'; + +import {Injectable} from 'angular2/di'; + +import { + Directive, + Component, + View, +} from 'angular2/annotations'; + +import {NgFor} from 'angular2/src/directives/ng_for'; + +@Injectable() +class Logger { + log: List; + + constructor() { this.log = ListWrapper.create(); } + + add(thing: string) { ListWrapper.push(this.log, thing); } +} + +@Directive({selector: '[message]', properties: ['message']}) +@Injectable() +class MessageDir { + logger: Logger; + + constructor(logger: Logger) { this.logger = logger; } + + set message(newMessage) { this.logger.add(newMessage); } +} + +@Component({selector: 'child-comp'}) +@View({ + template: `
+ Child +
+ {{childBinding}}`, + directives: [MessageDir] +}) +@Injectable() +class ChildComp { + childBinding: string; + + constructor() { this.childBinding = 'Original'; } +} + +@Component({selector: 'parent-comp', appInjector: [Logger]}) +@View({ + template: `
+ Parent +
+ {{parentBinding}} + `, + directives: [ChildComp, MessageDir] +}) +@Injectable() +class ParentComp { + parentBinding: string; + constructor() { this.parentBinding = 'OriginalParent'; } +} + +@Directive({selector: 'custom-emitter', events: ['myevent']}) +@Injectable() +class CustomEmitter { + myevent: EventEmitter; + + constructor() { this.myevent = new EventEmitter(); } +} + +@Component({selector: 'events-comp'}) +@View({ + template: ` + `, + directives: [CustomEmitter] +}) +@Injectable() +class EventsComp { + clicked: boolean; + customed: boolean; + + constructor() { + this.clicked = false; + this.customed = false; + } + + handleClick() { this.clicked = true; } + + handleCustom() { this.customed = true; } +} + +@Component({selector: 'using-for', appInjector: [Logger]}) +@View({ + template: `{{thing}} +
    +
  • {{item}}
  • +
`, + directives: [NgFor, MessageDir] +}) +@Injectable() +class UsingFor { + stuff: List; + + constructor() { this.stuff = ['one', 'two', 'three']; } +} + +export function main() { + describe('debug element', function() { + + it('should list component child elements', + inject([TestComponentBuilder, AsyncTestCompleter], (tcb, async) => { + + tcb.createAsync(ParentComp) + .then((rootTestComponent) => { + rootTestComponent.detectChanges(); + + var childEls = rootTestComponent.children; + // The root is a lone component, and has no children in the light dom. + expect(childEls.length).toEqual(0); + + var rootCompChildren = rootTestComponent.componentViewChildren; + // The root component has 3 elements in its shadow view. + expect(rootCompChildren.length).toEqual(3); + expect(DOM.hasClass(rootCompChildren[0].domElement, 'parent')).toBe(true); + expect(DOM.hasClass(rootCompChildren[1].domElement, 'parent')).toBe(true); + expect(DOM.hasClass(rootCompChildren[2].domElement, 'child-comp-class')).toBe(true); + + var nested = rootCompChildren[0].children; + expect(nested.length).toEqual(1); + expect(DOM.hasClass(nested[0].domElement, 'parentnested')).toBe(true); + + var childComponent = rootCompChildren[2]; + expect(childComponent.children.length).toEqual(0); + + var childCompChildren = childComponent.componentViewChildren; + expect(childCompChildren.length).toEqual(2); + expect(DOM.hasClass(childCompChildren[0].domElement, 'child')).toBe(true); + expect(DOM.hasClass(childCompChildren[1].domElement, 'child')).toBe(true); + + var childNested = childCompChildren[0].children; + expect(childNested.length).toEqual(1); + expect(DOM.hasClass(childNested[0].domElement, 'childnested')).toBe(true); + + async.done(); + }); + })); + + it('should list child elements within viewports', + inject([TestComponentBuilder, AsyncTestCompleter], (tcb, async) => { + + tcb.createAsync(UsingFor).then((rootTestComponent) => { + rootTestComponent.detectChanges(); + + var childEls = rootTestComponent.componentViewChildren; + // TODO should this count include the