Merge remote-tracking branch 'origin/open614d' into open-master

This commit is contained in:
bwyu 2015-01-05 11:14:07 -08:00
commit 9981543156
43 changed files with 1058 additions and 52 deletions

1
.gitignore vendored
View File

@ -4,6 +4,7 @@
*.tgz
*.DS_Store
*.idea
*.sass-cache
# External dependencies

View File

@ -13,6 +13,7 @@ define(
* @constructor
*/
function SinewaveTelemetryProvider($q, $timeout) {
var subscriptions = [];
//
function matchesSource(request) {
@ -43,8 +44,48 @@ define(
}, 0);
}
function handleSubscriptions() {
subscriptions.forEach(function (subscription) {
var requests = subscription.requests;
subscription.callback(doPackage(
requests.filter(matchesSource).map(generateData)
));
});
}
function startGenerating() {
$timeout(function () {
handleSubscriptions();
if (subscriptions.length > 0) {
startGenerating();
}
}, 1000);
}
function subscribe(callback, requests) {
var subscription = {
callback: callback,
requests: requests
};
function unsubscribe() {
subscriptions = subscriptions.filter(function (s) {
return s !== subscription;
});
}
subscriptions.push(subscription);
if (subscriptions.length === 1) {
startGenerating();
}
return unsubscribe;
}
return {
requestTelemetry: requestTelemetry
requestTelemetry: requestTelemetry,
subscribe: subscribe
};
}

View File

@ -0,0 +1,29 @@
{
"name": "Example taxonomy",
"description": "Example illustrating the addition of a static top-level hierarchy",
"extensions": {
"roots": [
{
"id": "exampleTaxonomy",
"model": {
"type": "folder",
"name": "Stub Subsystems",
"composition": [
"examplePacket0",
"examplePacket1",
"examplePacket2"
]
}
}
],
"components": [
{
"provides": "modelService",
"type": "provider",
"implementation": "ExampleTaxonomyModelProvider.js",
"depends": [ "$q" ]
}
]
}
}

View File

@ -0,0 +1,48 @@
/*global define*/
define(
[],
function () {
"use strict";
function ExampleTaxonomyModelProvider($q) {
var models = {},
packetId,
telemetryId,
i,
j;
// Add some "subsystems"
for (i = 0; i < 3; i += 1) {
packetId = "examplePacket" + i;
models[packetId] = {
name: "Stub Subsystem " + (i + 1),
type: "telemetry.panel",
composition: []
};
// Add some "telemetry points"
for (j = 0; j < 100 * (i + 1); j += 1) {
telemetryId = "exampleTelemetry" + j;
models[telemetryId] = {
name: "SWG" + i + "." + j,
type: "generator",
telemetry: {
period: 10 + i + j
}
};
models[packetId].composition.push(telemetryId);
}
}
return {
getModels: function () {
return $q.when(models);
}
};
}
return ExampleTaxonomyModelProvider;
}
);

View File

