From 5d241ebe4c5f6c74e0b05cf43c3f004470946758 Mon Sep 17 00:00:00 2001 From: John Hill Date: Thu, 25 Jan 2024 09:23:45 -0800 Subject: [PATCH 1/9] rename variable to be more informative --- tests/e2e/yamcs/network.e2e.spec.js | 42 ++++++++++++++--------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/tests/e2e/yamcs/network.e2e.spec.js b/tests/e2e/yamcs/network.e2e.spec.js index 67ce032c..734ada70 100644 --- a/tests/e2e/yamcs/network.e2e.spec.js +++ b/tests/e2e/yamcs/network.e2e.spec.js @@ -32,18 +32,18 @@ import { setFixedTimeMode } from '../opensource/appActions.js'; */ test.describe("Quickstart network requests @yamcs", () => { // Keeping track of network requests during the tests. - let networkRequests = []; - let filteredRequests = []; + let allNetworkRequests = []; + let fetchRequests = []; // These variables hold the promises for specific network requests we expect to occur. let parameterArchiveGet, batchGetStaleness, allParams, userGet, mdbOverride, mdbGet; test('Validate network traffic to YAMCS', async ({ page }) => { - // Listening for all network requests and pushing them into networkRequests array. - page.on('request', request => networkRequests.push(request)); + // Listening for all network requests and pushing them into allNetworkRequests array. + page.on('request', request => allNetworkRequests.push(request)); // Setting up promises to wait for specific network responses. - networkRequests = []; + allNetworkRequests = []; mdbGet = page.waitForResponse('**/api/mdb/myproject/space-systems'); allParams = page.waitForResponse('**/api/mdb/myproject/parameters?details=yes&limit=1000'); userGet = page.waitForResponse('**/api/user/'); @@ -52,8 +52,8 @@ test.describe("Quickstart network requests @yamcs", () => { // Testing the initial page load and verifying the presence of specific elements. await page.goto("./", { waitUntil: "networkidle" }); await Promise.all([mdbGet, allParams, userGet, mdbOverride]); - filteredRequests = filterNonFetchRequests(networkRequests); - expect(filteredRequests.length).toBe(4); + fetchRequests = filterNonFetchRequests(allNetworkRequests); + expect(fetchRequests.length).toBe(4); // I'm not sure what is going on here const myProjectTreeItem = page.locator('.c-tree__item').filter({ hasText: 'myproject' }); @@ -63,16 +63,16 @@ test.describe("Quickstart network requests @yamcs", () => { // More UI interactions and network request verifications. await page.waitForLoadState('networkidle'); - networkRequests = []; + allNetworkRequests = []; batchGetStaleness = page.waitForResponse('**/api/processors/myproject/realtime/parameters:batchGet'); await page.getByRole('treeitem', { name: 'Expand CCSDS_Packet_Sequence' }).click(); await batchGetStaleness; await page.waitForLoadState('networkidle'); - expect(networkRequests.length).toBe(1); + expect(allNetworkRequests.length).toBe(1); // Further UI interactions and network requests verification. - networkRequests = []; + allNetworkRequests = []; parameterArchiveGet = page.waitForResponse('**/api/archive/myproject/parameters/myproject/CCSDS_Packet_Length/samples**'); batchGetStaleness = page.waitForResponse('**/api/processors/myproject/realtime/parameters:batchGet'); await page.getByRole('treeitem', { name: 'CCSDS_Packet_Length' }).click(); @@ -80,18 +80,18 @@ test.describe("Quickstart network requests @yamcs", () => { await Promise.all([parameterArchiveGet, batchGetStaleness]); await page.waitForLoadState('networkidle'); - expect(networkRequests.length).toBe(2); + expect(allNetworkRequests.length).toBe(2); // Simulating the change to fixed time mode and validating network requests. - networkRequests = []; + allNetworkRequests = []; parameterArchiveGet = page.waitForResponse('**/api/archive/myproject/parameters/myproject/CCSDS_Packet_Length/samples**'); await setFixedTimeMode(page); await page.waitForLoadState('networkidle'); await parameterArchiveGet; - expect(networkRequests.length).toBe(1); + expect(allNetworkRequests.length).toBe(1); // Clicking on a different telemetry item to generate new requests. - networkRequests = []; + allNetworkRequests = []; let groupFlagsGet = page.waitForResponse('**/api/archive/myproject/parameters/myproject/CCSDS_Packet_Sequence.GroupFlags**'); let countGet = page.waitForResponse('**/api/archive/myproject/parameters/myproject/CCSDS_Packet_Sequence.Count**'); batchGetStaleness = page.waitForResponse('**/api/processors/myproject/realtime/parameters:batchGet'); @@ -101,10 +101,10 @@ test.describe("Quickstart network requests @yamcs", () => { await Promise.all([groupFlagsGet, countGet, batchGetStaleness]); - expect(networkRequests.length).toBe(3); + expect(allNetworkRequests.length).toBe(3); // Clicking on the telemetry item in Fixed Time mode to generate two requests. - networkRequests = []; + allNetworkRequests = []; parameterArchiveGet = page.waitForResponse('**/api/archive/myproject/parameters/myproject/CCSDS_Packet_Length/samples**'); batchGetStaleness = page.waitForResponse('**/api/processors/myproject/realtime/parameters:batchGet'); await page.getByRole('treeitem', { name: 'CCSDS_Packet_Length' }).click(); @@ -113,10 +113,10 @@ test.describe("Quickstart network requests @yamcs", () => { await Promise.all([parameterArchiveGet, batchGetStaleness]); // Waiting for debounced requests in YAMCS Latest Telemetry Provider to finish. - expect(networkRequests.length).toBe(2); + expect(allNetworkRequests.length).toBe(2); // Simulating a page refresh to generate a sequence of network requests. - networkRequests = []; + allNetworkRequests = []; userGet = page.waitForResponse('**/api/user/'); allParams = page.waitForResponse('**/api/mdb/myproject/parameters?details=yes&limit=1000'); mdbOverride = page.waitForResponse('**/api/mdb-overrides/myproject/realtime'); @@ -128,11 +128,11 @@ test.describe("Quickstart network requests @yamcs", () => { await Promise.all([allParams, userGet, mdbOverride, parameterArchiveGet, batchGetStaleness, mdbOverride]); // Waiting for debounced requests in YAMCS Latest Telemetry Provider to finish. - filteredRequests = filterNonFetchRequests(networkRequests); - expect(filteredRequests.length).toBe(6); + fetchRequests = filterNonFetchRequests(allNetworkRequests); + expect(fetchRequests.length).toBe(6); // Removing the 'request' event listener to prevent potential memory leaks. - page.removeListener('request', request => networkRequests.push(request)); + page.removeListener('request', request => allNetworkRequests.push(request)); }); /** From a9695f9d6f87e853e2ee2ba66df8e2bba2d4ce2c Mon Sep 17 00:00:00 2001 From: John Hill Date: Thu, 25 Jan 2024 11:06:41 -0800 Subject: [PATCH 2/9] for review --- .../framework/quickstartFixtures.e2e.spec.js | 43 ++++++ tests/e2e/playwright-quickstart.config.js | 2 +- tests/e2e/quickstartFixtures.js | 53 +++++++ tests/e2e/yamcs/actions.e2e.spec.js | 131 ++++++++++++++++++ 4 files changed, 228 insertions(+), 1 deletion(-) create mode 100644 tests/e2e/framework/quickstartFixtures.e2e.spec.js create mode 100644 tests/e2e/quickstartFixtures.js create mode 100644 tests/e2e/yamcs/actions.e2e.spec.js diff --git a/tests/e2e/framework/quickstartFixtures.e2e.spec.js b/tests/e2e/framework/quickstartFixtures.e2e.spec.js new file mode 100644 index 00000000..022066dd --- /dev/null +++ b/tests/e2e/framework/quickstartFixtures.e2e.spec.js @@ -0,0 +1,43 @@ +/***************************************************************************** + * 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. + *****************************************************************************/ + +/* +This test suite is dedicated to testing our use of our custom fixtures to verify +that they are working as expected. +*/ + +import { expect, request, test} from '../quickstartFixtures.js'; + +test.describe('quickstartFixtures tests', () => { + test('filterNonFetchRequests', async ({ page }) => { + // Listening for all network requests and pushing them into allNetworkRequests array. + page.on('request', request => allNetworkRequests.push(request)); + + // Testing the initial page load and verifying the presence of specific elements. + await page.goto("./", { waitUntil: "networkidle" }); + fetchRequests = filterNonFetchRequests(allNetworkRequests); + expect(fetchRequests.length).toBe(4); + + // Removing the 'request' event listener to prevent potential memory leaks. + page.removeListener('request', request => allNetworkRequests.push(request)); + }); +}); diff --git a/tests/e2e/playwright-quickstart.config.js b/tests/e2e/playwright-quickstart.config.js index c7cbe537..b21e82c3 100644 --- a/tests/e2e/playwright-quickstart.config.js +++ b/tests/e2e/playwright-quickstart.config.js @@ -21,7 +21,7 @@ const config = { command: 'npm run start:coverage', url: 'http://localhost:9000/#', timeout: 120 * 1000, - reuseExistingServer: false + reuseExistingServer: true }, workers: 1, projects: [ diff --git a/tests/e2e/quickstartFixtures.js b/tests/e2e/quickstartFixtures.js new file mode 100644 index 00000000..a91ce2ff --- /dev/null +++ b/tests/e2e/quickstartFixtures.js @@ -0,0 +1,53 @@ +/* eslint-disable no-undef */ +/***************************************************************************** + * 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. + *****************************************************************************/ + +/** + * The file contains custom fixtures which extend the base functionality of the Playwright fixtures + * and appActions. These fixtures should be generalized across all plugins. + */ + +// import { createDomainObjectWithDefaults } from './appActions.js'; +import { test, request, expect } from './opensource/pluginFixtures.js'; + +/** + * The name of the "My Items" folder in the domain object tree. + * + * Default: `"My Items"` + * + * @type {string} + */ +export const myItemsFolderName = 'My Items'; + +export { expect, request, test }; + +/** + * Filters out non-fetch requests from the given array of network requests. + * This includes preflight CORS, fetching stylesheets, page icons, etc. + * Requires that a page requests object be instantiated. + * page.on('request', request => allNetworkRequests.push(request)); + * @param {Array} requests - Array of network requests to filter. + * @returns {Array} Filtered network requests. + */ +export function filterNonFetchRequests(requests) { + return requests.filter(request => request.resourceType() === 'fetch'); +} \ No newline at end of file diff --git a/tests/e2e/yamcs/actions.e2e.spec.js b/tests/e2e/yamcs/actions.e2e.spec.js new file mode 100644 index 00000000..4408a75b --- /dev/null +++ b/tests/e2e/yamcs/actions.e2e.spec.js @@ -0,0 +1,131 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2022, 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. + *****************************************************************************/ +/* + * Network Specific Tests for Open MCT and YAMCS connectivity. + * This suite verifies the network requests made by the application to ensure correct interaction with YAMCS. + */ + +import { test, expect, filterNonFetchRequests } from '../quickstartFixtures.js'; +import { createDomainObjectWithDefaults, expandEntireTree, setFixedTimeMode } from '../opensource/appActions.js'; + +test.describe('Reload action', () => { + test.beforeEach(async ({ page }) => { + await page.goto('./', { waitUntil: 'domcontentloaded' }); + + const displayLayout = await createDomainObjectWithDefaults(page, { + type: 'Display Layout' + }); + + const alphaTable = await createDomainObjectWithDefaults(page, { + type: 'Telemetry Table', + name: 'Alpha Table' + }); + + await page.getByLabel('Expand myproject folder').click(); + await page.getByLabel('Expand myproject folder').click(); + await page.locator('.c-table__body-w').click(); + await page.getByLabel('Edit Object').click(); + await page.getByRole('treeitem', { name: 'Battery1_Temp' }).dragTo(page.locator('.c-table__body-w')); + + // Save (exit edit mode) + await page.getByRole('button', { name: 'Save' }).click(); + await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); + + const betaTable = await createDomainObjectWithDefaults(page, { + type: 'Telemetry Table', + name: 'Beta Table' + }); + + await page.locator('.c-table__body-w').click(); + await page.getByLabel('Edit Object').click(); + await page.getByRole('treeitem', { name: 'Battery1_Voltage' }).dragTo(page.locator('.c-table__body-w')); + + // Save (exit edit mode) + await page.getByRole('button', { name: 'Save' }).click(); + await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); + + await page.goto(displayLayout.url); + + + + await page.getByLabel('Edit Object', { exact: true }).click(); + + await page.dragAndDrop(`text='Alpha Table'`, '.l-layout__grid-holder', { + targetPosition: { x: 0, y: 0 } + }); + + await page.dragAndDrop(`text='Beta Table'`, '.l-layout__grid-holder', { + targetPosition: { x: 0, y: 250 } + }); + + await page.locator('button[title="Save"]').click(); + await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); + }); + + test.only('can reload display layout and its children', async ({ page }) => { + const beforeReloadAlphaTelemetryValue = await page + .getByLabel('Alpha Table table content') + .getByLabel('wavelengths table cell') + .first() + .getAttribute('title'); + const beforeReloadBetaTelemetryValue = await page + .getByLabel('Beta Table table content') + .getByLabel('wavelengths table cell') + .first() + .getAttribute('title'); + // reload alpha + await page.getByTitle('View menu items').first().click(); + await page.getByRole('menuitem', { name: /Reload/ }).click(); + + const afterReloadAlphaTelemetryValue = await page + .getByLabel('Alpha Table table content') + .getByLabel('wavelengths table cell') + .first() + .getAttribute('title'); + const afterReloadBetaTelemetryValue = await page + .getByLabel('Beta Table table content') + .getByLabel('wavelengths table cell') + .first() + .getAttribute('title'); + + expect(beforeReloadAlphaTelemetryValue).not.toEqual(afterReloadAlphaTelemetryValue); + expect(beforeReloadBetaTelemetryValue).toEqual(afterReloadBetaTelemetryValue); + + // now reload parent + await page.getByTitle('More actions').click(); + await page.getByRole('menuitem', { name: /Reload/ }).click(); + + const fullReloadAlphaTelemetryValue = await page + .getByLabel('Alpha Table table content') + .getByLabel('wavelengths table cell') + .first() + .getAttribute('title'); + const fullReloadBetaTelemetryValue = await page + .getByLabel('Beta Table table content') + .getByLabel('wavelengths table cell') + .first() + .getAttribute('title'); + + expect(fullReloadAlphaTelemetryValue).not.toEqual(afterReloadAlphaTelemetryValue); + expect(fullReloadBetaTelemetryValue).not.toEqual(afterReloadBetaTelemetryValue); + }); +}); \ No newline at end of file From f590a2c1ade9dba87bfc28e6816505a17bdc28a6 Mon Sep 17 00:00:00 2001 From: John Hill Date: Fri, 26 Jan 2024 09:02:44 -0800 Subject: [PATCH 3/9] more fixes --- src/providers/limit-provider.js | 1 + .../framework/quickstartFixtures.e2e.spec.js | 28 +++-- tests/e2e/quickstartFixtures.js | 5 +- tests/e2e/yamcs/actions.e2e.spec.js | 108 ++++++++++++------ 4 files changed, 97 insertions(+), 45 deletions(-) diff --git a/src/providers/limit-provider.js b/src/providers/limit-provider.js index 54722ed6..8e64dd05 100644 --- a/src/providers/limit-provider.js +++ b/src/providers/limit-provider.js @@ -100,6 +100,7 @@ export default class LimitProvider { const limits = domainObject.configuration.limits; return { + // eslint-disable-next-line require-await limits: async () => limits }; } diff --git a/tests/e2e/framework/quickstartFixtures.e2e.spec.js b/tests/e2e/framework/quickstartFixtures.e2e.spec.js index 022066dd..7d91dbd1 100644 --- a/tests/e2e/framework/quickstartFixtures.e2e.spec.js +++ b/tests/e2e/framework/quickstartFixtures.e2e.spec.js @@ -25,19 +25,25 @@ This test suite is dedicated to testing our use of our custom fixtures to verify that they are working as expected. */ -import { expect, request, test} from '../quickstartFixtures.js'; +import { expect, filterNonFetchRequests, test} from '../quickstartFixtures.js'; test.describe('quickstartFixtures tests', () => { - test('filterNonFetchRequests', async ({ page }) => { - // Listening for all network requests and pushing them into allNetworkRequests array. - page.on('request', request => allNetworkRequests.push(request)); + // Keeping track of network requests during the tests. + let allNetworkRequests = []; + let fetchRequests = []; - // Testing the initial page load and verifying the presence of specific elements. - await page.goto("./", { waitUntil: "networkidle" }); - fetchRequests = filterNonFetchRequests(allNetworkRequests); - expect(fetchRequests.length).toBe(4); + test('filterNonFetchRequests', async ({ page }) => { + // Listening for all network requests and pushing them into allNetworkRequests array. + page.on('request', request => allNetworkRequests.push(request)); - // Removing the 'request' event listener to prevent potential memory leaks. - page.removeListener('request', request => allNetworkRequests.push(request)); - }); + // Setting up promises to wait for specific network responses. + // Testing the initial page load and verifying the presence of specific elements. + await page.goto("./", { waitUntil: "networkidle" }); + fetchRequests = filterNonFetchRequests(allNetworkRequests); + expect(allNetworkRequests.length).toBeGreaterThan(0); + expect(fetchRequests.length).toBeGreaterThan(0); + + // Removing the 'request' event listener to prevent potential memory leaks. + page.removeListener('request', request => allNetworkRequests.push(request)); + }); }); diff --git a/tests/e2e/quickstartFixtures.js b/tests/e2e/quickstartFixtures.js index a91ce2ff..31ccf0b5 100644 --- a/tests/e2e/quickstartFixtures.js +++ b/tests/e2e/quickstartFixtures.js @@ -1,4 +1,3 @@ -/* eslint-disable no-undef */ /***************************************************************************** * Open MCT, Copyright (c) 2014-2024, United States Government * as represented by the Administrator of the National Aeronautics and Space @@ -43,8 +42,10 @@ export { expect, request, test }; /** * Filters out non-fetch requests from the given array of network requests. * This includes preflight CORS, fetching stylesheets, page icons, etc. - * Requires that a page requests object be instantiated. + * Requires that a page requests instantiated like so + * ```js * page.on('request', request => allNetworkRequests.push(request)); + * ``` * @param {Array} requests - Array of network requests to filter. * @returns {Array} Filtered network requests. */ diff --git a/tests/e2e/yamcs/actions.e2e.spec.js b/tests/e2e/yamcs/actions.e2e.spec.js index 4408a75b..2b63c44b 100644 --- a/tests/e2e/yamcs/actions.e2e.spec.js +++ b/tests/e2e/yamcs/actions.e2e.spec.js @@ -25,107 +25,151 @@ */ import { test, expect, filterNonFetchRequests } from '../quickstartFixtures.js'; -import { createDomainObjectWithDefaults, expandEntireTree, setFixedTimeMode } from '../opensource/appActions.js'; +import { createDomainObjectWithDefaults, setFixedTimeMode } from '../opensource/appActions.js'; test.describe('Reload action', () => { + let displayLayout; + let batchGet; + let batchGet2; + let battery1tempRequest; + let battery1tempRequestCont; + let battery1voltageRequest; + let battery1voltageRequestCont; + let allNetworkRequests = []; test.beforeEach(async ({ page }) => { await page.goto('./', { waitUntil: 'domcontentloaded' }); - const displayLayout = await createDomainObjectWithDefaults(page, { + displayLayout = await createDomainObjectWithDefaults(page, { type: 'Display Layout' }); - const alphaTable = await createDomainObjectWithDefaults(page, { + await createDomainObjectWithDefaults(page, { type: 'Telemetry Table', name: 'Alpha Table' }); + //Expand the quickstart myproject twice await page.getByLabel('Expand myproject folder').click(); await page.getByLabel('Expand myproject folder').click(); - await page.locator('.c-table__body-w').click(); + + //Add Battery1_Temp to Alpha Table await page.getByLabel('Edit Object').click(); await page.getByRole('treeitem', { name: 'Battery1_Temp' }).dragTo(page.locator('.c-table__body-w')); + await page.getByRole('button', { name: 'Save' }).click(); + await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); - // Save (exit edit mode) - await page.getByRole('button', { name: 'Save' }).click(); - await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); - - const betaTable = await createDomainObjectWithDefaults(page, { + await createDomainObjectWithDefaults(page, { type: 'Telemetry Table', name: 'Beta Table' }); - await page.locator('.c-table__body-w').click(); - await page.getByLabel('Edit Object').click(); - await page.getByRole('treeitem', { name: 'Battery1_Voltage' }).dragTo(page.locator('.c-table__body-w')); - - // Save (exit edit mode) - await page.getByRole('button', { name: 'Save' }).click(); - await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); - - await page.goto(displayLayout.url); - - + //Add Battery1_Voltage to Beta Table + await page.getByLabel('Edit Object').click(); + await page.getByRole('treeitem', { name: 'Battery1_Voltage' }).dragTo(page.locator('.c-table__body-w')); + await page.getByRole('button', { name: 'Save' }).click(); + await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); + //Add both Telemetry Tables to the Display Layout + await page.goto(displayLayout.url, { waitUntil: 'domcontentloaded' }); await page.getByLabel('Edit Object', { exact: true }).click(); - + await page.getByLabel('Collapse myproject folder').first().click(); + await page.getByLabel('Expand My Items folder').click(); await page.dragAndDrop(`text='Alpha Table'`, '.l-layout__grid-holder', { targetPosition: { x: 0, y: 0 } }); - await page.dragAndDrop(`text='Beta Table'`, '.l-layout__grid-holder', { targetPosition: { x: 0, y: 250 } }); - await page.locator('button[title="Save"]').click(); await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); + + + await page.goto(displayLayout.url, { waitUntil: 'networkidle' }); + //Set to 1 Minute to reduce the time the opportunity for paginated data + // await page.getByRole('menuitem', { name: /Real-Time/ }).click(); + await page.getByLabel('Start offset: 00:30:00').click(); + await page.getByLabel('Start offset minutes').fill('1'); + await page.getByLabel('Submit time offsets').click(); + await page.waitForLoadState('networkidle'); }); - test.only('can reload display layout and its children', async ({ page }) => { + test.only('can reload display layout and its telemetry table children', async ({ page }) => { + // Listening for all network requests and pushing them into allNetworkRequests array. + page.on('request', request => allNetworkRequests.push(request)); + const beforeReloadAlphaTelemetryValue = await page .getByLabel('Alpha Table table content') - .getByLabel('wavelengths table cell') + .getByLabel('value table cell') .first() .getAttribute('title'); const beforeReloadBetaTelemetryValue = await page .getByLabel('Beta Table table content') - .getByLabel('wavelengths table cell') + .getByLabel('value table cell') .first() .getAttribute('title'); - // reload alpha + + //reload Alpha Table and verify network correctness + await page.waitForLoadState('networkidle'); + allNetworkRequests = []; + + battery1tempRequest = page.waitForResponse('**/api/archive/myproject/parameters/myproject/Battery1_Temp**') + battery1tempRequestCont = page.waitForResponse('**/api/archive/myproject/parameters/myproject/Battery1_Temp**') + batchGet = page.waitForResponse('**/api/processors/myproject/realtime/parameters:batchGet'); + await page.getByTitle('View menu items').first().click(); await page.getByRole('menuitem', { name: /Reload/ }).click(); + await Promise.all([battery1tempRequest, battery1tempRequestCont, batchGet]); + await page.waitForLoadState('networkidle'); + expect(allNetworkRequests.length).toBe(2); + const afterReloadAlphaTelemetryValue = await page .getByLabel('Alpha Table table content') - .getByLabel('wavelengths table cell') + .getByLabel('value table cell') .first() .getAttribute('title'); const afterReloadBetaTelemetryValue = await page .getByLabel('Beta Table table content') - .getByLabel('wavelengths table cell') + .getByLabel('value table cell') .first() .getAttribute('title'); expect(beforeReloadAlphaTelemetryValue).not.toEqual(afterReloadAlphaTelemetryValue); expect(beforeReloadBetaTelemetryValue).toEqual(afterReloadBetaTelemetryValue); + await page.waitForLoadState('networkidle'); + allNetworkRequests = []; + + battery1tempRequest = page.waitForResponse('**/api/archive/myproject/parameters/myproject/Battery1_Temp**') + battery1tempRequestCont = page.waitForResponse('**/api/archive/myproject/parameters/myproject/Battery1_Temp**') + battery1voltageRequest = page.waitForResponse('**/api/archive/myproject/parameters/myproject/Battery1_Voltage**') + battery1voltageRequestCont = page.waitForResponse('**/api/archive/myproject/parameters/myproject/Battery1_Voltage**') + batchGet = page.waitForResponse('**/api/processors/myproject/realtime/parameters:batchGet'); + batchGet2 = page.waitForResponse('**/api/processors/myproject/realtime/parameters:batchGet'); + + await page.getByTitle('More actions').click(); + await page.getByRole('menuitem', { name: /Reload/ }).click(); + await Promise.all([battery1tempRequest, battery1tempRequestCont, batchGet]); + expect(allNetworkRequests.length).toBe(3); + // now reload parent await page.getByTitle('More actions').click(); await page.getByRole('menuitem', { name: /Reload/ }).click(); const fullReloadAlphaTelemetryValue = await page .getByLabel('Alpha Table table content') - .getByLabel('wavelengths table cell') + .getByLabel('value table cell') .first() .getAttribute('title'); const fullReloadBetaTelemetryValue = await page .getByLabel('Beta Table table content') - .getByLabel('wavelengths table cell') + .getByLabel('value table cell') .first() .getAttribute('title'); expect(fullReloadAlphaTelemetryValue).not.toEqual(afterReloadAlphaTelemetryValue); expect(fullReloadBetaTelemetryValue).not.toEqual(afterReloadBetaTelemetryValue); }); -}); \ No newline at end of file + test.fixme('can reload notebook', async ({ page }) => {}); + test.fixme('can reload gauge', async ({ page }) => {}); +}); From 1b1165921aac979bc8ee6d773bd9b210e2f75adc Mon Sep 17 00:00:00 2001 From: John Hill Date: Tue, 26 Mar 2024 11:02:16 -0700 Subject: [PATCH 4/9] get this off my laptop --- tests/e2e/yamcs/actions.e2e.spec.js | 10 +-- tests/e2e/yamcs/tabs.e2e.spec.js | 98 +++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 7 deletions(-) create mode 100644 tests/e2e/yamcs/tabs.e2e.spec.js diff --git a/tests/e2e/yamcs/actions.e2e.spec.js b/tests/e2e/yamcs/actions.e2e.spec.js index 2b63c44b..b6660271 100644 --- a/tests/e2e/yamcs/actions.e2e.spec.js +++ b/tests/e2e/yamcs/actions.e2e.spec.js @@ -141,20 +141,16 @@ test.describe('Reload action', () => { allNetworkRequests = []; battery1tempRequest = page.waitForResponse('**/api/archive/myproject/parameters/myproject/Battery1_Temp**') - battery1tempRequestCont = page.waitForResponse('**/api/archive/myproject/parameters/myproject/Battery1_Temp**') battery1voltageRequest = page.waitForResponse('**/api/archive/myproject/parameters/myproject/Battery1_Voltage**') - battery1voltageRequestCont = page.waitForResponse('**/api/archive/myproject/parameters/myproject/Battery1_Voltage**') batchGet = page.waitForResponse('**/api/processors/myproject/realtime/parameters:batchGet'); batchGet2 = page.waitForResponse('**/api/processors/myproject/realtime/parameters:batchGet'); await page.getByTitle('More actions').click(); await page.getByRole('menuitem', { name: /Reload/ }).click(); - await Promise.all([battery1tempRequest, battery1tempRequestCont, batchGet]); - expect(allNetworkRequests.length).toBe(3); + await Promise.all([battery1tempRequest, battery1voltageRequest, batchGet, batchGet2]); + await page.waitForLoadState('networkidle'); - // now reload parent - await page.getByTitle('More actions').click(); - await page.getByRole('menuitem', { name: /Reload/ }).click(); + expect(allNetworkRequests.length).toBe(3); const fullReloadAlphaTelemetryValue = await page .getByLabel('Alpha Table table content') diff --git a/tests/e2e/yamcs/tabs.e2e.spec.js b/tests/e2e/yamcs/tabs.e2e.spec.js new file mode 100644 index 00000000..512f0e91 --- /dev/null +++ b/tests/e2e/yamcs/tabs.e2e.spec.js @@ -0,0 +1,98 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2022, 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. + *****************************************************************************/ +/* + * Network Specific Tests for Open MCT and YAMCS connectivity with regard to tabs + */ + +import { test, expect, filterNonFetchRequests } from '../quickstartFixtures.js'; +import { createDomainObjectWithDefaults, setFixedTimeMode } from '../opensource/appActions.js'; + +test.describe('Tabs View', () => { + let tabsView; + let table; + let notebook; + let sineWaveGenerator; + + test.beforeEach(async ({ page }) => { + await page.goto('./', { waitUntil: 'domcontentloaded' }); + + tabsView = await createDomainObjectWithDefaults(page, { + type: 'Tabs View' + }); + table = await createDomainObjectWithDefaults(page, { + type: 'Telemetry Table', + parent: tabsView.uuid + }); + await createDomainObjectWithDefaults(page, { + type: 'Event Message Generator', + parent: table.uuid + }); + notebook = await createDomainObjectWithDefaults(page, { + type: 'Notebook', + parent: tabsView.uuid + }); + sineWaveGenerator = await createDomainObjectWithDefaults(page, { + type: 'Sine Wave Generator', + parent: tabsView.uuid + }); + }); + + test('Renders tabbed elements', async ({ page }) => { + await page.goto(tabsView.url); + + // select first tab + await page.getByLabel(`${table.name} tab`, { exact: true }).click(); + // ensure table header visible + await expect(page.getByRole('searchbox', { name: 'message filter input' })).toBeVisible(); + + // no canvas (i.e., sine wave generator) in the document should be visible + await expect(page.locator('canvas[id=webglContext]')).toBeHidden(); + + // select second tab + await page.getByLabel(`${notebook.name} tab`, { exact: true }).click(); + + // ensure notebook visible + await expect(page.locator('.c-notebook__drag-area')).toBeVisible(); + + // no canvas (i.e., sine wave generator) in the document should be visible + await expect(page.locator('canvas[id=webglContext]')).toBeHidden(); + + // select third tab + await page.getByLabel(`${sineWaveGenerator.name} tab`, { exact: true }).click(); + + // expect sine wave generator visible + await expect(page.locator('.c-plot')).toBeVisible(); + + // expect two canvases (i.e., overlay & main canvas for sine wave generator) to be visible + await expect(page.locator('canvas')).toHaveCount(2); + await expect(page.locator('canvas').nth(0)).toBeVisible(); + await expect(page.locator('canvas').nth(1)).toBeVisible(); + + // now try to select the first tab again + await page.getByLabel(`${table.name} tab`, { exact: true }).click(); + // ensure table header visible + await expect(page.getByRole('searchbox', { name: 'message filter input' })).toBeVisible(); + + // no canvas (i.e., sine wave generator) in the document should be visible + await expect(page.locator('canvas[id=webglContext]')).toBeHidden(); + }); +}); \ No newline at end of file From edd8ec4f8b6f098482328d01b6bc5d0142e331e1 Mon Sep 17 00:00:00 2001 From: John Hill Date: Tue, 17 Sep 2024 07:12:23 -0700 Subject: [PATCH 5/9] Mct7343 tests merge (#479) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Telemetry batching and throttling (#412) * Refactored for new batched worker provider * Skipp callbacks with no telemetry * Process messages for that remote-clock before any others to avoid race conditions * Fixing bugs * Added explanatory comments. Make max batch size and throttle rate configuration * Removed TODO comment in lieu of a followup ticket * Make batch configuration explicit in the example * Tell Istanbul to ignore serialized functions * Fixing linting errors * Allow passing in a promise that resolves to a request cache strategy for dictionary load (#418) * temporarily enable source maps * pass in cacheStrategy to objectProvider * remove unused parameters * change cache strategy to be dynamic * disable eval-source-map in prod * lint fix * fix lint error * add README section explaining new feature --------- Co-authored-by: John Hill * Change promise to function `getDictionaryRequestOptions` (#421) * make request promise a function getRequestOptions * rename for clarity * change promise to a function * describe using getRequestOptions * temporarily enable source maps for debugging * Revert "temporarily enable source maps for debugging" This reverts commit 6e40684a55316934e1822f90a5e89b60015b6237. * Default to returning a Promise --------- Co-authored-by: Andrew Henry * Bump nick-fields/retry from 2 to 3 (#422) Bumps [nick-fields/retry](https://github.com/nick-fields/retry) from 2 to 3. - [Release notes](https://github.com/nick-fields/retry/releases) - [Changelog](https://github.com/nick-fields/retry/blob/master/.releaserc.js) - [Commits](https://github.com/nick-fields/retry/compare/v2...v3) --- updated-dependencies: - dependency-name: nick-fields/retry dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * feat: Mission Status for Situational Awareness (#415) * feat: first cut of `missionStatus` parameter and telemetry * feat(WIP): link up mission statuses * fix: typo * fix: register missionStatus type in Open MCT * feat: continue hooking up moving parts * refactor: more method renames * refactor: operatorStatusParameter to export functions * refactor: rename * remove: unimplemented argument * refactor: a single endline * refactor: rename `MissionRole` -> `MissionAction` * refactor: woops missed one * feat: user authorization * fix: use `openmct.telemetry` api for realtime subscriptions * Check that the mdb override is of parameter type. Discard any other mdb overrides (like algorithms) (#382) * Check that the mdb override is of parameter type. Discard any other mdb overrides (like algorithms) * Small change to logic * Fix search test locator * Address review comments * fix: typo --------- Co-authored-by: Mazzella, Jesse D. (ARC-TI)[KBR Wyle Services, LLC] * Update to 4.0.0 (#426) Remove traces of 3.3.0 * Break at buildtime (#429) * Add formatter for Binary values from YAMCS (#438) * using atob on binary values * using custom base64 to hex function on binary values * add a little flair, aka, a hex prefix * moving formatting to a custom formatter in openmct * adding new binary to hex formatter * fix typo * module import syntax errors * fix another typo * formatting * Fix realtime limits (#396) * Add logic to clear limits in yamcs before running each limits test. Add a new test to check that no network requests occur when redrawing limits due to resizing * Add test to ensure no network requests are sent on resizing plots * Fault management and staleness fixes - they will come in a separate PR --------- Co-authored-by: Andrew Henry * Improve telemetry batching (#433) * Install remote clock to allow more flight-like testing, but leave Local Clock as the default. * Auto-fixed some linting errors that snuck into master in binaryToHexFormatter so ignore those changes. * Renamed rate to maxBatchWait to improve clarity. * Added extensive e2e tests to verify that Open MCT: * Does not drop telemetry while loading (the original issue) * Always displays the latest data according to Yamcs * Drops telemetry when UI is under sufficient load, and warns the user * Reconnects after a WebSocket dropout, and re-subscribes (good catch @shefalijoshi !) * VIPEROMCT-494: realtime and historical providers should provide same datum format (#444) * do not include id * refactor so realtime and historical use same datum generator * fix(#447): Return mdb override limits correctly on resize (#448) * fix: correctly return limits * test(e2e): assert limit lines values before and after resize --------- Co-authored-by: Scott Bell * Staleness and fault management fix (#440) * Add logic to clear limits in yamcs before running each limits test. Add a new test to check that no network requests occur when redrawing limits due to resizing * Remove test.only * Change test organization, based on review comments * Remove focused test * Add test to ensure no network requests are sent on resizing plots * remove focused test * Clear limits before and after each test * Create a staleness e2e tests stub * Use the openmct telemetry API to find the provider for realtime fault management alarm objects * Pass openmct as a parameter to yamcs fault management provider * adds basic stubs for staleness and fault management tests * Update tests/e2e/yamcs/faultManagement.e2e.spec.js Co-authored-by: Jesse Mazzella * Update tests/e2e/yamcs/staleness.e2e.js Co-authored-by: Jesse Mazzella * Remove openmct argument --------- Co-authored-by: Jesse Mazzella Co-authored-by: Andrew Henry * Add format metadata to Events and Commands (#443) * WIP * Added gauges and alphanumerics to the test * Do not activate remote clock by default * Activate remote clock in tests * Make max batch wait config rather than hard-coded * Don't hard-code quickstart URL * Remove debugging code * Clean up and externalize test configuration * Reference modified Yamcs quickstart branch until integrated * Fixed linting errors * Fixed quickstart tools tests which I had broken * Use akhenry/quickstart not yamcs/quickstart for now * Build against matching Open MCT branch * Fixed open mct references * Trying to make work with no feedback about why it's failing * Set playback rate * Add target for 10Hz * Removed parameters missing from github action? (jvm introspection stuff. Maybe security?) * Revert change to quickstart URLs * Fixed issue where resubscribe was not happening on websocket dropout * auto-fixed linting errors * Do not configure remote clock by default. Creates a dependency on yamcs-quickstart * Switch back to building against master * source maps * test * adding formats so sorting works * adding format for reception time as well * adding metadata to commands so they will sort without issues * removing source maps * lint fixes --------- Co-authored-by: Andrew Henry Co-authored-by: Jesse Mazzella * Warn if default alarms are of a different type than a staticAlarm and… (#435) * Warn if default alarms are of a different type than a staticAlarm and allow the app to continue * Use better english for warning message. * Fix linting issue --------- Co-authored-by: Andrew Henry Co-authored-by: Jesse Mazzella * Update PULL_REQUEST_TEMPLATE.md (#450) Sync with openmct's template * Fix import require (#446) * chore: rename webpack.*.js -> webpack.*.mjs * chore: rename *e2e*.js -> *e2e*.mjs * chore: add workspace, update opensource script and playwright config * fix: get openmct e2e tests before npm install * chore: *.js -> *.mjs * fix: npm script * chore: revert workflow change, use post script instead * refactor: async ops deps check * refactor: add docs, resolve path * chore: generate `source-map` for coverage * fix: remove `.only` * fix: change branch for testing * fix: point to correct file * fix: make yamcs example work again * fix: add back context, remove unused alias * fix: use absolute path * coverage: match mjs files too * chore: specify working dirs instead of cd * fix: yaml whitespace issues * fix: swap order of operations on quickstart job * chore: remove debug branches * fix: bad merge * fix: upgrade playwright version in the workflow (fixme) * test: fix historicalData tests * [Historical Telemetry Provider] Respect "order" request options (#455) * accepting incoming order value from request options * verifying descending order for requests in limited mode * refactor: historicalData tests --------- Co-authored-by: Andrew Henry Co-authored-by: Mazzella, Jesse D. (ARC-TI)[KBR Wyle Services, LLC] * chore:bump deps to match openmct 4.0.0 (#457) * chore:bump deps to match openmct 4.0.0 * fix: update webpack devServer schema + lint/format --------- Co-authored-by: Mazzella, Jesse D. (ARC-TI)[KBR Wyle Services, LLC] * docs: add JSDocs for Mission Status (#432) * docs: mission-status-parameter * docs(WIP): mission-status-telemetry * docs: finish documention on `mission-status-telemetry` * docs: document `mission-status-parameter` * docs: document Mission Status configuration * Test for Telemetry Table mutation on non-mutable objects (#458) * update README.md steps as they have changed * adding a test to check for telemetry tables trying to mutate immutable objects * clarity * adding general error failure as well as assertion of mode change * chore: bump playwright to 1.44.0 (#460) * chore: bump playwright to 1.45.0 (#462) * chore: bump playwright to `1.45.2` (#465) * chore: bump playwright to `1.45.2` * chore: bump playwright to `1.45.2`, swap order of steps to be correct * test: fix locator * test: fix locator * test: fix locator * test: fix locators in anticipation of openmct e2e fixes (#467) * test: fix locators in anticipation of openmct e2e fixes * chore: use `make all`. `make all-10hz` had no effect before (maybe we thought it did?) but fails now due to Makefile upgrades * fix: forkin' hell - ok so it turns out that we were using a custom fork with the ability to change the data rate of the yamcs simulator script. akhenry is on vacation so i updated the branch and pushed it to my fork so we can get things moving. * test: revert changes which broke assertion * refactor: missed a spot :/ * build: go back to using actual quickstart repo now that our changes our merged (#473) * Deal with Parameter mdb overrides only (#471) * Check that the override is of type parameter before getting it's details. * New test to ensure that openmct loads correctly when mdb overrides exist * Update tests to use myproject visible assertion --------- Co-authored-by: John Hill * Test for Performance Mode changes incorrectly changing sort order (#469) * adding new test for sort order being sent in requests * Update yamcs quickstart version to match viper (#475) --------- Signed-off-by: dependabot[bot] Co-authored-by: Andrew Henry Co-authored-by: David Tsay <3614296+davetsay@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jesse Mazzella Co-authored-by: Shefali Joshi Co-authored-by: Mazzella, Jesse D. (ARC-TI)[KBR Wyle Services, LLC] Co-authored-by: Jamie V Co-authored-by: Scott Bell --- .github/PULL_REQUEST_TEMPLATE.md | 7 +- .github/workflows/yamcs-quickstart-e2e.yml | 23 +- .../{webpack.common.js => webpack.common.mjs} | 0 ...bpack.coverage.js => webpack.coverage.mjs} | 10 +- .webpack/{webpack.dev.js => webpack.dev.mjs} | 80 +-- .../{webpack.prod.js => webpack.prod.mjs} | 2 +- README.md | 17 + check-optional-dependencies.js | 24 - check-optional-dependencies.mjs | 55 ++ example/index.html | 2 +- example/index.js | 134 ++--- package.json | 53 +- src/const.js | 3 + src/openmct-yamcs.js | 44 +- .../BinaryToHexFormatter.js | 56 ++ .../binaryToHexFormatter/base64ToHex.js | 35 ++ .../binaryToHexFormatter/hexToBase64.js | 39 ++ src/plugins/binaryToHexFormatter/plugin.js | 29 + src/providers/commands.js | 64 ++- src/providers/events.js | 19 +- .../realtime-fault-provider.js | 9 +- .../yamcs-fault-provider.js | 6 +- .../historical-telemetry-provider.js | 26 +- src/providers/limit-provider.js | 5 +- src/providers/messages.js | 1 + src/providers/mission-status/README.md | 105 ++++ .../mission-status-parameter.js | 75 +++ .../mission-status-telemetry.js | 281 ++++++++++ src/providers/object-provider.js | 47 +- src/providers/realtime-provider.js | 270 +++++----- src/providers/staleness-provider.js | 3 +- .../user/operator-status-parameter.js | 24 +- src/providers/user/user-provider.js | 60 ++- src/utils.js | 35 +- tests/README.md | 10 +- tests/e2e/playwright-quickstart.config.js | 3 +- tests/e2e/yamcs/faultManagement.e2e.spec.mjs | 43 ++ ...lters.e2e.spec.js => filters.e2e.spec.mjs} | 5 +- ...2e.spec.js => historicalData.e2e.spec.mjs} | 126 ++--- ...limits.e2e.spec.js => limits.e2e.spec.mjs} | 136 +++-- tests/e2e/yamcs/load.e2e.spec.mjs | 74 +++ tests/e2e/yamcs/namesToParametersMap.json | 48 ++ ...twork.e2e.spec.js => network.e2e.spec.mjs} | 5 +- ...e.spec.js => quickstartSmoke.e2e.spec.mjs} | 3 +- tests/e2e/yamcs/quickstartTools.e2e.spec.mjs | 81 +++ tests/e2e/yamcs/quickstartTools.mjs | 81 +++ tests/e2e/yamcs/realtimeData.e2e.spec.mjs | 499 ++++++++++++++++++ ...search.e2e.spec.js => search.e2e.spec.mjs} | 3 +- tests/e2e/yamcs/staleness.e2e.mjs | 44 ++ tests/e2e/yamcs/telemetryTables.e2e.spec.mjs | 80 +++ .../test-data/e2e-real-time-test-layout.json | 1 + tests/git-opensource-tests.sh | 5 + 52 files changed, 2361 insertions(+), 529 deletions(-) rename .webpack/{webpack.common.js => webpack.common.mjs} (100%) rename .webpack/{webpack.coverage.js => webpack.coverage.mjs} (87%) rename .webpack/{webpack.dev.js => webpack.dev.mjs} (54%) rename .webpack/{webpack.prod.js => webpack.prod.mjs} (97%) delete mode 100644 check-optional-dependencies.js create mode 100644 check-optional-dependencies.mjs create mode 100644 src/plugins/binaryToHexFormatter/BinaryToHexFormatter.js create mode 100644 src/plugins/binaryToHexFormatter/base64ToHex.js create mode 100644 src/plugins/binaryToHexFormatter/hexToBase64.js create mode 100644 src/plugins/binaryToHexFormatter/plugin.js create mode 100644 src/providers/mission-status/README.md create mode 100644 src/providers/mission-status/mission-status-parameter.js create mode 100644 src/providers/mission-status/mission-status-telemetry.js create mode 100644 tests/e2e/yamcs/faultManagement.e2e.spec.mjs rename tests/e2e/yamcs/{filters.e2e.spec.js => filters.e2e.spec.mjs} (96%) rename tests/e2e/yamcs/{historicalData.e2e.spec.js => historicalData.e2e.spec.mjs} (61%) rename tests/e2e/yamcs/{limits.e2e.spec.js => limits.e2e.spec.mjs} (62%) create mode 100644 tests/e2e/yamcs/load.e2e.spec.mjs create mode 100644 tests/e2e/yamcs/namesToParametersMap.json rename tests/e2e/yamcs/{network.e2e.spec.js => network.e2e.spec.mjs} (98%) rename tests/e2e/yamcs/{quickstartSmoke.e2e.spec.js => quickstartSmoke.e2e.spec.mjs} (97%) create mode 100644 tests/e2e/yamcs/quickstartTools.e2e.spec.mjs create mode 100644 tests/e2e/yamcs/quickstartTools.mjs create mode 100644 tests/e2e/yamcs/realtimeData.e2e.spec.mjs rename tests/e2e/yamcs/{search.e2e.spec.js => search.e2e.spec.mjs} (96%) create mode 100644 tests/e2e/yamcs/staleness.e2e.mjs create mode 100644 tests/e2e/yamcs/telemetryTables.e2e.spec.mjs create mode 100644 tests/e2e/yamcs/test-data/e2e-real-time-test-layout.json diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index f6728a7d..444d552e 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -8,14 +8,15 @@ Closes + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +### `roles.yaml` + +In the `roles.yaml` file, the user with permission to set mission status must be given `WriteParameter` permission to the path(s) which +contain the Mission Status parameters: + +```yaml +# roles.yaml example granting the "Flight" role permission to set mission status +Flight: + Command: [] + CommandHistory: [ ".*" ] + ManageBucket: [] + ReadAlgorithm: [ ".*" ] + ReadBucket: [ ".*" ] + ReadPacket: [ ".*" ] + ReadParameter: [ ".*" ] + Stream: [] + WriteParameter: + - "/MyProject/MissionStatus/.*" + System: + - GetMissionDatabase + - ReadAlarms + - ReadCommandHistory + - ReadEvents + - ReadFileTransfers + - ReadLinks +``` + +### User Provider + +See the [Open MCT documentation](https://github.com/nasa/openmct/blob/634aeef06e8712d3806bcd15fa9e5901386e12b3/src/plugins/userIndicator/README.md) for information on how to configure the User Provider to support Mission Status. diff --git a/src/providers/mission-status/mission-status-parameter.js b/src/providers/mission-status/mission-status-parameter.js new file mode 100644 index 00000000..b585c566 --- /dev/null +++ b/src/providers/mission-status/mission-status-parameter.js @@ -0,0 +1,75 @@ +/***************************************************************************** + * 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. + *****************************************************************************/ + +const MISSION_STATUS_TYPE = 'yamcs.missionStatus'; +const MISSION_ACTION_NAMESPACE = 'OpenMCT:action'; + +/** + * Check if the parameter is a mission status parameter + * @param {Parameter} parameter + * @returns {boolean} true if the parameter is a mission status parameter, false otherwise + */ +export function isMissionStatusParameter(parameter) { + const aliases = parameter.alias; + + return aliases !== undefined + && aliases.some(alias => alias.name === MISSION_STATUS_TYPE); +} + +/** + * Get the mission action from the parameter + * @param {Parameter} parameter + * @returns {import("./mission-status-telemetry").MissionAction? } the mission action name if the parameter is a mission action parameter, null otherwise + */ +export function getMissionActionFromParameter(parameter) { + const aliases = parameter.alias; + + return aliases.find(alias => alias.namespace === MISSION_ACTION_NAMESPACE)?.name ?? null; +} + +/** + * Get the possible mission action statuses from the parameter + * @param {Parameter} parameter + * @returns {string[]} + */ +export function getPossibleMissionActionStatusesFromParameter(parameter) { + return parameter.type.enumValue; +} + +/** + * @typedef {import("./mission-status-telemetry").MdbEntry} MdbEntry + */ + +/** + * @typedef {object} Parameter + * @property {string} name + * @property {string} qualifiedName + * @property {object} type + * @property {string} type.engType + * @property {object} type.dataEncoding + * @property {string} type.dataEncoding.type + * @property {boolean} type.dataEncoding.littleEndian + * @property {number} type.dataEncoding.sizeInBits + * @property {string} type.dataEncoding.encoding + * @property {MdbEntry[]} type.enumValue + * @property {string} dataSource + */ diff --git a/src/providers/mission-status/mission-status-telemetry.js b/src/providers/mission-status/mission-status-telemetry.js new file mode 100644 index 00000000..0bec09fc --- /dev/null +++ b/src/providers/mission-status/mission-status-telemetry.js @@ -0,0 +1,281 @@ +/***************************************************************************** + * 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 { + idToQualifiedName +} from '../../utils.js'; + +export default class MissionStatusTelemetry { + #missionStatusMap; + #missionActions; + /** @type {Set} */ + #missionStatusParameterNames; + #missionActionToTelemetryObjectMap; + #setReady; + #readyPromise; + #url; + #instance; + #processor; + #openmct; + + constructor(openmct, { url, instance, processor = 'realtime' }) { + this.#missionStatusMap = {}; + this.#missionActions = new Set(); + this.#missionStatusParameterNames = new Set(); + this.#missionActionToTelemetryObjectMap = {}; + this.#readyPromise = new Promise((resolve) => this.#setReady = resolve); + this.#url = url; + this.#instance = instance; + this.#processor = processor; + this.#openmct = openmct; + } + + /** + * Set the status for a particular mission action. + * @param {MissionAction} action the mission action + * @param {MissionStatus} status the status + * @returns {Promise} true if the status was set successfully + */ + async setStatusForMissionAction(action, status) { + const telemetryObject = await this.getTelemetryObjectForAction(action); + const setParameterUrl = this.#buildUrl(telemetryObject.identifier); + let success = false; + + try { + const result = await fetch(setParameterUrl, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + type: 'SINT64', + sint64Value: status.key + }) + }); + + success = result.ok === true; + } catch (error) { + console.error(error); + } + + return success; + } + + /** + * Get the possible mission statuses. + * i.e: "Go" or "No Go" + * @returns {Promise} + */ + async getPossibleMissionStatuses() { + await this.#readyPromise; + + return Object.values(this.#missionStatusMap).map(status => this.toMissionStatusFromMdbEntry(status)); + } + + /** + * Get the default status for any mission action. + * Returns the first status in the list of possible statuses. + * @returns {Promise} + */ + async getDefaultStatusForAction() { + const possibleStatuses = await this.getPossibleMissionStatuses(); + + return possibleStatuses[0]; + } + + /** + * Adds a mission status to the list of possible statuses. + * @param {MissionStatus} status + */ + addStatus(status) { + this.#missionStatusMap[status.value] = status; + } + + /** + * Get the telemetry object for a mission action. + * @param {MissionAction} action the mission action + * @returns {Promise} the telemetry object + */ + async getTelemetryObjectForAction(action) { + await this.#readyPromise; + + return this.#missionActionToTelemetryObjectMap[action]; + } + + /** + * Check if this parameter name is a mission status parameter name. + * @param {string} parameterName + * @returns {boolean} true if the parameter name is a mission status parameter name + */ + async isMissionStatusParameterName(parameterName) { + await this.#readyPromise; + if (this.#missionStatusParameterNames.has(parameterName)) { + return true; + } + + const parameterRegExp = new RegExp(`^${parameterName}$`); + for (const missionStatusParameterName of this.#missionStatusParameterNames) { + if (parameterRegExp.test(missionStatusParameterName)) { + return true; + } + } + + return false; + } + + /** + * Set the telemetry object for a mission action. + * @param {MissionAction} action + * @param {TelemetryObject} telemetryObject + */ + setTelemetryObjectForAction(action, telemetryObject) { + this.#missionActionToTelemetryObjectMap[action] = telemetryObject; + } + + /** + * Add a mission action to the list of possible actions. + * @param {MissionAction} action + */ + addMissionAction(action) { + this.#missionActions.add(action); + } + + /** + * Add a mission status parameter name to the list of parameter names. + * @param {string} parameterName + */ + addMissionStatusParameterName(parameterName) { + this.#missionStatusParameterNames.add(parameterName); + } + + /** + * Get a list of all mission actions. + * @returns {Promise} + */ + async getAllMissionActions() { + await this.#readyPromise; + + return Array.from(this.#missionActions); + } + + /** + * Get the current status of a mission action given its MDB entry. + * @param {MdbEntry} yamcsStatus the MDB entry + * @returns {MissionStatus} + */ + toMissionStatusFromMdbEntry(yamcsStatus) { + return { + // eslint-disable-next-line radix + key: parseInt(yamcsStatus.value), + label: yamcsStatus.label + }; + } + + /** + * Receives a telemetry object and a datum and returns a mission status. + * @param {TelemetryObject} telemetryObject the telemetry object + * @param {Datum} datum the datum object + * @returns {MissionStatus} + */ + toStatusFromTelemetry(telemetryObject, datum) { + const metadata = this.#openmct.telemetry.getMetadata(telemetryObject); + const rangeMetadata = metadata.valuesForHints(['range'])[0]; + const formatter = this.#openmct.telemetry.getValueFormatter(rangeMetadata); + const timestampMetadata = metadata.valuesForHints(['domain'])[0]; + const dateFormatter = this.#openmct.telemetry.getValueFormatter(timestampMetadata); + + return { + key: formatter.parse(datum), + label: formatter.format(datum), + timestamp: dateFormatter.parse(datum) + }; + } + + /** + * Fires when the dictionary is loaded. + */ + dictionaryLoadComplete() { + this.#setReady(); + } + + /** + * Construct the URL for a parameter. + * @param {import('openmct').Identifier} id the identifier + * @returns {string} + */ + #buildUrl(id) { + let url = `${this.#url}api/processors/${this.#instance}/${this.#processor}/parameters/${idToQualifiedName(id.key)}`; + + return url; + } +} + +/** + * @typedef {Object} MissionStatus + * @property {number} key + * @property {string} label + * @property {number?} timestamp + */ + +/** + * @typedef {string} MissionAction + */ + +/** + * @typedef {Object} TelemetryObject + * @property {import('openmct').Identifier} identifier + * @property {string} name + * @property {string} type + * @property {string} location + * @property {string} configuration + * @property {string} domain + * @property {object} telemetry + * @property {TelemetryValue[]} telemetry.values + * @property {string} metadata + * @property {string} composition + * @property {string} object + * @property {string} value + */ + +/** + * @typedef {object} TelemetryValue + * @property {string} key + * @property {string} name + * @property {string} format + * @property {string} source + * @property {object} hints + * @property {number} hints.domain + */ + +/** + * @typedef {object} Datum + * @property {string} id + * @property {string} timestamp + * @property {string} acquisitionStatus + * @property {*} value + */ + +/** + * @typedef {object} MdbEntry + * @property {string} value + * @property {string} label + * @property {string} description + */ diff --git a/src/providers/object-provider.js b/src/providers/object-provider.js index de499d49..6dca9e9b 100644 --- a/src/providers/object-provider.js +++ b/src/providers/object-provider.js @@ -28,18 +28,18 @@ import { } from '../utils.js'; import { OBJECT_TYPES, NAMESPACE } from '../const.js'; -import OperatorStatusParameter from './user/operator-status-parameter.js'; import { createCommandsObject } from './commands.js'; import { createEventsObject } from './events.js'; +import { getPossibleStatusesFromParameter, getRoleFromParameter, isOperatorStatusParameter } from './user/operator-status-parameter.js'; +import { getMissionActionFromParameter, getPossibleMissionActionStatusesFromParameter, isMissionStatusParameter } from './mission-status/mission-status-parameter.js'; const YAMCS_API_MAP = { 'space-systems': 'spaceSystems', 'parameters': 'parameters' }; -const operatorStatusParameter = new OperatorStatusParameter(); export default class YamcsObjectProvider { - constructor(openmct, url, instance, folderName, roleStatusTelemetry, pollQuestionParameter, pollQuestionTelemetry, realtimeTelemetryProvider, processor = 'realtime') { + constructor(openmct, url, instance, folderName, roleStatusTelemetry, missionStatusTelemetry, pollQuestionParameter, pollQuestionTelemetry, realtimeTelemetryProvider, processor = 'realtime', getDictionaryRequestOptions = () => Promise.resolve({})) { this.openmct = openmct; this.url = url; this.instance = instance; @@ -52,7 +52,9 @@ export default class YamcsObjectProvider { this.dictionary = {}; this.limitOverrides = {}; this.dictionaryPromise = null; + this.getDictionaryRequestOptions = getDictionaryRequestOptions; this.roleStatusTelemetry = roleStatusTelemetry; + this.missionStatusTelemetry = missionStatusTelemetry; this.pollQuestionParameter = pollQuestionParameter; this.pollQuestionTelemetry = pollQuestionTelemetry; @@ -180,9 +182,10 @@ export default class YamcsObjectProvider { #getTelemetryDictionary() { if (!this.dictionaryPromise) { - this.dictionaryPromise = this.#loadTelemetryDictionary(this.url, this.instance, this.folderName) + this.dictionaryPromise = this.#loadTelemetryDictionary() .finally(() => { this.roleStatusTelemetry.dictionaryLoadComplete(); + this.missionStatusTelemetry.dictionaryLoadComplete(); }); } @@ -193,8 +196,11 @@ export default class YamcsObjectProvider { const operation = 'parameters?details=yes&limit=1000'; const parameterUrl = this.url + 'api/mdb/' + this.instance + '/' + operation; const url = this.#getMdbUrl('space-systems'); - const spaceSystems = await accumulateResults(url, {}, 'spaceSystems', []); - const parameters = await accumulateResults(parameterUrl, {}, 'parameters', []); + + const requestOptions = await this.getDictionaryRequestOptions(); + + const spaceSystems = await accumulateResults(url, requestOptions, 'spaceSystems', []); + const parameters = await accumulateResults(parameterUrl, requestOptions, 'parameters', []); /* Sort the space systems by name, so that the children of the root object are in sorted order. */ @@ -324,7 +330,9 @@ export default class YamcsObjectProvider { if (defaultAlarm?.staticAlarmRange) { return getLimitFromAlarmRange(defaultAlarm.staticAlarmRange); } else { - throw new Error(`Passed alarm has invalid object syntax for limit conversion`, defaultAlarm); + console.warn('Open MCT supports default static alarms only at this time', defaultAlarm); + + return {}; } } @@ -395,18 +403,31 @@ export default class YamcsObjectProvider { telemetryValue.unit = unitSuffix; } - if (operatorStatusParameter.isOperatorStatusParameter(parameter)) { - const role = operatorStatusParameter.getRoleFromParameter(parameter); + if (isOperatorStatusParameter(parameter)) { + const role = getRoleFromParameter(parameter); if (!role) { throw new Error(`Operator Status Parameter "${parameter.qualifiedName}" does not specify a role`); } - const possibleStatuses = operatorStatusParameter.getPossibleStatusesFromParameter(parameter); + const possibleStatuses = getPossibleStatusesFromParameter(parameter); possibleStatuses.forEach(state => this.roleStatusTelemetry.addStatus(state)); this.roleStatusTelemetry.addStatusRole(role); this.roleStatusTelemetry.setTelemetryObjectForRole(role, obj); } + if (isMissionStatusParameter(parameter)) { + const action = getMissionActionFromParameter(parameter); + if (!action) { + throw new Error(`Mission Status Parameter "${parameter.qualifiedName}" does not specify a mission action`); + } + + const possibleStatuses = getPossibleMissionActionStatusesFromParameter(parameter); + possibleStatuses.forEach(status => this.missionStatusTelemetry.addStatus(status)); + this.missionStatusTelemetry.addMissionStatusParameterName(parameter.qualifiedName); + this.missionStatusTelemetry.addMissionAction(action); + this.missionStatusTelemetry.setTelemetryObjectForAction(action, obj); + } + if (this.pollQuestionParameter.isPollQuestionParameter(parameter)) { this.pollQuestionParameter.setPollQuestionParameter(parameter); this.pollQuestionTelemetry.setTelemetryObject(obj); @@ -431,7 +452,7 @@ export default class YamcsObjectProvider { }); } - if (this.#isArray(parameter)) { + if (this.#isArray(parameter) || this.#isBinary(parameter)) { telemetryValue.format = parameter.type.engType; } @@ -484,6 +505,10 @@ export default class YamcsObjectProvider { return parameter?.type?.engType === 'enumeration'; } + #isBinary(parameter) { + return parameter?.type?.engType === 'binary'; + } + #isArray(parameter) { return parameter?.type?.engType.endsWith('[]'); } diff --git a/src/providers/realtime-provider.js b/src/providers/realtime-provider.js index 2a00c09b..7889b577 100644 --- a/src/providers/realtime-provider.js +++ b/src/providers/realtime-provider.js @@ -24,25 +24,26 @@ import { SUBSCRIBE, UNSUBSCRIBE } from './messages.js'; import { OBJECT_TYPES, DATA_TYPES, - AGGREGATE_TYPE, METADATA_TIME_KEY, STALENESS_STATUS_MAP, - MDB_OBJECT + MDB_OBJECT, + MDB_CHANGES_PARAMETER_TYPE } from '../const.js'; import { buildStalenessResponseObject, idToQualifiedName, - qualifiedNameToId, - getValue, addLimitInformation, - getLimitFromAlarmRange + getLimitFromAlarmRange, + convertYamcsToOpenMctDatum } from '../utils.js'; import { commandToTelemetryDatum } from './commands.js'; import { eventToTelemetryDatum, eventShouldBeFiltered } from './events.js'; -const FALLBACK_AND_WAIT_MS = [1000, 5000, 5000, 10000, 10000, 30000]; export default class RealtimeProvider { - constructor(url, instance, processor = 'realtime') { + #socketWorker = null; + #openmct; + + constructor(openmct, url, instance, processor = 'realtime', maxBatchWait = 1000, maxBatchSize = 15) { this.url = url; this.instance = instance; this.processor = processor; @@ -57,9 +58,56 @@ export default class RealtimeProvider { this.lastSubscriptionId = 1; this.subscriptionsByCall = new Map(); this.subscriptionsById = {}; + this.#socketWorker = new openmct.telemetry.BatchingWebSocket(openmct); + this.#openmct = openmct; + this.#setBatchingStrategy(maxBatchWait, maxBatchSize); this.addSupportedObjectTypes(Object.values(OBJECT_TYPES)); this.addSupportedDataTypes(Object.values(DATA_TYPES)); + const setCallFromClockIfNecessary = this.#setCallFromClockIfNecessary.bind(this); + + openmct.time.on('clock', setCallFromClockIfNecessary); + + openmct.once('destroy', () => { + openmct.time.off('clock', setCallFromClockIfNecessary); + }); + } + + #setCallFromClockIfNecessary(clock) { + if (clock === undefined) { + this.unsetCall(); + } + + if (clock.key === 'remote-clock') { + this.#setCallFromClock(clock); + } + } + #setBatchingStrategy(maxBatchWait, maxBatchSize) { + // This strategy batches parameter value messages + this.#socketWorker.setBatchingStrategy({ + /* istanbul ignore next */ + shouldBatchMessage: /* istanbul ignore next */ (message) => { + // If a parameter value message, the message type will be "parameters" + // The type field is always located at a character offset of 13 and + // if it is "parameters" will be 10 characters long. + const type = message.substring(13, 23); + + return type === 'parameters'; + }, + /* istanbul ignore next */ + getBatchIdFromMessage: /* istanbul ignore next */ (message) => { + // Only dealing with "parameters" messages at this point. The call number + // identifies the parameter, and is used for batching. Will be located + // at a character offset of 36. Because it is of indeterminate length + // (we don't know the number) we have to do a sequential search forward + // from the 37th character for a terminating ",". + const callNumber = message.substring(36, message.indexOf(",", 37)); + + return callNumber; + } + }); + this.#socketWorker.setMaxBatchWait(maxBatchWait); + this.#socketWorker.setMaxBatchSize(maxBatchSize); } addSupportedObjectTypes(types) { @@ -69,7 +117,6 @@ export default class RealtimeProvider { addSupportedDataTypes(dataTypes) { dataTypes.forEach(dataType => this.supportedDataTypes[dataType] = dataType); } - supportsSubscribe(domainObject) { return this.isSupportedObjectType(domainObject.type); } @@ -111,7 +158,7 @@ export default class RealtimeProvider { if (subscriptionDetails) { this.sendUnsubscribeMessage(subscriptionDetails); - this.subscriptionsByCall.delete(subscriptionDetails.call); + this.subscriptionsByCall.delete(subscriptionDetails.call.toString()); delete this.subscriptionsById[id]; } }; @@ -136,7 +183,7 @@ export default class RealtimeProvider { this.sendUnsubscribeMessage(subscriptionDetails); if (this.subscriptionsById[id]) { - this.subscriptionsByCall.delete(this.subscriptionsById[id].call); + this.subscriptionsByCall.delete(this.subscriptionsById[id].call.toString()); delete this.subscriptionsById[id]; } }; @@ -162,44 +209,64 @@ export default class RealtimeProvider { const domainObject = subscriptionDetails.domainObject; const message = SUBSCRIBE[domainObject.type](subscriptionDetails); - this.sendOrQueueMessage(message); + this.sendMessage(message); } sendUnsubscribeMessage(subscriptionDetails) { let message = UNSUBSCRIBE(subscriptionDetails); - this.sendOrQueueMessage(message); + this.sendMessage(message); } - reconnect() { - this.subscriptionsByCall.clear(); + #setCallFromClock(clock) { + const correspondingSubscription = Object.values(this.subscriptionsById).find(subscription => { + return subscription.domainObject.identifier.key === clock.identifier.key; + }); - if (this.reconnectTimeout) { - return; + if (correspondingSubscription !== undefined) { + this.remoteClockCallNumber = correspondingSubscription.call.toString(); + } else { + delete this.remoteClockCallNumber; } + } - this.reconnectTimeout = setTimeout(() => { - this.connect(); - delete this.reconnectTimeout; - }, FALLBACK_AND_WAIT_MS[this.currentWaitIndex]); + #processBatchQueue(batchQueue, call) { + let subscriptionDetails = this.subscriptionsByCall.get(call); + let telemetryData = []; - if (this.currentWaitIndex < FALLBACK_AND_WAIT_MS.length - 1) { - this.currentWaitIndex++; + // possibly cancelled + if (!subscriptionDetails) { + return; } - } - sendOrQueueMessage(request) { - if (this.connected) { - try { - this.sendMessage(request); - } catch (error) { - this.connected = false; - this.requests.push(request); - console.error("🚨 Error while attempting to send to websocket, closing websocket", error); - this.socket.close(); - } - } else { - this.requests.push(request); + batchQueue.forEach((rawMessage) => { + const message = JSON.parse(rawMessage); + const values = message.data.values || []; + const parentName = subscriptionDetails.domainObject.name; + + values.forEach(parameter => { + const datum = convertYamcsToOpenMctDatum(parameter, parentName); + + if (this.observingStaleness[subscriptionDetails.name] !== undefined) { + const status = STALENESS_STATUS_MAP[parameter.acquisitionStatus]; + + if (this.observingStaleness[subscriptionDetails.name].response.isStale !== status) { + const stalenesResponseObject = buildStalenessResponseObject( + status, + parameter[METADATA_TIME_KEY] + ); + this.observingStaleness[subscriptionDetails.name].response = stalenesResponseObject; + this.observingStaleness[subscriptionDetails.name].callback(stalenesResponseObject); + } + } + + addLimitInformation(parameter, datum); + telemetryData.push(datum); + }); + }); + + if (telemetryData.length > 0) { + subscriptionDetails.callback(telemetryData); } } @@ -211,80 +278,64 @@ export default class RealtimeProvider { let wsUrl = `${this.url}`; this.lastSubscriptionId = 1; this.connected = false; - this.socket = new WebSocket(wsUrl); - this.socket.onopen = () => { - clearTimeout(this.reconnectTimeout); + this.#socketWorker.connect(wsUrl); + this.#socketWorker.addEventListener('reconnected', () => { + this.resubscribeToAll(); + }); + + this.#socketWorker.addEventListener('batch', (batchEvent) => { + const batch = batchEvent.detail; - this.connected = true; - console.debug(`🔌 Established websocket connection to ${wsUrl}`); + let remoteClockValue; + // If remote clock active, process its value before any telemetry values to ensure the bounds are always up to date. + if (this.remoteClockCallNumber !== undefined) { + remoteClockValue = batch[this.remoteClockCallNumber]; + if (remoteClockValue !== undefined) { + this.#processBatchQueue(batch[this.remoteClockCallNumber], this.remoteClockCallNumber); - this.currentWaitIndex = 0; - this.resubscribeToAll(); - this.flushQueue(); - }; + // Delete so we don't process it twice. + delete batch[this.remoteClockCallNumber]; + } + } - this.socket.onmessage = (event) => { - const message = JSON.parse(event.data); + Object.keys(batch).forEach((call) => { + this.#processBatchQueue(batch[call], call); + }); + }); + this.#socketWorker.addEventListener('message', (messageEvent) => { + const message = JSON.parse(messageEvent.detail); if (!this.isSupportedDataType(message.type)) { return; } const isReply = message.type === DATA_TYPES.DATA_TYPE_REPLY; + const call = message.call; let subscriptionDetails; if (isReply) { const id = message.data.replyTo; - const call = message.call; subscriptionDetails = this.subscriptionsById[id]; subscriptionDetails.call = call; - this.subscriptionsByCall.set(call, subscriptionDetails); + // Subsequent retrieval uses a string, so for performance reasons we use a string as a key. + this.subscriptionsByCall.set(call.toString(), subscriptionDetails); + + const remoteClockIdentifier = this.#openmct.time.getClock()?.identifier; + const isRemoteClockActive = remoteClockIdentifier !== undefined; + + if (isRemoteClockActive && subscriptionDetails.domainObject.identifier.key === remoteClockIdentifier.key) { + this.remoteClockCallNumber = call.toString(); + } } else { - subscriptionDetails = this.subscriptionsByCall.get(message.call); + subscriptionDetails = this.subscriptionsByCall.get(message.call.toString()); // possibly cancelled if (!subscriptionDetails) { return; } - if (this.isTelemetryMessage(message)) { - let values = message.data.values || []; - let parentName = subscriptionDetails.domainObject.name; - - values.forEach(parameter => { - let datum = { - id: qualifiedNameToId(subscriptionDetails.name), - timestamp: parameter[METADATA_TIME_KEY] - }; - let value = getValue(parameter, parentName); - - if (this.observingStaleness[subscriptionDetails.name] !== undefined) { - const status = STALENESS_STATUS_MAP[parameter.acquisitionStatus]; - - if (this.observingStaleness[subscriptionDetails.name].response.isStale !== status) { - const stalenesResponseObject = buildStalenessResponseObject( - status, - parameter[METADATA_TIME_KEY] - ); - this.observingStaleness[subscriptionDetails.name].response = stalenesResponseObject; - this.observingStaleness[subscriptionDetails.name].callback(stalenesResponseObject); - } - } - - if (parameter.engValue.type !== AGGREGATE_TYPE) { - datum.value = value; - } else { - datum = { - ...datum, - ...value - }; - } - - addLimitInformation(parameter, datum); - subscriptionDetails.callback(datum); - }); - } else if (this.isCommandMessage(message)) { + if (this.isCommandMessage(message)) { const datum = commandToTelemetryDatum(message.data); subscriptionDetails.callback(datum); } else if (this.isEventMessage(message)) { @@ -295,6 +346,10 @@ export default class RealtimeProvider { subscriptionDetails.callback(datum); } } else if (this.isMdbChangesMessage(message)) { + if (!this.isParameterType(message)) { + return; + } + const parameterName = message.data.parameterOverride.parameter; if (this.observingLimitChanges[parameterName] !== undefined) { const alarmRange = message.data.parameterOverride.defaultAlarm?.staticAlarmRange ?? []; @@ -308,20 +363,7 @@ export default class RealtimeProvider { subscriptionDetails.callback(message.data); } } - }; - - this.socket.onerror = (error) => { - console.error(`🚨 Websocket error, closing websocket`, error); - this.socket.close(); - }; - - this.socket.onclose = () => { - console.warn('🚪 Websocket closed. Attempting to reconnect...'); - this.connected = false; - this.socket = null; - - this.reconnect(); - }; + }); } resubscribeToAll() { @@ -330,30 +372,8 @@ export default class RealtimeProvider { }); } - flushQueue() { - let shouldCloseWebsocket = false; - this.requests = this.requests.filter((request) => { - try { - this.sendMessage(request); - } catch (error) { - this.connected = false; - console.error('🚨 Error while attempting to send to websocket, closing websocket', error); - - shouldCloseWebsocket = true; - - return true; - } - - return false; - }); - - if (shouldCloseWebsocket) { - this.socket.close(); - } - } - sendMessage(message) { - this.socket.send(message); + this.#socketWorker.sendMessage(message); } isTelemetryMessage(message) { @@ -371,4 +391,8 @@ export default class RealtimeProvider { isMdbChangesMessage(message) { return message.type === DATA_TYPES.DATA_TYPE_MDB_CHANGES; } + + isParameterType(message) { + return message.data?.type === MDB_CHANGES_PARAMETER_TYPE; + } } diff --git a/src/providers/staleness-provider.js b/src/providers/staleness-provider.js index 29dad810..2557cf91 100644 --- a/src/providers/staleness-provider.js +++ b/src/providers/staleness-provider.js @@ -24,8 +24,7 @@ import { OBJECT_TYPES, STALENESS_STATUS_MAP } from '../const.js'; import { buildStalenessResponseObject } from '../utils.js'; export default class YamcsStalenessProvider { - constructor(openmct, realtimeTelemetryProvider, latestTelemetryProvider) { - this.openmct = openmct; + constructor(realtimeTelemetryProvider, latestTelemetryProvider) { this.realtimeTelemetryProvider = realtimeTelemetryProvider; this.latestTelemetryProvider = latestTelemetryProvider; } diff --git a/src/providers/user/operator-status-parameter.js b/src/providers/user/operator-status-parameter.js index 34d7eca4..4bab4385 100644 --- a/src/providers/user/operator-status-parameter.js +++ b/src/providers/user/operator-status-parameter.js @@ -22,21 +22,19 @@ const OPERATOR_STATUS_TYPE = 'yamcs.operatorStatus'; -export default class OperatorStatusParameter { - isOperatorStatusParameter(parameter) { - const aliases = parameter.alias; +export function isOperatorStatusParameter(parameter) { + const aliases = parameter.alias; - return aliases !== undefined - && aliases.some(alias => alias.name === OPERATOR_STATUS_TYPE); - } + return aliases !== undefined + && aliases.some(alias => alias.name === OPERATOR_STATUS_TYPE); +} - getRoleFromParameter(parameter) { - const aliases = parameter.alias; +export function getRoleFromParameter(parameter) { + const aliases = parameter.alias; - return aliases.find(alias => alias.namespace === 'OpenMCT:role')?.name; - } + return aliases.find(alias => alias.namespace === 'OpenMCT:role')?.name; +} - getPossibleStatusesFromParameter(parameter) { - return parameter.type.enumValue; - } +export function getPossibleStatusesFromParameter(parameter) { + return parameter.type.enumValue; } diff --git a/src/providers/user/user-provider.js b/src/providers/user/user-provider.js index ab93ec9d..d14db8f0 100644 --- a/src/providers/user/user-provider.js +++ b/src/providers/user/user-provider.js @@ -24,7 +24,7 @@ import createYamcsUser from './createYamcsUser.js'; import { EventEmitter } from 'eventemitter3'; export default class UserProvider extends EventEmitter { - constructor(openmct, {userEndpoint, roleStatus, latestTelemetryProvider, realtimeTelemetryProvider, pollQuestionParameter, pollQuestionTelemetry}) { + constructor(openmct, {userEndpoint, roleStatus, latestTelemetryProvider, pollQuestionParameter, pollQuestionTelemetry, missionStatus}) { super(); this.openmct = openmct; @@ -32,12 +32,13 @@ export default class UserProvider extends EventEmitter { this.user = undefined; this.loggedIn = false; this.roleStatus = roleStatus; + this.missionStatus = missionStatus; this.pollQuestionParameter = pollQuestionParameter; this.pollQuestionTelemetry = pollQuestionTelemetry; this.unsubscribeStatus = {}; + this.unsubscribeMissionStatus = {}; this.latestTelemetryProvider = latestTelemetryProvider; - this.realtimeTelemetryProvider = realtimeTelemetryProvider; this.YamcsUser = createYamcsUser(openmct.user.User); this.openmct.once('destroy', () => { @@ -92,6 +93,29 @@ export default class UserProvider extends EventEmitter { }); } + async canSetMissionStatus() { + const user = await this.getCurrentUser(); + const writeParameters = user.getWriteParameters(); + + const areParametersStatus = await Promise.all( + writeParameters.map(parameterName => this.missionStatus.isMissionStatusParameterName(parameterName)) + ); + + return areParametersStatus.some(isParameterStatus => isParameterStatus); + } + + async getPossibleMissionActions() { + const possibleActions = await this.missionStatus.getAllMissionActions(); + + return possibleActions; + } + + async getPossibleMissionActionStatuses() { + const statuses = await this.missionStatus.getPossibleMissionStatuses(); + + return statuses; + } + async canSetPollQuestion() { const user = await this.getCurrentUser(); const writeParameters = user.getWriteParameters(); @@ -109,10 +133,31 @@ export default class UserProvider extends EventEmitter { return success; } + async getStatusForMissionAction(action) { + const missionStatusTelemetryObject = await this.missionStatus.getTelemetryObjectForAction(action); + if (this.unsubscribeMissionStatus[action] === undefined) { + this.unsubscribeMissionStatus[action] = this.openmct.telemetry.subscribe(missionStatusTelemetryObject, (datum) => { + this.emit('missionActionStatusChange', { + action, + status: this.missionStatus.toStatusFromTelemetry(missionStatusTelemetryObject, datum) + }); + }); + } + + const status = await this.latestTelemetryProvider.requestLatest(missionStatusTelemetryObject); + if (status !== undefined) { + return this.missionStatus.toStatusFromTelemetry(missionStatusTelemetryObject, status); + } else { + const defaultStatus = await this.missionStatus.getDefaultStatusForAction(action); + + return defaultStatus; + } + } + async getStatusForRole(role) { const statusTelemetryObject = await this.roleStatus.getTelemetryObjectForRole(role); if (this.unsubscribeStatus[role] === undefined) { - this.unsubscribeStatus[role] = this.realtimeTelemetryProvider.subscribe(statusTelemetryObject, (datum) => { + this.unsubscribeStatus[role] = this.openmct.telemetry.subscribe(statusTelemetryObject, (datum) => { this.emit('statusChange', { role, status: this.roleStatus.toStatusFromTelemetry(statusTelemetryObject, datum) @@ -142,6 +187,12 @@ export default class UserProvider extends EventEmitter { return success; } + async setStatusForMissionAction(action, status) { + const success = await this.missionStatus.setStatusForMissionAction(action, status); + + return success; + } + async getPossibleStatuses() { const possibleStatuses = await this.roleStatus.getPossibleStatuses(); @@ -152,7 +203,7 @@ export default class UserProvider extends EventEmitter { const pollQuestionTelemetryObject = await this.pollQuestionTelemetry.getTelemetryObject(); if (this.unsubscribePollQuestion === undefined) { - this.unsubscribePollQuestion = this.realtimeTelemetryProvider.subscribe(pollQuestionTelemetryObject, (datum) => { + this.unsubscribePollQuestion = this.openmct.telemetry.subscribe(pollQuestionTelemetryObject, (datum) => { const formattedPollQuestion = this.pollQuestionTelemetry.toPollQuestionObjectFromTelemetry(pollQuestionTelemetryObject, datum); this.emit("pollQuestionChange", formattedPollQuestion); }); @@ -200,4 +251,3 @@ export default class UserProvider extends EventEmitter { } } - diff --git a/src/utils.js b/src/utils.js index 7f268d37..04ccabc1 100644 --- a/src/utils.js +++ b/src/utils.js @@ -19,7 +19,7 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ -import { AGGREGATE_TYPE, UNSUPPORTED_TYPE } from './const.js'; +import {AGGREGATE_TYPE, UNSUPPORTED_TYPE, METADATA_TIME_KEY, MDB_CHANGES_PARAMETER_TYPE} from './const.js'; import limitConfig from "./limits-config.json"; function idToQualifiedName(id) { @@ -211,12 +211,14 @@ async function getLimitOverrides(url) { const overrides = await requestLimitOverrides(url); overrides.forEach((override) => { - const parameterOverride = override.parameterOverride; - const parameter = parameterOverride.parameter; - const alarmRange = parameterOverride?.defaultAlarm?.staticAlarmRange ?? []; - - limitOverrides[parameter] = getLimitFromAlarmRange(alarmRange); + if (override.type === MDB_CHANGES_PARAMETER_TYPE) { + const parameter = override?.parameterOverride?.parameter; + const alarmRange = override?.parameterOverride?.defaultAlarm?.staticAlarmRange ?? []; + if (parameter && alarmRange) { + limitOverrides[parameter] = getLimitFromAlarmRange(alarmRange); + } + } }); return limitOverrides; @@ -362,6 +364,24 @@ function flattenObjectArray(array, baseObj = {}) { }, baseObj); } +function convertYamcsToOpenMctDatum(parameter, parentName) { + let datum = { + timestamp: parameter[METADATA_TIME_KEY] + }; + const value = getValue(parameter, parentName); + + if (parameter.engValue.type !== AGGREGATE_TYPE) { + datum.value = value; + } else { + datum = { + ...datum, + ...value + }; + } + + return datum; +} + export { buildStalenessResponseObject, getLimitFromAlarmRange, @@ -373,5 +393,6 @@ export { accumulateResults, addLimitInformation, yieldResults, - getLimitOverrides + getLimitOverrides, + convertYamcsToOpenMctDatum }; diff --git a/tests/README.md b/tests/README.md index d4daf3c0..2d1c1041 100644 --- a/tests/README.md +++ b/tests/README.md @@ -3,6 +3,7 @@ This project is using Open MCT's e2e-as-a-dependency model. To learn more, please see the official documentation on the [Official README](https://github.com/nasa/openmct/blob/master/e2e/README.md) ## How to Run Locally + To run the tests, we recommend the following workflow which bridges two separate github repos: yamcs/quickstart and openmct-yamcs (this one). @@ -11,8 +12,7 @@ yamcs/quickstart and openmct-yamcs (this one). 3. `make all` in yamcs/quickstart 4. `cd openmct-yamcs` to move out of yamcs/quickstart 5. `npm install` in openmct-yamcs -6. `npx playwright@1.39.0 install chromium` in openmct-yamcs -7. Sanity test that yamcs is up with `npm run wait-for-yamcs` in openmct-yamcs -8. `npm run build:example` -9. `npm run test:getopensource` -10. `npm run test:e2e:quickstart:local` +6. Sanity test that yamcs is up with `npm run wait-for-yamcs` in openmct-yamcs +7. `npm run test:getopensource` +8. `npm run build:example` or `npm run build:example:master` +9. `npm run test:e2e:watch` diff --git a/tests/e2e/playwright-quickstart.config.js b/tests/e2e/playwright-quickstart.config.js index b21e82c3..525f359f 100644 --- a/tests/e2e/playwright-quickstart.config.js +++ b/tests/e2e/playwright-quickstart.config.js @@ -5,7 +5,7 @@ const config = { retries: 1, testDir: '.', - testMatch: '**/*.e2e.spec.js', + testMatch: /.*\.e2e\.spec\.(mjs|js)$/, timeout: 30 * 1000, use: { headless: false, @@ -18,6 +18,7 @@ const config = { failOnConsoleError: false }, webServer: { + cwd: '../', command: 'npm run start:coverage', url: 'http://localhost:9000/#', timeout: 120 * 1000, diff --git a/tests/e2e/yamcs/faultManagement.e2e.spec.mjs b/tests/e2e/yamcs/faultManagement.e2e.spec.mjs new file mode 100644 index 00000000..d3713788 --- /dev/null +++ b/tests/e2e/yamcs/faultManagement.e2e.spec.mjs @@ -0,0 +1,43 @@ +/***************************************************************************** + * 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. + *****************************************************************************/ + +/* +Staleness Specific Tests +*/ + +import { pluginFixtures } from 'openmct-e2e'; +const { test } = pluginFixtures; + +test.describe.fixme("Fault management tests @yamcs", () => { + // eslint-disable-next-line require-await + test('Show faults ', async ({ page }) => { + test.step('for historic alarm violations', () => { + // Navigate to fault management in the tree + // Expect that there is indication of a fault + }); + + test.step('show historic and live faults when new alarms are triggered in real time', () => { + // Wait for new data + // Expect that live faults are displayed + }); + }); +}); diff --git a/tests/e2e/yamcs/filters.e2e.spec.js b/tests/e2e/yamcs/filters.e2e.spec.mjs similarity index 96% rename from tests/e2e/yamcs/filters.e2e.spec.js rename to tests/e2e/yamcs/filters.e2e.spec.mjs index 66534072..09dc18a0 100644 --- a/tests/e2e/yamcs/filters.e2e.spec.js +++ b/tests/e2e/yamcs/filters.e2e.spec.mjs @@ -24,8 +24,9 @@ Filter Specific Tests */ -import { test, expect } from '../opensource/pluginFixtures.js'; -import { createDomainObjectWithDefaults } from '../opensource/appActions.js'; +import { pluginFixtures, appActions } from 'openmct-e2e'; +const { test, expect } = pluginFixtures; +const { createDomainObjectWithDefaults } = appActions; test.describe("Filter tests @yamcs", () => { test('Can filter events by severity', async ({ page }) => { diff --git a/tests/e2e/yamcs/historicalData.e2e.spec.js b/tests/e2e/yamcs/historicalData.e2e.spec.mjs similarity index 61% rename from tests/e2e/yamcs/historicalData.e2e.spec.js rename to tests/e2e/yamcs/historicalData.e2e.spec.mjs index a17642eb..f3c41bcc 100644 --- a/tests/e2e/yamcs/historicalData.e2e.spec.js +++ b/tests/e2e/yamcs/historicalData.e2e.spec.mjs @@ -24,36 +24,36 @@ Network Specific Tests */ -import { test, expect } from '../opensource/pluginFixtures.js'; -import { setFixedTimeMode } from '../opensource/appActions.js'; +import { pluginFixtures, appActions } from 'openmct-e2e'; +const { test, expect } = pluginFixtures; +const { setFixedTimeMode } = appActions; test.describe("Samples endpoint with useRawValue search param @yamcs", () => { // Collect all request events, specifically for YAMCS - let networkRequests = []; let filteredRequests = []; - - test('When in plot view, samples endpoint is used for enum type parameters with the useRawValue parameter', async ({ page }) => { + let networkRequests = []; + test.beforeEach(async ({ page }) => { page.on('request', (request) => networkRequests.push(request)); // Go to baseURL - await page.goto("./", { waitUntil: "networkidle" }); - + await page.goto("./", { waitUntil: "domcontentloaded" }); + await expect(page.getByText('Loading...')).toBeHidden(); // Change to fixed time await setFixedTimeMode(page); - const myProjectTreeItem = page.locator('.c-tree__item').filter({ hasText: 'myproject'}); - await expect(myProjectTreeItem).toBeVisible(); - const firstMyProjectTriangle = myProjectTreeItem.first().locator('span.c-disclosure-triangle'); - await firstMyProjectTriangle.click(); - const secondMyProjectTriangle = myProjectTreeItem.nth(1).locator('span.c-disclosure-triangle'); - await secondMyProjectTriangle.click(); - - await page.waitForLoadState('networkidle'); + // Expand myproject and subfolder myproject + await page.getByLabel('Expand myproject').click(); + await page.getByLabel('Expand myproject').click(); + // await expect(page.getByText('Loading...')).toBeHidden(); networkRequests = []; - await page.locator('text=Enum_Para_1').first().click(); - await page.waitForLoadState('networkidle'); + filteredRequests = []; + }); + + test('When in plot view, samples endpoint is used for enum type parameters with the useRawValue parameter', async ({ page }) => { + await page.getByLabel('Navigate to Enum_Para_1 yamcs').click(); // wait for debounced requests in YAMCS Latest Telemetry Provider to finish - await new Promise(resolve => setTimeout(resolve, 500)); + // FIXME: can we use waitForRequest? + await page.waitForTimeout(500); filteredRequests = filterNonFetchRequests(networkRequests); @@ -67,29 +67,11 @@ test.describe("Samples endpoint with useRawValue search param @yamcs", () => { }); test('When in plot view, samples endpoint is used for scalar (number) type parameters with no useRawValue parameter', async ({ page }) => { - networkRequests = []; - filteredRequests = []; - page.on('request', (request) => networkRequests.push(request)); - // Go to baseURL - await page.goto("./", { waitUntil: "networkidle" }); - - // Change to fixed time - await setFixedTimeMode(page); - - const myProjectTreeItem = page.locator('.c-tree__item').filter({ hasText: 'myproject'}); - await expect(myProjectTreeItem).toBeVisible(); - const firstMyProjectTriangle = myProjectTreeItem.first().locator('span.c-disclosure-triangle'); - await firstMyProjectTriangle.click(); - const secondMyProjectTriangle = myProjectTreeItem.nth(1).locator('span.c-disclosure-triangle'); - await secondMyProjectTriangle.click(); - - await page.waitForLoadState('networkidle'); - networkRequests = []; - await page.locator('text=CCSDS_Packet_Length').first().click(); - await page.waitForLoadState('networkidle'); + await page.getByLabel('Navigate to CCSDS_Packet_Length yamcs').click(); // wait for debounced requests in YAMCS Latest Telemetry Provider to finish - await new Promise(resolve => setTimeout(resolve, 500)); + // FIXME: can we use waitForRequest? + await page.waitForTimeout(500); filteredRequests = filterNonFetchRequests(networkRequests); @@ -103,34 +85,17 @@ test.describe("Samples endpoint with useRawValue search param @yamcs", () => { }); test('When in table view, samples endpoint and useRawValue are not used for scalar (number) type parameters', async ({ page }) => { - networkRequests = []; - filteredRequests = []; - page.on('request', (request) => networkRequests.push(request)); - // Go to baseURL - await page.goto("./", { waitUntil: "networkidle" }); - - // Change to fixed time - await setFixedTimeMode(page); - - const myProjectTreeItem = page.locator('.c-tree__item').filter({ hasText: 'myproject'}); - await expect(myProjectTreeItem).toBeVisible(); - const firstMyProjectTriangle = myProjectTreeItem.first().locator('span.c-disclosure-triangle'); - await firstMyProjectTriangle.click(); - const secondMyProjectTriangle = myProjectTreeItem.nth(1).locator('span.c-disclosure-triangle'); - await secondMyProjectTriangle.click(); - - await page.waitForLoadState('networkidle'); - await page.locator('text=Enum_Para_1').first().click(); - await page.waitForLoadState('networkidle'); + await page.getByLabel('Navigate to Enum_Para_1 yamcs').click(); //switch to table view networkRequests = []; - await page.locator("button[title='Change the current view']").click(); + await page.getByLabel('Open the View Switcher Menu').click(); await page.getByRole('menuitem', { name: /Telemetry Table/ }).click(); await page.waitForLoadState('networkidle'); // wait for debounced requests in YAMCS Latest Telemetry Provider to finish - await new Promise(resolve => setTimeout(resolve, 500)); + // FIXME: can we use waitForRequest? + await page.waitForTimeout(500); filteredRequests = filterNonFetchRequests(networkRequests); @@ -146,35 +111,40 @@ test.describe("Samples endpoint with useRawValue search param @yamcs", () => { expect(nonSampleRequests.length).toBe(filteredRequests.length); }); - test('When in table view, samples endpoint and useRawValue are not used for enum type parameters', async ({ page }) => { + test('When in table view and in unlimited mode, requests contain the "order=desc" parameter', async ({ page }) => { + await page.getByLabel('Navigate to Enum_Para_1 yamcs').click(); + + //switch to table view networkRequests = []; - filteredRequests = []; - page.on('request', (request) => networkRequests.push(request)); - // Go to baseURL - await page.goto("./", { waitUntil: "networkidle" }); + await page.getByLabel('Open the View Switcher Menu').click(); + await page.getByRole('menuitem', { name: /Telemetry Table/ }).click(); + await page.waitForLoadState('networkidle'); - // Change to fixed time - await setFixedTimeMode(page); + // wait for debounced requests in YAMCS Latest Telemetry Provider to finish + // FIXME: can we use waitForRequest? + await page.waitForTimeout(500); - const myProjectTreeItem = page.locator('.c-tree__item').filter({ hasText: 'myproject'}); - await expect(myProjectTreeItem).toBeVisible(); - const firstMyProjectTriangle = myProjectTreeItem.first().locator('span.c-disclosure-triangle'); - await firstMyProjectTriangle.click(); - const secondMyProjectTriangle = myProjectTreeItem.nth(1).locator('span.c-disclosure-triangle'); - await secondMyProjectTriangle.click(); + filteredRequests = filterNonFetchRequests(networkRequests); + // Verify we are in "Limited" mode + await expect(page.getByRole('button', { name: 'SHOW UNLIMITED' })).toBeVisible(); - await page.waitForLoadState('networkidle'); - await page.locator('text=Enum_Para_1').first().click(); - await page.waitForLoadState('networkidle'); + // Check if any request URL contains the 'order=desc' parameter + const hasOrderDesc = filteredRequests.some(request => request.url().includes('order=desc')); + expect(hasOrderDesc).toBe(true); + }); + + test('When in table view, samples endpoint and useRawValue are not used for enum type parameters', async ({ page }) => { + await page.getByLabel('Navigate to Enum_Para_1 yamcs').click(); //switch to table view networkRequests = []; - await page.locator("button[title='Change the current view']").click(); + await page.getByLabel('Open the View Switcher Menu').click(); await page.getByRole('menuitem', { name: /Telemetry Table/ }).click(); await page.waitForLoadState('networkidle'); // wait for debounced requests in YAMCS Latest Telemetry Provider to finish - await new Promise(resolve => setTimeout(resolve, 500)); + // FIXME: can we use waitForRequest? + await page.waitForTimeout(500); filteredRequests = filterNonFetchRequests(networkRequests); diff --git a/tests/e2e/yamcs/limits.e2e.spec.js b/tests/e2e/yamcs/limits.e2e.spec.mjs similarity index 62% rename from tests/e2e/yamcs/limits.e2e.spec.js rename to tests/e2e/yamcs/limits.e2e.spec.mjs index c7144c41..3d934bcf 100644 --- a/tests/e2e/yamcs/limits.e2e.spec.js +++ b/tests/e2e/yamcs/limits.e2e.spec.mjs @@ -24,10 +24,21 @@ MDB Limits Specific Tests */ -import { test, expect } from '../opensource/pluginFixtures.js'; -import { createDomainObjectWithDefaults, waitForPlotsToRender } from '../opensource/appActions.js'; +import { pluginFixtures, appActions } from 'openmct-e2e'; +const { test, expect } = pluginFixtures; +const { createDomainObjectWithDefaults, waitForPlotsToRender } = appActions; +const YAMCS_URL = 'http://localhost:8090/'; test.describe("Mdb runtime limits tests @yamcs", () => { + + test.beforeEach(async ({ page }) => { + await clearLimitsForParameter(page); + }); + + test.afterEach(async ({ page }) => { + await clearLimitsForParameter(page); + }); + test('Can show mdb limits when changed', async ({ page }) => { // Go to baseURL await page.goto("./", { waitUntil: "networkidle" }); @@ -70,22 +81,15 @@ test.describe("Mdb runtime limits tests @yamcs", () => { // Expand the "Detector_Temp" plot series options and enable limit lines await page.getByRole('tab', { name: 'Config' }).click(); - await page - .getByRole('list', { name: 'Plot Series Properties' }) - .locator('span') - .first() - .click(); - await page - .getByRole('list', { name: 'Plot Series Properties' }) - .locator('[title="Display limit lines"]~div input') - .check(); + await page.getByLabel('Expand Detector_Temp Plot').click(); + await page.getByLabel('Limit lines').check(); // Save (exit edit mode) await page.getByRole('button', { name: 'Save' }).click(); await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); - // Change the limits for the Detector_Temp parameter using the yamcs API) - const runTimeLimitChangeResponse = await page.request.patch('http://localhost:8090/api/mdb-overrides/myproject/realtime/parameters/myproject/Detector_Temp', { + // Change the limits for the Detector_Temp parameter using the yamcs API + const runTimeLimitChangeResponse = await page.request.patch(`${YAMCS_URL}api/mdb-overrides/myproject/realtime/parameters/myproject/Detector_Temp`, { data: { action: 'SET_DEFAULT_ALARMS', defaultAlarm: { @@ -107,12 +111,12 @@ test.describe("Mdb runtime limits tests @yamcs", () => { await assertLimitLinesExistAndAreVisible(page); }); - test('Can show changed mdb limits when you navigate away from the view and back', async ({ page }) => { + test('Can show changed mdb limits when you navigate away from the view and back and no new requests are made on resize', async ({ page }) => { // Go to baseURL await page.goto("./", { waitUntil: "networkidle" }); // Reset the limits for the Detector_Temp parameter using the yamcs API - const runTimeLimitResetResponse = await page.request.patch('http://localhost:8090/api/mdb-overrides/myproject/realtime/parameters/myproject/Detector_Temp', { + const runTimeLimitResetResponse = await page.request.patch(`${YAMCS_URL}api/mdb-overrides/myproject/realtime/parameters/myproject/Detector_Temp`, { data: {} }); await expect(runTimeLimitResetResponse).toBeOK(); @@ -123,27 +127,19 @@ test.describe("Mdb runtime limits tests @yamcs", () => { }); //Expand the myproject folder (/myproject) - const myProjectTreeItem = page.locator('.c-tree__item').filter({ hasText: 'myproject'}); - const firstMyProjectTriangle = myProjectTreeItem.first().locator('span.c-disclosure-triangle'); - await firstMyProjectTriangle.click(); - + await page.getByLabel('Expand myproject folder').click(); //Expand the myproject under the previous folder (/myproject/myproject) - const viperRoverTreeItem = page.locator('.c-tree__item').filter({ hasText: 'myproject'}); - const viperRoverProjectTriangle = viperRoverTreeItem.nth(1).locator('span.c-disclosure-triangle'); - await viperRoverProjectTriangle.click(); + await page.getByLabel('Expand myproject folder').click(); //Find the Detector_Temp parameter (/myproject/myproject/Detector_Temp) const detectorTreeItem = page.getByRole('treeitem', { name: /Detector_Temp/ }); - - // Enter edit mode for the overlay plot await page.getByLabel('Edit Object').click(); //Drag and drop the Detector_Temp telemetry endpoint into this overlay plot - const objectPane = page.locator('.c-object-view'); - await detectorTreeItem.dragTo(objectPane); + await detectorTreeItem.dragTo(page.locator('.c-object-view')); // Save (exit edit mode) - await page.getByRole('button', { name: 'Save' }).click(); + await page.getByLabel('Save').click(); await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); // Assert that no limit lines are shown by default @@ -155,15 +151,8 @@ test.describe("Mdb runtime limits tests @yamcs", () => { // Expand the "Detector_Temp" plot series options and enable limit lines await page.getByRole('tab', { name: 'Config' }).click(); - await page - .getByRole('list', { name: 'Plot Series Properties' }) - .locator('span') - .first() - .click(); - await page - .getByRole('list', { name: 'Plot Series Properties' }) - .locator('[title="Display limit lines"]~div input') - .check(); + await page.getByLabel('Expand Detector_Temp Plot').click(); + await page.getByLabel('Limit lines').check(); // Save (exit edit mode) await page.getByRole('button', { name: 'Save' }).click(); @@ -173,7 +162,7 @@ test.describe("Mdb runtime limits tests @yamcs", () => { await page.goto("./", { waitUntil: "networkidle" }); // Change the limits for the Detector_Temp parameter using the yamcs API - const runTimeLimitChangeResponse = await page.request.patch('http://localhost:8090/api/mdb-overrides/myproject/realtime/parameters/myproject/Detector_Temp', { + const runTimeLimitChangeResponse = await page.request.patch(`${YAMCS_URL}api/mdb-overrides/myproject/realtime/parameters/myproject/Detector_Temp`, { data: { action: 'SET_DEFAULT_ALARMS', defaultAlarm: { @@ -196,6 +185,41 @@ test.describe("Mdb runtime limits tests @yamcs", () => { // Ensure that the changed limits are now displayed without a reload await assertLimitLinesExistAndAreVisible(page); + await page.locator('.plot-legend-item').hover(); + await expect(page.locator('.c-plot-limit')).toHaveCount(2); + await assertExpectedLimitsValues(page.locator('.c-plot-limit'), { + minInclusive: -0.8, + maxInclusive: 0.5 + }); + + // Setting up checks for the absence of specific network responses after networkidle. + const responsesChecks = [ + checkForNoResponseAfterNetworkIdle(page, '**/api/mdb/myproject/space-systems'), + checkForNoResponseAfterNetworkIdle(page, '**/api/mdb/myproject/parameters?details=yes&limit=1000'), + checkForNoResponseAfterNetworkIdle(page, '**/api/user/'), + checkForNoResponseAfterNetworkIdle(page, '**/api/mdb-overrides/myproject/realtime') + ]; + + // Resize the chart container by showing the snapshot pane. + await page.getByLabel('Show Snapshots').click(); + // Wait for all checks to complete + const responsesNotFound = await Promise.all(responsesChecks); + // Ensure no network responses were found + const noResponsesFound = responsesNotFound.every(notFound => notFound); + expect(noResponsesFound).toBe(true); + + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/akhenry/openmct-yamcs/issues/447' + }); + // Ensure that the limits still show and have not changed + await assertLimitLinesExistAndAreVisible(page); + await page.locator('.plot-legend-item').hover(); + await expect(page.locator('.c-plot-limit')).toHaveCount(2); + await assertExpectedLimitsValues(page.locator('.c-plot-limit'), { + minInclusive: -0.8, + maxInclusive: 0.5 + }); }); }); @@ -208,10 +232,44 @@ async function assertLimitLinesExistAndAreVisible(page) { await waitForPlotsToRender(page); // Wait for limit lines to be created await page.waitForSelector('.c-plot-limit-line', { state: 'attached' }); - const limitLineCount = await page.locator('.c-plot-limit-line').count(); // There should be 2 limit lines created by default - expect(await page.locator('.c-plot-limit-line').count()).toBe(2); + await expect(page.locator('.c-plot-limit-line')).toHaveCount(2); + const limitLineCount = await page.locator('.c-plot-limit-line').count(); for (let i = 0; i < limitLineCount; i++) { await expect(page.locator('.c-plot-limit-line').nth(i)).toBeVisible(); } } + +/** + * Asserts that the limit line has the expected min and max values + * @param {import('@playwright/test').Locator} limitLine + * @param {{ minInclusive: number, maxInclusive: number }} expectedResults + */ +async function assertExpectedLimitsValues(limitLine, { minInclusive, maxInclusive }) { + await expect(limitLine.first()).toContainText(`${maxInclusive}`); + await expect(limitLine.nth(1)).toContainText(`${minInclusive}`); +} + +// Function to check for the absence of a network response after networkidle +async function checkForNoResponseAfterNetworkIdle(page, urlPattern) { + let responseReceived = false; + // Listen for the network response before navigating to ensure we catch early requests + page.on('response', response => { + if (response.url().match(urlPattern)) { + responseReceived = true; + } + }); + // Wait for the network to be idle + await page.waitForLoadState('networkidle'); + + // Return the inverse of responseReceived to indicate absence of response + return !responseReceived; +} + +async function clearLimitsForParameter(page) { + // clear the limits for the Detector_Temp parameter using the yamcs API + const runTimeLimitChangeResponse = await page.request.patch(`${YAMCS_URL}api/mdb-overrides/myproject/realtime/parameters/myproject/Detector_Temp`, { + data: {} + }); + await expect(runTimeLimitChangeResponse).toBeOK(); +} diff --git a/tests/e2e/yamcs/load.e2e.spec.mjs b/tests/e2e/yamcs/load.e2e.spec.mjs new file mode 100644 index 00000000..f0caf544 --- /dev/null +++ b/tests/e2e/yamcs/load.e2e.spec.mjs @@ -0,0 +1,74 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2022, 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. + *****************************************************************************/ + +/* +Open MCT load Specific Tests +*/ + +import { pluginFixtures } from 'openmct-e2e'; +const { test, expect } = pluginFixtures; +const YAMCS_URL = 'http://localhost:8090/'; + +test.describe("Tests to ensure that open mct loads correctly @yamcs", () => { + test.beforeEach(async ({ page }) => { + await clearCustomAlgorithm(page); + }); + + test.afterEach(async ({ page }) => { + await clearCustomAlgorithm(page); + }); + + test('Can load correctly when mdb algorithms are changed at runtime', async ({ page }) => { + // Go to baseURL + await page.goto("./", {waitUntil: "networkidle"}); + await expect(page.getByLabel('Navigate to myproject folder')).toBeVisible(); + + await updateCustomAlgorithm(page); + + await page.reload({waitUntil: "networkidle"}); + + await expect(page.getByLabel('Navigate to myproject folder')).toBeVisible(); + }); +}); + +async function clearCustomAlgorithm(page) { + // clear the custom algorithm for the copySunsensor using the yamcs API + const runTimeCustomAlgorithmResetResponse = await page.request.patch(`${YAMCS_URL}api/mdb/myproject/realtime/algorithms/myproject/copySunsensor`, { + data: { + "action": "RESET" + } + }); + await expect(runTimeCustomAlgorithmResetResponse).toBeOK(); +} + +async function updateCustomAlgorithm(page) { + // Change the custom algorithm for the copySunsensor using the yamcs API + const runTimeCustomAlgorithmChangeResponse = await page.request.patch(`${YAMCS_URL}api/mdb/myproject/realtime/algorithms/myproject/copySunsensor`, { + data: { + "action": "SET", + "algorithm": { + "text": "\n\t\t\t\t\tout0.setFloatValue(in.getEngValue().getFloatValue()); \n\t\t\t\t" + } + } + }); + await expect(runTimeCustomAlgorithmChangeResponse).toBeOK(); +} diff --git a/tests/e2e/yamcs/namesToParametersMap.json b/tests/e2e/yamcs/namesToParametersMap.json new file mode 100644 index 00000000..132d18bb --- /dev/null +++ b/tests/e2e/yamcs/namesToParametersMap.json @@ -0,0 +1,48 @@ +{ + "A": "/myproject/A", + "ADCS_Error_Flag": "/myproject/ADCS_Error_Flag", + "Battery1_Temp": "/myproject/Battery1_Temp", + "Battery1_Voltage": "/myproject/Battery1_Voltage", + "Battery2_Temp": "/myproject/Battery2_Temp", + "Battery2_Voltage": "/myproject/Battery2_Voltage", + "CCSDS_Packet_Length": "/myproject/CCSDS_Packet_Length", + "CDHS_Error_Flag": "/myproject/CDHS_Error_Flag", + "CDHS_Status": "/myproject/CDHS_Status", + "COMMS_Error_Flag": "/myproject/COMMS_Error_Flag", + "COMMS_Status": "/myproject/COMMS_Status", + "Contact_Golbasi_GS": "/myproject/Contact_Golbasi_GS", + "Contact_Svalbard": "/myproject/Contact_Svalbard", + "Detector_Temp": "/myproject/Detector_Temp", + "ElapsedSeconds": "/myproject/ElapsedSeconds", + "Enum_Para_1": "/myproject/Enum_Para_1", + "Enum_Para_2": "/myproject/Enum_Para_2", + "Enum_Para_3": "/myproject/Enum_Para_3", + "EpochUSNO": "/myproject/EpochUSNO", + "EPS_Error_Flag": "/myproject/EPS_Error_Flag", + "Gyro.x": "/myproject/Gyro.x", + "Gyro.y": "/myproject/Gyro.y", + "Gyro.z": "/myproject/Gyro.z", + "Height": "/myproject/Height", + "Latitude": "/myproject/Latitude", + "Longitude": "/myproject/Longitude", + "Magnetometer.x": "/myproject/Magnetometer.x", + "Magnetometer.y": "/myproject/Magnetometer.y", + "Magnetometer.z": "/myproject/Magnetometer.z", + "Mode_Day": "/myproject/Mode_Day", + "Mode_Night": "/myproject/Mode_Night", + "Mode_Payload": "/myproject/Mode_Payload", + "Mode_Safe": "/myproject/Mode_Safe", + "Mode_SBand": "/myproject/Mode_SBand", + "Mode_XBand": "/myproject/Mode_XBand", + "OrbitNumberCumulative": "/myproject/OrbitNumberCumulative", + "Payload_Error_Flag": "/myproject/Payload_Error_Flag", + "Payload_Status": "/myproject/Payload_Status", + "Position.x": "/myproject/Position.x", + "Position.y": "/myproject/Position.y", + "Position.z": "/myproject/Position.z", + "Shadow": "/myproject/Shadow", + "Sunsensor": "/myproject/Sunsensor", + "Velocity.x": "/myproject/Velocity.x", + "Velocity.y": "/myproject/Velocity.y", + "Velocity.z": "/myproject/Velocity.z" +} \ No newline at end of file diff --git a/tests/e2e/yamcs/network.e2e.spec.js b/tests/e2e/yamcs/network.e2e.spec.mjs similarity index 98% rename from tests/e2e/yamcs/network.e2e.spec.js rename to tests/e2e/yamcs/network.e2e.spec.mjs index 734ada70..e1559496 100644 --- a/tests/e2e/yamcs/network.e2e.spec.js +++ b/tests/e2e/yamcs/network.e2e.spec.mjs @@ -24,8 +24,9 @@ * This suite verifies the network requests made by the application to ensure correct interaction with YAMCS. */ -import { test, expect } from '../opensource/pluginFixtures.js'; -import { setFixedTimeMode } from '../opensource/appActions.js'; +import { pluginFixtures, appActions } from 'openmct-e2e'; +const { test, expect } = pluginFixtures; +const { setFixedTimeMode } = appActions; /** * This test suite checks the network requests made by Open MCT to YAMCS. diff --git a/tests/e2e/yamcs/quickstartSmoke.e2e.spec.js b/tests/e2e/yamcs/quickstartSmoke.e2e.spec.mjs similarity index 97% rename from tests/e2e/yamcs/quickstartSmoke.e2e.spec.js rename to tests/e2e/yamcs/quickstartSmoke.e2e.spec.mjs index 611cef9c..9388276c 100644 --- a/tests/e2e/yamcs/quickstartSmoke.e2e.spec.js +++ b/tests/e2e/yamcs/quickstartSmoke.e2e.spec.mjs @@ -33,7 +33,8 @@ comfortable running this test during a live mission?" Avoid creating or deleting Make no assumptions about the order that elements appear in the DOM. */ -import { test, expect } from '../opensource/baseFixtures.js'; +import { baseFixtures } from 'openmct-e2e'; +const { test, expect } = baseFixtures; test.describe("Quickstart smoke tests @yamcs", () => { test('Verify that the create button appears and that the Folder Domain Object is available for selection', async ({ page }) => { diff --git a/tests/e2e/yamcs/quickstartTools.e2e.spec.mjs b/tests/e2e/yamcs/quickstartTools.e2e.spec.mjs new file mode 100644 index 00000000..e7260c13 --- /dev/null +++ b/tests/e2e/yamcs/quickstartTools.e2e.spec.mjs @@ -0,0 +1,81 @@ +/***************************************************************************** + * 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 { + enableLink, + disableLink, + isLinkEnabled, + latestParameterValues, + parameterArchive +} from './quickstartTools.mjs'; +import { pluginFixtures } from 'openmct-e2e'; +const { test, expect } = pluginFixtures; + +test.describe('Quickstart library functions', () => { + let yamcsURL; + + test.beforeEach(async ({page}) => { + // Go to baseURL so we can get relative URL + await page.goto('./', { waitUntil: 'domcontentloaded' }); + yamcsURL = new URL('/yamcs-proxy/', page.url()).toString(); + await enableLink(yamcsURL); + }); + test('Link can be disabled', async ({ page }) => { + await disableLink(yamcsURL); + expect(await isLinkEnabled(yamcsURL)).toBe(false); + }); + test('Link can be enabled', async ({ page }) => { + await disableLink(yamcsURL); + expect(await isLinkEnabled(yamcsURL)).toBe(false); + + await enableLink(yamcsURL); + expect(await isLinkEnabled(yamcsURL)).toBe(true); + }); + test('Latest values can be retrieved', async () => { + const latestValues = await latestParameterValues(['/myproject/Battery1_Temp', '/myproject/Battery1_Voltage'], yamcsURL); + expect(latestValues.length).toBe(2); + const areAllParameterValuesNumbers = latestValues.every((parameter) => { + return !isNaN(parameter.engValue.floatValue); + }); + + expect(areAllParameterValuesNumbers).toBe(true); + }); + test('Parameter archive values can be retrieved', async () => { + const now = new Date(); + const ONE_MINUTE = 60 * 1000; + const then = new Date(now - ONE_MINUTE); + const latestValues = await parameterArchive({ + start: then.toISOString(), + end: now.toISOString(), + parameterId: '/myproject/Battery1_Temp', + yamcsURL + }); + expect(latestValues.length).toBeGreaterThan(0); + + const areAllParameterValuesNumbers = latestValues.every((parameter) => { + return !isNaN(parameter.engValue.floatValue); + }); + + expect(areAllParameterValuesNumbers).toBe(true); + }); + +}); diff --git a/tests/e2e/yamcs/quickstartTools.mjs b/tests/e2e/yamcs/quickstartTools.mjs new file mode 100644 index 00000000..a755d81c --- /dev/null +++ b/tests/e2e/yamcs/quickstartTools.mjs @@ -0,0 +1,81 @@ +/***************************************************************************** + * 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. + *****************************************************************************/ +async function disableLink(yamcsURL) { + const url = new URL(`api/links/myproject/udp-in:disable`, yamcsURL); + await fetch(url.toString(), { + method: 'POST' + }); +} + +async function enableLink(yamcsURL) { + const url = new URL(`api/links/myproject/udp-in:enable`, yamcsURL); + await fetch(url.toString(), { + method: 'POST' + }); +} + +async function isLinkEnabled(yamcsURL) { + const url = new URL(`api/links/myproject/udp-in`, yamcsURL); + const response = await (await fetch(url.toString())).json(); + + return response.disabled !== true; +} + +async function latestParameterValues(parameterIds, yamcsURL) { + const parameterIdsRequest = { + fromCache: true, + id: parameterIds.map(parameterName => { + return { + name: parameterName + }; + }) + }; + const parameterIdsRequestSerialized = JSON.stringify(parameterIdsRequest); + const url = new URL('api/processors/myproject/realtime/parameters:batchGet', yamcsURL); + const response = await (await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: parameterIdsRequestSerialized + })).json(); + + return response.value; +} + +async function parameterArchive({start, end, parameterId, yamcsURL}) { + const url = new URL(`api/archive/myproject/parameters/${parameterId}`, `${yamcsURL}`); + url.searchParams.set('start', start); + url.searchParams.set('stop', end); + + const response = await (await fetch(url.toString())).json(); + + return response.parameter; +} + +export { + disableLink, + enableLink, + isLinkEnabled, + latestParameterValues, + parameterArchive +}; diff --git a/tests/e2e/yamcs/realtimeData.e2e.spec.mjs b/tests/e2e/yamcs/realtimeData.e2e.spec.mjs new file mode 100644 index 00000000..4aad789a --- /dev/null +++ b/tests/e2e/yamcs/realtimeData.e2e.spec.mjs @@ -0,0 +1,499 @@ +/***************************************************************************** + * 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. + *****************************************************************************/ + +/** + * IMPORTANT: CANNOT BE RUN IN PARALLEL, ENABLES & DISABLES LINKS + */ +import { expect, test } from '@playwright/test'; +import { fileURLToPath } from 'url'; +import { latestParameterValues, disableLink, enableLink, parameterArchive } from './quickstartTools.mjs'; + +import fs from 'fs'; + +const namesToParametersMap = JSON.parse(fs.readFileSync(new URL('./namesToParametersMap.json', import.meta.url))); +const realTimeDisplayPath = fileURLToPath( + new URL('./test-data/e2e-real-time-test-layout.json', import.meta.url) +); + +// Wait 1s from when telemetry is received before sampling values in the UI. This is 1s because by default +// Open MCT is configured to release batches of telemetry every 1s. So depending on when it is sampled it +// may take up to 1s for telemetry to propagate to the UI from when it is received. +const TELEMETRY_PROPAGATION_TIME = 1000; +const THIRTY_MINUTES = 30 * 60 * 1000; + +test.describe('Realtime telemetry displays', () => { + let yamcsURL; + let websocketWorker; + + test.beforeEach(async ({ page }) => { + page.on('worker', worker => { + if (worker.url().startsWith('blob')) { + websocketWorker = worker; + } + }); + + // Go to baseURL + await page.goto('./', { waitUntil: 'domcontentloaded' }); + await page.evaluate((thirtyMinutes) => { + const openmct = window.openmct; + + openmct.install(openmct.plugins.RemoteClock({ + namespace: "taxonomy", + key: "~myproject~Battery1_Temp" + })); + + openmct.time.setClock('remote-clock'); + openmct.time.setClockOffsets({ + start: -thirtyMinutes, + end: 0 + }); + }, THIRTY_MINUTES); + yamcsURL = new URL('/yamcs-proxy/', page.url()).toString(); + await enableLink(yamcsURL); + + await page + .getByRole('treeitem', { + name: /My Items/ + }) + .click({ + button: 'right' + }); + + await page + .getByRole('menuitem', { + name: /Import from JSON/ + }) + .click(); + + // Upload memory-leak-detection.json + await page.setInputFiles('#fileElem', realTimeDisplayPath); + await page + .getByRole('button', { + name: 'Save' + }) + .click(); + + await expect(page.locator('a:has-text("e2e real-time test layout")')).toBeVisible(); + }); + test.afterEach(async ({ page }) => { + await enableLink(yamcsURL); + }); + + test.describe('A complex display', () => { + test.beforeEach(async ({ page }) => { + const searchBox = page.getByRole('searchbox', { name: 'Search Input' }); + await searchBox.click(); + // Fill Search input + await searchBox.fill("e2e real-time test layout"); + + const searchResults = page.getByLabel('Search Results Dropdown'); + + //Search Result Appears and is clicked + const layoutSearchResult = searchResults.getByText("e2e real-time test layout", { exact: true }); + await layoutSearchResult.click(); + }); + + test('renders correctly', async ({ page }) => { + let count = await page.getByLabel('lad name').count(); + expect(count).toBe(Object.entries(namesToParametersMap).length); + }); + + test('Correctly shows the latest values', async ({ page }) => { + // Wait a reasonable amount of time for new telemetry to come in. + // There is nothing significant about the number chosen. + const WAIT_FOR_MORE_TELEMETRY = 3000; + + const ladTable = await getLadTableByName(page, 'Test LAD Table'); + + // Let it run for a few seconds + await page.waitForTimeout(WAIT_FOR_MORE_TELEMETRY); + + // Disable playback + await disableLink(yamcsURL); + + // Wait 1 second for values to propagate to client and render on screen. + await page.waitForTimeout(TELEMETRY_PROPAGATION_TIME); + + const latestValueObjects = await latestParameterValues(Object.values(namesToParametersMap), yamcsURL); + const parameterNamesToLatestValues = toParameterNameToValueMap(latestValueObjects); + const tableValuesByParameterName = await getParameterValuesFromLadTable(ladTable); + const allAlphaNumericValuesByName = await getParameterValuesFromAllAlphaNumerics(page); + const allGaugeValuesByName = await getParameterValuesFromAllGauges(page); + const tableTimestampsByParameterName = await getParameterTimestampsFromLadTable(ladTable); + assertParameterMapsAreEqual(parameterNamesToLatestValues, tableValuesByParameterName); + assertParameterMapsAreEqual(parameterNamesToLatestValues, allAlphaNumericValuesByName); + assertParameterMapsAreEqual(allGaugeValuesByName, parameterNamesToLatestValues, 2); + + // Enable playback + await enableLink(yamcsURL); + + // Let it run for a few seconds to cycle through a few telemetry values + await page.waitForTimeout(WAIT_FOR_MORE_TELEMETRY); + + // Disable playback + await disableLink(yamcsURL); + + // Wait 1 second for values to propagate to client and render on screen. + await page.waitForTimeout(TELEMETRY_PROPAGATION_TIME); + + const secondLatestValueObjects = await latestParameterValues(Object.values(namesToParametersMap), yamcsURL); + const secondParameterNamesToLatestValues = toParameterNameToValueMap(secondLatestValueObjects); + const secondTableValuesByParameterName = await getParameterValuesFromLadTable(ladTable); + const secondTableTimestampsByParameterName = await getParameterTimestampsFromLadTable(ladTable); + const secondAlphaNumericValuesByName = await getParameterValuesFromAllAlphaNumerics(page); + const secondGaugeValuesByName = await getParameterValuesFromAllGauges(page); + + //First compare timestamps to make sure telemetry on screen is actually changing. + Object.keys(namesToParametersMap).forEach(key => { + expect(tableTimestampsByParameterName[key]).not.toBe(secondTableTimestampsByParameterName[key]); + }); + + // Next confirm that the values on screen are, again, the same as the latest values in Yamcs + assertParameterMapsAreEqual(secondParameterNamesToLatestValues, secondTableValuesByParameterName); + assertParameterMapsAreEqual(secondParameterNamesToLatestValues, secondAlphaNumericValuesByName); + assertParameterMapsAreEqual(secondGaugeValuesByName, parameterNamesToLatestValues, 2); + }); + + test('Correctly reconnects and shows the latest values after websocket drop', async ({ page }) => { + // Wait a reasonable amount of time for new telemetry to come in. + // There is nothing significant about the number chosen. + const WAIT_FOR_MORE_TELEMETRY = 3000; + + const ladTable = await getLadTableByName(page, 'Test LAD Table'); + + // Let it run for a few seconds + await page.waitForTimeout(WAIT_FOR_MORE_TELEMETRY); + + // Disable playback + await disableLink(yamcsURL); + + // Wait 1 second for values to propagate to client and render on screen. + await page.waitForTimeout(TELEMETRY_PROPAGATION_TIME); + + const latestValueObjects = await latestParameterValues(Object.values(namesToParametersMap), yamcsURL); + const parameterNamesToLatestValues = toParameterNameToValueMap(latestValueObjects); + const tableValuesByParameterName = await getParameterValuesFromLadTable(ladTable); + const allAlphaNumericValuesByName = await getParameterValuesFromAllAlphaNumerics(page); + const allGaugeValuesByName = await getParameterValuesFromAllGauges(page); + const tableTimestampsByParameterName = await getParameterTimestampsFromLadTable(ladTable); + assertParameterMapsAreEqual(parameterNamesToLatestValues, tableValuesByParameterName); + assertParameterMapsAreEqual(parameterNamesToLatestValues, allAlphaNumericValuesByName); + assertParameterMapsAreEqual(allGaugeValuesByName, parameterNamesToLatestValues, 2); + + // Enable playback + await enableLink(yamcsURL); + + // Drop the websocket + websocketWorker.evaluate(() => { + self.currentWebSocket.close(); + }); + + //Wait for websocket to be re-established + await page.waitForEvent('websocket'); + + // Let it run for a few seconds to cycle through a few telemetry values + await page.waitForTimeout(WAIT_FOR_MORE_TELEMETRY); + + // Disable playback + await disableLink(yamcsURL); + + // Wait 1 second for values to propagate to client and render on screen. + await page.waitForTimeout(TELEMETRY_PROPAGATION_TIME); + + const secondLatestValueObjects = await latestParameterValues(Object.values(namesToParametersMap), yamcsURL); + const secondParameterNamesToLatestValues = toParameterNameToValueMap(secondLatestValueObjects); + const secondTableValuesByParameterName = await getParameterValuesFromLadTable(ladTable); + const secondTableTimestampsByParameterName = await getParameterTimestampsFromLadTable(ladTable); + const secondAlphaNumericValuesByName = await getParameterValuesFromAllAlphaNumerics(page); + const secondGaugeValuesByName = await getParameterValuesFromAllGauges(page); + + //First compare timestamps to make sure telemetry on screen is actually changing. + Object.keys(namesToParametersMap).forEach(key => { + expect(tableTimestampsByParameterName[key]).not.toBe(secondTableTimestampsByParameterName[key]); + }); + + // Next confirm that the values on screen are, again, the same as the latest values in Yamcs + assertParameterMapsAreEqual(secondParameterNamesToLatestValues, secondTableValuesByParameterName); + assertParameterMapsAreEqual(secondParameterNamesToLatestValues, secondAlphaNumericValuesByName); + assertParameterMapsAreEqual(secondGaugeValuesByName, parameterNamesToLatestValues, 2); + }); + + test('Open MCT does not drop telemetry while app is loading', async ({ page }) => { + const notification = page.getByRole('alert'); + const count = await notification.count(); + + if (count > 0) { + const text = await notification.innerText(); + expect(text).not.toBe('Telemetry dropped due to client rate limiting.'); + } else { + expect(notification).toHaveCount(0); + } + }); + + test('Open MCT does drop telemetry when the UI is under load', async ({ page }) => { + // 1. Make sure the display is done loading, and populated with values (ie. we are in a steady state) + const ladTable = await getLadTableByName(page, 'Test LAD Table'); + await getParameterValuesFromLadTable(ladTable); + + // 2. Block the UI with a loop + await page.evaluate(() => { + return new Promise((resolveBlockingLoop) => { + //5s x 10Hz data = 50 telemetry values which should easily overrun the buffer length of 20. + let start = Date.now(); + let now = Date.now(); + // Block the UI thread for 5s + while (now - start < 5000) { + now = Date.now(); + } + + resolveBlockingLoop(); + }); + }); + // Check for telemetry dropped notification + const notification = page.getByRole('alert'); + expect(notification).toHaveCount(1); + const text = await notification.innerText(); + expect(text).toBe('Telemetry dropped due to client rate limiting.'); + }); + + test('Open MCT shows the latest telemetry after UI is temporarily blocked', async ({ page }) => { + const ladTable = await getLadTableByName(page, 'Test LAD Table'); + // 1. Subscribe to batched telemetry, + // 3. Confirm that it is correct and only the _oldest_ values missing + await page.evaluate(() => { + return new Promise((resolveBlockingLoop) => { + let start = Date.now(); + let now = Date.now(); + // Block the UI thread for 5s + while (now - start < 5000) { + now = Date.now(); + } + + resolveBlockingLoop(); + }); + }); + + // Disable playback + await disableLink(yamcsURL); + + // Wait 1 second for values to propagate to client and render on screen. + await page.waitForTimeout(TELEMETRY_PROPAGATION_TIME); + + const latestValueObjects = await latestParameterValues(Object.values(namesToParametersMap), yamcsURL); + const parameterNamesToLatestValues = toParameterNameToValueMap(latestValueObjects); + const tableValuesByParameterName = await getParameterValuesFromLadTable(ladTable); + assertParameterMapsAreEqual(parameterNamesToLatestValues, tableValuesByParameterName); + }); + }); + + test('Open MCT accurately batches telemetry when requested', async ({ page }) => { + + // 1. Subscribe to batched telemetry, + const telemetryValues = await page.evaluate(async () => { + const openmct = window.openmct; + const telemetryObject = await openmct.objects.get({ + namespace: 'taxonomy', + key: '~myproject~Battery1_Temp' + }); + + return new Promise((resolveWithTelemetry) => { + // First callback is the latest value for the parameter. + let haveReceivedLatest = false; + openmct.telemetry.subscribe(telemetryObject, (telemetry) => { + if (haveReceivedLatest === false) { + haveReceivedLatest = true; + } else { + resolveWithTelemetry(telemetry); + } + }, {strategy: 'batch'}); + }); + }); + await disableLink(yamcsURL); + sortOpenMctTelemetryAscending(telemetryValues); + + // 2. confirm that it is received as an array. + expect(telemetryValues.length).toBeGreaterThan(1); + const start = new Date(new Date(telemetryValues[0].timestamp).getTime() - 1).toISOString(); + const end = new Date(telemetryValues[telemetryValues.length - 1].timestamp).toISOString(); + const parameterArchiveTelemetry = await parameterArchive({ + start, + end, + parameterId: `/myproject/Battery1_Temp`, + yamcsURL + }); + const formattedParameterArchiveTelemetry = toOpenMctTelemetryFormat(parameterArchiveTelemetry); + sortOpenMctTelemetryAscending(formattedParameterArchiveTelemetry); + + telemetryValues.forEach((telemetry, index) => { + expect(telemetry.value).toBe(formattedParameterArchiveTelemetry[index].value); + expect(telemetry.timestamp).toBe(formattedParameterArchiveTelemetry[index].timestamp); + }); + }); + + function sortOpenMctTelemetryAscending(telemetry) { + return telemetry.sort((a, b) => { + if (a.timestamp < b.timestamp) { + return -1; + } else if (a.timestamp > b.timestamp) { + return 1; + } else if (a.timestamp === b.timestamp) { + return 0; + } else { + return undefined; + } + }); + } + + function assertParameterMapsAreEqual(parameterNamesToLatestValues, tableValuesByParameterName, toPrecision) { + Object.keys(parameterNamesToLatestValues).forEach((parameterName) => { + const valueInYamcs = parameterNamesToLatestValues[parameterName]; + const valueOnScreen = tableValuesByParameterName[parameterName]; + if (toPrecision !== undefined && !isNaN(valueInYamcs) && !isNaN(valueOnScreen)) { + const numericalValueInYamcs = parseFloat(valueInYamcs).toFixed(toPrecision); + const numericalValueOnScreen = parseFloat(valueInYamcs).toFixed(toPrecision); + + expect(numericalValueOnScreen).toBe(numericalValueInYamcs); + } else { + expect(valueOnScreen).toBe(valueInYamcs); + } + }); + } + + function toParameterNameToValueMap(latestParameterValueObjects) { + return latestParameterValueObjects.reduce((mapping, parameterValue) => { + mapping[parameterValue.id.name.substring(parameterValue.id.name.lastIndexOf('/') + 1)] = + String(parameterValue.engValue.floatValue + ?? parameterValue.engValue.stringValue + ?? parameterValue.engValue.uint32Value + ?? parameterValue.engValue.booleanValue); + + return mapping; + }, {}); + } + + function toOpenMctTelemetryFormat(listOfParameterValueObjects) { + return listOfParameterValueObjects.map((parameterValue) => { + return { + timestamp: parameterValue.generationTime, + value: parameterValue.engValue.floatValue + ?? parameterValue.engValue.stringValue + ?? parameterValue.engValue.uint32Value + ?? parameterValue.engValue.booleanValue + }; + }); + } + + async function getLadTableByName(page, ladTableName) { + const matchingLadTableFrames = await page.getByLabel("sub object frame").filter({ + has: page.getByLabel("object name", { + name: ladTableName + }) + }); + + return matchingLadTableFrames.getByLabel('lad table').first(); + + } + + /** + * @param {import('playwright').Page} page + * @returns {Promise<{parameterNameText: string, parameterValueText: string}[]>} + */ + async function getParameterValuesFromAllGauges(page) { + const allGauges = await (page.getByLabel('sub object frame', { exact: true}).filter({ + has: page.getByLabel('Gauge', { + exact: true + }) + })).all(); + const arrayOfValues = await Promise.all(allGauges.map(async (gauge) => { + const parameterNameText = await (gauge.getByLabel("object name")).innerText(); + const parameterValueText = await (gauge.getByLabel(/gauge value.*/)).innerText(); + + return { + parameterNameText, + parameterValueText + }; + })); + + return arrayOfValues.reduce((map, row) => { + map[row.parameterNameText] = row.parameterValueText; + + return map; + }, {}); + } + + async function getParameterValuesFromLadTable(ladTable) { + const allRows = await (await ladTable.getByLabel('lad row')).all(); + const arrayOfValues = await Promise.all(allRows.map(async (row) => { + const parameterNameText = await row.getByLabel('lad name').innerText(); + const parameterValueText = await row.getByLabel('lad value').innerText(); + + return { + parameterNameText, + parameterValueText + }; + })); + + return arrayOfValues.reduce((map, row) => { + map[row.parameterNameText] = row.parameterValueText; + + return map; + }, {}); + } + + async function getParameterValuesFromAllAlphaNumerics(page) { + const allAlphaNumerics = await (page.getByLabel('Alpha-numeric telemetry', {exact: true})).all(); + const arrayOfValues = await Promise.all(allAlphaNumerics.map(async (alphaNumeric) => { + const parameterNameText = await (alphaNumeric.getByLabel(/Alpha-numeric telemetry name.*/)).innerText(); + const parameterValueText = await (alphaNumeric.getByLabel(/Alpha-numeric telemetry value.*/)).innerText(); + + return { + parameterNameText, + parameterValueText + }; + })); + + return arrayOfValues.reduce((map, row) => { + map[row.parameterNameText] = row.parameterValueText; + + return map; + }, {}); + } + + async function getParameterTimestampsFromLadTable(ladTable) { + const allRows = await (await ladTable.getByLabel('lad row')).all(); + const arrayOfValues = await Promise.all(allRows.map(async (row) => { + const parameterNameText = await row.getByLabel('lad name').innerText(); + const parameterValueText = await row.getByLabel('lad timestamp').innerText(); + + return { + parameterNameText, + parameterValueText + }; + })); + + return arrayOfValues.reduce((map, row) => { + map[row.parameterNameText] = row.parameterValueText; + + return map; + }, {}); + } +}); diff --git a/tests/e2e/yamcs/search.e2e.spec.js b/tests/e2e/yamcs/search.e2e.spec.mjs similarity index 96% rename from tests/e2e/yamcs/search.e2e.spec.js rename to tests/e2e/yamcs/search.e2e.spec.mjs index 72984e40..5254a8c0 100644 --- a/tests/e2e/yamcs/search.e2e.spec.js +++ b/tests/e2e/yamcs/search.e2e.spec.mjs @@ -24,7 +24,8 @@ Search Specific Tests */ -import { test, expect } from '../opensource/pluginFixtures.js'; +import { pluginFixtures } from 'openmct-e2e'; +const { test, expect } = pluginFixtures; test.describe("Quickstart search tests @yamcs", () => { test('Validate aggregate in search result', async ({ page }) => { diff --git a/tests/e2e/yamcs/staleness.e2e.mjs b/tests/e2e/yamcs/staleness.e2e.mjs new file mode 100644 index 00000000..1494f9e4 --- /dev/null +++ b/tests/e2e/yamcs/staleness.e2e.mjs @@ -0,0 +1,44 @@ +/***************************************************************************** + * 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. + *****************************************************************************/ + +/* +Staleness Specific Tests +*/ + +import { pluginFixtures } from 'openmct-e2e'; +const { test } = pluginFixtures; + +test.describe.fixme("Staleness tests @yamcs", () => { + // eslint-disable-next-line require-await + test('Staleness ', async ({ page }) => { + test.step('Indicator is displayed for historic data', () => { + // Create a plot + // Add a telemetry endpoint that has stale data to this plot + // Expect that there is indication of staleness for the plot + }); + + test.step('Indicator is removed when new data arrives in real time', () => { + // Wait for new data + // Expect that stale indication is removed + }); + }); +}); diff --git a/tests/e2e/yamcs/telemetryTables.e2e.spec.mjs b/tests/e2e/yamcs/telemetryTables.e2e.spec.mjs new file mode 100644 index 00000000..e151dfac --- /dev/null +++ b/tests/e2e/yamcs/telemetryTables.e2e.spec.mjs @@ -0,0 +1,80 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2022, 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. + *****************************************************************************/ + +/* +Telemetry Table Specific Tests +*/ + +import { pluginFixtures } from 'openmct-e2e'; +const { test, expect } = pluginFixtures; + +test.describe("Telemetry Tables tests @yamcs", () => { + + // An error will be thrown if an attempt to mutate an immutable object is made, this will cover + // that case as well as any other errors during the test + test.use({ failOnConsoleError: true }); + + test.beforeEach(async ({ page }) => { + // Go to baseURL + await page.goto("./", { waitUntil: "domcontentloaded" }); + await expect(page.getByText('Loading...')).toBeHidden(); + + // Expand myproject + await page.getByLabel('Expand myproject').click(); + }); + + test('Telemetry Tables viewing an unpersistable object, will not modify the configuration on mode change', async ({ page }) => { + // Navigat to the Events table + await page.getByLabel('Navigate to Events yamcs.').click(); + + // Find the mode switch button and click it, this will trigger a mutation on mutable objects configuration + await page.getByRole('button', { name: 'SHOW UNLIMITED' }).click(); + + // Assert that the 'SHOW LIMITED' button is now visible + await expect(page.getByRole('button', { name: 'SHOW LIMITED' })).toBeVisible(); + }); + + test('Telemetry tables when changing mode, will not change the sort order of the request', async ({ page }) => { + // Set up request promise for an events request in descending order + let eventRequestOrderDescending = page.waitForRequest(/.*\/api\/.*\/events.*order=desc$/); + + // Navigate to the Events table + await page.getByLabel('Navigate to Events yamcs.').click(); + await page.waitForLoadState('networkidle'); + + // Wait for the descending events request + await eventRequestOrderDescending; + + // Reset request promise for an events request in descending order + eventRequestOrderDescending = page.waitForRequest(/.*\/api\/.*\/events.*order=desc$/); + + // Find the mode switch button and click it, this will trigger another events request + await page.getByRole('button', { name: 'SHOW UNLIMITED' }).click(); + await page.waitForLoadState('networkidle'); + + await eventRequestOrderDescending; + + // Assert that the 'SHOW LIMITED' button is now visible + await expect(page.getByRole('button', { name: 'SHOW LIMITED' })).toBeVisible(); + }); + +}); diff --git a/tests/e2e/yamcs/test-data/e2e-real-time-test-layout.json b/tests/e2e/yamcs/test-data/e2e-real-time-test-layout.json new file mode 100644 index 00000000..12c44f15 --- /dev/null +++ b/tests/e2e/yamcs/test-data/e2e-real-time-test-layout.json @@ -0,0 +1 @@ +{"openmct":{"c965501f-86c7-4d63-8857-95618462ea2d":{"identifier":{"key":"c965501f-86c7-4d63-8857-95618462ea2d","namespace":""},"name":"e2e real-time test layout","type":"layout","composition":[{"key":"797785c7-035a-4ea2-b69b-d595cf48b49c","namespace":""},{"key":"~myproject~A","namespace":"taxonomy"},{"key":"~myproject~ADCS_Error_Flag","namespace":"taxonomy"},{"key":"~myproject~Battery1_Temp","namespace":"taxonomy"},{"key":"~myproject~Battery1_Voltage","namespace":"taxonomy"},{"key":"~myproject~Battery2_Temp","namespace":"taxonomy"},{"key":"~myproject~Battery2_Voltage","namespace":"taxonomy"},{"key":"~myproject~CCSDS_Packet_Length","namespace":"taxonomy"},{"key":"~myproject~CDHS_Error_Flag","namespace":"taxonomy"},{"key":"~myproject~CDHS_Status","namespace":"taxonomy"},{"key":"~myproject~COMMS_Error_Flag","namespace":"taxonomy"},{"key":"~myproject~COMMS_Status","namespace":"taxonomy"},{"key":"~myproject~Contact_Golbasi_GS","namespace":"taxonomy"},{"key":"~myproject~Contact_Svalbard","namespace":"taxonomy"},{"key":"~myproject~Detector_Temp","namespace":"taxonomy"},{"key":"~myproject~ElapsedSeconds","namespace":"taxonomy"},{"key":"~myproject~Enum_Para_1","namespace":"taxonomy"},{"key":"~myproject~Enum_Para_2","namespace":"taxonomy"},{"key":"~myproject~Enum_Para_3","namespace":"taxonomy"},{"key":"~myproject~EpochUSNO","namespace":"taxonomy"},{"key":"~myproject~EPS_Error_Flag","namespace":"taxonomy"},{"key":"~myproject~Gyro.x","namespace":"taxonomy"},{"key":"~myproject~Gyro.y","namespace":"taxonomy"},{"key":"~myproject~Gyro.z","namespace":"taxonomy"},{"key":"~myproject~Height","namespace":"taxonomy"},{"key":"~myproject~Latitude","namespace":"taxonomy"},{"key":"~myproject~Longitude","namespace":"taxonomy"},{"key":"~myproject~Magnetometer.x","namespace":"taxonomy"},{"key":"~myproject~Magnetometer.y","namespace":"taxonomy"},{"key":"~myproject~Magnetometer.z","namespace":"taxonomy"},{"key":"~myproject~Mode_Day","namespace":"taxonomy"},{"key":"~myproject~Mode_Night","namespace":"taxonomy"},{"key":"~myproject~Mode_Payload","namespace":"taxonomy"},{"key":"~myproject~Mode_Safe","namespace":"taxonomy"},{"key":"~myproject~Mode_SBand","namespace":"taxonomy"},{"key":"~myproject~Mode_XBand","namespace":"taxonomy"},{"key":"~myproject~OrbitNumberCumulative","namespace":"taxonomy"},{"key":"~myproject~Payload_Error_Flag","namespace":"taxonomy"},{"key":"~myproject~Payload_Status","namespace":"taxonomy"},{"key":"~myproject~Position.x","namespace":"taxonomy"},{"key":"~myproject~Position.y","namespace":"taxonomy"},{"key":"~myproject~Position.z","namespace":"taxonomy"},{"key":"~myproject~Shadow","namespace":"taxonomy"},{"key":"~myproject~Velocity.x","namespace":"taxonomy"},{"key":"~myproject~Velocity.y","namespace":"taxonomy"},{"key":"~myproject~Velocity.z","namespace":"taxonomy"},{"key":"1816debc-38b6-4680-8019-701689be4fc5","namespace":""},{"key":"cc400028-b156-43cc-8bae-de13171d5431","namespace":""},{"key":"798e729f-420c-45f6-aecf-5bbc44b55dad","namespace":""},{"key":"fada1f9c-668d-49c4-a653-81bb3d466922","namespace":""},{"key":"c3eb99c4-db3c-4a92-8967-3b3052efd0cf","namespace":""},{"key":"0dd60b42-66a3-4326-b829-2cde71025d32","namespace":""},{"key":"eeda19bc-6302-47b6-b115-81204b16aaa3","namespace":""},{"key":"3093a1ef-ac30-4ac3-abff-e4ef5ca858e6","namespace":""},{"key":"e4b66733-2e89-4bf8-8a12-5ef3f247e4ec","namespace":""},{"key":"a91d265d-b36d-41fe-9d11-b799e52036ed","namespace":""},{"key":"739c26b2-776a-4abb-949f-24a2066aee80","namespace":""},{"key":"94997ceb-00c8-4702-af70-afb014cdf629","namespace":""},{"key":"803121a1-d187-489f-a124-5e903657bedb","namespace":""},{"key":"c984a025-e4a8-4c25-9b0e-1723b5a30303","namespace":""},{"key":"95cac0f8-cc42-4357-aac8-77d7ea5e31c9","namespace":""},{"key":"1011058f-8dec-4426-87d9-58135f663ea6","namespace":""},{"key":"c1881c9a-a638-4bbe-b320-a0ea5d7f4e4f","namespace":""},{"key":"d510bf8a-f03d-4cac-b58c-1242cc039b22","namespace":""},{"key":"a211ae9a-f013-4a97-87ed-401baee94a3e","namespace":""},{"key":"f051b323-d22a-419a-a6e6-42ea6041b238","namespace":""},{"key":"77ce4615-53d4-4ae7-809c-8535a57a041b","namespace":""},{"key":"b425c7bd-6912-4e40-908e-6a21c73c7db3","namespace":""},{"key":"~myproject~Sunsensor","namespace":"taxonomy"}],"configuration":{"items":[{"width":84,"height":96,"x":1,"y":13,"identifier":{"key":"797785c7-035a-4ea2-b69b-d595cf48b49c","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"24a9bf1f-0423-4863-bc78-e828907da303"},{"identifier":{"key":"~myproject~A","namespace":"taxonomy"},"x":85,"y":6,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"211674f3-e3c2-4c6a-bfc9-d463c19fe692"},{"identifier":{"key":"~myproject~ADCS_Error_Flag","namespace":"taxonomy"},"x":85,"y":8,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"0d7d24fb-423b-4fdf-8e72-ce58e5110fa8"},{"identifier":{"key":"~myproject~Battery1_Temp","namespace":"taxonomy"},"x":85,"y":10,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"0f7b5287-0a2f-494e-81d4-5deeee850f10"},{"identifier":{"key":"~myproject~Battery1_Voltage","namespace":"taxonomy"},"x":85,"y":12,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"78ebfb21-0126-4064-85bc-d4d25a4ddb58"},{"identifier":{"key":"~myproject~Battery2_Temp","namespace":"taxonomy"},"x":85,"y":14,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"67bf67ae-e574-447b-a8b7-889c0e7608a9"},{"identifier":{"key":"~myproject~Battery2_Voltage","namespace":"taxonomy"},"x":85,"y":16,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"131b0b30-e720-4993-949c-7a830cce113e"},{"identifier":{"key":"~myproject~CCSDS_Packet_Length","namespace":"taxonomy"},"x":85,"y":18,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"78abfd26-16d1-48f8-bae1-1b9f843bd75e"},{"identifier":{"key":"~myproject~CDHS_Error_Flag","namespace":"taxonomy"},"x":85,"y":20,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"9456ef72-7970-4695-b949-6f89183b92f2"},{"identifier":{"key":"~myproject~CDHS_Status","namespace":"taxonomy"},"x":85,"y":22,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"a263ef3e-5093-4c3b-8f96-0c2f63403c95"},{"identifier":{"key":"~myproject~COMMS_Error_Flag","namespace":"taxonomy"},"x":85,"y":24,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"0aa18240-ee67-4f9d-949d-b637df870ca9"},{"identifier":{"key":"~myproject~COMMS_Status","namespace":"taxonomy"},"x":85,"y":26,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"f09c1fc7-f353-4ceb-a0c4-0c66fe23051a"},{"identifier":{"key":"~myproject~Contact_Golbasi_GS","namespace":"taxonomy"},"x":85,"y":28,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"8de22bbf-e0b8-435a-91be-6f79bb9187b9"},{"identifier":{"key":"~myproject~Contact_Svalbard","namespace":"taxonomy"},"x":85,"y":30,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"41d0c878-591c-4540-b072-67ace5b753d6"},{"identifier":{"key":"~myproject~Detector_Temp","namespace":"taxonomy"},"x":85,"y":32,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"1bb8a77e-03e3-4c97-b327-3ba351fddd59"},{"identifier":{"key":"~myproject~ElapsedSeconds","namespace":"taxonomy"},"x":85,"y":34,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"22b1edd6-a520-405b-9297-01cb220b7f25"},{"identifier":{"key":"~myproject~Enum_Para_1","namespace":"taxonomy"},"x":85,"y":36,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"fb336c08-ce84-4708-9105-f401ffd6c275"},{"identifier":{"key":"~myproject~Enum_Para_2","namespace":"taxonomy"},"x":85,"y":38,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"76d94b1e-e65b-484b-a051-ba873b0d4acc"},{"identifier":{"key":"~myproject~Enum_Para_3","namespace":"taxonomy"},"x":85,"y":40,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"52bba941-b28c-4f00-a846-773b0cbd1b9c"},{"identifier":{"key":"~myproject~EpochUSNO","namespace":"taxonomy"},"x":85,"y":42,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"2771956c-7b11-4cfb-ac01-546203b7dc21"},{"identifier":{"key":"~myproject~EPS_Error_Flag","namespace":"taxonomy"},"x":85,"y":44,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"f95748cd-2f63-44fb-88a8-540bee063a3c"},{"identifier":{"key":"~myproject~Gyro.x","namespace":"taxonomy"},"x":85,"y":46,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"c6265d87-332c-4295-beb6-01e19d7c367b"},{"identifier":{"key":"~myproject~Gyro.y","namespace":"taxonomy"},"x":85,"y":48,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"0517dab9-ec57-4d4c-8e1f-aec5f263e00a"},{"identifier":{"key":"~myproject~Gyro.z","namespace":"taxonomy"},"x":85,"y":50,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"68fb8115-6d53-46cb-b0cd-ee2682b8c233"},{"identifier":{"key":"~myproject~Height","namespace":"taxonomy"},"x":85,"y":52,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"4a1ce445-17c0-4fb2-b103-ae0c90cf5683"},{"identifier":{"key":"~myproject~Latitude","namespace":"taxonomy"},"x":85,"y":60,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"eaf4fdf1-b14b-45b2-b138-69e72e85fdbd"},{"identifier":{"key":"~myproject~Longitude","namespace":"taxonomy"},"x":85,"y":62,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"259174e8-4a30-449e-bc2f-dbfb58f0754d"},{"identifier":{"key":"~myproject~Magnetometer.x","namespace":"taxonomy"},"x":85,"y":64,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"3e427410-9906-454a-8cfc-fc6e2955dc2e"},{"identifier":{"key":"~myproject~Magnetometer.y","namespace":"taxonomy"},"x":85,"y":66,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"0d735f42-efd5-4122-afc4-f97067776d6a"},{"identifier":{"key":"~myproject~Magnetometer.z","namespace":"taxonomy"},"x":85,"y":68,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"e7ec6551-1e6c-4133-8521-9c2ceb408341"},{"identifier":{"key":"~myproject~Mode_Day","namespace":"taxonomy"},"x":85,"y":70,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"aa7bf8ef-581c-4ce4-9989-5a10e2bee67a"},{"identifier":{"key":"~myproject~Mode_Night","namespace":"taxonomy"},"x":85,"y":72,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"2d043561-9b84-4f30-9f1c-8ebb76c05d66"},{"identifier":{"key":"~myproject~Mode_Payload","namespace":"taxonomy"},"x":85,"y":74,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"eaf50270-1ec4-4c01-b8ee-1434a00e5880"},{"identifier":{"key":"~myproject~Mode_Safe","namespace":"taxonomy"},"x":85,"y":76,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"87a3749e-411b-4997-859d-16c5b245915e"},{"identifier":{"key":"~myproject~Mode_SBand","namespace":"taxonomy"},"x":85,"y":78,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"a6f0dffa-bf9d-4015-88a6-eb2ffd7b6843"},{"identifier":{"key":"~myproject~Mode_XBand","namespace":"taxonomy"},"x":85,"y":80,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"e2e052b0-c65b-4656-a950-b1e7daeab350"},{"identifier":{"key":"~myproject~OrbitNumberCumulative","namespace":"taxonomy"},"x":85,"y":82,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"1b25db44-8063-4bc7-ae97-00d6dc850516"},{"identifier":{"key":"~myproject~Payload_Error_Flag","namespace":"taxonomy"},"x":85,"y":84,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"caac2d53-dabf-4b93-bde9-fb9fd913a562"},{"identifier":{"key":"~myproject~Payload_Status","namespace":"taxonomy"},"x":85,"y":86,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"f9c1fc8b-6545-41ac-956c-8beb8df47675"},{"identifier":{"key":"~myproject~Position.x","namespace":"taxonomy"},"x":85,"y":88,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"25b25ba0-0983-4613-8f71-96e04f6091c1"},{"identifier":{"key":"~myproject~Position.y","namespace":"taxonomy"},"x":85,"y":90,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"7ea7c493-49af-469c-854e-cea5fcd58e6a"},{"identifier":{"key":"~myproject~Position.z","namespace":"taxonomy"},"x":85,"y":92,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"a7a6904f-e438-443d-b746-a790ffb86f6a"},{"identifier":{"key":"~myproject~Shadow","namespace":"taxonomy"},"x":85,"y":94,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"25e93f35-138c-4fff-b032-852647d642dc"},{"identifier":{"key":"~myproject~Velocity.x","namespace":"taxonomy"},"x":85,"y":96,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"b7085d65-8192-4e57-97b9-5efc7b6100e8"},{"identifier":{"key":"~myproject~Velocity.y","namespace":"taxonomy"},"x":85,"y":98,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"ce20ed52-545e-46f6-8d08-adcf4ede2737"},{"identifier":{"key":"~myproject~Velocity.z","namespace":"taxonomy"},"x":85,"y":100,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"da57e178-0680-4c88-886e-c05ff5cc0ec9"},{"width":32,"height":18,"x":124,"y":56,"identifier":{"key":"1816debc-38b6-4680-8019-701689be4fc5","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"d5a9f702-c463-4d26-bdde-20389902169d"},{"width":32,"height":18,"x":156,"y":56,"identifier":{"key":"cc400028-b156-43cc-8bae-de13171d5431","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"d42d03b9-c936-4a9d-8e4a-00a3df8ae384"},{"width":32,"height":18,"x":124,"y":2,"identifier":{"key":"798e729f-420c-45f6-aecf-5bbc44b55dad","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"d544d8df-07cc-4054-a076-d61426a9e82d"},{"width":32,"height":18,"x":156,"y":38,"identifier":{"key":"fada1f9c-668d-49c4-a653-81bb3d466922","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"ca660084-6262-4578-a520-591b187d88f1"},{"width":32,"height":18,"x":156,"y":2,"identifier":{"key":"c3eb99c4-db3c-4a92-8967-3b3052efd0cf","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"c97e1608-bc2b-43ee-ac72-8cf26f6dff3b"},{"width":32,"height":18,"x":124,"y":74,"identifier":{"key":"0dd60b42-66a3-4326-b829-2cde71025d32","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"06c85011-f501-4bc2-84f2-0a901ade5361"},{"width":32,"height":18,"x":124,"y":20,"identifier":{"key":"eeda19bc-6302-47b6-b115-81204b16aaa3","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"a04e01e6-2e43-42e2-8968-969288cc7580"},{"width":32,"height":18,"x":156,"y":20,"identifier":{"key":"3093a1ef-ac30-4ac3-abff-e4ef5ca858e6","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"25d60c4a-a7e4-4c89-b3bc-710bde8b205f"},{"width":32,"height":18,"x":124,"y":38,"identifier":{"key":"e4b66733-2e89-4bf8-8a12-5ef3f247e4ec","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"ff6578eb-8ba3-4004-ad5d-6eabad72a64a"},{"width":32,"height":18,"x":124,"y":92,"identifier":{"key":"a91d265d-b36d-41fe-9d11-b799e52036ed","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"94c9c35e-bf2c-43e5-9926-801947805262"},{"width":32,"height":18,"x":188,"y":20,"identifier":{"key":"739c26b2-776a-4abb-949f-24a2066aee80","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"2a683f8f-2683-4388-9395-1cbe10e61a8a"},{"width":32,"height":18,"x":188,"y":2,"identifier":{"key":"94997ceb-00c8-4702-af70-afb014cdf629","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"801c5226-dc3d-4ba6-a32f-b000bb49cf68"},{"width":32,"height":18,"x":188,"y":38,"identifier":{"key":"803121a1-d187-489f-a124-5e903657bedb","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"62d173e7-dceb-4a4f-b2aa-9a9b776cd799"},{"width":32,"height":18,"x":188,"y":56,"identifier":{"key":"c984a025-e4a8-4c25-9b0e-1723b5a30303","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"9f3b0d07-e282-4d20-986e-ec986072174f"},{"width":32,"height":18,"x":156,"y":74,"identifier":{"key":"95cac0f8-cc42-4357-aac8-77d7ea5e31c9","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"f979e809-017a-43fe-ae53-ac681131a6b6"},{"width":32,"height":18,"x":188,"y":74,"identifier":{"key":"1011058f-8dec-4426-87d9-58135f663ea6","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"29dd421b-c146-4812-9ae6-362c943d6364"},{"width":32,"height":18,"x":156,"y":92,"identifier":{"key":"c1881c9a-a638-4bbe-b320-a0ea5d7f4e4f","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"2f2f5de0-a7ab-4f5b-97af-667ae4fe1485"},{"width":32,"height":18,"x":188,"y":92,"identifier":{"key":"d510bf8a-f03d-4cac-b58c-1242cc039b22","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"962c557d-1220-49ce-a478-a2f513d4aadf"},{"width":21,"height":13,"x":1,"y":0,"identifier":{"key":"a211ae9a-f013-4a97-87ed-401baee94a3e","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"dea5d1fb-bef0-426a-bb94-de8c2b11e431"},{"width":21,"height":13,"x":22,"y":0,"identifier":{"key":"f051b323-d22a-419a-a6e6-42ea6041b238","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"3bed0cbd-3630-46e8-af19-d07f28ea77b0"},{"width":21,"height":13,"x":43,"y":0,"identifier":{"key":"77ce4615-53d4-4ae7-809c-8535a57a041b","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"845a3124-331a-47c4-88bb-25bb1db37dda"},{"width":21,"height":13,"x":64,"y":0,"identifier":{"key":"b425c7bd-6912-4e40-908e-6a21c73c7db3","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"262a783c-9e5c-4987-9e65-f8d8705b0772"},{"identifier":{"key":"~myproject~Sunsensor","namespace":"taxonomy"},"x":85,"y":102,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"b27f8bbe-7aea-4680-95be-2dac4ab0399d"}],"layoutGrid":[10,10],"objectStyles":{}},"modified":1709249767953,"location":"mine","modifiedBy":"guest","createdBy":"guest","created":1707870958033,"persisted":1709249767979},"797785c7-035a-4ea2-b69b-d595cf48b49c":{"identifier":{"key":"797785c7-035a-4ea2-b69b-d595cf48b49c","namespace":""},"name":"Test LAD Table","type":"LadTable","composition":[{"key":"~myproject~A","namespace":"taxonomy"},{"key":"~myproject~ADCS_Error_Flag","namespace":"taxonomy"},{"key":"~myproject~Battery1_Temp","namespace":"taxonomy"},{"key":"~myproject~Battery1_Voltage","namespace":"taxonomy"},{"key":"~myproject~Battery2_Temp","namespace":"taxonomy"},{"key":"~myproject~Battery2_Voltage","namespace":"taxonomy"},{"key":"~myproject~CCSDS_Packet_Length","namespace":"taxonomy"},{"key":"~myproject~CDHS_Error_Flag","namespace":"taxonomy"},{"key":"~myproject~CDHS_Status","namespace":"taxonomy"},{"key":"~myproject~COMMS_Error_Flag","namespace":"taxonomy"},{"key":"~myproject~COMMS_Status","namespace":"taxonomy"},{"key":"~myproject~Contact_Golbasi_GS","namespace":"taxonomy"},{"key":"~myproject~Contact_Svalbard","namespace":"taxonomy"},{"key":"~myproject~Detector_Temp","namespace":"taxonomy"},{"key":"~myproject~ElapsedSeconds","namespace":"taxonomy"},{"key":"~myproject~EpochUSNO","namespace":"taxonomy"},{"key":"~myproject~Enum_Para_1","namespace":"taxonomy"},{"key":"~myproject~Enum_Para_2","namespace":"taxonomy"},{"key":"~myproject~Enum_Para_3","namespace":"taxonomy"},{"key":"~myproject~EPS_Error_Flag","namespace":"taxonomy"},{"key":"~myproject~Gyro.x","namespace":"taxonomy"},{"key":"~myproject~Gyro.y","namespace":"taxonomy"},{"key":"~myproject~Gyro.z","namespace":"taxonomy"},{"key":"~myproject~Height","namespace":"taxonomy"},{"key":"~myproject~Latitude","namespace":"taxonomy"},{"key":"~myproject~Longitude","namespace":"taxonomy"},{"key":"~myproject~Magnetometer.x","namespace":"taxonomy"},{"key":"~myproject~Magnetometer.y","namespace":"taxonomy"},{"key":"~myproject~Magnetometer.z","namespace":"taxonomy"},{"key":"~myproject~Mode_Day","namespace":"taxonomy"},{"key":"~myproject~Mode_Night","namespace":"taxonomy"},{"key":"~myproject~Mode_Payload","namespace":"taxonomy"},{"key":"~myproject~Mode_Safe","namespace":"taxonomy"},{"key":"~myproject~Mode_SBand","namespace":"taxonomy"},{"key":"~myproject~Mode_XBand","namespace":"taxonomy"},{"key":"~myproject~OrbitNumberCumulative","namespace":"taxonomy"},{"key":"~myproject~Payload_Error_Flag","namespace":"taxonomy"},{"key":"~myproject~Payload_Status","namespace":"taxonomy"},{"key":"~myproject~Position.x","namespace":"taxonomy"},{"key":"~myproject~Position.y","namespace":"taxonomy"},{"key":"~myproject~Position.z","namespace":"taxonomy"},{"key":"~myproject~Shadow","namespace":"taxonomy"},{"key":"~myproject~Velocity.x","namespace":"taxonomy"},{"key":"~myproject~Velocity.y","namespace":"taxonomy"},{"key":"~myproject~Velocity.z","namespace":"taxonomy"},{"key":"~myproject~Sunsensor","namespace":"taxonomy"}],"modifiedBy":"guest","createdBy":"guest","created":1701396684986,"location":"c965501f-86c7-4d63-8857-95618462ea2d","modified":1708721945970,"persisted":1708721945972},"1816debc-38b6-4680-8019-701689be4fc5":{"identifier":{"key":"1816debc-38b6-4680-8019-701689be4fc5","namespace":""},"type":"telemetry.plot.overlay","composition":[{"key":"~myproject~Magnetometer.x","namespace":"taxonomy"}],"configuration":{"series":[{"identifier":{"key":"~myproject~Magnetometer.x","namespace":"taxonomy"}}]},"name":"Magnetometer.x","location":"c965501f-86c7-4d63-8857-95618462ea2d","modified":1708720530618,"modifiedBy":"guest","createdBy":"guest","created":1708720440462,"persisted":1708720530618},"cc400028-b156-43cc-8bae-de13171d5431":{"identifier":{"key":"cc400028-b156-43cc-8bae-de13171d5431","namespace":""},"type":"telemetry.plot.overlay","composition":[{"key":"~myproject~Magnetometer.y","namespace":"taxonomy"}],"configuration":{"series":[{"identifier":{"key":"~myproject~Magnetometer.y","namespace":"taxonomy"}}]},"name":"Magnetometer.y","location":"c965501f-86c7-4d63-8857-95618462ea2d","modified":1708720530625,"modifiedBy":"guest","createdBy":"guest","created":1708720446196,"persisted":1708720530625},"798e729f-420c-45f6-aecf-5bbc44b55dad":{"identifier":{"key":"798e729f-420c-45f6-aecf-5bbc44b55dad","namespace":""},"type":"telemetry.plot.overlay","composition":[{"key":"~myproject~Magnetometer.z","namespace":"taxonomy"}],"configuration":{"series":[{"identifier":{"key":"~myproject~Magnetometer.z","namespace":"taxonomy"}}]},"name":"Magnetometer.z","location":"c965501f-86c7-4d63-8857-95618462ea2d","modified":1708720530631,"modifiedBy":"guest","createdBy":"guest","created":1708720452504,"persisted":1708720530631},"fada1f9c-668d-49c4-a653-81bb3d466922":{"identifier":{"key":"fada1f9c-668d-49c4-a653-81bb3d466922","namespace":""},"type":"telemetry.plot.overlay","composition":[{"key":"~myproject~Position.x","namespace":"taxonomy"}],"configuration":{"series":[{"identifier":{"key":"~myproject~Position.x","namespace":"taxonomy"}}]},"name":"Position.x","location":"c965501f-86c7-4d63-8857-95618462ea2d","modified":1708720530637,"modifiedBy":"guest","createdBy":"guest","created":1708720459109,"persisted":1708720530637},"c3eb99c4-db3c-4a92-8967-3b3052efd0cf":{"identifier":{"key":"c3eb99c4-db3c-4a92-8967-3b3052efd0cf","namespace":""},"type":"telemetry.plot.overlay","composition":[{"key":"~myproject~Position.y","namespace":"taxonomy"}],"configuration":{"series":[{"identifier":{"key":"~myproject~Position.y","namespace":"taxonomy"}}]},"name":"Position.y","location":"c965501f-86c7-4d63-8857-95618462ea2d","modified":1708720530643,"modifiedBy":"guest","createdBy":"guest","created":1708720465842,"persisted":1708720530643},"0dd60b42-66a3-4326-b829-2cde71025d32":{"identifier":{"key":"0dd60b42-66a3-4326-b829-2cde71025d32","namespace":""},"type":"telemetry.plot.overlay","composition":[{"key":"~myproject~Position.z","namespace":"taxonomy"}],"configuration":{"series":[{"identifier":{"key":"~myproject~Position.z","namespace":"taxonomy"}}]},"name":"Position.z","location":"c965501f-86c7-4d63-8857-95618462ea2d","modified":1708720530649,"modifiedBy":"guest","createdBy":"guest","created":1708720471945,"persisted":1708720530649},"eeda19bc-6302-47b6-b115-81204b16aaa3":{"identifier":{"key":"eeda19bc-6302-47b6-b115-81204b16aaa3","namespace":""},"type":"telemetry.plot.overlay","composition":[{"key":"~myproject~Velocity.x","namespace":"taxonomy"}],"configuration":{"series":[{"identifier":{"key":"~myproject~Velocity.x","namespace":"taxonomy"}}]},"name":"Velocity.x","location":"c965501f-86c7-4d63-8857-95618462ea2d","modified":1708720530655,"modifiedBy":"guest","createdBy":"guest","created":1708720478416,"persisted":1708720530655},"3093a1ef-ac30-4ac3-abff-e4ef5ca858e6":{"identifier":{"key":"3093a1ef-ac30-4ac3-abff-e4ef5ca858e6","namespace":""},"type":"telemetry.plot.overlay","composition":[{"key":"~myproject~Velocity.y","namespace":"taxonomy"}],"configuration":{"series":[{"identifier":{"key":"~myproject~Velocity.y","namespace":"taxonomy"}}]},"name":"Velocity.y","location":"c965501f-86c7-4d63-8857-95618462ea2d","modified":1708720530661,"modifiedBy":"guest","createdBy":"guest","created":1708720484585,"persisted":1708720530661},"e4b66733-2e89-4bf8-8a12-5ef3f247e4ec":{"identifier":{"key":"e4b66733-2e89-4bf8-8a12-5ef3f247e4ec","namespace":""},"type":"telemetry.plot.overlay","composition":[{"key":"~myproject~Velocity.z","namespace":"taxonomy"}],"configuration":{"series":[{"identifier":{"key":"~myproject~Velocity.z","namespace":"taxonomy"}}]},"name":"Velocity.z","location":"c965501f-86c7-4d63-8857-95618462ea2d","modified":1708720530667,"modifiedBy":"guest","createdBy":"guest","created":1708720489953,"persisted":1708720530667},"a91d265d-b36d-41fe-9d11-b799e52036ed":{"identifier":{"key":"a91d265d-b36d-41fe-9d11-b799e52036ed","namespace":""},"type":"telemetry.plot.overlay","composition":[{"key":"~myproject~Battery1_Temp","namespace":"taxonomy"}],"configuration":{"series":[{"identifier":{"key":"~myproject~Battery1_Temp","namespace":"taxonomy"}}]},"name":"Battery1_Temp","location":"c965501f-86c7-4d63-8857-95618462ea2d","modified":1708720829096,"modifiedBy":"guest","createdBy":"guest","created":1708720730175,"persisted":1708720829096},"739c26b2-776a-4abb-949f-24a2066aee80":{"identifier":{"key":"739c26b2-776a-4abb-949f-24a2066aee80","namespace":""},"type":"telemetry.plot.overlay","composition":[{"key":"~myproject~Battery1_Voltage","namespace":"taxonomy"}],"configuration":{"series":[{"identifier":{"key":"~myproject~Battery1_Voltage","namespace":"taxonomy"}}]},"name":"Battery1_Voltage","location":"c965501f-86c7-4d63-8857-95618462ea2d","modified":1708720829103,"modifiedBy":"guest","createdBy":"guest","created":1708720736954,"persisted":1708720829103},"94997ceb-00c8-4702-af70-afb014cdf629":{"identifier":{"key":"94997ceb-00c8-4702-af70-afb014cdf629","namespace":""},"type":"telemetry.plot.overlay","composition":[{"key":"~myproject~Battery2_Temp","namespace":"taxonomy"}],"configuration":{"series":[{"identifier":{"key":"~myproject~Battery2_Temp","namespace":"taxonomy"}}]},"name":"Battery2_Temp","location":"c965501f-86c7-4d63-8857-95618462ea2d","modified":1708720829110,"modifiedBy":"guest","createdBy":"guest","created":1708720741955,"persisted":1708720829110},"803121a1-d187-489f-a124-5e903657bedb":{"identifier":{"key":"803121a1-d187-489f-a124-5e903657bedb","namespace":""},"type":"telemetry.plot.overlay","composition":[{"key":"~myproject~Battery2_Voltage","namespace":"taxonomy"}],"configuration":{"series":[{"identifier":{"key":"~myproject~Battery2_Voltage","namespace":"taxonomy"}}]},"name":"Battery2_Voltage","location":"c965501f-86c7-4d63-8857-95618462ea2d","modified":1708720829117,"modifiedBy":"guest","createdBy":"guest","created":1708720755105,"persisted":1708720829117},"c984a025-e4a8-4c25-9b0e-1723b5a30303":{"identifier":{"key":"c984a025-e4a8-4c25-9b0e-1723b5a30303","namespace":""},"type":"telemetry.plot.overlay","composition":[{"key":"~myproject~ElapsedSeconds","namespace":"taxonomy"}],"configuration":{"series":[{"identifier":{"key":"~myproject~ElapsedSeconds","namespace":"taxonomy"}}]},"name":"ElapsedSeconds","location":"c965501f-86c7-4d63-8857-95618462ea2d","modified":1708720829124,"modifiedBy":"guest","createdBy":"guest","created":1708720769807,"persisted":1708720829124},"95cac0f8-cc42-4357-aac8-77d7ea5e31c9":{"identifier":{"key":"95cac0f8-cc42-4357-aac8-77d7ea5e31c9","namespace":""},"type":"telemetry.plot.overlay","composition":[{"key":"~myproject~Gyro.x","namespace":"taxonomy"}],"configuration":{"series":[{"identifier":{"key":"~myproject~Gyro.x","namespace":"taxonomy"}}]},"name":"Gyro.x","location":"c965501f-86c7-4d63-8857-95618462ea2d","modified":1708720829132,"modifiedBy":"guest","createdBy":"guest","created":1708720777813,"persisted":1708720829132},"1011058f-8dec-4426-87d9-58135f663ea6":{"identifier":{"key":"1011058f-8dec-4426-87d9-58135f663ea6","namespace":""},"type":"telemetry.plot.overlay","composition":[{"key":"~myproject~Gyro.y","namespace":"taxonomy"}],"configuration":{"series":[{"identifier":{"key":"~myproject~Gyro.y","namespace":"taxonomy"}}]},"name":"Gyro.y","location":"c965501f-86c7-4d63-8857-95618462ea2d","modified":1708720829138,"modifiedBy":"guest","createdBy":"guest","created":1708720785052,"persisted":1708720829138},"c1881c9a-a638-4bbe-b320-a0ea5d7f4e4f":{"identifier":{"key":"c1881c9a-a638-4bbe-b320-a0ea5d7f4e4f","namespace":""},"type":"telemetry.plot.overlay","composition":[{"key":"~myproject~Gyro.z","namespace":"taxonomy"}],"configuration":{"series":[{"identifier":{"key":"~myproject~Gyro.z","namespace":"taxonomy"}}]},"name":"Gyro.z","location":"c965501f-86c7-4d63-8857-95618462ea2d","modified":1708720829144,"modifiedBy":"guest","createdBy":"guest","created":1708720792578,"persisted":1708720829144},"d510bf8a-f03d-4cac-b58c-1242cc039b22":{"identifier":{"key":"d510bf8a-f03d-4cac-b58c-1242cc039b22","namespace":""},"type":"telemetry.plot.overlay","composition":[{"key":"~myproject~Latitude","namespace":"taxonomy"}],"configuration":{"series":[{"identifier":{"key":"~myproject~Latitude","namespace":"taxonomy"}}]},"name":"Latitude","location":"c965501f-86c7-4d63-8857-95618462ea2d","modified":1708720829150,"modifiedBy":"guest","createdBy":"guest","created":1708720799474,"persisted":1708720829150},"a211ae9a-f013-4a97-87ed-401baee94a3e":{"identifier":{"key":"a211ae9a-f013-4a97-87ed-401baee94a3e","namespace":""},"name":"Battery1_Temp","type":"gauge","composition":[{"key":"~myproject~Battery1_Temp","namespace":"taxonomy"}],"configuration":{"gaugeController":{"gaugeType":"dial-filled","isDisplayMinMax":true,"isDisplayCurVal":true,"isDisplayUnits":true,"isUseTelemetryLimits":true,"limitLow":10,"limitHigh":90,"max":100,"min":0,"precision":2}},"modified":1709078820625,"location":"c965501f-86c7-4d63-8857-95618462ea2d","modifiedBy":"guest","createdBy":"guest","created":1708720863228,"persisted":1709078820625},"f051b323-d22a-419a-a6e6-42ea6041b238":{"identifier":{"key":"f051b323-d22a-419a-a6e6-42ea6041b238","namespace":""},"name":"Battery1_Voltage","type":"gauge","composition":[{"key":"~myproject~Battery1_Voltage","namespace":"taxonomy"}],"configuration":{"gaugeController":{"gaugeType":"dial-filled","isDisplayMinMax":true,"isDisplayCurVal":true,"isDisplayUnits":true,"isUseTelemetryLimits":true,"limitLow":10,"limitHigh":90,"max":100,"min":0,"precision":2}},"modified":1709078831357,"location":"c965501f-86c7-4d63-8857-95618462ea2d","modifiedBy":"guest","createdBy":"guest","created":1708720957879,"persisted":1709078831358},"77ce4615-53d4-4ae7-809c-8535a57a041b":{"identifier":{"key":"77ce4615-53d4-4ae7-809c-8535a57a041b","namespace":""},"name":"Battery2_Temp","type":"gauge","composition":[{"key":"~myproject~Battery2_Temp","namespace":"taxonomy"}],"configuration":{"gaugeController":{"gaugeType":"dial-filled","isDisplayMinMax":true,"isDisplayCurVal":true,"isDisplayUnits":true,"isUseTelemetryLimits":true,"limitLow":10,"limitHigh":90,"max":100,"min":0,"precision":2}},"modified":1709078840709,"location":"c965501f-86c7-4d63-8857-95618462ea2d","modifiedBy":"guest","createdBy":"guest","created":1708720982313,"persisted":1709078840709},"b425c7bd-6912-4e40-908e-6a21c73c7db3":{"identifier":{"key":"b425c7bd-6912-4e40-908e-6a21c73c7db3","namespace":""},"name":"Battery2_Voltage","type":"gauge","composition":[{"key":"~myproject~Battery2_Voltage","namespace":"taxonomy"}],"configuration":{"gaugeController":{"gaugeType":"dial-filled","isDisplayMinMax":true,"isDisplayCurVal":true,"isDisplayUnits":true,"isUseTelemetryLimits":true,"limitLow":10,"limitHigh":90,"max":100,"min":0,"precision":2}},"modified":1709078853693,"location":"c965501f-86c7-4d63-8857-95618462ea2d","modifiedBy":"guest","createdBy":"guest","created":1708721005947,"persisted":1709078853693}},"rootId":"c965501f-86c7-4d63-8857-95618462ea2d"} \ No newline at end of file diff --git a/tests/git-opensource-tests.sh b/tests/git-opensource-tests.sh index 419bbbf5..9c962a6f 100644 --- a/tests/git-opensource-tests.sh +++ b/tests/git-opensource-tests.sh @@ -21,6 +21,7 @@ REPO_URL=https://github.com/nasa/openmct.git REPO_PATH=e2e LOCAL_REPO_ROOT="e2e/opensource" +# remove the branch later git clone --no-checkout --depth 1 $REPO_URL "$LOCAL_REPO_ROOT" cd "$LOCAL_REPO_ROOT" git config core.sparsecheckout true @@ -30,6 +31,10 @@ git read-tree -m -u HEAD # moving back to /tests/ dir cd .. +# Move index.js to root +mv opensource/e2e/index.js ./opensource +# Move package.json, package-lock.json +mv opensource/e2e/package*.json ./opensource # Move fixtures and appActions mv opensource/e2e/*Fixtures.js ./opensource mv opensource/e2e/appActions.js ./opensource From eb65f10b4ea37076c98116003ec1d7ca5ce196e4 Mon Sep 17 00:00:00 2001 From: John Hill Date: Tue, 17 Sep 2024 12:43:16 -0700 Subject: [PATCH 6/9] Add makefile for quickly reviewing tests --- .gitignore | 2 ++ Makefile | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 9 +++++---- 3 files changed, 61 insertions(+), 4 deletions(-) create mode 100644 Makefile diff --git a/.gitignore b/.gitignore index 5f9eecfc..af4685d1 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,5 @@ test-results # Misc .DS_Store .vscode/settings.json + +quickstart/ diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..5f74e368 --- /dev/null +++ b/Makefile @@ -0,0 +1,54 @@ +.PHONY: all clone-quickstart setup-quickstart setup-openmct-yamcs sanity-test build-example test-getopensource test-e2e clean + +all: clone-quickstart setup-quickstart setup-openmct-yamcs sanity-test build-example test-getopensource test-e2e + +clone-quickstart: + @echo "Running target: clone-quickstart" + @echo "Current working directory: $(shell pwd)" + if [ ! -d "quickstart" ]; then \ + git clone https://github.com/yamcs/quickstart; \ + else \ + echo "Directory 'quickstart' already exists."; \ + fi + +setup-quickstart: + @echo "Running target: setup-quickstart" + cd quickstart/docker && make wait-for-sent + +setup-openmct-yamcs: + @echo "Running target: setup-openmct-yamcs" + npm install + +sanity-test: + @echo "Running target: sanity-test" + npm run wait-for-yamcs + +build-example: + @echo "Running target: build-example" + @current_branch=$(shell git rev-parse --abbrev-ref HEAD) + @echo "Current branch of openmct-yamcs: $$current_branch checking if it exists in openmct repository" + @if git ls-remote --exit-code --heads https://github.com/nasa/openmct.git refs/heads/$$current_branch; then \ + echo "Branch $$current_branch exists in openmct repository. Running build:example:current"; \ + npm run build:example:current || { echo "Failed to run build:example:current"; exit 1; }; \ + else \ + echo "Branch $$current_branch does not exist in openmct repository. Running build:example:master"; \ + npm run build:example:master || { echo "Failed to run build:example:master"; exit 1; }; \ + fi + +test-getopensource: + @echo "Running target: test-getopensource" + npm run test:getopensource + +test-e2e: + @echo "Running target: test-e2e" + npm run test:e2e:quickstart:local + +clean: + @echo "Running target: clean" + npm run clean + if [ -d "quickstart" ]; then \ + rm -rf quickstart; \ + echo "Removed 'quickstart' directory."; \ + else \ + echo "Directory 'quickstart' does not exist."; \ + fi diff --git a/package.json b/package.json index acf02948..29b7c982 100644 --- a/package.json +++ b/package.json @@ -20,10 +20,11 @@ "lint:fix": "eslint src example --fix", "build:dist": "webpack --config ./.webpack/webpack.prod.mjs", "build:example": "npm install openmct@unstable --no-save", - "build:example:master": "npm install nasa/openmct --no-save", - "postbuild:example": "node check-optional-dependencies.mjs", - "start": "npx webpack serve --config ./.webpack/webpack.dev.mjs", - "start:coverage": "npx webpack serve --config ./.webpack/webpack.coverage.mjs", + "build:example:master": "npm install nasa/openmct#master --no-save", + "build:example:current": "npm install nasa/openmct#$(git rev-parse --abbrev-ref HEAD) --no-save --verbose", + "postbuild:example": "node check-optional-dependencies.js", + "start": "npx webpack serve --config ./.webpack/webpack.dev.js", + "start:coverage": "npx webpack serve --config ./.webpack/webpack.coverage.js", "prepare": "npm run build:dist", "test:getopensource": "sh ./tests/git-opensource-tests.sh", "posttest:getopensource": "npm install", From c90d4937b352779035d53330d0479b581c7134ac Mon Sep 17 00:00:00 2001 From: John Hill Date: Wed, 18 Sep 2024 09:20:12 -0700 Subject: [PATCH 7/9] updates --- Makefile | 29 +++-- package.json | 14 +-- ...pec.js => quickstartFixtures.e2e.spec.mjs} | 0 ...ig.js => playwright-quickstart.config.mjs} | 6 +- ...tartFixtures.js => quickstartFixtures.mjs} | 2 +- ...tions.e2e.spec.js => actions.e2e.spec.mjs} | 107 +++++++++++++----- ...aleness.e2e.mjs => staleness.e2e.spec.mjs} | 0 .../{tabs.e2e.spec.js => tabs.e2e.spec.mjs} | 2 +- 8 files changed, 108 insertions(+), 52 deletions(-) rename tests/e2e/framework/{quickstartFixtures.e2e.spec.js => quickstartFixtures.e2e.spec.mjs} (100%) rename tests/e2e/{playwright-quickstart.config.js => playwright-quickstart.config.mjs} (91%) rename tests/e2e/{quickstartFixtures.js => quickstartFixtures.mjs} (96%) rename tests/e2e/yamcs/{actions.e2e.spec.js => actions.e2e.spec.mjs} (64%) rename tests/e2e/yamcs/{staleness.e2e.mjs => staleness.e2e.spec.mjs} (100%) rename tests/e2e/yamcs/{tabs.e2e.spec.js => tabs.e2e.spec.mjs} (99%) diff --git a/Makefile b/Makefile index 5f74e368..ad370f14 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ -.PHONY: all clone-quickstart setup-quickstart setup-openmct-yamcs sanity-test build-example test-getopensource test-e2e clean +.PHONY: all clone-quickstart install-quickstart start-quickstart install-openmct-yamcs sanity-test build-example test-getopensource test-e2e clean -all: clone-quickstart setup-quickstart setup-openmct-yamcs sanity-test build-example test-getopensource test-e2e +all: clone-quickstart install-quickstart install-openmct-yamcs sanity-test build-example test-getopensource test-e2e clone-quickstart: @echo "Running target: clone-quickstart" @@ -11,12 +11,22 @@ clone-quickstart: echo "Directory 'quickstart' already exists."; \ fi -setup-quickstart: - @echo "Running target: setup-quickstart" - cd quickstart/docker && make wait-for-sent +install-quickstart: + @echo "Running target: install-quickstart" + @cd quickstart/docker && $(MAKE) wait-for-sent -setup-openmct-yamcs: - @echo "Running target: setup-openmct-yamcs" +start-quickstart: + @echo "Running target: start-quickstart" + @cd quickstart/docker && $(MAKE) all + +restart-quickstart: + @echo "Running target: reset-quickstart" + @cd quickstart/docker && $(MAKE) yamcs-down + @cd quickstart/docker && $(MAKE) simulator-down + @cd quickstart/docker && $(MAKE) all + +install-openmct-yamcs: + @echo "Running target: install-openmct-yamcs" npm install sanity-test: @@ -35,12 +45,9 @@ build-example: npm run build:example:master || { echo "Failed to run build:example:master"; exit 1; }; \ fi -test-getopensource: - @echo "Running target: test-getopensource" - npm run test:getopensource - test-e2e: @echo "Running target: test-e2e" + npm run test:getopensource npm run test:e2e:quickstart:local clean: diff --git a/package.json b/package.json index 29b7c982..3095a00d 100644 --- a/package.json +++ b/package.json @@ -22,16 +22,16 @@ "build:example": "npm install openmct@unstable --no-save", "build:example:master": "npm install nasa/openmct#master --no-save", "build:example:current": "npm install nasa/openmct#$(git rev-parse --abbrev-ref HEAD) --no-save --verbose", - "postbuild:example": "node check-optional-dependencies.js", - "start": "npx webpack serve --config ./.webpack/webpack.dev.js", - "start:coverage": "npx webpack serve --config ./.webpack/webpack.coverage.js", + "postbuild:example": "node check-optional-dependencies.mjs", + "start": "npx webpack serve --config ./.webpack/webpack.dev.mjs", + "start:coverage": "npx webpack serve --config ./.webpack/webpack.coverage.mjs", "prepare": "npm run build:dist", "test:getopensource": "sh ./tests/git-opensource-tests.sh", "posttest:getopensource": "npm install", - "test:e2e:smoke": "npm test --workspace tests/e2e/opensource -- --config=../playwright-quickstart.config.js --project=chromium quickstartSmoke", - "test:e2e:quickstart": "npm test --workspace tests/e2e/opensource -- --config=../playwright-quickstart.config.js --project=chromium tests/e2e/yamcs/", - "test:e2e:quickstart:local": "npm test --workspace tests/e2e/opensource -- --config=../playwright-quickstart.config.js --project=local-chrome tests/e2e/yamcs/", - "test:e2e:watch": "npm test --workspace tests/e2e/opensource -- --ui --config=../playwright-quickstart.config.js", + "test:e2e:smoke": "npm test --workspace tests/e2e/opensource -- --config=../playwright-quickstart.config.mjs --project=chromium quickstartSmoke", + "test:e2e:quickstart": "npm test --workspace tests/e2e/opensource -- --config=../playwright-quickstart.config.mjs --project=chromium tests/e2e/yamcs/", + "test:e2e:quickstart:local": "npm test --workspace tests/e2e/opensource -- --config=../playwright-quickstart.config.mjs --project=local-chrome tests/e2e/yamcs/", + "test:e2e:watch": "npm test --workspace tests/e2e/opensource -- --ui --config=../playwright-quickstart.config.mjs", "wait-for-yamcs": "wait-on http-get://localhost:8090/ -v" }, "keywords": [ diff --git a/tests/e2e/framework/quickstartFixtures.e2e.spec.js b/tests/e2e/framework/quickstartFixtures.e2e.spec.mjs similarity index 100% rename from tests/e2e/framework/quickstartFixtures.e2e.spec.js rename to tests/e2e/framework/quickstartFixtures.e2e.spec.mjs diff --git a/tests/e2e/playwright-quickstart.config.js b/tests/e2e/playwright-quickstart.config.mjs similarity index 91% rename from tests/e2e/playwright-quickstart.config.js rename to tests/e2e/playwright-quickstart.config.mjs index 525f359f..8c6b3f51 100644 --- a/tests/e2e/playwright-quickstart.config.js +++ b/tests/e2e/playwright-quickstart.config.mjs @@ -15,7 +15,7 @@ const config = { baseURL: 'http://localhost:9000/#', ignoreHTTPSErrors: true, myItemsFolderName: "My Items", - failOnConsoleError: false + failOnConsoleError: true }, webServer: { cwd: '../', @@ -28,7 +28,7 @@ const config = { projects: [ { name: "chromium", - grepInvert: /@unstable|@snapshot|@localStorage|@addInit/, + grepInvert: /@snapshot|@localStorage|@addInit/, use: { browserName: 'chromium', headless: true, @@ -40,7 +40,7 @@ const config = { // -- Local Browsers -- { name: "local-chrome", - grepInvert: /@unstable|@snapshot|@localStorage|@addInit/, + grepInvert: /@snapshot|@localStorage|@addInit/, use: { browserName: 'chromium', channel: 'chrome' diff --git a/tests/e2e/quickstartFixtures.js b/tests/e2e/quickstartFixtures.mjs similarity index 96% rename from tests/e2e/quickstartFixtures.js rename to tests/e2e/quickstartFixtures.mjs index 31ccf0b5..2b1ba290 100644 --- a/tests/e2e/quickstartFixtures.js +++ b/tests/e2e/quickstartFixtures.mjs @@ -26,7 +26,7 @@ */ // import { createDomainObjectWithDefaults } from './appActions.js'; -import { test, request, expect } from './opensource/pluginFixtures.js'; +const { test, request, expect } = await import('./opensource/pluginFixtures.js'); /** * The name of the "My Items" folder in the domain object tree. diff --git a/tests/e2e/yamcs/actions.e2e.spec.js b/tests/e2e/yamcs/actions.e2e.spec.mjs similarity index 64% rename from tests/e2e/yamcs/actions.e2e.spec.js rename to tests/e2e/yamcs/actions.e2e.spec.mjs index b6660271..01ae3d2b 100644 --- a/tests/e2e/yamcs/actions.e2e.spec.js +++ b/tests/e2e/yamcs/actions.e2e.spec.mjs @@ -24,23 +24,23 @@ * This suite verifies the network requests made by the application to ensure correct interaction with YAMCS. */ -import { test, expect, filterNonFetchRequests } from '../quickstartFixtures.js'; +import { test, expect, filterNonFetchRequests } from '../quickstartFixtures.mjs'; import { createDomainObjectWithDefaults, setFixedTimeMode } from '../opensource/appActions.js'; test.describe('Reload action', () => { - let displayLayout; - let batchGet; - let batchGet2; - let battery1tempRequest; - let battery1tempRequestCont; - let battery1voltageRequest; - let battery1voltageRequestCont; - let allNetworkRequests = []; + let displayLayout; + let batchGetResponse; + let battery1tempResponse; + let battery1tempResponseCont; + let battery1voltageResponse; + let battery1voltageResponeCont; + let allNetworkRequests = []; test.beforeEach(async ({ page }) => { await page.goto('./', { waitUntil: 'domcontentloaded' }); displayLayout = await createDomainObjectWithDefaults(page, { - type: 'Display Layout' + type: 'Display Layout', + name: 'Display Layout' }); await createDomainObjectWithDefaults(page, { @@ -48,7 +48,7 @@ test.describe('Reload action', () => { name: 'Alpha Table' }); - //Expand the quickstart myproject twice + //Expand the quickstart myproject twice to get to the telemetry in the tree await page.getByLabel('Expand myproject folder').click(); await page.getByLabel('Expand myproject folder').click(); @@ -74,26 +74,32 @@ test.describe('Reload action', () => { await page.getByLabel('Edit Object', { exact: true }).click(); await page.getByLabel('Collapse myproject folder').first().click(); await page.getByLabel('Expand My Items folder').click(); - await page.dragAndDrop(`text='Alpha Table'`, '.l-layout__grid-holder', { + + await page.getByLabel('Preview Alpha Table table') + .dragTo(page.getByLabel('Display Layout Layout Grid').locator('div').nth(1), { targetPosition: { x: 0, y: 0 } }); - await page.dragAndDrop(`text='Beta Table'`, '.l-layout__grid-holder', { + + await page.getByLabel('Preview Beta Table table') + .dragTo(page.getByLabel('Display Layout Layout Grid').locator('div').nth(1), { targetPosition: { x: 0, y: 250 } }); - await page.locator('button[title="Save"]').click(); + + + await page.getByRole('button', { name: 'Save' }).click(); await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); - await page.goto(displayLayout.url, { waitUntil: 'networkidle' }); //Set to 1 Minute to reduce the time the opportunity for paginated data // await page.getByRole('menuitem', { name: /Real-Time/ }).click(); + await page.getByLabel('Start offset: 00:30:00').click(); await page.getByLabel('Start offset minutes').fill('1'); await page.getByLabel('Submit time offsets').click(); await page.waitForLoadState('networkidle'); }); - test.only('can reload display layout and its telemetry table children', async ({ page }) => { + test('can reload display layout and its telemetry table children', async ({ page }) => { // Listening for all network requests and pushing them into allNetworkRequests array. page.on('request', request => allNetworkRequests.push(request)); @@ -112,17 +118,57 @@ test.describe('Reload action', () => { await page.waitForLoadState('networkidle'); allNetworkRequests = []; - battery1tempRequest = page.waitForResponse('**/api/archive/myproject/parameters/myproject/Battery1_Temp**') - battery1tempRequestCont = page.waitForResponse('**/api/archive/myproject/parameters/myproject/Battery1_Temp**') - batchGet = page.waitForResponse('**/api/processors/myproject/realtime/parameters:batchGet'); + //Create response promises + battery1tempResponse = page.waitForResponse('**/api/archive/myproject/parameters/myproject/Battery1_Temp**') + batchGetResponse = page.waitForResponse('**/api/processors/myproject/realtime/parameters:batchGet'); + + //Intercept the request to /api/archive/myproject/parameters/myproject/Battery1_Temp + await page.route('**/api/archive/myproject/parameters/myproject/Battery1_Temp', async route => { + const response = await route.fetch(); + const json = await response.json(); + + // Replace every "floatValue" with 1337 + json.parameter.forEach(param => { + if (param.rawValue && param.rawValue.type === 'FLOAT') { + param.rawValue.floatValue = 1337; + } + if (param.engValue && param.engValue.type === 'FLOAT') { + param.engValue.floatValue = 1337; + } + }); + + console.log('Request:', JSON.stringify({ + url: route.request().url(), + method: route.request().method(), + headers: route.request().headers(), + postData: route.request().postData() + }, null, 2)); + + console.log('Response:', JSON.stringify({ + status: response.status(), + statusText: response.statusText(), + headers: response.headers(), + body: json + }, null, 2)); + + // Fulfill using the modified JSON object + await route.fulfill({ + status: response.status(), + headers: response.headers(), + contentType: response.headers()['content-type'], + body: JSON.stringify(json) + }); + }); + + //Click on Reload Action on Alpha Table + await page.getByLabel('Alpha Table Frame Controls').getByLabel('View menu items').click(); + await page.getByLabel('Reload').click(); + + await Promise.all([battery1tempResponse, batchGetResponse]); - await page.getByTitle('View menu items').first().click(); - await page.getByRole('menuitem', { name: /Reload/ }).click(); - await Promise.all([battery1tempRequest, battery1tempRequestCont, batchGet]); await page.waitForLoadState('networkidle'); expect(allNetworkRequests.length).toBe(2); - const afterReloadAlphaTelemetryValue = await page .getByLabel('Alpha Table table content') .getByLabel('value table cell') @@ -134,21 +180,24 @@ test.describe('Reload action', () => { .first() .getAttribute('title'); + //After reload, the telemetry value should be different for Alpha Table expect(beforeReloadAlphaTelemetryValue).not.toEqual(afterReloadAlphaTelemetryValue); - expect(beforeReloadBetaTelemetryValue).toEqual(afterReloadBetaTelemetryValue); + + // expect(beforeReloadBetaTelemetryValue).toEqual(afterReloadBetaTelemetryValue); await page.waitForLoadState('networkidle'); allNetworkRequests = []; - battery1tempRequest = page.waitForResponse('**/api/archive/myproject/parameters/myproject/Battery1_Temp**') - battery1voltageRequest = page.waitForResponse('**/api/archive/myproject/parameters/myproject/Battery1_Voltage**') - batchGet = page.waitForResponse('**/api/processors/myproject/realtime/parameters:batchGet'); - batchGet2 = page.waitForResponse('**/api/processors/myproject/realtime/parameters:batchGet'); + battery1tempResponse = page.waitForResponse('**/api/archive/myproject/parameters/myproject/Battery1_Temp**') + battery1voltageResponse = page.waitForResponse('**/api/archive/myproject/parameters/myproject/Battery1_Voltage**') + batchGetResponse = page.waitForResponse('**/api/processors/myproject/realtime/parameters:batchGet'); await page.getByTitle('More actions').click(); await page.getByRole('menuitem', { name: /Reload/ }).click(); - await Promise.all([battery1tempRequest, battery1voltageRequest, batchGet, batchGet2]); + + await Promise.all([battery1tempResponse, battery1voltageResponse, batchGetResponse]); await page.waitForLoadState('networkidle'); + console.log(JSON.stringify(allNetworkRequests)); expect(allNetworkRequests.length).toBe(3); diff --git a/tests/e2e/yamcs/staleness.e2e.mjs b/tests/e2e/yamcs/staleness.e2e.spec.mjs similarity index 100% rename from tests/e2e/yamcs/staleness.e2e.mjs rename to tests/e2e/yamcs/staleness.e2e.spec.mjs diff --git a/tests/e2e/yamcs/tabs.e2e.spec.js b/tests/e2e/yamcs/tabs.e2e.spec.mjs similarity index 99% rename from tests/e2e/yamcs/tabs.e2e.spec.js rename to tests/e2e/yamcs/tabs.e2e.spec.mjs index 512f0e91..7ecc66ac 100644 --- a/tests/e2e/yamcs/tabs.e2e.spec.js +++ b/tests/e2e/yamcs/tabs.e2e.spec.mjs @@ -23,7 +23,7 @@ * Network Specific Tests for Open MCT and YAMCS connectivity with regard to tabs */ -import { test, expect, filterNonFetchRequests } from '../quickstartFixtures.js'; +import { test, expect, filterNonFetchRequests } from '../quickstartFixtures.mjs'; import { createDomainObjectWithDefaults, setFixedTimeMode } from '../opensource/appActions.js'; test.describe('Tabs View', () => { From 84bb8b7247ab456e275fa1e8a999c36db9106fdb Mon Sep 17 00:00:00 2001 From: John Hill Date: Tue, 24 Sep 2024 13:33:23 -0700 Subject: [PATCH 8/9] fix the currentbranch logic --- Makefile | 12 +++++++++--- package.json | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index ad370f14..fb0d40bf 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,8 @@ .PHONY: all clone-quickstart install-quickstart start-quickstart install-openmct-yamcs sanity-test build-example test-getopensource test-e2e clean -all: clone-quickstart install-quickstart install-openmct-yamcs sanity-test build-example test-getopensource test-e2e +all: clone-quickstart install-quickstart install-openmct-yamcs sanity-test build-example test-e2e + +start: clone-quickstart install-quickstart install-openmct-yamcs sanity-test build-example start-openmct clone-quickstart: @echo "Running target: clone-quickstart" @@ -38,13 +40,17 @@ build-example: @current_branch=$(shell git rev-parse --abbrev-ref HEAD) @echo "Current branch of openmct-yamcs: $$current_branch checking if it exists in openmct repository" @if git ls-remote --exit-code --heads https://github.com/nasa/openmct.git refs/heads/$$current_branch; then \ - echo "Branch $$current_branch exists in openmct repository. Running build:example:current"; \ - npm run build:example:current || { echo "Failed to run build:example:current"; exit 1; }; \ + echo "Branch $$current_branch exists in openmct repository. Running build:example:currentbranch"; \ + npm run build:example:currentbranch || { echo "Failed to run build:example:currentbranch"; exit 1; }; \ else \ echo "Branch $$current_branch does not exist in openmct repository. Running build:example:master"; \ npm run build:example:master || { echo "Failed to run build:example:master"; exit 1; }; \ fi +start-openmct: + @echo "Running target: start-openmct" + npm start + test-e2e: @echo "Running target: test-e2e" npm run test:getopensource diff --git a/package.json b/package.json index 3095a00d..a8b1b3fa 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "build:dist": "webpack --config ./.webpack/webpack.prod.mjs", "build:example": "npm install openmct@unstable --no-save", "build:example:master": "npm install nasa/openmct#master --no-save", - "build:example:current": "npm install nasa/openmct#$(git rev-parse --abbrev-ref HEAD) --no-save --verbose", + "build:example:currentbranch": "npm install nasa/openmct#$(git rev-parse --abbrev-ref HEAD) --no-save --verbose", "postbuild:example": "node check-optional-dependencies.mjs", "start": "npx webpack serve --config ./.webpack/webpack.dev.mjs", "start:coverage": "npx webpack serve --config ./.webpack/webpack.coverage.mjs", From b5f9d47915db1bb9bc5261a4308dc47232cd06d2 Mon Sep 17 00:00:00 2001 From: John Hill Date: Tue, 24 Sep 2024 13:36:16 -0700 Subject: [PATCH 9/9] stash some fixes --- tests/e2e/quickstartFixtures.mjs | 4 +- tests/e2e/yamcs/actions.e2e.spec.mjs | 117 +++++++++++++-------------- 2 files changed, 59 insertions(+), 62 deletions(-) diff --git a/tests/e2e/quickstartFixtures.mjs b/tests/e2e/quickstartFixtures.mjs index 2b1ba290..a861445f 100644 --- a/tests/e2e/quickstartFixtures.mjs +++ b/tests/e2e/quickstartFixtures.mjs @@ -51,4 +51,6 @@ export { expect, request, test }; */ export function filterNonFetchRequests(requests) { return requests.filter(request => request.resourceType() === 'fetch'); -} \ No newline at end of file +} + +//get linkZeroTelemetryToCurrentObject \ No newline at end of file diff --git a/tests/e2e/yamcs/actions.e2e.spec.mjs b/tests/e2e/yamcs/actions.e2e.spec.mjs index 01ae3d2b..54b6a3fb 100644 --- a/tests/e2e/yamcs/actions.e2e.spec.mjs +++ b/tests/e2e/yamcs/actions.e2e.spec.mjs @@ -25,7 +25,7 @@ */ import { test, expect, filterNonFetchRequests } from '../quickstartFixtures.mjs'; -import { createDomainObjectWithDefaults, setFixedTimeMode } from '../opensource/appActions.js'; +import { createDomainObjectWithDefaults, setFixedTimeMode, navigateToObjectWithRealTime,navigateToObjectWithFixedTimeBounds } from '../opensource/appActions.js'; test.describe('Reload action', () => { let displayLayout; @@ -45,14 +45,14 @@ test.describe('Reload action', () => { await createDomainObjectWithDefaults(page, { type: 'Telemetry Table', - name: 'Alpha Table' + name: 'Battery1_Temp Table' }); //Expand the quickstart myproject twice to get to the telemetry in the tree await page.getByLabel('Expand myproject folder').click(); await page.getByLabel('Expand myproject folder').click(); - //Add Battery1_Temp to Alpha Table + //Add Battery1_Temp to Battery1_Temp Table await page.getByLabel('Edit Object').click(); await page.getByRole('treeitem', { name: 'Battery1_Temp' }).dragTo(page.locator('.c-table__body-w')); await page.getByRole('button', { name: 'Save' }).click(); @@ -75,7 +75,7 @@ test.describe('Reload action', () => { await page.getByLabel('Collapse myproject folder').first().click(); await page.getByLabel('Expand My Items folder').click(); - await page.getByLabel('Preview Alpha Table table') + await page.getByLabel('Preview Battery1_Temp Table table') .dragTo(page.getByLabel('Display Layout Layout Grid').locator('div').nth(1), { targetPosition: { x: 0, y: 0 } }); @@ -89,41 +89,19 @@ test.describe('Reload action', () => { await page.getByRole('button', { name: 'Save' }).click(); await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); - await page.goto(displayLayout.url, { waitUntil: 'networkidle' }); - //Set to 1 Minute to reduce the time the opportunity for paginated data - // await page.getByRole('menuitem', { name: /Real-Time/ }).click(); - - await page.getByLabel('Start offset: 00:30:00').click(); - await page.getByLabel('Start offset minutes').fill('1'); - await page.getByLabel('Submit time offsets').click(); - await page.waitForLoadState('networkidle'); }); - test('can reload display layout and its telemetry table children', async ({ page }) => { - // Listening for all network requests and pushing them into allNetworkRequests array. - page.on('request', request => allNetworkRequests.push(request)); - - const beforeReloadAlphaTelemetryValue = await page - .getByLabel('Alpha Table table content') - .getByLabel('value table cell') - .first() - .getAttribute('title'); - const beforeReloadBetaTelemetryValue = await page - .getByLabel('Beta Table table content') - .getByLabel('value table cell') - .first() - .getAttribute('title'); - - //reload Alpha Table and verify network correctness - await page.waitForLoadState('networkidle'); - allNetworkRequests = []; - - //Create response promises - battery1tempResponse = page.waitForResponse('**/api/archive/myproject/parameters/myproject/Battery1_Temp**') - batchGetResponse = page.waitForResponse('**/api/processors/myproject/realtime/parameters:batchGet'); - - //Intercept the request to /api/archive/myproject/parameters/myproject/Battery1_Temp - await page.route('**/api/archive/myproject/parameters/myproject/Battery1_Temp', async route => { + test('can reload telemetry table in display layout in real time mode', async ({ page }) => { + /* + * This test will + * 1. Load the display layout in realtime mode with seeded values + * 2. Reload the telemetry table + * 3. Verify that the network requests are correct + * 4. Verify that the telemetry values are different + */ + + //Intercept the request to /api/archive/myproject/parameters/myproject/Battery1_Temp before pageload + await page.route('**/api/archive/myproject/parameters/myproject/Battery1_Temp**', async route => { const response = await route.fetch(); const json = await response.json(); @@ -137,40 +115,49 @@ test.describe('Reload action', () => { } }); - console.log('Request:', JSON.stringify({ - url: route.request().url(), - method: route.request().method(), - headers: route.request().headers(), - postData: route.request().postData() - }, null, 2)); - - console.log('Response:', JSON.stringify({ - status: response.status(), - statusText: response.statusText(), - headers: response.headers(), - body: json - }, null, 2)); - // Fulfill using the modified JSON object await route.fulfill({ - status: response.status(), - headers: response.headers(), - contentType: response.headers()['content-type'], body: JSON.stringify(json) }); }); + //Set to 1 Minute to reduce the time the opportunity for paginated data + await navigateToObjectWithRealTime(page, displayLayout.url, 60000, 0); + + // Listening for all network requests and pushing them into allNetworkRequests array. + page.on('request', request => allNetworkRequests.push(request)); + + const beforeReloadBattery1TempTelemetryValue = await page + .getByLabel('Battery1_Temp Table table content') + .getByLabel('value table cell') + .first() + .getAttribute('title'); + + const beforeReloadBetaTelemetryValue = await page + .getByLabel('Beta Table table content') + .getByLabel('value table cell') + .first() + .getAttribute('title'); + + //reload Battery1_Temp Table and verify network correctness + await page.waitForLoadState('networkidle'); + allNetworkRequests = []; + + //Create response promises + battery1tempResponse = page.waitForResponse('**/api/archive/myproject/parameters/myproject/Battery1_Temp**') + batchGetResponse = page.waitForResponse('**/api/processors/myproject/realtime/parameters:batchGet'); - //Click on Reload Action on Alpha Table - await page.getByLabel('Alpha Table Frame Controls').getByLabel('View menu items').click(); + //Click on Reload Action on Battery1_Temp Table + await page.getByLabel('Battery1_Temp Table Frame Controls').getByLabel('View menu items').click(); await page.getByLabel('Reload').click(); await Promise.all([battery1tempResponse, batchGetResponse]); await page.waitForLoadState('networkidle'); + //Only 2 requests should be made expect(allNetworkRequests.length).toBe(2); const afterReloadAlphaTelemetryValue = await page - .getByLabel('Alpha Table table content') + .getByLabel('Battery1_Temp Table table content') .getByLabel('value table cell') .first() .getAttribute('title'); @@ -180,10 +167,10 @@ test.describe('Reload action', () => { .first() .getAttribute('title'); - //After reload, the telemetry value should be different for Alpha Table - expect(beforeReloadAlphaTelemetryValue).not.toEqual(afterReloadAlphaTelemetryValue); + //After reload, the telemetry value should be different for Battery1_Temp Table + expect(beforeReloadBattery1TempTelemetryValue).not.toEqual(afterReloadAlphaTelemetryValue); - // expect(beforeReloadBetaTelemetryValue).toEqual(afterReloadBetaTelemetryValue); + expect(beforeReloadBetaTelemetryValue).toEqual(afterReloadBetaTelemetryValue); await page.waitForLoadState('networkidle'); allNetworkRequests = []; @@ -196,13 +183,14 @@ test.describe('Reload action', () => { await page.getByRole('menuitem', { name: /Reload/ }).click(); await Promise.all([battery1tempResponse, battery1voltageResponse, batchGetResponse]); + await page.waitForLoadState('networkidle'); console.log(JSON.stringify(allNetworkRequests)); expect(allNetworkRequests.length).toBe(3); const fullReloadAlphaTelemetryValue = await page - .getByLabel('Alpha Table table content') + .getByLabel('Battery1_Temp Table table content') .getByLabel('value table cell') .first() .getAttribute('title'); @@ -215,6 +203,13 @@ test.describe('Reload action', () => { expect(fullReloadAlphaTelemetryValue).not.toEqual(afterReloadAlphaTelemetryValue); expect(fullReloadBetaTelemetryValue).not.toEqual(afterReloadBetaTelemetryValue); }); + test('can reload telemetry table in display layout in fixed time mode', async ({ page }) => { + http://localhost:9000/#/browse/mine/83ae30a5-8d13-4def-881a-920b9fa795c1?tc.mode=local&tc.startDelta=60000&tc.endDelta=0&tc.timeSystem=utc + + + // Switch to fixed time mode with all plan events within the bounds + await navigateToObjectWithFixedTimeBounds(page, displayLayout.url, 60000, endBound); + }); test.fixme('can reload notebook', async ({ page }) => {}); test.fixme('can reload gauge', async ({ page }) => {}); });