diff --git a/.circleci/config.yml b/.circleci/config.yml index ffc8573372..e17e4f8a18 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,7 +2,7 @@ version: 2 jobs: build: docker: - - image: circleci/node:8-browsers + - image: circleci/node:13-browsers environment: CHROME_BIN: "/usr/bin/google-chrome" steps: @@ -11,12 +11,12 @@ jobs: name: Update npm command: 'sudo npm install -g npm@latest' - restore_cache: - key: dependency-cache-{{ checksum "package.json" }} + key: dependency-cache-13-{{ checksum "package.json" }} - run: name: Installing dependencies (npm install) command: npm install - save_cache: - key: dependency-cache-{{ checksum "package.json" }} + key: dependency-cache-13-{{ checksum "package.json" }} paths: - node_modules - run: diff --git a/package.json b/package.json index 8b6fab4940..53e032e169 100644 --- a/package.json +++ b/package.json @@ -84,10 +84,10 @@ "build:prod": "cross-env NODE_ENV=production webpack", "build:dev": "webpack", "build:watch": "webpack --watch", - "test": "karma start --single-run", + "test": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run", "test:debug": "cross-env NODE_ENV=debug karma start --no-single-run", - "test:coverage": "./scripts/test-coverage.sh", - "test:watch": "karma start --no-single-run", + "test:coverage": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" COVERAGE=true karma start --single-run", + "test:watch": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --no-single-run", "verify": "concurrently 'npm:test' 'npm:lint'", "jsdoc": "jsdoc -c jsdoc.json -R API.md -r -d dist/docs/api", "otherdoc": "node docs/gendocs.js --in docs/src --out dist/docs --suppress-toc 'docs/src/index.md|docs/src/process/index.md'", diff --git a/scripts/test-coverage.sh b/scripts/test-coverage.sh deleted file mode 100755 index de6807b61b..0000000000 --- a/scripts/test-coverage.sh +++ /dev/null @@ -1,2 +0,0 @@ -export NODE_OPTIONS=--max_old_space_size=4096 -cross-env COVERAGE=true karma start --single-run diff --git a/src/MCT.js b/src/MCT.js index 4ea629f5c2..f219368c97 100644 --- a/src/MCT.js +++ b/src/MCT.js @@ -266,6 +266,7 @@ define([ this.install(this.plugins.WebPage()); this.install(this.plugins.Condition()); this.install(this.plugins.ConditionWidget()); + this.install(this.plugins.URLTimeSettingsSynchronizer()); this.install(this.plugins.NotificationIndicator()); } @@ -433,6 +434,10 @@ define([ plugin(this); }; + MCT.prototype.destroy = function () { + this.emit('destroy'); + }; + MCT.prototype.plugins = plugins; return MCT; diff --git a/src/MCTSpec.js b/src/MCTSpec.js index 53417f4314..65678e6e3d 100644 --- a/src/MCTSpec.js +++ b/src/MCTSpec.js @@ -23,7 +23,7 @@ define([ './plugins/plugins', 'legacyRegistry', - 'testUtils' + 'utils/testing' ], function (plugins, legacyRegistry, testUtils) { describe("MCT", function () { var openmct; @@ -32,6 +32,10 @@ define([ var mockListener; var oldBundles; + beforeAll(() => { + testUtils.resetApplicationState(); + }); + beforeEach(function () { mockPlugin = jasmine.createSpy('plugin'); mockPlugin2 = jasmine.createSpy('plugin2'); @@ -52,6 +56,7 @@ define([ legacyRegistry.delete(bundle); } }); + testUtils.resetApplicationState(openmct); }); it("exposes plugins", function () { diff --git a/src/adapter/bundle.js b/src/adapter/bundle.js index 94b1c73823..aca7248b21 100644 --- a/src/adapter/bundle.js +++ b/src/adapter/bundle.js @@ -29,7 +29,6 @@ define([ './capabilities/APICapabilityDecorator', './policies/AdaptedViewPolicy', './runs/AlternateCompositionInitializer', - './runs/TimeSettingsURLHandler', './runs/TypeDeprecationChecker', './runs/LegacyTelemetryProvider', './runs/RegisterLegacyTypes', @@ -46,7 +45,6 @@ define([ APICapabilityDecorator, AdaptedViewPolicy, AlternateCompositionInitializer, - TimeSettingsURLHandler, TypeDeprecationChecker, LegacyTelemetryProvider, RegisterLegacyTypes, @@ -134,16 +132,6 @@ define([ implementation: AlternateCompositionInitializer, depends: ["openmct"] }, - { - implementation: function (openmct, $location, $rootScope) { - return new TimeSettingsURLHandler( - openmct.time, - $location, - $rootScope - ); - }, - depends: ["openmct", "$location", "$rootScope"] - }, { implementation: LegacyTelemetryProvider, depends: [ diff --git a/src/adapter/runs/TimeSettingsURLHandler.js b/src/adapter/runs/TimeSettingsURLHandler.js deleted file mode 100644 index 1806cc66dc..0000000000 --- a/src/adapter/runs/TimeSettingsURLHandler.js +++ /dev/null @@ -1,150 +0,0 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2018, 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([ - 'lodash' -], function ( - _ -) { - // Parameter names in query string - var SEARCH = { - MODE: 'tc.mode', - TIME_SYSTEM: 'tc.timeSystem', - START_BOUND: 'tc.startBound', - END_BOUND: 'tc.endBound', - START_DELTA: 'tc.startDelta', - END_DELTA: 'tc.endDelta' - }; - var TIME_EVENTS = ['bounds', 'timeSystem', 'clock', 'clockOffsets']; - // Used to shorthand calls to $location, which clears null parameters - var NULL_PARAMETERS = { key: null, start: null, end: null }; - - /** - * Communicates settings from the URL to the time API, - * and vice versa. - */ - function TimeSettingsURLHandler(time, $location, $rootScope) { - this.time = time; - this.$location = $location; - - $rootScope.$on('$locationChangeSuccess', this.updateTime.bind(this)); - - TIME_EVENTS.forEach(function (event) { - this.time.on(event, this.updateQueryParams.bind(this)); - }, this); - - this.updateTime(); // Initialize - } - - TimeSettingsURLHandler.prototype.updateQueryParams = function () { - var clock = this.time.clock(); - var fixed = !clock; - var mode = fixed ? 'fixed' : clock.key; - var timeSystem = this.time.timeSystem() || NULL_PARAMETERS; - var bounds = fixed ? this.time.bounds() : NULL_PARAMETERS; - var deltas = fixed ? NULL_PARAMETERS : this.time.clockOffsets(); - - bounds = bounds || NULL_PARAMETERS; - deltas = deltas || NULL_PARAMETERS; - if (deltas.start) { - deltas = { start: -deltas.start, end: deltas.end }; - } - - this.$location.search(SEARCH.MODE, mode); - this.$location.search(SEARCH.TIME_SYSTEM, timeSystem.key); - this.$location.search(SEARCH.START_BOUND, bounds.start); - this.$location.search(SEARCH.END_BOUND, bounds.end); - this.$location.search(SEARCH.START_DELTA, deltas.start); - this.$location.search(SEARCH.END_DELTA, deltas.end); - }; - - TimeSettingsURLHandler.prototype.parseQueryParams = function () { - var searchParams = _.pick(this.$location.search(), Object.values(SEARCH)); - var parsedParams = { - clock: searchParams[SEARCH.MODE], - timeSystem: searchParams[SEARCH.TIME_SYSTEM] - }; - if (!isNaN(parseInt(searchParams[SEARCH.START_DELTA], 0xA)) && - !isNaN(parseInt(searchParams[SEARCH.END_DELTA], 0xA))) { - parsedParams.clockOffsets = { - start: -searchParams[SEARCH.START_DELTA], - end: +searchParams[SEARCH.END_DELTA] - }; - } - if (!isNaN(parseInt(searchParams[SEARCH.START_BOUND], 0xA)) && - !isNaN(parseInt(searchParams[SEARCH.END_BOUND], 0xA))) { - parsedParams.bounds = { - start: +searchParams[SEARCH.START_BOUND], - end: +searchParams[SEARCH.END_BOUND] - }; - } - return parsedParams; - }; - - TimeSettingsURLHandler.prototype.updateTime = function () { - var params = this.parseQueryParams(); - if (_.isEqual(params, this.last)) { - return; // Do nothing; - } - this.last = params; - - if (!params.timeSystem) { - this.updateQueryParams(); - } else if (params.clock === 'fixed' && params.bounds) { - if (!this.time.timeSystem() || - this.time.timeSystem().key !== params.timeSystem) { - - this.time.timeSystem( - params.timeSystem, - params.bounds - ); - } else if (!_.isEqual(this.time.bounds(), params.bounds)) { - this.time.bounds(params.bounds); - } - if (this.time.clock()) { - this.time.stopClock(); - } - } else if (params.clockOffsets) { - if (params.clock === 'fixed') { - this.time.stopClock(); - return; - } - if (!this.time.clock() || - this.time.clock().key !== params.clock) { - - this.time.clock(params.clock, params.clockOffsets); - } else if (!_.isEqual(this.time.clockOffsets(), params.clockOffsets)) { - this.time.clockOffsets(params.clockOffsets); - } - if (!this.time.timeSystem() || - this.time.timeSystem().key !== params.timeSystem) { - - this.time.timeSystem(params.timeSystem); - } - } else { - // Neither found, update from timeSystem. - this.updateQueryParams(); - } - }; - - return TimeSettingsURLHandler; -}); diff --git a/src/adapter/runs/TimeSettingsURLHandlerSpec.js b/src/adapter/runs/TimeSettingsURLHandlerSpec.js deleted file mode 100644 index 0c5e8115d5..0000000000 --- a/src/adapter/runs/TimeSettingsURLHandlerSpec.js +++ /dev/null @@ -1,576 +0,0 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2018, 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([ - './TimeSettingsURLHandler', - '../../api/time/TimeAPI' -], function ( - TimeSettingsURLHandler, - TimeAPI -) { - describe("TimeSettingsURLHandler", function () { - var time; - var $location; - var $rootScope; - var search; - var handler; // eslint-disable-line - var clockA; - var clockB; - var timeSystemA; - var timeSystemB; - var boundsA; - var boundsB; - var offsetsA; - var offsetsB; - var initialize; - var triggerLocationChange; - - - beforeEach(function () { - clockA = jasmine.createSpyObj('clockA', ['on', 'off']); - clockA.key = 'clockA'; - clockA.currentValue = function () { - return 1000; - }; - clockB = jasmine.createSpyObj('clockB', ['on', 'off']); - clockB.key = 'clockB'; - clockB.currentValue = function () { - return 2000; - }; - timeSystemA = {key: 'timeSystemA'}; - timeSystemB = {key: 'timeSystemB'}; - boundsA = { - start: 10, - end: 20 - }; - boundsB = { - start: 120, - end: 360 - }; - offsetsA = { - start: -100, - end: 0 - }; - offsetsB = { - start: -50, - end: 50 - }; - - time = new TimeAPI(); - - [ - 'on', - 'bounds', - 'clockOffsets', - 'timeSystem', - 'clock', - 'stopClock' - ].forEach(function (method) { - spyOn(time, method).and.callThrough(); - }); - time.addTimeSystem(timeSystemA); - time.addTimeSystem(timeSystemB); - time.addClock(clockA); - time.addClock(clockB); - - $location = jasmine.createSpyObj('$location', [ - 'search' - ]); - $rootScope = jasmine.createSpyObj('$rootScope', [ - '$on' - ]); - - search = {}; - $location.search.and.callFake(function (key, value) { - if (arguments.length === 0) { - return search; - } - if (value === null) { - delete search[key]; - } else { - search[key] = String(value); - } - return this; - }); - - expect(time.timeSystem()).toBeUndefined(); - expect(time.bounds()).toEqual({}); - expect(time.clockOffsets()).toBeUndefined(); - expect(time.clock()).toBeUndefined(); - - initialize = function () { - handler = new TimeSettingsURLHandler( - time, - $location, - $rootScope - ); - expect($rootScope.$on).toHaveBeenCalledWith( - '$locationChangeSuccess', - jasmine.any(Function) - ); - triggerLocationChange = $rootScope.$on.calls.mostRecent().args[1]; - - }; - }); - - it("initializes with missing time system", function () { - // This handles an odd transitory case where a url does not include - // a timeSystem. It's generally only experienced by those who - // based their code on the tutorial before it specified a time - // system. - search['tc.mode'] = 'clockA'; - search['tc.timeSystem'] = undefined; - search['tc.startDelta'] = '123'; - search['tc.endDelta'] = '456'; - - // We don't specify behavior right now other than "don't break." - expect(initialize).not.toThrow(); - }); - - it("can initalize fixed mode from location", function () { - search['tc.mode'] = 'fixed'; - search['tc.timeSystem'] = 'timeSystemA'; - search['tc.startBound'] = '123'; - search['tc.endBound'] = '456'; - - initialize(); - - expect(time.timeSystem).toHaveBeenCalledWith( - 'timeSystemA', - { - start: 123, - end: 456 - } - ); - }); - - it("can initialize clock mode from location", function () { - search['tc.mode'] = 'clockA'; - search['tc.timeSystem'] = 'timeSystemA'; - search['tc.startDelta'] = '123'; - search['tc.endDelta'] = '456'; - - initialize(); - - expect(time.clock).toHaveBeenCalledWith( - 'clockA', - { - start: -123, - end: 456 - } - ); - expect(time.timeSystem).toHaveBeenCalledWith( - 'timeSystemA' - ); - }); - - it("can initialize fixed mode from time API", function () { - time.timeSystem(timeSystemA.key, boundsA); - initialize(); - expect($location.search) - .toHaveBeenCalledWith('tc.mode', 'fixed'); - expect($location.search) - .toHaveBeenCalledWith('tc.timeSystem', 'timeSystemA'); - expect($location.search) - .toHaveBeenCalledWith('tc.startBound', 10); - expect($location.search) - .toHaveBeenCalledWith('tc.endBound', 20); - expect($location.search) - .toHaveBeenCalledWith('tc.startDelta', null); - expect($location.search) - .toHaveBeenCalledWith('tc.endDelta', null); - }); - - it("can initialize clock mode from time API", function () { - time.clock(clockA.key, offsetsA); - time.timeSystem(timeSystemA.key); - initialize(); - expect($location.search) - .toHaveBeenCalledWith('tc.mode', 'clockA'); - expect($location.search) - .toHaveBeenCalledWith('tc.timeSystem', 'timeSystemA'); - expect($location.search) - .toHaveBeenCalledWith('tc.startBound', null); - expect($location.search) - .toHaveBeenCalledWith('tc.endBound', null); - expect($location.search) - .toHaveBeenCalledWith('tc.startDelta', 100); - expect($location.search) - .toHaveBeenCalledWith('tc.endDelta', 0); - }); - - describe('location changes in fixed mode', function () { - - beforeEach(function () { - time.timeSystem(timeSystemA.key, boundsA); - initialize(); - time.timeSystem.calls.reset(); - time.bounds.calls.reset(); - time.clock.calls.reset(); - time.stopClock.calls.reset(); - }); - - it("does not change on spurious location change", function () { - triggerLocationChange(); - expect(time.timeSystem).not.toHaveBeenCalledWith( - 'timeSystemA', - jasmine.any(Object) - ); - expect(time.bounds).not.toHaveBeenCalledWith( - jasmine.any(Object) - ); - expect(time.stopClock).not.toHaveBeenCalled(); - }); - - it("updates timeSystem changes", function () { - search['tc.timeSystem'] = 'timeSystemB'; - triggerLocationChange(); - expect(time.timeSystem).toHaveBeenCalledWith( - 'timeSystemB', - { - start: 10, - end: 20 - } - ); - }); - - it("updates bounds changes", function () { - search['tc.startBound'] = '100'; - search['tc.endBound'] = '200'; - triggerLocationChange(); - expect(time.timeSystem).not.toHaveBeenCalledWith( - jasmine.anything(), jasmine.anything() - ); - expect(time.bounds).toHaveBeenCalledWith({ - start: 100, - end: 200 - }); - search['tc.endBound'] = '300'; - triggerLocationChange(); - expect(time.timeSystem).not.toHaveBeenCalledWith( - jasmine.anything(), jasmine.anything() - ); - expect(time.bounds).toHaveBeenCalledWith({ - start: 100, - end: 300 - }); - }); - - it("updates clock mode w/o timeSystem change", function () { - search['tc.mode'] = 'clockA'; - search['tc.startDelta'] = '50'; - search['tc.endDelta'] = '50'; - delete search['tc.endBound']; - delete search['tc.startBound']; - triggerLocationChange(); - expect(time.clock).toHaveBeenCalledWith( - 'clockA', - { - start: -50, - end: 50 - } - ); - expect(time.timeSystem).not.toHaveBeenCalledWith( - jasmine.anything(), jasmine.anything() - ); - }); - - it("updates clock mode and timeSystem", function () { - search['tc.mode'] = 'clockA'; - search['tc.startDelta'] = '50'; - search['tc.endDelta'] = '50'; - search['tc.timeSystem'] = 'timeSystemB'; - delete search['tc.endBound']; - delete search['tc.startBound']; - triggerLocationChange(); - expect(time.clock).toHaveBeenCalledWith( - 'clockA', - { - start: -50, - end: 50 - } - ); - expect(time.timeSystem).toHaveBeenCalledWith('timeSystemB'); - }); - }); - - describe('location changes in clock mode', function () { - - beforeEach(function () { - time.clock(clockA.key, offsetsA); - time.timeSystem(timeSystemA.key); - initialize(); - time.timeSystem.calls.reset(); - time.bounds.calls.reset(); - time.clock.calls.reset(); - time.clockOffsets.calls.reset(); - time.stopClock.calls.reset(); - }); - - it("does not change on spurious location change", function () { - triggerLocationChange(); - expect(time.timeSystem).not.toHaveBeenCalledWith( - 'timeSystemA', - jasmine.any(Object) - ); - expect(time.clockOffsets).not.toHaveBeenCalledWith( - jasmine.any(Object) - ); - expect(time.clock).not.toHaveBeenCalledWith( - jasmine.any(Object) - ); - expect(time.bounds).not.toHaveBeenCalledWith( - jasmine.any(Object) - ); - }); - - it("changes time system", function () { - search['tc.timeSystem'] = 'timeSystemB'; - triggerLocationChange(); - expect(time.timeSystem).toHaveBeenCalledWith( - 'timeSystemB' - ); - expect(time.clockOffsets).not.toHaveBeenCalledWith( - jasmine.any(Object) - ); - expect(time.clock).not.toHaveBeenCalledWith( - jasmine.any(Object) - ); - expect(time.stopClock).not.toHaveBeenCalled(); - expect(time.bounds).not.toHaveBeenCalledWith( - jasmine.any(Object) - ); - }); - - it("changes offsets", function () { - search['tc.startDelta'] = '50'; - search['tc.endDelta'] = '50'; - triggerLocationChange(); - expect(time.timeSystem).not.toHaveBeenCalledWith( - 'timeSystemA', - jasmine.any(Object) - ); - expect(time.clockOffsets).toHaveBeenCalledWith( - { - start: -50, - end: 50 - } - ); - expect(time.clock).not.toHaveBeenCalledWith( - jasmine.any(Object) - ); - }); - - it("updates to fixed w/o timeSystem change", function () { - search['tc.mode'] = 'fixed'; - search['tc.startBound'] = '234'; - search['tc.endBound'] = '567'; - delete search['tc.endDelta']; - delete search['tc.startDelta']; - - triggerLocationChange(); - expect(time.stopClock).toHaveBeenCalled(); - expect(time.bounds).toHaveBeenCalledWith({ - start: 234, - end: 567 - }); - expect(time.timeSystem).not.toHaveBeenCalledWith( - jasmine.anything(), jasmine.anything() - ); - }); - - it("updates fixed and timeSystem", function () { - search['tc.mode'] = 'fixed'; - search['tc.startBound'] = '234'; - search['tc.endBound'] = '567'; - search['tc.timeSystem'] = 'timeSystemB'; - delete search['tc.endDelta']; - delete search['tc.startDelta']; - - triggerLocationChange(); - expect(time.stopClock).toHaveBeenCalled(); - expect(time.timeSystem).toHaveBeenCalledWith( - 'timeSystemB', - { - start: 234, - end: 567 - } - ); - }); - - it("updates clock", function () { - search['tc.mode'] = 'clockB'; - triggerLocationChange(); - expect(time.clock).toHaveBeenCalledWith( - 'clockB', - { - start: -100, - end: 0 - } - ); - expect(time.timeSystem).not.toHaveBeenCalledWith(jasmine.anything()); - }); - - it("updates clock and timeSystem", function () { - search['tc.mode'] = 'clockB'; - search['tc.timeSystem'] = 'timeSystemB'; - triggerLocationChange(); - expect(time.clock).toHaveBeenCalledWith( - 'clockB', - { - start: -100, - end: 0 - } - ); - expect(time.timeSystem).toHaveBeenCalledWith( - 'timeSystemB' - ); - }); - - it("updates clock and timeSystem and offsets", function () { - search['tc.mode'] = 'clockB'; - search['tc.timeSystem'] = 'timeSystemB'; - search['tc.startDelta'] = '50'; - search['tc.endDelta'] = '50'; - triggerLocationChange(); - expect(time.clock).toHaveBeenCalledWith( - 'clockB', - { - start: -50, - end: 50 - } - ); - expect(time.timeSystem).toHaveBeenCalledWith( - 'timeSystemB' - ); - }); - - it("stops the clock", function () { - // this is a robustness test, unsure if desired, requires - // user to be manually editing location strings. - search['tc.mode'] = 'fixed'; - triggerLocationChange(); - expect(time.stopClock).toHaveBeenCalled(); - }); - }); - - - describe("location updates from time API in fixed", function () { - beforeEach(function () { - time.timeSystem(timeSystemA.key, boundsA); - initialize(); - }); - - it("updates on bounds change", function () { - time.bounds(boundsB); - expect(search).toEqual({ - 'tc.mode': 'fixed', - 'tc.startBound': '120', - 'tc.endBound': '360', - 'tc.timeSystem': 'timeSystemA' - }); - }); - - it("updates on timeSystem change", function () { - time.timeSystem(timeSystemB, boundsA); - expect(search).toEqual({ - 'tc.mode': 'fixed', - 'tc.startBound': '10', - 'tc.endBound': '20', - 'tc.timeSystem': 'timeSystemB' - }); - time.timeSystem(timeSystemA, boundsB); - expect(search).toEqual({ - 'tc.mode': 'fixed', - 'tc.startBound': '120', - 'tc.endBound': '360', - 'tc.timeSystem': 'timeSystemA' - }); - }); - - it("Updates to clock", function () { - time.clock(clockA, offsetsA); - expect(search).toEqual({ - 'tc.mode': 'clockA', - 'tc.startDelta': '100', - 'tc.endDelta': '0', - 'tc.timeSystem': 'timeSystemA' - }); - }); - }); - - describe("location updates from time API in fixed", function () { - beforeEach(function () { - time.clock(clockA.key, offsetsA); - time.timeSystem(timeSystemA.key); - initialize(); - }); - - it("updates offsets", function () { - time.clockOffsets(offsetsB); - expect(search).toEqual({ - 'tc.mode': 'clockA', - 'tc.startDelta': '50', - 'tc.endDelta': '50', - 'tc.timeSystem': 'timeSystemA' - }); - }); - - it("updates clocks", function () { - time.clock(clockB, offsetsA); - expect(search).toEqual({ - 'tc.mode': 'clockB', - 'tc.startDelta': '100', - 'tc.endDelta': '0', - 'tc.timeSystem': 'timeSystemA' - }); - time.clock(clockA, offsetsB); - expect(search).toEqual({ - 'tc.mode': 'clockA', - 'tc.startDelta': '50', - 'tc.endDelta': '50', - 'tc.timeSystem': 'timeSystemA' - }); - }); - - it("updates timesystems", function () { - time.timeSystem(timeSystemB); - expect(search).toEqual({ - 'tc.mode': 'clockA', - 'tc.startDelta': '100', - 'tc.endDelta': '0', - 'tc.timeSystem': 'timeSystemB' - }); - }); - - it("stops the clock", function () { - time.stopClock(); - expect(search).toEqual({ - 'tc.mode': 'fixed', - 'tc.startBound': '900', - 'tc.endBound': '1000', - 'tc.timeSystem': 'timeSystemA' - }); - }); - }); - }); -}); diff --git a/src/plugins/URLTimeSettingsSynchronizer/URLTimeSettingsSynchronizer.js b/src/plugins/URLTimeSettingsSynchronizer/URLTimeSettingsSynchronizer.js new file mode 100644 index 0000000000..878b66d221 --- /dev/null +++ b/src/plugins/URLTimeSettingsSynchronizer/URLTimeSettingsSynchronizer.js @@ -0,0 +1,230 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2020, 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. + *****************************************************************************/ +import { + getAllSearchParams, + setAllSearchParams +} from 'utils/openmctLocation'; + +const TIME_EVENTS = ['bounds', 'timeSystem', 'clock', 'clockOffsets']; +const SEARCH_MODE = 'tc.mode'; +const SEARCH_TIME_SYSTEM = 'tc.timeSystem'; +const SEARCH_START_BOUND = 'tc.startBound'; +const SEARCH_END_BOUND = 'tc.endBound'; +const SEARCH_START_DELTA = 'tc.startDelta'; +const SEARCH_END_DELTA = 'tc.endDelta'; +const MODE_FIXED = 'fixed'; + +export default class URLTimeSettingsSynchronizer { + constructor(openmct) { + this.openmct = openmct; + this.isUrlUpdateInProgress = false; + + this.initialize = this.initialize.bind(this); + this.destroy = this.destroy.bind(this); + this.updateTimeSettings = this.updateTimeSettings.bind(this); + this.setUrlFromTimeApi = this.setUrlFromTimeApi.bind(this); + + openmct.on('start', this.initialize); + openmct.on('destroy', this.destroy); + } + + initialize() { + this.updateTimeSettings(); + + window.addEventListener('hashchange', this.updateTimeSettings); + TIME_EVENTS.forEach(event => { + this.openmct.time.on(event, this.setUrlFromTimeApi); + }); + + } + + destroy() { + window.removeEventListener('hashchange', this.updateTimeSettings); + this.openmct.off('start', this.initialize); + this.openmct.off('destroy', this.destroy); + + TIME_EVENTS.forEach(event => { + this.openmct.time.off(event, this.setUrlFromTimeApi); + }); + } + + updateTimeSettings() { + // Prevent from triggering self + if (!this.isUrlUpdateInProgress) { + let timeParameters = this.parseParametersFromUrl(); + + + if (this.areTimeParametersValid(timeParameters)) { + this.setTimeApiFromUrl(timeParameters); + } else { + this.setUrlFromTimeApi(); + } + } else { + this.isUrlUpdateInProgress = false; + } + } + + parseParametersFromUrl() { + let searchParams = getAllSearchParams(); + + let mode = searchParams.get(SEARCH_MODE); + let timeSystem = searchParams.get(SEARCH_TIME_SYSTEM); + + let startBound = parseInt(searchParams.get(SEARCH_START_BOUND), 10); + let endBound = parseInt(searchParams.get(SEARCH_END_BOUND), 10); + let bounds = { + start: startBound, + end: endBound + }; + + let startOffset = parseInt(searchParams.get(SEARCH_START_DELTA), 10); + let endOffset = parseInt(searchParams.get(SEARCH_END_DELTA), 10); + let clockOffsets = { + start: 0 - startOffset, + end: endOffset + }; + + return { + mode, + timeSystem, + bounds, + clockOffsets + }; + } + + setTimeApiFromUrl(timeParameters) { + if (timeParameters.mode === 'fixed') { + if (this.openmct.time.timeSystem().key !== timeParameters.timeSystem) { + this.openmct.time.timeSystem( + timeParameters.timeSystem, + timeParameters.bounds + ); + } else if (!this.areStartAndEndEqual(this.openmct.time.bounds(), timeParameters.bounds)) { + this.openmct.time.bounds(timeParameters.bounds); + } + if (this.openmct.time.clock()) { + this.openmct.time.stopClock(); + } + } else { + if (!this.openmct.time.clock() || + this.openmct.time.clock().key !== timeParameters.mode) { + this.openmct.time.clock(timeParameters.mode, timeParameters.clockOffsets); + } else if (!this.areStartAndEndEqual(this.openmct.time.clockOffsets(), timeParameters.clockOffsets)) { + this.openmct.time.clockOffsets(timeParameters.clockOffsets); + } + if (!this.openmct.time.timeSystem() || + this.openmct.time.timeSystem().key !== timeParameters.timeSystem) { + this.openmct.time.timeSystem(timeParameters.timeSystem); + } + } + } + + setUrlFromTimeApi() { + let searchParams = getAllSearchParams(); + let clock = this.openmct.time.clock(); + let bounds = this.openmct.time.bounds(); + let clockOffsets = this.openmct.time.clockOffsets(); + + if (clock === undefined) { + searchParams.set(SEARCH_MODE, MODE_FIXED); + searchParams.set(SEARCH_START_BOUND, bounds.start); + searchParams.set(SEARCH_END_BOUND, bounds.end); + + searchParams.delete(SEARCH_START_DELTA); + searchParams.delete(SEARCH_END_DELTA); + } else { + searchParams.set(SEARCH_MODE, clock.key); + + if (clockOffsets !== undefined) { + searchParams.set(SEARCH_START_DELTA, 0 - clockOffsets.start); + searchParams.set(SEARCH_END_DELTA, clockOffsets.end); + } else { + searchParams.delete(SEARCH_START_DELTA); + searchParams.delete(SEARCH_END_DELTA); + } + searchParams.delete(SEARCH_START_BOUND); + searchParams.delete(SEARCH_END_BOUND); + } + + searchParams.set(SEARCH_TIME_SYSTEM, this.openmct.time.timeSystem().key); + this.isUrlUpdateInProgress = true; + setAllSearchParams(searchParams); + } + + areTimeParametersValid(timeParameters) { + let isValid = false; + + if (this.isModeValid(timeParameters.mode) && + this.isTimeSystemValid(timeParameters.timeSystem)) { + + if (timeParameters.mode === 'fixed') { + isValid = this.areStartAndEndValid(timeParameters.bounds); + } else { + isValid = this.areStartAndEndValid(timeParameters.clockOffsets); + } + } + + return isValid; + } + + areStartAndEndValid(bounds) { + return bounds !== undefined && + bounds.start !== undefined && + bounds.start !== null && + bounds.end !== undefined && + bounds.start !== null && + !isNaN(bounds.start) && + !isNaN(bounds.end); + } + + isTimeSystemValid(timeSystem) { + let isValid = timeSystem !== undefined; + if (isValid) { + let timeSystemObject = this.openmct.time.timeSystems.get(timeSystem); + isValid = timeSystemObject !== undefined; + } + return isValid; + } + + isModeValid(mode) { + let isValid = false; + + if (mode !== undefined && + mode !== null) { + isValid = true; + } + + if (isValid) { + if (mode.toLowerCase() === MODE_FIXED) { + isValid = true; + } else { + isValid = this.openmct.time.clocks.get(mode) !== undefined; + } + } + return isValid; + } + + areStartAndEndEqual(firstBounds, secondBounds) { + return firstBounds.start === secondBounds.start && + firstBounds.end === secondBounds.end; + } +} diff --git a/src/plugins/URLTimeSettingsSynchronizer/plugin.js b/src/plugins/URLTimeSettingsSynchronizer/plugin.js new file mode 100644 index 0000000000..a6db4dfcf5 --- /dev/null +++ b/src/plugins/URLTimeSettingsSynchronizer/plugin.js @@ -0,0 +1,28 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2020, 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. + *****************************************************************************/ +import URLTimeSettingsSynchronizer from "./URLTimeSettingsSynchronizer.js"; + +export default function () { + return function install(openmct) { + return new URLTimeSettingsSynchronizer(openmct); + } +} diff --git a/src/plugins/URLTimeSettingsSynchronizer/pluginSpec.js b/src/plugins/URLTimeSettingsSynchronizer/pluginSpec.js new file mode 100644 index 0000000000..49943d4581 --- /dev/null +++ b/src/plugins/URLTimeSettingsSynchronizer/pluginSpec.js @@ -0,0 +1,307 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2020, 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. + *****************************************************************************/ +import { + createOpenMct, + resetApplicationState +} from 'utils/testing'; + +describe("The URLTimeSettingsSynchronizer", () => { + let openmct; + let testClock; + beforeAll(() => resetApplicationState()); + + beforeEach((done) => { + openmct = createOpenMct(); + openmct.install(openmct.plugins.LocalTimeSystem()); + testClock = jasmine.createSpyObj("testClock", ["start", "stop", "tick", "currentValue", "on", "off"]); + testClock.key = "test-clock"; + testClock.currentValue.and.returnValue(0); + + openmct.time.addClock(testClock); + + openmct.on('start', done); + openmct.startHeadless(); + }); + + afterEach(() => resetApplicationState(openmct)); + + describe("realtime mode", () => { + it("when the clock is set via the time API, it is immediately reflected in the URL", () => { + //Test expected initial conditions + expect(window.location.hash.includes('tc.mode=fixed')).toBe(true); + + openmct.time.clock('local', {start: -1000, end: 100}); + + expect(window.location.hash.includes('tc.mode=local')).toBe(true); + + //Test that expected initial conditions are no longer true + expect(window.location.hash.includes('tc.mode=fixed')).toBe(false); + }); + it("when offsets are set via the time API, they are immediately reflected in the URL", () => { + //Test expected initial conditions + expect(window.location.hash.includes('tc.startDelta')).toBe(false); + expect(window.location.hash.includes('tc.endDelta')).toBe(false); + + openmct.time.clock('local', {start: -1000, end: 100}); + expect(window.location.hash.includes('tc.startDelta=1000')).toBe(true); + expect(window.location.hash.includes('tc.endDelta=100')).toBe(true); + + openmct.time.clockOffsets({start: -2000, end: 200}); + expect(window.location.hash.includes('tc.startDelta=2000')).toBe(true); + expect(window.location.hash.includes('tc.endDelta=200')).toBe(true); + + //Test that expected initial conditions are no longer true + expect(window.location.hash.includes('tc.mode=fixed')).toBe(false); + }); + describe("when set in the url", () => { + it("will change from fixed to realtime mode when the mode changes", () => { + expectLocationToBeInFixedMode(); + return switchToRealtimeMode().then(() => { + let clock = openmct.time.clock(); + + expect(clock).toBeDefined(); + expect(clock.key).toBe('local'); + }); + }); + it("the clock is correctly set in the API from the URL parameters", () => { + return switchToRealtimeMode().then(() => { + let resolveFunction; + return new Promise((resolve) => { + resolveFunction = resolve; + + //The 'hashchange' event appears to be asynchronous, so we need to wait until a clock change has been + //detected in the API. + openmct.time.on('clock', resolveFunction); + let hash = window.location.hash; + hash = hash.replace('tc.mode=local', 'tc.mode=test-clock'); + window.location.hash = hash; + }).then(() => { + let clock = openmct.time.clock(); + expect(clock).toBeDefined(); + expect(clock.key).toBe('test-clock'); + openmct.time.off('clock', resolveFunction); + }); + }); + }); + it("the clock offsets are correctly set in the API from the URL parameters", () => { + return switchToRealtimeMode().then(() => { + let resolveFunction; + return new Promise((resolve) => { + resolveFunction = resolve; + //The 'hashchange' event appears to be asynchronous, so we need to wait until a clock change has been + //detected in the API. + openmct.time.on('clockOffsets', resolveFunction); + let hash = window.location.hash; + hash = hash.replace('tc.startDelta=1000', 'tc.startDelta=2000'); + hash = hash.replace('tc.endDelta=100', 'tc.endDelta=200'); + window.location.hash = hash; + }).then(() => { + let clockOffsets = openmct.time.clockOffsets(); + expect(clockOffsets).toBeDefined(); + expect(clockOffsets.start).toBe(-2000); + expect(clockOffsets.end).toBe(200); + openmct.time.off('clockOffsets', resolveFunction); + }); + }); + }); + it("the time system is correctly set in the API from the URL parameters", () => { + return switchToRealtimeMode().then(() => { + let resolveFunction; + return new Promise((resolve) => { + resolveFunction = resolve; + + //The 'hashchange' event appears to be asynchronous, so we need to wait until a clock change has been + //detected in the API. + openmct.time.on('timeSystem', resolveFunction); + let hash = window.location.hash; + hash = hash.replace('tc.timeSystem=utc', 'tc.timeSystem=local'); + window.location.hash = hash; + }).then(() => { + let timeSystem = openmct.time.timeSystem(); + expect(timeSystem).toBeDefined(); + expect(timeSystem.key).toBe('local'); + openmct.time.off('timeSystem', resolveFunction); + }); + }); + }); + }); + }); + describe("fixed timespan mode", () => { + beforeEach(() => { + openmct.time.stopClock(); + openmct.time.timeSystem('utc', {start: 0, end: 1}); + }); + + it("when bounds are set via the time API, they are immediately reflected in the URL", ()=>{ + //Test expected initial conditions + expect(window.location.hash.includes('tc.startBound=0')).toBe(true); + expect(window.location.hash.includes('tc.endBound=1')).toBe(true); + + openmct.time.bounds({start: 10, end: 20}); + + expect(window.location.hash.includes('tc.startBound=10')).toBe(true); + expect(window.location.hash.includes('tc.endBound=20')).toBe(true); + + //Test that expected initial conditions are no longer true + expect(window.location.hash.includes('tc.startBound=0')).toBe(false); + expect(window.location.hash.includes('tc.endBound=1')).toBe(false); + }); + + it("when time system is set via the time API, it is immediately reflected in the URL", ()=>{ + //Test expected initial conditions + expect(window.location.hash.includes('tc.timeSystem=utc')).toBe(true); + + openmct.time.timeSystem('local', {start: 20, end: 30}); + + expect(window.location.hash.includes('tc.timeSystem=local')).toBe(true); + + //Test that expected initial conditions are no longer true + expect(window.location.hash.includes('tc.timeSystem=utc')).toBe(false); + }); + describe("when set in the url", () => { + it("time system changes are reflected in the API", () => { + let resolveFunction; + + return new Promise((resolve) => { + let timeSystem = openmct.time.timeSystem(); + resolveFunction = resolve; + + expect(timeSystem.key).toBe('utc'); + window.location.hash = window.location.hash.replace('tc.timeSystem=utc', 'tc.timeSystem=local'); + + openmct.time.on('timeSystem', resolveFunction); + }).then(() => { + let timeSystem = openmct.time.timeSystem(); + expect(timeSystem.key).toBe('local'); + + openmct.time.off('timeSystem', resolveFunction); + }); + }); + it("mode can be changed from realtime to fixed", () => { + return switchToRealtimeMode().then(() => { + expectLocationToBeInRealtimeMode(); + + expect(openmct.time.clock()).toBeDefined(); + }).then(switchToFixedMode).then(() => { + let clock = openmct.time.clock(); + expect(clock).not.toBeDefined(); + }); + }); + it("bounds are correctly set in the API from the URL parameters", () => { + let resolveFunction; + + expectLocationToBeInFixedMode(); + + return new Promise((resolve) => { + resolveFunction = resolve; + openmct.time.on('bounds', resolveFunction); + let hash = window.location.hash; + hash = hash.replace('tc.startBound=0', 'tc.startBound=222') + .replace('tc.endBound=1', 'tc.endBound=333'); + window.location.hash = hash; + }).then(() => { + let bounds = openmct.time.bounds(); + + expect(bounds).toBeDefined(); + expect(bounds.start).toBe(222); + expect(bounds.end).toBe(333); + }); + }); + it("bounds are correctly set in the API from the URL parameters where only the end bound changes", () => { + let resolveFunction; + + expectLocationToBeInFixedMode(); + + return new Promise((resolve) => { + resolveFunction = resolve; + openmct.time.on('bounds', resolveFunction); + let hash = window.location.hash; + hash = hash.replace('tc.endBound=1', 'tc.endBound=333'); + window.location.hash = hash; + }).then(() => { + let bounds = openmct.time.bounds(); + + expect(bounds).toBeDefined(); + expect(bounds.start).toBe(0); + expect(bounds.end).toBe(333); + }); + }); + }); + }); + + function setRealtimeLocationParameters() { + let hash = window.location.hash.toString() + .replace('tc.mode=fixed', 'tc.mode=local') + .replace('tc.startBound=0', 'tc.startDelta=1000') + .replace('tc.endBound=1', 'tc.endDelta=100'); + + window.location.hash = hash; + } + + function setFixedLocationParameters() { + let hash = window.location.hash.toString() + .replace('tc.mode=local', 'tc.mode=fixed') + .replace('tc.timeSystem=utc', 'tc.timeSystem=local') + .replace('tc.startDelta=1000', 'tc.startBound=50') + .replace('tc.endDelta=100', 'tc.endBound=60'); + + window.location.hash = hash; + } + + function switchToRealtimeMode() { + let resolveFunction; + return new Promise((resolve) => { + resolveFunction = resolve; + openmct.time.on('clock', resolveFunction); + setRealtimeLocationParameters(); + }).then(() => { + openmct.time.off('clock', resolveFunction); + }); + } + + function switchToFixedMode() { + let resolveFunction; + return new Promise((resolve) => { + resolveFunction = resolve; + //The 'hashchange' event appears to be asynchronous, so we need to wait until a clock change has been + //detected in the API. + openmct.time.on('clock', resolveFunction); + setFixedLocationParameters(); + }).then(() => { + openmct.time.off('clock', resolveFunction); + }); + } + + function expectLocationToBeInRealtimeMode() { + expect(window.location.hash.includes('tc.mode=local')).toBe(true); + expect(window.location.hash.includes('tc.startDelta=1000')).toBe(true); + expect(window.location.hash.includes('tc.endDelta=100')).toBe(true); + expect(window.location.hash.includes('tc.mode=fixed')).toBe(false); + } + + function expectLocationToBeInFixedMode() { + expect(window.location.hash.includes('tc.mode=fixed')).toBe(true); + expect(window.location.hash.includes('tc.startBound=0')).toBe(true); + expect(window.location.hash.includes('tc.endBound=1')).toBe(true); + expect(window.location.hash.includes('tc.mode=local')).toBe(false); + } +}); diff --git a/src/plugins/condition/pluginSpec.js b/src/plugins/condition/pluginSpec.js index 75702df793..55caedb446 100644 --- a/src/plugins/condition/pluginSpec.js +++ b/src/plugins/condition/pluginSpec.js @@ -20,7 +20,7 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import { createOpenMct } from "testUtils"; +import { createOpenMct, resetApplicationState } from "utils/testing"; import ConditionPlugin from "./plugin"; import StylesView from "./components/inspector/StylesView.vue"; import Vue from 'vue'; @@ -33,6 +33,10 @@ describe('the plugin', function () { let child; let openmct; + beforeAll(() => { + resetApplicationState(openmct); + }); + beforeEach((done) => { openmct = createOpenMct(); openmct.install(new ConditionPlugin()); @@ -57,6 +61,10 @@ describe('the plugin', function () { openmct.startHeadless(); }); + afterEach(() => { + resetApplicationState(openmct); + }); + let mockConditionSetObject = { name: 'Condition Set', key: 'conditionSet', diff --git a/src/plugins/notificationIndicator/pluginSpec.js b/src/plugins/notificationIndicator/pluginSpec.js index 61b6efa486..9ee8401b6d 100644 --- a/src/plugins/notificationIndicator/pluginSpec.js +++ b/src/plugins/notificationIndicator/pluginSpec.js @@ -23,8 +23,9 @@ import NotificationIndicatorPlugin from './plugin.js'; import Vue from 'vue'; import { - createOpenMct -} from 'testUtils'; + createOpenMct, + resetApplicationState +} from 'utils/testing'; describe('the plugin', () => { let notificationIndicatorPlugin, @@ -34,6 +35,10 @@ describe('the plugin', () => { parentElement, mockMessages = ['error', 'test', 'notifications']; + beforeAll(() => { + resetApplicationState(); + }); + beforeEach((done) => { openmct = createOpenMct(); @@ -55,6 +60,10 @@ describe('the plugin', () => { openmct.startHeadless(); }); + afterEach(() => { + resetApplicationState(openmct); + }); + describe('the indicator plugin element', () => { beforeEach(() => { parentElement.append(indicatorElement); diff --git a/src/plugins/plugins.js b/src/plugins/plugins.js index d177dcd9a8..6ba5b8d676 100644 --- a/src/plugins/plugins.js +++ b/src/plugins/plugins.js @@ -52,6 +52,7 @@ define([ './themes/espresso', './themes/maelstrom', './themes/snow', + './URLTimeSettingsSynchronizer/plugin', './notificationIndicator/plugin' ], function ( _, @@ -85,6 +86,7 @@ define([ Espresso, Maelstrom, Snow, + URLTimeSettingsSynchronizer, NotificationIndicator ) { var bundleMap = { @@ -194,6 +196,7 @@ define([ plugins.Snow = Snow.default; plugins.Condition = ConditionPlugin.default; plugins.ConditionWidget = ConditionWidgetPlugin.default; + plugins.URLTimeSettingsSynchronizer = URLTimeSettingsSynchronizer.default; plugins.NotificationIndicator = NotificationIndicator.default; return plugins; diff --git a/src/plugins/telemetryTable/components/table.vue b/src/plugins/telemetryTable/components/table.vue index e64acd8464..a52f07d419 100644 --- a/src/plugins/telemetryTable/components/table.vue +++ b/src/plugins/telemetryTable/components/table.vue @@ -463,6 +463,7 @@ export default { start = end - VISIBLE_ROW_COUNT + 1; } } + this.rowOffset = start; this.visibleRows = filteredRows.slice(start, end); diff --git a/src/plugins/telemetryTable/pluginSpec.js b/src/plugins/telemetryTable/pluginSpec.js index 105000e67a..2b0f0facd6 100644 --- a/src/plugins/telemetryTable/pluginSpec.js +++ b/src/plugins/telemetryTable/pluginSpec.js @@ -23,8 +23,10 @@ import TablePlugin from './plugin.js'; import Vue from 'vue'; import { createOpenMct, - createMouseEvent -} from 'testUtils'; + createMouseEvent, + spyOnBuiltins, + resetApplicationState +} from 'utils/testing'; describe("the plugin", () => { let openmct; @@ -32,6 +34,10 @@ describe("the plugin", () => { let element; let child; + beforeAll(() => { + resetApplicationState(); + }) + beforeEach((done) => { openmct = createOpenMct(); @@ -40,16 +46,27 @@ describe("the plugin", () => { tablePlugin = new TablePlugin(); openmct.install(tablePlugin); + spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([])); + element = document.createElement('div'); child = document.createElement('div'); element.appendChild(child); - spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([])); + openmct.time.timeSystem('utc', {start: 0, end: 4}); + + spyOnBuiltins(['requestAnimationFrame']); + window.requestAnimationFrame.and.callFake((callBack) => { + callBack(); + }); openmct.on('start', done); openmct.startHeadless(); }); + afterEach(() => { + resetApplicationState(openmct); + }); + describe("defines a table object", function () { it("that is creatable", () => { let tableType = openmct.types.get('table'); @@ -86,32 +103,73 @@ describe("the plugin", () => { name: "Test Object", telemetry: { values: [{ + key: "utc", + format: "utc", + name: "Time", + hints: { + domain: 1 + } + },{ key: "some-key", name: "Some attribute", hints: { - domain: 1 + range: 1 } }, { key: "some-other-key", name: "Another attribute", hints: { - range: 1 + range: 2 } }] } }; + const testTelemetry = [ + { + 'utc': 1, + 'some-key': 'some-value 1', + 'some-other-key' : 'some-other-value 1' + }, + { + 'utc': 2, + 'some-key': 'some-value 2', + 'some-other-key' : 'some-other-value 2' + }, + { + 'utc': 3, + 'some-key': 'some-value 3', + 'some-other-key' : 'some-other-value 3' + } + ]; + let telemetryPromiseResolve; + let telemetryPromise = new Promise((resolve) => { + telemetryPromiseResolve = resolve; + }); + openmct.telemetry.request.and.callFake(() => { + telemetryPromiseResolve(testTelemetry); + return telemetryPromise; + }); + applicableViews = openmct.objectViews.get(testTelemetryObject); tableViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'table'); - tableView = tableViewProvider.view(testTelemetryObject, true, [testTelemetryObject]); + tableView = tableViewProvider.view(testTelemetryObject, [testTelemetryObject]); tableView.show(child, true); - return Vue.nextTick(); + + return telemetryPromise.then(() => Vue.nextTick()); }); + it("Renders a row for every telemetry datum returned",() => { + let rows = element.querySelectorAll('table.c-telemetry-table__body tr'); + expect(rows.length).toBe(3); + }); + + it("Renders a column for every item in telemetry metadata",() => { let headers = element.querySelectorAll('span.c-telemetry-table__headers__label'); - expect(headers.length).toBe(2); - expect(headers[0].innerText).toBe('Some attribute'); - expect(headers[1].innerText).toBe('Another attribute'); + expect(headers.length).toBe(3); + expect(headers[0].innerText).toBe('Time'); + expect(headers[1].innerText).toBe('Some attribute'); + expect(headers[2].innerText).toBe('Another attribute'); }); it("Supports column reordering via drag and drop",() => { diff --git a/src/testUtils.js b/src/testUtils.js deleted file mode 100644 index 03b2f192dd..0000000000 --- a/src/testUtils.js +++ /dev/null @@ -1,18 +0,0 @@ -import MCT from 'MCT'; - -export function createOpenMct() { - const openmct = new MCT(); - openmct.install(openmct.plugins.LocalStorage()); - openmct.install(openmct.plugins.UTCTimeSystem()); - openmct.time.timeSystem('utc', {start: 0, end: 1}); - - return openmct; -} - -export function createMouseEvent(eventName) { - return new MouseEvent(eventName, { - bubbles: true, - cancelable: true, - view: window - }); -} diff --git a/src/utils/openmctLocation.js b/src/utils/openmctLocation.js new file mode 100644 index 0000000000..9d94c96e2f --- /dev/null +++ b/src/utils/openmctLocation.js @@ -0,0 +1,106 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2020, 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. + *****************************************************************************/ + +import objectUtils from '../api/objects/object-utils.js'; + +/** + * Utility functions for getting and setting Open MCT search parameters and navigated object path. + * Open MCT encodes application state into the "hash" of the url, making it awkward to use standard browser API such + * as URL for modifying state in the URL. This wraps native API with some utility functions that operate only on the + * hash section of the URL. + */ + +export function setSearchParam(paramName, paramValue) { + let url = getHashRelativeURL(); + + url.searchParams.set(paramName, paramValue); + setLocationFromUrl(url); +} + +export function deleteSearchParam(paramName) { + let url = getHashRelativeURL(); + + url.searchParams.delete(paramName); + setLocationFromUrl(url); +} + +/** + * Will replace all current search parameters with the ones defined in urlSearchParams + * @param {URLSearchParams} paramMap + */ +export function setAllSearchParams(newSearchParams) { + let url = getHashRelativeURL(); + + Array.from(url.searchParams.keys()).forEach((key) => url.searchParams.delete(key)); + + Array.from(newSearchParams.keys()).forEach(key => { + url.searchParams.set(key, newSearchParams.get(key)); + }); + + setLocationFromUrl(url); +} + +export function getSearchParam(paramName) { + return getAllSearchParams().get(paramName); +} + +/** + * @returns {URLSearchParams} A {@link https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/entries|URLSearchParams} + * object for accessing all current search parameters + */ +export function getAllSearchParams() { + return getHashRelativeURL().searchParams; +} + +export function getObjectPath() { + return getHashRelativeURL().pathname; +} + +export function setObjectPath(objectPath) { + let objectPathString; + let url = getHashRelativeURL(); + + if (objectPath instanceof Array) { + if (objectPath.length > 0 && isDomainObject(objectPath[0])) { + throw 'setObjectPath must be called with either a string, or an array of Domain Objects'; + } + objectPathString = objectPath.reduce((pathString, object) => { + return `${pathString}/${objectUtils.makeKeyString(object.identifier)}`; + }, ''); + } else { + objectPathString = objectPath + } + url.pathname = objectPathString; + setLocationFromUrl(url); +} + +function isDomainObject(potentialObject) { + return potentialObject.identifier === undefined; +} + +function setLocationFromUrl(url) { + window.location.hash = `${url.pathname}${url.search}`; +} + +function getHashRelativeURL() { + return new URL(window.location.hash.substring(1), window.location.origin); +} diff --git a/src/utils/openmctLocationSpec.js b/src/utils/openmctLocationSpec.js new file mode 100644 index 0000000000..e999faef97 --- /dev/null +++ b/src/utils/openmctLocationSpec.js @@ -0,0 +1,114 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2020, 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. + *****************************************************************************/ +import { + setSearchParam, + deleteSearchParam, + getAllSearchParams, + getSearchParam, + setAllSearchParams, + getObjectPath, + setObjectPath +} from './openmctLocation'; + +import {resetApplicationState} from 'utils/testing'; + +describe('the openmct location utility functions', () => { + beforeAll(() => resetApplicationState()); + afterEach(() => resetApplicationState()); + + it('The setSearchParam function sets an individual search parameters in the window location hash', () => { + setSearchParam('testParam', 'testValue'); + expect(window.location.hash.includes('testParam=testValue')).toBe(true); + }); + + it('The deleteSearchParam function deletes an individual search paramater in the window location hash', () => { + window.location.hash = '#/?testParam=testValue'; + deleteSearchParam('testParam'); + expect(window.location.hash.includes('testParam=testValue')).toBe(false); + }); + + it('The getSearchParam function returns the value of an individual search paramater in the window location hash', () => { + window.location.hash = '#/?testParam=testValue'; + expect(getSearchParam('testParam')).toBe('testValue'); + }); + + it('The getAllSearchParams function returns the values of all search paramaters in the window location hash', () => { + window.location.hash = '#/?testParam1=testValue1&testParam2=testValue2&testParam3=testValue3'; + let searchParams = getAllSearchParams(); + expect(searchParams.get('testParam1')).toBe('testValue1'); + expect(searchParams.get('testParam2')).toBe('testValue2'); + expect(searchParams.get('testParam3')).toBe('testValue3'); + }); + + it('The setAllSearchParams function replaces all search paramaters in the window location hash', () => { + window.location.hash = '#/?testParam1=testValue1&testParam2=testValue2&testParam3=testValue3'; + let searchParams = getAllSearchParams(); + searchParams.delete('testParam3'); + searchParams.set('testParam1', 'updatedTestValue1'); + searchParams.set('newTestParam4', 'newTestValue4'); + setAllSearchParams(searchParams); + expect(window.location.hash).toBe('#/?testParam1=updatedTestValue1&testParam2=testValue2&newTestParam4=newTestValue4'); + }); + + it('The getObjectPath function returns the current object path', () => { + window.location.hash = '#/some/object/path?someParameter=someValue'; + expect(getObjectPath()).toBe('/some/object/path'); + }); + + it('The setObjectPath function allows the object path to be set to a given string', () => { + window.location.hash = '#/some/object/path?someParameter=someValue'; + setObjectPath('/some/other/object/path'); + expect(window.location.hash).toBe('#/some/other/object/path?someParameter=someValue'); + }); + + it('The setObjectPath function allows the object path to be set from an array of domain objects', () => { + const OBJECT_PATH = [ + { + identifier: { + namespace: 'namespace', + key: 'objectKey1' + } + }, + { + identifier: { + namespace: 'namespace', + key: 'objectKey2' + } + }, + { + identifier: { + namespace: 'namespace', + key: 'objectKey3' + } + } + ] + window.location.hash = '#/some/object/path?someParameter=someValue'; + setObjectPath(OBJECT_PATH); + expect(window.location.hash).toBe('#/namespace:objectKey1/namespace:objectKey2/namespace:objectKey3?someParameter=someValue'); + }); + + it('The setObjectPath function throws an error if called with anything other than a string or an array of domain objects', () => { + expect(() => setObjectPath(["array", "of", "strings"])).toThrow(); + expect(() => setObjectPath([{}, {someKey: 'someValue'}])).toThrow(); + }); +}); + diff --git a/src/utils/testing.js b/src/utils/testing.js new file mode 100644 index 0000000000..358e1c1ea3 --- /dev/null +++ b/src/utils/testing.js @@ -0,0 +1,70 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2020, 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. + *****************************************************************************/ + +import MCT from 'MCT'; +let nativeFunctions = []; + +export function createOpenMct() { + const openmct = new MCT(); + openmct.install(openmct.plugins.LocalStorage()); + openmct.install(openmct.plugins.UTCTimeSystem()); + openmct.time.timeSystem('utc', {start: 0, end: 1}); + + return openmct; +} + +export function createMouseEvent(eventName) { + return new MouseEvent(eventName, { + bubbles: true, + cancelable: true, + view: window + }); +} + +export function spyOnBuiltins(functionNames, object = window) { + functionNames.forEach(functionName => { + if (nativeFunctions[functionName]) { + throw `Builtin spy function already defined for ${functionName}`; + } + + nativeFunctions.push({functionName, object, nativeFunction: object[functionName]}); + spyOn(object, functionName); + }); +} + +export function clearBuiltinSpies() { + nativeFunctions.forEach(clearBuiltinSpy); + nativeFunctions = []; +} + +export function resetApplicationState(openmct) { + clearBuiltinSpies(); + window.location.hash = '#'; + + if (openmct !== undefined) { + openmct.destroy(); + } +} + +function clearBuiltinSpy(funcDefinition) { + funcDefinition.object[funcDefinition.functionName] = funcDefinition.nativeFunction; +} diff --git a/webpack.config.js b/webpack.config.js index f8862b2a63..3e435cc506 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -42,8 +42,9 @@ const webpackConfig = { "printj": path.join(__dirname, "node_modules/printj/dist/printj.min.js"), "styles": path.join(__dirname, "src/styles"), "MCT": path.join(__dirname, "src/MCT"), - "testUtils": path.join(__dirname, "src/testUtils.js"), - "objectUtils": path.join(__dirname, "src/api/objects/object-utils.js") + "testUtils": path.join(__dirname, "src/utils/testUtils.js"), + "objectUtils": path.join(__dirname, "src/api/objects/object-utils.js"), + "utils": path.join(__dirname, "src/utils") } }, devtool: devMode ? 'eval-source-map' : 'source-map',