Skip to content
Permalink

Comparing changes

This is a direct comparison between two commits made in this repository or its related repositories. View the default comparison for this range or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: nasa/openmct
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 0946220f31f7c05eaf1c4f62ce698fb16398de29
Choose a base ref
..
head repository: nasa/openmct
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: e193ab84e9557287b8249890335ee9430402d08c
Choose a head ref
3 changes: 2 additions & 1 deletion .cspell.json
Original file line number Diff line number Diff line change
@@ -483,7 +483,8 @@
"countup",
"darkmatter",
"Undeletes",
"SSSZ"
"SSSZ",
"pageerror"
],
"dictionaries": ["npm", "softwareTerms", "node", "html", "css", "bash", "en_US", "en-gb", "misc"],
"ignorePaths": [
16 changes: 16 additions & 0 deletions e2e/appActions.js
Original file line number Diff line number Diff line change
@@ -682,6 +682,21 @@ async function linkParameterToObject(page, parameterName, objectName) {
await page.getByLabel('Save').click();
}

/**
* Rename the currently viewed `domainObject` from the browse bar.
*
* @param {import('@playwright/test').Page} page
* @param {string} newName
*/
async function renameCurrentObjectFromBrowseBar(page, newName) {
const nameInput = page.getByLabel('Browse bar object name');
await nameInput.click();
await nameInput.fill('');
await nameInput.fill(newName);
// Click the browse bar container to save changes
await page.getByLabel('Browse bar', { exact: true }).click();
}

export {
createDomainObjectWithDefaults,
createExampleTelemetryObject,
@@ -693,6 +708,7 @@ export {
linkParameterToObject,
navigateToObjectWithFixedTimeBounds,
navigateToObjectWithRealTime,
renameCurrentObjectFromBrowseBar,
setEndOffset,
setFixedIndependentTimeConductorBounds,
setFixedTimeMode,
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, 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 { createDomainObjectWithDefaults, setRealTimeMode } from '../../../appActions.js';
import { MISSION_TIME } from '../../../constants.js';
import { expect, test } from '../../../pluginFixtures.js';

const TELEMETRY_RATE = 2500;

test.describe('Example Event Generator Acknowledge with Controlled Clock @clock', () => {
test.beforeEach(async ({ page }) => {
await page.clock.install({ time: MISSION_TIME });
await page.clock.resume();

await page.goto('./', { waitUntil: 'domcontentloaded' });

await setRealTimeMode(page);

await createDomainObjectWithDefaults(page, {
type: 'Event Message Generator with Acknowledge'
});
});

test('Rows are updatable in place', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7938'
});

await test.step('First telemetry datum gets added as new row', async () => {
await page.clock.fastForward(TELEMETRY_RATE);
const rows = page.getByLabel('table content').getByLabel('Table Row');
const acknowledgeCell = rows.first().getByLabel('acknowledge table cell');

await expect(rows).toHaveCount(1);
await expect(acknowledgeCell).not.toHaveAttribute('title', 'OK');
});

await test.step('Incoming Telemetry datum matching an existing rows in place update key has data merged to existing row', async () => {
await page.clock.fastForward(TELEMETRY_RATE * 2);
const rows = page.getByLabel('table content').getByLabel('Table Row');
const acknowledgeCell = rows.first().getByLabel('acknowledge table cell');

await expect(rows).toHaveCount(1);
await expect(acknowledgeCell).toHaveAttribute('title', 'OK');
});
});
});
145 changes: 145 additions & 0 deletions e2e/tests/functional/plugins/displayLayout/displayLayout.e2e.spec.js
Original file line number Diff line number Diff line change
@@ -507,8 +507,153 @@ test.describe('Display Layout', () => {
// In real time mode, we don't fetch annotations at all
await expect.poll(() => networkRequests, { timeout: 10000 }).toHaveLength(0);
});

