From d304cc4343415bcc18c451a103f17b21ac512b25 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Tue, 2 Dec 2014 14:51:48 -0800 Subject: [PATCH 1/6] [Scrolling] Bring in scrolling list view Bring in scrolling list view from the sandbox branch in preparation for clean up, testing, and integration to complete transition of scrolling list views, WTD-534. --- platform/features/scrolling/bundle.json | 24 +++ .../scrolling/res/templates/scrolling.html | 25 +++ .../features/scrolling/src/DomainColumn.js | 31 ++++ platform/features/scrolling/src/NameColumn.js | 28 +++ .../features/scrolling/src/RangeColumn.js | 31 ++++ .../scrolling/src/ScrollingListController.js | 174 ++++++++++++++++++ 6 files changed, 313 insertions(+) create mode 100644 platform/features/scrolling/bundle.json create mode 100644 platform/features/scrolling/res/templates/scrolling.html create mode 100644 platform/features/scrolling/src/DomainColumn.js create mode 100644 platform/features/scrolling/src/NameColumn.js create mode 100644 platform/features/scrolling/src/RangeColumn.js create mode 100644 platform/features/scrolling/src/ScrollingListController.js diff --git a/platform/features/scrolling/bundle.json b/platform/features/scrolling/bundle.json new file mode 100644 index 0000000000..e2763b03fc --- /dev/null +++ b/platform/features/scrolling/bundle.json @@ -0,0 +1,24 @@ +{ + "name": "Scrolling Lists", + "description": "Time-ordered list of latest data.", + "extensions": { + "views": [ + { + "key": "scrolling", + "name": "Scrolling", + "description": "Scrolling list of data values.", + "templateUrl": "templates/scrolling.html", + "needs": [ "telemetry" ], + "delegation": true + } + ], + "controllers": [ + { + "key": "ScrollingListController", + "implementation": "ScrollingListController.js", + "depends": [ "$scope" ] + } + ] + } + +} \ No newline at end of file diff --git a/platform/features/scrolling/res/templates/scrolling.html b/platform/features/scrolling/res/templates/scrolling.html new file mode 100644 index 0000000000..a26fd6503e --- /dev/null +++ b/platform/features/scrolling/res/templates/scrolling.html @@ -0,0 +1,25 @@ +
+ +
+
+ +
+ +
+ {{header}}{{header}} +
+
+ +
+
+ {{cell}} +
+
+ +
+
+
+ + diff --git a/platform/features/scrolling/src/DomainColumn.js b/platform/features/scrolling/src/DomainColumn.js new file mode 100644 index 0000000000..7c8f03c788 --- /dev/null +++ b/platform/features/scrolling/src/DomainColumn.js @@ -0,0 +1,31 @@ +/*global define,Promise*/ + +/** + * Module defining DomainColumn. Created by vwoeltje on 11/18/14. + */ +define( + [], + function () { + "use strict"; + + /** + * + * @constructor + */ + function DomainColumn(domainMetadata) { + return { + getTitle: function () { + return domainMetadata.name; + }, + getValue: function (domainObject, data, index) { + return data.getDomainValue( + index, + domainMetadata.key + ); + } + }; + } + + return DomainColumn; + } +); \ No newline at end of file diff --git a/platform/features/scrolling/src/NameColumn.js b/platform/features/scrolling/src/NameColumn.js new file mode 100644 index 0000000000..a997b95d38 --- /dev/null +++ b/platform/features/scrolling/src/NameColumn.js @@ -0,0 +1,28 @@ +/*global define,Promise*/ + +/** + * Module defining NameColumn. Created by vwoeltje on 11/18/14. + */ +define( + [], + function () { + "use strict"; + + /** + * + * @constructor + */ + function NameColumn() { + return { + getTitle: function () { + return "Name"; + }, + getValue: function (domainObject) { + return domainObject.getModel().name; + } + }; + } + + return NameColumn; + } +); \ No newline at end of file diff --git a/platform/features/scrolling/src/RangeColumn.js b/platform/features/scrolling/src/RangeColumn.js new file mode 100644 index 0000000000..cf60f4ed80 --- /dev/null +++ b/platform/features/scrolling/src/RangeColumn.js @@ -0,0 +1,31 @@ +/*global define,Promise*/ + +/** + * Module defining DomainColumn. Created by vwoeltje on 11/18/14. + */ +define( + [], + function () { + "use strict"; + + /** + * + * @constructor + */ + function RangeColumn(rangeMetadata) { + return { + getTitle: function () { + return rangeMetadata.name; + }, + getValue: function (domainObject, data, index) { + return data.getRangeValue( + index, + rangeMetadata.key + ); + } + }; + } + + return RangeColumn; + } +); \ No newline at end of file diff --git a/platform/features/scrolling/src/ScrollingListController.js b/platform/features/scrolling/src/ScrollingListController.js new file mode 100644 index 0000000000..415a22eb67 --- /dev/null +++ b/platform/features/scrolling/src/ScrollingListController.js @@ -0,0 +1,174 @@ +/*global define,Promise*/ + +/** + * Module defining ListController. Created by vwoeltje on 11/18/14. + */ +define( + ["./NameColumn", "./DomainColumn", "./RangeColumn"], + function (NameColumn, DomainColumn, RangeColumn) { + "use strict"; + + var ROW_COUNT = 18; + + /** + * + * @constructor + */ + function ScrollingListController($scope) { + var columns = []; // Domain used + + /** + * Look up the most recent values from a set of data objects. + * Returns an array of objects in the order in which data + * should be displayed; each element is an object with + * two properties: + * + * * objectIndex: The index of the domain object associated + * with the data point to be displayed in that + * row. + * * pointIndex: The index of the data point itself, within + * its data set. + * + * @param {Array} datas an array of the most recent + * data objects; expected to be in the same order + * as the domain objects provided at constructor + * @param {Array= 0) { + nextTime = data.getDomainValue(pointIndex); + if (nextTime > candidateTime) { + candidateTime = nextTime; + candidate = { + objectIndex: i, + pointIndex: pointIndex + }; + } + } + } + + // Assemble a list of the most recent data points + while (latest.length < ROW_COUNT) { + // Reset variables pre-search + candidateTime = Number.NEGATIVE_INFINITY; + candidate = undefined; + + // Linear search for most recent + datas.forEach(findCandidate); + + if (candidate) { + // Record this data point - it is the most recent + latest.push(candidate); + + // Track the data points used so we can look farther back + // in the data set on the next iteration + used[candidate.objectIndex] = used[candidate.objectIndex] + 1; + } else { + break; // Ran out of candidates; not enough data points available + } + } + + return latest; + } + + function getRows(telemetry) { + var datas = telemetry.getResponse(), + objects = telemetry.getTelemetryObjects(), + values = getLatestDataValues(datas); + + return values.map(function (value) { + return columns.map(function (column) { + return column.getValue( + objects[value.objectIndex], + datas[value.objectIndex], + value.pointIndex + ); + }); + }); + } + + function updateRows() { + var telemetry = $scope.telemetry; + $scope.rows = telemetry ? getRows(telemetry) : []; + } + + function setupColumns() { + var telemetry = $scope.telemetry, + domainKeys = {}, + rangeKeys = {}, + metadata; + + function addDomain(domain) { + var key = domain.key; + if (key && !domainKeys[key]) { + columns.push(new DomainColumn(domain)); + } + } + + function addRange(range) { + var key = range.key; + if (key && !rangeKeys[key]) { + columns.push(new RangeColumn(range)); + } + } + + if (!telemetry) { + columns = []; + $scope.rows = []; + $scope.headers = []; + return; + } + + columns = [ new NameColumn() ]; + + // Add domain, range columns + metadata = telemetry.getMetadata(); + (metadata || []).forEach(function (metadata) { + (metadata.domains || []).forEach(addDomain); + (metadata.ranges || []).forEach(addRange); + }); + + // Add default domain, range columns if none + // were described in metadata. + if (Object.keys(domainKeys).length < 1) { + columns.push(new DomainColumn({ name: "Time" })); + } + if (Object.keys(rangeKeys).length < 1) { + columns.push(new RangeColumn({ name: "Value" })); + } + + $scope.headers = columns.map(function (column) { + return column.getTitle(); + }); + + updateRows(); + } + + $scope.$on("telemetryUpdate", updateRows); + $scope.$watch("telemetry", setupColumns); + } + + return ScrollingListController; + } +); \ No newline at end of file From e711073cb517eb8889a1b51739a1846dc41578c7 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Tue, 2 Dec 2014 15:21:26 -0800 Subject: [PATCH 2/6] [Scrolling] Add JSDoc, clean up Clean up scripts for Scrolling List view and add some JSDoc. WTD-534. --- .../features/scrolling/src/DomainColumn.js | 26 +++++++++++++--- platform/features/scrolling/src/NameColumn.js | 11 +++++++ .../features/scrolling/src/RangeColumn.js | 20 ++++++++++--- .../scrolling/src/ScrollingListController.js | 30 +++++++++++++++---- 4 files changed, 74 insertions(+), 13 deletions(-) diff --git a/platform/features/scrolling/src/DomainColumn.js b/platform/features/scrolling/src/DomainColumn.js index 7c8f03c788..cf4383a37a 100644 --- a/platform/features/scrolling/src/DomainColumn.js +++ b/platform/features/scrolling/src/DomainColumn.js @@ -1,27 +1,45 @@ -/*global define,Promise*/ +/*global define,moment*/ /** * Module defining DomainColumn. Created by vwoeltje on 11/18/14. */ define( - [], + ["../../plot/lib/moment.min"], function () { "use strict"; + // Date format to use for domain values; in particular, + // use day-of-year instead of month/day + var DATE_FORMAT = "YYYY-DDD HH:mm:ss"; + /** + * A column which will report telemetry domain values + * (typically, timestamps.) Used by the ScrollingListController. * * @constructor + * @param domainMetadata an object with the machine- and human- + * readable names for this domain (in `key` and `name` + * fields, respectively.) */ function DomainColumn(domainMetadata) { return { + /** + * Get the title to display in this column's header. + * @returns {string} the title to display + */ getTitle: function () { return domainMetadata.name; }, + /** + * Get the text to display inside a row under this + * column. + * @returns {string} the text to display + */ getValue: function (domainObject, data, index) { - return data.getDomainValue( + return moment.utc(data.getDomainValue( index, domainMetadata.key - ); + )).format(DATE_FORMAT); } }; } diff --git a/platform/features/scrolling/src/NameColumn.js b/platform/features/scrolling/src/NameColumn.js index a997b95d38..4b72f791ed 100644 --- a/platform/features/scrolling/src/NameColumn.js +++ b/platform/features/scrolling/src/NameColumn.js @@ -9,14 +9,25 @@ define( "use strict"; /** + * A column which will report the name of the domain object + * which exposed specific telemetry values. * * @constructor */ function NameColumn() { return { + /** + * Get the title to display in this column's header. + * @returns {string} the title to display + */ getTitle: function () { return "Name"; }, + /** + * Get the text to display inside a row under this + * column. This returns the domain object's name. + * @returns {string} the text to display + */ getValue: function (domainObject) { return domainObject.getModel().name; } diff --git a/platform/features/scrolling/src/RangeColumn.js b/platform/features/scrolling/src/RangeColumn.js index cf60f4ed80..f710b31a05 100644 --- a/platform/features/scrolling/src/RangeColumn.js +++ b/platform/features/scrolling/src/RangeColumn.js @@ -9,19 +9,31 @@ define( "use strict"; /** + * A column which will report telemetry range values + * (typically, measurements.) Used by the ScrollingListController. * * @constructor + * @param rangeMetadata an object with the machine- and human- + * readable names for this range (in `key` and `name` + * fields, respectively.) */ function RangeColumn(rangeMetadata) { return { + /** + * Get the title to display in this column's header. + * @returns {string} the title to display + */ getTitle: function () { return rangeMetadata.name; }, + /** + * Get the text to display inside a row under this + * column. + * @returns {string} the text to display + */ getValue: function (domainObject, data, index) { - return data.getRangeValue( - index, - rangeMetadata.key - ); + var value = data.getRangeValue(index, rangeMetadata.key); + return value && value.toFixed(3); } }; } diff --git a/platform/features/scrolling/src/ScrollingListController.js b/platform/features/scrolling/src/ScrollingListController.js index 415a22eb67..3df32c4d71 100644 --- a/platform/features/scrolling/src/ScrollingListController.js +++ b/platform/features/scrolling/src/ScrollingListController.js @@ -11,7 +11,8 @@ define( var ROW_COUNT = 18; /** - * + * The ScrollingListController is responsible for populating + * the contents of the scrolling list view. * @constructor */ function ScrollingListController($scope) { @@ -85,18 +86,25 @@ define( // in the data set on the next iteration used[candidate.objectIndex] = used[candidate.objectIndex] + 1; } else { - break; // Ran out of candidates; not enough data points available + // Ran out of candidates; not enough data points + // available to fill all rows. + break; } } return latest; } + // Get a set of populated, ready-to-display rows for the + // latest data values. function getRows(telemetry) { var datas = telemetry.getResponse(), objects = telemetry.getTelemetryObjects(), values = getLatestDataValues(datas); + // Each value will become a row, which will contain + // some value in each column (rendering by the + // column object itself) return values.map(function (value) { return columns.map(function (column) { return column.getValue( @@ -108,17 +116,22 @@ define( }); } + // Update the contents function updateRows() { var telemetry = $scope.telemetry; $scope.rows = telemetry ? getRows(telemetry) : []; } - function setupColumns() { - var telemetry = $scope.telemetry, - domainKeys = {}, + // Set up columns based on telemetry metadata. This will + // include one column for each domain and range type, as + // well as a column for the domain object name. + function setupColumns(telemetry) { + var domainKeys = {}, rangeKeys = {}, metadata; + // Add a domain to the set of columns, if a domain + // with the same key has not yet been inclued. function addDomain(domain) { var key = domain.key; if (key && !domainKeys[key]) { @@ -126,6 +139,8 @@ define( } } + // Add a range to the set of columns, if a range + // with the same key has not yet been inclued. function addRange(range) { var key = range.key; if (key && !rangeKeys[key]) { @@ -133,6 +148,8 @@ define( } } + // We cannot proceed if the telemetry controller + // is not available; clear all rows/columns. if (!telemetry) { columns = []; $scope.rows = []; @@ -158,10 +175,13 @@ define( columns.push(new RangeColumn({ name: "Value" })); } + // We have all columns now, so populate the headers + // for these columns in the scope. $scope.headers = columns.map(function (column) { return column.getTitle(); }); + // Fill in the contents of the rows. updateRows(); } From 4905e68ad2504cf3d8ed2c29aca41f05ffe92ef5 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Tue, 2 Dec 2014 15:21:46 -0800 Subject: [PATCH 3/6] [Scrolling] Include scrolling lists in bundles Include the scrolling list view in the set of active plug-ins. WTD-534. --- bundles.json | 1 + 1 file changed, 1 insertion(+) diff --git a/bundles.json b/bundles.json index c9bd1a9484..bb37355f0e 100644 --- a/bundles.json +++ b/bundles.json @@ -8,6 +8,7 @@ "platform/commonUI/general", "platform/telemetry", "platform/features/plot", + "platform/features/scrolling", "example/generator", "example/persistence" From c0a34149ca3d1389ebfe154aab274ee22a402595 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Tue, 2 Dec 2014 15:25:23 -0800 Subject: [PATCH 4/6] [Scrolling] Add empty tests Add empty tests for scripts associated with the scrolling list view. WTD-534. --- .../features/scrolling/test/DomainColumnSpec.js | 14 ++++++++++++++ platform/features/scrolling/test/NameColumnSpec.js | 14 ++++++++++++++ .../features/scrolling/test/RangeColumnSpec.js | 14 ++++++++++++++ .../scrolling/test/ScrollingListControllerSpec.js | 14 ++++++++++++++ platform/features/scrolling/test/suite.json | 6 ++++++ 5 files changed, 62 insertions(+) create mode 100644 platform/features/scrolling/test/DomainColumnSpec.js create mode 100644 platform/features/scrolling/test/NameColumnSpec.js create mode 100644 platform/features/scrolling/test/RangeColumnSpec.js create mode 100644 platform/features/scrolling/test/ScrollingListControllerSpec.js create mode 100644 platform/features/scrolling/test/suite.json diff --git a/platform/features/scrolling/test/DomainColumnSpec.js b/platform/features/scrolling/test/DomainColumnSpec.js new file mode 100644 index 0000000000..d2f1629c55 --- /dev/null +++ b/platform/features/scrolling/test/DomainColumnSpec.js @@ -0,0 +1,14 @@ +/*global define,describe,it,expect,beforeEach,waitsFor,jasmine*/ + +/** + * MergeModelsSpec. Created by vwoeltje on 11/6/14. + */ +define( + ["../src/DomainColumn"], + function (DomainColumn) { + "use strict"; + + describe("A domain column", function () { + }); + } +); \ No newline at end of file diff --git a/platform/features/scrolling/test/NameColumnSpec.js b/platform/features/scrolling/test/NameColumnSpec.js new file mode 100644 index 0000000000..5763bd6021 --- /dev/null +++ b/platform/features/scrolling/test/NameColumnSpec.js @@ -0,0 +1,14 @@ +/*global define,describe,it,expect,beforeEach,waitsFor,jasmine*/ + +/** + * MergeModelsSpec. Created by vwoeltje on 11/6/14. + */ +define( + ["../src/NameColumn"], + function (NameColumn) { + "use strict"; + + describe("A name column", function () { + }); + } +); \ No newline at end of file diff --git a/platform/features/scrolling/test/RangeColumnSpec.js b/platform/features/scrolling/test/RangeColumnSpec.js new file mode 100644 index 0000000000..39983bc153 --- /dev/null +++ b/platform/features/scrolling/test/RangeColumnSpec.js @@ -0,0 +1,14 @@ +/*global define,describe,it,expect,beforeEach,waitsFor,jasmine*/ + +/** + * MergeModelsSpec. Created by vwoeltje on 11/6/14. + */ +define( + ["../src/RangeColumn"], + function (RangeColumn) { + "use strict"; + + describe("A range column", function () { + }); + } +); \ No newline at end of file diff --git a/platform/features/scrolling/test/ScrollingListControllerSpec.js b/platform/features/scrolling/test/ScrollingListControllerSpec.js new file mode 100644 index 0000000000..25922d442a --- /dev/null +++ b/platform/features/scrolling/test/ScrollingListControllerSpec.js @@ -0,0 +1,14 @@ +/*global define,describe,it,expect,beforeEach,waitsFor,jasmine*/ + +/** + * MergeModelsSpec. Created by vwoeltje on 11/6/14. + */ +define( + ["../src/ScrollingListController"], + function (ScrollingListController) { + "use strict"; + + describe("The scrolling list controller", function () { + }); + } +); \ No newline at end of file diff --git a/platform/features/scrolling/test/suite.json b/platform/features/scrolling/test/suite.json new file mode 100644 index 0000000000..b62eaf30b6 --- /dev/null +++ b/platform/features/scrolling/test/suite.json @@ -0,0 +1,6 @@ +[ + "DomainColumn", + "NameColumn", + "RangeColumn", + "ScrollingListController" +] \ No newline at end of file From e953e5b4b4257145f357bc4a020e9fbdbd6f8cdc Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Tue, 2 Dec 2014 15:59:09 -0800 Subject: [PATCH 5/6] [Scrolling] Fill in specs Fill in specs for ScrollingListController, and for the specific Column types that support it. Separate out the code that produces actual rows in order to improve testability and maintainability. WTD-534. --- .../scrolling/src/ScrollingListController.js | 115 +++--------------- .../scrolling/src/ScrollingListPopulator.js | 115 ++++++++++++++++++ .../scrolling/test/DomainColumnSpec.js | 32 +++++ .../features/scrolling/test/NameColumnSpec.js | 23 ++++ .../scrolling/test/RangeColumnSpec.js | 31 +++++ .../test/ScrollingListControllerSpec.js | 75 ++++++++++++ 6 files changed, 293 insertions(+), 98 deletions(-) create mode 100644 platform/features/scrolling/src/ScrollingListPopulator.js diff --git a/platform/features/scrolling/src/ScrollingListController.js b/platform/features/scrolling/src/ScrollingListController.js index 3df32c4d71..8ef0bb4a4a 100644 --- a/platform/features/scrolling/src/ScrollingListController.js +++ b/platform/features/scrolling/src/ScrollingListController.js @@ -4,8 +4,8 @@ * Module defining ListController. Created by vwoeltje on 11/18/14. */ define( - ["./NameColumn", "./DomainColumn", "./RangeColumn"], - function (NameColumn, DomainColumn, RangeColumn) { + ["./NameColumn", "./DomainColumn", "./RangeColumn", "./ScrollingListPopulator"], + function (NameColumn, DomainColumn, RangeColumn, ScrollingListPopulator) { "use strict"; var ROW_COUNT = 18; @@ -16,104 +16,16 @@ define( * @constructor */ function ScrollingListController($scope) { - var columns = []; // Domain used + var populator; - /** - * Look up the most recent values from a set of data objects. - * Returns an array of objects in the order in which data - * should be displayed; each element is an object with - * two properties: - * - * * objectIndex: The index of the domain object associated - * with the data point to be displayed in that - * row. - * * pointIndex: The index of the data point itself, within - * its data set. - * - * @param {Array} datas an array of the most recent - * data objects; expected to be in the same order - * as the domain objects provided at constructor - * @param {Array= 0) { - nextTime = data.getDomainValue(pointIndex); - if (nextTime > candidateTime) { - candidateTime = nextTime; - candidate = { - objectIndex: i, - pointIndex: pointIndex - }; - } - } - } - - // Assemble a list of the most recent data points - while (latest.length < ROW_COUNT) { - // Reset variables pre-search - candidateTime = Number.NEGATIVE_INFINITY; - candidate = undefined; - - // Linear search for most recent - datas.forEach(findCandidate); - - if (candidate) { - // Record this data point - it is the most recent - latest.push(candidate); - - // Track the data points used so we can look farther back - // in the data set on the next iteration - used[candidate.objectIndex] = used[candidate.objectIndex] + 1; - } else { - // Ran out of candidates; not enough data points - // available to fill all rows. - break; - } - } - - return latest; - } // Get a set of populated, ready-to-display rows for the // latest data values. function getRows(telemetry) { var datas = telemetry.getResponse(), - objects = telemetry.getTelemetryObjects(), - values = getLatestDataValues(datas); + objects = telemetry.getTelemetryObjects(); - // Each value will become a row, which will contain - // some value in each column (rendering by the - // column object itself) - return values.map(function (value) { - return columns.map(function (column) { - return column.getValue( - objects[value.objectIndex], - datas[value.objectIndex], - value.pointIndex - ); - }); - }); + return populator.getRows(datas, objects, ROW_COUNT); } // Update the contents @@ -128,6 +40,7 @@ define( function setupColumns(telemetry) { var domainKeys = {}, rangeKeys = {}, + columns = [], metadata; // Add a domain to the set of columns, if a domain @@ -135,6 +48,7 @@ define( function addDomain(domain) { var key = domain.key; if (key && !domainKeys[key]) { + domainKeys[key] = true; columns.push(new DomainColumn(domain)); } } @@ -144,6 +58,7 @@ define( function addRange(range) { var key = range.key; if (key && !rangeKeys[key]) { + rangeKeys[key] = true; columns.push(new RangeColumn(range)); } } @@ -163,6 +78,8 @@ define( metadata = telemetry.getMetadata(); (metadata || []).forEach(function (metadata) { (metadata.domains || []).forEach(addDomain); + }); + (metadata || []).forEach(function (metadata) { (metadata.ranges || []).forEach(addRange); }); @@ -175,11 +92,13 @@ define( columns.push(new RangeColumn({ name: "Value" })); } - // We have all columns now, so populate the headers - // for these columns in the scope. - $scope.headers = columns.map(function (column) { - return column.getTitle(); - }); + // We have all columns now; use them to initializer + // the populator, which will use them to generate + // actual rows and headers. + populator = new ScrollingListPopulator(columns); + + // Initialize headers + $scope.headers = populator.getHeaders(); // Fill in the contents of the rows. updateRows(); diff --git a/platform/features/scrolling/src/ScrollingListPopulator.js b/platform/features/scrolling/src/ScrollingListPopulator.js new file mode 100644 index 0000000000..b2d1a5da72 --- /dev/null +++ b/platform/features/scrolling/src/ScrollingListPopulator.js @@ -0,0 +1,115 @@ +/*global define*/ + +define( + [], + function () { + "use strict"; + + function ScrollingListPopulator(columns) { + /** + * Look up the most recent values from a set of data objects. + * Returns an array of objects in the order in which data + * should be displayed; each element is an object with + * two properties: + * + * * objectIndex: The index of the domain object associated + * with the data point to be displayed in that + * row. + * * pointIndex: The index of the data point itself, within + * its data set. + * + * @param {Array} datas an array of the most recent + * data objects; expected to be in the same order + * as the domain objects provided at constructor + * @param {Array= 0) { + nextTime = data.getDomainValue(pointIndex); + if (nextTime > candidateTime) { + candidateTime = nextTime; + candidate = { + objectIndex: i, + pointIndex: pointIndex + }; + } + } + } + + // Assemble a list of the most recent data points + while (latest.length < count) { + // Reset variables pre-search + candidateTime = Number.NEGATIVE_INFINITY; + candidate = undefined; + + // Linear search for most recent + datas.forEach(findCandidate); + + if (candidate) { + // Record this data point - it is the most recent + latest.push(candidate); + + // Track the data points used so we can look farther back + // in the data set on the next iteration + used[candidate.objectIndex] = used[candidate.objectIndex] + 1; + } else { + // Ran out of candidates; not enough data points + // available to fill all rows. + break; + } + } + + return latest; + } + + + return { + getHeaders: function () { + return columns.map(function (column) { + return column.getTitle(); + }); + }, + getRows: function (datas, objects, count) { + var values = getLatestDataValues(datas, count); + + // Each value will become a row, which will contain + // some value in each column (rendering by the + // column object itself) + return values.map(function (value) { + return columns.map(function (column) { + return column.getValue( + objects[value.objectIndex], + datas[value.objectIndex], + value.pointIndex + ); + }); + }); + } + }; + } + + return ScrollingListPopulator; + + } +); \ No newline at end of file diff --git a/platform/features/scrolling/test/DomainColumnSpec.js b/platform/features/scrolling/test/DomainColumnSpec.js index d2f1629c55..9fe38779e1 100644 --- a/platform/features/scrolling/test/DomainColumnSpec.js +++ b/platform/features/scrolling/test/DomainColumnSpec.js @@ -9,6 +9,38 @@ define( "use strict"; describe("A domain column", function () { + var mockDataSet, + testMetadata, + column; + + beforeEach(function () { + mockDataSet = jasmine.createSpyObj( + "data", + [ "getDomainValue" ] + ); + testMetadata = { + key: "testKey", + name: "Test Name" + }; + column = new DomainColumn(testMetadata); + }); + + it("reports a column header from domain metadata", function () { + expect(column.getTitle()).toEqual("Test Name"); + }); + + it("looks up data from a data set", function () { + column.getValue(undefined, mockDataSet, 42); + expect(mockDataSet.getDomainValue) + .toHaveBeenCalledWith(42, "testKey"); + }); + + it("formats domain values as time", function () { + mockDataSet.getDomainValue.andReturn(402513731000); + expect(column.getValue(undefined, mockDataSet, 42)) + .toEqual("1982-276 17:22:11"); + }); + }); } ); \ No newline at end of file diff --git a/platform/features/scrolling/test/NameColumnSpec.js b/platform/features/scrolling/test/NameColumnSpec.js index 5763bd6021..696cc07707 100644 --- a/platform/features/scrolling/test/NameColumnSpec.js +++ b/platform/features/scrolling/test/NameColumnSpec.js @@ -9,6 +9,29 @@ define( "use strict"; 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)) + .toEqual("Test object name"); + }); + }); } ); \ No newline at end of file diff --git a/platform/features/scrolling/test/RangeColumnSpec.js b/platform/features/scrolling/test/RangeColumnSpec.js index 39983bc153..efb262420a 100644 --- a/platform/features/scrolling/test/RangeColumnSpec.js +++ b/platform/features/scrolling/test/RangeColumnSpec.js @@ -9,6 +9,37 @@ define( "use strict"; describe("A range column", function () { + var mockDataSet, + testMetadata, + column; + + beforeEach(function () { + mockDataSet = jasmine.createSpyObj( + "data", + [ "getRangeValue" ] + ); + testMetadata = { + key: "testKey", + name: "Test Name" + }; + column = new RangeColumn(testMetadata); + }); + + it("reports a column header from range metadata", function () { + expect(column.getTitle()).toEqual("Test Name"); + }); + + it("looks up data from a data set", function () { + column.getValue(undefined, mockDataSet, 42); + expect(mockDataSet.getRangeValue) + .toHaveBeenCalledWith(42, "testKey"); + }); + + it("formats range values as time", function () { + mockDataSet.getRangeValue.andReturn(123.45678); + expect(column.getValue(undefined, mockDataSet, 42)) + .toEqual("123.457"); + }); }); } ); \ No newline at end of file diff --git a/platform/features/scrolling/test/ScrollingListControllerSpec.js b/platform/features/scrolling/test/ScrollingListControllerSpec.js index 25922d442a..7aec4a7e40 100644 --- a/platform/features/scrolling/test/ScrollingListControllerSpec.js +++ b/platform/features/scrolling/test/ScrollingListControllerSpec.js @@ -9,6 +9,81 @@ define( "use strict"; describe("The scrolling list controller", function () { + var mockScope, + mockTelemetry, + testMetadata, + controller; + + beforeEach(function () { + mockScope = jasmine.createSpyObj( + "$scope", + [ "$on", "$watch" ] + ); + mockTelemetry = jasmine.createSpyObj( + "telemetryController", + [ "getResponse", "getMetadata", "getTelemetryObjects" ] + ); + testMetadata = [ + { + domains: [ + { key: "d0", name: "D0" }, + { key: "d1", name: "D1" } + ], + ranges: [ + { key: "r0", name: "R0" }, + { key: "r1", name: "R1" } + ] + }, + { + domains: [ + { key: "d0", name: "D0" }, + { key: "d2", name: "D2" } + ], + ranges: [ + { key: "r0", name: "R0" } + ] + } + ]; + mockTelemetry.getMetadata.andReturn(testMetadata); + mockTelemetry.getResponse.andReturn([]); + mockTelemetry.getTelemetryObjects.andReturn([]); + mockScope.telemetry = mockTelemetry; + controller = new ScrollingListController(mockScope); + }); + + it("listens for telemetry data updates", function () { + expect(mockScope.$on).toHaveBeenCalledWith( + "telemetryUpdate", + jasmine.any(Function) + ); + }); + + it("watches for telemetry controller changes", function () { + expect(mockScope.$watch).toHaveBeenCalledWith( + "telemetry", + jasmine.any(Function) + ); + }); + + it("provides a column for each name and each unique domain, range", function () { + // Should have six columns based on metadata above, + // (name, d0, d1, d2, r0, r1) + mockScope.$watch.mostRecentCall.args[1](mockTelemetry); + expect(mockScope.headers).toEqual(["Name", "D0", "D1", "D2", "R0", "R1"]); + }); + + it("does not throw if telemetry controller is undefined", function () { + // Just a general robustness check + mockScope.telemetry = undefined; + expect(mockScope.$watch.mostRecentCall.args[1]) + .not.toThrow(); + }); + + it("provides default columns if domain/range metadata is unavailable", function () { + mockTelemetry.getMetadata.andReturn([]); + mockScope.$watch.mostRecentCall.args[1](mockTelemetry); + expect(mockScope.headers).toEqual(["Name", "Time", "Value"]); + }); }); } ); \ No newline at end of file From a1edb6047ce38a7e5e49fb855e6e473daea6c080 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Tue, 2 Dec 2014 16:15:28 -0800 Subject: [PATCH 6/6] [Scrolling] Complete specs for scrolling lists Complete specs for scrolling list views, WTD-534. --- .../test/ScrollingListPopulatorSpec.js | 84 +++++++++++++++++++ platform/features/scrolling/test/suite.json | 3 +- 2 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 platform/features/scrolling/test/ScrollingListPopulatorSpec.js diff --git a/platform/features/scrolling/test/ScrollingListPopulatorSpec.js b/platform/features/scrolling/test/ScrollingListPopulatorSpec.js new file mode 100644 index 0000000000..aeb006bd5c --- /dev/null +++ b/platform/features/scrolling/test/ScrollingListPopulatorSpec.js @@ -0,0 +1,84 @@ +/*global define,describe,it,expect,beforeEach,waitsFor,jasmine*/ + +/** + * MergeModelsSpec. Created by vwoeltje on 11/6/14. + */ +define( + ["../src/ScrollingListPopulator"], + function (ScrollingListPopulator) { + "use strict"; + + describe("The scrolling list populator", function () { + var mockColumns, + mockDatas, + mockDomainObjects, + populator; + + function makeMockColumn(name, index) { + var mockColumn = jasmine.createSpyObj( + "column" + index, + [ "getTitle", "getValue" ] + ); + mockColumn.getTitle.andReturn(name); + mockColumn.getValue.andCallFake(function (obj, data, i) { + return data.getDomainValue(i); + }); + return mockColumn; + } + + function makeMockData(bias, index) { + var mockData = jasmine.createSpyObj( + "data" + index, + [ "getDomainValue", "getPointCount" ] + ); + mockData.getPointCount.andReturn(1000); + mockData.getDomainValue.andCallFake(function (i) { + return i + bias; + }); + return mockData; + } + + function makeMockDomainObject(name, index) { + var mockDomainObject = jasmine.createSpyObj( + "domainObject" + index, + [ "getId", "getModel" ] + ); + return mockDomainObject; + } + + beforeEach(function () { + mockColumns = ["A", "B", "C", "D"].map(makeMockColumn); + mockDatas = [ 10, 0, 3 ].map(makeMockData); + mockDomainObjects = ["A", "B", "C"].map(makeMockDomainObject); + populator = new ScrollingListPopulator(mockColumns); + }); + + it("returns column headers", function () { + expect(populator.getHeaders()).toEqual(["A", "B", "C", "D"]); + }); + + it("provides rows on request, with all columns in each row", function () { + var rows = populator.getRows(mockDatas, mockDomainObjects, 84); + expect(rows.length).toEqual(84); + rows.forEach(function (row) { + expect(row.length).toEqual(4); // number of columns + }); + }); + + it("returns rows in reverse domain order", function () { + var rows = populator.getRows(mockDatas, mockDomainObjects, 84), + previous = Number.POSITIVE_INFINITY; + + // Should always be most-recent-first; since the mockColumn + // returns the domain value, column contents should be + // non-increasing. + rows.forEach(function (row) { + expect(row[0]).not.toBeGreaterThan(previous); + previous = row[0]; + }); + + }); + + }); + } +); \ No newline at end of file diff --git a/platform/features/scrolling/test/suite.json b/platform/features/scrolling/test/suite.json index b62eaf30b6..b36b9c8668 100644 --- a/platform/features/scrolling/test/suite.json +++ b/platform/features/scrolling/test/suite.json @@ -2,5 +2,6 @@ "DomainColumn", "NameColumn", "RangeColumn", - "ScrollingListController" + "ScrollingListController", + "ScrollingListPopulator" ] \ No newline at end of file