Skip to content

Commit e81e5fb

Browse files
committed
feat(testability): add an initial scaffold for the testability api
Make each application component register itself onto the testability API and exports the API onto the window object.
1 parent f68cdf3 commit e81e5fb

File tree

15 files changed

+369
-4
lines changed

15 files changed

+369
-4
lines changed

modules/angular2/src/core/application.js

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,14 @@ import {StyleInliner} from 'angular2/src/core/compiler/style_inliner';
2727
import {CssProcessor} from 'angular2/src/core/compiler/css_processor';
2828
import {Component} from 'angular2/src/core/annotations/annotations';
2929
import {PrivateComponentLoader} from 'angular2/src/core/compiler/private_component_loader';
30+
import {TestabilityRegistry, Testability} from 'angular2/src/core/testability/testability';
3031

3132
var _rootInjector: Injector;
3233

3334
// Contains everything that is safe to share between applications.
3435
var _rootBindings = [
35-
bind(Reflector).toValue(reflector)
36+
bind(Reflector).toValue(reflector),
37+
TestabilityRegistry
3638
];
3739

3840
export var appViewToken = new OpaqueToken('AppView');
@@ -57,9 +59,12 @@ function _injectorBindings(appComponentType): List<Binding> {
5759
}
5860
return element;
5961
}, [appComponentAnnotatedTypeToken, appDocumentToken]),
60-
6162
bind(appViewToken).toAsyncFactory((changeDetection, compiler, injector, appElement,
62-
appComponentAnnotatedType, strategy, eventManager) => {
63+
appComponentAnnotatedType, strategy, eventManager, testability, registry) => {
64+
65+
// We need to do this here to ensure that we create Testability and
66+
// it's ready on the window for users.
67+
registry.registerApplication(appElement, testability);
6368
var annotation = appComponentAnnotatedType.annotation;
6469
if(!isBlank(annotation) && !(annotation instanceof Component)) {
6570
var type = appComponentAnnotatedType.type;
@@ -79,7 +84,7 @@ function _injectorBindings(appComponentType): List<Binding> {
7984
return view;
8085
});
8186
}, [ChangeDetection, Compiler, Injector, appElementToken, appComponentAnnotatedTypeToken,
82-
ShadowDomStrategy, EventManager]),
87+
ShadowDomStrategy, EventManager, Testability, TestabilityRegistry]),
8388