@ -24,49 +24,59 @@
"controllers": [
{
"key": "TreeNodeController",
"implementation": "TreeNodeController.js",
"implementation": "controllers/TreeNodeController.js",
"depends": [ "$scope", "$timeout" ]
},
{
"key": "ActionGroupController",
"implementation": "ActionGroupController.js",
"implementation": "controllers/ActionGroupController.js",
"depends": [ "$scope" ]
},
{
"key": "ToggleController",
"implementation": "ToggleController.js"
"implementation": "controllers/ToggleController.js"
},
{
"key": "ContextMenuController",
"implementation": "ContextMenuController.js",
"implementation": "controllers/ContextMenuController.js",
"depends": [ "$scope" ]
},
{
"key": "ClickAwayController",
"implementation": "ClickAwayController.js",
"implementation": "controllers/ClickAwayController.js",
"depends": [ "$scope", "$document" ]
},
{
"key": "ViewSwitcherController",
"implementation": "ViewSwitcherController.js",
"implementation": "controllers/ViewSwitcherController.js",
"depends": [ "$scope" ]
},
{
"key": "BottomBarController",
"implementation": "BottomBarController.js",
"implementation": "controllers/BottomBarController.js",
"depends": [ "indicators[]" ]
},
{
"key": "GetterSetterController",
"implementation": "controllers/GetterSetterController.js",
"depends": [ "$scope" ]
}
],
"directives": [
{
"key": "mctContainer",
"implementation": "MCTContainer.js",
"implementation": "directives/MCTContainer.js",
"depends": [ "containers[]" ]
},
{
"key": "mctDrag",
"implementation": "MCTDrag.js",
"implementation": "directives/MCTDrag.js",
"depends": [ "$document" ]
},
{
"key": "mctResize",
"implementation": "directives/MCTResize.js",
"depends": [ "$timeout" ]
}
],
"containers": [

View File

@ -1,5 +1,13 @@
<!-- look at action-button for example -->
<span class="t-filter l-filter">
<input type="search" class="t-filter-input" ng-model="filter" placeholder="Filter..."/>
<a class="ui-symbol t-a-clear s-a-clear" ng-click="filter = null">x</a>
<span class="t-filter l-filter"
ng-controller="GetterSetterController">
<input type="search"
class="t-filter-input"
ng-model="getterSetter.value"
placeholder="Filter..."/>
<a class="ui-symbol t-a-clear s-a-clear"
ng-show="getterSetter.value !== ''"
ng-click="getterSetter.value = ''">
x
</a>
</span>

View File

@ -0,0 +1,69 @@
/*global define*/
define(
[],
function () {
"use strict";
/**
* This controller acts as an adapter to permit getter-setter
* functions to be used as ng-model arguments to controls,
* such as the input-filter. This is supported natively in
* Angular 1.3+ via `ng-model-options`, so this controller
* should be made obsolete after any upgrade to Angular 1.3.
*
* It expects to find in scope a value `ngModel` which is a
* function which, when called with no arguments, acts as a
* getter, and when called with one argument, acts as a setter.
*
* It also publishes into the scope a value `getterSetter.value`
* which is meant to be used as an assignable expression.
*
* This controller watches both of these; when one changes,
* it will update the other's value to match. Because of this,
* the `ngModel` function should be both stable and computationally
* inexpensive, as it will be invoked often.
*
* Getter-setter style models can be preferable when there
* is significant indirection between templates; "dotless"
* expressions in `ng-model` can behave unexpectedly due to the
* rules of scope, but dots are lost when passed in via `ng-model`
* (so if a control is internally implemented using regular
* form elements, it can't transparently pass through the `ng-model`
* parameter it received.) Getter-setter functions are never the
* target of a scope assignment and so avoid this problem.
*
* @constructor
* @param {Scope} $scope the controller's scope
*/
function GetterSetterController($scope) {
// Update internal assignable state based on changes
// to the getter-setter function.
function updateGetterSetter() {
if (typeof $scope.ngModel === 'function') {
$scope.getterSetter.value = $scope.ngModel();
}
}
// Update the external getter-setter based on changes
// to the assignable state.
function updateNgModel() {
if (typeof $scope.ngModel === 'function') {
$scope.ngModel($scope.getterSetter.value);
}
}
// Watch for changes to both expressions
$scope.$watch("ngModel()", updateGetterSetter);
$scope.$watch("getterSetter.value", updateNgModel);
// Publish an assignable field into scope.
$scope.getterSetter = {};
}
return GetterSetterController;
}
);

View File

@ -0,0 +1,82 @@
/*global define*/
define(
[],
function () {
"use strict";
// Default resize interval
var DEFAULT_INTERVAL = 100;
/**
* The mct-resize directive allows the size of a displayed
* HTML element to be tracked. This is done by polling,
* since the DOM API does not currently provide suitable
* events to watch this reliably.
*
* Attributes related to this directive are interpreted as
* follows:
*
* * `mct-resize`: An Angular expression to evaluate when
* the size changes; the variable `bounds` will be provided
* with two fields, `width` and `height`, both in pixels.
* * `mct-resize-interval`: Optional; the interval, in milliseconds,
* at which to watch for updates. In some cases checking for
* resize can carry a cost (it forces recalculation of
* positions within the document) so it may be preferable to watch
* infrequently. If omitted, a default of 100ms will be used.
* This is an Angular expression, and it will be re-evaluated after
* each interval.
*
* @constructor
*
*/
function MCTResize($timeout) {
// Link; start listening for changes to an element's size
function link(scope, element, attrs) {
var lastBounds;
// Determine how long to wait before the next update
function currentInterval() {
return attrs.mctResizeInterval ?
scope.$eval(attrs.mctResizeInterval) :
DEFAULT_INTERVAL;
}
// Evaluate mct-resize with the current bounds
function fireEval(bounds) {
// Only update when bounds actually change
if (!lastBounds ||
lastBounds.width !== bounds.width ||
lastBounds.height !== bounds.height) {
scope.$eval(attrs.mctResize, { bounds: bounds });
lastBounds = bounds;
}
}
// Callback to fire after each timeout;
// update bounds and schedule another timeout
function onInterval() {
fireEval({
width: element[0].offsetWidth,
height: element[0].offsetHeight
});
$timeout(onInterval, currentInterval());
}
// Handle the initial callback
onInterval();
}
return {
// mct-resize only makes sense as an attribute
restrict: "A",
// Link function, to begin watching for changes
link: link
};
}
return MCTResize;
}
);

View File

@ -1,7 +1,7 @@
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
define(
["../src/ActionGroupController"],
["../../src/controllers/ActionGroupController"],
function (ActionGroupController) {
"use strict";

View File

@ -1,7 +1,7 @@
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
define(
["../src/BottomBarController"],
["../../src/controllers/BottomBarController"],
function (BottomBarController) {
"use strict";

View File

@ -1,7 +1,7 @@
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
define(
["../src/ClickAwayController"],
["../../src/controllers/ClickAwayController"],
function (ClickAwayController) {
"use strict";

View File

@ -1,7 +1,7 @@
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
define(
["../src/ContextMenuController"],
["../../src/controllers/ContextMenuController"],
function (ContextMenuController) {
"use strict";

View File

@ -0,0 +1,64 @@
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
define(
["../../src/controllers/GetterSetterController"],
function (GetterSetterController) {
"use strict";
describe("The getter-setter controller", function () {
var mockScope,
mockModel,
controller;
beforeEach(function () {
mockScope = jasmine.createSpyObj("$scope", ["$watch"]);
mockModel = jasmine.createSpy("ngModel");
mockScope.ngModel = mockModel;
controller = new GetterSetterController(mockScope);
});
it("watches for changes to external and internal mode", function () {
expect(mockScope.$watch).toHaveBeenCalledWith(
"ngModel()",
jasmine.any(Function)
);
expect(mockScope.$watch).toHaveBeenCalledWith(
"getterSetter.value",
jasmine.any(Function)
);
});
it("updates an external function when changes are detected", function () {
mockScope.getterSetter.value = "some new value";
// Verify precondition
expect(mockScope.ngModel)
.not.toHaveBeenCalledWith("some new value");
// Fire the matching watcher
mockScope.$watch.calls.forEach(function (call) {
if (call.args[0] === "getterSetter.value") {
call.args[1](mockScope.getterSetter.value);
}
});
// Verify getter-setter was notified
expect(mockScope.ngModel)
.toHaveBeenCalledWith("some new value");
});
it("updates internal state when external changes are detected", function () {
mockScope.ngModel.andReturn("some other new value");
// Verify precondition
expect(mockScope.getterSetter.value).toBeUndefined();
// Fire the matching watcher
mockScope.$watch.calls.forEach(function (call) {
if (call.args[0] === "ngModel()") {
call.args[1]("some other new value");
}
});
// Verify state in scope was updated
expect(mockScope.getterSetter.value)
.toEqual("some other new value");
});
});
}
);

View File

@ -1,7 +1,7 @@
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
define(
["../src/ToggleController"],
["../../src/controllers/ToggleController"],
function (ToggleController) {
"use strict";

View File

@ -1,7 +1,7 @@
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
define(
["../src/TreeNodeController"],
["../../src/controllers/TreeNodeController"],
function (TreeNodeController) {
"use strict";

View File

@ -4,7 +4,7 @@
* MCTRepresentationSpec. Created by vwoeltje on 11/6/14.
*/
define(
["../src/ViewSwitcherController"],
["../../src/controllers/ViewSwitcherController"],
function (ViewSwitcherController) {
"use strict";

View File

@ -1,7 +1,7 @@
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
define(
["../src/MCTContainer"],
["../../src/directives/MCTContainer"],
function (MCTContainer) {
"use strict";

View File

@ -1,7 +1,7 @@
/*global define,describe,it,expect,beforeEach,jasmine*/
define(
["../src/MCTDrag"],
["../../src/directives/MCTDrag"],
function (MCTDrag) {
"use strict";

View File

@ -0,0 +1,68 @@
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
define(
["../../src/directives/MCTResize"],
function (MCTResize) {
"use strict";
describe("The mct-resize directive", function () {
var mockTimeout,
mockScope,
testElement,
testAttrs,
mctResize;
beforeEach(function () {
mockTimeout = jasmine.createSpy("$timeout");
mockScope = jasmine.createSpyObj("$scope", ["$eval"]);
testElement = { offsetWidth: 100, offsetHeight: 200 };
testAttrs = { mctResize: "some-expr" };
mctResize = new MCTResize(mockTimeout);
});
it("is applicable as an attribute only", function () {
expect(mctResize.restrict).toEqual("A");
});
it("starts tracking size changes upon link", function () {
expect(mockTimeout).not.toHaveBeenCalled();
mctResize.link(mockScope, [testElement], testAttrs);
expect(mockTimeout).toHaveBeenCalledWith(
jasmine.any(Function),
jasmine.any(Number)
);
expect(mockScope.$eval).toHaveBeenCalledWith(
testAttrs.mctResize,
{ bounds: { width: 100, height: 200 } }
);
});
it("reports size changes on a timeout", function () {
mctResize.link(mockScope, [testElement], testAttrs);
// Change the element's apparent size
testElement.offsetWidth = 300;
testElement.offsetHeight = 350;
// Shouldn't know about this yet...
expect(mockScope.$eval).not.toHaveBeenCalledWith(
testAttrs.mctResize,
{ bounds: { width: 300, height: 350 } }
);
// Fire the timeout
mockTimeout.mostRecentCall.args[0]();
// Should have triggered an evaluation of mctResize
// with the new width & height
expect(mockScope.$eval).toHaveBeenCalledWith(
testAttrs.mctResize,
{ bounds: { width: 300, height: 350 } }
);
});
});
}
);

View File

@ -1,11 +1,13 @@
[
"ActionGroupController",
"BottomBarController",
"ClickAwayController",
"ContextMenuController",
"MCTContainer",
"MCTDrag",
"ToggleController",
"TreeNodeController",
"ViewSwitcherController"
"controllers/ActionGroupController",
"controllers/BottomBarController",
"controllers/ClickAwayController",
"controllers/ContextMenuController",
"controllers/GetterSetterController",
"controllers/ToggleController",
"controllers/TreeNodeController",
"controllers/ViewSwitcherController",
"directives/MCTContainer",
"directives/MCTDrag",
"directives/MCTResize"
]

View File

@ -28,6 +28,11 @@
{
"key": "telemetryFormatter",
"implementation": "TelemetryFormatter.js"
},
{
"key": "telemetrySubscriber",
"implementation": "TelemetrySubscriber.js",
"depends": [ "$q", "$timeout" ]
}
]
}

View File

@ -38,6 +38,23 @@ define(
})).then(mergeResults);
}
// Subscribe to updates from all providers
function subscribe(callback, requests) {
var unsubscribes = telemetryProviders.map(function (provider) {
return provider.subscribe(callback, requests);
});
// Return an unsubscribe function that invokes unsubscribe
// for all providers.
return function () {
unsubscribes.forEach(function (unsubscribe) {
if (unsubscribe) {
unsubscribe();
}
});
};
}
return {
/**
* Request telemetry data.
@ -47,7 +64,23 @@ define(
* which may (or may not, depending on
* availability) satisfy the requests
*/
requestTelemetry: requestTelemetry
requestTelemetry: requestTelemetry,
/**
* Subscribe to streaming updates to telemetry data.
* The provided callback will be invoked as new
* telemetry becomes available; as an argument, it
* will receive an object of key-value pairs, where
* keys are source identifiers and values are objects
* of key-value pairs, where keys are point identifiers
* and values are TelemetrySeries objects containing
* the latest streaming telemetry.
* @param {Function} callback the callback to invoke
* @param {TelemetryRequest[]} requests an array of
* requests to be subscribed upon
* @returns {Function} a function which can be called
* to unsubscribe
*/
subscribe: subscribe
};
}

View File

@ -16,20 +16,22 @@ define(
* @constructor
*/
function TelemetryCapability($injector, $q, $log, domainObject) {
var telemetryService;
var telemetryService,
subscriptions = [],
unsubscribeFunction;
// We could depend on telemetryService directly, but
// there isn't a platform implementation of this;
function getTelemetryService() {
if (!telemetryService) {
if (telemetryService === undefined) {
try {
telemetryService =
$q.when($injector.get("telemetryService"));
$injector.get("telemetryService");
} catch (e) {
// $injector should throw is telemetryService
// $injector should throw if telemetryService
// is unavailable or unsatisfiable.
$log.warn("Telemetry service unavailable");
telemetryService = $q.reject(e);
telemetryService = null;
}
}
return telemetryService;
@ -83,16 +85,34 @@ define(
}
// Issue a request to the service
function requestTelemetryFromService(telemetryService) {
function requestTelemetryFromService() {
return telemetryService.requestTelemetry([fullRequest]);
}
// If a telemetryService is not available,
// getTelemetryService() should reject, and this should
// bubble through subsequent then calls.
return getTelemetryService()
.then(requestTelemetryFromService)
.then(getRelevantResponse);
return getTelemetryService() &&
requestTelemetryFromService()
.then(getRelevantResponse);
}
// Listen for real-time and/or streaming updates
function subscribe(callback, request) {
var fullRequest = buildRequest(request || {});
// Unpack the relevant telemetry series
function update(telemetries) {
var source = fullRequest.source,
key = fullRequest.key,
result = ((telemetries || {})[source] || {})[key];
if (result) {
callback(result);
}
}
return getTelemetryService() &&
telemetryService.subscribe(update, [fullRequest]);
}
return {
@ -115,7 +135,18 @@ define(
// type-level and object-level telemetry
// properties
return buildRequest({});
}
},
/**
* Subscribe to updates to telemetry data for this domain
* object.
* @param {Function} callback a function to call when new
* data becomes available; the telemetry series
* containing the data will be given as an argument.
* @param {TelemetryRequest} [request] parameters for the
* subscription request
*/
subscribe: subscribe
};
}

View File

@ -49,7 +49,11 @@ define(
// Used for getTelemetryObjects; a reference is
// stored so that this can be called in a watch
telemetryObjects: []
telemetryObjects: [],
// Whether or not this controller is active; once
// scope is destroyed, polling should stop.
active: true
};
// Broadcast that a telemetryUpdate has occurred.
@ -227,14 +231,25 @@ define(
}
self.refreshing = false;
startTimeout();
if (self.active) {
startTimeout();
}
}, self.interval);
}
}
// Stop polling for changes
function deactivate() {
self.active = false;
}
// Watch for a represented domain object
$scope.$watch("domainObject", getTelemetryObjects);
// Stop polling when destroyed
$scope.$on("$destroy", deactivate);
// Begin polling for data changes
startTimeout();

View File

@ -0,0 +1,54 @@
/*global define*/
define(
["./TelemetrySubscription"],
function (TelemetrySubscription) {
"use strict";
/**
* The TelemetrySubscriber is a service which allows
* subscriptions to be made for new data associated with
* domain objects. It is exposed as a service named
* `telemetrySubscriber`.
*
* Subscriptions may also be made directly using the
* `telemetry` capability of a domain objcet; the subscriber
* uses this as well, but additionally handles delegation
* (e.g. for telemetry panels) as well as latest-value
* extraction.
*
* @constructor
* @param $q Angular's $q
* @param $timeout Angular's $timeout
*/
function TelemetrySubscriber($q, $timeout) {
return {
/**
* Subscribe to streaming telemetry updates
* associated with this domain object (either
* directly or via capability delegation.)
*
* @param {DomainObject} domainObject the object whose
* associated telemetry data is of interest
* @param {Function} callback a function to invoke
* when new data has become available.
* @returns {TelemetrySubscription} the subscription,
* which will provide access to latest values.
*
* @method
* @memberof TelemetrySubscriber
*/
subscribe: function (domainObject, callback) {
return new TelemetrySubscription(
$q,
$timeout,
domainObject,
callback
);
}
};
}
return TelemetrySubscriber;
}
);

View File

@ -0,0 +1,199 @@
/*global define*/
define(
[],
function () {
"use strict";
/**
* A TelemetrySubscription tracks latest values for streaming
* telemetry data and handles notifying interested observers.
* It implements the interesting behavior behind the
* `telemetrySubscriber` service.
*
* Subscriptions may also be made directly using the
* `telemetry` capability of a domain objcet; the subscriber
* uses this as well, but additionally handles delegation
* (e.g. for telemetry panels) as well as latest-value
* extraction.
*
* @constructor
* @param $q Angular's $q
* @param $timeout Angular's $timeout
* @param {DomainObject} domainObject the object whose
* associated telemetry data is of interest
* @param {Function} callback a function to invoke
* when new data has become available.
*/
function TelemetrySubscription($q, $timeout, domainObject, callback) {
var unsubscribePromise,
latestValues = {},
telemetryObjects = [],
updatePending;
// Look up domain objects which have telemetry capabilities.
// This will either be the object in view, or object that
// this object delegates its telemetry capability to.
function promiseRelevantObjects(domainObject) {
// If object has been cleared, there are no relevant
// telemetry-providing domain objects.
if (!domainObject) {
return $q.when([]);
}
// Otherwise, try delegation first, and attach the
// object itself if it has a telemetry capability.
return $q.when(domainObject.useCapability(
"delegation",
"telemetry"
)).then(function (result) {
var head = domainObject.hasCapability("telemetry") ?
[ domainObject ] : [],
tail = result || [];
return head.concat(tail);
});
}
// Invoke the observer callback to notify that new streaming
// data has become available.
function fireCallback() {
callback();
// Clear the pending flag so that future updates will
// schedule this callback.
updatePending = false;
}
// Update the latest telemetry data for a specific
// domain object. This will notify listeners.
function update(domainObject, telemetry) {
var count = telemetry && telemetry.getPointCount();
// Only schedule notification if there isn't already
// a notification pending (and if we actually have
// data)
if (!updatePending && count) {
updatePending = true;
$timeout(fireCallback, 0);
}
// Update the latest-value table
if (count > 0) {
latestValues[domainObject.getId()] = {
domain: telemetry.getDomainValue(count - 1),
range: telemetry.getRangeValue(count - 1)
};
}
}
// Prepare a subscription to a specific telemetry-providing
// domain object.
function subscribe(domainObject) {
var telemetryCapability =
domainObject.getCapability("telemetry");
return telemetryCapability.subscribe(function (telemetry) {
update(domainObject, telemetry);
});
}
// Prepare subscriptions to all relevant telemetry-providing
// domain objects.
function subscribeAll(domainObjects) {
return domainObjects.map(subscribe);
}
// Cache a reference to all relevant telemetry-providing
// domain objects. This will be called during the
// initial subscription chain; this allows `getTelemetryObjects()`
// to return a non-Promise to simplify usage elsewhere.
function cacheObjectReferences(objects) {
telemetryObjects = objects;
return objects;
}
// Get a reference to relevant objects (those with telemetry
// capabilities) and subscribe to their telemetry updates.
// Keep a reference to their promised return values, as these
// will be unsubscribe functions. (This must be a promise
// because delegation is supported, and retrieving delegate
// telemetry-capable objects may be an asynchronous operation.)
unsubscribePromise =
promiseRelevantObjects(domainObject)
.then(cacheObjectReferences)
.then(subscribeAll);
return {
/**
* Terminate all underlying subscriptions associated
* with this object.
* @method
* @memberof TelemetrySubscription
*/
unsubscribe: function () {
return unsubscribePromise.then(function (unsubscribes) {
return $q.all(unsubscribes.map(function (unsubscribe) {
return unsubscribe();
}));
});
},
/**
* Get the most recent domain value that has been observed
* for the specified domain object. This will typically be
* a timestamp.
*
* The domain object passed here should be one that is
* subscribed-to here; that is, it should be one of the
* domain objects returned by `getTelemetryObjects()`.
*
* @param {DomainObject} domainObject the object of interest
* @returns the most recent domain value observed
* @method
* @memberof TelemetrySubscription
*/
getDomainValue: function (domainObject) {
var id = domainObject.getId();
return (latestValues[id] || {}).domain;
},
/**
* Get the most recent range value that has been observed
* for the specified domain object. This will typically
* be a numeric measurement.
*
* The domain object passed here should be one that is
* subscribed-to here; that is, it should be one of the
* domain objects returned by `getTelemetryObjects()`.
*
* @param {DomainObject} domainObject the object of interest
* @returns the most recent range value observed
* @method
* @memberof TelemetrySubscription
*/
getRangeValue: function (domainObject) {
var id = domainObject.getId();
return (latestValues[id] || {}).range;
},
/**
* Get all telemetry-providing domain objects which are
* being observed as part of this subscription.
*
* Capability delegation will be taken into account (so, if
* a Telemetry Panel was passed in the constructor, this will
* return its contents.) Capability delegation is resolved
* asynchronously so the return value here may change over
* time; while this resolution is pending, this method will
* return an empty array.
*
* @returns {DomainObject[]} all subscribed-to domain objects
* @method
* @memberof TelemetrySubscription
*/
getTelemetryObjects: function () {
return telemetryObjects;
}
};
}
return TelemetrySubscription;
}
);

View File

@ -8,6 +8,7 @@ define(
describe("The telemetry aggregator", function () {
var mockQ,
mockProviders,
mockUnsubscribes,
aggregator;
function mockPromise(value) {
@ -20,10 +21,15 @@ define(
function mockProvider(key, index) {
var provider = jasmine.createSpyObj(
"provider" + index,
[ "requestTelemetry" ]
);
"provider" + index,
[ "requestTelemetry", "subscribe" ]
),
unsubscribe = jasmine.createSpy("unsubscribe" + index);
provider.requestTelemetry.andReturn({ someKey: key });
provider.subscribe.andReturn(unsubscribe);
// Store to verify interactions later
mockUnsubscribes[index] = unsubscribe;
return provider;
}
@ -31,6 +37,7 @@ define(
mockQ = jasmine.createSpyObj("$q", [ "all" ]);
mockQ.all.andReturn(mockPromise([]));
mockUnsubscribes = [];
mockProviders = [ "a", "b", "c" ].map(mockProvider);
aggregator = new TelemetryAggregator(mockQ, mockProviders);
@ -74,6 +81,24 @@ define(
});
});
it("broadcasts subscriptions from all providers", function () {
var mockCallback = jasmine.createSpy("callback"),
subscription = aggregator.subscribe(mockCallback);
// Make sure all providers got subscribed to
mockProviders.forEach(function (mockProvider) {
expect(mockProvider.subscribe).toHaveBeenCalled();
});
// Verify that unsubscription gets broadcast too
mockUnsubscribes.forEach(function (mockUnsubscribe) {
expect(mockUnsubscribe).not.toHaveBeenCalled();
});
subscription(); // unsubscribe
mockUnsubscribes.forEach(function (mockUnsubscribe) {
expect(mockUnsubscribe).toHaveBeenCalled();
});
});
});
}

View File

@ -12,6 +12,7 @@ define(
mockDomainObject,
mockTelemetryService,
mockReject,
mockUnsubscribe,
telemetry;
@ -33,9 +34,10 @@ define(
);
mockTelemetryService = jasmine.createSpyObj(
"telemetryService",
[ "requestTelemetry" ]
[ "requestTelemetry", "subscribe" ]
);
mockReject = jasmine.createSpyObj("reject", ["then"]);
mockUnsubscribe = jasmine.createSpy("unsubscribe");
mockInjector.get.andReturn(mockTelemetryService);
@ -50,6 +52,11 @@ define(
}
});
mockTelemetryService.requestTelemetry
.andReturn(mockPromise({}));
mockTelemetryService.subscribe
.andReturn(mockUnsubscribe);
// Bubble up...
mockReject.then.andReturn(mockReject);
@ -124,6 +131,36 @@ define(
expect(mockLog.warn).toHaveBeenCalled();
});
it("allows subscriptions to updates", function () {
var mockCallback = jasmine.createSpy("callback"),
subscription = telemetry.subscribe(mockCallback);
// Verify subscription to the appropriate object
expect(mockTelemetryService.subscribe).toHaveBeenCalledWith(
jasmine.any(Function),
[{
id: "testId", // from domain object
source: "testSource",
key: "testKey"
}]
);
// Check that the callback gets invoked
expect(mockCallback).not.toHaveBeenCalled();
mockTelemetryService.subscribe.mostRecentCall.args[0]({
testSource: { testKey: { someKey: "some value" } }
});
expect(mockCallback).toHaveBeenCalledWith(
{ someKey: "some value" }
);
// Finally, unsubscribe
expect(mockUnsubscribe).not.toHaveBeenCalled();
subscription(); // should be an unsubscribe function
expect(mockUnsubscribe).toHaveBeenCalled();
});
});
}
);

View File

@ -187,6 +187,13 @@ define(
.toHaveBeenCalledWith("telemetryUpdate");
});
it("listens for scope destruction to clean up", function () {
expect(mockScope.$on).toHaveBeenCalledWith(
"$destroy",
jasmine.any(Function)
);
mockScope.$on.mostRecentCall.args[1]();
});
});
}

