From a3311e4c5783467f9c692987a4091857b671c080 Mon Sep 17 00:00:00 2001 From: Henry Date: Thu, 26 Jan 2017 10:59:22 -0800 Subject: [PATCH] [Tables] Tests and style fixes --- .../conductor/utcTimeSystem/bundle.js | 3 +- .../table/res/templates/telemetry-table.html | 1 - .../features/table/src/TableConfiguration.js | 47 +-- .../features/table/src/TelemetryCollection.js | 165 ++++++-- .../src/controllers/MCTTableController.js | 44 +- .../controllers/TelemetryTableController.js | 231 +++++++---- .../features/table/test/DomainColumnSpec.js | 80 ---- .../features/table/test/NameColumnSpec.js | 56 --- .../features/table/test/RangeColumnSpec.js | 74 ---- .../table/test/TableConfigurationSpec.js | 142 +++---- .../table/test/TelemetryCollectionSpec.js | 191 +++++++++ .../HistoricalTableControllerSpec.js | 380 ------------------ .../controllers/MCTTableControllerSpec.js | 100 ++--- .../RealtimeTableControllerSpec.js | 171 -------- .../TelemetryTableControllerSpec.js | 364 +++++++++++++++++ 15 files changed, 970 insertions(+), 1079 deletions(-) delete mode 100644 platform/features/table/test/DomainColumnSpec.js delete mode 100644 platform/features/table/test/NameColumnSpec.js delete mode 100644 platform/features/table/test/RangeColumnSpec.js create mode 100644 platform/features/table/test/TelemetryCollectionSpec.js delete mode 100644 platform/features/table/test/controllers/HistoricalTableControllerSpec.js delete mode 100644 platform/features/table/test/controllers/RealtimeTableControllerSpec.js create mode 100644 platform/features/table/test/controllers/TelemetryTableControllerSpec.js diff --git a/platform/features/conductor/utcTimeSystem/bundle.js b/platform/features/conductor/utcTimeSystem/bundle.js index 806087e2ea..5db4bd968f 100644 --- a/platform/features/conductor/utcTimeSystem/bundle.js +++ b/platform/features/conductor/utcTimeSystem/bundle.js @@ -22,8 +22,7 @@ define([ "./src/UTCTimeSystem", - 'legacyRegistry', - 'openmct' + "legacyRegistry" ], function ( UTCTimeSystem, legacyRegistry diff --git a/platform/features/table/res/templates/telemetry-table.html b/platform/features/table/res/templates/telemetry-table.html index 24c6a7702f..5bda288f1c 100644 --- a/platform/features/table/res/templates/telemetry-table.html +++ b/platform/features/table/res/templates/telemetry-table.html @@ -1,6 +1,5 @@
- Request time: {{tableController.lastRequestTime | date : 'HH:mm:ss:sss'}} 0){ + if (discarded && discarded.length > 0) { + /** + * A `discarded` event is thrown when telemetry data fall out of + * bounds due to a bounds change event + * @type {object[]} discarded the telemetry data + * discarded as a result of the bounds change + */ this.emit('discarded', discarded); } if (added && added.length > 0) { + /** + * An `added` event is thrown when a bounds change results in + * received telemetry falling within the new bounds. + * @type {object[]} added the telemetry data that is now within bounds + */ this.emit('added', added); } this.lastBounds = bounds; }; - TelemetryCollection.prototype.inBounds = function (element) { + /** + * Determines is a given telemetry datum is within the bounds currently + * defined for this telemetry collection. + * @private + * @param datum + * @returns {boolean} + */ + TelemetryCollection.prototype.inBounds = function (datum) { var noBoundsDefined = !this.lastBounds || (this.lastBounds.start === undefined && this.lastBounds.end === undefined); var withinBounds = - _.get(element, this.sortField) >= this.lastBounds.start && - _.get(element, this.sortField) <= this.lastBounds.end; + _.get(datum, this.sortField) >= this.lastBounds.start && + _.get(datum, this.sortField) <= this.lastBounds.end; return noBoundsDefined || withinBounds; }; /** + * Adds an individual item to the collection. Used internally only * @private - * @param element + * @param item */ - TelemetryCollection.prototype.addOne = function (element) { + TelemetryCollection.prototype.addOne = function (item) { var isDuplicate = false; - var boundsDefined = this.lastBounds && (this.lastBounds.start && this.lastBounds.end); + var boundsDefined = this.lastBounds && + (this.lastBounds.start !== undefined && this.lastBounds.end !== undefined); var array; + var boundsLow; + var boundsHigh; + + // If collection is not sorted by a time field, we cannot respond to + // bounds events, so no bounds checking necessary + if (this.sortField === undefined) { + this.telemetry.push(item); + return true; + } // Insert into either in-bounds array, or the out of bounds high buffer. // Data in the high buffer will be re-evaluated for possible insertion on next tick if (boundsDefined) { - var boundsHigh = _.get(element, this.sortField) > this.lastBounds.end; - var boundsLow = _.get(element, this.sortField) < this.lastBounds.start; + boundsHigh = _.get(item, this.sortField) > this.lastBounds.end; + boundsLow = _.get(item, this.sortField) < this.lastBounds.start; if (!boundsHigh && !boundsLow) { array = this.telemetry; @@ -119,26 +166,26 @@ define( // If out of bounds low, disregard data if (!boundsLow) { + // Going to check for duplicates. Bound the search problem to - // elements around the given time. Use sortedIndex because it + // items around the given time. Use sortedIndex because it // employs a binary search which is O(log n). Can use binary search // based on time stamp because the array is guaranteed ordered due // to sorted insertion. - - var startIx = _.sortedIndex(array, element, this.sortField); + var startIx = _.sortedIndex(array, item, this.sortField); if (startIx !== array.length) { - var endIx = _.sortedLastIndex(array, element, this.sortField); + var endIx = _.sortedLastIndex(array, item, this.sortField); // Create an array of potential dupes, based on having the // same time stamp var potentialDupes = array.slice(startIx, endIx + 1); // Search potential dupes for exact dupe - isDuplicate = _.findIndex(potentialDupes, _.isEqual.bind(undefined, element)) > -1; + isDuplicate = _.findIndex(potentialDupes, _.isEqual.bind(undefined, item)) > -1; } if (!isDuplicate) { - array.splice(startIx, 0, element); + array.splice(startIx, 0, item); //Return true if it was added and in bounds return array === this.telemetry; @@ -147,28 +194,60 @@ define( return false; }; - TelemetryCollection.prototype.addAll = function (elements) { - var added = elements.filter(this.addOne); + /** + * Add an array of objects to this telemetry collection + * @fires TelemetryCollection#added + * @param {object[]} items + */ + TelemetryCollection.prototype.add = function (items) { + var added = items.filter(this.addOne); this.emit('added', added); }; - //Todo: addAll for initial historical data - TelemetryCollection.prototype.add = function (element) { - if (this.addOne(element)){ - this.emit('added', [element]); - return true; - } else { - return false; - } - }; - + /** + * Clears the contents of the telemetry collection + */ TelemetryCollection.prototype.clear = function () { this.telemetry = []; }; - TelemetryCollection.prototype.sort = function (sortField){ + /** + * Sorts the telemetry collection based on the provided sort field + * specifier. + * @example + * // First build some mock telemetry for the purpose of an example + * let now = Date.now(); + * let telemetry = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(function (value) { + * return { + * // define an object property to demonstrate nested paths + * timestamp: { + * ms: now - value * 1000, + * text: + * }, + * value: value + * } + * }); + * let collection = new TelemetryCollection(); + * + * collection.add(telemetry); + * + * // Sort by telemetry value + * collection.sort("value"); + * + * // Sort by ms since epoch + * collection.sort("timestamp.ms"); + * + * // Sort by formatted date text + * collection.sort("timestamp.text"); + * + * + * @param {string} sortField An object property path. + */ + TelemetryCollection.prototype.sort = function (sortField) { this.sortField = sortField; - this.telemetry = _.sortBy(this.telemetry, this.iteratee); + if (sortField !== undefined) { + this.telemetry = _.sortBy(this.telemetry, this.iteratee); + } }; return TelemetryCollection; diff --git a/platform/features/table/src/controllers/MCTTableController.js b/platform/features/table/src/controllers/MCTTableController.js index af942f8b55..c987a0c3b8 100644 --- a/platform/features/table/src/controllers/MCTTableController.js +++ b/platform/features/table/src/controllers/MCTTableController.js @@ -1,7 +1,10 @@ define( - ['zepto'], - function ($) { + [ + 'zepto', + 'lodash' + ], + function ($, _) { /** * A controller for the MCTTable directive. Populates scope with @@ -134,7 +137,7 @@ define( */ $scope.$watch('defaultSort', function (newColumn, oldColumn) { if (newColumn !== oldColumn) { - $scope.toggleSort(newColumn) + $scope.toggleSort(newColumn); } }); @@ -163,7 +166,7 @@ define( } }.bind(this)); - $scope.$on('$destroy', function() { + $scope.$on('$destroy', function () { this.scrollable.off('scroll', this.onScroll); this.destroyConductorListeners(); @@ -172,7 +175,7 @@ define( delete this.$scope; }.bind(this)); - }; + } MCTTableController.prototype.destroyConductorListeners = function () { this.conductor.off('timeSystem', this.changeTimeSystem); @@ -227,7 +230,7 @@ define( */ MCTTableController.prototype.removeRows = function (event, rows) { var indexInDisplayRows; - rows.forEach(function (row){ + rows.forEach(function (row) { // Do a sequential search here. Only way of finding row is by // object equality, so array is in effect unsorted. indexInDisplayRows = this.$scope.displayRows.indexOf(row); @@ -252,7 +255,7 @@ define( * @private */ MCTTableController.prototype.onScroll = function (event) { - requestAnimationFrame(function () { + this.$window.requestAnimationFrame(function () { this.setVisibleRows(); this.digest(); @@ -344,18 +347,11 @@ define( this.$scope.visibleRows[this.$scope.visibleRows.length - 1] .rowIndex === end) { return this.digest(); - //return Promise.resolve(); // don't update if no changes are required. } } //Set visible rows from display rows, based on calculated offset. this.$scope.visibleRows = this.$scope.displayRows.slice(start, end) .map(function (row, i) { -/* var formattedRow = JSON.parse(JSON.stringify(row)); - if (self.$scope.formatCell) { - Object.keys(formattedRow).forEach(function (header) { - formattedRow[header].text = self.$scope.formatCell(header, row[header].text); - }); - } */ return { rowIndex: start + i, offsetY: ((start + i) * self.$scope.rowHeight) + @@ -585,19 +581,20 @@ define( MCTTableController.prototype.digest = function () { var scope = this.$scope; var self = this; - var requestAnimationFrame = this.$window.requestAnimationFrame; + var raf = this.$window.requestAnimationFrame; + var promise = this.digestPromise; - if (!this.digestPromise) { - this.digestPromise = new Promise(function (resolve) { - requestAnimationFrame(function() { + if (!promise) { + self.digestPromise = promise = new Promise(function (resolve) { + raf(function () { scope.$digest(); - delete self.digestPromise; + self.digestPromise = undefined; resolve(); }); }); } - return this.digestPromise; + return promise; }; /** @@ -640,8 +637,10 @@ define( } this.$scope.displayRows = this.filterAndSort(newRows || []); - this.resize(newRows) - .then(this.setVisibleRows) + return this.resize(newRows) + .then(function (rows) { + return this.setVisibleRows(rows); + }.bind(this)) //Timeout following setVisibleRows to allow digest to // perform DOM changes, otherwise scrollTo won't work. .then(function () { @@ -692,6 +691,7 @@ define( }; /** + * Scroll the view to a given row index * @param displayRowIndex {number} The index in the displayed rows * to scroll to. */ diff --git a/platform/features/table/src/controllers/TelemetryTableController.js b/platform/features/table/src/controllers/TelemetryTableController.js index 7e8fe1fe29..5beb49cb7e 100644 --- a/platform/features/table/src/controllers/TelemetryTableController.js +++ b/platform/features/table/src/controllers/TelemetryTableController.js @@ -19,6 +19,7 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ +/* global console*/ /** * This bundle adds a table view for displaying telemetry data. @@ -28,10 +29,11 @@ define( [ '../TableConfiguration', '../../../../../src/api/objects/object-utils', - '../TelemetryCollection' + '../TelemetryCollection', + 'lodash' ], - function (TableConfiguration, objectUtils, TelemetryCollection) { + function (TableConfiguration, objectUtils, TelemetryCollection, _) { /** * The TableController is responsible for getting data onto the page @@ -46,6 +48,7 @@ define( $timeout, openmct ) { + this.$scope = $scope; this.$timeout = $timeout; this.openmct = openmct; @@ -55,14 +58,14 @@ define( * Initialization block */ this.columns = {}; //Range and Domain columns - this.deregisterListeners = []; + this.unobserveObject = undefined; this.subscriptions = []; this.timeColumns = []; $scope.rows = []; this.table = new TableConfiguration($scope.domainObject, openmct); this.lastBounds = this.openmct.conductor.bounds(); - this.requestTime = 0; + this.lastRequestTime = 0; this.telemetry = new TelemetryCollection(); /* @@ -81,38 +84,45 @@ define( 'changeBounds', 'setScroll', 'addRowsToTable', - 'removeRowsFromTable', + 'removeRowsFromTable' ]); - this.getData(); - this.registerChangeListeners(); + // Retrieve data when domain object is available. + // Also deferring telemetry request makes testing easier as controller + // construction has no unintended consequences. + $scope.$watch("domainObject", function () { + this.getData(); + this.registerChangeListeners(); + }.bind(this)); - this.openmct.conductor.on('follow', this.setScroll); this.setScroll(this.openmct.conductor.follow()); - this.telemetry.on('added', this.addRowsToTable); - this.telemetry.on('discarded', this.removeRowsFromTable); - this.$scope.$on("$destroy", this.destroy); } - TelemetryTableController.prototype.setScroll = function (scroll){ + /** + * @private + * @param {boolean} scroll + */ + TelemetryTableController.prototype.setScroll = function (scroll) { this.$scope.autoScroll = scroll; }; /** * Based on the selected time system, find a matching domain column * to sort by. By default will just match on key. - * @param timeSystem + * + * @private + * @param {TimeSystem} timeSystem */ TelemetryTableController.prototype.sortByTimeSystem = function (timeSystem) { var scope = this.$scope; - var sortColumn = undefined; + var sortColumn; scope.defaultSort = undefined; if (timeSystem) { this.table.columns.forEach(function (column) { - if (column.metadata.key === timeSystem.metadata.key) { + if (column.getKey() === timeSystem.metadata.key) { sortColumn = column; } }); @@ -124,44 +134,66 @@ define( }; /** - * Attach listeners to domain object to respond to changes due to - * composition, etc. + * Attaches listeners that respond to state change in domain object, + * conductor, and receipt of telemetry + * * @private */ TelemetryTableController.prototype.registerChangeListeners = function () { - this.deregisterListeners.forEach(function (deregister){ - deregister(); - }); - this.deregisterListeners = []; + if (this.unobserveObject) { + this.unobserveObject(); + } - this.deregisterListeners.push( - this.openmct.objects.observe(this.newObject, "*", - function (domainObject){ + this.unobserveObject = this.openmct.objects.observe(this.newObject, "*", + function (domainObject) { this.newObject = domainObject; this.getData(); }.bind(this) - ) - ); + ); + this.openmct.conductor.on('timeSystem', this.sortByTimeSystem); this.openmct.conductor.on('bounds', this.changeBounds); + this.openmct.conductor.on('follow', this.setScroll); + + this.telemetry.on('added', this.addRowsToTable); + this.telemetry.on('discarded', this.removeRowsFromTable); }; + /** + * On receipt of new telemetry, informs mct-table directive that new rows + * are available and passes populated rows to it + * + * @private + * @param rows + */ TelemetryTableController.prototype.addRowsToTable = function (rows) { this.$scope.$broadcast('add:rows', rows); }; + /** + * When rows are to be removed, informs mct-table directive. Row removal + * happens when rows call outside the bounds of the time conductor + * + * @private + * @param rows + */ TelemetryTableController.prototype.removeRowsFromTable = function (rows) { this.$scope.$broadcast('remove:rows', rows); }; + /** + * On Time Conductor bounds change, update displayed telemetry. In the + * case of a tick, previously visible telemetry that is now out of band + * will be removed from the table. + * @param {openmct.TimeConductorBounds~TimeConductorBounds} bounds + */ TelemetryTableController.prototype.changeBounds = function (bounds) { - //console.log('bounds.end: ' + bounds.end); var follow = this.openmct.conductor.follow(); var isTick = follow && bounds.start !== this.lastBounds.start && bounds.end !== this.lastBounds.end; - if (isTick){ + if (isTick) { this.telemetry.bounds(bounds); } else { // Is fixed bounds change @@ -171,7 +203,7 @@ define( }; /** - * Release the current subscription (called when scope is destroyed) + * Clean controller, deregistering listeners etc. */ TelemetryTableController.prototype.destroy = function () { @@ -182,11 +214,11 @@ define( this.subscriptions.forEach(function (subscription) { subscription(); }); - this.deregisterListeners.forEach(function (deregister){ - deregister(); - }); + + if (this.unobserveObject) { + this.unobserveObject(); + } this.subscriptions = []; - this.deregisterListeners = []; if (this.timeoutHandle) { this.$timeout.cancel(this.timeoutHandle); @@ -200,9 +232,10 @@ define( }; /** + * For given objects, populate column metadata and table headers. * @private - * @param objects - * @returns {*} + * @param {module:openmct.DomainObject[]} objects the domain objects for + * which columns should be populated */ TelemetryTableController.prototype.loadColumns = function (objects) { var telemetryApi = this.openmct.telemetry; @@ -220,25 +253,28 @@ define( this.filterColumns(); + // Default to no sort on underlying telemetry collection. Sorting + // is necessary to do bounds filtering, but this is only possible + // if data matches selected time system + this.telemetry.sort(undefined); + var timeSystem = this.openmct.conductor.timeSystem(); if (timeSystem) { this.sortByTimeSystem(timeSystem); } - if (!this.telemetry.sortColumn && domainColumns.length > 0) { - this.telemetry.sort(domainColumns[0].name + '.value'); - } - } return objects; }; /** + * Request telemetry data from an historical store for given objects. * @private - * @param objects The domain objects to request telemetry for - * @returns {*|{configFile}|app|boolean|Route|Object} + * @param {object[]} The domain objects to request telemetry for + * @returns {Promise} resolved when historical data is available */ TelemetryTableController.prototype.getHistoricalData = function (objects) { + var self = this; var openmct = this.openmct; var bounds = openmct.conductor.bounds(); var scope = this.$scope; @@ -247,15 +283,22 @@ define( var requestTime = this.lastRequestTime = Date.now(); var telemetryCollection = this.telemetry; - return new Promise(function (resolve, reject){ - function finishProcessing(){ - telemetryCollection.addAll(rowData); + var promise = new Promise(function (resolve, reject) { + /* + * On completion of batched processing, set the rows on scope + */ + function finishProcessing() { + telemetryCollection.add(rowData); scope.rows = telemetryCollection.telemetry; scope.loading = false; + resolve(scope.rows); } - function processData(historicalData, index, limitEvaluator){ + /* + * Process a batch of historical data + */ + function processData(historicalData, index, limitEvaluator) { if (index >= historicalData.length) { processedObjects++; @@ -263,51 +306,57 @@ define( finishProcessing(); } } else { - rowData = rowData.concat(historicalData.slice(index, index + this.batchSize) - .map(this.table.getRowValues.bind(this.table, limitEvaluator))); + rowData = rowData.concat(historicalData.slice(index, index + self.batchSize) + .map(self.table.getRowValues.bind(self.table, limitEvaluator))); - this.timeoutHandle = this.$timeout(processData.bind( - this, - historicalData, - index + this.batchSize, - limitEvaluator - )); + /* + Use timeout to yield process to other UI activities. On + return, process next batch + */ + self.timeoutHandle = self.$timeout(function () { + processData(historicalData, index + self.batchSize, limitEvaluator); + }); } } function makeTableRows(object, historicalData) { - // Only process one request at a time - if (requestTime === this.lastRequestTime) { + // Only process the most recent request + if (requestTime === self.lastRequestTime) { var limitEvaluator = openmct.telemetry.limitEvaluator(object); - processData.call(this, historicalData, 0, limitEvaluator); + processData(historicalData, 0, limitEvaluator); } else { resolve(rowData); } } - function requestData (object) { + /* + Use the telemetry API to request telemetry for a given object + */ + function requestData(object) { return openmct.telemetry.request(object, { start: bounds.start, end: bounds.end - }).then(makeTableRows.bind(this, object)) + }).then(makeTableRows.bind(undefined, object)) .catch(reject); } this.$timeout.cancel(this.timeoutHandle); - if (objects.length > 0){ - objects.forEach(requestData.bind(this)); + if (objects.length > 0) { + objects.forEach(requestData); } else { scope.loading = false; resolve([]); } }.bind(this)); + + return promise; }; /** + * Subscribe to real-time data for the given objects. * @private - * @param objects - * @returns {*} + * @param {object[]} objects The objects to subscribe to. */ TelemetryTableController.prototype.subscribeToNewData = function (objects) { var telemetryApi = this.openmct.telemetry; @@ -317,6 +366,8 @@ define( var maxRows = Number.MAX_VALUE; var limitEvaluator; var added = false; + var scope = this.$scope; + var table = this.table; this.subscriptions.forEach(function (subscription) { subscription(); @@ -325,20 +376,20 @@ define( function newData(domainObject, datum) { limitEvaluator = telemetryApi.limitEvaluator(domainObject); - added = telemetryCollection.add(this.table.getRowValues(limitEvaluator, datum)); + added = telemetryCollection.add([table.getRowValues(limitEvaluator, datum)]); //Inform table that a new row has been added - if (this.$scope.rows.length > maxRows) { - this.$scope.$broadcast('remove:rows', this.$scope.rows[0]); - this.$scope.rows.shift(); + if (scope.rows.length > maxRows) { + scope.$broadcast('remove:rows', scope.rows[0]); + scope.rows.shift(); } - if (!this.$scope.loading && added) { - this.$scope.$broadcast('add:row', - this.$scope.rows.length - 1); + if (!scope.loading && added) { + scope.$broadcast('add:row', + scope.rows.length - 1); } } - objects.forEach(function (object){ + objects.forEach(function (object) { this.subscriptions.push( telemetryApi.subscribe(object, newData.bind(this, object), {})); }.bind(this)); @@ -346,6 +397,12 @@ define( return objects; }; + /** + * Request historical data, and subscribe to for real-time data. + * @private + * @returns {Promise} A promise that is resolved once subscription is + * established, and historical telemetry is received and processed. + */ TelemetryTableController.prototype.getData = function () { var telemetryApi = this.openmct.telemetry; var compositionApi = this.openmct.composition; @@ -359,41 +416,37 @@ define( function error(e) { scope.loading = false; - console.error(e); + console.error(e.stack); } - function filterForTelemetry(objects){ + function filterForTelemetry(objects) { return objects.filter(telemetryApi.canProvideTelemetry.bind(telemetryApi)); } function getDomainObjects() { - return new Promise(function (resolve, reject){ - var objects = [newObject]; - var composition = compositionApi.get(newObject); + var objects = [newObject]; + var composition = compositionApi.get(newObject); - if (composition) { - composition - .load() - .then(function (children) { - return objects.concat(children); - }) - .then(resolve) - .catch(reject); - } else { - return resolve(objects); - } - }); + if (composition) { + return composition + .load() + .then(function (children) { + return objects.concat(children); + }); + } else { + return Promise.resolve(objects); + } } scope.headers = []; scope.rows = []; - getDomainObjects() + return getDomainObjects() .then(filterForTelemetry) .then(this.loadColumns) .then(this.subscribeToNewData) .then(this.getHistoricalData) - .catch(error) + .catch(error); }; /** diff --git a/platform/features/table/test/DomainColumnSpec.js b/platform/features/table/test/DomainColumnSpec.js deleted file mode 100644 index 3c144b8427..0000000000 --- a/platform/features/table/test/DomainColumnSpec.js +++ /dev/null @@ -1,80 +0,0 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2016, 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. - *****************************************************************************/ - -/** - * MergeModelsSpec. Created by vwoeltje on 11/6/14. - */ -define( - ["../src/DomainColumn"], - function (DomainColumn) { - - var TEST_DOMAIN_VALUE = "some formatted domain value"; - - describe("A domain column", function () { - var mockDatum, - testMetadata, - mockFormatter, - column; - - beforeEach(function () { - - mockFormatter = jasmine.createSpyObj( - "formatter", - ["formatDomainValue", "formatRangeValue"] - ); - testMetadata = { - key: "testKey", - name: "Test Name", - format: "Test Format" - }; - mockFormatter.formatDomainValue.andReturn(TEST_DOMAIN_VALUE); - - column = new DomainColumn(testMetadata, mockFormatter); - }); - - it("reports a column header from domain metadata", function () { - expect(column.getTitle()).toEqual("Test Name"); - }); - - describe("when given a datum", function () { - beforeEach(function () { - mockDatum = { - testKey: "testKeyValue" - }; - }); - - it("looks up data from the given datum", function () { - expect(column.getValue(undefined, mockDatum)) - .toEqual({ text: TEST_DOMAIN_VALUE }); - }); - - it("uses formatter to format domain values as requested", function () { - column.getValue(undefined, mockDatum); - expect(mockFormatter.formatDomainValue) - .toHaveBeenCalledWith("testKeyValue", "Test Format"); - }); - - }); - - }); - } -); diff --git a/platform/features/table/test/NameColumnSpec.js b/platform/features/table/test/NameColumnSpec.js deleted file mode 100644 index 13e858c2ed..0000000000 --- a/platform/features/table/test/NameColumnSpec.js +++ /dev/null @@ -1,56 +0,0 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2016, 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. - *****************************************************************************/ - -/** - * MergeModelsSpec. Created by vwoeltje on 11/6/14. - */ -define( - ["../src/NameColumn"], - function (NameColumn) { - - describe("A name column", function () { - var mockDomainObject, - column; - - beforeEach(function () { - mockDomainObject = jasmine.createSpyObj( - "domainObject", - ["getModel"] - ); - mockDomainObject.getModel.andReturn({ - name: "Test object name" - }); - column = new NameColumn(); - }); - - it("reports a column header", function () { - expect(column.getTitle()).toEqual("Name"); - }); - - it("looks up name from an object's model", function () { - expect(column.getValue(mockDomainObject).text) - .toEqual("Test object name"); - }); - - }); - } -); diff --git a/platform/features/table/test/RangeColumnSpec.js b/platform/features/table/test/RangeColumnSpec.js deleted file mode 100644 index 473f26ae56..0000000000 --- a/platform/features/table/test/RangeColumnSpec.js +++ /dev/null @@ -1,74 +0,0 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2016, 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. - *****************************************************************************/ - -/** - * MergeModelsSpec. Created by vwoeltje on 11/6/14. - */ -define( - ["../src/RangeColumn"], - function (RangeColumn) { - - var TEST_RANGE_VALUE = "some formatted range value"; - - describe("A range column", function () { - var testDatum, - testMetadata, - mockFormatter, - mockDomainObject, - column; - - beforeEach(function () { - testDatum = { testKey: 123, otherKey: 456 }; - mockFormatter = jasmine.createSpyObj( - "formatter", - ["formatDomainValue", "formatRangeValue"] - ); - testMetadata = { - key: "testKey", - name: "Test Name" - }; - mockDomainObject = jasmine.createSpyObj( - "domainObject", - ["getModel", "getCapability"] - ); - mockFormatter.formatRangeValue.andReturn(TEST_RANGE_VALUE); - - column = new RangeColumn(testMetadata, mockFormatter); - }); - - it("reports a column header from range metadata", function () { - expect(column.getTitle()).toEqual("Test Name"); - }); - - it("formats range values as numbers", function () { - expect(column.getValue(mockDomainObject, testDatum).text) - .toEqual(TEST_RANGE_VALUE); - - // Make sure that service interactions were as expected - expect(mockFormatter.formatRangeValue) - .toHaveBeenCalledWith(testDatum.testKey); - expect(mockFormatter.formatDomainValue) - .not.toHaveBeenCalled(); - }); - }); - } -); diff --git a/platform/features/table/test/TableConfigurationSpec.js b/platform/features/table/test/TableConfigurationSpec.js index db90cc1c62..af4f2ee2a8 100644 --- a/platform/features/table/test/TableConfigurationSpec.js +++ b/platform/features/table/test/TableConfigurationSpec.js @@ -22,13 +22,14 @@ define( [ - "../src/TableConfiguration", - "../src/DomainColumn" + "../src/TableConfiguration" ], - function (Table, DomainColumn) { + function (Table) { describe("A table", function () { var mockDomainObject, + mockAPI, + mockTelemetryAPI, mockTelemetryFormatter, table, mockModel; @@ -41,90 +42,63 @@ define( mockDomainObject.getModel.andReturn(mockModel); mockTelemetryFormatter = jasmine.createSpyObj('telemetryFormatter', [ - 'formatDomainValue', - 'formatRangeValue' + 'format' ]); - mockTelemetryFormatter.formatDomainValue.andCallFake(function (valueIn) { - return valueIn; - }); - mockTelemetryFormatter.formatRangeValue.andCallFake(function (valueIn) { + mockTelemetryFormatter.format.andCallFake(function (valueIn) { return valueIn; }); - table = new Table(mockDomainObject, mockTelemetryFormatter); - }); + mockTelemetryAPI = jasmine.createSpyObj('telemetryAPI', [ + 'getValueFormatter' + ]); + mockAPI = { + telemetry: mockTelemetryAPI + }; + mockTelemetryAPI.getValueFormatter.andReturn(mockTelemetryFormatter); - it("Add column with no index adds new column to the end", function () { - var firstColumn = {title: 'First Column'}, - secondColumn = {title: 'Second Column'}, - thirdColumn = {title: 'Third Column'}; - - table.addColumn(firstColumn); - table.addColumn(secondColumn); - table.addColumn(thirdColumn); - - expect(table.columns).toBeDefined(); - expect(table.columns.length).toBe(3); - expect(table.columns[0]).toBe(firstColumn); - expect(table.columns[1]).toBe(secondColumn); - expect(table.columns[2]).toBe(thirdColumn); - }); - - it("Add column with index adds new column at the specified" + - " position", function () { - var firstColumn = {title: 'First Column'}, - secondColumn = {title: 'Second Column'}, - thirdColumn = {title: 'Third Column'}; - - table.addColumn(firstColumn); - table.addColumn(thirdColumn); - table.addColumn(secondColumn, 1); - - expect(table.columns).toBeDefined(); - expect(table.columns.length).toBe(3); - expect(table.columns[0]).toBe(firstColumn); - expect(table.columns[1]).toBe(secondColumn); - expect(table.columns[2]).toBe(thirdColumn); + table = new Table(mockDomainObject, mockAPI); }); describe("Building columns from telemetry metadata", function () { - var metadata = [{ - ranges: [ - { - name: 'Range 1', - key: 'range1' - }, - { - name: 'Range 2', - key: 'range2' + var metadata = [ + { + name: 'Range 1', + key: 'range1', + hints: { + y: 1 } - ], - domains: [ - { - name: 'Domain 1', - key: 'domain1', - format: 'utc' - }, - { - name: 'Domain 2', - key: 'domain2', - format: 'utc' + }, + { + name: 'Range 2', + key: 'range2', + hints: { + y: 2 } - ] - }]; + }, + { + name: 'Domain 1', + key: 'domain1', + format: 'utc', + hints: { + x: 1 + } + }, + { + name: 'Domain 2', + key: 'domain2', + format: 'utc', + hints: { + x: 2 + } + } + ]; beforeEach(function () { table.populateColumns(metadata); }); it("populates columns", function () { - expect(table.columns.length).toBe(5); - }); - - it("Build columns populates columns with domains to the left", function () { - expect(table.columns[1] instanceof DomainColumn).toBeTruthy(); - expect(table.columns[2] instanceof DomainColumn).toBeTruthy(); - expect(table.columns[3] instanceof DomainColumn).toBeFalsy(); + expect(table.columns.length).toBe(4); }); it("Produces headers for each column based on title", function () { @@ -133,7 +107,7 @@ define( spyOn(firstColumn, 'getTitle'); headers = table.getHeaders(); - expect(headers.length).toBe(5); + expect(headers.length).toBe(4); expect(firstColumn.getTitle).toHaveBeenCalled(); }); @@ -170,23 +144,33 @@ define( beforeEach(function () { datum = { - 'range1': 'range 1 value', - 'range2': 'range 2 value', + 'range1': 10, + 'range2': 20, 'domain1': 0, 'domain2': 1 }; - rowValues = table.getRowValues(mockDomainObject, datum); + var limitEvaluator = { + evaluate: function () { + return { + "cssClass": "alarm-class" + }; + } + }; + rowValues = table.getRowValues(limitEvaluator, datum); }); it("Returns a value for every column", function () { expect(rowValues['Range 1'].text).toBeDefined(); - expect(rowValues['Range 1'].text).toEqual('range 1' + - ' value'); + expect(rowValues['Range 1'].text).toEqual(10); }); - it("Uses the telemetry formatter to appropriately format" + + it("Applies appropriate css class if limit violated.", function () { + expect(rowValues['Range 1'].cssClass).toEqual("alarm-class"); + }); + + it("Uses telemetry formatter to appropriately format" + " telemetry values", function () { - expect(mockTelemetryFormatter.formatRangeValue).toHaveBeenCalled(); + expect(mockTelemetryFormatter.format).toHaveBeenCalled(); }); }); }); diff --git a/platform/features/table/test/TelemetryCollectionSpec.js b/platform/features/table/test/TelemetryCollectionSpec.js new file mode 100644 index 0000000000..014e5c684e --- /dev/null +++ b/platform/features/table/test/TelemetryCollectionSpec.js @@ -0,0 +1,191 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2016, 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/TelemetryCollection" + ], + function (TelemetryCollection) { + + describe("A telemetry collection", function () { + + var collection; + var telemetryObjects; + var ms; + var integerTextMap = ["ZERO", "ONE", "TWO", "THREE", "FOUR", "FIVE", + "SIX", "SEVEN", "EIGHT", "NINE", "TEN", "ELEVEN"]; + + beforeEach(function () { + telemetryObjects = [0,9,2,4,7,8,5,1,3,6].map(function (number) { + ms = number * 1000; + return { + timestamp: ms, + value: { + integer: number, + text: integerTextMap[number] + } + }; + }); + collection = new TelemetryCollection(); + }); + + it("Sorts inserted telemetry by specified field", + function () { + collection.sort('value.integer'); + collection.add(telemetryObjects); + expect(collection.telemetry[0].value.integer).toBe(0); + expect(collection.telemetry[1].value.integer).toBe(1); + expect(collection.telemetry[2].value.integer).toBe(2); + expect(collection.telemetry[3].value.integer).toBe(3); + + collection.sort('value.text'); + expect(collection.telemetry[0].value.text).toBe("EIGHT"); + expect(collection.telemetry[1].value.text).toBe("FIVE"); + expect(collection.telemetry[2].value.text).toBe("FOUR"); + expect(collection.telemetry[3].value.text).toBe("NINE"); + } + ); + + describe("on bounds change", function () { + var discardedCallback; + + beforeEach(function () { + discardedCallback = jasmine.createSpy("discarded"); + collection.on("discarded", discardedCallback); + collection.sort("timestamp"); + collection.add(telemetryObjects); + collection.bounds({start: 5000, end: 8000}); + }); + + + it("emits an event indicating that telemetry has " + + "been discarded", function () { + expect(discardedCallback).toHaveBeenCalled(); + }); + + it("discards telemetry data with a time stamp " + + "before specified start bound", function () { + var discarded = discardedCallback.mostRecentCall.args[0]; + + // Expect 5 because as an optimization, the TelemetryCollection + // will not consider telemetry values that exceed the upper + // bounds. Arbitrary bounds changes in which the end bound is + // decreased is assumed to require a new historical query, and + // hence re-population of the collection anyway + expect(discarded.length).toBe(5); + expect(discarded[0].value.integer).toBe(0); + expect(discarded[1].value.integer).toBe(1); + expect(discarded[4].value.integer).toBe(4); + }); + }); + + describe("when adding telemetry to a collection", function () { + var addedCallback; + beforeEach(function () { + collection.sort("timestamp"); + collection.add(telemetryObjects); + addedCallback = jasmine.createSpy("added"); + collection.on("added", addedCallback); + }); + + it("emits an event", + function () { + var addedObject = { + timestamp: 10000, + value: { + integer: 10, + text: integerTextMap[10] + } + }; + collection.add([addedObject]); + expect(addedCallback).toHaveBeenCalledWith([addedObject]); + } + ); + it("inserts in the correct order", + function () { + var addedObjectA = { + timestamp: 10000, + value: { + integer: 10, + text: integerTextMap[10] + } + }; + var addedObjectB = { + timestamp: 11000, + value: { + integer: 11, + text: integerTextMap[11] + } + }; + collection.add([addedObjectB, addedObjectA]); + + expect(collection.telemetry[11]).toBe(addedObjectB); + } + ); + }); + + describe("buffers telemetry", function () { + var addedObjectA; + var addedObjectB; + + beforeEach(function () { + collection.sort("timestamp"); + collection.add(telemetryObjects); + + addedObjectA = { + timestamp: 10000, + value: { + integer: 10, + text: integerTextMap[10] + } + }; + addedObjectB = { + timestamp: 11000, + value: { + integer: 11, + text: integerTextMap[11] + } + }; + + collection.bounds({start: 0, end: 10000}); + collection.add([addedObjectA, addedObjectB]); + }); + it("when it falls outside of bounds", function () { + expect(collection.highBuffer).toBeDefined(); + expect(collection.highBuffer.length).toBe(1); + expect(collection.highBuffer[0]).toBe(addedObjectB); + }); + it("and adds it to collection when it falls within bounds", function () { + expect(collection.telemetry.length).toBe(11); + collection.bounds({start: 0, end: 11000}); + expect(collection.telemetry.length).toBe(12); + expect(collection.telemetry[11]).toBe(addedObjectB); + }); + it("and removes it from the buffer when it falls within bounds", function () { + expect(collection.highBuffer.length).toBe(1); + collection.bounds({start: 0, end: 11000}); + expect(collection.highBuffer.length).toBe(0); + }); + }); + }); + } +); diff --git a/platform/features/table/test/controllers/HistoricalTableControllerSpec.js b/platform/features/table/test/controllers/HistoricalTableControllerSpec.js deleted file mode 100644 index 39f7d1a8f5..0000000000 --- a/platform/features/table/test/controllers/HistoricalTableControllerSpec.js +++ /dev/null @@ -1,380 +0,0 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2016, 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/controllers/HistoricalTableController" - ], - function (TableController) { - - describe('The Table Controller', function () { - var mockScope, - mockTelemetryHandler, - mockTelemetryHandle, - mockTelemetryFormatter, - mockDomainObject, - mockTable, - mockConfiguration, - mockAngularTimeout, - mockTimeoutHandle, - watches, - mockConductor, - controller; - - function promise(value) { - return { - then: function (callback) { - return promise(callback(value)); - } - }; - } - - function getCallback(target, event) { - return target.calls.filter(function (call) { - return call.args[0] === event; - })[0].args[1]; - } - - beforeEach(function () { - watches = {}; - mockScope = jasmine.createSpyObj('scope', [ - '$on', - '$watch', - '$watchCollection' - ]); - - mockScope.$on.andCallFake(function (expression, callback) { - watches[expression] = callback; - }); - mockScope.$watch.andCallFake(function (expression, callback) { - watches[expression] = callback; - }); - mockScope.$watchCollection.andCallFake(function (expression, callback) { - watches[expression] = callback; - }); - - mockTimeoutHandle = jasmine.createSpy("timeoutHandle"); - mockAngularTimeout = jasmine.createSpy("$timeout"); - mockAngularTimeout.andReturn(mockTimeoutHandle); - mockAngularTimeout.cancel = jasmine.createSpy("cancelTimeout"); - - mockConfiguration = { - 'range1': true, - 'range2': true, - 'domain1': true - }; - - mockTable = jasmine.createSpyObj('table', - [ - 'populateColumns', - 'buildColumnConfiguration', - 'getRowValues', - 'saveColumnConfiguration' - ] - ); - mockTable.columns = []; - mockTable.buildColumnConfiguration.andReturn(mockConfiguration); - - mockDomainObject = jasmine.createSpyObj('domainObject', [ - 'getCapability', - 'useCapability', - 'getModel' - ]); - mockDomainObject.getModel.andReturn({}); - - mockScope.domainObject = mockDomainObject; - - mockTelemetryHandle = jasmine.createSpyObj('telemetryHandle', [ - 'request', - 'promiseTelemetryObjects', - 'getTelemetryObjects', - 'getMetadata', - 'getSeries', - 'unsubscribe', - 'makeDatum' - ]); - mockTelemetryHandle.promiseTelemetryObjects.andReturn(promise(undefined)); - mockTelemetryHandle.request.andReturn(promise(undefined)); - mockTelemetryHandle.getTelemetryObjects.andReturn([]); - mockTelemetryHandle.getMetadata.andReturn([]); - - mockTelemetryHandler = jasmine.createSpyObj('telemetryHandler', [ - 'handle' - ]); - mockTelemetryHandler.handle.andReturn(mockTelemetryHandle); - - mockConductor = jasmine.createSpyObj("conductor", [ - "timeSystem", - "on", - "off" - ]); - - controller = new TableController(mockScope, mockTelemetryHandler, - mockTelemetryFormatter, mockAngularTimeout, {conductor: mockConductor}); - - controller.table = mockTable; - controller.handle = mockTelemetryHandle; - }); - - it('subscribes to telemetry handler for telemetry updates', function () { - controller.subscribe(); - expect(mockTelemetryHandler.handle).toHaveBeenCalled(); - expect(mockTelemetryHandle.request).toHaveBeenCalled(); - }); - - it('Unsubscribes from telemetry when scope is destroyed', function () { - controller.handle = mockTelemetryHandle; - watches.$destroy(); - expect(mockTelemetryHandle.unsubscribe).toHaveBeenCalled(); - }); - - describe('makes use of the table', function () { - - it('to create column definitions from telemetry' + - ' metadata', function () { - controller.setup(); - expect(mockTable.populateColumns).toHaveBeenCalled(); - }); - - it('to create column configuration, which is written to the' + - ' object model', function () { - controller.setup(); - expect(mockTable.buildColumnConfiguration).toHaveBeenCalled(); - }); - }); - - it('updates the rows on scope when historical telemetry is received', function () { - var mockSeries = { - getPointCount: function () { - return 5; - }, - getDomainValue: function () { - return 'Domain Value'; - }, - getRangeValue: function () { - return 'Range Value'; - } - }, - mockRow = {'domain': 'Domain Value', 'range': 'Range' + - ' Value'}; - - mockTelemetryHandle.makeDatum.andCallFake(function () { - return mockRow; - }); - mockTable.getRowValues.andReturn(mockRow); - mockTelemetryHandle.getTelemetryObjects.andReturn([mockDomainObject]); - mockTelemetryHandle.getSeries.andReturn(mockSeries); - - controller.addHistoricalData(mockDomainObject, mockSeries); - - // Angular timeout is called a minumum of twice, regardless - // of batch size used. - expect(mockAngularTimeout).toHaveBeenCalled(); - mockAngularTimeout.mostRecentCall.args[0](); - expect(mockAngularTimeout.calls.length).toEqual(2); - mockAngularTimeout.mostRecentCall.args[0](); - - expect(controller.$scope.rows.length).toBe(5); - expect(controller.$scope.rows[0]).toBe(mockRow); - }); - - it('filters the visible columns based on configuration', function () { - controller.filterColumns(); - expect(controller.$scope.headers.length).toBe(3); - expect(controller.$scope.headers[2]).toEqual('domain1'); - - mockConfiguration.domain1 = false; - controller.filterColumns(); - expect(controller.$scope.headers.length).toBe(2); - expect(controller.$scope.headers[2]).toBeUndefined(); - }); - - describe('creates event listeners', function () { - beforeEach(function () { - spyOn(controller, 'subscribe'); - spyOn(controller, 'filterColumns'); - }); - - it('triggers telemetry subscription update when domain' + - ' object changes', function () { - controller.registerChangeListeners(); - //'watches' object is populated by fake scope watch and - // watchCollection functions defined above - expect(watches.domainObject).toBeDefined(); - watches.domainObject(mockDomainObject); - expect(controller.subscribe).toHaveBeenCalled(); - }); - - it('triggers telemetry subscription update when domain' + - ' object composition changes', function () { - controller.registerChangeListeners(); - expect(watches['domainObject.getModel().composition']).toBeDefined(); - watches['domainObject.getModel().composition']([], []); - expect(controller.subscribe).toHaveBeenCalled(); - }); - - it('triggers telemetry subscription update when time' + - ' conductor bounds change', function () { - controller.registerChangeListeners(); - expect(watches['telemetry:display:bounds']).toBeDefined(); - watches['telemetry:display:bounds'](); - expect(controller.subscribe).toHaveBeenCalled(); - }); - - it('triggers refiltering of the columns when configuration' + - ' changes', function () { - controller.setup(); - expect(watches['domainObject.getModel().configuration.table.columns']).toBeDefined(); - watches['domainObject.getModel().configuration.table.columns'](); - expect(controller.filterColumns).toHaveBeenCalled(); - }); - - }); - describe('After populating columns', function () { - var metadata; - beforeEach(function () { - metadata = [{domains: [{name: 'time domain 1'}, {name: 'time domain 2'}]}, {domains: [{name: 'time domain 3'}, {name: 'time domain 4'}]}]; - controller.populateColumns(metadata); - }); - - it('Automatically identifies time columns', function () { - expect(controller.timeColumns.length).toBe(4); - expect(controller.timeColumns[0]).toBe('time domain 1'); - }); - - it('Automatically sorts by time column that matches current' + - ' time system', function () { - var key = 'time_domain_1', - name = 'time domain 1', - mockTimeSystem = { - metadata: { - key: key - } - }; - - mockTable.columns = [ - { - domainMetadata: { - key: key - }, - getTitle: function () { - return name; - } - }, - { - domainMetadata: { - key: 'anotherColumn' - }, - getTitle: function () { - return 'some other column'; - } - }, - { - domainMetadata: { - key: 'thirdColumn' - }, - getTitle: function () { - return 'a third column'; - } - } - ]; - - expect(mockConductor.on).toHaveBeenCalledWith('timeSystem', jasmine.any(Function)); - getCallback(mockConductor.on, 'timeSystem')(mockTimeSystem); - expect(controller.$scope.defaultSort).toBe(name); - }); - }); - describe('Yields thread', function () { - var mockSeries, - mockRow; - - beforeEach(function () { - mockSeries = { - getPointCount: function () { - return 5; - }, - getDomainValue: function () { - return 'Domain Value'; - }, - getRangeValue: function () { - return 'Range Value'; - } - }; - mockRow = {'domain': 'Domain Value', 'range': 'Range Value'}; - - mockTelemetryHandle.makeDatum.andCallFake(function () { - return mockRow; - }); - mockTable.getRowValues.andReturn(mockRow); - mockTelemetryHandle.getTelemetryObjects.andReturn([mockDomainObject]); - mockTelemetryHandle.getSeries.andReturn(mockSeries); - }); - it('when row count exceeds batch size', function () { - controller.batchSize = 3; - controller.addHistoricalData(mockDomainObject, mockSeries); - - //Timeout is called a minimum of two times - expect(mockAngularTimeout).toHaveBeenCalled(); - mockAngularTimeout.mostRecentCall.args[0](); - - expect(mockAngularTimeout.calls.length).toEqual(2); - mockAngularTimeout.mostRecentCall.args[0](); - - //Because it yields, timeout will have been called a - // third time for the batch. - expect(mockAngularTimeout.calls.length).toEqual(3); - mockAngularTimeout.mostRecentCall.args[0](); - - expect(controller.$scope.rows.length).toBe(5); - expect(controller.$scope.rows[0]).toBe(mockRow); - }); - it('cancelling any outstanding timeouts', function () { - controller.batchSize = 3; - controller.addHistoricalData(mockDomainObject, mockSeries); - - expect(mockAngularTimeout).toHaveBeenCalled(); - mockAngularTimeout.mostRecentCall.args[0](); - - controller.addHistoricalData(mockDomainObject, mockSeries); - - expect(mockAngularTimeout.cancel).toHaveBeenCalledWith(mockTimeoutHandle); - }); - it('cancels timeout on scope destruction', function () { - controller.batchSize = 3; - controller.addHistoricalData(mockDomainObject, mockSeries); - - //Destroy is used by parent class as well, so multiple - // calls are made to scope.$on - var destroyCalls = mockScope.$on.calls.filter(function (call) { - return call.args[0] === '$destroy'; - }); - //Call destroy function - expect(destroyCalls.length).toEqual(2); - - destroyCalls[0].args[1](); - expect(mockAngularTimeout.cancel).toHaveBeenCalledWith(mockTimeoutHandle); - - }); - }); - }); - } -); diff --git a/platform/features/table/test/controllers/MCTTableControllerSpec.js b/platform/features/table/test/controllers/MCTTableControllerSpec.js index 970ca72047..f57b981c50 100644 --- a/platform/features/table/test/controllers/MCTTableControllerSpec.js +++ b/platform/features/table/test/controllers/MCTTableControllerSpec.js @@ -39,21 +39,13 @@ define( var controller, mockScope, watches, - mockTimeout, + mockWindow, mockElement, mockExportService, mockConductor, mockFormatService, mockFormat; - function promise(value) { - return { - then: function (callback) { - return promise(callback(value)); - } - }; - } - function getCallback(target, event) { return target.calls.filter(function (call) { return call.args[0] === event; @@ -66,7 +58,8 @@ define( mockScope = jasmine.createSpyObj('scope', [ '$watch', '$on', - '$watchCollection' + '$watchCollection', + '$digest' ]); mockScope.$watchCollection.andCallFake(function (event, callback) { watches[event] = callback; @@ -86,8 +79,11 @@ define( ]); mockScope.displayHeaders = true; - mockTimeout = jasmine.createSpy('$timeout'); - mockTimeout.andReturn(promise(undefined)); + mockWindow = jasmine.createSpyObj('$window', ['requestAnimationFrame']); + mockWindow.requestAnimationFrame.andCallFake(function (f) { + return f(); + }); + mockFormat = jasmine.createSpyObj('formatter', [ 'parse', 'format' @@ -99,7 +95,7 @@ define( controller = new MCTTableController( mockScope, - mockTimeout, + mockWindow, mockElement, mockExportService, mockFormatService, @@ -114,12 +110,12 @@ define( expect(mockScope.$watch).toHaveBeenCalledWith('rows', jasmine.any(Function)); }); - it('destroys listeners on destruction', function () { - expect(mockScope.$on).toHaveBeenCalledWith('$destroy', controller.destroyConductorListeners); + it('unregisters listeners on destruction', function () { + expect(mockScope.$on).toHaveBeenCalledWith('$destroy', jasmine.any(Function)); getCallback(mockScope.$on, '$destroy')(); expect(mockConductor.off).toHaveBeenCalledWith('timeSystem', controller.changeTimeSystem); - expect(mockConductor.off).toHaveBeenCalledWith('timeOfInterest', controller.setTimeOfInterest); + expect(mockConductor.off).toHaveBeenCalledWith('timeOfInterest', controller.changeTimeOfInterest); expect(mockConductor.off).toHaveBeenCalledWith('bounds', controller.changeBounds); }); @@ -233,9 +229,20 @@ define( //Mock setting the rows on scope var rowsCallback = getCallback(mockScope.$watch, 'rows'); - rowsCallback(rowsAsc); + var setRowsPromise = rowsCallback(rowsAsc); + var promiseResolved = false; + setRowsPromise.then(function () { + promiseResolved = true; + }); + + waitsFor(function () { + return promiseResolved; + }, "promise to resolve", 100); + + runs(function () { + expect(mockScope.toiRowIndex).toBe(2); + }); - expect(mockScope.toiRowIndex).toBe(2); }); }); @@ -287,7 +294,7 @@ define( }); it('Supports adding rows individually', function () { - var addRowFunc = getCallback(mockScope.$on, 'add:row'), + var addRowFunc = getCallback(mockScope.$on, 'add:rows'), row4 = { 'col1': {'text': 'row3 col1'}, 'col2': {'text': 'ghi'}, @@ -296,15 +303,15 @@ define( controller.setRows(testRows); expect(mockScope.displayRows.length).toBe(3); testRows.push(row4); - addRowFunc(undefined, 3); + addRowFunc(undefined, [row4]); expect(mockScope.displayRows.length).toBe(4); }); it('Supports removing rows individually', function () { - var removeRowFunc = getCallback(mockScope.$on, 'remove:row'); + var removeRowFunc = getCallback(mockScope.$on, 'remove:rows'); controller.setRows(testRows); expect(mockScope.displayRows.length).toBe(3); - removeRowFunc(undefined, 2); + removeRowFunc(undefined, [testRows[2]]); expect(mockScope.displayRows.length).toBe(2); expect(controller.setVisibleRows).toHaveBeenCalled(); }); @@ -366,7 +373,7 @@ define( it('Allows sort column to be changed externally by ' + 'setting or changing sortBy attribute', function () { mockScope.displayRows = testRows; - var sortByCB = getCallback(mockScope.$watch, 'sortColumn'); + var sortByCB = getCallback(mockScope.$watch, 'defaultSort'); sortByCB('col2'); expect(mockScope.sortDirection).toEqual('asc'); @@ -381,10 +388,21 @@ define( it('updates visible rows in scope', function () { var oldRows; mockScope.rows = testRows; - controller.setRows(testRows); + var setRowsPromise = controller.setRows(testRows); + var promiseResolved = false; + setRowsPromise.then(function () { + promiseResolved = true; + }); oldRows = mockScope.visibleRows; mockScope.toggleSort('col2'); - expect(mockScope.visibleRows).not.toEqual(oldRows); + + waitsFor(function () { + return promiseResolved; + }, "promise to resolve", 100); + + runs(function () { + expect(mockScope.visibleRows).not.toEqual(oldRows); + }); }); it('correctly sorts rows of differing types', function () { @@ -464,21 +482,10 @@ define( mockScope.displayRows = controller.sortRows(testRows.slice(0)); - mockScope.rows.push(row4); - controller.addRows(undefined, mockScope.rows.length - 1); + controller.addRows(undefined, [row4, row5, row6, row6]); expect(mockScope.displayRows[0].col2.text).toEqual('xyz'); - - mockScope.rows.push(row5); - controller.addRows(undefined, mockScope.rows.length - 1); - expect(mockScope.displayRows[4].col2.text).toEqual('aaa'); - - mockScope.rows.push(row6); - controller.addRows(undefined, mockScope.rows.length - 1); - expect(mockScope.displayRows[2].col2.text).toEqual('ggg'); - - //Add a duplicate row - mockScope.rows.push(row6); - controller.addRows(undefined, mockScope.rows.length - 1); + expect(mockScope.displayRows[6].col2.text).toEqual('aaa'); + //Added a duplicate row expect(mockScope.displayRows[2].col2.text).toEqual('ggg'); expect(mockScope.displayRows[3].col2.text).toEqual('ggg'); }); @@ -493,13 +500,11 @@ define( mockScope.displayRows = controller.sortRows(testRows.slice(0)); mockScope.displayRows = controller.filterRows(testRows); - mockScope.rows.push(row5); - controller.addRows(undefined, mockScope.rows.length - 1); + controller.addRows(undefined, [row5]); expect(mockScope.displayRows.length).toBe(2); expect(mockScope.displayRows[1].col2.text).toEqual('aaa'); - mockScope.rows.push(row6); - controller.addRows(undefined, mockScope.rows.length - 1); + controller.addRows(undefined, [row6]); expect(mockScope.displayRows.length).toBe(2); //Row was not added because does not match filter }); @@ -512,12 +517,10 @@ define( mockScope.displayRows = testRows.slice(0); - mockScope.rows.push(row5); - controller.addRows(undefined, mockScope.rows.length - 1); + controller.addRows(undefined, [row5]); expect(mockScope.displayRows[3].col2.text).toEqual('aaa'); - mockScope.rows.push(row6); - controller.addRows(undefined, mockScope.rows.length - 1); + controller.addRows(undefined, [row6]); expect(mockScope.displayRows[4].col2.text).toEqual('ggg'); }); @@ -535,8 +538,7 @@ define( mockScope.displayRows = testRows.slice(0); - mockScope.rows.push(row7); - controller.addRows(undefined, mockScope.rows.length - 1); + controller.addRows(undefined, [row7]); expect(controller.$scope.sizingRow.col2).toEqual({text: 'some longer string'}); }); diff --git a/platform/features/table/test/controllers/RealtimeTableControllerSpec.js b/platform/features/table/test/controllers/RealtimeTableControllerSpec.js deleted file mode 100644 index bf29c3d7bd..0000000000 --- a/platform/features/table/test/controllers/RealtimeTableControllerSpec.js +++ /dev/null @@ -1,171 +0,0 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2016, 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/controllers/RealtimeTableController" - ], - function (TableController) { - - describe('The real-time table controller', function () { - var mockScope, - mockTelemetryHandler, - mockTelemetryHandle, - mockTelemetryFormatter, - mockDomainObject, - mockTable, - mockConfiguration, - watches, - mockTableRow, - mockConductor, - controller; - - function promise(value) { - return { - then: function (callback) { - return promise(callback(value)); - } - }; - } - - beforeEach(function () { - watches = {}; - mockTableRow = {'col1': 'val1', 'col2': 'row2'}; - - mockScope = jasmine.createSpyObj('scope', [ - '$on', - '$watch', - '$watchCollection', - '$digest', - '$broadcast' - ]); - mockScope.$on.andCallFake(function (expression, callback) { - watches[expression] = callback; - }); - mockScope.$watch.andCallFake(function (expression, callback) { - watches[expression] = callback; - }); - mockScope.$watchCollection.andCallFake(function (expression, callback) { - watches[expression] = callback; - }); - - mockConfiguration = { - 'range1': true, - 'range2': true, - 'domain1': true - }; - - mockTable = jasmine.createSpyObj('table', - [ - 'populateColumns', - 'buildColumnConfiguration', - 'getRowValues', - 'saveColumnConfiguration' - ] - ); - mockTable.columns = []; - mockTable.buildColumnConfiguration.andReturn(mockConfiguration); - mockTable.getRowValues.andReturn(mockTableRow); - - mockDomainObject = jasmine.createSpyObj('domainObject', [ - 'getCapability', - 'useCapability', - 'getModel' - ]); - mockDomainObject.getModel.andReturn({}); - mockDomainObject.getCapability.andReturn( - { - getMetadata: function () { - return {ranges: [{format: 'string'}]}; - } - }); - - mockScope.domainObject = mockDomainObject; - - mockTelemetryHandle = jasmine.createSpyObj('telemetryHandle', [ - 'getMetadata', - 'unsubscribe', - 'getDatum', - 'promiseTelemetryObjects', - 'getTelemetryObjects', - 'request', - 'getMetadata' - ]); - - // Arbitrary array with non-zero length, contents are not - // used by mocks - mockTelemetryHandle.getTelemetryObjects.andReturn([{}]); - mockTelemetryHandle.promiseTelemetryObjects.andReturn(promise(undefined)); - mockTelemetryHandle.getDatum.andReturn({}); - mockTelemetryHandle.request.andReturn(promise(undefined)); - mockTelemetryHandle.getMetadata.andReturn([]); - - mockTelemetryHandler = jasmine.createSpyObj('telemetryHandler', [ - 'handle' - ]); - mockTelemetryHandler.handle.andReturn(mockTelemetryHandle); - - mockConductor = jasmine.createSpyObj('conductor', [ - 'on', - 'off', - 'bounds', - 'timeSystem', - 'timeOfInterest' - ]); - - controller = new TableController(mockScope, mockTelemetryHandler, mockTelemetryFormatter, {conductor: mockConductor}); - controller.table = mockTable; - controller.handle = mockTelemetryHandle; - }); - - it('registers for streaming telemetry', function () { - controller.subscribe(); - expect(mockTelemetryHandler.handle).toHaveBeenCalledWith(jasmine.any(Object), jasmine.any(Function), true); - }); - - describe('receives new telemetry', function () { - beforeEach(function () { - controller.subscribe(); - mockScope.rows = []; - }); - - it('updates table with new streaming telemetry', function () { - mockTelemetryHandler.handle.mostRecentCall.args[1](); - expect(mockScope.$broadcast).toHaveBeenCalledWith('add:row', 0); - }); - it('observes the row limit', function () { - var i = 0; - controller.maxRows = 10; - - //Fill rows array with elements - for (; i < 10; i++) { - mockScope.rows.push({row: i}); - } - mockTelemetryHandler.handle.mostRecentCall.args[1](); - expect(mockScope.rows.length).toBe(controller.maxRows); - expect(mockScope.rows[mockScope.rows.length - 1]).toBe(mockTableRow); - expect(mockScope.rows[0].row).toBe(1); - }); - }); - }); - } -); diff --git a/platform/features/table/test/controllers/TelemetryTableControllerSpec.js b/platform/features/table/test/controllers/TelemetryTableControllerSpec.js new file mode 100644 index 0000000000..4f403edc32 --- /dev/null +++ b/platform/features/table/test/controllers/TelemetryTableControllerSpec.js @@ -0,0 +1,364 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2016, 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/controllers/TelemetryTableController', + '../../../../../src/api/objects/object-utils', + 'lodash' + ], + function (TelemetryTableController, objectUtils, _) { + + describe('The TelemetryTableController', function () { + + var controller, + mockScope, + mockTimeout, + mockConductor, + mockAPI, + mockDomainObject, + mockTelemetryAPI, + mockObjectAPI, + mockCompositionAPI, + unobserve, + mockBounds; + + function getCallback(target, event) { + return target.calls.filter(function (call) { + return call.args[0] === event; + })[0].args[1]; + } + + beforeEach(function () { + mockBounds = { + start: 0, + end: 10 + }; + mockConductor = jasmine.createSpyObj("conductor", [ + "bounds", + "follow", + "on", + "off", + "timeSystem" + ]); + mockConductor.bounds.andReturn(mockBounds); + mockConductor.follow.andReturn(false); + + mockDomainObject = jasmine.createSpyObj("domainObject", [ + "getModel", + "getId", + "useCapability" + ]); + mockDomainObject.getModel.andReturn({}); + mockDomainObject.getId.andReturn("mockId"); + mockDomainObject.useCapability.andReturn(true); + + mockCompositionAPI = jasmine.createSpyObj("compositionAPI", [ + "get" + ]); + + mockObjectAPI = jasmine.createSpyObj("objectAPI", [ + "observe" + ]); + unobserve = jasmine.createSpy("unobserve"); + mockObjectAPI.observe.andReturn(unobserve); + + mockScope = jasmine.createSpyObj("scope", [ + "$on", + "$watch", + "$broadcast" + ]); + mockScope.domainObject = mockDomainObject; + + mockTelemetryAPI = jasmine.createSpyObj("telemetryAPI", [ + "canProvideTelemetry", + "subscribe", + "getMetadata", + "commonValuesForHints", + "request", + "limitEvaluator", + "getValueFormatter" + ]); + mockTelemetryAPI.commonValuesForHints.andReturn([]); + mockTelemetryAPI.request.andReturn(Promise.resolve([])); + + + mockTelemetryAPI.canProvideTelemetry.andReturn(false); + + mockTimeout = jasmine.createSpy("timeout"); + mockTimeout.andReturn(1); // Return something + mockTimeout.cancel = jasmine.createSpy("cancel"); + + mockAPI = { + conductor: mockConductor, + objects: mockObjectAPI, + telemetry: mockTelemetryAPI, + composition: mockCompositionAPI + }; + controller = new TelemetryTableController(mockScope, mockTimeout, mockAPI); + }); + + describe('listens for', function () { + beforeEach(function () { + controller.registerChangeListeners(); + }); + it('object mutation', function () { + var calledObject = mockObjectAPI.observe.mostRecentCall.args[0]; + + expect(mockObjectAPI.observe).toHaveBeenCalled(); + expect(calledObject.identifier.key).toEqual(mockDomainObject.getId()); + }); + it('conductor changes', function () { + expect(mockConductor.on).toHaveBeenCalledWith("timeSystem", jasmine.any(Function)); + expect(mockConductor.on).toHaveBeenCalledWith("bounds", jasmine.any(Function)); + expect(mockConductor.on).toHaveBeenCalledWith("follow", jasmine.any(Function)); + }); + }); + + describe('deregisters all listeners on scope destruction', function () { + var timeSystemListener, + boundsListener, + followListener; + + beforeEach(function () { + controller.registerChangeListeners(); + + timeSystemListener = getCallback(mockConductor.on, "timeSystem"); + boundsListener = getCallback(mockConductor.on, "bounds"); + followListener = getCallback(mockConductor.on, "follow"); + + var destroy = getCallback(mockScope.$on, "$destroy"); + destroy(); + }); + + it('object mutation', function () { + expect(unobserve).toHaveBeenCalled(); + }); + it('conductor changes', function () { + expect(mockConductor.off).toHaveBeenCalledWith("timeSystem", timeSystemListener); + expect(mockConductor.off).toHaveBeenCalledWith("bounds", boundsListener); + expect(mockConductor.off).toHaveBeenCalledWith("follow", followListener); + }); + }); + + describe ('Subscribes to new data', function () { + var mockComposition, + mockTelemetryObject, + mockChildren, + unsubscribe, + done; + + beforeEach(function () { + mockComposition = jasmine.createSpyObj("composition", [ + "load" + ]); + + mockTelemetryObject = jasmine.createSpyObj("mockTelemetryObject", [ + "something" + ]); + mockTelemetryObject.identifier = { + key: "mockTelemetryObject" + }; + + unsubscribe = jasmine.createSpy("unsubscribe"); + mockTelemetryAPI.subscribe.andReturn(unsubscribe); + + mockChildren = [mockTelemetryObject]; + mockComposition.load.andReturn(Promise.resolve(mockChildren)); + mockCompositionAPI.get.andReturn(mockComposition); + + mockTelemetryAPI.canProvideTelemetry.andCallFake(function (obj) { + return obj.identifier.key === mockTelemetryObject.identifier.key; + }); + + done = false; + controller.getData().then(function () { + done = true; + }); + }); + + it('fetches historical data', function () { + waitsFor(function () { + return done; + }, "getData to return", 100); + + runs(function () { + expect(mockTelemetryAPI.request).toHaveBeenCalledWith(mockTelemetryObject, jasmine.any(Object)); + }); + }); + + it('fetches historical data for the time period specified by the conductor bounds', function () { + waitsFor(function () { + return done; + }, "getData to return", 100); + + runs(function () { + expect(mockTelemetryAPI.request).toHaveBeenCalledWith(mockTelemetryObject, mockBounds); + }); + }); + + it('subscribes to new data', function () { + waitsFor(function () { + return done; + }, "getData to return", 100); + + runs(function () { + expect(mockTelemetryAPI.subscribe).toHaveBeenCalledWith(mockTelemetryObject, jasmine.any(Function), {}); + }); + + }); + it('and unsubscribes on view destruction', function () { + waitsFor(function () { + return done; + }, "getData to return", 100); + + runs(function () { + var destroy = getCallback(mockScope.$on, "$destroy"); + destroy(); + + expect(unsubscribe).toHaveBeenCalled(); + }); + }); + }); + + it('When in real-time mode, enables auto-scroll', function () { + controller.registerChangeListeners(); + + var followCallback = getCallback(mockConductor.on, "follow"); + //Confirm pre-condition + expect(mockScope.autoScroll).toBeFalsy(); + + //Mock setting the conductor to 'follow' mode + followCallback(true); + expect(mockScope.autoScroll).toBe(true); + }); + + describe('populates table columns', function () { + var domainMetadata; + var allMetadata; + var mockTimeSystem; + + beforeEach(function () { + domainMetadata = [{ + key: "column1", + name: "Column 1", + hints: {} + }]; + + allMetadata = [{ + key: "column1", + name: "Column 1", + hints: {} + }, { + key: "column2", + name: "Column 2", + hints: {} + }, { + key: "column3", + name: "Column 3", + hints: {} + }]; + + mockTimeSystem = { + metadata: { + key: "column1" + } + }; + + mockTelemetryAPI.commonValuesForHints.andCallFake(function (metadata, hints) { + if (_.eq(hints, ["x"])) { + return domainMetadata; + } else if (_.eq(hints, [])) { + return allMetadata; + } + }); + + controller.loadColumns([mockDomainObject]); + }); + + it('based on metadata for given objects', function () { + expect(mockScope.headers).toBeDefined(); + expect(mockScope.headers.length).toBeGreaterThan(0); + expect(mockScope.headers.indexOf(allMetadata[0].name)).not.toBe(-1); + expect(mockScope.headers.indexOf(allMetadata[1].name)).not.toBe(-1); + expect(mockScope.headers.indexOf(allMetadata[2].name)).not.toBe(-1); + }); + + it('and sorts by column matching time system', function () { + expect(mockScope.defaultSort).not.toEqual("Column 1"); + controller.sortByTimeSystem(mockTimeSystem); + expect(mockScope.defaultSort).toEqual("Column 1"); + }); + + it('batches processing of rows for performance when receiving historical telemetry', function () { + var mockHistoricalData = [ + { + "column1": 1, + "column2": 2, + "column3": 3 + },{ + "column1": 4, + "column2": 5, + "column3": 6 + }, { + "column1": 7, + "column2": 8, + "column3": 9 + } + ]; + controller.batchSize = 2; + mockTelemetryAPI.request.andReturn(Promise.resolve(mockHistoricalData)); + controller.getHistoricalData([mockDomainObject]); + + waitsFor(function () { + return !!controller.timeoutHandle; + }, "first batch to be processed", 100); + + runs(function () { + //Verify that timeout is being used to yield process + expect(mockTimeout).toHaveBeenCalled(); + mockTimeout.mostRecentCall.args[0](); + expect(mockTimeout.calls.length).toBe(2); + mockTimeout.mostRecentCall.args[0](); + expect(mockScope.rows.length).toBe(3); + }); + }); + }); + + it('Removes telemetry rows from table when they fall out of bounds', function () { + var discardedRows = [ + {"column1": "value 1"}, + {"column2": "value 2"}, + {"column3": "value 3"} + ]; + + spyOn(controller.telemetry, "on").andCallThrough(); + + controller.registerChangeListeners(); + expect(controller.telemetry.on).toHaveBeenCalledWith("discarded", jasmine.any(Function)); + var onDiscard = getCallback(controller.telemetry.on, "discarded"); + onDiscard(discardedRows); + expect(mockScope.$broadcast).toHaveBeenCalledWith("remove:rows", discardedRows); + }); + + }); + });