From 1fde0d9e38ba039ab2e531c5f508d5fe6b6dd169 Mon Sep 17 00:00:00 2001 From: Shefali Joshi Date: Sat, 18 Jan 2025 07:50:24 -0800 Subject: [PATCH 1/9] Don't disallow mouse events when in compact mode for plots (#7975) * Allow highlights and locking highlight points for plots in compact mode, but still disallow pan and zoom. * Remove unnecessary watch on cursor guides and grid lines * Test for cursor guides in compact mode --- .../plot/plotControlsCompactMode.e2e.spec.js | 58 +++++++++++++++++++ src/plugins/plot/MctPlot.vue | 46 ++++++++------- src/plugins/plot/PlotView.vue | 8 --- 3 files changed, 84 insertions(+), 28 deletions(-) create mode 100644 e2e/tests/functional/plugins/plot/plotControlsCompactMode.e2e.spec.js diff --git a/e2e/tests/functional/plugins/plot/plotControlsCompactMode.e2e.spec.js b/e2e/tests/functional/plugins/plot/plotControlsCompactMode.e2e.spec.js new file mode 100644 index 0000000000..d9700c60cb --- /dev/null +++ b/e2e/tests/functional/plugins/plot/plotControlsCompactMode.e2e.spec.js @@ -0,0 +1,58 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2025, 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. + *****************************************************************************/ + +/* + * This test suite is dedicated to testing the rendering and interaction of plots. + * + */ + +import { createDomainObjectWithDefaults } from '../../../../appActions.js'; +import { expect, test } from '../../../../pluginFixtures.js'; + +test.describe('Plot Controls in compact mode', () => { + let timeStrip; + + test.beforeEach(async ({ page }) => { + // Open a browser, navigate to the main page, and wait until all networkevents to resolve + await page.goto('./', { waitUntil: 'domcontentloaded' }); + timeStrip = await createDomainObjectWithDefaults(page, { + type: 'Time Strip' + }); + + // Create an overlay plot with a sine wave generator + await createDomainObjectWithDefaults(page, { + type: 'Sine Wave Generator', + parent: timeStrip.uuid + }); + await page.goto(`${timeStrip.url}`); + }); + + test('Plots show cursor guides', async ({ page }) => { + // hover over plot for plot controls + await page.getByLabel('Plot Canvas').hover(); + // click on cursor guides control + await page.getByTitle('Toggle cursor guides').click(); + await page.getByLabel('Plot Canvas').hover(); + await expect(page.getByLabel('Vertical cursor guide')).toBeVisible(); + await expect(page.getByLabel('Horizontal cursor guide')).toBeVisible(); + }); +}); diff --git a/src/plugins/plot/MctPlot.vue b/src/plugins/plot/MctPlot.vue index 76815cc53d..08e2a6f3d0 100644 --- a/src/plugins/plot/MctPlot.vue +++ b/src/plugins/plot/MctPlot.vue @@ -164,11 +164,13 @@
@@ -854,13 +856,11 @@ export default { this.canvas = this.$refs.chartContainer.querySelectorAll('canvas')[1]; - if (!this.options.compact) { - this.listenTo(this.canvas, 'mousemove', this.trackMousePosition, this); - this.listenTo(this.canvas, 'mouseleave', this.untrackMousePosition, this); - this.listenTo(this.canvas, 'mousedown', this.onMouseDown, this); - this.listenTo(this.canvas, 'click', this.selectNearbyAnnotations, this); - this.listenTo(this.canvas, 'wheel', this.wheelZoom, this); - } + this.listenTo(this.canvas, 'mousemove', this.trackMousePosition, this); + this.listenTo(this.canvas, 'mouseleave', this.untrackMousePosition, this); + this.listenTo(this.canvas, 'mousedown', this.onMouseDown, this); + this.listenTo(this.canvas, 'click', this.selectNearbyAnnotations, this); + this.listenTo(this.canvas, 'wheel', this.wheelZoom, this); }, marqueeAnnotations(annotationsToSelect) { @@ -1115,19 +1115,21 @@ export default { this.listenTo(window, 'mouseup', this.onMouseUp, this); this.listenTo(window, 'mousemove', this.trackMousePosition, this); - // track frozen state on mouseDown to be read on mouseUp - const isFrozen = - this.config.xAxis.get('frozen') === true && this.config.yAxis.get('frozen') === true; - this.isFrozenOnMouseDown = isFrozen; + if (!this.options.compact) { + // track frozen state on mouseDown to be read on mouseUp + const isFrozen = + this.config.xAxis.get('frozen') === true && this.config.yAxis.get('frozen') === true; + this.isFrozenOnMouseDown = isFrozen; - if (event.altKey && !event.shiftKey) { - return this.startPan(event); - } else if (event.altKey && event.shiftKey) { - this.freeze(); + if (event.altKey && !event.shiftKey) { + return this.startPan(event); + } else if (event.altKey && event.shiftKey) { + this.freeze(); - return this.startMarquee(event, true); - } else { - return this.startMarquee(event, false); + return this.startMarquee(event, true); + } else { + return this.startMarquee(event, false); + } } }, @@ -1158,11 +1160,15 @@ export default { }, isMouseClick() { - if (!this.marquee) { + // We may not have a marquee if we've disabled pan/zoom, but we still need to know if it's a mouse click for highlights and lock points. + if (!this.marquee && !this.positionOverPlot) { return false; } - const { start, end } = this.marquee; + const { start, end } = this.marquee ?? { + start: this.positionOverPlot, + end: this.positionOverPlot + }; const someYPositionOverPlot = start.y.some((y) => y); return start.x === end.x && someYPositionOverPlot; diff --git a/src/plugins/plot/PlotView.vue b/src/plugins/plot/PlotView.vue index 34eac98331..48395b4dbf 100644 --- a/src/plugins/plot/PlotView.vue +++ b/src/plugins/plot/PlotView.vue @@ -162,14 +162,6 @@ export default { } } }, - watch: { - gridLines(newGridLines) { - this.gridLines = newGridLines; - }, - cursorGuide(newCursorGuide) { - this.cursorGuide = newCursorGuide; - } - }, created() { eventHelpers.extend(this); this.imageExporter = new ImageExporter(this.openmct); From a6517bb33e94360ff35df6af917a2802a6d21acd Mon Sep 17 00:00:00 2001 From: Shefali Joshi Date: Fri, 7 Feb 2025 10:03:00 -0800 Subject: [PATCH 2/9] migrate from actions/upload-artifact: v3 to v4. (#8000) * migrate from actions/upload-artifact: v3 to v4. https://github.com/actions/upload-artifact/blob/main/docs/MIGRATION.md * Add names for artifacts and allow overwriting them --- .github/workflows/e2e-couchdb.yml | 10 +++++++--- .github/workflows/e2e-flakefinder.yml | 4 +++- .github/workflows/e2e-perf.yml | 4 +++- .github/workflows/e2e-pr.yml | 4 +++- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/.github/workflows/e2e-couchdb.yml b/.github/workflows/e2e-couchdb.yml index edab4e9fb4..dd4fce54cc 100644 --- a/.github/workflows/e2e-couchdb.yml +++ b/.github/workflows/e2e-couchdb.yml @@ -51,7 +51,7 @@ jobs: env: COMMIT_INFO_SHA: ${{github.event.pull_request.head.sha }} run: npm run test:e2e:couchdb - + - name: Generate Code Coverage Report run: npm run cov:e2e:report @@ -66,15 +66,19 @@ jobs: - name: Archive test results if: success() || failure() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: + name: e2e-couchdb-test-results path: test-results + overwrite: true - name: Archive html test results if: success() || failure() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: + name: e2e-couchdb-html-test-results path: html-test-results + overwrite: true - name: Remove pr:e2e:couchdb label (if present) if: always() diff --git a/.github/workflows/e2e-flakefinder.yml b/.github/workflows/e2e-flakefinder.yml index 09c912f424..c6eaffe748 100644 --- a/.github/workflows/e2e-flakefinder.yml +++ b/.github/workflows/e2e-flakefinder.yml @@ -38,9 +38,11 @@ jobs: - name: Archive test results if: success() || failure() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: + name: e2e-flakefinder-test-results path: test-results + overwrite: true - name: Remove pr:e2e:flakefinder label (if present) if: always() diff --git a/.github/workflows/e2e-perf.yml b/.github/workflows/e2e-perf.yml index 36285589b1..afa8147409 100644 --- a/.github/workflows/e2e-perf.yml +++ b/.github/workflows/e2e-perf.yml @@ -35,9 +35,11 @@ jobs: - run: npm run test:perf:memory - name: Archive test results if: success() || failure() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: + name: e2e-perf-test-results path: test-results + overwrite: true - name: Remove pr:e2e:perf label (if present) if: always() diff --git a/.github/workflows/e2e-pr.yml b/.github/workflows/e2e-pr.yml index 869fd2dfbf..897434480e 100644 --- a/.github/workflows/e2e-pr.yml +++ b/.github/workflows/e2e-pr.yml @@ -45,9 +45,11 @@ jobs: npm run cov:e2e:full:publish - name: Archive test results if: success() || failure() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: + name: e2e-pr-test-results path: test-results + overwrite: true - name: Remove pr:e2e label (if present) if: always() From ecd120387cb43bec08b542732f6873fb658e211c Mon Sep 17 00:00:00 2001 From: Shefali Joshi Date: Mon, 10 Feb 2025 13:46:00 -0800 Subject: [PATCH 3/9] Independent time conductor related handling for plot synchronization. (#7956) * Ensure that the mode set when independent time conductor is enabled/disabled is propagated correctly. Also ensure that global time conductor changes are not picked up by the independent time conductor when the user has enabled it at least once before * Use structuredClone instead of deep copy * Add e2e test * Assert that you're in fixed mode after sync time conductor * Comment explaining new time context test * Change test to be a little less complicated * Fix linting errors --- .../plugins/plot/plotControls.e2e.spec.js | 38 +++++++++++++++++++ src/api/time/IndependentTimeContext.js | 22 ++++++++++- src/api/time/TimeAPI.js | 6 ++- src/plugins/plot/MctPlot.vue | 1 + .../independent/IndependentTimeConductor.vue | 12 +++++- 5 files changed, 75 insertions(+), 4 deletions(-) diff --git a/e2e/tests/functional/plugins/plot/plotControls.e2e.spec.js b/e2e/tests/functional/plugins/plot/plotControls.e2e.spec.js index 87f608f528..e654ff213d 100644 --- a/e2e/tests/functional/plugins/plot/plotControls.e2e.spec.js +++ b/e2e/tests/functional/plugins/plot/plotControls.e2e.spec.js @@ -108,4 +108,42 @@ test.describe('Plot Controls', () => { // Expect before and after plot points to match await expect(plotPixelSizeAtPause).toEqual(plotPixelSizeAfterWait); }); + + /* + Test to verify that switching a plot's time context from global to + its own independent time context and then back to global context works correctly. + + After switching from fixed time mode (ITC) to real time mode (global context), + the pause control for the plot should be available, indicating that it is following the right context. + */ + test('Plots follow the right time context', async ({ page }) => { + // Set global time conductor to real-time mode + await setRealTimeMode(page); + + // hover over plot for plot controls + await page.getByLabel('Plot Canvas').hover(); + // Ensure pause control is visible since global time conductor is in Real time mode. + await expect(page.getByTitle('Pause incoming real-time data')).toBeVisible(); + + // Toggle independent time conductor ON + await page.getByLabel('Enable Independent Time Conductor').click(); + + // Bring up the independent time conductor popup and switch to fixed time mode + await page.getByLabel('Independent Time Conductor Settings').click(); + await page.getByLabel('Independent Time Conductor Mode Menu').click(); + await page.getByRole('menuitem', { name: /Fixed Timespan/ }).click(); + + // hover over plot for plot controls + await page.getByLabel('Plot Canvas').hover(); + // Ensure pause control is no longer visible since the plot is following the independent time context + await expect(page.getByTitle('Pause incoming real-time data')).toBeHidden(); + + // Toggle independent time conductor OFF - Note that the global time conductor is still in Real time mode + await page.getByLabel('Disable Independent Time Conductor').click(); + + // hover over plot for plot controls + await page.getByLabel('Plot Canvas').hover(); + // Ensure pause control is visible since the global time conductor is in real time mode + await expect(page.getByTitle('Pause incoming real-time data')).toBeVisible(); + }); }); diff --git a/src/api/time/IndependentTimeContext.js b/src/api/time/IndependentTimeContext.js index 9f9edbcc4d..148bf52adf 100644 --- a/src/api/time/IndependentTimeContext.js +++ b/src/api/time/IndependentTimeContext.js @@ -359,6 +359,18 @@ class IndependentTimeContext extends TimeContext { } } + /** + * @returns {boolean} + * @override + */ + isFixed() { + if (this.upstreamTimeContext) { + return this.upstreamTimeContext.isFixed(...arguments); + } else { + return super.isFixed(...arguments); + } + } + /** * @returns {number} * @override @@ -400,7 +412,7 @@ class IndependentTimeContext extends TimeContext { } /** - * Reset the time context to the global time context + * Reset the time context from the global time context */ resetContext() { if (this.upstreamTimeContext) { @@ -428,6 +440,10 @@ class IndependentTimeContext extends TimeContext { // Emit bounds so that views that are changing context get the upstream bounds this.emit('bounds', this.getBounds()); this.emit(TIME_CONTEXT_EVENTS.boundsChanged, this.getBounds()); + // Also emit the mode in case it's different from previous time context + if (this.getMode()) { + this.emit(TIME_CONTEXT_EVENTS.modeChanged, this.#copy(this.getMode())); + } } /** @@ -502,6 +518,10 @@ class IndependentTimeContext extends TimeContext { // Emit bounds so that views that are changing context get the upstream bounds this.emit('bounds', this.getBounds()); this.emit(TIME_CONTEXT_EVENTS.boundsChanged, this.getBounds()); + // Also emit the mode in case it's different from the global time context + if (this.getMode()) { + this.emit(TIME_CONTEXT_EVENTS.modeChanged, this.#copy(this.getMode())); + } // now that the view's context is set, tell others to check theirs in case they were following this view's context. this.globalTimeContext.emit('refreshContext', viewKey); } diff --git a/src/api/time/TimeAPI.js b/src/api/time/TimeAPI.js index 3cc2d8b6e1..8b29a7c5ff 100644 --- a/src/api/time/TimeAPI.js +++ b/src/api/time/TimeAPI.js @@ -23,6 +23,7 @@ import { FIXED_MODE_KEY, REALTIME_MODE_KEY } from '@/api/time/constants'; import IndependentTimeContext from '@/api/time/IndependentTimeContext'; +import { TIME_CONTEXT_EVENTS } from './constants'; import GlobalTimeContext from './GlobalTimeContext.js'; /** @@ -142,7 +143,7 @@ class TimeAPI extends GlobalTimeContext { addIndependentContext(key, value, clockKey) { let timeContext = this.getIndependentContext(key); - //stop following upstream time context since the view has it's own + //stop following upstream time context since the view has its own timeContext.resetContext(); if (clockKey) { @@ -152,6 +153,9 @@ class TimeAPI extends GlobalTimeContext { timeContext.setMode(FIXED_MODE_KEY, value); } + // Also emit the mode in case it's different from the previous time context + timeContext.emit(TIME_CONTEXT_EVENTS.modeChanged, structuredClone(timeContext.getMode())); + // Notify any nested views to update, pass in the viewKey so that particular view can skip getting an upstream context this.emit('refreshContext', key); diff --git a/src/plugins/plot/MctPlot.vue b/src/plugins/plot/MctPlot.vue index 08e2a6f3d0..cd395974b8 100644 --- a/src/plugins/plot/MctPlot.vue +++ b/src/plugins/plot/MctPlot.vue @@ -539,6 +539,7 @@ export default { this.followTimeContext(); }, followTimeContext() { + this.updateMode(); this.updateDisplayBounds(this.timeContext.getBounds()); this.timeContext.on('modeChanged', this.updateMode); this.timeContext.on('boundsChanged', this.updateDisplayBounds); diff --git a/src/plugins/timeConductor/independent/IndependentTimeConductor.vue b/src/plugins/timeConductor/independent/IndependentTimeConductor.vue index 5ca91492a4..2ff26ee74e 100644 --- a/src/plugins/timeConductor/independent/IndependentTimeConductor.vue +++ b/src/plugins/timeConductor/independent/IndependentTimeConductor.vue @@ -243,12 +243,20 @@ export default { this.timeContext.off(TIME_CONTEXT_EVENTS.modeChanged, this.setTimeOptionsMode); }, setTimeOptionsClock(clock) { + // If the user has persisted any time options, then don't override them with global settings. + if (this.independentTCEnabled) { + return; + } this.setTimeOptionsOffsets(); this.timeOptions.clock = clock.key; }, setTimeOptionsMode(mode) { - this.setTimeOptionsOffsets(); - this.timeOptions.mode = mode; + // If the user has persisted any time options, then don't override them with global settings. + if (this.independentTCEnabled) { + this.setTimeOptionsOffsets(); + this.timeOptions.mode = mode; + this.isFixed = this.timeOptions.mode === FIXED_MODE_KEY; + } }, setTimeOptionsOffsets() { this.timeOptions.clockOffsets = From 28b5d7c41c438bb393e93ef0f60d47755919ede0 Mon Sep 17 00:00:00 2001 From: Shefali Joshi Date: Mon, 17 Feb 2025 10:23:48 -0800 Subject: [PATCH 4/9] Time strip marcus banes line "now line" fix for right y-axis and when now is out of bounds (#7993) * Account for right y-axes when calculating now line position. Don't show the now line if it's out of bounds of the time axis * Add test for now marker in realtime and out of bounds modes --- .../functional/planning/timestrip.e2e.spec.js | 83 +++++++++++++++---- src/ui/components/TimeSystemAxis.vue | 12 ++- 2 files changed, 76 insertions(+), 19 deletions(-) diff --git a/e2e/tests/functional/planning/timestrip.e2e.spec.js b/e2e/tests/functional/planning/timestrip.e2e.spec.js index 1d82071cd1..4303f308c2 100644 --- a/e2e/tests/functional/planning/timestrip.e2e.spec.js +++ b/e2e/tests/functional/planning/timestrip.e2e.spec.js @@ -24,7 +24,9 @@ import { createDomainObjectWithDefaults, createPlanFromJSON, navigateToObjectWithFixedTimeBounds, - setFixedIndependentTimeConductorBounds + setFixedIndependentTimeConductorBounds, + setFixedTimeMode, + setTimeConductorBounds } from '../../../appActions.js'; import { expect, test } from '../../../pluginFixtures.js'; @@ -74,21 +76,14 @@ const testPlan = { }; test.describe('Time Strip', () => { - test('Create two Time Strips, add a single Plan to both, and verify they can have separate Independent Time Contexts', async ({ - page - }) => { - test.info().annotations.push({ - type: 'issue', - description: 'https://github.com/nasa/openmct/issues/5627' - }); - - // Constant locators - const activityBounds = page.locator('.activity-bounds'); + let timestrip; + let plan; + test.beforeEach(async ({ page }) => { // Goto baseURL await page.goto('./', { waitUntil: 'domcontentloaded' }); - const timestrip = await test.step('Create a Time Strip', async () => { + timestrip = await test.step('Create a Time Strip', async () => { const createdTimeStrip = await createDomainObjectWithDefaults(page, { type: 'Time Strip' }); const objectName = await page.locator('.l-browse-bar__object-name').innerText(); expect(objectName).toBe(createdTimeStrip.name); @@ -96,7 +91,7 @@ test.describe('Time Strip', () => { return createdTimeStrip; }); - const plan = await test.step('Create a Plan and add it to the timestrip', async () => { + plan = await test.step('Create a Plan and add it to the timestrip', async () => { const createdPlan = await createPlanFromJSON(page, { name: 'Test Plan', json: testPlan @@ -110,6 +105,22 @@ test.describe('Time Strip', () => { .dragTo(page.getByLabel('Object View')); await page.getByLabel('Save').click(); await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); + + return createdPlan; + }); + }); + test('Create two Time Strips, add a single Plan to both, and verify they can have separate Independent Time Contexts', async ({ + page + }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/nasa/openmct/issues/5627' + }); + + // Constant locators + const activityBounds = page.locator('.activity-bounds'); + + await test.step('Set time strip to fixed timespan mode and verify activities', async () => { const startBound = testPlan.TEST_GROUP[0].start; const endBound = testPlan.TEST_GROUP[testPlan.TEST_GROUP.length - 1].end; @@ -119,8 +130,6 @@ test.describe('Time Strip', () => { // Verify all events are displayed const eventCount = await page.locator('.activity-bounds').count(); expect(eventCount).toEqual(testPlan.TEST_GROUP.length); - - return createdPlan; }); await test.step('TimeStrip can use the Independent Time Conductor', async () => { @@ -177,4 +186,48 @@ test.describe('Time Strip', () => { expect(await activityBounds.count()).toEqual(1); }); }); + + test('Time strip now line', async ({ page }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/nasa/openmct/issues/7817' + }); + + await test.step('Is displayed in realtime mode', async () => { + await expect(page.getByLabel('Now Marker')).toBeVisible(); + }); + + await test.step('Is hidden when out of bounds of the time axis', async () => { + // Switch to fixed timespan mode + await setFixedTimeMode(page); + // Get the end bounds + const endBounds = await page.getByLabel('End bounds').textContent(); + + // Add 2 minutes to end bound datetime and use it as the new end time + let endTimeStamp = new Date(endBounds); + endTimeStamp.setUTCMinutes(endTimeStamp.getUTCMinutes() + 2); + const endDate = endTimeStamp.toISOString().split('T')[0]; + const milliseconds = endTimeStamp.getMilliseconds(); + const endTime = endTimeStamp.toISOString().split('T')[1].replace(`.${milliseconds}Z`, ''); + + // Subtract 1 minute from the end bound and use it as the new start time + let startTimeStamp = new Date(endBounds); + startTimeStamp.setUTCMinutes(startTimeStamp.getUTCMinutes() + 1); + const startDate = startTimeStamp.toISOString().split('T')[0]; + const startMilliseconds = startTimeStamp.getMilliseconds(); + const startTime = startTimeStamp + .toISOString() + .split('T')[1] + .replace(`.${startMilliseconds}Z`, ''); + // Set fixed timespan mode to the future so that "now" is out of bounds. + await setTimeConductorBounds(page, { + startDate, + endDate, + startTime, + endTime + }); + + await expect(page.getByLabel('Now Marker')).toBeHidden(); + }); + }); }); diff --git a/src/ui/components/TimeSystemAxis.vue b/src/ui/components/TimeSystemAxis.vue index 92014e54df..a1dc86e3e9 100644 --- a/src/ui/components/TimeSystemAxis.vue +++ b/src/ui/components/TimeSystemAxis.vue @@ -21,7 +21,9 @@ -->