[Tables] #707 Added auto-scroll, addressed race condition in Sinewave and event telemetry providers

Fixed issue with visible padding row

Incremental improvements

Added tests

Added tests for sorted insert, and fixed lint errors
This commit is contained in:
Henry 2016-03-08 21:36:33 -08:00
parent a4eb9d6a94
commit 7da1a218ba
15 changed files with 548 additions and 125 deletions

View File

@ -57,6 +57,13 @@ define([
},
"telemetry": {
"source": "eventGenerator",
"domains": [
{
"key": "time",
"name": "Time",
"format": "utc"
}
],
"ranges": [
{
"format": "string"

View File

@ -36,7 +36,9 @@ define(
function EventTelemetryProvider($q, $timeout) {
var
subscriptions = [],
genInterval = 1000;
genInterval = 1000,
generating = false,
id = Math.random() * 100000;
//
function matchesSource(request) {
@ -78,10 +80,13 @@ define(
}
function startGenerating() {
generating = true;
$timeout(function () {
handleSubscriptions();
if (subscriptions.length > 0) {
if (generating && subscriptions.length > 0) {
startGenerating();
} else {
generating = false;
}
}, genInterval);
}
@ -91,8 +96,6 @@ define(
callback: callback,
requests: requests
};
console.log("subscribe... " + Date.now() / 1000 + " request:" +
" " + requests[0].id);
function unsubscribe() {
subscriptions = subscriptions.filter(function (s) {
return s !== subscription;
@ -100,7 +103,7 @@ define(
}
subscriptions.push(subscription);
if (subscriptions.length === 1) {
if (!generating) {
startGenerating();
}

View File

@ -34,7 +34,8 @@ define(
* @constructor
*/
function SinewaveTelemetryProvider($q, $timeout) {
var subscriptions = [];
var subscriptions = [],
generating = false;
//
function matchesSource(request) {
@ -75,10 +76,13 @@ define(
}
function startGenerating() {
generating = true;
$timeout(function () {
handleSubscriptions();
if (subscriptions.length > 0) {
if (generating && subscriptions.length > 0) {
startGenerating();
} else {
generating = false;
}
}, 1000);
}
@ -97,7 +101,7 @@ define(
subscriptions.push(subscription);
if (subscriptions.length === 1) {
if (!generating) {
startGenerating();
}

View File

@ -101,7 +101,8 @@ define([
"composition": []
},
"views": [
"realtime"
"rt-table",
"scrolling-table"
]
}
],
@ -137,7 +138,7 @@ define([
},
{
"name": "Real-time Table",
"key": "realtime",
"key": "rt-table",
"glyph": "\ue605",
"templateUrl": "templates/rt-table.html",
"needs": [

View File

@ -1,5 +1,7 @@
<div class="l-view-section scrolling"
style="overflow: auto;"
ng-style="overrideRowPositioning ?
{'overflow': 'auto'} :
{'overflow': 'scroll'}"
>
<table class="filterable"
ng-style="overrideRowPositioning && {
@ -59,6 +61,5 @@
</td>
</tr>
</tbody>
</table>
</div>

View File

@ -3,6 +3,7 @@
headers="headers"
rows="rows"
enableFilter="true"
enableSort="true">
enableSort="true"
auto-scroll="autoScroll">
</mct-table>
</div>

View File

@ -0,0 +1,9 @@
<div ng-controller="RTTelemetryTableController">
<mct-table
headers="headers"
rows="rows"
enableFilter="true"
enableSort="true"
auto-scroll="true">
</mct-table>
</div>

View File

@ -54,10 +54,6 @@ define(
if (metadata) {
if (metadata.length > 1){
self.addColumn(new NameColumn(), 0);
}
metadata.forEach(function (metadatum) {
//Push domains first
(metadatum.domains || []).forEach(function (domainMetadata) {
@ -67,6 +63,10 @@ define(
self.addColumn(new RangeColumn(rangeMetadata, self.telemetryFormatter));
});
});
if (this.columns.length > 0){
self.addColumn(new NameColumn(), 0);
}
}
return this;
};

View File

@ -5,6 +5,15 @@ define(
function () {
"use strict";
/**
* A controller for the MCTTable directive. Populates scope with
* data used for populating, sorting, and filtering
* tables.
* @param $scope
* @param $timeout
* @param element
* @constructor
*/
function MCTTableController($scope, $timeout, element) {
var self = this;
@ -12,10 +21,11 @@ define(
this.element = element;
this.$timeout = $timeout;
this.maxDisplayRows = 50;
element.find('div').on('scroll', this.setVisibleRows.bind(this));
this.scrollable = element.find('div')[0];
$scope.visibleRows = [];
this.scrollable = element.find('div');
this.scrollable.on('scroll', this.onScroll.bind(this));
$scope.visibleRows = [];
$scope.overrideRowPositioning = false;
/**
@ -51,27 +61,82 @@ define(
self.updateRows($scope.rows);
};
/*
* Define watches to listen for changes to headers and rows.
*/
$scope.$watchCollection('filters', function () {
self.updateRows(self.$scope.displayRows);
self.updateRows($scope.rows);
});
$scope.$watchCollection('headers', this.updateHeaders.bind(this));
$scope.$watch('headers', this.updateHeaders.bind(this));
$scope.$watch('rows', this.updateRows.bind(this));
$scope.$on('newRow', this.newRow.bind(this));
}
MCTTableController.prototype.newRow = function (event, newRow) {
this.$scope.displayRows.push(newRow);
this.filterAndSort(this.$scope.displayRows);
this.$timeout(this.setElementSizes(), 0);
/*
* Listen for rows added individually (eg. for real-time tables)
*/
$scope.$on('addRow', this.newRow.bind(this));
}
/**
* Re-synchronize between data rows and visible rows, based on array
* content and scroll state.
* If auto-scroll is enabled, this function will scroll to the
* bottom of the page
* @private
*/
MCTTableController.prototype.scrollToBottom = function() {
var self = this;
//Use timeout to defer execution until next digest when any
// pending UI changes have completed, eg. a new row in the table.
if (this.$scope.autoScroll) {
this.$timeout(function(){
self.scrollable[0].scrollTop = self.scrollable[0].scrollHeight;
});
}
};
/**
* Handles a row add event. Rows can be added as needed using the
* `addRow` broadcast event.
* @private
*/
MCTTableController.prototype.newRow = function (event, row) {
//Add row to the filtered, sorted list of all rows
if (this.filterRows([row]).length > 0) {
this.insertSorted(this.$scope.displayRows, row);
}
//Keep 'rows' synchronized as it provides the unsorted,
// unfiltered model for this view
if (!this.$scope.rows) {
this.$scope.rows = [];
}
this.$scope.rows.push(row);
this.$timeout(this.setElementSizes.bind(this))
.then(this.scrollToBottom.bind(this));
};
/**
* @private
*/
MCTTableController.prototype.onScroll = function (event) {
//If user scrolls away from bottom, disable auto-scroll.
// Auto-scroll will be re-enabled if user scrolls to bottom again.
if (this.scrollable[0].scrollTop < (this.scrollable[0].scrollHeight - this.scrollable[0].offsetHeight)) {
this.$scope.autoScroll = false;
} else {
this.$scope.autoScroll = true;
}
this.setVisibleRows();
this.$scope.$digest();
};
/**
* Sets visible rows based on array
* content and current scroll state.
*/
MCTTableController.prototype.setVisibleRows = function () {
var self = this,
target = this.scrollable,
target = this.scrollable[0],
topScroll = target.scrollTop,
bottomScroll = topScroll + target.offsetHeight,
firstVisible,
@ -87,7 +152,7 @@ define(
// rows (if data added)
if (this.$scope.visibleRows.length != this.$scope.displayRows.length){
start = 0;
end = this.$scope.displayRows.length-1;
end = this.$scope.displayRows.length;
} else {
//Data is in sync, and no need to calculate scroll,
// so do nothing.
@ -114,13 +179,12 @@ define(
if (start < 0) {
start = 0;
//end = this.$scope.visibleRows.length - 1;
end = Math.min(this.maxDisplayRows, this.$scope.displayRows.length) - 1;
end = Math.min(this.maxDisplayRows, this.$scope.displayRows.length);
} else if (end >= this.$scope.displayRows.length) {
end = this.$scope.displayRows.length - 1;
end = this.$scope.displayRows.length;
start = end - this.maxDisplayRows + 1;
}
if (this.$scope.visibleRows[0].rowIndex === start &&
if (this.$scope.visibleRows[0] && this.$scope.visibleRows[0].rowIndex === start &&
this.$scope.visibleRows[this.$scope.visibleRows.length - 1]
.rowIndex === end) {
@ -137,8 +201,6 @@ define(
contents: row
};
});
this.$scope.$digest();
};
/**
@ -156,7 +218,7 @@ define(
this.$scope.filters = {};
}
// Reset column sort information unless the new headers
// contain the column current sorted on.
// contain the column currently sorted on.
if (this.$scope.enableSort && newHeaders.indexOf(this.$scope.sortColumn) === -1) {
this.$scope.sortColumn = undefined;
this.$scope.sortDirection = undefined;
@ -175,7 +237,6 @@ define(
firstRow = tbody.find('tr'),
column = firstRow.find('td'),
headerHeight = thead.prop('offsetHeight'),
//row height is hard-coded for now.
rowHeight = 20,
overallHeight = headerHeight + (rowHeight * (this.$scope.displayRows ? this.$scope.displayRows.length - 1 : 0));
@ -192,6 +253,80 @@ define(
this.$scope.overrideRowPositioning = true;
};
/**
* @private
*/
MCTTableController.prototype.insertSorted = function(array, element) {
var index = -1,
self = this,
sortKey = this.$scope.sortColumn;
function binarySearch(searchArray, searchElement, min, max){
var sampleAt = Math.floor((max - min) / 2) + min;
if (max < min) {
return min; // Element is not in array, min gives direction
}
switch(self.sortComparator(searchElement[sortKey].text, searchArray[sampleAt][sortKey].text)) {
case -1:
return binarySearch(searchArray, searchElement, min, sampleAt - 1);
case 0 :
return sampleAt;
case 1 :
return binarySearch(searchArray, searchElement, sampleAt + 1, max);
}
}
if (!this.$scope.sortColumn || !this.$scope.sortDirection) {
//No sorting applied, push it on the end.
index = array.length;
} else {
//Sort is enabled, perform binary search to find insertion point
index = binarySearch(array, element, 0, array.length - 1);
}
if (index === -1){
array.unshift(element);
} else if (index === array.length){
array.push(element);
} else {
array.splice(index, 0, element);
}
};
/**
* Compare two variables, returning a number that represents
* which is larger. Similar to the default array sort
* comparator, but does not coerce all values to string before
* conversion. Strings are lowercased before comparison.
*
* @private
*/
MCTTableController.prototype.sortComparator = function(a, b) {
var result = 0,
sortDirectionMultiplier;
if (typeof a === "string" && typeof b === "string") {
a = a.toLowerCase();
b = b.toLowerCase();
}
if (a < b) {
result = -1;
}
if (a > b) {
result = 1;
}
if (this.$scope.sortDirection === 'asc') {
sortDirectionMultiplier = 1;
} else if (this.$scope.sortDirection === 'desc') {
sortDirectionMultiplier = -1;
}
return result * sortDirectionMultiplier;
};
/**
* Returns a new array which is a result of applying the sort
* criteria defined in $scope.
@ -199,38 +334,12 @@ define(
* Does not modify the array that was passed in.
*/
MCTTableController.prototype.sortRows = function(rowsToSort) {
/**
* Compare two variables, returning a number that represents
* which is larger. Similar to the default array sort
* comparator, but does not coerce all values to string before
* conversion. Strings are lowercased before comparison.
*/
function genericComparator(a, b) {
if (typeof a === "string" && typeof b === "string") {
a = a.toLowerCase();
b = b.toLowerCase();
}
if (a < b) {
return -1;
}
if (a > b) {
return 1;
}
return 0;
}
var self = this,
sortKey = this.$scope.sortColumn;
if (!this.$scope.sortColumn || !this.$scope.sortDirection) {
return rowsToSort;
}
var sortKey = this.$scope.sortColumn,
sortDirectionMultiplier;
if (this.$scope.sortDirection === 'asc') {
sortDirectionMultiplier = 1;
} else if (this.$scope.sortDirection === 'desc') {
sortDirectionMultiplier = -1;
}
return rowsToSort.slice(0).sort(function(a, b) {
//If the values to compare can be compared as
@ -239,8 +348,7 @@ define(
var valA = isNaN(a[sortKey].text) ? a[sortKey].text : parseFloat(a[sortKey].text),
valB = isNaN(b[sortKey].text) ? b[sortKey].text : parseFloat(b[sortKey].text);
return genericComparator(valA, valB) *
sortDirectionMultiplier;
return self.sortComparator(valA, valB);
});
};
@ -271,9 +379,10 @@ define(
return largestRow;
}, JSON.parse(JSON.stringify(rows[0] || {})));
largestRow = JSON.parse(JSON.stringify(largestRow));
// Pad with characters to accomodate variable-width fonts,
// and remove characters that would allow word-wrapping.
largestRow = JSON.parse(JSON.stringify(largestRow));
Object.keys(largestRow).forEach(function(key) {
var padCharacters,
i;
@ -289,8 +398,15 @@ define(
return largestRow;
};
/**
* Calculates the widest row in the table, pads that row, and adds
* it to the table. Allows the table to size itself, then uses this
* as basis for column dimensions.
* @private
*/
MCTTableController.prototype.resize = function (){
var largestRow = this.findLargestRow(this.$scope.displayRows);
var largestRow = this.findLargestRow(this.$scope.displayRows),
self = this;
this.$scope.visibleRows = [
{
rowIndex: 0,
@ -299,9 +415,18 @@ define(
}
];
this.$timeout(this.setElementSizes.bind(this));
//Wait a timeout to allow digest of previous change to visible
// rows to happen.
this.$timeout(function() {
//Remove temporary padding row used for setting column widths
self.$scope.visibleRows = [];
self.setElementSizes();
});
};
/**
* @priate
*/
MCTTableController.prototype.filterAndSort = function(rows) {
var displayRows = rows;
if (this.$scope.enableFilter) {
@ -319,19 +444,25 @@ define(
* will be sorted before display.
*/
MCTTableController.prototype.updateRows = function (newRows) {
//Reset visible rows because new row data available.
this.$scope.visibleRows = [];
this.$scope.overrideRowPositioning = false;
//Nothing to show because no columns visible
if (!this.$scope.displayHeaders) {
return;
}
this.filterAndSort(newRows || []);
//Apply filters and sort a copy of the the new rows
this.filterAndSort((newRows || []).slice(0));
//Resize columns appropriately
this.resize();
};
/**
* Filter rows.
* Applies user defined filters to rows. These filters are based on
* the text entered in the search areas in each column
*/
MCTTableController.prototype.filterRows = function(rowsToFilter) {
var filters = {},

View File

@ -21,23 +21,16 @@
*****************************************************************************/
/*global define*/
/**
* This bundle adds a table view for displaying telemetry data.
* @namespace platform/features/table
*/
define(
[
'./TelemetryTableController',
'../TableConfiguration',
'../NameColumn'
'./TelemetryTableController'
],
function (TableController, Table, NameColumn) {
function (TableController) {
"use strict";
/**
* The TableController is responsible for getting data onto the page
* in the table widget. This includes handling composition,
* configuration, and telemetry subscriptions.
* Extends TelemetryTableController and adds real-time streaming
* support.
* @memberof platform/features/table
* @param $scope
* @param telemetryHandler
@ -46,16 +39,44 @@ define(
*/
function RTTelemetryTableController($scope, telemetryHandler, telemetryFormatter) {
TableController.call(this, $scope, telemetryHandler, telemetryFormatter);
$scope.autoScroll = false;
/*
* Determine if auto-scroll should be enabled. Is enabled
* automatically when telemetry type is string
*/
function hasStringTelemetry(domainObject) {
var telemetry = domainObject &&
domainObject.getCapability('telemetry'),
metadata = telemetry ? telemetry.getMetadata() : {},
ranges = metadata.ranges || [];
return ranges.some(function (range) {
return range.format === 'string';
});
}
$scope.$watch('domainObject', function(domainObject) {
//When a domain object becomes available, check whether the
// view should auto-scroll to the bottom.
if (domainObject && hasStringTelemetry(domainObject)){
$scope.autoScroll = true;
}
});
}
RTTelemetryTableController.prototype = Object.create(TableController.prototype);
/**
Create a new telemetry subscription.
Override the subscribe function defined on the parent controller in
order to handle realtime telemetry instead of historical.
*/
RTTelemetryTableController.prototype.subscribe = function() {
console.trace();
var self = this;
self.$scope.rows = undefined;
(this.subscriptions || []).forEach(function(unsubscribe){
unsubscribe();
});
if (this.handle) {
this.handle.unsubscribe();
@ -66,11 +87,8 @@ define(
self.handle.getTelemetryObjects().forEach(function(telemetryObject){
datum = self.handle.getDatum(telemetryObject);
if (datum) {
if (!self.$scope.rows) {
self.$scope.rows = [self.table.getRowValues(telemetryObject, datum)];
} else {
self.updateRows(telemetryObject, datum);
}
var rowValue = self.table.getRowValues(telemetryObject, datum);
self.$scope.$broadcast('addRow', rowValue);
}
});
@ -85,16 +103,6 @@ define(
this.setup();
};
/**
* Add data to rows
* @param object The object for which data is available (table may
* be composed of multiple objects)
* @param datum The data received from the telemetry source
*/
RTTelemetryTableController.prototype.updateRows = function (object, datum) {
this.$scope.$broadcast('newRow', this.table.getRowValues(object, datum));
};
return RTTelemetryTableController;
}
);

View File

@ -72,21 +72,22 @@ define(
this.$scope.$on("$destroy", this.destroy.bind(this));
}
/**
* Defer registration of change listeners until domain object is
* available in order to avoid race conditions
* @private
*/
TelemetryTableController.prototype.registerChangeListeners = function() {
//Defer registration of change listeners until domain object is
// available in order to avoid race conditions
this.changeListeners.forEach(function (listener) {
return listener && listener();
});
this.changeListeners = [];
// When composition changes, re-subscribe to the various
// telemetry subscriptions
//this.changeListeners.push(this.$scope.$watchCollection('domainObject.getModel().composition', this.subscribe.bind(this)));
this.changeListeners.push(this.$scope.$watchCollection('domainObject.getModel().composition', this.subscribe.bind(this)));
//Change of bounds in time conductor
this.changeListeners.push(this.$scope.$on('telemetry:display:bounds', this.subscribe.bind(this)));
};
/**
@ -100,16 +101,15 @@ define(
};
/**
Create a new subscription. This is called when
Create a new subscription. This can be overridden by children to
change default behaviour (which is to retrieve historical telemetry
only).
*/
TelemetryTableController.prototype.subscribe = function() {
console.trace();
if (this.handle) {
this.handle.unsubscribe();
}
this.$scope.rows = [];
//Noop because not supporting realtime data right now
function noop(){
}
@ -127,12 +127,17 @@ define(
/**
* Add any historical data available
* @private
*/
TelemetryTableController.prototype.addHistoricalData = function(domainObject, series) {
var i;
var i,
newRows = [];
for (i=0; i < series.getPointCount(); i++) {
this.updateRows(domainObject, this.handle.makeDatum(domainObject, series, i));
newRows.push(this.table.getRowValues(domainObject, this.handle.makeDatum(domainObject, series, i)));
}
this.$scope.rows = newRows;
};
/**
@ -160,7 +165,7 @@ define(
};
/**
* Add data to rows
* @private
* @param object The object for which data is available (table may
* be composed of multiple objects)
* @param datum The data received from the telemetry source
@ -172,6 +177,7 @@ define(
/**
* When column configuration changes, update the visible headers
* accordingly.
* @private
*/
TelemetryTableController.prototype.filterColumns = function (columnConfig) {
if (!columnConfig){

View File

@ -1,20 +1,30 @@
/*global define*/
define(
["../controllers/MCTTableController"],
function (MCTTableController) {
[
"../controllers/MCTTableController",
"text!../../res/templates/mct-table.html"
],
function (MCTTableController, TableTemplate) {
"use strict";
/**
* Defines a generic 'Table' component. The table can be populated
* en-masse by setting the rows attribute, or rows can be added as
* needed via a broadcast 'addRow' event.
* @constructor
*/
function MCTTable($timeout) {
return {
restrict: "E",
templateUrl: "platform/features/table/res/templates/mct-data-table.html",
template: TableTemplate,
controller: ['$scope', '$timeout', '$element', MCTTableController],
scope: {
headers: "=",
rows: "=",
enableFilter: "=?",
enableSort: "=?"
enableSort: "=?",
autoScroll: "=?"
},
};
}

View File

@ -120,13 +120,13 @@ define(
});
it("populates the columns attribute", function() {
expect(table.columns.length).toBe(4);
expect(table.columns.length).toBe(5);
});
it("Build columns populates columns with domains to the left", function() {
expect(table.columns[0] instanceof DomainColumn).toBeTruthy();
expect(table.columns[1] instanceof DomainColumn).toBeTruthy();
expect(table.columns[2] instanceof DomainColumn).toBeFalsy();
expect(table.columns[2] instanceof DomainColumn).toBeTruthy();
expect(table.columns[3] instanceof DomainColumn).toBeFalsy();
});
it("Produces headers for each column based on title", function() {
@ -135,7 +135,7 @@ define(
spyOn(firstColumn, 'getTitle');
headers = table.getHeaders();
expect(headers.length).toBe(4);
expect(headers.length).toBe(5);
expect(firstColumn.getTitle).toHaveBeenCalled();
});

View File

@ -48,6 +48,8 @@ define(
watches = {};
mockScope = jasmine.createSpyObj('scope', [
'$watch',
'$on',
'$watchCollection'
]);
mockScope.$watchCollection.andCallFake(function(event, callback) {
@ -62,14 +64,15 @@ define(
mockScope.displayHeaders = true;
mockTimeout = jasmine.createSpy('$timeout');
mockTimeout.andReturn(promise(undefined));
controller = new MCTTableController(mockScope, mockTimeout, mockElement);
});
it('Reacts to changes to filters, headers, and rows', function() {
expect(mockScope.$watchCollection).toHaveBeenCalledWith('filters', jasmine.any(Function));
expect(mockScope.$watchCollection).toHaveBeenCalledWith('headers', jasmine.any(Function));
expect(mockScope.$watchCollection).toHaveBeenCalledWith('rows', jasmine.any(Function));
expect(mockScope.$watch).toHaveBeenCalledWith('headers', jasmine.any(Function));
expect(mockScope.$watch).toHaveBeenCalledWith('rows', jasmine.any(Function));
});
describe('rows', function() {
@ -116,6 +119,19 @@ define(
expect(mockScope.displayRows).toEqual(testRows);
});
it('Supports adding rows individually', function() {
var addRowFunc = mockScope.$on.mostRecentCall.args[1],
row4 = {
'col1': {'text': 'row3 col1'},
'col2': {'text': 'ghi'},
'col3': {'text': 'row3 col3'}
};
controller.updateRows(testRows);
expect(mockScope.displayRows.length).toBe(3);
addRowFunc(row4);
expect(mockScope.displayRows.length).toBe(4);
});
describe('sorting', function() {
var sortedRows;
@ -149,7 +165,87 @@ define(
expect(sortedRows[1].col2.text).toEqual('def');
expect(sortedRows[2].col2.text).toEqual('abc');
});
describe('Adding new rows', function() {
var row4,
row5,
row6;
beforeEach(function() {
row4 = {
'col1': {'text': 'row5 col1'},
'col2': {'text': 'xyz'},
'col3': {'text': 'row5 col3'}
};
row5 = {
'col1': {'text': 'row6 col1'},
'col2': {'text': 'aaa'},
'col3': {'text': 'row6 col3'}
};
row6 = {
'col1': {'text': 'row6 col1'},
'col2': {'text': 'ggg'},
'col3': {'text': 'row6 col3'}
};
});
it('Adds new rows at the correct sort position when' +
' sorted ', function() {
mockScope.sortColumn = 'col2';
mockScope.sortDirection = 'desc';
mockScope.displayRows = controller.sortRows(testRows);
controller.newRow(undefined, row4);
expect(mockScope.displayRows[0].col2.text).toEqual('xyz');
controller.newRow(undefined, row5);
expect(mockScope.displayRows[4].col2.text).toEqual('aaa');
controller.newRow(undefined, row6);
expect(mockScope.displayRows[2].col2.text).toEqual('ggg');
//Add a duplicate row
controller.newRow(undefined, row6);
expect(mockScope.displayRows[2].col2.text).toEqual('ggg');
expect(mockScope.displayRows[3].col2.text).toEqual('ggg');
});
it('Adds new rows at the correct sort position when' +
' sorted and filtered', function() {
mockScope.sortColumn = 'col2';
mockScope.sortDirection = 'desc';
mockScope.filters = {'col2': 'a'};//Include only
// rows with 'a'
mockScope.displayRows = controller.sortRows(testRows);
mockScope.displayRows = controller.filterRows(testRows);
controller.newRow(undefined, row5);
expect(mockScope.displayRows.length).toBe(2);
expect(mockScope.displayRows[1].col2.text).toEqual('aaa');
controller.newRow(undefined, row6);
expect(mockScope.displayRows.length).toBe(2);
//Row was not added because does not match filter
});
it('Adds new rows at the correct sort position when' +
' not sorted ', function() {
mockScope.sortColumn = undefined;
mockScope.sortDirection = undefined;
mockScope.filters = {};
mockScope.displayRows = testRows;
controller.newRow(undefined, row5);
expect(mockScope.displayRows[3].col2.text).toEqual('aaa');
controller.newRow(undefined, row6);
expect(mockScope.displayRows[4].col2.text).toEqual('ggg');
});
});
});
});
});
});

View File

@ -0,0 +1,146 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,xit*/
define(
[
"../../src/controllers/RTTelemetryTableController"
],
function (TableController) {
"use strict";
describe('The real-time table controller', function() {
var mockScope,
mockTelemetryHandler,
mockTelemetryHandle,
mockTelemetryFormatter,
mockDomainObject,
mockTable,
mockConfiguration,
watches,
mockTableRow,
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',
'$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',
[
'buildColumns',
'getColumnConfiguration',
'getRowValues',
'saveColumnConfiguration'
]
);
mockTable.columns = [];
mockTable.getColumnConfiguration.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'
]);
// Arbitrary array with non-zero length, contents are not
// used by mocks
mockTelemetryHandle.getTelemetryObjects.andReturn([{}]);
mockTelemetryHandle.promiseTelemetryObjects.andReturn(promise(undefined));
mockTelemetryHandle.getDatum.andReturn({});
mockTelemetryHandler = jasmine.createSpyObj('telemetryHandler', [
'handle'
]);
mockTelemetryHandler.handle.andReturn(mockTelemetryHandle);
controller = new TableController(mockScope, mockTelemetryHandler, mockTelemetryFormatter);
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);
});
it('updates table with new streaming telemetry', function() {
controller.subscribe();
mockTelemetryHandler.handle.mostRecentCall.args[1]();
expect(mockScope.$broadcast).toHaveBeenCalledWith('addRow', mockTableRow);
});
it('enables autoscroll for event telemetry', function() {
controller.subscribe();
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
expect(mockScope.autoScroll).toBe(true);
});
});
}
);