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.
This commit is contained in:
parent
f68cdf3878
commit
e81e5fb2b9
|
@ -27,12 +27,14 @@ import {StyleInliner} from 'angular2/src/core/compiler/style_inliner';
|
|||
import {CssProcessor} from 'angular2/src/core/compiler/css_processor';
|
||||
import {Component} from 'angular2/src/core/annotations/annotations';
|
||||
import {PrivateComponentLoader} from 'angular2/src/core/compiler/private_component_loader';
|
||||
import {TestabilityRegistry, Testability} from 'angular2/src/core/testability/testability';
|
||||
|
||||
var _rootInjector: Injector;
|
||||
|
||||
// Contains everything that is safe to share between applications.
|
||||
var _rootBindings = [
|
||||
bind(Reflector).toValue(reflector)
|
||||
bind(Reflector).toValue(reflector),
|
||||
TestabilityRegistry
|
||||
];
|
||||
|
||||
export var appViewToken = new OpaqueToken('AppView');
|
||||
|
@ -57,9 +59,12 @@ function _injectorBindings(appComponentType): List<Binding> {
|
|||
}
|
||||
return element;
|
||||
}, [appComponentAnnotatedTypeToken, appDocumentToken]),
|
||||
|
||||
bind(appViewToken).toAsyncFactory((changeDetection, compiler, injector, appElement,
|
||||
appComponentAnnotatedType, strategy, eventManager) => {
|
||||
appComponentAnnotatedType, strategy, eventManager, testability, registry) => {
|
||||
|
||||
// We need to do this here to ensure that we create Testability and
|
||||
// it's ready on the window for users.
|
||||
registry.registerApplication(appElement, testability);
|
||||
var annotation = appComponentAnnotatedType.annotation;
|
||||
if(!isBlank(annotation) && !(annotation instanceof Component)) {
|
||||
var type = appComponentAnnotatedType.type;
|
||||
|
@ -79,7 +84,7 @@ function _injectorBindings(appComponentType): List<Binding> {
|
|||
return view;
|
||||
});
|
||||
}, [ChangeDetection, Compiler, Injector, appElementToken, appComponentAnnotatedTypeToken,
|
||||
ShadowDomStrategy, EventManager]),
|
||||
ShadowDomStrategy, EventManager, Testability, TestabilityRegistry]),
|
||||
|
||||
bind(appChangeDetectorToken).toFactory((rootView) => rootView.changeDetector,
|
||||
[appViewToken]),
|
||||
|
@ -109,6 +114,7 @@ function _injectorBindings(appComponentType): List<Binding> {
|
|||
StyleInliner,
|
||||
bind(CssProcessor).toFactory(() => new CssProcessor(null), []),
|
||||
PrivateComponentLoader,
|
||||
Testability,
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
library testability.get_testability;
|
||||
|
||||
import './testability.dart';
|
||||
|
||||
import 'dart:html';
|
||||
import 'dart:js' as js;
|
||||
|
||||
// Work around http://dartbug.com/17752, copied from
|
||||
// https://github.com/angular/angular.dart/blob/master/lib/introspection.dart
|
||||
// Proxies a Dart function that accepts up to 10 parameters.
|
||||
js.JsFunction _jsFunction(Function fn) {
|
||||
const Object X = __varargSentinel;
|
||||
return new js.JsFunction.withThis(
|
||||
(thisArg, [o1=X, o2=X, o3=X, o4=X, o5=X, o6=X, o7=X, o8=X, o9=X, o10=X]) {
|
||||
return __invokeFn(fn, o1, o2, o3, o4, o5, o6, o7, o8, o9, o10);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const Object __varargSentinel = const Object();
|
||||
|
||||
|
||||
__invokeFn(fn, o1, o2, o3, o4, o5, o6, o7, o8, o9, o10) {
|
||||
var args = [o1, o2, o3, o4, o5, o6, o7, o8, o9, o10];
|
||||
while (args.length > 0 && identical(args.last, __varargSentinel)) {
|
||||
args.removeLast();
|
||||
}
|
||||
return _jsify(Function.apply(fn, args));
|
||||
}
|
||||
|
||||
|
||||
// Helper function to JSify a Dart object. While this is *required* to JSify
|
||||
// the result of a scope.eval(), other uses are not required and are used to
|
||||
// work around http://dartbug.com/17752 in a convenient way (that bug affects
|
||||
// dart2js in checked mode.)
|
||||
_jsify(var obj) {
|
||||
if (obj == null || obj is js.JsObject) {
|
||||
return obj;
|
||||
}
|
||||
if (obj is _JsObjectProxyable) {
|
||||
return obj._toJsObject();
|
||||
}
|
||||
if (obj is Function) {
|
||||
return _jsFunction(obj);
|
||||
}
|
||||
if ((obj is Map) || (obj is Iterable)) {
|
||||
var mappedObj = (obj is Map) ?
|
||||
new Map.fromIterables(obj.keys, obj.values.map(_jsify)) : obj.map(_jsify);
|
||||
if (obj is List) {
|
||||
return new js.JsArray.from(mappedObj);
|
||||
} else {
|
||||
return new js.JsObject.jsify(mappedObj);
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
abstract class _JsObjectProxyable {
|
||||
js.JsObject _toJsObject();
|
||||
}
|
||||
|
||||
class PublicTestability implements _JsObjectProxyable {
|
||||
Testability _testability;
|
||||
PublicTestability(Testability testability) {
|
||||
this._testability = testability;
|
||||
}
|
||||
|
||||
whenStable(Function callback) {
|
||||
return this._testability.whenStable(callback);
|
||||
}
|
||||
|
||||
findBindings(Element elem, String binding, bool exactMatch) {
|
||||
return this._testability.findBindings(elem, binding, exactMatch);
|
||||
}
|
||||
|
||||
js.JsObject _toJsObject() {
|
||||
return _jsify({
|
||||
'findBindings': (bindingString, [exactMatch, allowNonElementNodes]) =>
|
||||
findBindings(bindingString, exactMatch, allowNonElementNodes),
|
||||
'whenStable': (callback) =>
|
||||
whenStable(() => callback.apply([])),
|
||||
})..['_dart_'] = this;
|
||||
}
|
||||
}
|
||||
|
||||
class GetTestability {
|
||||
static addToWindow(TestabilityRegistry registry) {
|
||||
js.context['angular2'] = _jsify({
|
||||
'getTestability': (Element elem) {
|
||||
Testability testability = registry.findTestabilityInTree(elem);
|
||||
return _jsify(new PublicTestability(testability));
|
||||
},
|
||||
'resumeBootstrap': ([arg]) {},
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
import {TestabilityRegistry, Testability} from 'angular2/src/core/testability/testability';
|
||||
|
||||
class PublicTestability {
|
||||
_testabililty: Testability;
|
||||
|
||||
constructor(testability: Testability) {
|
||||
this._testability = testability;
|
||||
}
|
||||
|
||||
whenStable(callback: Function) {
|
||||
this._testability.whenStable(callback);
|
||||
}
|
||||
|
||||
findBindings(using, binding: string, exactMatch: boolean) {
|
||||
return this._testability.findBindings(using, binding, exactMatch);
|
||||
}
|
||||
}
|
||||
|
||||
export class GetTestability {
|
||||
static addToWindow(registry: TestabilityRegistry) {
|
||||
if (!window.angular2) {
|
||||
window.angular2 = {};
|
||||
}
|
||||
window.angular2.getTestability = function(elem): PublicTestability {
|
||||
var testability = registry.findTestabilityInTree(elem);
|
||||
|
||||
if (testability == null) {
|
||||
throw new Error('Could not find testability for element.');
|
||||
}
|
||||
return new PublicTestability(testability);
|
||||
};
|
||||
window.angular2.resumeBootstrap = function() {
|
||||
// Intentionally left blank. This will allow Protractor to run
|
||||
// against angular2 without turning off Angular synchronization.
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
import {DOM} from 'angular2/src/dom/dom_adapter';
|
||||
import {Map, MapWrapper, List, ListWrapper} from 'angular2/src/facade/collection';
|
||||
import {StringWrapper, isBlank, BaseException} from 'angular2/src/facade/lang';
|
||||
import * as getTestabilityModule from 'angular2/src/core/testability/get_testability';
|
||||
|
||||
|
||||
/**
|
||||
* The Testability service provides testing hooks that can be accessed from
|
||||
* the browser and by services such as Protractor. Each bootstrapped Angular
|
||||
* application on the page will have an instance of Testability.
|
||||
*/
|
||||
export class Testability {
|
||||
_pendingCount: number;
|
||||
_callbacks: List;
|
||||
|
||||
constructor() {
|
||||
this._pendingCount = 0;
|
||||
this._callbacks = ListWrapper.create();
|
||||
}
|
||||
|
||||
increaseCount(delta: number = 1) {
|
||||
this._pendingCount += delta;
|
||||
if (this._pendingCount < 0) {
|
||||
throw new BaseException('pending async requests below zero');
|
||||
} else if (this._pendingCount == 0) {
|
||||
this._runCallbacks();
|
||||
}
|
||||
return this._pendingCount;
|
||||
}
|
||||
|
||||
_runCallbacks() {
|
||||
while (this._callbacks.length !== 0) {
|
||||
ListWrapper.removeLast(this._callbacks)();
|
||||
}
|
||||
}
|
||||
|
||||
whenStable(callback: Function) {
|
||||
ListWrapper.push(this._callbacks, callback);
|
||||
|
||||
if (this._pendingCount === 0) {
|
||||
this._runCallbacks();
|
||||
}
|
||||
// TODO(juliemr) - hook into the zone api.
|
||||
}
|
||||
|
||||
getPendingCount(): number {
|
||||
return this._pendingCount;
|
||||
}
|
||||
|
||||
findBindings(using, binding: string, exactMatch: boolean): List {
|
||||
// TODO(juliemr): implement.
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export class TestabilityRegistry {
|
||||
_applications: Map;
|
||||
|
||||
constructor() {
|
||||
this._applications = MapWrapper.create();
|
||||
|
||||
getTestabilityModule.GetTestability.addToWindow(this);
|
||||
}
|
||||
|
||||
registerApplication(token, testability: Testability) {
|
||||
MapWrapper.set(this._applications, token, testability);
|
||||
}
|
||||
|
||||
findTestabilityInTree(elem) : Testability {
|
||||
if (elem == null) {
|
||||
return null;
|
||||
}
|
||||
if (MapWrapper.contains(this._applications, elem)) {
|
||||
return MapWrapper.get(this._applications, elem);
|
||||
}
|
||||
if (DOM.isShadowRoot(elem)) {
|
||||
return this.findTestabilityInTree(DOM.getHost(elem));
|
||||
}
|
||||
return this.findTestabilityInTree(DOM.parentElement(elem));
|
||||
}
|
||||
}
|
|
@ -123,6 +123,7 @@ class BrowserDomAdapter extends GenericBrowserDomAdapter {
|
|||
}
|
||||
ShadowRoot createShadowRoot(Element el) => el.createShadowRoot();
|
||||
ShadowRoot getShadowRoot(Element el) => el.shadowRoot;
|
||||
Element getHost(Element el) => (el as ShadowRoot).host;
|
||||
clone(Node node) => node.clone(true);
|
||||
bool hasProperty(Element element, String name) =>
|
||||
new JsObject.fromBrowserObject(element).hasProperty(name);
|
||||
|
@ -188,6 +189,9 @@ class BrowserDomAdapter extends GenericBrowserDomAdapter {
|
|||
bool hasShadowRoot(Node node) {
|
||||
return node is Element && node.shadowRoot != null;
|
||||
}
|
||||
bool isShadowRoot(Node node) {
|
||||
return node is ShadowRoot;
|
||||
}
|
||||
Node importIntoDoc(Node node) {
|
||||
return document.importNode(node, true);
|
||||
}
|
||||
|
|
|
@ -158,6 +158,9 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter {
|
|||
getShadowRoot(el:HTMLElement): ShadowRoot {
|
||||
return el.shadowRoot;
|
||||
}
|
||||
getHost(el:HTMLElement): HTMLElement {
|
||||
return el.host;
|
||||
}
|
||||
clone(node:Node) {
|
||||
return node.cloneNode(true);
|
||||
}
|
||||
|
@ -245,6 +248,9 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter {
|
|||
hasShadowRoot(node):boolean {
|
||||
return node instanceof HTMLElement && isPresent(node.shadowRoot);
|
||||
}
|
||||
isShadowRoot(node):boolean {
|
||||
return node instanceof ShadowRoot;
|
||||
}
|
||||
importIntoDoc(node:Node) {
|
||||
var result = document.importNode(node, true);
|
||||
// Workaround WebKit https://bugs.webkit.org/show_bug.cgi?id=137619
|
||||
|
|
|
@ -147,6 +147,9 @@ export class DomAdapter {
|
|||
getShadowRoot(el) {
|
||||
throw _abstract();
|
||||
}
|
||||
getHost(el) {
|
||||
throw _abstract();
|
||||
}
|
||||
getDistributedNodes(el) {
|
||||
throw _abstract();
|
||||
}
|
||||
|
@ -231,6 +234,9 @@ export class DomAdapter {
|
|||
hasShadowRoot(node):boolean {
|
||||
throw _abstract();
|
||||
}
|
||||
isShadowRoot(node):boolean {
|
||||
throw _abstract();
|
||||
}
|
||||
importIntoDoc(node) {
|
||||
throw _abstract();
|
||||
}
|
||||
|
|
|
@ -120,6 +120,15 @@ class Html5LibDomAdapter implements DomAdapter {
|
|||
createStyleElement(String css, [doc]) {
|
||||
throw 'not implemented';
|
||||
}
|
||||
createShadowRoot(el) {
|
||||
throw 'not implemented';
|
||||
}
|
||||
getShadowRoot(el) {
|
||||
throw 'not implemented';
|
||||
}
|
||||
getHost(el) {
|
||||
throw 'not implemented';
|
||||
}
|
||||
clone(node) {
|
||||
throw 'not implemented';
|
||||
}
|
||||
|
@ -199,6 +208,9 @@ class Html5LibDomAdapter implements DomAdapter {
|
|||
bool hasShadowRoot(node) {
|
||||
throw 'not implemented';
|
||||
}
|
||||
bool isShadowRoot(node) {
|
||||
throw 'not implemented';
|
||||
}
|
||||
importIntoDoc(node) {
|
||||
throw 'not implemented';
|
||||
}
|
||||
|
|
|
@ -252,6 +252,9 @@ export class Parse5DomAdapter extends DomAdapter {
|
|||
getShadowRoot(el) {
|
||||
return el.shadowRoot;
|
||||
}
|
||||
getHost(el) {
|
||||
return el.host;
|
||||
}
|
||||
getDistributedNodes(el) {
|
||||
throw _notImplemented('getDistributedNodes');
|
||||
}
|
||||
|
@ -395,6 +398,9 @@ export class Parse5DomAdapter extends DomAdapter {
|
|||
hasShadowRoot(node):boolean {
|
||||
return isPresent(node.shadowRoot);
|
||||
}
|
||||
isShadowRoot(node): boolean {
|
||||
return this.getShadowRoot(node) == node;
|
||||
}
|
||||
importIntoDoc(node) {
|
||||
return this.clone(node);
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ import {PromiseWrapper} from 'angular2/src/facade/async';
|
|||
import {bind, Inject} from 'angular2/di';
|
||||
import {Template} from 'angular2/src/core/annotations/template';
|
||||
import {LifeCycle} from 'angular2/src/core/life_cycle/life_cycle';
|
||||
import {Testability, TestabilityRegistry} from 'angular2/src/core/testability/testability';
|
||||
|
||||
@Component({selector: 'hello-app'})
|
||||
@Template({inline: '{{greeting}} world!'})
|
||||
|
@ -180,5 +181,21 @@ export function main() {
|
|||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should register each application with the testability registry', inject([AsyncTestCompleter], (async) => {
|
||||
var injectorPromise1 = bootstrap(HelloRootCmp, testBindings);
|
||||
var injectorPromise2 = bootstrap(HelloRootCmp2, testBindings);
|
||||
|
||||
PromiseWrapper.all([injectorPromise1, injectorPromise2]).then((injectors) => {
|
||||
var registry = injectors[0].get(TestabilityRegistry);
|
||||
PromiseWrapper.all([
|
||||
injectors[0].asyncGet(Testability),
|
||||
injectors[1].asyncGet(Testability)]).then((testabilities) => {
|
||||
expect(registry.findTestabilityInTree(el)).toEqual(testabilities[0]);
|
||||
expect(registry.findTestabilityInTree(el2)).toEqual(testabilities[1]);
|
||||
async.done();
|
||||
});
|
||||
});
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
import {describe, ddescribe, it, iit, xit, xdescribe, expect, beforeEach} from 'angular2/test_lib';
|
||||
import {Testability} from 'angular2/src/core/testability/testability';
|
||||
|
||||
|
||||
export function main() {
|
||||
describe('Testability', () => {
|
||||
var testability, executed;
|
||||
|
||||
beforeEach(() => {
|
||||
testability = new Testability();
|
||||
executed = false;
|
||||
});
|
||||
|
||||
it('should start with a pending count of 0', () => {
|
||||
expect(testability.getPendingCount()).toEqual(0);
|
||||
});
|
||||
|
||||
it('should fire whenstable callbacks if pending count is 0', () => {
|
||||
testability.whenStable(() => executed = true);
|
||||
expect(executed).toBe(true);
|
||||
});
|
||||
|
||||
it('should not call whenstable callbacks when there are pending counts', () => {
|
||||
testability.increaseCount(2);
|
||||
testability.whenStable(() => executed = true);
|
||||
|
||||
expect(executed).toBe(false);
|
||||
testability.increaseCount(-1);
|
||||
expect(executed).toBe(false);
|
||||
});
|
||||
|
||||
it('should fire whenstable callbacks when pending drops to 0', () => {
|
||||
testability.increaseCount(2);
|
||||
testability.whenStable(() => executed = true);
|
||||
|
||||
expect(executed).toBe(false);
|
||||
|
||||
testability.increaseCount(-2);
|
||||
expect(executed).toBe(true);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -18,6 +18,7 @@ import {ComponentUrlMapper} from 'angular2/src/core/compiler/component_url_mappe
|
|||
import {StyleInliner} from 'angular2/src/core/compiler/style_inliner';
|
||||
import {CssProcessor} from 'angular2/src/core/compiler/css_processor';
|
||||
import {PrivateComponentLoader} from 'angular2/src/core/compiler/private_component_loader';
|
||||
import {TestabilityRegistry, Testability} from 'angular2/src/core/testability/testability';
|
||||
|
||||
import {reflector} from 'angular2/src/reflection/reflection';
|
||||
import {DOM} from 'angular2/src/dom/dom_adapter';
|
||||
|
@ -228,6 +229,18 @@ function setupReflector() {
|
|||
"annotations": []
|
||||
});
|
||||
|
||||
reflector.registerType(TestabilityRegistry, {
|
||||
"factory": () => new TestabilityRegistry(),
|
||||
"parameters": [],
|
||||
"annotations": []
|
||||
});
|
||||
|
||||
reflector.registerType(Testability, {
|
||||
"factory": () => new Testability(),
|
||||
"parameters": [],
|
||||
"annotations": []
|
||||
});
|
||||
|
||||
reflector.registerType(Content, {
|
||||
"factory": (lightDom, el) => new Content(lightDom, el),
|
||||
"parameters": [[DestinationLightDom], [NgElement]],
|
||||
|
|
|
@ -25,6 +25,7 @@ import {ComponentUrlMapper} from 'angular2/src/core/compiler/component_url_mappe
|
|||
import {StyleInliner} from 'angular2/src/core/compiler/style_inliner';
|
||||
import {CssProcessor} from 'angular2/src/core/compiler/css_processor';
|
||||
import {PrivateComponentLoader} from 'angular2/src/core/compiler/private_component_loader';
|
||||
import {TestabilityRegistry, Testability} from 'angular2/src/core/testability/testability';
|
||||
|
||||
import {If, For} from 'angular2/directives';
|
||||
import {App, setupReflectorForApp} from './app';
|
||||
|
@ -294,6 +295,18 @@ export function setupReflectorForAngular() {
|
|||
"annotations" : [new Decorator({selector: '[content]'})]
|
||||
});
|
||||
|
||||
reflector.registerType(TestabilityRegistry, {
|
||||
"factory": () => new TestabilityRegistry(),
|
||||
"parameters": [],
|
||||
"annotations": []
|
||||
});
|
||||
|
||||
reflector.registerType(Testability, {
|
||||
"factory": () => new Testability(),
|
||||
"parameters": [],
|
||||
"annotations": []
|
||||
});
|
||||
|
||||
reflector.registerType(StyleInliner, {
|
||||
"factory": (xhr, styleUrlResolver, urlResolver) =>
|
||||
new StyleInliner(xhr, styleUrlResolver, urlResolver),
|
||||
|
|
|
@ -18,6 +18,7 @@ import {ComponentUrlMapper} from 'angular2/src/core/compiler/component_url_mappe
|
|||
import {StyleInliner} from 'angular2/src/core/compiler/style_inliner';
|
||||
import {CssProcessor} from 'angular2/src/core/compiler/css_processor';
|
||||
import {PrivateComponentLoader} from 'angular2/src/core/compiler/private_component_loader';
|
||||
import {TestabilityRegistry, Testability} from 'angular2/src/core/testability/testability';
|
||||
|
||||
import {reflector} from 'angular2/src/reflection/reflection';
|
||||
import {DOM} from 'angular2/src/dom/dom_adapter';
|
||||
|
@ -138,6 +139,18 @@ function setupReflector() {
|
|||
"annotations": []
|
||||
});
|
||||
|
||||
reflector.registerType(TestabilityRegistry, {
|
||||
"factory": () => new TestabilityRegistry(),
|
||||
"parameters": [],
|
||||
"annotations": []
|
||||
});
|
||||
|
||||
reflector.registerType(Testability, {
|
||||
"factory": () => new Testability(),
|
||||
"parameters": [],
|
||||
"annotations": []
|
||||
});
|
||||
|
||||
reflector.registerType(StyleUrlResolver, {
|
||||
"factory": (urlResolver) => new StyleUrlResolver(urlResolver),
|
||||
"parameters": [[UrlResolver]],
|
||||
|
|
|
@ -21,6 +21,7 @@ import {StyleInliner} from 'angular2/src/core/compiler/style_inliner';
|
|||
import {CssProcessor} from 'angular2/src/core/compiler/css_processor';
|
||||
import {EventManager} from 'angular2/src/core/events/event_manager';
|
||||
import {PrivateComponentLoader} from 'angular2/src/core/compiler/private_component_loader';
|
||||
import {TestabilityRegistry, Testability} from 'angular2/src/core/testability/testability';
|
||||
|
||||
import {reflector} from 'angular2/src/reflection/reflection';
|
||||
|
||||
|
@ -185,6 +186,18 @@ function setup() {
|
|||
"annotations": []
|
||||
});
|
||||
|
||||
reflector.registerType(TestabilityRegistry, {
|
||||
"factory": () => new TestabilityRegistry(),
|
||||
"parameters": [],
|
||||
"annotations": []
|
||||
});
|
||||
|
||||
reflector.registerType(Testability, {
|
||||
"factory": () => new Testability(),
|
||||
"parameters": [],
|
||||
"annotations": []
|
||||
});
|
||||
|
||||
reflector.registerGetters({
|
||||
"greeting": (a) => a.greeting
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue