Skip to content
This repository was archived by the owner on Feb 22, 2018. It is now read-only.

Commit f2d1f2e

Browse files
committed
feat(testability): implement the testability for ProtractorDart
See https://pub.dartlang.org/packages/protractor
1 parent b3f2e6c commit f2d1f2e

13 files changed

+368
-107
lines changed

lib/angular.dart

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ export 'package:angular/application.dart';
55
export 'package:angular/core/module.dart';
66
export 'package:angular/directive/module.dart';
77
export 'package:angular/core/annotation.dart';
8-
export 'package:angular/introspection.dart';
8+
export 'package:angular/introspection.dart' hide
9+
elementExpando, publishToJavaScript;
910
export 'package:angular/formatter/module.dart';
1011
export 'package:angular/routing/module.dart';
1112
export 'package:di/di.dart' hide lastKeyId;

lib/application.dart

+1-1
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ import 'package:angular/core_dom/module_internal.dart';
8181
import 'package:angular/directive/module.dart';
8282
import 'package:angular/formatter/module_internal.dart';
8383
import 'package:angular/routing/module.dart';
84-
import 'package:angular/introspection_js.dart';
84+
import 'package:angular/introspection.dart';
8585

8686
import 'package:angular/core_dom/static_keys.dart';
8787

lib/core_dom/element_binder.dart

