From c6eaa3d52895dbf9090281c7a870826f00320205 Mon Sep 17 00:00:00 2001 From: Andrew Henry Date: Wed, 24 Aug 2016 15:28:19 +0100 Subject: [PATCH] [Time Conductor] Adding tests and fixing failing ones. #933 --- .../commonUI/formats/src/UTCTimeFormat.js | 10 +- .../commonUI/formats/src/UTCTimeFormatSpec.js | 62 ++++ .../compatibility/src/ConductorRepresenter.js | 2 +- .../compatibility/src/ConductorService.js | 2 +- .../src/ConductorTelemetryDecorator.js | 2 +- .../features/conductor-v2/conductor/bundle.js | 1 + .../conductor/src/TimeConductorSpec.js | 12 +- .../src/timeSystems/LocalClockSpec.js | 50 +++ .../conductor/src/ui/MctConductorAxis.js | 164 +++++---- .../conductor/src/ui/MctConductorAxisSpec.js | 136 +++++++ .../conductor/src/ui/NumberFormatSpec.js | 49 +++ .../src/ui/TimeConductorController.js | 36 +- .../src/ui/TimeConductorControllerSpec.js | 335 ++++++++++++++++++ .../conductor/src/ui/TimeConductorMode.js | 5 +- .../conductor/src/ui/TimeConductorModeSpec.js | 210 +++++++++++ .../src/ui/TimeConductorValidation.js | 2 +- .../src/ui/TimeConductorValidationSpec.js | 73 ++++ .../src/ui/TimeConductorViewService.js | 37 +- .../src/ui/TimeConductorViewServiceSpec.js | 185 ++++++++++ .../utcTimeSystem/src/UTCTimeSystemSpec.js | 46 +++ platform/features/plot/src/PlotController.js | 6 +- .../features/plot/test/PlotControllerSpec.js | 37 +- 22 files changed, 1333 insertions(+), 129 deletions(-) create mode 100644 platform/commonUI/formats/src/UTCTimeFormatSpec.js create mode 100644 platform/features/conductor-v2/conductor/src/timeSystems/LocalClockSpec.js create mode 100644 platform/features/conductor-v2/conductor/src/ui/MctConductorAxisSpec.js create mode 100644 platform/features/conductor-v2/conductor/src/ui/NumberFormatSpec.js create mode 100644 platform/features/conductor-v2/conductor/src/ui/TimeConductorControllerSpec.js create mode 100644 platform/features/conductor-v2/conductor/src/ui/TimeConductorModeSpec.js create mode 100644 platform/features/conductor-v2/conductor/src/ui/TimeConductorValidationSpec.js create mode 100644 platform/features/conductor-v2/conductor/src/ui/TimeConductorViewServiceSpec.js create mode 100644 platform/features/conductor-v2/utcTimeSystem/src/UTCTimeSystemSpec.js diff --git a/platform/commonUI/formats/src/UTCTimeFormat.js b/platform/commonUI/formats/src/UTCTimeFormat.js index 46ba240995d..7a61338bd98 100644 --- a/platform/commonUI/formats/src/UTCTimeFormat.js +++ b/platform/commonUI/formats/src/UTCTimeFormat.js @@ -57,7 +57,7 @@ define([ * @private */ function getScaledFormat (d) { - var m = moment.utc(d); + var momentified = moment.utc(d); /** * Uses logic from d3 Time-Scales, v3 of the API. See * https://github.com/d3/d3-3.x-api-reference/blob/master/Time-Scales.md @@ -71,17 +71,17 @@ define([ ["HH", function(m) { return m.hours(); }], ["ddd DD", function(m) { return m.days() && - m.date() != 1; + m.date() !== 1; }], - ["MMM DD", function(m) { return m.date() != 1; }], + ["MMM DD", function(m) { return m.date() !== 1; }], ["MMMM", function(m) { return m.month(); }], ["YYYY", function() { return true; }] ].filter(function (row){ - return row[1](m); + return row[1](momentified); })[0][0]; - }; + } /** * diff --git a/platform/commonUI/formats/src/UTCTimeFormatSpec.js b/platform/commonUI/formats/src/UTCTimeFormatSpec.js new file mode 100644 index 00000000000..c4111709a33 --- /dev/null +++ b/platform/commonUI/formats/src/UTCTimeFormatSpec.js @@ -0,0 +1,62 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web 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 Web 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([ + "./UTCTimeFormat", + "moment" +], function ( + UTCTimeFormat, + moment +) { + describe("The UTCTimeFormat class", function () { + var format; + var scale; + + beforeEach(function () { + format = new UTCTimeFormat(); + scale = {min: 0, max: 0}; + }); + + it("Provides an appropriately scaled time format based on the input" + + " time", function () { + var TWO_HUNDRED_MS = 200; + var THREE_SECONDS = 3000; + var FIVE_MINUTES = 5 * 60 * 1000; + var ONE_HOUR_TWENTY_MINS = (1 * 60 * 60 * 1000) + (20 * 60 * 1000); + var TEN_HOURS = (10 * 60 * 60 * 1000); + + var JUNE_THIRD = moment.utc("2016-06-03", "YYYY-MM-DD"); + var APRIL = moment.utc("2016-04", "YYYY-MM"); + var TWENTY_SIXTEEN = moment.utc("2016", "YYYY"); + + expect(format.format(TWO_HUNDRED_MS, scale)).toBe(".200"); + expect(format.format(THREE_SECONDS, scale)).toBe(":03"); + expect(format.format(FIVE_MINUTES, scale)).toBe("00:05"); + expect(format.format(ONE_HOUR_TWENTY_MINS, scale)).toBe("01:20"); + expect(format.format(TEN_HOURS, scale)).toBe("10"); + + expect(format.format(JUNE_THIRD, scale)).toBe("Fri 03"); + expect(format.format(APRIL, scale)).toBe("April"); + expect(format.format(TWENTY_SIXTEEN, scale)).toBe("2016"); + }); + }); +}); diff --git a/platform/features/conductor-v2/compatibility/src/ConductorRepresenter.js b/platform/features/conductor-v2/compatibility/src/ConductorRepresenter.js index a0dcf5c66df..fe19bbc66a7 100644 --- a/platform/features/conductor-v2/compatibility/src/ConductorRepresenter.js +++ b/platform/features/conductor-v2/compatibility/src/ConductorRepresenter.js @@ -78,7 +78,7 @@ define( this.conductor.on("bounds", this.boundsListener); this.conductor.on("timeSystem", this.timeSystemListener); - this.conductor.on("follow", this.followListener) + this.conductor.on("follow", this.followListener); } }; diff --git a/platform/features/conductor-v2/compatibility/src/ConductorService.js b/platform/features/conductor-v2/compatibility/src/ConductorService.js index 681ecb73d18..df2c119793a 100644 --- a/platform/features/conductor-v2/compatibility/src/ConductorService.js +++ b/platform/features/conductor-v2/compatibility/src/ConductorService.js @@ -66,7 +66,7 @@ define([ ConductorService.prototype.getConductor = function () { return this.tc; - } + }; return ConductorService; }); diff --git a/platform/features/conductor-v2/compatibility/src/ConductorTelemetryDecorator.js b/platform/features/conductor-v2/compatibility/src/ConductorTelemetryDecorator.js index 3e84b52123f..908fc21267b 100644 --- a/platform/features/conductor-v2/compatibility/src/ConductorTelemetryDecorator.js +++ b/platform/features/conductor-v2/compatibility/src/ConductorTelemetryDecorator.js @@ -79,7 +79,7 @@ define( return function() { unsubscribeFunc(); conductor.off('bounds', amendRequests); - } + }; }; return ConductorTelemetryDecorator; diff --git a/platform/features/conductor-v2/conductor/bundle.js b/platform/features/conductor-v2/conductor/bundle.js index 4739aaea19d..f917e94183d 100644 --- a/platform/features/conductor-v2/conductor/bundle.js +++ b/platform/features/conductor-v2/conductor/bundle.js @@ -64,6 +64,7 @@ define([ "implementation": TimeConductorController, "depends": [ "$scope", + "$window", "timeConductor", "timeConductorViewService", "timeSystems[]" diff --git a/platform/features/conductor-v2/conductor/src/TimeConductorSpec.js b/platform/features/conductor-v2/conductor/src/TimeConductorSpec.js index 745b61cd226..7701f5e9b4e 100644 --- a/platform/features/conductor-v2/conductor/src/TimeConductorSpec.js +++ b/platform/features/conductor-v2/conductor/src/TimeConductorSpec.js @@ -50,21 +50,21 @@ define(['./TimeConductor'], function (TimeConductor) { it("Allows setting of valid bounds", function () { bounds = {start: 0, end: 1}; - expect(tc.bounds()).not.toBe(bounds); + expect(tc.bounds()).not.toEqual(bounds); expect(tc.bounds.bind(tc, bounds)).not.toThrow(); - expect(tc.bounds()).toBe(bounds); + expect(tc.bounds()).toEqual(bounds); }); it("Disallows setting of invalid bounds", function () { bounds = {start: 1, end: 0}; - expect(tc.bounds()).not.toBe(bounds); + expect(tc.bounds()).not.toEqual(bounds); expect(tc.bounds.bind(tc, bounds)).toThrow(); - expect(tc.bounds()).not.toBe(bounds); + expect(tc.bounds()).not.toEqual(bounds); bounds = {start: 1}; - expect(tc.bounds()).not.toBe(bounds); + expect(tc.bounds()).not.toEqual(bounds); expect(tc.bounds.bind(tc, bounds)).toThrow(); - expect(tc.bounds()).not.toBe(bounds); + expect(tc.bounds()).not.toEqual(bounds); }); it("Allows setting of time system with bounds", function () { diff --git a/platform/features/conductor-v2/conductor/src/timeSystems/LocalClockSpec.js b/platform/features/conductor-v2/conductor/src/timeSystems/LocalClockSpec.js new file mode 100644 index 00000000000..b34b625f430 --- /dev/null +++ b/platform/features/conductor-v2/conductor/src/timeSystems/LocalClockSpec.js @@ -0,0 +1,50 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web 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 Web 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(["./LocalClock"], function (LocalClock) { + describe("The LocalClock class", function () { + var clock, + mockTimeout, + timeoutHandle = {}; + + beforeEach(function () { + mockTimeout = jasmine.createSpy("timeout"); + mockTimeout.andReturn(timeoutHandle); + mockTimeout.cancel = jasmine.createSpy("cancel"); + + clock = new LocalClock(mockTimeout, 0); + clock.start(); + }); + + it("calls listeners on tick with current time", function () { + var mockListener = jasmine.createSpy("listener"); + clock.listen(mockListener); + clock.tick(); + expect(mockListener).toHaveBeenCalledWith(jasmine.any(Number)); + }); + + it("stops ticking when stop is called", function () { + clock.stop(); + expect(mockTimeout.cancel).toHaveBeenCalledWith(timeoutHandle); + }); + }); +}); diff --git a/platform/features/conductor-v2/conductor/src/ui/MctConductorAxis.js b/platform/features/conductor-v2/conductor/src/ui/MctConductorAxis.js index ca46c52344c..bb641ed3835 100644 --- a/platform/features/conductor-v2/conductor/src/ui/MctConductorAxis.js +++ b/platform/features/conductor-v2/conductor/src/ui/MctConductorAxis.js @@ -22,10 +22,10 @@ define( [ - "zepto", "d3" ], - function ($, d3) { + function (d3) { + var PADDING = 1; /** * The mct-conductor-axis renders a horizontal axis with regular @@ -33,88 +33,102 @@ define( * be specified as attributes. */ function MCTConductorAxis(conductor, formatService) { - function link(scope, element, attrs, ngModelController) { - var target = element[0].firstChild, - height = target.offsetHeight, - padding = 1; - - var vis = d3.select(target) - .append('svg:svg') - .attr('width', '100%') - .attr('height', height); - var xAxis = d3.axisTop(); - var xScale = d3.scaleUtc(); - - // draw x axis with labels and move to the bottom of the chart area - var axisElement = vis.append("g") - .attr("transform", "translate(0," + (height - padding) + ")"); - - function setScale() { - var width = target.offsetWidth; - var timeSystem = conductor.timeSystem(); - var bounds = conductor.bounds(); - - if (timeSystem.isUTCBased()) { - xScale.domain([new Date(bounds.start), new Date(bounds.end)]); - } else { - xScale.domain([bounds.start, bounds.end]); - } + // Dependencies + this.d3 = d3; + this.conductor = conductor; + this.formatService = formatService; + + // Runtime properties (set by 'link' function) + this.target = undefined; + this.xScale = undefined; + this.xAxis = undefined; + this.axisElement = undefined; + this.setScale = this.setScale.bind(this); + this.changeTimeSystem = this.changeTimeSystem.bind(this); + + // Angular Directive interface + this.link = this.link.bind(this); + this.restrict = "E"; + this.template = + "
"; + this.priority = 1000; + } + + MCTConductorAxis.prototype.setScale = function () { + var width = this.target.offsetWidth; + var timeSystem = this.conductor.timeSystem(); + var bounds = this.conductor.bounds(); + + if (timeSystem.isUTCBased()) { + this.xScale = this.xScale || this.d3.scaleUtc(); + this.xScale.domain([new Date(bounds.start), new Date(bounds.end)]); + } else { + this.xScale = this.xScale || this.d3.scaleLinear(); + this.xScale.domain([bounds.start, bounds.end]); + } - xScale.range([padding, width - padding * 2]); - axisElement.call(xAxis); + this.xScale.range([PADDING, width - PADDING * 2]); + this.axisElement.call(this.xAxis); + }; + + MCTConductorAxis.prototype.changeTimeSystem = function (timeSystem) { + var key = timeSystem.formats()[0]; + if (key !== undefined) { + var format = this.formatService.getFormat(key); + var bounds = this.conductor.bounds(); + + if (timeSystem.isUTCBased()) { + this.xScale = this.d3.scaleUtc(); + } else { + this.xScale = this.d3.scaleLinear(); } - function changeTimeSystem(timeSystem) { - var key = timeSystem.formats()[0]; - if (key !== undefined) { - var format = formatService.getFormat(key); - var b = conductor.bounds(); - - if (timeSystem.isUTCBased()) { - xScale = d3.scaleUtc(); - } else { - xScale = d3.scaleLinear(); - } - - xAxis.scale(xScale); - //Define a custom format function - xAxis.tickFormat(function (tickValue) { - // Normalize date representations to numbers - if (tickValue instanceof Date){ - tickValue = tickValue.getTime(); - } - return format.format(tickValue, { - min: b.start, - max: b.end - }); - }); - axisElement.call(xAxis); + this.xAxis.scale(this.xScale); + //Define a custom format function + this.xAxis.tickFormat(function (tickValue) { + // Normalize date representations to numbers + if (tickValue instanceof Date){ + tickValue = tickValue.getTime(); } - } + return format.format(tickValue, { + min: bounds.start, + max: bounds.end + }); + }); + this.axisElement.call(this.xAxis); + } + }; - scope.resize = setScale; + MCTConductorAxis.prototype.link = function (scope, element) { + var conductor = this.conductor; + this.target = element[0].firstChild; + var height = this.target.offsetHeight; + var vis = this.d3.select(this.target) + .append('svg:svg') + .attr('width', '100%') + .attr('height', height); - conductor.on('timeSystem', changeTimeSystem); + this.xAxis = this.d3.axisTop(); - //On conductor bounds changes, redraw ticks - conductor.on('bounds', function (bounds) { - setScale(); - }); + // draw x axis with labels and move to the bottom of the chart area + this.axisElement = vis.append("g") + .attr("transform", "translate(0," + (height - PADDING) + ")"); - if (conductor.timeSystem() !== undefined) { - changeTimeSystem(conductor.timeSystem()); - setScale(); - } - } + scope.resize = this.setScale; - return { - restrict: "E", - template: "
", - priority: 1000, - link: link - }; - } + conductor.on('timeSystem', this.changeTimeSystem); + + //On conductor bounds changes, redraw ticks + conductor.on('bounds', this.setScale); + + if (conductor.timeSystem() !== undefined) { + this.changeTimeSystem(conductor.timeSystem()); + this.setScale(); + } + }; - return MCTConductorAxis; + return function(conductor, formatService) { + return new MCTConductorAxis(conductor, formatService); + }; } ); diff --git a/platform/features/conductor-v2/conductor/src/ui/MctConductorAxisSpec.js b/platform/features/conductor-v2/conductor/src/ui/MctConductorAxisSpec.js new file mode 100644 index 00000000000..e99300083b4 --- /dev/null +++ b/platform/features/conductor-v2/conductor/src/ui/MctConductorAxisSpec.js @@ -0,0 +1,136 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web 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 Web 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(['./MctConductorAxis'], function (MctConductorAxis) { + describe("The MctConductorAxis directive", function () { + var directive, + mockConductor, + mockFormatService, + mockScope, + mockElement, + mockTarget, + mockBounds, + d3; + + beforeEach(function () { + mockScope = {}; + + //Add some HTML elements + mockTarget = { + offsetWidth: 0, + offsetHeight: 0 + }; + mockElement = { + firstChild: mockTarget + }; + mockBounds = { + start: 100, + end: 200 + }; + mockConductor = jasmine.createSpyObj("conductor", [ + "timeSystem", + "bounds", + "on" + ]); + mockConductor.bounds.andReturn(mockBounds); + + mockFormatService = jasmine.createSpyObj("formatService", [ + "getFormat" + ]); + + var d3Functions = [ + "scale", + "scaleUtc", + "scaleLinear", + "select", + "append", + "attr", + "axisTop", + "call", + "tickFormat", + "domain", + "range" + ]; + d3 = jasmine.createSpyObj("d3", d3Functions); + d3Functions.forEach(function(func) { + d3[func].andReturn(d3); + }); + + directive = new MctConductorAxis(mockConductor, mockFormatService); + directive.d3 = d3; + directive.link(mockScope, [mockElement]); + }); + + it("listens for changes to time system and bounds", function () { + expect(mockConductor.on).toHaveBeenCalledWith("timeSystem", directive.changeTimeSystem); + expect(mockConductor.on).toHaveBeenCalledWith("bounds", directive.setScale); + }); + + describe("when the time system changes", function () { + var mockTimeSystem; + var mockFormat; + + beforeEach(function() { + mockTimeSystem = jasmine.createSpyObj("timeSystem", [ + "formats", + "isUTCBased" + ]); + mockFormat = jasmine.createSpyObj("format", [ + "format" + ]); + + mockTimeSystem.formats.andReturn(["mockFormat"]); + mockFormatService.getFormat.andReturn(mockFormat); + }); + + it("uses a UTC scale for UTC time systems", function () { + mockTimeSystem.isUTCBased.andReturn(true); + directive.changeTimeSystem(mockTimeSystem); + expect(d3.scaleUtc).toHaveBeenCalled(); + expect(d3.scaleLinear).not.toHaveBeenCalled(); + }); + + it("uses a linear scale for non-UTC time systems", function () { + mockTimeSystem.isUTCBased.andReturn(false); + directive.changeTimeSystem(mockTimeSystem); + expect(d3.scaleLinear).toHaveBeenCalled(); + expect(d3.scaleUtc).not.toHaveBeenCalled(); + }); + + it("sets axis domain to time conductor bounds", function () { + mockTimeSystem.isUTCBased.andReturn(false); + mockConductor.timeSystem.andReturn(mockTimeSystem); + + directive.setScale(); + expect(d3.domain).toHaveBeenCalledWith([mockBounds.start, mockBounds.end]); + }); + + it("uses the format specified by the time system to format tick" + + " labels", function () { + directive.changeTimeSystem(mockTimeSystem); + expect(d3.tickFormat).toHaveBeenCalled(); + d3.tickFormat.mostRecentCall.args[0](); + expect(mockFormat.format).toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/platform/features/conductor-v2/conductor/src/ui/NumberFormatSpec.js b/platform/features/conductor-v2/conductor/src/ui/NumberFormatSpec.js new file mode 100644 index 00000000000..521ed506e29 --- /dev/null +++ b/platform/features/conductor-v2/conductor/src/ui/NumberFormatSpec.js @@ -0,0 +1,49 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web 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 Web 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(['./NumberFormat'], function (NumberFormat) { + describe("The NumberFormat class", function () { + var format; + beforeEach(function () { + format = new NumberFormat(); + }); + + it("The format function takes a string and produces a number", function () { + var text = format.format(1); + expect(text).toBe("1"); + expect(typeof(text)).toBe("string"); + }); + + it("The parse function takes a string and produces a number", function () { + var number = format.parse("1"); + expect(number).toBe(1); + expect(typeof(number)).toBe("number"); + }); + + it("validates that the input is a number", function () { + expect(format.validate("1")).toBe(true); + expect(format.validate(1)).toBe(true); + expect(format.validate("1.1")).toBe(true); + expect(format.validate("abc")).toBe(false); + }); + }); +}); diff --git a/platform/features/conductor-v2/conductor/src/ui/TimeConductorController.js b/platform/features/conductor-v2/conductor/src/ui/TimeConductorController.js index bb5906fbe79..6e42fd9918d 100644 --- a/platform/features/conductor-v2/conductor/src/ui/TimeConductorController.js +++ b/platform/features/conductor-v2/conductor/src/ui/TimeConductorController.js @@ -26,7 +26,7 @@ define( ], function (TimeConductorValidation) { - function TimeConductorController($scope, timeConductor, conductorViewService, timeSystems) { + function TimeConductorController($scope, $window, timeConductor, conductorViewService, timeSystems) { var self = this; @@ -38,6 +38,7 @@ define( }); this.$scope = $scope; + this.$window = $window; this.conductorViewService = conductorViewService; this.conductor = timeConductor; this.modes = conductorViewService.availableModes(); @@ -99,7 +100,6 @@ define( // Watch scope for selection of mode or time system by user this.$scope.$watch('modeModel.selectedKey', this.setMode); - this.$scope.$watch('timeSystem', this.changeTimeSystem); }; /** @@ -113,7 +113,7 @@ define( this.$scope.boundsModel.end = bounds.end; if (!this.pendingUpdate) { this.pendingUpdate = true; - requestAnimationFrame(function () { + this.$window.requestAnimationFrame(function () { this.pendingUpdate = false; this.$scope.$digest(); }.bind(this)); @@ -136,16 +136,8 @@ define( * @private */ TimeConductorController.prototype.setFormFromDeltas = function (deltas) { - /* - * If the selected mode defines deltas, set them in the form - */ - if (deltas !== undefined) { - this.$scope.boundsModel.startDelta = deltas.start; - this.$scope.boundsModel.endDelta = deltas.end; - } else { - this.$scope.boundsModel.startDelta = 0; - this.$scope.boundsModel.endDelta = 0; - } + this.$scope.boundsModel.startDelta = deltas.start; + this.$scope.boundsModel.endDelta = deltas.end; }; /** @@ -180,7 +172,7 @@ define( var deltas = { start: boundsFormModel.startDelta, end: boundsFormModel.endDelta - } + }; if (this.validation.validateStartDelta(deltas.start) && this.validation.validateEndDelta(deltas.end)) { //Sychronize deltas between form and mode this.conductorViewService.deltas(deltas); @@ -204,6 +196,8 @@ define( }; /** + * Respond to time system selection from UI + * * Allows time system to be changed by key. This supports selection * from the menu. Resolves a TimeSystem object and then invokes * TimeConductorController#setTimeSystem @@ -211,13 +205,15 @@ define( * @see TimeConductorController#setTimeSystem */ TimeConductorController.prototype.selectTimeSystemByKey = function(key){ - var selected = this.timeSystems.find(function (timeSystem){ + var selected = this.timeSystems.filter(function (timeSystem){ return timeSystem.metadata.key === key; - }); + })[0]; this.conductor.timeSystem(selected, selected.defaults().bounds); }; /** + * Handles time system change from time conductor + * * Sets the selected time system. Will populate form with the default * bounds and deltas defined in the selected time system. * @@ -226,7 +222,13 @@ define( */ TimeConductorController.prototype.changeTimeSystem = function (newTimeSystem) { if (newTimeSystem && (newTimeSystem !== this.$scope.timeSystemModel.selected)) { - this.setFormFromDeltas((newTimeSystem.defaults() || {}).deltas); + if (newTimeSystem.defaults()){ + var deltas = newTimeSystem.defaults().deltas || {start: 0, end: 0}; + var bounds = newTimeSystem.defaults().bounds || {start: 0, end: 0}; + + this.setFormFromDeltas(deltas); + this.setFormFromBounds(bounds); + } this.setFormFromTimeSystem(newTimeSystem); } }; diff --git a/platform/features/conductor-v2/conductor/src/ui/TimeConductorControllerSpec.js b/platform/features/conductor-v2/conductor/src/ui/TimeConductorControllerSpec.js new file mode 100644 index 00000000000..3dd5b47d103 --- /dev/null +++ b/platform/features/conductor-v2/conductor/src/ui/TimeConductorControllerSpec.js @@ -0,0 +1,335 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web 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 Web 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(['./TimeConductorController'], function (TimeConductorController) { + describe("The time conductor controller", function () { + var mockScope; + var mockWindow; + var mockTimeConductor; + var mockConductorViewService; + var mockTimeSystems; + var controller; + + beforeEach(function () { + mockScope = jasmine.createSpyObj("$scope", ["$watch"]); + mockWindow = jasmine.createSpyObj("$window", ["requestAnimationFrame"]); + mockTimeConductor = jasmine.createSpyObj( + "TimeConductor", + [ + "bounds", + "timeSystem", + "on" + ] + ); + mockTimeConductor.bounds.andReturn({start: undefined, end: undefined}); + + mockConductorViewService = jasmine.createSpyObj( + "ConductorViewService", + [ + "availableModes", + "mode", + "availableTimeSystems", + "deltas" + ] + ); + mockConductorViewService.availableModes.andReturn([]); + mockConductorViewService.availableTimeSystems.andReturn([]); + + mockTimeSystems = []; + }); + + function getListener(name) { + return mockTimeConductor.on.calls.filter(function (call){ + return call.args[0] === name; + })[0].args[1]; + } + + describe("", function (){ + beforeEach(function() { + controller = new TimeConductorController( + mockScope, + mockWindow, + mockTimeConductor, + mockConductorViewService, + mockTimeSystems + ); + }); + + }); + + describe("when time conductor state changes", function () { + var mockFormat; + var mockDeltaFormat; + var defaultBounds; + var defaultDeltas; + var mockDefaults; + var timeSystem; + var tsListener; + + beforeEach(function () { + mockFormat = {}; + mockDeltaFormat = {}; + defaultBounds = { + start: 2, + end: 3 + }; + defaultDeltas = { + start: 10, + end: 20 + }; + mockDefaults = { + deltas: defaultDeltas, + bounds: defaultBounds + }; + timeSystem = { + formats: function () { + return [mockFormat]; + }, + deltaFormat: function () { + return mockDeltaFormat; + }, + defaults: function () { + return mockDefaults; + } + }; + + controller = new TimeConductorController( + mockScope, + mockWindow, + mockTimeConductor, + mockConductorViewService, + mockTimeSystems + ); + + tsListener = getListener("timeSystem"); + }); + + it("listens for changes to conductor state", function () { + expect(mockTimeConductor.on).toHaveBeenCalledWith("timeSystem", jasmine.any(Function)); + expect(mockTimeConductor.on).toHaveBeenCalledWith("bounds", jasmine.any(Function)); + expect(mockTimeConductor.on).toHaveBeenCalledWith("follow", jasmine.any(Function)); + }); + + it("when time system changes, sets time system on scope", function () { + expect(tsListener).toBeDefined(); + tsListener(timeSystem); + + expect(mockScope.timeSystemModel).toBeDefined(); + expect(mockScope.timeSystemModel.selected).toBe(timeSystem); + expect(mockScope.timeSystemModel.format).toBe(mockFormat); + expect(mockScope.timeSystemModel.deltaFormat).toBe(mockDeltaFormat); + }); + + it("when time system changes, sets defaults on scope", function () { + expect(tsListener).toBeDefined(); + tsListener(timeSystem); + + expect(mockScope.boundsModel.start).toEqual(defaultBounds.start); + expect(mockScope.boundsModel.end).toEqual(defaultBounds.end); + + expect(mockScope.boundsModel.startDelta).toEqual(defaultDeltas.start); + expect(mockScope.boundsModel.endDelta).toEqual(defaultDeltas.end); + }); + + it("when bounds change, sets them on scope", function () { + var bounds = { + start: 1, + end: 2 + }; + + var boundsListener = getListener("bounds"); + expect(boundsListener).toBeDefined(); + boundsListener(bounds); + + expect(mockScope.boundsModel).toBeDefined(); + expect(mockScope.boundsModel.start).toEqual(bounds.start); + expect(mockScope.boundsModel.end).toEqual(bounds.end); + }); + + it("responds to a change in 'follow' state of the time conductor", function () { + var followListener = getListener("follow"); + expect(followListener).toBeDefined(); + + followListener(true); + expect(mockScope.followMode).toEqual(true); + + followListener(false); + expect(mockScope.followMode).toEqual(false); + }); + }); + + describe("when user makes changes from UI", function () { + var mode = "realtime"; + var ts1Metadata; + var ts2Metadata; + var ts3Metadata; + var mockTimeSystemConstructors; + + beforeEach(function () { + mode = "realtime"; + ts1Metadata = { + 'key': 'ts1', + 'name': 'Time System One', + 'cssClass': 'cssClassOne' + }; + ts2Metadata = { + 'key': 'ts2', + 'name': 'Time System Two', + 'cssClass': 'cssClassTwo' + }; + ts3Metadata = { + 'key': 'ts3', + 'name': 'Time System Three', + 'cssClass': 'cssClassThree' + }; + mockTimeSystems = [ + { + metadata: ts1Metadata + }, + { + metadata: ts2Metadata + }, + { + metadata: ts3Metadata + } + ]; + + //Wrap in mock constructors + mockTimeSystemConstructors = mockTimeSystems.map(function (mockTimeSystem) { + return function () { + return mockTimeSystem; + }; + }); + }); + + it("sets the mode on scope", function () { + controller = new TimeConductorController( + mockScope, + mockWindow, + mockTimeConductor, + mockConductorViewService, + mockTimeSystemConstructors + ); + + mockConductorViewService.availableTimeSystems.andReturn(mockTimeSystems); + controller.setMode(mode); + + expect(mockScope.modeModel.selectedKey).toEqual(mode); + }); + + it("sets available time systems on scope when mode changes", function () { + controller = new TimeConductorController( + mockScope, + mockWindow, + mockTimeConductor, + mockConductorViewService, + mockTimeSystemConstructors + ); + + mockConductorViewService.availableTimeSystems.andReturn(mockTimeSystems); + controller.setMode(mode); + + expect(mockScope.timeSystemModel.options.length).toEqual(3); + expect(mockScope.timeSystemModel.options[0]).toEqual(ts1Metadata); + expect(mockScope.timeSystemModel.options[1]).toEqual(ts2Metadata); + expect(mockScope.timeSystemModel.options[2]).toEqual(ts3Metadata); + }); + + it("sets bounds on the time conductor", function () { + var formModel = { + start: 1, + end: 10 + }; + + + controller = new TimeConductorController( + mockScope, + mockWindow, + mockTimeConductor, + mockConductorViewService, + mockTimeSystemConstructors + ); + + controller.updateBoundsFromForm(formModel); + expect(mockTimeConductor.bounds).toHaveBeenCalledWith(formModel); + }); + + it("applies deltas when they change in form", function () { + var deltas = { + start: 1000, + end: 2000 + }; + var formModel = { + startDelta: deltas.start, + endDelta: deltas.end + }; + + controller = new TimeConductorController( + mockScope, + mockWindow, + mockTimeConductor, + mockConductorViewService, + mockTimeSystemConstructors + ); + + controller.updateDeltasFromForm(formModel); + expect(mockConductorViewService.deltas).toHaveBeenCalledWith(deltas); + }); + + it("sets the time system on the time conductor", function () { + var defaultBounds = { + start: 5, + end: 6 + }; + var timeSystem = { + metadata: { + key: 'testTimeSystem' + }, + defaults: function() { + return { + bounds: defaultBounds + }; + } + }; + + mockTimeSystems = [ + // Wrap as constructor function + function() { + return timeSystem; + } + ]; + + controller = new TimeConductorController( + mockScope, + mockWindow, + mockTimeConductor, + mockConductorViewService, + mockTimeSystems + ); + + controller.selectTimeSystemByKey('testTimeSystem'); + expect(mockTimeConductor.timeSystem).toHaveBeenCalledWith(timeSystem, defaultBounds); + }); + }); + + }); +}); diff --git a/platform/features/conductor-v2/conductor/src/ui/TimeConductorMode.js b/platform/features/conductor-v2/conductor/src/ui/TimeConductorMode.js index 1f892250364..4fed0610a83 100644 --- a/platform/features/conductor-v2/conductor/src/ui/TimeConductorMode.js +++ b/platform/features/conductor-v2/conductor/src/ui/TimeConductorMode.js @@ -39,8 +39,8 @@ define( this._tickSourceUnlisten = undefined; this._timeSystems = timeSystems; this._availableTickSources = undefined; - this.changeTimeSystem = this.changeTimeSystem.bind(this); + this.tick = this.tick.bind(this); //Set the time system initially if (conductor.timeSystem()) { @@ -81,6 +81,7 @@ define( end: 0 } }; + this.conductor.bounds(defaults.bounds); this.deltas(defaults.deltas); @@ -130,7 +131,7 @@ define( } this._tickSource = tickSource; if (tickSource) { - this._tickSourceUnlisten = tickSource.listen(this.tick.bind(this)); + this._tickSourceUnlisten = tickSource.listen(this.tick); //Now following a tick source this.conductor.follow(true); } else { diff --git a/platform/features/conductor-v2/conductor/src/ui/TimeConductorModeSpec.js b/platform/features/conductor-v2/conductor/src/ui/TimeConductorModeSpec.js new file mode 100644 index 00000000000..23541ebfa00 --- /dev/null +++ b/platform/features/conductor-v2/conductor/src/ui/TimeConductorModeSpec.js @@ -0,0 +1,210 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web 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 Web 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(['./TimeConductorMode'], function (TimeConductorMode) { + describe("The Time Conductor Mode", function () { + var mockTimeConductor, + fixedModeMetaData, + mockTimeSystems, + fixedTimeSystem, + + realtimeModeMetaData, + realtimeTimeSystem, + mockTickSource, + + mockBounds, + mode; + + beforeEach(function () { + fixedModeMetaData = { + key: "fixed" + }; + realtimeModeMetaData = { + key: "realtime" + }; + mockBounds = { + start: 0, + end: 1 + }; + + fixedTimeSystem = jasmine.createSpyObj("timeSystem", [ + "defaults", + "tickSources" + ]); + fixedTimeSystem.tickSources.andReturn([]); + + mockTickSource = jasmine.createSpyObj("tickSource", [ + "listen" + ]); + mockTickSource.metadata = { + mode: "realtime" + }; + realtimeTimeSystem = jasmine.createSpyObj("realtimeTimeSystem", [ + "defaults", + "tickSources" + ]); + realtimeTimeSystem.tickSources.andReturn([mockTickSource]); + + //Do not return any time systems initially for a default + // construction configuration that works without any additional work + mockTimeSystems = []; + + mockTimeConductor = jasmine.createSpyObj("timeConductor", [ + "bounds", + "timeSystem", + "on", + "off", + "follow" + ]); + mockTimeConductor.bounds.andReturn(mockBounds); + }); + + it("Reacts to changes in conductor time system", function () { + mode = new TimeConductorMode(fixedModeMetaData, mockTimeConductor, mockTimeSystems); + expect(mockTimeConductor.on).toHaveBeenCalledWith("timeSystem", mode.changeTimeSystem); + }); + + it("Stops listening to time system changes on destroy", function () { + mode = new TimeConductorMode(fixedModeMetaData, mockTimeConductor, mockTimeSystems); + mode.destroy(); + expect(mockTimeConductor.off).toHaveBeenCalledWith("timeSystem", mode.changeTimeSystem); + }); + + it("Filters available time systems to those with tick sources that" + + " support this mode", function () { + mockTimeSystems = [fixedTimeSystem, realtimeTimeSystem]; + mode = new TimeConductorMode(realtimeModeMetaData, mockTimeConductor, mockTimeSystems); + + var availableTimeSystems = mode.availableTimeSystems(); + expect(availableTimeSystems.length).toBe(1); + expect(availableTimeSystems.indexOf(fixedTimeSystem)).toBe(-1); + expect(availableTimeSystems.indexOf(realtimeTimeSystem)).toBe(0); + }); + + describe("Changing the time system", function () { + var defaults; + + beforeEach(function () { + defaults = { + bounds: { + start: 1, + end: 2 + }, + deltas: { + start: 3, + end: 4 + } + }; + + fixedTimeSystem.defaults.andReturn(defaults); + + }); + it ("sets defaults from new time system", function() { + mode = new TimeConductorMode(fixedModeMetaData, mockTimeConductor, mockTimeSystems); + spyOn(mode, "deltas"); + mode.deltas.andCallThrough(); + + mode.changeTimeSystem(fixedTimeSystem); + expect(mockTimeConductor.bounds).toHaveBeenCalledWith(defaults.bounds); + expect(mode.deltas).toHaveBeenCalledWith(defaults.deltas); + }); + it ("If a tick source is available, sets the tick source", function() { + mode = new TimeConductorMode(realtimeModeMetaData, mockTimeConductor, mockTimeSystems); + mode.changeTimeSystem(realtimeTimeSystem); + + var currentTickSource = mode.tickSource(); + expect(currentTickSource).toBe(mockTickSource); + }); + }); + + describe("Setting a tick source", function () { + var mockUnlistener; + + beforeEach(function() { + mockUnlistener = jasmine.createSpy("unlistener"); + mockTickSource.listen.andReturn(mockUnlistener); + + mode = new TimeConductorMode(realtimeModeMetaData, mockTimeConductor, mockTimeSystems); + mode.tickSource(mockTickSource); + }); + + it ("Unlistens from old tick source", function() { + mode.tickSource(mockTickSource); + expect(mockUnlistener).toHaveBeenCalled(); + }); + + it ("Listens to new tick source", function() { + expect(mockTickSource.listen).toHaveBeenCalledWith(mode.tick); + }); + + it ("Sets 'follow' state on time conductor", function() { + expect(mockTimeConductor.follow).toHaveBeenCalledWith(true); + }); + + it ("on destroy, unlistens from tick source", function() { + mode.destroy(); + expect(mockUnlistener).toHaveBeenCalled(); + }); + }); + + describe("setting deltas", function () { + beforeEach(function() { + mode = new TimeConductorMode(realtimeModeMetaData, mockTimeConductor, mockTimeSystems); + }); + it ("sets the bounds on the time conductor based on new delta" + + " values", function() { + var deltas = { + start: 20, + end: 10 + }; + + mode.deltas(deltas); + + expect(mockTimeConductor.bounds).toHaveBeenCalledWith({ + start: mockBounds.end - deltas.start, + end: mockBounds.end + deltas.end + }); + }); + }); + + describe("ticking", function () { + beforeEach(function() { + mode = new TimeConductorMode(realtimeModeMetaData, mockTimeConductor, mockTimeSystems); + }); + it ("sets bounds based on current delta values", function() { + var deltas = { + start: 20, + end: 10 + }; + var time = 100; + + mode.deltas(deltas); + mode.tick(time); + + expect(mockTimeConductor.bounds).toHaveBeenCalledWith({ + start: time - deltas.start, + end:time + deltas.end + }); + }); + }); + }); +}); diff --git a/platform/features/conductor-v2/conductor/src/ui/TimeConductorValidation.js b/platform/features/conductor-v2/conductor/src/ui/TimeConductorValidation.js index bf802b6cc1c..3778c26a21f 100644 --- a/platform/features/conductor-v2/conductor/src/ui/TimeConductorValidation.js +++ b/platform/features/conductor-v2/conductor/src/ui/TimeConductorValidation.js @@ -31,7 +31,7 @@ define( */ function TimeConductorValidation(conductor) { var self = this; - this.conductor = conductor + this.conductor = conductor; /* * Bind all class functions to 'this' diff --git a/platform/features/conductor-v2/conductor/src/ui/TimeConductorValidationSpec.js b/platform/features/conductor-v2/conductor/src/ui/TimeConductorValidationSpec.js new file mode 100644 index 00000000000..4dc954d38c3 --- /dev/null +++ b/platform/features/conductor-v2/conductor/src/ui/TimeConductorValidationSpec.js @@ -0,0 +1,73 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web 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 Web 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(['./TimeConductorValidation'], function (TimeConductorValidation) { + describe("The Time Conductor Validation class", function () { + var timeConductorValidation, + mockTimeConductor; + + beforeEach(function () { + mockTimeConductor = jasmine.createSpyObj("timeConductor", [ + "validateBounds", + "bounds" + ]); + timeConductorValidation = new TimeConductorValidation(mockTimeConductor); + }); + + describe("Validates start and end values using Time Conductor", function () { + beforeEach(function() { + var mockBounds = { + start: 10, + end: 20 + }; + + mockTimeConductor.bounds.andReturn(mockBounds); + + }); + it("Validates start values using Time Conductor", function () { + var startValue = 30; + timeConductorValidation.validateStart(startValue); + expect(mockTimeConductor.validateBounds).toHaveBeenCalled(); + }); + it("Validates end values using Time Conductor", function () { + var endValue = 40; + timeConductorValidation.validateEnd(endValue); + expect(mockTimeConductor.validateBounds).toHaveBeenCalled(); + }); + }); + + it("Validates that start delta is valid number > 0", function () { + expect(timeConductorValidation.validateStartDelta(-1)).toBe(false); + expect(timeConductorValidation.validateStartDelta("abc")).toBe(false); + expect(timeConductorValidation.validateStartDelta("1")).toBe(true); + expect(timeConductorValidation.validateStartDelta(1)).toBe(true); + }); + + it("Validates that end delta is valid number >= 0", function () { + expect(timeConductorValidation.validateEndDelta(-1)).toBe(false); + expect(timeConductorValidation.validateEndDelta("abc")).toBe(false); + expect(timeConductorValidation.validateEndDelta("1")).toBe(true); + expect(timeConductorValidation.validateEndDelta(0)).toBe(true); + expect(timeConductorValidation.validateEndDelta(1)).toBe(true); + }); + }); +}); diff --git a/platform/features/conductor-v2/conductor/src/ui/TimeConductorViewService.js b/platform/features/conductor-v2/conductor/src/ui/TimeConductorViewService.js index 6c6dca7c73a..4e4a02ebcb1 100644 --- a/platform/features/conductor-v2/conductor/src/ui/TimeConductorViewService.js +++ b/platform/features/conductor-v2/conductor/src/ui/TimeConductorViewService.js @@ -36,7 +36,7 @@ define( * @constructor */ function TimeConductorViewService(conductor, timeSystems) { - this._timeSystems = timeSystems = timeSystems.map( + this._timeSystems = timeSystems.map( function (timeSystemConstructor) { return timeSystemConstructor(); }); @@ -63,34 +63,38 @@ define( } }; - function timeSystemsForMode(sourceType) { - return timeSystems.filter(function (timeSystem){ - return timeSystem.tickSources().some(function (tickSource){ - return tickSource.metadata.mode === sourceType; - }); + function hasTickSource(sourceType, timeSystem) { + return timeSystem.tickSources().some(function (tickSource){ + return tickSource.metadata.mode === sourceType; }); } + var timeSystemsForMode = function (sourceType) { + return this._timeSystems.filter(hasTickSource.bind(this, sourceType)); + }.bind(this); + //Only show 'real-time mode' if appropriate time systems available if (timeSystemsForMode('realtime').length > 0 ) { - this._availableModes['realtime'] = { + var realtimeMode = { key: 'realtime', cssclass: 'icon-clock', label: 'Real-time', name: 'Real-time Mode', description: 'Monitor real-time streaming data as it comes in. The Time Conductor and displays will automatically advance themselves based on a UTC clock.' }; + this._availableModes[realtimeMode.key] = realtimeMode; } //Only show 'LAD mode' if appropriate time systems available if (timeSystemsForMode('LAD').length > 0) { - this._availableModes['latest'] = { + var ladMode = { key: 'LAD', cssclass: 'icon-database', label: 'LAD', name: 'LAD Mode', description: 'Latest Available Data mode monitors real-time streaming data as it comes in. The Time Conductor and displays will only advance when data becomes available.' }; + this._availableModes[ladMode.key] = ladMode; } } @@ -114,6 +118,12 @@ define( * */ TimeConductorViewService.prototype.mode = function (newModeKey) { + function contains(timeSystems, ts) { + return timeSystems.filter(function (t) { + return t.metadata.key === ts.metadata.key; + }).length > 0; + } + if (arguments.length === 1) { var timeSystem = this._conductor.timeSystem(); var modes = this.availableModes(); @@ -122,13 +132,10 @@ define( if (this._mode) { this._mode.destroy(); } - - function contains(timeSystems, timeSystem) { - return timeSystems.find(function (t) { - return t.metadata.key === timeSystem.metadata.key; - }) !== undefined; - } this._mode = new TimeConductorMode(modeMetaData, this._conductor, this._timeSystems); + + // If no time system set on time conductor, or the currently selected time system is not available in + // the new mode, default to first available time system if (!timeSystem || !contains(this._mode.availableTimeSystems(), timeSystem)) { timeSystem = this._mode.availableTimeSystems()[0]; this._conductor.timeSystem(timeSystem, timeSystem.defaults().bounds); @@ -169,7 +176,7 @@ define( */ TimeConductorViewService.prototype.deltas = function () { //Deltas stored on mode. Use .apply to preserve arguments - return this._mode.deltas.apply(this._mode, arguments) + return this._mode.deltas.apply(this._mode, arguments); }; /** diff --git a/platform/features/conductor-v2/conductor/src/ui/TimeConductorViewServiceSpec.js b/platform/features/conductor-v2/conductor/src/ui/TimeConductorViewServiceSpec.js new file mode 100644 index 00000000000..13ec9f8022f --- /dev/null +++ b/platform/features/conductor-v2/conductor/src/ui/TimeConductorViewServiceSpec.js @@ -0,0 +1,185 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web 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 Web 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(['./TimeConductorViewService'], function (TimeConductorViewService) { + describe("The Time Conductor view service", function () { + var mockTimeConductor; + var basicTimeSystem; + var tickingTimeSystem; + var viewService; + var tickingTimeSystemDefaults; + + function mockConstructor(object) { + return function () { + return object; + }; + } + + beforeEach(function () { + mockTimeConductor = jasmine.createSpyObj("timeConductor", [ + "timeSystem", + "bounds", + "follow", + "on", + "off" + ]); + + basicTimeSystem = jasmine.createSpyObj("basicTimeSystem", [ + "tickSources", + "defaults" + ]); + basicTimeSystem.metadata = { + key: "basic" + }; + basicTimeSystem.tickSources.andReturn([]); + basicTimeSystem.defaults.andReturn({ + bounds: { + start: 0, + end: 1 + }, + deltas: { + start: 0, + end: 0 + } + }); + //Initialize conductor + mockTimeConductor.timeSystem.andReturn(basicTimeSystem); + mockTimeConductor.bounds.andReturn({start: 0, end: 1}); + + tickingTimeSystem = jasmine.createSpyObj("tickingTimeSystem", [ + "tickSources", + "defaults" + ]); + tickingTimeSystem.metadata = { + key: "ticking" + }; + tickingTimeSystemDefaults = { + bounds: { + start: 100, + end: 200 + }, + deltas: { + start: 1000, + end: 500 + } + }; + tickingTimeSystem.defaults.andReturn(tickingTimeSystemDefaults); + }); + + it("At a minimum supports fixed mode", function () { + var mockTimeSystems = [mockConstructor(basicTimeSystem)]; + viewService = new TimeConductorViewService(mockTimeConductor, mockTimeSystems); + + var availableModes = viewService.availableModes(); + expect(availableModes.fixed).toBeDefined(); + }); + + it("Supports realtime mode if appropriate tick source(s) availables", function () { + var mockTimeSystems = [mockConstructor(tickingTimeSystem)]; + var mockRealtimeTickSource = { + metadata: { + mode: 'realtime' + } + }; + tickingTimeSystem.tickSources.andReturn([mockRealtimeTickSource]); + + viewService = new TimeConductorViewService(mockTimeConductor, mockTimeSystems); + + var availableModes = viewService.availableModes(); + expect(availableModes.realtime).toBeDefined(); + }); + + it("Supports LAD mode if appropriate tick source(s) available", function () { + var mockTimeSystems = [mockConstructor(tickingTimeSystem)]; + var mockLADTickSource = { + metadata: { + mode: 'LAD' + } + }; + tickingTimeSystem.tickSources.andReturn([mockLADTickSource]); + + viewService = new TimeConductorViewService(mockTimeConductor, mockTimeSystems); + + var availableModes = viewService.availableModes(); + expect(availableModes.LAD).toBeDefined(); + }); + + describe("when mode is changed", function () { + + it("destroys previous mode", function () { + var mockTimeSystems = [mockConstructor(basicTimeSystem)]; + + var oldMode = jasmine.createSpyObj("conductorMode", [ + "destroy" + ]); + + viewService = new TimeConductorViewService(mockTimeConductor, mockTimeSystems); + viewService._mode = oldMode; + viewService.mode('fixed'); + expect(oldMode.destroy).toHaveBeenCalled(); + }); + + describe("the time system", function () { + it("is retained if available in new mode", function () { + var mockTimeSystems = [mockConstructor(basicTimeSystem), mockConstructor(tickingTimeSystem)]; + var mockRealtimeTickSource = { + metadata: { + mode: 'realtime' + }, + listen: function() {} + }; + tickingTimeSystem.tickSources.andReturn([mockRealtimeTickSource]); + + viewService = new TimeConductorViewService(mockTimeConductor, mockTimeSystems); + + //Set time system to one known to support realtime mode + mockTimeConductor.timeSystem.andReturn(tickingTimeSystem); + + //Select realtime mode + mockTimeConductor.timeSystem.reset(); + viewService.mode('realtime'); + expect(mockTimeConductor.timeSystem).not.toHaveBeenCalledWith(tickingTimeSystem, tickingTimeSystemDefaults.bounds); + }); + it("is defaulted if selected time system not available in new mode", function () { + var mockTimeSystems = [mockConstructor(basicTimeSystem), mockConstructor(tickingTimeSystem)]; + var mockRealtimeTickSource = { + metadata: { + mode: 'realtime' + }, + listen: function() {} + }; + tickingTimeSystem.tickSources.andReturn([mockRealtimeTickSource]); + + viewService = new TimeConductorViewService(mockTimeConductor, mockTimeSystems); + + //Set time system to one known to not support realtime mode + mockTimeConductor.timeSystem.andReturn(basicTimeSystem); + + //Select realtime mode + mockTimeConductor.timeSystem.reset(); + viewService.mode('realtime'); + expect(mockTimeConductor.timeSystem).toHaveBeenCalledWith(tickingTimeSystem, tickingTimeSystemDefaults.bounds); + }); + }); + }); + }); +}); diff --git a/platform/features/conductor-v2/utcTimeSystem/src/UTCTimeSystemSpec.js b/platform/features/conductor-v2/utcTimeSystem/src/UTCTimeSystemSpec.js new file mode 100644 index 00000000000..808eade72e5 --- /dev/null +++ b/platform/features/conductor-v2/utcTimeSystem/src/UTCTimeSystemSpec.js @@ -0,0 +1,46 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web 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 Web 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(['./UTCTimeSystem'], function (UTCTimeSystem) { + describe("The UTCTimeSystem class", function () { + var timeSystem, + mockTimeout; + + beforeEach(function () { + mockTimeout = jasmine.createSpy("timeout"); + timeSystem = new UTCTimeSystem(mockTimeout); + }); + + it("defines at least one format", function () { + expect(timeSystem.formats().length).toBeGreaterThan(0); + }); + + it("defines a tick source", function () { + var tickSources = timeSystem.tickSources(); + expect(tickSources.length).toBeGreaterThan(0); + }); + + it("defines some defaults", function () { + expect(timeSystem.defaults()).toBeDefined(); + }); + }); +}); diff --git a/platform/features/plot/src/PlotController.js b/platform/features/plot/src/PlotController.js index 6589deeb025..8e2749aa5a2 100644 --- a/platform/features/plot/src/PlotController.js +++ b/platform/features/plot/src/PlotController.js @@ -239,7 +239,7 @@ define( setBasePanZoom(bounds); requery(); } - self.updateStatus($scope.domainObject, follow); + self.setUnsynchedStatus($scope.domainObject, follow && self.isZoomed()); } this.modeOptions = new PlotModeOptions([], subPlotFactory); @@ -370,9 +370,9 @@ define( return this.pending; }; - PlotController.prototype.updateStatus = function (domainObject, follow) { + PlotController.prototype.setUnsynchedStatus = function (domainObject, status) { if (domainObject.hasCapability('status')) { - domainObject.getCapability('status').set('timeconductor-unsynced', follow && this.isZoomed()); + domainObject.getCapability('status').set('timeconductor-unsynced', status); } }; diff --git a/platform/features/plot/test/PlotControllerSpec.js b/platform/features/plot/test/PlotControllerSpec.js index a3de710bdfe..c7bc8fd594a 100644 --- a/platform/features/plot/test/PlotControllerSpec.js +++ b/platform/features/plot/test/PlotControllerSpec.js @@ -35,6 +35,7 @@ define( mockHandle, mockDomainObject, mockSeries, + mockStatusCapability, controller; function bind(method, thisObj) { @@ -71,7 +72,7 @@ define( ); mockDomainObject = jasmine.createSpyObj( "domainObject", - ["getId", "getModel", "getCapability"] + ["getId", "getModel", "getCapability", "hasCapability"] ); mockHandler = jasmine.createSpyObj( "telemetrySubscriber", @@ -95,6 +96,11 @@ define( ['getPointCount', 'getDomainValue', 'getRangeValue'] ); + mockStatusCapability = jasmine.createSpyObj( + "statusCapability", + ["set"] + ); + mockHandler.handle.andReturn(mockHandle); mockThrottle.andCallFake(function (fn) { return fn; @@ -230,6 +236,34 @@ define( expect(bind(controller.unzoom, controller)).not.toThrow(); }); + it("sets status when plot becomes detached from time conductor", function () { + mockScope.$watch.mostRecentCall.args[1](mockDomainObject); + + function boundsEvent(){ + fireEvent("telemetry:display:bounds", [ + {}, + { start: 10, end: 100 }, + true + ]); + } + + mockDomainObject.hasCapability.andCallFake(function (name) { + return name === "status"; + }); + mockDomainObject.getCapability.andReturn(mockStatusCapability); + spyOn(controller, "isZoomed"); + + //Mock zoomed in state + controller.isZoomed.andReturn(true); + boundsEvent(); + expect(mockStatusCapability.set).toHaveBeenCalledWith("timeconductor-unsynced", true); + + //"Reset" zoom + controller.isZoomed.andReturn(false); + boundsEvent(); + expect(mockStatusCapability.set).toHaveBeenCalledWith("timeconductor-unsynced", false); + }); + it("indicates if a request is pending", function () { mockScope.$watch.mostRecentCall.args[1](mockDomainObject); expect(controller.isRequestPending()).toBeTruthy(); @@ -286,7 +320,6 @@ define( expect(mockHandle.request.calls.length).toEqual(2); }); - it("maintains externally-provided domain axis bounds after data is received", function () { mockSeries.getPointCount.andReturn(3); mockSeries.getRangeValue.andReturn(42);