View File

@ -1,8 +1,5 @@
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
/**
* MergeModelsSpec. Created by vwoeltje on 11/6/14.
*/
define(
["../src/TelemetryFormatter"],
function (TelemetryFormatter) {

View File

@ -0,0 +1,54 @@
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
define(
["../src/TelemetrySubscriber"],
function (TelemetrySubscriber) {
"use strict";
describe("The telemetry subscriber", function () {
// TelemetrySubscriber just provides a factory
// for TelemetrySubscription, so most real testing
// should happen there.
var mockQ,
mockTimeout,
mockDomainObject,
mockCallback,
mockPromise,
subscriber;
beforeEach(function () {
mockQ = jasmine.createSpyObj("$q", ["when"]);
mockTimeout = jasmine.createSpy("$timeout");
mockDomainObject = jasmine.createSpyObj(
"domainObject",
[ "getCapability", "useCapability", "hasCapability" ]
);
mockCallback = jasmine.createSpy("callback");
mockPromise = jasmine.createSpyObj("promise", ["then"]);
mockQ.when.andReturn(mockPromise);
mockPromise.then.andReturn(mockPromise);
subscriber = new TelemetrySubscriber(mockQ, mockTimeout);
});
it("acts as a factory for subscription objects", function () {
var subscription = subscriber.subscribe(
mockDomainObject,
mockCallback
);
// Just verify that this looks like a TelemetrySubscription
[
"unsubscribe",
"getTelemetryObjects",
"getRangeValue",
"getDomainValue"
].forEach(function (method) {
expect(subscription[method])
.toEqual(jasmine.any(Function));
});
});
});
}
);

View File

@ -0,0 +1,125 @@
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
define(
["../src/TelemetrySubscription"],
function (TelemetrySubscription) {
"use strict";
describe("A telemetry subscription", function () {
var mockQ,
mockTimeout,
mockDomainObject,
mockCallback,
mockTelemetry,
mockUnsubscribe,
mockSeries,
subscription;
function mockPromise(value) {
return (value && value.then) ? value : {
then: function (callback) {
return mockPromise(callback(value));
}
};
}
beforeEach(function () {
mockQ = jasmine.createSpyObj("$q", ["when", "all"]);
mockTimeout = jasmine.createSpy("$timeout");
mockDomainObject = jasmine.createSpyObj(
"domainObject",
[ "getCapability", "useCapability", "hasCapability", "getId" ]
);
mockCallback = jasmine.createSpy("callback");
mockTelemetry = jasmine.createSpyObj(
"telemetry",
["subscribe"]
);
mockUnsubscribe = jasmine.createSpy("unsubscribe");
mockSeries = jasmine.createSpyObj(
"series",
[ "getPointCount", "getDomainValue", "getRangeValue" ]
);
mockQ.when.andCallFake(mockPromise);
mockDomainObject.hasCapability.andReturn(true);
mockDomainObject.getCapability.andReturn(mockTelemetry);
mockDomainObject.getId.andReturn('test-id');
mockTelemetry.subscribe.andReturn(mockUnsubscribe);
mockSeries.getPointCount.andReturn(42);
mockSeries.getDomainValue.andReturn(123456);
mockSeries.getRangeValue.andReturn(789);
subscription = new TelemetrySubscription(
mockQ,
mockTimeout,
mockDomainObject,
mockCallback
);
});
it("subscribes to the provided object", function () {
expect(mockTelemetry.subscribe).toHaveBeenCalled();
});
it("unsubscribes on request", function () {
expect(mockUnsubscribe).not.toHaveBeenCalled();
subscription.unsubscribe();
expect(mockUnsubscribe).toHaveBeenCalled();
});
it("fires callbacks when subscriptions update", function () {
expect(mockCallback).not.toHaveBeenCalled();
mockTelemetry.subscribe.mostRecentCall.args[0](mockSeries);
// This gets fired via a timeout, so trigger that
expect(mockTimeout).toHaveBeenCalledWith(
jasmine.any(Function),
0
);
mockTimeout.mostRecentCall.args[0]();
// Should have triggered the callback to alert that
// new data was available
expect(mockCallback).toHaveBeenCalled();
});
it("fires subscription callbacks once per cycle", function () {
var i;
for (i = 0; i < 100; i += 1) {
mockTelemetry.subscribe.mostRecentCall.args[0](mockSeries);
}
// This gets fired via a timeout, so trigger any of those
mockTimeout.calls.forEach(function (call) {
call.args[0]();
});
// Should have only triggered the
expect(mockCallback.calls.length).toEqual(1);
});
it("reports its latest observed data values", function () {
mockTelemetry.subscribe.mostRecentCall.args[0](mockSeries);
// This gets fired via a timeout, so trigger that
mockTimeout.mostRecentCall.args[0]();
// Verify that the last sample was looked at
expect(mockSeries.getDomainValue).toHaveBeenCalledWith(41);
expect(mockSeries.getRangeValue).toHaveBeenCalledWith(41);
// Domain and range values should now be available
expect(subscription.getDomainValue(mockDomainObject))
.toEqual(123456);
expect(subscription.getRangeValue(mockDomainObject))
.toEqual(789);
});
it("provides no objects if no domain object is provided", function () {
// omit last arguments
subscription = new TelemetrySubscription(mockQ, mockTimeout);
// Should have no objects
expect(subscription.getTelemetryObjects()).toEqual([]);
});
});
}
);

View File

@ -2,5 +2,7 @@
"TelemetryAggregator",
"TelemetryCapability",
"TelemetryController",
"TelemetryFormatter"
"TelemetryFormatter",
"TelemetrySubscriber",
"TelemetrySubscription"
]