8489
bind(appChangeDetectorToken).toFactory((rootView) => rootView.changeDetector,
8590
[appViewToken]),
@@ -109,6 +114,7 @@ function _injectorBindings(appComponentType): List<Binding> {
109114
StyleInliner,
110115
bind(CssProcessor).toFactory(() => new CssProcessor(null), []),
111116
PrivateComponentLoader,
117+
Testability,
112118
];
113119
}
114120

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
library testability.get_testability;
2+
3+
import './testability.dart';
4+
5+
import 'dart:html';
6+
import 'dart:js' as js;
7+
8+
// Work around http://dartbug.com/17752, copied from
9+
// https://github.com/angular/angular.dart/blob/master/lib/introspection.dart
10+
// Proxies a Dart function that accepts up to 10 parameters.
11+
js.JsFunction _jsFunction(Function fn) {
12+
const Object X = __varargSentinel;
13+
return new js.JsFunction.withThis(
14+
(thisArg, [o1=X, o2=X, o3=X, o4=X, o5=X, o6=X, o7=X, o8=X, o9=X, o10=X]) {
15+
return __invokeFn(fn, o1, o2, o3, o4, o5, o6, o7, o8, o9, o10);
16+
});
17+
}
18+
19+
20+
const Object __varargSentinel = const Object();
21+
22+
23+
__invokeFn(fn, o1, o2, o3, o4, o5, o6, o7, o8, o9, o10) {
24+
var args = [o1, o2, o3, o4, o5, o6, o7, o8, o9, o10];
25+
while (args.length > 0 && identical(args.last, __varargSentinel)) {
26+
args.removeLast();
27+
}
28+
return _jsify(Function.apply(fn, args));
29+
}
30+
31+
32+
// Helper function to JSify a Dart object. While this is *required* to JSify
33+
// the result of a scope.eval(), other uses are not required and are used to
34+
// work around http://dartbug.com/17752 in a convenient way (that bug affects
35+
// dart2js in checked mode.)
36+
_jsify(var obj) {
37+
if (obj == null || obj is js.JsObject) {
38+
return obj;
39+
}
40+
if (obj is _JsObjectProxyable) {
41+
return obj._toJsObject();
42+
}
43+
if (obj is Function) {
44+
return _jsFunction(obj);
45+
}
46+
if ((obj is Map) || (obj is Iterable)) {
47+
var mappedObj = (obj is Map) ?
48+
new Map.fromIterables(obj.keys, obj.values.map(_jsify)) : obj.map(_jsify);
49+
if (obj is List) {
50+
return new js.JsArray.from(mappedObj);
51+
} else {
52+
return new js.JsObject.jsify(mappedObj);
53+
}
54+
}
55+
return obj;
56+
}
57+
58+
abstract class _JsObjectProxyable {
59+
js.JsObject _toJsObject();
60+
}
61+
62+
class PublicTestability implements _JsObjectProxyable {
63+
Testability _testability;
64+
PublicTestability(Testability testability) {
65+
this._testability = testability;
66+
}
67+
68+
whenStable(Function callback) {
69+
return this._testability.whenStable(callback);
70+
}
71+
72+
findBindings(Element elem, String binding, bool exactMatch) {
73+
return this._testability.findBindings(elem, binding, exactMatch);
74+
}
75+
76+
js.JsObject _toJsObject() {
77+
return _jsify({
78+
'findBindings': (bindingString, [exactMatch, allowNonElementNodes]) =>
79+
findBindings(bindingString, exactMatch, allowNonElementNodes),
80+
'whenStable': (callback) =>
81+
whenStable(() => callback.apply([])),
82+
})..['_dart_'] = this;
83+
}
84+
}
85+
86+
class GetTestability {
87+
static addToWindow(TestabilityRegistry registry) {
88+
js.context['angular2'] = _jsify({
89+
'getTestability': (Element elem) {
90+
Testability testability = registry.findTestabilityInTree(elem);
91+
return _jsify(new PublicTestability(testability));
92+
},
93+
'resumeBootstrap': ([arg]) {},
94+
});
95+
}
96+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import {TestabilityRegistry, Testability} from 'angular2/src/core/testability/testability';
2+
3+
class PublicTestability {
4+
_testabililty: Testability;
5+
6+
constructor(testability: Testability) {
7+
this._testability = testability;
8+
}
9+
10+
whenStable(callback: Function) {
11+
this._testability.whenStable(callback);
12+
}
13+
14+
findBindings(using, binding: string, exactMatch: boolean) {
15+
return this._testability.findBindings(using, binding, exactMatch);
16+
}
17+
}
18+
19+
export class GetTestability {
20+
static addToWindow(registry: TestabilityRegistry) {
21+
if (!window.angular2) {
22+
window.angular2 = {};
23+
}
24+
window.angular2.getTestability = function(elem): PublicTestability {
25+
var testability = registry.findTestabilityInTree(elem);
26+
27+
if (testability == null) {
28+
throw new Error('Could not find testability for element.');
29+
}
30+
return new PublicTestability(testability);
31+
};
32+
window.angular2.resumeBootstrap = function() {
33+
// Intentionally left blank. This will allow Protractor to run
34+
// against angular2 without turning off Angular synchronization.
35+
};
36+
}
37+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import {DOM} from 'angular2/src/dom/dom_adapter';
2+
import {Map, MapWrapper, List, ListWrapper} from 'angular2/src/facade/collection';
3+
import {StringWrapper, isBlank, BaseException} from 'angular2/src/facade/lang';
4+
import * as getTestabilityModule from 'angular2/src/core/testability/get_testability';
5+
6+
7+
/**
8+
* The Testability service provides testing hooks that can be accessed from
9+
* the browser and by services such as Protractor. Each bootstrapped Angular
10+
* application on the page will have an instance of Testability.
11+
*/
12+
export class Testability {
13+
_pendingCount: number;
14+
_callbacks: List;
15+
16+
constructor() {
17+
this._pendingCount = 0;
18+
this._callbacks = ListWrapper.create();
19+
}
20+
21+
increaseCount(delta: number = 1) {
22+
this._pendingCount += delta;
23+
if (this._pendingCount < 0) {
24+
throw new BaseException('pending async requests below zero');
25+
} else if (this._pendingCount == 0) {
26+
this._runCallbacks();
27+
}
28+
return this._pendingCount;
29+
}
30+
31+
_runCallbacks() {
32+
while (this._callbacks.length !== 0) {
33+
ListWrapper.removeLast(this._callbacks)();
34+
}
35+
}
36+
37+
whenStable(callback: Function) {
38+
ListWrapper.push(this._callbacks, callback);
39+
40+
if (this._pendingCount === 0) {
41+
this._runCallbacks();
42+
}
43+
// TODO(juliemr) - hook into the zone api.
44+
}
45+
46+
getPendingCount(): number {
47+
return this._pendingCount;
48+
}
49+
50+
findBindings(using, binding: string, exactMatch: boolean): List {
51+
// TODO(juliemr): implement.
52+
return [];
53+
}
54+
}
55+
56+
export class TestabilityRegistry {
57+
_applications: Map;
58+
59+
constructor() {
60+
this._applications = MapWrapper.create();
61+
62+
getTestabilityModule.GetTestability.addToWindow(this);
63+
}
64+
65+
registerApplication(token, testability: Testability) {
66+
MapWrapper.set(this._applications, token, testability);
67+
}
68+
69+
findTestabilityInTree(elem) : Testability {
70+
if (elem == null) {
71+
return null;
72+
}
73+
if (MapWrapper.contains(this._applications, elem)) {
74+
return MapWrapper.get(this._applications, elem);
75+
}
76+
if (DOM.isShadowRoot(elem)) {
77+
return this.findTestabilityInTree(DOM.getHost(elem));
78+
}
79+
return this.findTestabilityInTree(DOM.parentElement(elem));
80+
}
81+
}

modules/angular2/src/dom/browser_adapter.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ class BrowserDomAdapter extends GenericBrowserDomAdapter {
123123
}
124124
ShadowRoot createShadowRoot(Element el) => el.createShadowRoot();
125125
ShadowRoot getShadowRoot(Element el) => el.shadowRoot;
126+
Element getHost(Element el) => (el as ShadowRoot).host;
126127
clone(Node node) => node.clone(true);
127128
bool hasProperty(Element element, String name) =>
128129
new JsObject.fromBrowserObject(element).hasProperty(name);
@@ -188,6 +189,9 @@ class BrowserDomAdapter extends GenericBrowserDomAdapter {
188189
bool hasShadowRoot(Node node) {
189190
return node is Element && node.shadowRoot != null;
190191
}
192+
bool isShadowRoot(Node node) {
193+
return node is ShadowRoot;
194+
}
191195
Node importIntoDoc(Node node) {
192196
return document.importNode(node, true);
193197
}

modules/angular2/src/dom/browser_adapter.es6

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,9 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter {
158158
getShadowRoot(el:HTMLElement): ShadowRoot {
159159
return el.shadowRoot;
160160
}
161+
getHost(el:HTMLElement): HTMLElement {
162+
return el.host;
163+
}
161164
clone(node:Node) {
162165
return node.cloneNode(true);
163166
}
@@ -245,6 +248,9 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter {
245248
hasShadowRoot(node):boolean {
246249
return node instanceof HTMLElement && isPresent(node.shadowRoot);
247250
}
251+
isShadowRoot(node):boolean {
252+
return node instanceof ShadowRoot;
253+
}
248254
importIntoDoc(node:Node) {
249255
var result = document.importNode(node, true);
250256
// Workaround WebKit https://bugs.webkit.org/show_bug.cgi?id=137619

modules/angular2/src/dom/dom_adapter.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,9 @@ export class DomAdapter {
147147
getShadowRoot(el) {
148148
throw _abstract();
149149
}
150+
getHost(el) {
151+
throw _abstract();
152+
}
150153
getDistributedNodes(el) {
151154
throw _abstract();
152155
}
@@ -231,6 +234,9 @@ export class DomAdapter {
231234
hasShadowRoot(node):boolean {
232235
throw _abstract();
233236
}
237+
isShadowRoot(node):boolean {
238+
throw _abstract();
239+
}
234240
importIntoDoc(node) {
235241
throw _abstract();
236242
}

modules/angular2/src/dom/html_adapter.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,15 @@ class Html5LibDomAdapter implements DomAdapter {
120120
createStyleElement(String css, [doc]) {
121121
throw 'not implemented';
122122
}
123+
createShadowRoot(el) {
124+
throw 'not implemented';
125+
}
126+
getShadowRoot(el) {
127+
throw 'not implemented';
128+
}
129+
getHost(el) {
130+
throw 'not implemented';
131+
}
123132
clone(node) {
124133
throw 'not implemented';
125134
}
@@ -199,6 +208,9 @@ class Html5LibDomAdapter implements DomAdapter {
199208
bool hasShadowRoot(node) {
200209
throw 'not implemented';
201210
}
211+
bool isShadowRoot(node) {
212+
throw 'not implemented';
213+
}
202214
importIntoDoc(node) {
203215
throw 'not implemented';
204216
}

modules/angular2/src/dom/parse5_adapter.cjs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,9 @@ export class Parse5DomAdapter extends DomAdapter {
252252
getShadowRoot(el) {
253253
return el.shadowRoot;
254254
}
255+
getHost(el) {
256+
return el.host;
257+
}
255258
getDistributedNodes(el) {
256259
throw _notImplemented('getDistributedNodes');
257260
}
@@ -395,6 +398,9 @@ export class Parse5DomAdapter extends DomAdapter {
395398
hasShadowRoot(node):boolean {
396399
return isPresent(node.shadowRoot);
397400
}
401+
isShadowRoot(node): boolean {
402+
return this.getShadowRoot(node) == node;
403+
}
398404
importIntoDoc(node) {
399405
return this.clone(node);
400406
}

modules/angular2/test/core/application_spec.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {PromiseWrapper} from 'angular2/src/facade/async';
1919
import {bind, Inject} from 'angular2/di';
2020
import {Template} from 'angular2/src/core/annotations/template';
2121
import {LifeCycle} from 'angular2/src/core/life_cycle/life_cycle';
22+
import {Testability, TestabilityRegistry} from 'angular2/src/core/testability/testability';
2223

2324
@Component({selector: 'hello-app'})
2425
@Template({inline: '{{greeting}} world!'})
@@ -180,5 +181,21 @@ export function main() {
180181
async.done();
181182
});
182183
}));
184+
185+
it('should register each application with the testability registry', inject([AsyncTestCompleter], (async) => {
186+
var injectorPromise1 = bootstrap(HelloRootCmp, testBindings);
187+
var injectorPromise2 = bootstrap(HelloRootCmp2, testBindings);
188+
189+
PromiseWrapper.all([injectorPromise1, injectorPromise2]).then((injectors) => {
190+
var registry = injectors[0].get(TestabilityRegistry);
191+
PromiseWrapper.all([
192+
injectors[0].asyncGet(Testability),
193+
injectors[1].asyncGet(Testability)]).then((testabilities) => {
194+
expect(registry.findTestabilityInTree(el)).toEqual(testabilities[0]);
195+
expect(registry.findTestabilityInTree(el2)).toEqual(testabilities[1]);
196+
async.done();
197+
});
198+
});
199+
}));
183200
});
184201
}

0 commit comments

Comments
 (0)