+7-3
Original file line numberDiff line numberDiff line change
@@ -237,9 +237,8 @@ class ElementBinder {
237237
void _createDirectiveFactories(DirectiveRef ref, nodeModule, node, nodesAttrsDirectives, nodeAttrs,
238238
visibility) {
239239
if (ref.type == TextMustache) {
240-
nodeModule.bind(TextMustache, toFactory: (Injector injector) {
241-
return new TextMustache(node, ref.valueAST, injector.getByKey(SCOPE_KEY));
242-
});
240+
nodeModule.bind(TextMustache, toFactory: (Injector injector) => new TextMustache(
241+
node, ref.valueAST, injector.getByKey(SCOPE_KEY)));
243242
} else if (ref.type == AttrMustache) {
244243
if (nodesAttrsDirectives.isEmpty) {
245244
nodeModule.bind(AttrMustache, toFactory: (Injector injector) {
@@ -310,6 +309,11 @@ class ElementBinder {
310309
probe = _expando[node] =
311310
new ElementProbe(parentInjector.getByKey(ELEMENT_PROBE_KEY),
312311
node, nodeInjector, scope);
312+
directiveRefs.forEach((DirectiveRef ref) {
313+
if (ref.valueAST != null) {
314+
probe.bindingExpressions.add(ref.valueAST.expression);
315+
}
316+
});
313317
scope.on(ScopeEvent.DESTROY).listen((_) {
314318
_expando[node] = null;
315319
});

lib/core_dom/mustache.dart

+3-3
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ class AttrMustache {
2626

2727
// This Directive is special and does not go through injection.
2828
AttrMustache(this._attrs,
29-
String this._attrName,
30-
AST valueAST,
31-
Scope scope) {
29+
String this._attrName,
30+
AST valueAST,
31+
Scope scope) {
3232
_updateMarkup('', 'INITIAL-VALUE');
3333

3434
_attrs.listenObserverChanges(_attrName, (hasObservers) {

lib/core_dom/view_factory.dart

+2
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,8 @@ class ElementProbe {
193193
final Injector injector;
194194
final Scope scope;
195195
final directives = [];
196+
final bindingExpressions = <String>[];
197+
final modelExpressions = <String>[];
196198

197199
ElementProbe(this.parent, this.element, this.injector, this.scope);
198200
}

lib/directive/ng_bind.dart

+6-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,12 @@ part of angular.directive;
2020
class NgBind {
2121
final dom.Element element;
2222

23-
NgBind(this.element);
23+
NgBind(this.element, ElementProbe probe) {
24+
// TODO(chirayu): Generalize this.
25+
if (probe != null) {
26+
probe.bindingExpressions.add(element.attributes['ng-bind']);
27+
}
28+
}
2429

2530
set value(value) => element.text = value == null ? '' : value.toString();
2631
}

lib/directive/ng_model.dart

+4-1
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,13 @@ class NgModel extends NgControl implements AttachAware {
4545
bool _watchCollection;
4646

4747
NgModel(this._scope, NgElement element, Injector injector, NodeAttrs attrs,
48-
Animate animate)
48+
Animate animate, ElementProbe probe)
4949
: super(element, injector, animate)
5050
{
5151
_expression = attrs["ng-model"];
52+
if (probe != null) {
53+
probe.modelExpressions.add(_expression);
54+
}
5255
watchCollection = false;
5356

5457
//Since the user will never be editing the value of a select element then

lib/introspection.dart

+216-13
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,60 @@
33
*/
44
library angular.introspection;
55

6+
import 'dart:async' as async;
67
import 'dart:html' as dom;
8+
import 'dart:js' as js;
79
import 'package:di/di.dart';
8-
import 'package:angular/introspection_js.dart';
10+
import 'package:angular/animate/module.dart';
911
import 'package:angular/core/module_internal.dart';
1012
import 'package:angular/core_dom/module_internal.dart';
13+
import 'package:angular/core/static_keys.dart';
14+
15+
16+
/**
17+
* A global write only variable which keeps track of objects attached to the
18+
* elements. This is useful for debugging AngularDart application from the
19+
* browser's REPL.
20+
*/
21+
var elementExpando = new Expando('element');
22+
23+
24+
ElementProbe _findProbeWalkingUp(dom.Node node, [dom.Node ascendUntil]) {
25+
while (node != null && node != ascendUntil) {
26+
var probe = elementExpando[node];
27+
if (probe != null) return probe;
28+
node = node.parent;
29+
}
30+
return null;
31+
}
32+
33+
34+
_walkProbesInTree(dom.Node node, Function walker) {
35+
var probe = elementExpando[node];
36+
if (probe == null || walker(probe) != true) {
37+
for (var child in node.childNodes) {
38+
_walkProbesInTree(child, walker);
39+
}
40+
}
41+
}
42+
43+
44+
ElementProbe _findProbeInTree(dom.Node node, [dom.Node ascendUntil]) {
45+
var probe;
46+
_walkProbesInTree(node, (_probe) {
47+
probe = _probe;
48+
return true;
49+
});
50+
return (probe != null) ? probe : _findProbeWalkingUp(node, ascendUntil);
51+
}
52+
53+
54+
List<ElementProbe> _findAllProbesInTree(dom.Node node) {
55+
List<ElementProbe> probes = [];
56+
_walkProbesInTree(node, probes.add);
57+
return probes;
58+
}
59+
1160

1261
/**
1362
* Return the [ElementProbe] object for the closest [Element] in the hierarchy.
@@ -21,25 +70,21 @@ import 'package:angular/core_dom/module_internal.dart';
2170
* function is not intended to be called from Angular application.
2271
*/
2372
ElementProbe ngProbe(nodeOrSelector) {
24-
var errorMsg;
25-
var node;
2673
if (nodeOrSelector == null) throw "ngProbe called without node";
74+
var node = nodeOrSelector;
2775
if (nodeOrSelector is String) {
2876
var nodes = ngQuery(dom.document, nodeOrSelector);
29-
if (nodes.isNotEmpty) node = nodes.first;
30-
errorMsg = "Could not find a probe for the selector '$nodeOrSelector' nor its parents";
31-
} else {
32-
node = nodeOrSelector;
33-
errorMsg = "Could not find a probe for the node '$node' nor its parents";
77+
node = (nodes.isNotEmpty) ? nodes.first : null;
3478
}
35-
while (node != null) {
36-
var probe = elementExpando[node];
37-
if (probe != null) return probe;
38-
node = node.parent;
79+
var probe = _findProbeWalkingUp(node);
80+
if (probe != null) {
81+
return probe;
3982
}
40-
throw errorMsg;
83+
var forWhat = (nodeOrSelector is String) ? "selector" : "node";
84+
throw "Could not find a probe for the $forWhat '$nodeOrSelector' nor its parents";
4185
}
4286

87+
4388
/**
4489
* Return the [Injector] associated with a current [Element].
4590
*
@@ -79,6 +124,7 @@ List<dom.Element> ngQuery(dom.Node element, String selector,
79124
return list;
80125
}
81126

127+
82128
/**
83129
* Return a List of directives associated with a current [Element].
84130
*
@@ -88,3 +134,160 @@ List<dom.Element> ngQuery(dom.Node element, String selector,
88134
*/
89135
List<Object> ngDirectives(nodeOrSelector) => ngProbe(nodeOrSelector).directives;
90136

137+
138+
139+
js.JsObject _jsProbe(ElementProbe probe) {
140+
return new js.JsObject.jsify({
141+
"element": probe.element,
142+
"injector": _jsInjector(probe.injector),
143+
"scope": _jsScopeFromProbe(probe),
144+
"directives": probe.directives.map((directive) => _jsDirective(directive)),
145+
"bindings": probe.bindingExpressions,
146+
"models": probe.modelExpressions
147+
})..['_dart_'] = probe;
148+
}
149+
150+
151+
js.JsObject _jsInjector(Injector injector) =>
152+
new js.JsObject.jsify({"get": injector.get})..['_dart_'] = injector;
153+
154+
155+
js.JsObject _jsScopeFromProbe(ElementProbe probe) =>
156+
_jsScope(probe.scope, probe.injector.getByKey(SCOPE_STATS_CONFIG_KEY));
157+
158+
159+
js.JsObject _jsScope(Scope scope, ScopeStatsConfig config) {
160+
return new js.JsObject.jsify({
161+
"apply": scope.apply,
162+
"broadcast": scope.broadcast,
163+
"context": scope.context,
164+
"destroy": scope.destroy,
165+
"digest": scope.rootScope.digest,
166+
"emit": scope.emit,
167+
"flush": scope.rootScope.flush,
168+
"get": (name) => scope.context[name],
169+
"isAttached": scope.isAttached,
170+
"isDestroyed": scope.isDestroyed,
171+
"set": (name, value) => scope.context[name] = value,
172+
"scopeStatsEnable": () => config.emit = true,
173+
"scopeStatsDisable": () => config.emit = false,
174+
r"$eval": (expr) => _jsify(scope.eval(expr)),
175+
})..['_dart_'] = scope;
176+
}
177+
178+
179+
// Helper function to JSify the result of a scope.eval() for simple cases.
180+
_jsify(var obj) {
181+
if (obj is js.JsObject) {
182+
return obj;
183+
} else if (obj is Iterable) {
184+
return new js.JsObject.jsify(obj)..['_dart_'] = obj;
185+
} else {
186+
return obj;
187+
}
188+
}
189+
190+
191+
_jsDirective(directive) => directive;
192+
193+
194+
abstract class _JsObjectProxyable {
195+
js.JsObject _toJsObject();
196+
}
197+
198+
199+
typedef List<String> _GetExpressionsFromProbe(ElementProbe probe);
200+
201+
202+
/**
203+
* Returns the "$testability service" object for JS / Protractor use.
204+
*
205+
* JS code expects to get a hold of this object in the following way:
206+
*
207+
* // Prereq: There is an "angular" object on window accessible via JS.
208+
* var testability = angular.element(document).injector().get('$testability');
209+
*/
210+
class _Testability implements _JsObjectProxyable {
211+
final dom.Node node;
212+
final ElementProbe probe;
213+
214+
_Testability(this.node, this.probe);
215+
_Testability.fromNode(dom.Node node): this(node, _findProbeInTree(node));
216+
217+
notifyWhenNoOutstandingRequests(callback) {
218+
probe.injector.get(VmTurnZone).run(
219+
() => new async.Timer(Duration.ZERO, callback));
220+
}
221+
222+
/**
223+
* Returns a list of all nodes in the selected tree that have an `ng-model`
224+
* binding specified by the [modelString]. If the optional [exactMatch]
225+
* parameter is provided and true, it restricts the searches to bindings that
226+
* are exact matches for [modelString].
227+
*/
228+
List<dom.Node> findModels(String modelString, [bool exactMatch]) => _findByExpression(
229+
modelString, exactMatch, (ElementProbe probe) => probe.modelExpressions);
230+
231+
/**
232+
* Returns a list of all nodes in the selected tree that have `ng-bind` or
233+
* mustache bindings specified by the [bindingString]. If the optional
234+
* [exactMatch] parameter is provided and true, it restricts the searches to
235+
* bindings that are exact matches for [bindingString].
236+
*/
237+
List<dom.Node> findBindings(String bindingString, [bool exactMatch]) => _findByExpression(
238+
bindingString, exactMatch, (ElementProbe probe) => probe.bindingExpressions);
239+
240+
List<dom.Node> _findByExpression(String query, bool exactMatch, _GetExpressionsFromProbe getExpressions) {
241+
List<ElementProbe> probes = _findAllProbesInTree(node);
242+
if (probes.length == 0) {
243+
probes.add(_findProbeWalkingUp(node));
244+
}
245+
List<dom.Node> results = [];
246+
for (ElementProbe probe in probes) {
247+
for (String expression in getExpressions(probe)) {
248+
if(exactMatch == true ? expression == query : expression.indexOf(query) >= 0) {
249+
results.add(probe.element);
250+
}
251+
}
252+
}
253+
return results;
254+
}
255+
256+
allowAnimations(bool allowed) {
257+
Animate animate = probe.injector.get(Animate);
258+
bool previous = animate.animationsAllowed;
259+
animate.animationsAllowed = (allowed == true);
260+
return previous;
261+
}
262+
263+
js.JsObject _toJsObject() {
264+
return new js.JsObject.jsify({
265+
'allowAnimations': allowAnimations,
266+
'findBindings': (bindingString, [exactMatch]) =>
267+
findBindings(bindingString, exactMatch),
268+
'findModels': (modelExpressions, [exactMatch]) =>
269+
findModels(modelExpressions, exactMatch),
270+
'notifyWhenNoOutstandingRequests': (callback) =>
271+
notifyWhenNoOutstandingRequests(() => callback.apply([])),
272+
'probe': () => _jsProbe(probe),
273+
'scope': () => _jsScopeFromProbe(probe),
274+
'eval': (expr) => probe.scope.eval(expr),
275+
'query': (String selector, [String containsText]) =>
276+
ngQuery(node, selector, containsText),
277+
})..['_dart_'] = this;
278+
}
279+
}
280+
281+
282+
void publishToJavaScript() {
283+
var C = js.context;
284+
C['ngProbe'] = (nodeOrSelector) => _jsProbe(ngProbe(nodeOrSelector));
285+
C['ngInjector'] = (nodeOrSelector) => _jsInjector(ngInjector(nodeOrSelector));
286+
C['ngScope'] = (nodeOrSelector) => _jsScopeFromProbe(ngProbe(nodeOrSelector));
287+
C['ngQuery'] = (dom.Node node, String selector, [String containsText]) =>
288+
new js.JsArray.from(ngQuery(node, selector, containsText));
289+
C['angular'] = new js.JsObject.jsify({
290+
'resumeBootstrap': ([arg]) {},
291+
'getTestability': (node) => new _Testability.fromNode(node)._toJsObject(),
292+
});
293+
}

0 commit comments

Comments
 (0)