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:
Julie Ralph 2015-03-23 16:46:18 -07:00
parent f68cdf3878
commit e81e5fb2b9
15 changed files with 369 additions and 4 deletions

View File

@ -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,
];
}

View File

@ -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]) {},
});
}
}

View File

@ -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.
};
}
}

View File

@ -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));
}
}

View File

@ -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);
}

View File

@ -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

View File

@ -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();
}

View File

@ -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';
}

View File

@ -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);
}

View File

@ -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();
});
});
}));
});
}

View File

@ -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);
});
});
}

View File

@ -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]],

View File

@ -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),

View File

@ -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]],

View File

@ -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
});