mirror of
https://github.com/nasa/openmct.git
synced 2024-12-23 15:02:23 +00:00
433dee0314
* Updated Karma and Jasmine versions * Added DOMObserver class. Supports promise-based testing of DOM changes Update asynchronous test specs to use promises or done() instead of waitsFor/runs * Modified ActionCapability to duplicate context object properties as own properties for better object equality comparisons * Global find + replace to fix syntax issues * Fixed various issues caused by non-deterministic runtime order of tests in Jasmine 3. Fixed issues caused by changes to determination of object equality * Addressed review comments * Resolved merge conflicts with master * Fixed style errors * Use spy.calls.count() instead of manually tracking
642 lines
25 KiB
JavaScript
642 lines
25 KiB
JavaScript
/*****************************************************************************
|
|
* Open MCT, Copyright (c) 2014-2018, United States Government
|
|
* as represented by the Administrator of the National Aeronautics and Space
|
|
* Administration. All rights reserved.
|
|
*
|
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
|
* "License"); you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
* License for the specific language governing permissions and limitations
|
|
* under the License.
|
|
*
|
|
* Open MCT includes source code licensed under additional open source
|
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
|
* this source code distribution or the Licensing information page available
|
|
* at runtime from the About dialog for additional information.
|
|
*****************************************************************************/
|
|
|
|
define(
|
|
[
|
|
"../src/FixedController",
|
|
"zepto"
|
|
],
|
|
function (
|
|
FixedController,
|
|
$
|
|
) {
|
|
|
|
describe("The Fixed Position controller", function () {
|
|
var mockScope,
|
|
mockQ,
|
|
mockDialogService,
|
|
mockFormatter,
|
|
mockDomainObject,
|
|
mockEvent,
|
|
testGrid,
|
|
testModel,
|
|
testConfiguration,
|
|
mockOpenMCT,
|
|
mockTelemetryAPI,
|
|
mockCompositionAPI,
|
|
mockCompositionCollection,
|
|
mockChildren,
|
|
mockConductor,
|
|
mockMetadata,
|
|
mockTimeSystem,
|
|
mockLimitEvaluator,
|
|
mockSelection,
|
|
mockObjects,
|
|
mockNewDomainObject,
|
|
unlistenFunc,
|
|
$element = [],
|
|
selectable = [],
|
|
controller;
|
|
|
|
// Utility function; find a $on calls for a given expression.
|
|
function findOn(expr) {
|
|
var on;
|
|
mockScope.$on.calls.all().forEach(function (call) {
|
|
if (call.args[0] === expr) {
|
|
on = call.args[1];
|
|
}
|
|
});
|
|
return on;
|
|
}
|
|
|
|
function makeMockDomainObject(id) {
|
|
return {
|
|
identifier: {
|
|
key: "domainObject-" + id,
|
|
namespace: ""
|
|
},
|
|
name: "Point " + id
|
|
};
|
|
}
|
|
|
|
beforeEach(function () {
|
|
mockScope = jasmine.createSpyObj(
|
|
'$scope',
|
|
["$on", "$watch", "$digest", "commit"]
|
|
);
|
|
mockQ = jasmine.createSpyObj('$q', ['when']);
|
|
mockDialogService = jasmine.createSpyObj(
|
|
'dialogService',
|
|
['getUserInput']
|
|
);
|
|
mockFormatter = jasmine.createSpyObj(
|
|
'telemetryFormatter',
|
|
['format']
|
|
);
|
|
mockFormatter.format.and.callFake(function (valueMetadata) {
|
|
return "Formatted " + valueMetadata.value;
|
|
});
|
|
|
|
mockConductor = jasmine.createSpyObj('conductor', [
|
|
'on',
|
|
'off',
|
|
'bounds',
|
|
'timeSystem',
|
|
'clock'
|
|
]);
|
|
mockConductor.bounds.and.returnValue({});
|
|
mockTimeSystem = {
|
|
metadata: {
|
|
key: 'key'
|
|
}
|
|
};
|
|
mockConductor.timeSystem.and.returnValue(mockTimeSystem);
|
|
|
|
mockEvent = jasmine.createSpyObj(
|
|
'event',
|
|
['preventDefault']
|
|
);
|
|
|
|
mockTelemetryAPI = jasmine.createSpyObj('telemetry',
|
|
[
|
|
'subscribe',
|
|
'request',
|
|
'isTelemetryObject',
|
|
'getMetadata',
|
|
'limitEvaluator',
|
|
'getValueFormatter'
|
|
]
|
|
);
|
|
mockTelemetryAPI.isTelemetryObject.and.returnValue(true);
|
|
mockTelemetryAPI.request.and.returnValue(Promise.resolve([]));
|
|
|
|
testGrid = [123, 456];
|
|
testModel = {
|
|
composition: ['a', 'b', 'c'],
|
|
layoutGrid: testGrid
|
|
};
|
|
testConfiguration = { elements: [
|
|
{ type: "fixed.telemetry", id: 'a', x: 1, y: 1, useGrid: true},
|
|
{ type: "fixed.telemetry", id: 'b', x: 1, y: 1, useGrid: true},
|
|
{ type: "fixed.telemetry", id: 'c', x: 1, y: 1, useGrid: true}
|
|
]};
|
|
|
|
mockChildren = testModel.composition.map(makeMockDomainObject);
|
|
mockCompositionCollection = jasmine.createSpyObj('compositionCollection', [
|
|
'load',
|
|
'on',
|
|
'off'
|
|
]);
|
|
mockCompositionAPI = jasmine.createSpyObj('composition', [
|
|
'get'
|
|
]);
|
|
mockCompositionAPI.get.and.returnValue(mockCompositionCollection);
|
|
mockCompositionCollection.load.and.returnValue(
|
|
Promise.resolve(mockChildren)
|
|
);
|
|
|
|
mockScope.model = testModel;
|
|
mockScope.configuration = testConfiguration;
|
|
|
|
mockNewDomainObject = jasmine.createSpyObj("newDomainObject", [
|
|
'layoutGrid',
|
|
'configuration',
|
|
'composition'
|
|
]);
|
|
mockNewDomainObject.layoutGrid = testGrid;
|
|
mockNewDomainObject.configuration = {
|
|
'fixed-display': testConfiguration
|
|
};
|
|
mockNewDomainObject.composition = ['a', 'b', 'c'];
|
|
|
|
mockDomainObject = jasmine.createSpyObj(
|
|
'domainObject',
|
|
['getId', 'getModel', 'getCapability', 'useCapability']
|
|
);
|
|
mockDomainObject.useCapability.and.returnValue(mockNewDomainObject);
|
|
mockScope.domainObject = mockDomainObject;
|
|
|
|
selectable[0] = {
|
|
context: {
|
|
oldItem: mockDomainObject
|
|
}
|
|
};
|
|
mockSelection = jasmine.createSpyObj("selection", [
|
|
'select',
|
|
'on',
|
|
'off',
|
|
'get'
|
|
]);
|
|
mockSelection.get.and.returnValue([]);
|
|
|
|
unlistenFunc = jasmine.createSpy("unlisten");
|
|
mockObjects = jasmine.createSpyObj('objects', [
|
|
'observe',
|
|
'get'
|
|
]);
|
|
mockObjects.observe.and.returnValue(unlistenFunc);
|
|
|
|
mockOpenMCT = {
|
|
time: mockConductor,
|
|
telemetry: mockTelemetryAPI,
|
|
composition: mockCompositionAPI,
|
|
selection: mockSelection,
|
|
objects: mockObjects
|
|
};
|
|
|
|
$element = $('<div></div>');
|
|
spyOn($element[0], 'click');
|
|
|
|
mockMetadata = jasmine.createSpyObj('mockMetadata', [
|
|
'valuesForHints',
|
|
'value',
|
|
'values'
|
|
]);
|
|
mockMetadata.value.and.returnValue({
|
|
key: 'value'
|
|
});
|
|
|
|
mockMetadata.valuesForHints.and.callFake(function (hints) {
|
|
if (hints === ['domain']) {
|
|
return [{
|
|
key: 'time'
|
|
}];
|
|
} else {
|
|
return [{
|
|
key: 'value'
|
|
}];
|
|
}
|
|
});
|
|
|
|
mockLimitEvaluator = jasmine.createSpyObj('limitEvaluator', [
|
|
'evaluate'
|
|
]);
|
|
|
|
mockLimitEvaluator.evaluate.and.returnValue({});
|
|
|
|
mockTelemetryAPI.getMetadata.and.returnValue(mockMetadata);
|
|
mockTelemetryAPI.limitEvaluator.and.returnValue(mockLimitEvaluator);
|
|
mockTelemetryAPI.getValueFormatter.and.returnValue(mockFormatter);
|
|
|
|
controller = new FixedController(
|
|
mockScope,
|
|
mockQ,
|
|
mockDialogService,
|
|
mockOpenMCT,
|
|
$element
|
|
);
|
|
spyOn(controller, "mutate");
|
|
});
|
|
|
|
it("subscribes a domain object", function () {
|
|
var object = makeMockDomainObject("mock");
|
|
|
|
return controller.getTelemetry(object).then(function () {
|
|
expect(mockTelemetryAPI.subscribe).toHaveBeenCalledWith(
|
|
object,
|
|
jasmine.any(Function),
|
|
jasmine.any(Object)
|
|
);
|
|
});
|
|
});
|
|
|
|
it("releases subscription when a domain objects is removed", function () {
|
|
var unsubscribe = jasmine.createSpy('unsubscribe');
|
|
var unsubscribePromise = new Promise(function (resolve) {
|
|
unsubscribe.and.callFake(resolve);
|
|
});
|
|
var object = makeMockDomainObject("mock");
|
|
|
|
mockTelemetryAPI.subscribe.and.returnValue(unsubscribe);
|
|
return controller.getTelemetry(object).then(function () {
|
|
controller.onCompositionRemove(object.identifier);
|
|
return unsubscribePromise;
|
|
}).then(function () {
|
|
expect(unsubscribe).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
it("exposes visible elements based on configuration", function () {
|
|
var elements = controller.getElements();
|
|
|
|
expect(elements.length).toEqual(3);
|
|
expect(elements[0].id).toEqual('a');
|
|
expect(elements[1].id).toEqual('b');
|
|
expect(elements[2].id).toEqual('c');
|
|
});
|
|
|
|
it("allows elements to be selected", function () {
|
|
selectable[0].context.elementProxy = controller.getElements()[1];
|
|
mockOpenMCT.selection.on.calls.mostRecent().args[1](selectable);
|
|
|
|
expect(controller.isElementSelected()).toBe(true);
|
|
});
|
|
|
|
it("allows selection retrieval", function () {
|
|
var elements = controller.getElements();
|
|
selectable[0].context.elementProxy = elements[1];
|
|
mockOpenMCT.selection.on.calls.mostRecent().args[1](selectable);
|
|
|
|
expect(controller.getSelectedElement()).toEqual(elements[1]);
|
|
});
|
|
|
|
it("selects the parent view when selected element is removed", function () {
|
|
var elements = controller.getElements();
|
|
selectable[0].context.elementProxy = elements[1];
|
|
mockOpenMCT.selection.on.calls.mostRecent().args[1](selectable);
|
|
controller.remove(elements[1]);
|
|
|
|
expect($element[0].click).toHaveBeenCalled();
|
|
});
|
|
|
|
it("retains selections during refresh", function () {
|
|
// Get elements; remove one of them; trigger refresh.
|
|
// Same element (at least by index) should still be selected.
|
|
var elements = controller.getElements();
|
|
selectable[0].context.elementProxy = elements[1];
|
|
mockOpenMCT.selection.on.calls.mostRecent().args[1](selectable);
|
|
|
|
expect(controller.getSelectedElement()).toEqual(elements[1]);
|
|
|
|
controller.remove(elements[2]);
|
|
elements = controller.getElements();
|
|
|
|
// Verify removal, as test assumes this
|
|
expect(elements.length).toEqual(2);
|
|
|
|
expect(controller.shouldSelect(elements[1])).toBe(true);
|
|
});
|
|
|
|
it("Displays received values for telemetry elements", function () {
|
|
var elements;
|
|
var mockTelemetry = {
|
|
time: 100,
|
|
value: 200
|
|
};
|
|
var testElement = {};
|
|
var telemetryObject = {
|
|
identifier: {
|
|
key: '12345'
|
|
}
|
|
};
|
|
mockConductor.clock.and.returnValue({});
|
|
controller.elementProxiesById = {};
|
|
controller.elementProxiesById['12345'] = [testElement];
|
|
controller.elementProxies = [testElement];
|
|
|
|
controller.subscribeToObject(telemetryObject);
|
|
mockTelemetryAPI.subscribe.calls.mostRecent().args[1](mockTelemetry);
|
|
|
|
return new Promise(function (resolve) {
|
|
mockScope.$digest.and.callFake(resolve);
|
|
}).then(function () {
|
|
// Get elements that controller is now exposing
|
|
elements = controller.getElements();
|
|
|
|
// Formatted values should be available
|
|
expect(elements[0].value).toEqual("Formatted 200");
|
|
});
|
|
});
|
|
|
|
it("updates elements styles when grid size changes", function () {
|
|
// Grid size is initially set to testGrid which is [123, 456]
|
|
var originalLeft = controller.getElements()[0].style.left;
|
|
|
|
// Change the grid size
|
|
controller.updateElementPositions([20, 20]);
|
|
|
|
expect(controller.getElements()[0].style.left).not.toEqual(originalLeft);
|
|
});
|
|
|
|
it("listens for drop events", function () {
|
|
mockScope.domainObject = mockDomainObject;
|
|
mockScope.model = testModel;
|
|
|
|
// Layout should position panels according to
|
|
// where the user dropped them, so it needs to
|
|
// listen for drop events.
|
|
expect(mockScope.$on).toHaveBeenCalledWith(
|
|
'mctDrop',
|
|
jasmine.any(Function)
|
|
);
|
|
|
|
// Verify precondition
|
|
expect(testConfiguration.elements.length).toEqual(3);
|
|
|
|
// Notify that a drop occurred
|
|
testModel.composition.push('d');
|
|
|
|
mockObjects.get.and.returnValue(Promise.resolve([]));
|
|
|
|
findOn('mctDrop')(
|
|
mockEvent,
|
|
'd',
|
|
{ x: 300, y: 100 }
|
|
);
|
|
|
|
// Should have added an element
|
|
expect(testConfiguration.elements.length).toEqual(4);
|
|
|
|
// ...and prevented default...
|
|
expect(mockEvent.preventDefault).toHaveBeenCalled();
|
|
});
|
|
|
|
it("ignores drops when default has been prevented", function () {
|
|
// Avoids redundant drop-handling, WTD-1233
|
|
mockEvent.defaultPrevented = true;
|
|
|
|
// Notify that a drop occurred
|
|
testModel.composition.push('d');
|
|
findOn('mctDrop')(
|
|
mockEvent,
|
|
'd',
|
|
{ x: 300, y: 100 }
|
|
);
|
|
|
|
// Should NOT have added an element
|
|
expect(testConfiguration.elements.length).toEqual(3);
|
|
});
|
|
|
|
it("unsubscribes when destroyed", function () {
|
|
var unsubscribe = jasmine.createSpy('unsubscribe');
|
|
var object = makeMockDomainObject("mock");
|
|
|
|
mockTelemetryAPI.subscribe.and.returnValue(unsubscribe);
|
|
|
|
return controller.getTelemetry(object).then(function () {
|
|
expect(unsubscribe).not.toHaveBeenCalled();
|
|
// Destroy the scope
|
|
findOn('$destroy')();
|
|
|
|
expect(unsubscribe).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
it("exposes its grid size", function () {
|
|
expect(controller.getGridSize()).toEqual(testGrid);
|
|
});
|
|
|
|
it("exposes drag handles", function () {
|
|
var handles;
|
|
selectable[0].context.elementProxy = controller.getElements()[1];
|
|
mockOpenMCT.selection.on.calls.mostRecent().args[1](selectable);
|
|
|
|
// Should have a non-empty array of handles
|
|
handles = controller.handles();
|
|
|
|
expect(handles).toEqual(jasmine.any(Array));
|
|
expect(handles.length).not.toEqual(0);
|
|
|
|
// And they should have start/continue/end drag methods
|
|
handles.forEach(function (handle) {
|
|
expect(handle.startDrag).toEqual(jasmine.any(Function));
|
|
expect(handle.continueDrag).toEqual(jasmine.any(Function));
|
|
expect(handle.endDrag).toEqual(jasmine.any(Function));
|
|
});
|
|
});
|
|
|
|
it("exposes a move handle", function () {
|
|
selectable[0].context.elementProxy = controller.getElements()[1];
|
|
mockOpenMCT.selection.on.calls.mostRecent().args[1](selectable);
|
|
|
|
// Should have a move handle
|
|
var handle = controller.moveHandle();
|
|
|
|
// And it should have start/continue/end drag methods
|
|
expect(handle.startDrag).toEqual(jasmine.any(Function));
|
|
expect(handle.continueDrag).toEqual(jasmine.any(Function));
|
|
expect(handle.endDrag).toEqual(jasmine.any(Function));
|
|
});
|
|
|
|
it("updates selection style during drag", function () {
|
|
var oldStyle;
|
|
selectable[0].context.elementProxy = controller.getElements()[1];
|
|
mockOpenMCT.selection.on.calls.mostRecent().args[1](selectable);
|
|
|
|
// Get style
|
|
oldStyle = controller.getSelectedElementStyle();
|
|
|
|
// Start a drag gesture
|
|
controller.moveHandle().startDrag();
|
|
|
|
// Haven't moved yet; style shouldn't have updated yet
|
|
expect(controller.getSelectedElementStyle()).toEqual(oldStyle);
|
|
|
|
// Drag a little
|
|
controller.moveHandle().continueDrag([1000, 100]);
|
|
|
|
// Style should have been updated
|
|
expect(controller.getSelectedElementStyle()).not.toEqual(oldStyle);
|
|
});
|
|
|
|
it("cleans up selection on scope destroy", function () {
|
|
expect(mockScope.$on).toHaveBeenCalledWith(
|
|
'$destroy',
|
|
jasmine.any(Function)
|
|
);
|
|
|
|
mockScope.$on.calls.mostRecent().args[1]();
|
|
|
|
expect(mockOpenMCT.selection.off).toHaveBeenCalledWith(
|
|
'change',
|
|
jasmine.any(Function)
|
|
);
|
|
});
|
|
|
|
describe("on display bounds changes", function () {
|
|
var testBounds;
|
|
var boundsChangeCallback;
|
|
var objectOne;
|
|
var objectTwo;
|
|
|
|
beforeEach(function () {
|
|
testBounds = { start: 123, end: 321 };
|
|
boundsChangeCallback = mockConductor.on.calls.mostRecent().args[1];
|
|
objectOne = {};
|
|
objectTwo = {};
|
|
controller.telemetryObjects = [
|
|
objectOne,
|
|
objectTwo
|
|
];
|
|
spyOn(controller, "fetchHistoricalData");
|
|
controller.fetchHistoricalData.and.callThrough();
|
|
});
|
|
|
|
it("registers a bounds change listener", function () {
|
|
expect(mockConductor.on).toHaveBeenCalledWith("bounds", jasmine.any(Function));
|
|
});
|
|
|
|
it("requests only a single point", function () {
|
|
mockConductor.clock.and.returnValue(undefined);
|
|
boundsChangeCallback(testBounds);
|
|
expect(mockTelemetryAPI.request.calls.count()).toBe(2);
|
|
|
|
mockTelemetryAPI.request.calls.all().forEach(function (call) {
|
|
expect(call.args[1].size).toBe(1);
|
|
});
|
|
});
|
|
|
|
it("Does not fetch historical data on tick", function () {
|
|
boundsChangeCallback(testBounds, true);
|
|
expect(mockTelemetryAPI.request.calls.count()).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe("on receipt of telemetry", function () {
|
|
var mockTelemetryObject;
|
|
var testValue;
|
|
var testElement;
|
|
|
|
beforeEach(function () {
|
|
mockTelemetryObject = {
|
|
identifier: {
|
|
key: '12345'
|
|
}
|
|
};
|
|
testValue = 30;
|
|
testElement = {};
|
|
|
|
controller.elementProxiesById = {};
|
|
controller.elementProxiesById['12345'] = [testElement];
|
|
controller.elementProxies = [testElement];
|
|
});
|
|
|
|
it("updates displayed values from historical telemetry", function () {
|
|
spyOn(controller, "updateView");
|
|
controller.updateView.and.callThrough();
|
|
|
|
mockTelemetryAPI.request.and.returnValue(Promise.resolve([{
|
|
time: 100,
|
|
value: testValue
|
|
}]));
|
|
|
|
controller.fetchHistoricalData(mockTelemetryObject);
|
|
|
|
return new Promise(function (resolve) {
|
|
mockScope.$digest.and.callFake(resolve);
|
|
}).then(function () {
|
|
expect(controller.updateView).toHaveBeenCalled();
|
|
expect(controller.getElements()[0].value)
|
|
.toEqual("Formatted " + testValue);
|
|
});
|
|
});
|
|
|
|
it("selects an range value to display, if available", function () {
|
|
mockMetadata.valuesForHints.and.returnValue([
|
|
{
|
|
key: 'range',
|
|
source: 'range'
|
|
}
|
|
]);
|
|
var key = controller.chooseValueMetadataToDisplay(mockMetadata).source;
|
|
expect(key).toEqual('range');
|
|
});
|
|
|
|
it("selects the first non-domain value to display, if no range available", function () {
|
|
mockMetadata.valuesForHints.and.returnValue([]);
|
|
mockMetadata.values.and.returnValue([
|
|
{
|
|
key: 'domain',
|
|
source: 'domain',
|
|
hints: {
|
|
domain: 1
|
|
}
|
|
},
|
|
{
|
|
key: 'image',
|
|
source: 'image',
|
|
hints: {
|
|
image: 1
|
|
}
|
|
}
|
|
]);
|
|
var key = controller.chooseValueMetadataToDisplay(mockMetadata).source;
|
|
expect(key).toEqual('image');
|
|
});
|
|
|
|
it("reflects limit status", function () {
|
|
mockLimitEvaluator.evaluate.and.returnValue({cssClass: "alarm-a"});
|
|
controller.updateView(mockTelemetryObject, [{
|
|
time: 100,
|
|
value: testValue
|
|
}]);
|
|
|
|
return new Promise(function (resolve) {
|
|
mockScope.$digest.and.callFake(resolve);
|
|
}).then(function () {
|
|
// Limit-based CSS classes should be available
|
|
expect(controller.getElements()[0].cssClass).toEqual("alarm-a");
|
|
});
|
|
});
|
|
|
|
it("listens for selection change events", function () {
|
|
expect(mockOpenMCT.selection.on).toHaveBeenCalledWith(
|
|
'change',
|
|
jasmine.any(Function)
|
|
);
|
|
});
|
|
|
|
});
|
|
});
|
|
}
|
|
);
|