Skip to content

Commit f9908cd

Browse files
committed
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 angular#1992
1 parent 24bc4b6 commit f9908cd

File tree

11 files changed

+669
-458
lines changed

11 files changed

+669
-458
lines changed

modules/angular2/debug.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './src/debug/debug_element';
2+
export {inspectDomElement, ELEMENT_PROBE_CONFIG} from './src/debug/debug_element_view_listener';
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
import {Type, isPresent, BaseException, isBlank} from 'angular2/src/facade/lang';
2+
import {List, ListWrapper, MapWrapper} from 'angular2/src/facade/collection';
3+
4+
import {DOM} from 'angular2/src/dom/dom_adapter';
5+
6+
import {ElementInjector} from 'angular2/src/core/compiler/element_injector';
7+
import {AppView} from 'angular2/src/core/compiler/view';
8+
import {internalView} from 'angular2/src/core/compiler/view_ref';
9+
import {ElementRef} from 'angular2/src/core/compiler/element_ref';
10+
11+
import {resolveInternalDomView} from 'angular2/src/render/dom/view/view';
12+
13+
/**
14+
* @exportedAs angular2/test
15+
*
16+
* An DebugElement contains information from the Angular compiler about an
17+
* element and provides access to the corresponding ElementInjector and
18+
* underlying dom Element, as well as a way to query for children.
19+
*/
20+
export class DebugElement {
21+
_elementInjector: ElementInjector;
22+
23+
constructor(private _parentView: AppView, private _boundElementIndex: number) {
24+
this._elementInjector = this._parentView.elementInjectors[this._boundElementIndex];
25+
}
26+
27+
static create(elementRef: ElementRef): DebugElement {
28+
return new DebugElement(internalView(elementRef.parentView), elementRef.boundElementIndex);
29+
}
30+
31+
get componentInstance(): any {
32+
if (!isPresent(this._elementInjector)) {
33+
return null;
34+
}
35+
return this._elementInjector.getComponent();
36+
}
37+
38+
get dynamicallyCreatedComponentInstance(): any {
39+
if (!isPresent(this._elementInjector)) {
40+
return null;
41+
}
42+
return this._elementInjector.getDynamicallyLoadedComponent();
43+
}
44+
45+
get domElement(): any {
46+
return resolveInternalDomView(this._parentView.render).boundElements[this._boundElementIndex];
47+
}
48+
49+
getDirectiveInstance(directiveIndex: number): any {
50+
return this._elementInjector.getDirectiveAtIndex(directiveIndex);
51+
}
52+
53+
/**
54+
* Get child DebugElements from within the Light DOM.
55+
*
56+
* @return {List<DebugElement>}
57+
*/
58+
get children(): List<DebugElement> {
59+
var thisElementBinder = this._parentView.proto.elementBinders[this._boundElementIndex];
60+
61+
return this._getChildElements(this._parentView, thisElementBinder.index);
62+
}
63+
64+
/**
65+
* Get the root DebugElement children of a component. Returns an empty
66+
* list if the current DebugElement is not a component root.
67+
*
68+
* @return {List<DebugElement>}
69+
*/
70+
get componentViewChildren(): List<DebugElement> {
71+
var shadowView = this._parentView.componentChildViews[this._boundElementIndex];
72+
73+
if (!isPresent(shadowView)) {
74+
// The current element is not a component.
75+
return ListWrapper.create();
76+
}
77+
78+
return this._getChildElements(shadowView, null);
79+
}
80+
81+
triggerEventHandler(eventName, eventObj): void {
82+
this._parentView.triggerEventHandlers(eventName, eventObj, this._boundElementIndex);
83+
}
84+
85+
hasDirective(type: Type): boolean {
86+
if (!isPresent(this._elementInjector)) {
87+
return false;
88+
}
89+
return this._elementInjector.hasDirective(type);
90+
}
91+
92+
inject(type: Type): any {
93+
if (!isPresent(this._elementInjector)) {
94+
return null;
95+
}
96+
return this._elementInjector.get(type);
97+
}
98+
99+
/**
100+
* Return the first descendant TestElememt matching the given predicate
101+
* and scope.
102+
*
103+
* @param {Function: boolean} predicate
104+
* @param {Scope} scope
105+
*
106+
* @return {DebugElement}
107+
*/
108+
query(predicate: Function, scope = Scope.all): DebugElement {
109+
var results = this.queryAll(predicate, scope);
110+
return results.length > 0 ? results[0] : null;
111+
}
112+
113+
/**
114+
* Return descendant TestElememts matching the given predicate
115+
* and scope.
116+
*
117+
* @param {Function: boolean} predicate
118+
* @param {Scope} scope
119+
*
120+
* @return {List<DebugElement>}
121+
*/
122+
queryAll(predicate: Function, scope = Scope.all): List<DebugElement> {
123+
var elementsInScope = scope(this);
124+
125+
return ListWrapper.filter(elementsInScope, predicate);
126+
}
127+
128+
_getChildElements(view: AppView, parentBoundElementIndex: number): List<DebugElement> {
129+
var els = ListWrapper.create();
130+
var parentElementBinder = null;
131+
if (isPresent(parentBoundElementIndex)) {
132+
parentElementBinder = view.proto.elementBinders[parentBoundElementIndex];
133+
}
134+
for (var i = 0; i < view.proto.elementBinders.length; ++i) {
135+
var binder = view.proto.elementBinders[i];
136+
if (binder.parent == parentElementBinder) {
137+
ListWrapper.push(els, new DebugElement(view, i));
138+
139+
var views = view.viewContainers[i];
140+
if (isPresent(views)) {
141+
ListWrapper.forEach(views.views, (nextView) => {
142+
els = ListWrapper.concat(els, this._getChildElements(nextView, null));
143+
});
144+
}
145+
}
146+
}
147+
return els;
148+
}
149+
}
150+
151+
export function inspectElement(elementRef: ElementRef): DebugElement {
152+
return DebugElement.create(elementRef);
153+
}
154+
155+
/**
156+
* @exportedAs angular2/test
157+
*/
158+
export class Scope {
159+
static all(debugElement): List<DebugElement> {
160+
var scope = ListWrapper.create();
161+
ListWrapper.push(scope, debugElement);
162+
163+
ListWrapper.forEach(debugElement.children,
164+
(child) => { scope = ListWrapper.concat(scope, Scope.all(child)); });
165+
166+
ListWrapper.forEach(debugElement.componentViewChildren,
167+
(child) => { scope = ListWrapper.concat(scope, Scope.all(child)); });
168+
169+
return scope;
170+
}
171+
static light(debugElement): List<DebugElement> {
172+
var scope = ListWrapper.create();
173+
ListWrapper.forEach(debugElement.children, (child) => {
174+
ListWrapper.push(scope, child);
175+
scope = ListWrapper.concat(scope, Scope.light(child));
176+
});
177+
return scope;
178+
}
179+
180+
static view(debugElement): List<DebugElement> {
181+
var scope = ListWrapper.create();
182+
183+
ListWrapper.forEach(debugElement.componentViewChildren, (child) => {
184+
ListWrapper.push(scope, child);
185+
scope = ListWrapper.concat(scope, Scope.light(child));
186+
});
187+
return scope;
188+
}
189+
}
190+
191+
/**
192+
* @exportedAs angular2/test
193+
*/
194+
export class By {
195+
static all(): Function { return (debugElement) => true; }
196+
197+
static css(selector: string): Function {
198+
return (debugElement) => { return DOM.elementMatches(debugElement.domElement, selector); };
199+
}
200+
static directive(type: Type): Function {
201+
return (debugElement) => { return debugElement.hasDirective(type); };
202+
}
203+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import {
2+
CONST_EXPR,
3+
isPresent,
4+
NumberWrapper,
5+
StringWrapper,
6+
RegExpWrapper
7+
} from 'angular2/src/facade/lang';
8+
import {MapWrapper, Map, ListWrapper, List} from 'angular2/src/facade/collection';
9+
import {Injectable, bind, Binding} from 'angular2/di';
10+
import {AppViewListener} from 'angular2/src/core/compiler/view_listener';
11+
import {AppView} from 'angular2/src/core/compiler/view';
12+
import {DOM} from 'angular2/src/dom/dom_adapter';
13+
import {resolveInternalDomView} from 'angular2/src/render/dom/view/view';
14+
import {DebugElement} from './debug_element';
15+
16+
const NG_ID_PROPERTY = 'ngid';
17+
const INSPECT_GLOBAL_NAME = 'ngProbe';
18+
19+
var NG_ID_SEPARATOR_RE = RegExpWrapper.create('#');
20+
var NG_ID_SEPARATOR = '#';
21+
22+
// Need to keep the views in a global Map so that multiple angular apps are supported
23+
var _allIdsByView: Map<AppView, number> = CONST_EXPR(MapWrapper.create());
24+
var _allViewsById: Map<number, AppView> = CONST_EXPR(MapWrapper.create());
25+
var _nextId = 0;
26+
27+
function _setElementId(element, indices: List<number>) {
28+
if (isPresent(element)) {
29+
DOM.setData(element, NG_ID_PROPERTY, ListWrapper.join(indices, NG_ID_SEPARATOR));
30+
}
31+
}
32+
33+
function _getElementId(element): List<number> {
34+
var elId = DOM.getData(element, NG_ID_PROPERTY);
35+
if (isPresent(elId)) {
36+
return ListWrapper.map(StringWrapper.split(elId, NG_ID_SEPARATOR_RE),
37+
(partStr) => NumberWrapper.parseInt(partStr, 10));
38+
} else {
39+
return null;
40+
}
41+
}
42+
43+
export function inspectDomElement(element): DebugElement {
44+
var elId = _getElementId(element);
45+
if (isPresent(elId)) {
46+
var view = MapWrapper.get(_allViewsById, elId[0]);
47+
if (isPresent(view)) {
48+
return new DebugElement(view, elId[1]);
49+
}
50+
}
51+
return null;
52+
}
53+
54+
@Injectable()
55+
export class DebugElementViewListener implements AppViewListener {
56+
constructor() { DOM.setGlobalVar(INSPECT_GLOBAL_NAME, inspectDomElement); }
57+
58+
viewCreated(view: AppView) {
59+
var viewId = _nextId++;
60+
MapWrapper.set(_allViewsById, viewId, view);
61+
MapWrapper.set(_allIdsByView, view, viewId);
62+
var renderView = resolveInternalDomView(view.render);
63+
for (var i = 0; i < renderView.boundElements.length; i++) {
64+
_setElementId(renderView.boundElements[i], [viewId, i]);
65+
}
66+
}
67+
68+
viewDestroyed(view: AppView) {
69+
var viewId = MapWrapper.get(_allIdsByView, view);
70+
MapWrapper.delete(_allIdsByView, view);
71+
MapWrapper.delete(_allViewsById, viewId);
72+
}
73+
}
74+
75+
export var ELEMENT_PROBE_CONFIG = [
76+
DebugElementViewListener,
77+
bind(AppViewListener).toAlias(DebugElementViewListener),
78+
];

modules/angular2/src/dom/browser_adapter.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -258,9 +258,7 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter {
258258
setData(element, name: string, value: string) { element.dataset[name] = value; }
259259
getData(element, name: string): string { return element.dataset[name]; }
260260
// TODO(tbosch): move this into a separate environment class once we have it
261-
setGlobalVar(name: string, value: any) {
262-
global[name] = value;
263-
}
261+
setGlobalVar(name: string, value: any) { global[name] = value; }
264262
}
265263

266264
// based on urlUtils.js in AngularJS 1

0 commit comments

Comments
 (0)