diff --git a/.eslintrc.js b/.eslintrc.cjs similarity index 94% rename from .eslintrc.js rename to .eslintrc.cjs index bb8e6131..2ebc9b85 100644 --- a/.eslintrc.js +++ b/.eslintrc.cjs @@ -25,7 +25,7 @@ module.exports = { "browser": true, "es6": true, "jasmine": true, - "amd": true + "amd": false }, "extends": "eslint:recommended", "parser": "@babel/eslint-parser", @@ -37,7 +37,16 @@ module.exports = { "impliedStrict": true } }, + "plugins": ['import'], "rules": { + 'import/no-amd': 'error', + 'import/no-commonjs': 'error', + 'import/named': 'error', + 'import/no-webpack-loader-syntax': 'error', + 'import/first': 'error', + 'import/no-import-module-exports': 'error', + 'import/no-mutable-exports': 'error', + 'import/no-unused-modules': 'error', "no-bitwise": "error", "curly": "error", "eqeqeq": "error", @@ -237,6 +246,12 @@ module.exports = { "varsIgnorePattern": "controller" } ] + } + }, + { + "files": ['*.eslintrc.cjs'], + "env": { + "node": true } } ] 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 ed538e79..6dca9e9b 100644 --- a/src/providers/object-provider.js +++ b/src/providers/object-provider.js @@ -27,19 +27,19 @@ import { getLimitOverrides } from '../utils.js'; -import { OBJECT_TYPES, NAMESPACE } from '../const'; -import OperatorStatusParameter from './user/operator-status-parameter.js'; +import { OBJECT_TYPES, NAMESPACE } from '../const.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 e9f34d93..6820740e 100644 --- a/src/providers/realtime-provider.js +++ b/src/providers/realtime-provider.js @@ -20,29 +20,36 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import * as MESSAGES from './messages'; +import { SUBSCRIBE, UNSUBSCRIBE } from './messages.js'; import { OBJECT_TYPES, DATA_TYPES, - AGGREGATE_TYPE, METADATA_TIME_KEY, STALENESS_STATUS_MAP, - MDB_OBJECT -} from '../const'; + 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'; -import { eventToTelemetryDatum, eventShouldBeFiltered } from './events'; +import { commandToTelemetryDatum } from './commands.js'; +import { eventToTelemetryDatum, eventShouldBeFiltered } from './events.js'; + +const ONE_SECOND = 1000; +const ONE_MILLION_CHARACTERS = 1000000; + +//Everything except parameter messages are housekeeping and if they're dropped bad things can happen. +const PARAMETER_MESSAGES = '^{[\\s]*"type":\\s"parameters'; -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', throttleRate = ONE_SECOND, maxBufferSize = ONE_MILLION_CHARACTERS) { this.url = url; this.instance = instance; this.processor = processor; @@ -57,9 +64,31 @@ export default class RealtimeProvider { this.lastSubscriptionId = 1; this.subscriptionsByCall = new Map(); this.subscriptionsById = {}; + this.#socketWorker = new openmct.telemetry.BatchingWebSocket(openmct); + this.#socketWorker.setThrottleMessagePattern(PARAMETER_MESSAGES); + this.#openmct = openmct; + this.#socketWorker.setThrottleRate(throttleRate); + this.#socketWorker.setMaxBufferSize(maxBufferSize); 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); + } } addSupportedObjectTypes(types) { @@ -69,7 +98,6 @@ export default class RealtimeProvider { addSupportedDataTypes(dataTypes) { dataTypes.forEach(dataType => this.supportedDataTypes[dataType] = dataType); } - supportsSubscribe(domainObject) { return this.isSupportedObjectType(domainObject.type); } @@ -142,6 +170,10 @@ export default class RealtimeProvider { }; } + getSubscriptionByObjectIdentifier(identifier) { + return Object.values(this.subscriptionsById).find(subscription => this.#openmct.objects.areIdsEqual(subscription.domainObject.identifier, identifier)); + } + buildSubscriptionDetails(domainObject, callback, options) { let subscriptionId = this.lastSubscriptionId++; let subscriptionDetails = { @@ -160,47 +192,93 @@ export default class RealtimeProvider { sendSubscribeMessage(subscriptionDetails) { const domainObject = subscriptionDetails.domainObject; - const message = MESSAGES.SUBSCRIBE[domainObject.type](subscriptionDetails); + const message = SUBSCRIBE[domainObject.type](subscriptionDetails); - this.sendOrQueueMessage(message); + this.sendMessage(message); } sendUnsubscribeMessage(subscriptionDetails) { - let message = MESSAGES.UNSUBSCRIBE(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; + } else { + delete this.remoteClockCallNumber; + } + } + + #processParameterUpdates(parameterValuesByCall) { + //If remote clock active, process its value before any telemetry values to ensure the bounds are always up to date. + if (this.remoteClockCallNumber !== undefined) { + const remoteClockValues = parameterValuesByCall.get(this.remoteClockCallNumber); + const subscriptionDetails = this.subscriptionsByCall.get(this.remoteClockCallNumber); + + if (remoteClockValues !== undefined && remoteClockValues.length > 0) { + const allClockValues = []; + + remoteClockValues.forEach((parameterValue) => { + this.#convertMessageToDatumAndReportStaleness(parameterValue, subscriptionDetails, allClockValues); + }); + + if (allClockValues.length > 0) { + subscriptionDetails.callback(allClockValues); + } + + // Delete so we don't process it twice. + parameterValuesByCall.delete(this.remoteClockCallNumber); + } } - this.reconnectTimeout = setTimeout(() => { - this.connect(); - delete this.reconnectTimeout; - }, FALLBACK_AND_WAIT_MS[this.currentWaitIndex]); + // Now process all non-clock parameter updates + for (const [call, parameterValues] of parameterValuesByCall.entries()) { + const allTelemetryData = []; + const subscriptionDetails = this.subscriptionsByCall.get(call); - if (this.currentWaitIndex < FALLBACK_AND_WAIT_MS.length - 1) { - this.currentWaitIndex++; + // possibly cancelled + if (!subscriptionDetails) { + continue; + } + + parameterValues.forEach((parameterValue) => { + this.#convertMessageToDatumAndReportStaleness(parameterValue, subscriptionDetails, allTelemetryData); + }); + + if (allTelemetryData.length > 0) { + subscriptionDetails.callback(allTelemetryData); + } } } - 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(); + #convertMessageToDatumAndReportStaleness(parameterValue, subscriptionDetails, allTelemetryData) { + const values = parameterValue.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); + } } - } else { - this.requests.push(request); - } + + addLimitInformation(parameter, datum); + allTelemetryData.push(datum); + }); } connect() { @@ -211,117 +289,95 @@ 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.connected = true; - console.debug(`🔌 Established websocket connection to ${wsUrl}`); - - this.currentWaitIndex = 0; + this.#socketWorker.connect(wsUrl); + this.#socketWorker.addEventListener('reconnected', () => { this.resubscribeToAll(); - this.flushQueue(); - }; + }); - this.socket.onmessage = (event) => { - const message = JSON.parse(event.data); + this.#socketWorker.addEventListener('batch', (batchEvent) => { + const newBatch = batchEvent.detail; + const parametersByCall = new Map(); + newBatch.forEach(messageString => { + const message = JSON.parse(messageString); + const call = message.call; + if (message.type === 'parameters') { + // First, group parameter updates by call + let arrayOfParametersForCall = parametersByCall.get(call); - if (!this.isSupportedDataType(message.type)) { - return; - } + if (arrayOfParametersForCall === undefined) { + arrayOfParametersForCall = []; + parametersByCall.set(call, arrayOfParametersForCall); + } - const isReply = message.type === DATA_TYPES.DATA_TYPE_REPLY; - let subscriptionDetails; + arrayOfParametersForCall.push(message); + } else { + if (!this.isSupportedDataType(message.type)) { + return; + } - if (isReply) { - const id = message.data.replyTo; - const call = message.call; - subscriptionDetails = this.subscriptionsById[id]; - subscriptionDetails.call = call; - this.subscriptionsByCall.set(call, subscriptionDetails); - } else { - subscriptionDetails = this.subscriptionsByCall.get(message.call); - - // possibly cancelled - if (!subscriptionDetails) { - return; - } + const isReply = message.type === DATA_TYPES.DATA_TYPE_REPLY; + let subscriptionDetails; - 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 (isReply) { + const id = message.data.replyTo; + subscriptionDetails = this.subscriptionsById[id]; - if (parameter.engValue.type !== AGGREGATE_TYPE) { - datum.value = value; - } else { - datum = { - ...datum, - ...value - }; + // Susbcriptions can be cancelled before we even get to this stage during tests due to rapid navigation. + if (!subscriptionDetails) { + return; } - addLimitInformation(parameter, datum); - subscriptionDetails.callback(datum); - }); - } else if (this.isCommandMessage(message)) { - const datum = commandToTelemetryDatum(message.data); - subscriptionDetails.callback(datum); - } else if (this.isEventMessage(message)) { - if (eventShouldBeFiltered(message.data, subscriptionDetails.options)) { - // ignore event + subscriptionDetails.call = call; + // Subsequent retrieval uses a string, so for performance reasons we use a string as a key. + this.subscriptionsByCall.set(call, subscriptionDetails); + + const remoteClockIdentifier = this.#openmct.time.getClock()?.identifier; + const isRemoteClockActive = remoteClockIdentifier !== undefined; + + if (isRemoteClockActive && subscriptionDetails.domainObject.identifier.key === remoteClockIdentifier.key) { + this.remoteClockCallNumber = call; + } } else { - const datum = eventToTelemetryDatum(message.data); - subscriptionDetails.callback(datum); - } - } else if (this.isMdbChangesMessage(message)) { - const parameterName = message.data.parameterOverride.parameter; - if (this.observingLimitChanges[parameterName] !== undefined) { - const alarmRange = message.data.parameterOverride.defaultAlarm?.staticAlarmRange ?? []; - this.observingLimitChanges[parameterName].callback(getLimitFromAlarmRange(alarmRange)); - } + subscriptionDetails = this.subscriptionsByCall.get(message.call); - if (subscriptionDetails.callback) { - subscriptionDetails.callback(message.data); - } - } else { - subscriptionDetails.callback(message.data); - } - } - }; + // possibly cancelled + if (!subscriptionDetails) { + return; + } - this.socket.onerror = (error) => { - console.error(`🚨 Websocket error, closing websocket`, error); - this.socket.close(); - }; + if (this.isCommandMessage(message)) { + const datum = commandToTelemetryDatum(message.data); + subscriptionDetails.callback(datum); + } else if (this.isEventMessage(message)) { + if (eventShouldBeFiltered(message.data, subscriptionDetails.options)) { + // ignore event + } else { + const datum = eventToTelemetryDatum(message.data); + subscriptionDetails.callback(datum); + } + } else if (this.isMdbChangesMessage(message)) { + if (!this.isParameterType(message)) { + return; + } - this.socket.onclose = () => { - console.warn('🚪 Websocket closed. Attempting to reconnect...'); - this.connected = false; - this.socket = null; + const parameterName = message.data.parameterOverride.parameter; + if (this.observingLimitChanges[parameterName] !== undefined) { + const alarmRange = message.data.parameterOverride.defaultAlarm?.staticAlarmRange ?? []; + this.observingLimitChanges[parameterName].callback(getLimitFromAlarmRange(alarmRange)); + } - this.reconnect(); - }; + if (subscriptionDetails.callback) { + subscriptionDetails.callback(message.data); + } + } else { + subscriptionDetails.callback(message.data); + } + } + } + }); + this.#processParameterUpdates(parametersByCall); + }); } resubscribeToAll() { @@ -330,30 +386,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 +405,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 dad80bac..2557cf91 100644 --- a/src/providers/staleness-provider.js +++ b/src/providers/staleness-provider.js @@ -20,12 +20,11 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import { OBJECT_TYPES, STALENESS_STATUS_MAP } from '../const'; -import { buildStalenessResponseObject } from '../utils'; +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 a50ef4d5..d14db8f0 100644 --- a/src/providers/user/user-provider.js +++ b/src/providers/user/user-provider.js @@ -20,11 +20,11 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import createYamcsUser from './createYamcsUser'; +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 bb9109a0..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'; +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 cdfbc1e3..c180b9cd 100644 --- a/tests/README.md +++ b/tests/README.md @@ -3,6 +3,15 @@ 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 + +** Note: if you are running the tests to match a specific branch of openmct, you will need to run `npm run build:example:currentbranch` ** + +### Makefile + +1. `make test-all` + +### Manually + To run the tests, we recommend the following workflow which bridges two separate github repos: yamcs/quickstart and openmct-yamcs (this one). @@ -11,8 +20,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.36.2 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` or `npm run build:example:currentbranch` +9. `npm run test:e2e:watch` \ No newline at end of file diff --git a/tests/e2e/playwright-quickstart.config.js b/tests/e2e/playwright-quickstart.config.js index e75bd6b0..fe446900 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, @@ -64,11 +65,8 @@ const config = { open: 'never', outputFolder: '../html-test-results' //Must be in different location due to https://github.com/microsoft/playwright/issues/12840 }], - ['junit', { outputFile: 'test-results/results.xml' }], - ['@deploysentinel/playwright'] - ] + ['junit', { outputFile: 'test-results/results.xml' }]] }; -// eslint-disable-next-line no-undef -module.exports = config; +export default config; diff --git a/tests/e2e/yamcs/barGraph.e2e.spec.mjs b/tests/e2e/yamcs/barGraph.e2e.spec.mjs new file mode 100644 index 00000000..bf2fb12d --- /dev/null +++ b/tests/e2e/yamcs/barGraph.e2e.spec.mjs @@ -0,0 +1,57 @@ +/***************************************************************************** + * 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 the Bar Graph component. + */ + +import { pluginFixtures, appActions } from 'openmct-e2e'; +import { searchAndLinkTelemetryToObject } from '../yamcsAppActions.mjs'; +const { test, expect } = pluginFixtures; +const { createDomainObjectWithDefaults } = appActions; + +test.describe('Bar Graph @yamcs', () => { + let barGraph; + let historicalGet; + + test.beforeEach(async ({ page }) => { + // Open a browser, navigate to the main page, and wait until all networkevents to resolve + await page.goto('./', { waitUntil: 'networkidle' }); + + // Create the Bar Graph + barGraph = await createDomainObjectWithDefaults(page, { type: 'Graph', name: 'Bar Graph' }); + // Enter edit mode for the overlay plot + await searchAndLinkTelemetryToObject(page, 'Magnetometer', barGraph.name); + }); + + test('Requests a single historical datum', async ({ page }) => { + + //http://localhost:9000/yamcs-proxy/api/archive/myproject/parameters/myproject/Magnetometer?start=2024-09-25T14%3A08%3A46.244Z&stop=2024-09-25T14%3A38%3A46.245Z&limit=1&order=desc + historicalGet = page.waitForRequest(/.*\/api\/.*\/parameters.*limit=1&order=desc$/); + + await page.goto(barGraph.url, { waitUntil: 'networkidle' }); + + await historicalGet; + + await expect(page.getByRole('main').getByText(barGraph.name)).toBeVisible(); + }); +}); diff --git a/tests/e2e/yamcs/faultManagement.e2e.spec.mjs b/tests/e2e/yamcs/faultManagement.e2e.spec.mjs new file mode 100644 index 00000000..6082427a --- /dev/null +++ b/tests/e2e/yamcs/faultManagement.e2e.spec.mjs @@ -0,0 +1,289 @@ +/***************************************************************************** + * 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, expect } = pluginFixtures; + +const YAMCS_API_URL = "http://localhost:8090/api/"; +const FAULT_PARAMETER = "Latitude"; + +/** + * Get the locator for a triggered fault list item by severity. + * @param {import('@playwright/test').Page} page - The page object. + * @param {string} severity - The severity of the fault. + * @returns {import('@playwright/test').Locator} - The locator for the fault's severity label. + */ +function getTriggeredFaultBySeverity(page, severity) { + return page.getByLabel(new RegExp(`Fault triggered at.*${severity}.*`, 'i')); +} + +test.describe("Fault Management @yamcs", () => { + test.beforeAll("activate alarms on the telemetry point", async () => { + // Set the default alarms for the parameter in such a way + // that it is guaranteed to produce a fault on load. + const response = await setDefaultAlarms(FAULT_PARAMETER, [ + { + level: 'WATCH', + minInclusive: 808, + maxInclusive: 810 + }, + { + level: 'WARNING', + minInclusive: 810.01, + maxInclusive: 812 + }, + { + level: 'DISTRESS', + minInclusive: 812.01, + maxInclusive: 814 + }, + { + level: 'CRITICAL', + minInclusive: 814.01, + maxInclusive: 820 + }, + { + level: 'SEVERE', + minInclusive: 820.01, + maxInclusive: 824 + } + ]); + expect(response.status).toBe(200); + }); + + test.beforeEach(async ({ page }) => { + const networkPromise = page.waitForResponse('**/api/mdb/myproject/parameters**'); + await page.goto('./', { waitUntil: 'domcontentloaded' }); + // Wait until the YAMCS parameter request resolves + await networkPromise; + }); + + test('Shows faults of differing severities ', async ({ page }) => { + // Intercept the request to set the alarm to WATCH severity + await page.route('**/api/**/alarms', route => modifyAlarmSeverity(route, FAULT_PARAMETER, 'WATCH')); + + await test.step('Shows fault with severity WATCH', async () => { + await page.goto('./', { waitUntil: 'domcontentloaded' }); + + const alarmsRequest = page.waitForRequest('**/api/**/alarms'); + await page.getByLabel('Navigate to Fault Management').click(); + await alarmsRequest; + await expect(getTriggeredFaultBySeverity(page, 'WATCH')).toBeVisible(); + }); + + // Intercept the request to set the alarm to WARNING severity + await page.route('**/api/**/alarms', route => modifyAlarmSeverity(route, FAULT_PARAMETER, 'WARNING')); + + await test.step('Shows fault with severity WARNING', async () => { + await page.goto('./', { waitUntil: 'domcontentloaded' }); + + const alarmsRequest = page.waitForRequest('**/api/**/alarms'); + await page.getByLabel('Navigate to Fault Management').click(); + await alarmsRequest; + await expect(getTriggeredFaultBySeverity(page, 'WARNING')).toBeVisible(); + }); + + // Intercept the request to set the alarm to CRITICAL severity + await page.route('**/api/**/alarms', route => modifyAlarmSeverity(route, FAULT_PARAMETER, 'CRITICAL')); + + await test.step('Shows fault with severity CRITICAL', async () => { + await page.goto('./', { waitUntil: 'domcontentloaded' }); + + const alarmsRequest = page.waitForRequest('**/api/**/alarms'); + await page.getByLabel('Navigate to Fault Management').click(); + await alarmsRequest; + await expect(getTriggeredFaultBySeverity(page, 'CRITICAL')).toBeVisible(); + }); + }); + + test('Faults may be shelved for a period of time', async ({ page }) => { + await test.step('Set the alarm to critical and mock the shelve request', async () => { + // Intercept the response to set the alarm to critical + await page.route('**/api/**/alarms', route => modifyAlarmSeverity(route, FAULT_PARAMETER, 'CRITICAL')); + + // Intercept the request to shelve the fault and set the duration to 1000ms so + // we don't have to wait long for the fault to un-shelve + await page.route('**/api/**/*:shelve', async route => { + if (route.request().method() === 'POST') { + let requestBody = await route.request().postDataJSON(); + requestBody.shelveDuration = 10000; + await route.continue({ postData: requestBody }); + } else { + await route.continue(); + } + }); + }); + + await test.step('Shelve the fault', async () => { + const alarmsRequest = page.waitForRequest('**/api/**/alarms'); + await page.getByLabel('Navigate to Fault Management').click(); + await alarmsRequest; + await expect(page.getByLabel(/Fault triggered at.*CRITICAL.*/)).toBeVisible(); + await page.getByLabel('Select fault: Latitude in /myproject').check(); + await page.getByLabel('Shelve selected faults').click(); + await page.locator('#comment-textarea').fill("Shelvin' a fault!"); + await page.getByLabel('Save').click(); + }); + + await test.step('Shelved faults are visible in the Shelved view', async () => { + await expect(page.getByLabel(/Fault triggered at.*CRITICAL.*/)).toBeHidden(); + await page.getByTitle('View Filter').getByRole('combobox').selectOption('Shelved'); + await expect(page.getByLabel(/Fault triggered at.*CRITICAL.*/)).toBeVisible(); + await page.getByTitle('View Filter').getByRole('combobox').selectOption('Standard View'); + }); + await test.step('Fault is visible in the Standard view after shelve duration expires', async () => { + // Have a longer timeout to account for the fault being shelved + await expect(getTriggeredFaultBySeverity(page, 'CRITICAL')).toBeVisible({ timeout: 10000 }); + await page.getByTitle('View Filter').getByRole('combobox').selectOption('Shelved'); + await expect(getTriggeredFaultBySeverity(page, 'CRITICAL')).toBeHidden(); + }); + }); + + test('Faults may be acknowledged', async ({ page }) => { + await test.step('Set the alarm to critical', async () => { + // Intercept the response to set the alarm to critical + await page.route('**/api/**/alarms', route => modifyAlarmSeverity(route, FAULT_PARAMETER, 'CRITICAL')); + + }); + + await test.step('Acknowledge the fault', async () => { + const alarmsRequest = page.waitForRequest('**/api/**/alarms'); + await page.getByLabel('Navigate to Fault Management').click(); + await alarmsRequest; + await expect(getTriggeredFaultBySeverity(page, 'CRITICAL')).toBeVisible(); + await page.getByLabel('Select fault: Latitude in /myproject').check(); + await page.getByLabel('Acknowledge selected faults').click(); + await page.locator('#comment-textarea').fill("Acknowledging a fault!"); + await page.getByLabel('Save').click(); + }); + + await test.step('Acknowledged faults are visible in the Acknowledged view', async () => { + await expect(getTriggeredFaultBySeverity(page, 'CRITICAL')).toBeHidden(); + await page.getByTitle('View Filter').getByRole('combobox').selectOption('Acknowledged'); + await expect(getTriggeredFaultBySeverity(page, 'CRITICAL')).toBeVisible(); + }); + }); + + test.afterAll("remove alarms from the telemetry point", async () => { + const responses = await clearAlarms(FAULT_PARAMETER); + for (const res of responses) { + expect.soft(res.status).toBe(200); + } + }); +}); + +/** + * @typedef {Object} AlarmRange + * @property {'WATCH' | 'WARNING' | 'DISTRESS' | 'CRITICAL' | 'SEVERE'} level - The alarm level. + * @property {number} minInclusive - The minimum inclusive value for the alarm. + * @property {number} maxInclusive - The maximum inclusive value for the alarm. + */ + +/** + * Set default alarms for a parameter. + * @param {string} parameter - The parameter to set alarms for. + * @param {AlarmRange[]} staticAlarmRanges - The static alarm ranges to set. + * @param {string} [instance='myproject'] - The instance name. + * @param {string} [processor='realtime'] - The processor name. + */ +// eslint-disable-next-line require-await +async function setDefaultAlarms(parameter, staticAlarmRanges = [], instance = 'myproject', processor = 'realtime') { + return fetch(`${YAMCS_API_URL}mdb-overrides/${instance}/${processor}/parameters/${instance}/${parameter}`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + action: 'SET_DEFAULT_ALARMS', + defaultAlarm: { + staticAlarmRange: staticAlarmRanges + } + }) + }); +} + +/** + * Clear alarms for a parameter. + * @param {string} parameter - The parameter to clear alarms for. + * @param {string} [instance='myproject'] - The instance name. + * @param {string} [processor='realtime'] - The processor name. + * @returns {Promise} - The response from the server. + */ +// eslint-disable-next-line require-await +async function clearAlarms(parameter, instance = 'myproject', processor = 'realtime') { + await setDefaultAlarms(parameter, [], instance, processor); + const response = await getAlarms(instance); + const alarms = await response.json(); + const alarmsToClear = Object.values(alarms).map(alarm => { + + return { + name: alarm[0].id.name, + seqNum: alarm[0].seqNum + }; + }); + + return Promise.all( + alarmsToClear.map(alarm => + fetch(`${YAMCS_API_URL}processors/${instance}/${processor}/alarms/${alarm.name}/${alarm.seqNum}:clear`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + }) + ) + ); +} + +// eslint-disable-next-line require-await +async function getAlarms(instance = 'myproject') { + return fetch(`${YAMCS_API_URL}archive/${instance}/alarms`); +} + +/** + * @param {import('@playwright/test').Route} route + * @param {string} alarmName + * @param {string} newSeverity + */ +async function modifyAlarmSeverity(route, alarmName, newSeverity) { + const response = await route.fetch(); + let body = await response.json(); + const newBody = { ...body }; + + // Modify the rawValue.floatValue to trigger a specific alarm + body.alarms.forEach((alarm, index) => { + if (alarm.id.name === alarmName) { + newBody.alarms[index].severity = newSeverity; + } + }); + + return route.fulfill({ + response, + json: newBody, + headers: { + ...response.headers() + } + }); +} 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 8a31448f..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 */ -const { test, expect } = require('../opensource/pluginFixtures'); -const { createDomainObjectWithDefaults } = require('../opensource/appActions'); +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 e25083f3..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 */ -const { test, expect } = require('../opensource/pluginFixtures'); -const { setFixedTimeMode } = require('../opensource/appActions'); +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 57% rename from tests/e2e/yamcs/limits.e2e.spec.js rename to tests/e2e/yamcs/limits.e2e.spec.mjs index 9d05971e..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 */ -const { test, expect } = require('../opensource/pluginFixtures'); -const { createDomainObjectWithDefaults, waitForPlotsToRender } = require('../opensource/appActions'); +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" }); @@ -51,41 +62,34 @@ test.describe("Mdb runtime limits tests @yamcs", () => { const detectorTreeItem = page.getByRole('treeitem', { name: /Detector_Temp/ }); // Enter edit mode for the overlay plot - await page.click('button[title="Edit"]'); + 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); // Save (exit edit mode) - await page.locator('button[title="Save"]').click(); - await page.locator('li[title="Save and Finish Editing"]').click(); + await page.getByRole('button', { name: 'Save' }).click(); + await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); // Assert that no limit lines are shown by default await page.waitForSelector('.js-limit-area', { state: 'attached' }); expect(await page.locator('.c-plot-limit-line').count()).toBe(0); // Enter edit mode - await page.click('button[title="Edit"]'); + await page.getByLabel('Edit Object').click(); // 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.locator('button[title="Save"]').click(); - await page.locator('li[title="Save and Finish Editing"]').click(); + 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,57 +127,42 @@ 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.click('button[title="Edit"]'); + 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.locator('button[title="Save"]').click(); - await page.locator('li[title="Save and Finish Editing"]').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 await page.waitForSelector('.js-limit-area', { state: 'attached' }); expect(await page.locator('.c-plot-limit-line').count()).toBe(0); // Enter edit mode - await page.click('button[title="Edit"]'); + await page.getByLabel('Edit Object').click(); // 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.locator('button[title="Save"]').click(); - await page.locator('li[title="Save and Finish Editing"]').click(); + await page.getByRole('button', { name: 'Save' }).click(); + await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); //navigate away from the overlay plot 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.js deleted file mode 100644 index dd89aa1b..00000000 --- a/tests/e2e/yamcs/network.e2e.spec.js +++ /dev/null @@ -1,135 +0,0 @@ -/***************************************************************************** - * 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 -*/ - -const { test, expect } = require('../opensource/pluginFixtures'); -const { setFixedTimeMode } = require('../opensource/appActions'); - -test.describe("Quickstart network requests @yamcs", () => { - // Collect all request events, specifically for YAMCS - let networkRequests = []; - let filteredRequests = []; - - test('Validate network traffic to YAMCS', async ({ page }) => { - page.on('request', (request) => networkRequests.push(request)); - // Go to baseURL - await page.goto("./", { waitUntil: "networkidle" }); - - 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_Sequence').first().click(); - await page.waitForLoadState('networkidle'); - - // wait for debounced requests in YAMCS Latest Telemetry Provider to finish - await new Promise(resolve => setTimeout(resolve, 500)); - - filteredRequests = filterNonFetchRequests(networkRequests); - - // Network requests for the composite telemetry with multiple items should be: - // 1. batched request for latest telemetry using the bulk API - expect(filteredRequests.length).toBe(1); - - await page.waitForLoadState('networkidle'); - networkRequests = []; - await page.locator('text=CCSDS_Packet_Length').click(); - await page.waitForLoadState('networkidle'); - - // wait for debounced requests in YAMCS Latest Telemetry Provider to finish - await new Promise(resolve => setTimeout(resolve, 500)); - - filteredRequests = filterNonFetchRequests(networkRequests); - - // Should only be fetching: - // 1. telemetry from parameter archive - // 2. POST: batchGet for staleness - expect(filteredRequests.length).toBe(2); - - // Change to fixed time - await setFixedTimeMode(page); - - await page.waitForLoadState('networkidle'); - networkRequests = []; - await page.locator('text=CCSDS_Packet_Sequence').first().click(); - await page.waitForLoadState('networkidle'); - - // wait for debounced requests in YAMCS Latest Telemetry Provider to finish - await new Promise(resolve => setTimeout(resolve, 500)); - - filteredRequests = filterNonFetchRequests(networkRequests); - - // Should fetch from parameter archive, so: - // 1. GET for first telemetry item from parameter archive - // 2. GET for second telemetry item from parameter archive - // 3. POST: batchGet for staleness - expect(filteredRequests.length).toBe(3); - - await page.waitForLoadState('networkidle'); - networkRequests = []; - await page.locator('text=CCSDS_Packet_Length').first().click(); - await page.waitForLoadState('networkidle'); - - // wait for debounced requests in YAMCS Latest Telemetry Provider to finish - await new Promise(resolve => setTimeout(resolve, 500)); - filteredRequests = filterNonFetchRequests(networkRequests); - // Should only be fetching telemetry from parameter archive, - // with no further request for limits should be made. - // 1. GET for telemetry item from parameter archive - // 2. POST: batchGet for staleness - expect(filteredRequests.length).toBe(2); - - networkRequests = []; - await page.reload(); - await page.waitForLoadState('networkidle'); - - // wait for debounced requests in YAMCS Latest Telemetry Provider to finish - await new Promise(resolve => setTimeout(resolve, 500)); - - // Should be fetching: - // 1. user api - // 2. space systems - // 3. parameter dictionary - // 4. specific parameter telemetry for CCSDS_Packet_Length - // 5. POST: batchGet for staleness - // 6. GET for telemetry item mdb overrides - filteredRequests = filterNonFetchRequests(networkRequests); - expect(filteredRequests.length).toBe(6); - - }); - - // Try to reduce indeterminism of browser requests by only returning fetch requests. - // Filter out preflight CORS, fetching stylesheets, page icons, etc. that can occur during tests - function filterNonFetchRequests(requests) { - return requests.filter(request => { - return (request.resourceType() === 'fetch'); - }); - } -}); diff --git a/tests/e2e/yamcs/network.e2e.spec.mjs b/tests/e2e/yamcs/network.e2e.spec.mjs new file mode 100644 index 00000000..4ce60e52 --- /dev/null +++ b/tests/e2e/yamcs/network.e2e.spec.mjs @@ -0,0 +1,148 @@ +/***************************************************************************** + * 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 { 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. + */ +test.describe("Quickstart network requests @yamcs", () => { + // Keeping track of network requests during the tests. + let networkRequests = []; + let filteredRequests = []; + + // 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)); + + // Setting up promises to wait for specific network responses. + networkRequests = []; + mdbGet = page.waitForResponse('**/api/mdb/myproject/space-systems'); + allParams = page.waitForResponse('**/api/mdb/myproject/parameters?details=yes&limit=1000'); + userGet = page.waitForResponse('**/api/user/'); + mdbOverride = page.waitForResponse('**/api/mdb-overrides/myproject/realtime'); + + // 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); + + // I'm not sure what is going on here + const myProjectTreeItem = page.locator('.c-tree__item').filter({ hasText: 'myproject' }); + await expect(myProjectTreeItem).toBeVisible(); + await myProjectTreeItem.first().locator('span.c-disclosure-triangle').click(); + await myProjectTreeItem.nth(1).locator('span.c-disclosure-triangle').click(); + + // More UI interactions and network request verifications. + await page.waitForLoadState('networkidle'); + networkRequests = []; + 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); + + // Further UI interactions and network requests verification. + networkRequests = []; + 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(); + + await Promise.all([parameterArchiveGet, batchGetStaleness]); + + await page.waitForLoadState('networkidle'); + expect(networkRequests.length).toBe(2); + + // Simulating the change to fixed time mode and validating network requests. + networkRequests = []; + 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); + + // Clicking on a different telemetry item to generate new requests. + networkRequests = []; + 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'); + + await page.getByRole('treeitem', { name: 'Expand CCSDS_Packet_Sequence' }).click(); + await page.waitForLoadState('networkidle'); + + await Promise.all([groupFlagsGet, countGet, batchGetStaleness]); + + expect(networkRequests.length).toBe(3); + + // Clicking on the telemetry item in Fixed Time mode to generate two requests. + networkRequests = []; + 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(); + await page.waitForLoadState('networkidle'); + + await Promise.all([parameterArchiveGet, batchGetStaleness]); + + // Waiting for debounced requests in YAMCS Latest Telemetry Provider to finish. + expect(networkRequests.length).toBe(2); + + // Simulating a page refresh to generate a sequence of network requests. + networkRequests = []; + userGet = page.waitForResponse('**/api/user/'); + allParams = page.waitForResponse('**/api/mdb/myproject/parameters?details=yes&limit=1000'); + mdbOverride = page.waitForResponse('**/api/mdb-overrides/myproject/realtime'); + parameterArchiveGet = page.waitForResponse('**/api/archive/myproject/parameters/myproject/CCSDS_Packet_Length/samples**'); + batchGetStaleness = page.waitForResponse('**/api/processors/myproject/realtime/parameters:batchGet'); + mdbOverride = page.waitForResponse('**/api/mdb-overrides/myproject/realtime'); + + await page.reload({ waitUntil: 'networkidle' }); + 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); + + // Removing the 'request' event listener to prevent potential memory leaks. + page.removeListener('request', request => networkRequests.push(request)); + }); + + /** + * Filters out non-fetch requests from the given array of network requests. + * This includes preflight CORS, fetching stylesheets, page icons, etc. + * @param {Array} requests - Array of network requests to filter. + * @returns {Array} Filtered network requests. + */ + function filterNonFetchRequests(requests) { + return requests.filter(request => request.resourceType() === 'fetch'); + } +}); diff --git a/tests/e2e/yamcs/quickstartSmoke.e2e.spec.js b/tests/e2e/yamcs/quickstartSmoke.e2e.spec.mjs similarity index 94% rename from tests/e2e/yamcs/quickstartSmoke.e2e.spec.js rename to tests/e2e/yamcs/quickstartSmoke.e2e.spec.mjs index 5692df3f..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. */ -const { test, expect } = require('../opensource/baseFixtures'); +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 }) => { @@ -41,7 +42,7 @@ test.describe("Quickstart smoke tests @yamcs", () => { await page.goto('./', { waitUntil: 'networkidle' }); //Click the Create button - await page.click('button:has-text("Create")'); + await page.getByRole('button', { name: 'Create' }).click(); // Verify that Create Folder appears in the dropdown await expect(page.locator(':nth-match(:text("Folder"), 2)')).toBeEnabled(); 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..c7a1cc05 --- /dev/null +++ b/tests/e2e/yamcs/realtimeData.e2e.spec.mjs @@ -0,0 +1,535 @@ +/***************************************************************************** + * 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: 'networkidle' }); + await page.evaluate((thirtyMinutes) => { + return new Promise((resolve) => { + 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: 15000 + }); + setTimeout(resolve, 2000); + }); + }, 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. It's + // long enough to ensure we have new telemetry, short enough that + // it doesn't significantly increase test time. + 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); + } + }); + /** + * This tests for an edge-case found during testing where throttling occurs during subscription handshaking with the server. + * In this scenario, subscribers never receive telemetry because the subscription was never properly initialized. + * This test confirms that after blocking the UI and inducing throttling, that all subscribed telemetry objects received telemetry. + */ + test('When the UI is blocked during initialization, does not drop subscription housekeeping messages', async ({ page }) => { + // 1. Block the UI + await page.evaluate(() => { + return new Promise((resolveBlockingLoop) => { + let start = Date.now(); + let now = Date.now(); + // Block the UI thread for 6s + while (now - start < 10000) { + now = Date.now(); + } + + resolveBlockingLoop(); + }); + }); + + //Confirm that throttling occurred + const notification = page.getByRole('alert'); + const text = await notification.innerText(); + expect(text).toBe('Telemetry dropped due to client rate limiting.'); + + //Confirm that all subscribed telemetry points receive telemetry. This tests that subscriptions were established successfully and + //tests for a failure mode where housekeeping telemetry was being dropped if the UI was blocked during initialization of telemetry subscriptions + const parametersToSubscribeTo = Object.values(namesToParametersMap).map(parameter => parameter.replaceAll('/', '~')); + const subscriptionsThatTelemetry = await page.evaluate(async (parameters) => { + const openmct = window.openmct; + const telemetryObjects = await Promise.all( + Object.values(parameters).map( + (parameterId) => openmct.objects.get( + { + namespace: 'taxonomy', + key: parameterId + } + ) + )); + const subscriptionsAllReturned = await Promise.all(telemetryObjects.map((telemetryObject) => { + return new Promise(resolve => { + const unsubscribe = openmct.telemetry.subscribe(telemetryObject, () => { + unsubscribe(); + resolve(true); + }); + }); + })); + + return subscriptionsAllReturned; + }, parametersToSubscribeTo); + + expect(subscriptionsThatTelemetry.length).toBe(parametersToSubscribeTo.length); + }); + + 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 10s + while (now - start < 10000) { + now = Date.now(); + } + + requestIdleCallback(resolveBlockingLoop); + }); + }); + + //Confirm that throttling occurred + const notification = page.getByRole('alert'); + const text = await notification.innerText(); + expect(text).toBe('Telemetry dropped due to client rate limiting.'); + + // Disable playback + await disableLink(yamcsURL); + + // Wait 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,e + 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 69% rename from tests/e2e/yamcs/search.e2e.spec.js rename to tests/e2e/yamcs/search.e2e.spec.mjs index 7c067b36..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 */ -const { test, expect } = require('../opensource/pluginFixtures'); +import { pluginFixtures } from 'openmct-e2e'; +const { test, expect } = pluginFixtures; test.describe("Quickstart search tests @yamcs", () => { test('Validate aggregate in search result', async ({ page }) => { @@ -34,13 +35,14 @@ test.describe("Quickstart search tests @yamcs", () => { await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click(); // Search for Sequence await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').fill('Sequence'); - await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toContainText("CCSDS_Packet_Sequence"); - await expect(page.locator('[aria-label="Search Result"] >> nth=1')).toContainText("CCSDS_Packet_Sequence.GroupFlags"); - await expect(page.locator('[aria-label="Search Result"] >> nth=2')).toContainText("CCSDS_Packet_Sequence.Count"); + + await expect(page.getByLabel('Object Search Result').nth(0)).toContainText("CCSDS_Packet_Sequence"); + await expect(page.getByLabel('Object Search Result').nth(1)).toContainText("CCSDS_Packet_Sequence.GroupFlags"); + await expect(page.getByLabel('Object Search Result').nth(2)).toContainText("CCSDS_Packet_Sequence.Count"); // Search for mixed case and get same results await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').fill('seQuence'); - await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toContainText("CCSDS_Packet_Sequence"); - await expect(page.locator('[aria-label="Search Result"] >> nth=1')).toContainText("CCSDS_Packet_Sequence.GroupFlags"); - await expect(page.locator('[aria-label="Search Result"] >> nth=2')).toContainText("CCSDS_Packet_Sequence.Count"); + await expect(page.getByLabel('Object Search Result').nth(0)).toContainText("CCSDS_Packet_Sequence"); + await expect(page.getByLabel('Object Search Result').nth(1)).toContainText("CCSDS_Packet_Sequence.GroupFlags"); + await expect(page.getByLabel('Object Search Result').nth(2)).toContainText("CCSDS_Packet_Sequence.Count"); }); }); 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/e2e/yamcsAppActions.mjs b/tests/e2e/yamcsAppActions.mjs new file mode 100644 index 00000000..436ee182 --- /dev/null +++ b/tests/e2e/yamcsAppActions.mjs @@ -0,0 +1,52 @@ +/***************************************************************************** + * 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 fixtures in this file are to be used to consolidate common actions performed by the + * various test suites. The goal is only to avoid duplication of code across test suites and not to abstract + * away the underlying functionality of the application. For more about the App Action pattern, see /e2e/README.md) + * + * For example, if two functions are nearly identical in + * timer.e2e.spec.js and notebook.e2e.spec.js, that function should be generalized and moved into this file. + */ + +/** + * Search for telemetry and link it to an object. objectName should come from the domainObject.name function. + * @param {import('@playwright/test').Page} page + * @param {string} parameterName + * @param {string} objectName + */ +async function searchAndLinkTelemetryToObject(page, parameterName, objectName) { + await page.getByRole('searchbox', { name: 'Search Input' }).click(); + await page.getByRole('searchbox', { name: 'Search Input' }).fill(parameterName); + await page.getByLabel(`${parameterName} yamcs.aggregate result`).getByText(parameterName).click(); + await page.getByLabel('More actions').click(); + await page.getByLabel('Create Link').click(); + await page.getByLabel('Modal Overlay').getByLabel('Search Input').click(); + await page.getByLabel('Modal Overlay').getByLabel('Search Input').fill(objectName); + await page.getByLabel('Navigate to Bar Graph').click(); + await page.getByText('Ok').click(); +} + +export { + searchAndLinkTelemetryToObject +}; diff --git a/tests/git-opensource-tests.sh b/tests/git-opensource-tests.sh index 0d7d6a20..9c962a6f 100644 --- a/tests/git-opensource-tests.sh +++ b/tests/git-opensource-tests.sh @@ -1,6 +1,6 @@ #!/bin/sh # -# This script can clone/checkout a single folder from git repository +# This script can clone/checkout a single folder from git repository # - Might be used for checking out micro-services from monolithic git repository # # - You can even do checkout into home directory, for example @@ -8,7 +8,7 @@ # SCRIPT_PATH=${0%/*} # Get the relative path to the script dir from the cwd -if [ "$0" != "$SCRIPT_PATH" ] && [ "$SCRIPT_PATH" != "" ]; then +if [ "$0" != "$SCRIPT_PATH" ] && [ "$SCRIPT_PATH" != "" ]; then cd "$SCRIPT_PATH" fi @@ -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,12 +31,16 @@ 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 # Move subfolders mv opensource/e2e/*/ ./opensource # Move eslint config -mv opensource/e2e/.eslintrc.js ./opensource +mv opensource/e2e/.eslintrc.*js ./opensource # Cleanup rm -rf opensource/e2e