feat(test): add element probe
Usage: bootstrap the app with the special binding `ELEMENT_PROBE_CONFIG` from `angular2/debug`. This will provide a global method `ngProbe(element)` that will expose a `DebugElement` with directive instances, ... on it. During tests that use Angular's test injector, the probe is enabled by default. The `DebugElement ` can be retrieved via the function `inspectDomElement` of `angular2/debug`. Note that the `TestComponentBuilder` already returns `DebugElement `s. Closes #1992
This commit is contained in:
203
modules/angular2/src/debug/debug_element.ts
Normal file
203
modules/angular2/src/debug/debug_element.ts
Normal file
@ -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<DebugElement>}
|
||||
*/
|
||||
get children(): List<DebugElement> {
|
||||
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<DebugElement>}
|
||||
*/
|
||||
get componentViewChildren(): List<DebugElement> {
|
||||
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<DebugElement>}
|
||||
*/
|
||||
queryAll(predicate: Function, scope = Scope.all): List<DebugElement> {
|
||||
var elementsInScope = scope(this);
|
||||
|
||||
return ListWrapper.filter(elementsInScope, predicate);
|
||||
}
|
||||
|
||||
_getChildElements(view: AppView, parentBoundElementIndex: number): List<DebugElement> {
|
||||
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<DebugElement> {
|
||||
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<DebugElement> {
|
||||
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<DebugElement> {
|
||||
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); };
|
||||
}
|
||||
}
|
78
modules/angular2/src/debug/debug_element_view_listener.ts
Normal file
78
modules/angular2/src/debug/debug_element_view_listener.ts
Normal file
@ -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<AppView, number> = CONST_EXPR(MapWrapper.create());
|
||||
var _allViewsById: Map<number, AppView> = CONST_EXPR(MapWrapper.create());
|
||||
var _nextId = 0;
|
||||
|
||||
function _setElementId(element, indices: List<number>) {
|
||||
if (isPresent(element)) {
|
||||
DOM.setData(element, NG_ID_PROPERTY, ListWrapper.join(indices, NG_ID_SEPARATOR));
|
||||
}
|
||||
}
|
||||
|
||||
function _getElementId(element): List<number> {
|
||||
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),
|
||||
];
|
@ -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
|
||||
|
@ -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<TestElement>}
|
||||
*/
|
||||
get children(): List<TestElement> {
|
||||
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<TestElement>}
|
||||
*/
|
||||
get componentViewChildren(): List<TestElement> {
|
||||
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<TestElement>}
|
||||
*/
|
||||
queryAll(predicate: Function, scope = Scope.all): List<TestElement> {
|
||||
var elementsInScope = scope(this);
|
||||
|
||||
return ListWrapper.filter(elementsInScope, predicate);
|
||||
}
|
||||
|
||||
_getChildElements(view: AppView, parentBoundElementIndex: number): List<TestElement> {
|
||||
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<TestElement> {
|
||||
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<TestElement> {
|
||||
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<TestElement> {
|
||||
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('<div id="root"></div>');
|
||||
var rootElId = `root${_nextRootElementId++}`;
|
||||
var rootEl = el(`<div id="${rootElId}"></div>`);
|
||||
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); });
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
Reference in New Issue
Block a user