From e953e5b4b4257145f357bc4a020e9fbdbd6f8cdc Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Tue, 2 Dec 2014 15:59:09 -0800 Subject: [PATCH] [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