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()
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/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/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/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/api/tooltips/ToolTipAPI.js b/src/api/tooltips/ToolTipAPI.js
index 04177378fa..5168e4b396 100644
--- a/src/api/tooltips/ToolTipAPI.js
+++ b/src/api/tooltips/ToolTipAPI.js
@@ -80,6 +80,7 @@ class TooltipAPI {
* @property {string} tooltipText text to show in the tooltip
* @property {TOOLTIP_LOCATIONS} tooltipLocation location to show the tooltip relative to the parentElement
* @property {HTMLElement} parentElement reference to the DOM node we're adding the tooltip to
+ * @property {Array} cssClasses css classes to use with the tool tip element
*/
/**
diff --git a/src/api/tooltips/tooltipMixins.js b/src/api/tooltips/tooltipMixins.js
index 5833d4c6fa..cd5681903d 100644
--- a/src/api/tooltips/tooltipMixins.js
+++ b/src/api/tooltips/tooltipMixins.js
@@ -48,7 +48,7 @@ const tooltipHelpers = {
.reverse()
.join(' / ');
},
- buildToolTip(tooltipText, tooltipLocation, elementRef) {
+ buildToolTip(tooltipText, tooltipLocation, elementRef, cssClasses) {
if (!tooltipText || tooltipText.length < 1) {
return;
}
@@ -59,7 +59,8 @@ const tooltipHelpers = {
this.tooltip = this.openmct.tooltips.tooltip({
toolTipText: tooltipText,
toolTipLocation: tooltipLocation,
- parentElement: parentElement
+ parentElement: parentElement,
+ cssClasses
});
},
hideToolTip() {
diff --git a/src/plugins/events/EventTimelineViewProvider.js b/src/plugins/events/EventTimelineViewProvider.js
index 419488f7c8..f2fe2e29eb 100644
--- a/src/plugins/events/EventTimelineViewProvider.js
+++ b/src/plugins/events/EventTimelineViewProvider.js
@@ -34,7 +34,10 @@ export default function EventTimestripViewProvider(openmct, extendedLinesBus) {
const hasDomain = metadata.valuesForHints(['domain']).length > 0;
const hasNoRange = !metadata.valuesForHints(['range'])?.length;
- return hasDomain && hasNoRange;
+ // for the moment, let's also exclude telemetry with images
+ const hasNoImages = !metadata.valuesForHints(['image']).length;
+
+ return hasDomain && hasNoRange && hasNoImages;
}
return {
@@ -42,7 +45,8 @@ export default function EventTimestripViewProvider(openmct, extendedLinesBus) {
name: 'Event Timeline View',
cssClass: 'icon-event',
priority: function () {
- return 6000; // big number!
+ // We want this to be higher priority than the TelemetryTableView
+ return openmct.priority.HIGH;
},
canView: function (domainObject, objectPath) {
const isChildOfTimeStrip = objectPath.some((object) => object.type === 'time-strip');
diff --git a/src/plugins/events/components/EventTimelineView.vue b/src/plugins/events/components/EventTimelineView.vue
index ce330d85d7..17294df99b 100644
--- a/src/plugins/events/components/EventTimelineView.vue
+++ b/src/plugins/events/components/EventTimelineView.vue
@@ -21,29 +21,47 @@
-->
-
+