test('Same objects with different request options have unique subscriptions', async ({
page
}) => {
// Expand My Items
await page.getByLabel('Expand My Items folder').click();

// Create a Display Layout
const displayLayout = await createDomainObjectWithDefaults(page, {
type: 'Display Layout',
name: 'Test Display'
});

// Create a State Generator, set to higher frequency updates
const stateGenerator = await createDomainObjectWithDefaults(page, {
type: 'State Generator',
name: 'State Generator'
});
const stateGeneratorTreeItem = page.getByRole('treeitem', {
name: stateGenerator.name
});
await stateGeneratorTreeItem.click({ button: 'right' });
await page.getByLabel('Edit Properties...').click();
await page.getByLabel('State Duration (seconds)', { exact: true }).fill('0.1');
await page.getByLabel('Save').click();

// Create a Table for filtering ON values
const tableFilterOnValue = await createDomainObjectWithDefaults(page, {
type: 'Telemetry Table',
name: 'Table Filter On Value'
});
const tableFilterOnTreeItem = page.getByRole('treeitem', {
name: tableFilterOnValue.name
});

// Create a Table for filtering OFF values
const tableFilterOffValue = await createDomainObjectWithDefaults(page, {
type: 'Telemetry Table',
name: 'Table Filter Off Value'
});
const tableFilterOffTreeItem = page.getByRole('treeitem', {
name: tableFilterOffValue.name
});

// Navigate to ON filtering table and add state generator and setup filters
await page.goto(tableFilterOnValue.url);
await stateGeneratorTreeItem.dragTo(page.getByLabel('Object View'));
await selectFilterOption(page, '1');
await page.getByLabel('Save').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();

// Navigate to OFF filtering table and add state generator and setup filters
await page.goto(tableFilterOffValue.url);
await stateGeneratorTreeItem.dragTo(page.getByLabel('Object View'));
await selectFilterOption(page, '0');
await page.getByLabel('Save').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();

// Navigate to the display layout and edit it
await page.goto(displayLayout.url);

// Add the tables to the display layout
await page.getByLabel('Edit Object').click();
await tableFilterOffTreeItem.dragTo(page.getByLabel('Layout Grid'), {
targetPosition: { x: 10, y: 300 }
});
await page.locator('.c-frame-edit > div:nth-child(4)').dragTo(page.getByLabel('Layout Grid'), {
targetPosition: { x: 400, y: 500 },
// eslint-disable-next-line playwright/no-force-option
force: true
});
await tableFilterOnTreeItem.dragTo(page.getByLabel('Layout Grid'), {
targetPosition: { x: 10, y: 100 }
});
await page.locator('.c-frame-edit > div:nth-child(4)').dragTo(page.getByLabel('Layout Grid'), {
targetPosition: { x: 400, y: 300 },
// eslint-disable-next-line playwright/no-force-option
force: true
});
await page.getByLabel('Save').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();

// Get the tables so we can verify filtering is working as expected
const tableFilterOn = page.getByLabel(`${tableFilterOnValue.name} Frame`, {
exact: true
});
const tableFilterOff = page.getByLabel(`${tableFilterOffValue.name} Frame`, {
exact: true
});

// Verify filtering is working correctly

// Create a promise that resolves when we've seen enough new rows added
const rowsMutationPromise = page.evaluate(() => {
return new Promise((resolve) => {
const targetTable = document.querySelector(
'table[aria-label="Table Filter Off Value table content"]'
);
const config = { childList: true, subtree: true };
let changeCount = 0;
const requiredChanges = 20; // Number of changes to wait for

const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
// Count added nodes
changeCount += mutation.addedNodes.length;
}
});

// Check if the required number of changes has been met
if (changeCount >= requiredChanges) {
observer.disconnect(); // Disconnect observer after the required changes
resolve();
}
});

observer.observe(targetTable, config);
});
});

await rowsMutationPromise;

// Check ON table doesn't have any OFF values
await expect(tableFilterOn.locator('td[title="OFF"]')).toHaveCount(0);
const onCount = await tableFilterOn.locator('td[title="ON"]').count();
await expect(onCount).toBeGreaterThan(0);

// Check OFF table doesn't have any ON values
await expect(tableFilterOff.locator('td[title="ON"]')).toHaveCount(0);
const offCount = await tableFilterOff.locator('td[title="OFF"]').count();
await expect(offCount).toBeGreaterThan(0);
});
});

async function selectFilterOption(page, filterOption) {
await page.getByRole('tab', { name: 'Filters' }).click();
await page
.getByLabel('Inspector Views')
.locator('li')
.filter({ hasText: 'State Generator' })
.locator('span')
.click();
await page.getByRole('switch').click();
await page.selectOption('select[name="setSelectionThreshold"]', filterOption);
}

async function addAndRemoveDrawingObjectAndAssert(page, layoutObject, DISPLAY_LAYOUT_NAME) {
await expect(page.getByLabel(layoutObject, { exact: true })).toHaveCount(0);
await addLayoutObject(page, DISPLAY_LAYOUT_NAME, layoutObject);
55 changes: 54 additions & 1 deletion e2e/tests/functional/plugins/gauge/gauge.e2e.spec.js
Original file line number Diff line number Diff line change
@@ -28,7 +28,9 @@ import { v4 as uuid } from 'uuid';

import {
createDomainObjectWithDefaults,
createExampleTelemetryObject
createExampleTelemetryObject,
setRealTimeMode,
setStartOffset
} from '../../../../appActions.js';
import { expect, test } from '../../../../pluginFixtures.js';

@@ -166,6 +168,57 @@ test.describe('Gauge', () => {
);
});

test('Gauge does not break when an object is missing', async ({ page }) => {
// Set up error listeners
const pageErrors = [];

// Listen for uncaught exceptions
page.on('pageerror', (err) => {
pageErrors.push(err.message);
});

await setRealTimeMode(page);

// Create a Gauge
const gauge = await createDomainObjectWithDefaults(page, {
type: 'Gauge',
name: 'Gauge with missing object'
});

// Create a Sine Wave Generator in the Gauge with a loading delay
const missingSWG = await createExampleTelemetryObject(page, gauge.uuid);

// Remove the object from local storage
await page.evaluate(
([missingObject]) => {
const mct = localStorage.getItem('mct');
const mctObjects = JSON.parse(mct);
delete mctObjects[missingObject.uuid];
localStorage.setItem('mct', JSON.stringify(mctObjects));
},
[missingSWG]
);

// Verify start bounds
await expect(page.getByLabel('Start offset: 00:30:00')).toBeVisible();

// Nav to the Gauge
await page.goto(gauge.url, { waitUntil: 'domcontentloaded' });

// adjust time bounds and ensure they are updated
await setStartOffset(page, {
startHours: '00',
startMins: '45',
startSecs: '00'
});

// Verify start bounds changed
await expect(page.getByLabel('Start offset: 00:45:00')).toBeVisible();

// // Verify no errors were thrown
expect(pageErrors).toHaveLength(0);
});

test('Gauge enforces composition policy', async ({ page }) => {
// Create a Gauge
await createDomainObjectWithDefaults(page, {
Loading