diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index f6728a7d..444d552e 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -8,14 +8,15 @@ Closes + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +### `roles.yaml` + +In the `roles.yaml` file, the user with permission to set mission status must be given `WriteParameter` permission to the path(s) which +contain the Mission Status parameters: + +```yaml +# roles.yaml example granting the "Flight" role permission to set mission status +Flight: + Command: [] + CommandHistory: [ ".*" ] + ManageBucket: [] + ReadAlgorithm: [ ".*" ] + ReadBucket: [ ".*" ] + ReadPacket: [ ".*" ] + ReadParameter: [ ".*" ] + Stream: [] + WriteParameter: + - "/MyProject/MissionStatus/.*" + System: + - GetMissionDatabase + - ReadAlarms + - ReadCommandHistory + - ReadEvents + - ReadFileTransfers + - ReadLinks +``` + +### User Provider + +See the [Open MCT documentation](https://github.com/nasa/openmct/blob/634aeef06e8712d3806bcd15fa9e5901386e12b3/src/plugins/userIndicator/README.md) for information on how to configure the User Provider to support Mission Status. diff --git a/src/providers/mission-status/mission-status-parameter.js b/src/providers/mission-status/mission-status-parameter.js new file mode 100644 index 00000000..b585c566 --- /dev/null +++ b/src/providers/mission-status/mission-status-parameter.js @@ -0,0 +1,75 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2024, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +const MISSION_STATUS_TYPE = 'yamcs.missionStatus'; +const MISSION_ACTION_NAMESPACE = 'OpenMCT:action'; + +/** + * Check if the parameter is a mission status parameter + * @param {Parameter} parameter + * @returns {boolean} true if the parameter is a mission status parameter, false otherwise + */ +export function isMissionStatusParameter(parameter) { + const aliases = parameter.alias; + + return aliases !== undefined + && aliases.some(alias => alias.name === MISSION_STATUS_TYPE); +} + +/** + * Get the mission action from the parameter + * @param {Parameter} parameter + * @returns {import("./mission-status-telemetry").MissionAction? } the mission action name if the parameter is a mission action parameter, null otherwise + */ +export function getMissionActionFromParameter(parameter) { + const aliases = parameter.alias; + + return aliases.find(alias => alias.namespace === MISSION_ACTION_NAMESPACE)?.name ?? null; +} + +/** + * Get the possible mission action statuses from the parameter + * @param {Parameter} parameter + * @returns {string[]} + */ +export function getPossibleMissionActionStatusesFromParameter(parameter) { + return parameter.type.enumValue; +} + +/** + * @typedef {import("./mission-status-telemetry").MdbEntry} MdbEntry + */ + +/** + * @typedef {object} Parameter + * @property {string} name + * @property {string} qualifiedName + * @property {object} type + * @property {string} type.engType + * @property {object} type.dataEncoding + * @property {string} type.dataEncoding.type + * @property {boolean} type.dataEncoding.littleEndian + * @property {number} type.dataEncoding.sizeInBits + * @property {string} type.dataEncoding.encoding + * @property {MdbEntry[]} type.enumValue + * @property {string} dataSource + */ diff --git a/src/providers/mission-status/mission-status-telemetry.js b/src/providers/mission-status/mission-status-telemetry.js new file mode 100644 index 00000000..0bec09fc --- /dev/null +++ b/src/providers/mission-status/mission-status-telemetry.js @@ -0,0 +1,281 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2024, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +import { + idToQualifiedName +} from '../../utils.js'; + +export default class MissionStatusTelemetry { + #missionStatusMap; + #missionActions; + /** @type {Set} */ + #missionStatusParameterNames; + #missionActionToTelemetryObjectMap; + #setReady; + #readyPromise; + #url; + #instance; + #processor; + #openmct; + + constructor(openmct, { url, instance, processor = 'realtime' }) { + this.#missionStatusMap = {}; + this.#missionActions = new Set(); + this.#missionStatusParameterNames = new Set(); + this.#missionActionToTelemetryObjectMap = {}; + this.#readyPromise = new Promise((resolve) => this.#setReady = resolve); + this.#url = url; + this.#instance = instance; + this.#processor = processor; + this.#openmct = openmct; + } + + /** + * Set the status for a particular mission action. + * @param {MissionAction} action the mission action + * @param {MissionStatus} status the status + * @returns {Promise} true if the status was set successfully + */ + async setStatusForMissionAction(action, status) { + const telemetryObject = await this.getTelemetryObjectForAction(action); + const setParameterUrl = this.#buildUrl(telemetryObject.identifier); + let success = false; + + try { + const result = await fetch(setParameterUrl, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + type: 'SINT64', + sint64Value: status.key + }) + }); + + success = result.ok === true; + } catch (error) { + console.error(error); + } + + return success; + } + + /** + * Get the possible mission statuses. + * i.e: "Go" or "No Go" + * @returns {Promise} + */ + async getPossibleMissionStatuses() { + await this.#readyPromise; + + return Object.values(this.#missionStatusMap).map(status => this.toMissionStatusFromMdbEntry(status)); + } + + /** + * Get the default status for any mission action. + * Returns the first status in the list of possible statuses. + * @returns {Promise} + */ + async getDefaultStatusForAction() { + const possibleStatuses = await this.getPossibleMissionStatuses(); + + return possibleStatuses[0]; + } + + /** + * Adds a mission status to the list of possible statuses. + * @param {MissionStatus} status + */ + addStatus(status) { + this.#missionStatusMap[status.value] = status; + } + + /** + * Get the telemetry object for a mission action. + * @param {MissionAction} action the mission action + * @returns {Promise} the telemetry object + */ + async getTelemetryObjectForAction(action) { + await this.#readyPromise; + + return this.#missionActionToTelemetryObjectMap[action]; + } + + /** + * Check if this parameter name is a mission status parameter name. + * @param {string} parameterName + * @returns {boolean} true if the parameter name is a mission status parameter name + */ + async isMissionStatusParameterName(parameterName) { + await this.#readyPromise; + if (this.#missionStatusParameterNames.has(parameterName)) { + return true; + } + + const parameterRegExp = new RegExp(`^${parameterName}$`); + for (const missionStatusParameterName of this.#missionStatusParameterNames) { + if (parameterRegExp.test(missionStatusParameterName)) { + return true; + } + } + + return false; + } + + /** + * Set the telemetry object for a mission action. + * @param {MissionAction} action + * @param {TelemetryObject} telemetryObject + */ + setTelemetryObjectForAction(action, telemetryObject) { + this.#missionActionToTelemetryObjectMap[action] = telemetryObject; + } + + /** + * Add a mission action to the list of possible actions. + * @param {MissionAction} action + */ + addMissionAction(action) { + this.#missionActions.add(action); + } + + /** + * Add a mission status parameter name to the list of parameter names. + * @param {string} parameterName + */ + addMissionStatusParameterName(parameterName) { + this.#missionStatusParameterNames.add(parameterName); + } + + /** + * Get a list of all mission actions. + * @returns {Promise} + */ + async getAllMissionActions() { + await this.#readyPromise; + + return Array.from(this.#missionActions); + } + + /** + * Get the current status of a mission action given its MDB entry. + * @param {MdbEntry} yamcsStatus the MDB entry + * @returns {MissionStatus} + */ + toMissionStatusFromMdbEntry(yamcsStatus) { + return { + // eslint-disable-next-line radix + key: parseInt(yamcsStatus.value), + label: yamcsStatus.label + }; + } + + /** + * Receives a telemetry object and a datum and returns a mission status. + * @param {TelemetryObject} telemetryObject the telemetry object + * @param {Datum} datum the datum object + * @returns {MissionStatus} + */ + toStatusFromTelemetry(telemetryObject, datum) { + const metadata = this.#openmct.telemetry.getMetadata(telemetryObject); + const rangeMetadata = metadata.valuesForHints(['range'])[0]; + const formatter = this.#openmct.telemetry.getValueFormatter(rangeMetadata); + const timestampMetadata = metadata.valuesForHints(['domain'])[0]; + const dateFormatter = this.#openmct.telemetry.getValueFormatter(timestampMetadata); + + return { + key: formatter.parse(datum), + label: formatter.format(datum), + timestamp: dateFormatter.parse(datum) + }; + } + + /** + * Fires when the dictionary is loaded. + */ + dictionaryLoadComplete() { + this.#setReady(); + } + + /** + * Construct the URL for a parameter. + * @param {import('openmct').Identifier} id the identifier + * @returns {string} + */ + #buildUrl(id) { + let url = `${this.#url}api/processors/${this.#instance}/${this.#processor}/parameters/${idToQualifiedName(id.key)}`; + + return url; + } +} + +/** + * @typedef {Object} MissionStatus + * @property {number} key + * @property {string} label + * @property {number?} timestamp + */ + +/** + * @typedef {string} MissionAction + */ + +/** + * @typedef {Object} TelemetryObject + * @property {import('openmct').Identifier} identifier + * @property {string} name + * @property {string} type + * @property {string} location + * @property {string} configuration + * @property {string} domain + * @property {object} telemetry + * @property {TelemetryValue[]} telemetry.values + * @property {string} metadata + * @property {string} composition + * @property {string} object + * @property {string} value + */ + +/** + * @typedef {object} TelemetryValue + * @property {string} key + * @property {string} name + * @property {string} format + * @property {string} source + * @property {object} hints + * @property {number} hints.domain + */ + +/** + * @typedef {object} Datum + * @property {string} id + * @property {string} timestamp + * @property {string} acquisitionStatus + * @property {*} value + */ + +/** + * @typedef {object} MdbEntry + * @property {string} value + * @property {string} label + * @property {string} description + */ diff --git a/src/providers/object-provider.js b/src/providers/object-provider.js index de499d49..6dca9e9b 100644 --- a/src/providers/object-provider.js +++ b/src/providers/object-provider.js @@ -28,18 +28,18 @@ import { } from '../utils.js'; import { OBJECT_TYPES, NAMESPACE } from '../const.js'; -import OperatorStatusParameter from './user/operator-status-parameter.js'; import { createCommandsObject } from './commands.js'; import { createEventsObject } from './events.js'; +import { getPossibleStatusesFromParameter, getRoleFromParameter, isOperatorStatusParameter } from './user/operator-status-parameter.js'; +import { getMissionActionFromParameter, getPossibleMissionActionStatusesFromParameter, isMissionStatusParameter } from './mission-status/mission-status-parameter.js'; const YAMCS_API_MAP = { 'space-systems': 'spaceSystems', 'parameters': 'parameters' }; -const operatorStatusParameter = new OperatorStatusParameter(); export default class YamcsObjectProvider { - constructor(openmct, url, instance, folderName, roleStatusTelemetry, pollQuestionParameter, pollQuestionTelemetry, realtimeTelemetryProvider, processor = 'realtime') { + constructor(openmct, url, instance, folderName, roleStatusTelemetry, missionStatusTelemetry, pollQuestionParameter, pollQuestionTelemetry, realtimeTelemetryProvider, processor = 'realtime', getDictionaryRequestOptions = () => Promise.resolve({})) { this.openmct = openmct; this.url = url; this.instance = instance; @@ -52,7 +52,9 @@ export default class YamcsObjectProvider { this.dictionary = {}; this.limitOverrides = {}; this.dictionaryPromise = null; + this.getDictionaryRequestOptions = getDictionaryRequestOptions; this.roleStatusTelemetry = roleStatusTelemetry; + this.missionStatusTelemetry = missionStatusTelemetry; this.pollQuestionParameter = pollQuestionParameter; this.pollQuestionTelemetry = pollQuestionTelemetry; @@ -180,9 +182,10 @@ export default class YamcsObjectProvider { #getTelemetryDictionary() { if (!this.dictionaryPromise) { - this.dictionaryPromise = this.#loadTelemetryDictionary(this.url, this.instance, this.folderName) + this.dictionaryPromise = this.#loadTelemetryDictionary() .finally(() => { this.roleStatusTelemetry.dictionaryLoadComplete(); + this.missionStatusTelemetry.dictionaryLoadComplete(); }); } @@ -193,8 +196,11 @@ export default class YamcsObjectProvider { const operation = 'parameters?details=yes&limit=1000'; const parameterUrl = this.url + 'api/mdb/' + this.instance + '/' + operation; const url = this.#getMdbUrl('space-systems'); - const spaceSystems = await accumulateResults(url, {}, 'spaceSystems', []); - const parameters = await accumulateResults(parameterUrl, {}, 'parameters', []); + + const requestOptions = await this.getDictionaryRequestOptions(); + + const spaceSystems = await accumulateResults(url, requestOptions, 'spaceSystems', []); + const parameters = await accumulateResults(parameterUrl, requestOptions, 'parameters', []); /* Sort the space systems by name, so that the children of the root object are in sorted order. */ @@ -324,7 +330,9 @@ export default class YamcsObjectProvider { if (defaultAlarm?.staticAlarmRange) { return getLimitFromAlarmRange(defaultAlarm.staticAlarmRange); } else { - throw new Error(`Passed alarm has invalid object syntax for limit conversion`, defaultAlarm); + console.warn('Open MCT supports default static alarms only at this time', defaultAlarm); + + return {}; } } @@ -395,18 +403,31 @@ export default class YamcsObjectProvider { telemetryValue.unit = unitSuffix; } - if (operatorStatusParameter.isOperatorStatusParameter(parameter)) { - const role = operatorStatusParameter.getRoleFromParameter(parameter); + if (isOperatorStatusParameter(parameter)) { + const role = getRoleFromParameter(parameter); if (!role) { throw new Error(`Operator Status Parameter "${parameter.qualifiedName}" does not specify a role`); } - const possibleStatuses = operatorStatusParameter.getPossibleStatusesFromParameter(parameter); + const possibleStatuses = getPossibleStatusesFromParameter(parameter); possibleStatuses.forEach(state => this.roleStatusTelemetry.addStatus(state)); this.roleStatusTelemetry.addStatusRole(role); this.roleStatusTelemetry.setTelemetryObjectForRole(role, obj); } + if (isMissionStatusParameter(parameter)) { + const action = getMissionActionFromParameter(parameter); + if (!action) { + throw new Error(`Mission Status Parameter "${parameter.qualifiedName}" does not specify a mission action`); + } + + const possibleStatuses = getPossibleMissionActionStatusesFromParameter(parameter); + possibleStatuses.forEach(status => this.missionStatusTelemetry.addStatus(status)); + this.missionStatusTelemetry.addMissionStatusParameterName(parameter.qualifiedName); + this.missionStatusTelemetry.addMissionAction(action); + this.missionStatusTelemetry.setTelemetryObjectForAction(action, obj); + } + if (this.pollQuestionParameter.isPollQuestionParameter(parameter)) { this.pollQuestionParameter.setPollQuestionParameter(parameter); this.pollQuestionTelemetry.setTelemetryObject(obj); @@ -431,7 +452,7 @@ export default class YamcsObjectProvider { }); } - if (this.#isArray(parameter)) { + if (this.#isArray(parameter) || this.#isBinary(parameter)) { telemetryValue.format = parameter.type.engType; } @@ -484,6 +505,10 @@ export default class YamcsObjectProvider { return parameter?.type?.engType === 'enumeration'; } + #isBinary(parameter) { + return parameter?.type?.engType === 'binary'; + } + #isArray(parameter) { return parameter?.type?.engType.endsWith('[]'); } diff --git a/src/providers/realtime-provider.js b/src/providers/realtime-provider.js index 2a00c09b..7889b577 100644 --- a/src/providers/realtime-provider.js +++ b/src/providers/realtime-provider.js @@ -24,25 +24,26 @@ import { SUBSCRIBE, UNSUBSCRIBE } from './messages.js'; import { OBJECT_TYPES, DATA_TYPES, - AGGREGATE_TYPE, METADATA_TIME_KEY, STALENESS_STATUS_MAP, - MDB_OBJECT + MDB_OBJECT, + MDB_CHANGES_PARAMETER_TYPE } from '../const.js'; import { buildStalenessResponseObject, idToQualifiedName, - qualifiedNameToId, - getValue, addLimitInformation, - getLimitFromAlarmRange + getLimitFromAlarmRange, + convertYamcsToOpenMctDatum } from '../utils.js'; import { commandToTelemetryDatum } from './commands.js'; import { eventToTelemetryDatum, eventShouldBeFiltered } from './events.js'; -const FALLBACK_AND_WAIT_MS = [1000, 5000, 5000, 10000, 10000, 30000]; export default class RealtimeProvider { - constructor(url, instance, processor = 'realtime') { + #socketWorker = null; + #openmct; + + constructor(openmct, url, instance, processor = 'realtime', maxBatchWait = 1000, maxBatchSize = 15) { this.url = url; this.instance = instance; this.processor = processor; @@ -57,9 +58,56 @@ export default class RealtimeProvider { this.lastSubscriptionId = 1; this.subscriptionsByCall = new Map(); this.subscriptionsById = {}; + this.#socketWorker = new openmct.telemetry.BatchingWebSocket(openmct); + this.#openmct = openmct; + this.#setBatchingStrategy(maxBatchWait, maxBatchSize); this.addSupportedObjectTypes(Object.values(OBJECT_TYPES)); this.addSupportedDataTypes(Object.values(DATA_TYPES)); + const setCallFromClockIfNecessary = this.#setCallFromClockIfNecessary.bind(this); + + openmct.time.on('clock', setCallFromClockIfNecessary); + + openmct.once('destroy', () => { + openmct.time.off('clock', setCallFromClockIfNecessary); + }); + } + + #setCallFromClockIfNecessary(clock) { + if (clock === undefined) { + this.unsetCall(); + } + + if (clock.key === 'remote-clock') { + this.#setCallFromClock(clock); + } + } + #setBatchingStrategy(maxBatchWait, maxBatchSize) { + // This strategy batches parameter value messages + this.#socketWorker.setBatchingStrategy({ + /* istanbul ignore next */ + shouldBatchMessage: /* istanbul ignore next */ (message) => { + // If a parameter value message, the message type will be "parameters" + // The type field is always located at a character offset of 13 and + // if it is "parameters" will be 10 characters long. + const type = message.substring(13, 23); + + return type === 'parameters'; + }, + /* istanbul ignore next */ + getBatchIdFromMessage: /* istanbul ignore next */ (message) => { + // Only dealing with "parameters" messages at this point. The call number + // identifies the parameter, and is used for batching. Will be located + // at a character offset of 36. Because it is of indeterminate length + // (we don't know the number) we have to do a sequential search forward + // from the 37th character for a terminating ",". + const callNumber = message.substring(36, message.indexOf(",", 37)); + + return callNumber; + } + }); + this.#socketWorker.setMaxBatchWait(maxBatchWait); + this.#socketWorker.setMaxBatchSize(maxBatchSize); } addSupportedObjectTypes(types) { @@ -69,7 +117,6 @@ export default class RealtimeProvider { addSupportedDataTypes(dataTypes) { dataTypes.forEach(dataType => this.supportedDataTypes[dataType] = dataType); } - supportsSubscribe(domainObject) { return this.isSupportedObjectType(domainObject.type); } @@ -111,7 +158,7 @@ export default class RealtimeProvider { if (subscriptionDetails) { this.sendUnsubscribeMessage(subscriptionDetails); - this.subscriptionsByCall.delete(subscriptionDetails.call); + this.subscriptionsByCall.delete(subscriptionDetails.call.toString()); delete this.subscriptionsById[id]; } }; @@ -136,7 +183,7 @@ export default class RealtimeProvider { this.sendUnsubscribeMessage(subscriptionDetails); if (this.subscriptionsById[id]) { - this.subscriptionsByCall.delete(this.subscriptionsById[id].call); + this.subscriptionsByCall.delete(this.subscriptionsById[id].call.toString()); delete this.subscriptionsById[id]; } }; @@ -162,44 +209,64 @@ export default class RealtimeProvider { const domainObject = subscriptionDetails.domainObject; const message = SUBSCRIBE[domainObject.type](subscriptionDetails); - this.sendOrQueueMessage(message); + this.sendMessage(message); } sendUnsubscribeMessage(subscriptionDetails) { let message = UNSUBSCRIBE(subscriptionDetails); - this.sendOrQueueMessage(message); + this.sendMessage(message); } - reconnect() { - this.subscriptionsByCall.clear(); + #setCallFromClock(clock) { + const correspondingSubscription = Object.values(this.subscriptionsById).find(subscription => { + return subscription.domainObject.identifier.key === clock.identifier.key; + }); - if (this.reconnectTimeout) { - return; + if (correspondingSubscription !== undefined) { + this.remoteClockCallNumber = correspondingSubscription.call.toString(); + } else { + delete this.remoteClockCallNumber; } + } - this.reconnectTimeout = setTimeout(() => { - this.connect(); - delete this.reconnectTimeout; - }, FALLBACK_AND_WAIT_MS[this.currentWaitIndex]); + #processBatchQueue(batchQueue, call) { + let subscriptionDetails = this.subscriptionsByCall.get(call); + let telemetryData = []; - if (this.currentWaitIndex < FALLBACK_AND_WAIT_MS.length - 1) { - this.currentWaitIndex++; + // possibly cancelled + if (!subscriptionDetails) { + return; } - } - sendOrQueueMessage(request) { - if (this.connected) { - try { - this.sendMessage(request); - } catch (error) { - this.connected = false; - this.requests.push(request); - console.error("🚨 Error while attempting to send to websocket, closing websocket", error); - this.socket.close(); - } - } else { - this.requests.push(request); + batchQueue.forEach((rawMessage) => { + const message = JSON.parse(rawMessage); + const values = message.data.values || []; + const parentName = subscriptionDetails.domainObject.name; + + values.forEach(parameter => { + const datum = convertYamcsToOpenMctDatum(parameter, parentName); + + if (this.observingStaleness[subscriptionDetails.name] !== undefined) { + const status = STALENESS_STATUS_MAP[parameter.acquisitionStatus]; + + if (this.observingStaleness[subscriptionDetails.name].response.isStale !== status) { + const stalenesResponseObject = buildStalenessResponseObject( + status, + parameter[METADATA_TIME_KEY] + ); + this.observingStaleness[subscriptionDetails.name].response = stalenesResponseObject; + this.observingStaleness[subscriptionDetails.name].callback(stalenesResponseObject); + } + } + + addLimitInformation(parameter, datum); + telemetryData.push(datum); + }); + }); + + if (telemetryData.length > 0) { + subscriptionDetails.callback(telemetryData); } } @@ -211,80 +278,64 @@ export default class RealtimeProvider { let wsUrl = `${this.url}`; this.lastSubscriptionId = 1; this.connected = false; - this.socket = new WebSocket(wsUrl); - this.socket.onopen = () => { - clearTimeout(this.reconnectTimeout); + this.#socketWorker.connect(wsUrl); + this.#socketWorker.addEventListener('reconnected', () => { + this.resubscribeToAll(); + }); + + this.#socketWorker.addEventListener('batch', (batchEvent) => { + const batch = batchEvent.detail; - this.connected = true; - console.debug(`🔌 Established websocket connection to ${wsUrl}`); + let remoteClockValue; + // If remote clock active, process its value before any telemetry values to ensure the bounds are always up to date. + if (this.remoteClockCallNumber !== undefined) { + remoteClockValue = batch[this.remoteClockCallNumber]; + if (remoteClockValue !== undefined) { + this.#processBatchQueue(batch[this.remoteClockCallNumber], this.remoteClockCallNumber); - this.currentWaitIndex = 0; - this.resubscribeToAll(); - this.flushQueue(); - }; + // Delete so we don't process it twice. + delete batch[this.remoteClockCallNumber]; + } + } - this.socket.onmessage = (event) => { - const message = JSON.parse(event.data); + Object.keys(batch).forEach((call) => { + this.#processBatchQueue(batch[call], call); + }); + }); + this.#socketWorker.addEventListener('message', (messageEvent) => { + const message = JSON.parse(messageEvent.detail); if (!this.isSupportedDataType(message.type)) { return; } const isReply = message.type === DATA_TYPES.DATA_TYPE_REPLY; + const call = message.call; let subscriptionDetails; if (isReply) { const id = message.data.replyTo; - const call = message.call; subscriptionDetails = this.subscriptionsById[id]; subscriptionDetails.call = call; - this.subscriptionsByCall.set(call, subscriptionDetails); + // Subsequent retrieval uses a string, so for performance reasons we use a string as a key. + this.subscriptionsByCall.set(call.toString(), subscriptionDetails); + + const remoteClockIdentifier = this.#openmct.time.getClock()?.identifier; + const isRemoteClockActive = remoteClockIdentifier !== undefined; + + if (isRemoteClockActive && subscriptionDetails.domainObject.identifier.key === remoteClockIdentifier.key) { + this.remoteClockCallNumber = call.toString(); + } } else { - subscriptionDetails = this.subscriptionsByCall.get(message.call); + subscriptionDetails = this.subscriptionsByCall.get(message.call.toString()); // possibly cancelled if (!subscriptionDetails) { return; } - if (this.isTelemetryMessage(message)) { - let values = message.data.values || []; - let parentName = subscriptionDetails.domainObject.name; - - values.forEach(parameter => { - let datum = { - id: qualifiedNameToId(subscriptionDetails.name), - timestamp: parameter[METADATA_TIME_KEY] - }; - let value = getValue(parameter, parentName); - - if (this.observingStaleness[subscriptionDetails.name] !== undefined) { - const status = STALENESS_STATUS_MAP[parameter.acquisitionStatus]; - - if (this.observingStaleness[subscriptionDetails.name].response.isStale !== status) { - const stalenesResponseObject = buildStalenessResponseObject( - status, - parameter[METADATA_TIME_KEY] - ); - this.observingStaleness[subscriptionDetails.name].response = stalenesResponseObject; - this.observingStaleness[subscriptionDetails.name].callback(stalenesResponseObject); - } - } - - if (parameter.engValue.type !== AGGREGATE_TYPE) { - datum.value = value; - } else { - datum = { - ...datum, - ...value - }; - } - - addLimitInformation(parameter, datum); - subscriptionDetails.callback(datum); - }); - } else if (this.isCommandMessage(message)) { + if (this.isCommandMessage(message)) { const datum = commandToTelemetryDatum(message.data); subscriptionDetails.callback(datum); } else if (this.isEventMessage(message)) { @@ -295,6 +346,10 @@ export default class RealtimeProvider { subscriptionDetails.callback(datum); } } else if (this.isMdbChangesMessage(message)) { + if (!this.isParameterType(message)) { + return; + } + const parameterName = message.data.parameterOverride.parameter; if (this.observingLimitChanges[parameterName] !== undefined) { const alarmRange = message.data.parameterOverride.defaultAlarm?.staticAlarmRange ?? []; @@ -308,20 +363,7 @@ export default class RealtimeProvider { subscriptionDetails.callback(message.data); } } - }; - - this.socket.onerror = (error) => { - console.error(`🚨 Websocket error, closing websocket`, error); - this.socket.close(); - }; - - this.socket.onclose = () => { - console.warn('🚪 Websocket closed. Attempting to reconnect...'); - this.connected = false; - this.socket = null; - - this.reconnect(); - }; + }); } resubscribeToAll() { @@ -330,30 +372,8 @@ export default class RealtimeProvider { }); } - flushQueue() { - let shouldCloseWebsocket = false; - this.requests = this.requests.filter((request) => { - try { - this.sendMessage(request); - } catch (error) { - this.connected = false; - console.error('🚨 Error while attempting to send to websocket, closing websocket', error); - - shouldCloseWebsocket = true; - - return true; - } - - return false; - }); - - if (shouldCloseWebsocket) { - this.socket.close(); - } - } - sendMessage(message) { - this.socket.send(message); + this.#socketWorker.sendMessage(message); } isTelemetryMessage(message) { @@ -371,4 +391,8 @@ export default class RealtimeProvider { isMdbChangesMessage(message) { return message.type === DATA_TYPES.DATA_TYPE_MDB_CHANGES; } + + isParameterType(message) { + return message.data?.type === MDB_CHANGES_PARAMETER_TYPE; + } } diff --git a/src/providers/staleness-provider.js b/src/providers/staleness-provider.js index 29dad810..2557cf91 100644 --- a/src/providers/staleness-provider.js +++ b/src/providers/staleness-provider.js @@ -24,8 +24,7 @@ import { OBJECT_TYPES, STALENESS_STATUS_MAP } from '../const.js'; import { buildStalenessResponseObject } from '../utils.js'; export default class YamcsStalenessProvider { - constructor(openmct, realtimeTelemetryProvider, latestTelemetryProvider) { - this.openmct = openmct; + constructor(realtimeTelemetryProvider, latestTelemetryProvider) { this.realtimeTelemetryProvider = realtimeTelemetryProvider; this.latestTelemetryProvider = latestTelemetryProvider; } diff --git a/src/providers/user/operator-status-parameter.js b/src/providers/user/operator-status-parameter.js index 34d7eca4..4bab4385 100644 --- a/src/providers/user/operator-status-parameter.js +++ b/src/providers/user/operator-status-parameter.js @@ -22,21 +22,19 @@ const OPERATOR_STATUS_TYPE = 'yamcs.operatorStatus'; -export default class OperatorStatusParameter { - isOperatorStatusParameter(parameter) { - const aliases = parameter.alias; +export function isOperatorStatusParameter(parameter) { + const aliases = parameter.alias; - return aliases !== undefined - && aliases.some(alias => alias.name === OPERATOR_STATUS_TYPE); - } + return aliases !== undefined + && aliases.some(alias => alias.name === OPERATOR_STATUS_TYPE); +} - getRoleFromParameter(parameter) { - const aliases = parameter.alias; +export function getRoleFromParameter(parameter) { + const aliases = parameter.alias; - return aliases.find(alias => alias.namespace === 'OpenMCT:role')?.name; - } + return aliases.find(alias => alias.namespace === 'OpenMCT:role')?.name; +} - getPossibleStatusesFromParameter(parameter) { - return parameter.type.enumValue; - } +export function getPossibleStatusesFromParameter(parameter) { + return parameter.type.enumValue; } diff --git a/src/providers/user/user-provider.js b/src/providers/user/user-provider.js index ab93ec9d..d14db8f0 100644 --- a/src/providers/user/user-provider.js +++ b/src/providers/user/user-provider.js @@ -24,7 +24,7 @@ import createYamcsUser from './createYamcsUser.js'; import { EventEmitter } from 'eventemitter3'; export default class UserProvider extends EventEmitter { - constructor(openmct, {userEndpoint, roleStatus, latestTelemetryProvider, realtimeTelemetryProvider, pollQuestionParameter, pollQuestionTelemetry}) { + constructor(openmct, {userEndpoint, roleStatus, latestTelemetryProvider, pollQuestionParameter, pollQuestionTelemetry, missionStatus}) { super(); this.openmct = openmct; @@ -32,12 +32,13 @@ export default class UserProvider extends EventEmitter { this.user = undefined; this.loggedIn = false; this.roleStatus = roleStatus; + this.missionStatus = missionStatus; this.pollQuestionParameter = pollQuestionParameter; this.pollQuestionTelemetry = pollQuestionTelemetry; this.unsubscribeStatus = {}; + this.unsubscribeMissionStatus = {}; this.latestTelemetryProvider = latestTelemetryProvider; - this.realtimeTelemetryProvider = realtimeTelemetryProvider; this.YamcsUser = createYamcsUser(openmct.user.User); this.openmct.once('destroy', () => { @@ -92,6 +93,29 @@ export default class UserProvider extends EventEmitter { }); } + async canSetMissionStatus() { + const user = await this.getCurrentUser(); + const writeParameters = user.getWriteParameters(); + + const areParametersStatus = await Promise.all( + writeParameters.map(parameterName => this.missionStatus.isMissionStatusParameterName(parameterName)) + ); + + return areParametersStatus.some(isParameterStatus => isParameterStatus); + } + + async getPossibleMissionActions() { + const possibleActions = await this.missionStatus.getAllMissionActions(); + + return possibleActions; + } + + async getPossibleMissionActionStatuses() { + const statuses = await this.missionStatus.getPossibleMissionStatuses(); + + return statuses; + } + async canSetPollQuestion() { const user = await this.getCurrentUser(); const writeParameters = user.getWriteParameters(); @@ -109,10 +133,31 @@ export default class UserProvider extends EventEmitter { return success; } + async getStatusForMissionAction(action) { + const missionStatusTelemetryObject = await this.missionStatus.getTelemetryObjectForAction(action); + if (this.unsubscribeMissionStatus[action] === undefined) { + this.unsubscribeMissionStatus[action] = this.openmct.telemetry.subscribe(missionStatusTelemetryObject, (datum) => { + this.emit('missionActionStatusChange', { + action, + status: this.missionStatus.toStatusFromTelemetry(missionStatusTelemetryObject, datum) + }); + }); + } + + const status = await this.latestTelemetryProvider.requestLatest(missionStatusTelemetryObject); + if (status !== undefined) { + return this.missionStatus.toStatusFromTelemetry(missionStatusTelemetryObject, status); + } else { + const defaultStatus = await this.missionStatus.getDefaultStatusForAction(action); + + return defaultStatus; + } + } + async getStatusForRole(role) { const statusTelemetryObject = await this.roleStatus.getTelemetryObjectForRole(role); if (this.unsubscribeStatus[role] === undefined) { - this.unsubscribeStatus[role] = this.realtimeTelemetryProvider.subscribe(statusTelemetryObject, (datum) => { + this.unsubscribeStatus[role] = this.openmct.telemetry.subscribe(statusTelemetryObject, (datum) => { this.emit('statusChange', { role, status: this.roleStatus.toStatusFromTelemetry(statusTelemetryObject, datum) @@ -142,6 +187,12 @@ export default class UserProvider extends EventEmitter { return success; } + async setStatusForMissionAction(action, status) { + const success = await this.missionStatus.setStatusForMissionAction(action, status); + + return success; + } + async getPossibleStatuses() { const possibleStatuses = await this.roleStatus.getPossibleStatuses(); @@ -152,7 +203,7 @@ export default class UserProvider extends EventEmitter { const pollQuestionTelemetryObject = await this.pollQuestionTelemetry.getTelemetryObject(); if (this.unsubscribePollQuestion === undefined) { - this.unsubscribePollQuestion = this.realtimeTelemetryProvider.subscribe(pollQuestionTelemetryObject, (datum) => { + this.unsubscribePollQuestion = this.openmct.telemetry.subscribe(pollQuestionTelemetryObject, (datum) => { const formattedPollQuestion = this.pollQuestionTelemetry.toPollQuestionObjectFromTelemetry(pollQuestionTelemetryObject, datum); this.emit("pollQuestionChange", formattedPollQuestion); }); @@ -200,4 +251,3 @@ export default class UserProvider extends EventEmitter { } } - diff --git a/src/utils.js b/src/utils.js index 7f268d37..04ccabc1 100644 --- a/src/utils.js +++ b/src/utils.js @@ -19,7 +19,7 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ -import { AGGREGATE_TYPE, UNSUPPORTED_TYPE } from './const.js'; +import {AGGREGATE_TYPE, UNSUPPORTED_TYPE, METADATA_TIME_KEY, MDB_CHANGES_PARAMETER_TYPE} from './const.js'; import limitConfig from "./limits-config.json"; function idToQualifiedName(id) { @@ -211,12 +211,14 @@ async function getLimitOverrides(url) { const overrides = await requestLimitOverrides(url); overrides.forEach((override) => { - const parameterOverride = override.parameterOverride; - const parameter = parameterOverride.parameter; - const alarmRange = parameterOverride?.defaultAlarm?.staticAlarmRange ?? []; - - limitOverrides[parameter] = getLimitFromAlarmRange(alarmRange); + if (override.type === MDB_CHANGES_PARAMETER_TYPE) { + const parameter = override?.parameterOverride?.parameter; + const alarmRange = override?.parameterOverride?.defaultAlarm?.staticAlarmRange ?? []; + if (parameter && alarmRange) { + limitOverrides[parameter] = getLimitFromAlarmRange(alarmRange); + } + } }); return limitOverrides; @@ -362,6 +364,24 @@ function flattenObjectArray(array, baseObj = {}) { }, baseObj); } +function convertYamcsToOpenMctDatum(parameter, parentName) { + let datum = { + timestamp: parameter[METADATA_TIME_KEY] + }; + const value = getValue(parameter, parentName); + + if (parameter.engValue.type !== AGGREGATE_TYPE) { + datum.value = value; + } else { + datum = { + ...datum, + ...value + }; + } + + return datum; +} + export { buildStalenessResponseObject, getLimitFromAlarmRange, @@ -373,5 +393,6 @@ export { accumulateResults, addLimitInformation, yieldResults, - getLimitOverrides + getLimitOverrides, + convertYamcsToOpenMctDatum }; diff --git a/tests/README.md b/tests/README.md index d4daf3c0..2d1c1041 100644 --- a/tests/README.md +++ b/tests/README.md @@ -3,6 +3,7 @@ This project is using Open MCT's e2e-as-a-dependency model. To learn more, please see the official documentation on the [Official README](https://github.com/nasa/openmct/blob/master/e2e/README.md) ## How to Run Locally + To run the tests, we recommend the following workflow which bridges two separate github repos: yamcs/quickstart and openmct-yamcs (this one). @@ -11,8 +12,7 @@ yamcs/quickstart and openmct-yamcs (this one). 3. `make all` in yamcs/quickstart 4. `cd openmct-yamcs` to move out of yamcs/quickstart 5. `npm install` in openmct-yamcs -6. `npx playwright@1.39.0 install chromium` in openmct-yamcs -7. Sanity test that yamcs is up with `npm run wait-for-yamcs` in openmct-yamcs -8. `npm run build:example` -9. `npm run test:getopensource` -10. `npm run test:e2e:quickstart:local` +6. Sanity test that yamcs is up with `npm run wait-for-yamcs` in openmct-yamcs +7. `npm run test:getopensource` +8. `npm run build:example` or `npm run build:example:master` +9. `npm run test:e2e:watch` diff --git a/tests/e2e/playwright-quickstart.config.js b/tests/e2e/playwright-quickstart.config.js index b21e82c3..525f359f 100644 --- a/tests/e2e/playwright-quickstart.config.js +++ b/tests/e2e/playwright-quickstart.config.js @@ -5,7 +5,7 @@ const config = { retries: 1, testDir: '.', - testMatch: '**/*.e2e.spec.js', + testMatch: /.*\.e2e\.spec\.(mjs|js)$/, timeout: 30 * 1000, use: { headless: false, @@ -18,6 +18,7 @@ const config = { failOnConsoleError: false }, webServer: { + cwd: '../', command: 'npm run start:coverage', url: 'http://localhost:9000/#', timeout: 120 * 1000, diff --git a/tests/e2e/yamcs/faultManagement.e2e.spec.mjs b/tests/e2e/yamcs/faultManagement.e2e.spec.mjs new file mode 100644 index 00000000..d3713788 --- /dev/null +++ b/tests/e2e/yamcs/faultManagement.e2e.spec.mjs @@ -0,0 +1,43 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2024, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +/* +Staleness Specific Tests +*/ + +import { pluginFixtures } from 'openmct-e2e'; +const { test } = pluginFixtures; + +test.describe.fixme("Fault management tests @yamcs", () => { + // eslint-disable-next-line require-await + test('Show faults ', async ({ page }) => { + test.step('for historic alarm violations', () => { + // Navigate to fault management in the tree + // Expect that there is indication of a fault + }); + + test.step('show historic and live faults when new alarms are triggered in real time', () => { + // Wait for new data + // Expect that live faults are displayed + }); + }); +}); diff --git a/tests/e2e/yamcs/filters.e2e.spec.js b/tests/e2e/yamcs/filters.e2e.spec.mjs similarity index 96% rename from tests/e2e/yamcs/filters.e2e.spec.js rename to tests/e2e/yamcs/filters.e2e.spec.mjs index 66534072..09dc18a0 100644 --- a/tests/e2e/yamcs/filters.e2e.spec.js +++ b/tests/e2e/yamcs/filters.e2e.spec.mjs @@ -24,8 +24,9 @@ Filter Specific Tests */ -import { test, expect } from '../opensource/pluginFixtures.js'; -import { createDomainObjectWithDefaults } from '../opensource/appActions.js'; +import { pluginFixtures, appActions } from 'openmct-e2e'; +const { test, expect } = pluginFixtures; +const { createDomainObjectWithDefaults } = appActions; test.describe("Filter tests @yamcs", () => { test('Can filter events by severity', async ({ page }) => { diff --git a/tests/e2e/yamcs/historicalData.e2e.spec.js b/tests/e2e/yamcs/historicalData.e2e.spec.mjs similarity index 61% rename from tests/e2e/yamcs/historicalData.e2e.spec.js rename to tests/e2e/yamcs/historicalData.e2e.spec.mjs index a17642eb..f3c41bcc 100644 --- a/tests/e2e/yamcs/historicalData.e2e.spec.js +++ b/tests/e2e/yamcs/historicalData.e2e.spec.mjs @@ -24,36 +24,36 @@ Network Specific Tests */ -import { test, expect } from '../opensource/pluginFixtures.js'; -import { setFixedTimeMode } from '../opensource/appActions.js'; +import { pluginFixtures, appActions } from 'openmct-e2e'; +const { test, expect } = pluginFixtures; +const { setFixedTimeMode } = appActions; test.describe("Samples endpoint with useRawValue search param @yamcs", () => { // Collect all request events, specifically for YAMCS - let networkRequests = []; let filteredRequests = []; - - test('When in plot view, samples endpoint is used for enum type parameters with the useRawValue parameter', async ({ page }) => { + let networkRequests = []; + test.beforeEach(async ({ page }) => { page.on('request', (request) => networkRequests.push(request)); // Go to baseURL - await page.goto("./", { waitUntil: "networkidle" }); - + await page.goto("./", { waitUntil: "domcontentloaded" }); + await expect(page.getByText('Loading...')).toBeHidden(); // Change to fixed time await setFixedTimeMode(page); - const myProjectTreeItem = page.locator('.c-tree__item').filter({ hasText: 'myproject'}); - await expect(myProjectTreeItem).toBeVisible(); - const firstMyProjectTriangle = myProjectTreeItem.first().locator('span.c-disclosure-triangle'); - await firstMyProjectTriangle.click(); - const secondMyProjectTriangle = myProjectTreeItem.nth(1).locator('span.c-disclosure-triangle'); - await secondMyProjectTriangle.click(); - - await page.waitForLoadState('networkidle'); + // Expand myproject and subfolder myproject + await page.getByLabel('Expand myproject').click(); + await page.getByLabel('Expand myproject').click(); + // await expect(page.getByText('Loading...')).toBeHidden(); networkRequests = []; - await page.locator('text=Enum_Para_1').first().click(); - await page.waitForLoadState('networkidle'); + filteredRequests = []; + }); + + test('When in plot view, samples endpoint is used for enum type parameters with the useRawValue parameter', async ({ page }) => { + await page.getByLabel('Navigate to Enum_Para_1 yamcs').click(); // wait for debounced requests in YAMCS Latest Telemetry Provider to finish - await new Promise(resolve => setTimeout(resolve, 500)); + // FIXME: can we use waitForRequest? + await page.waitForTimeout(500); filteredRequests = filterNonFetchRequests(networkRequests); @@ -67,29 +67,11 @@ test.describe("Samples endpoint with useRawValue search param @yamcs", () => { }); test('When in plot view, samples endpoint is used for scalar (number) type parameters with no useRawValue parameter', async ({ page }) => { - networkRequests = []; - filteredRequests = []; - page.on('request', (request) => networkRequests.push(request)); - // Go to baseURL - await page.goto("./", { waitUntil: "networkidle" }); - - // Change to fixed time - await setFixedTimeMode(page); - - const myProjectTreeItem = page.locator('.c-tree__item').filter({ hasText: 'myproject'}); - await expect(myProjectTreeItem).toBeVisible(); - const firstMyProjectTriangle = myProjectTreeItem.first().locator('span.c-disclosure-triangle'); - await firstMyProjectTriangle.click(); - const secondMyProjectTriangle = myProjectTreeItem.nth(1).locator('span.c-disclosure-triangle'); - await secondMyProjectTriangle.click(); - - await page.waitForLoadState('networkidle'); - networkRequests = []; - await page.locator('text=CCSDS_Packet_Length').first().click(); - await page.waitForLoadState('networkidle'); + await page.getByLabel('Navigate to CCSDS_Packet_Length yamcs').click(); // wait for debounced requests in YAMCS Latest Telemetry Provider to finish - await new Promise(resolve => setTimeout(resolve, 500)); + // FIXME: can we use waitForRequest? + await page.waitForTimeout(500); filteredRequests = filterNonFetchRequests(networkRequests); @@ -103,34 +85,17 @@ test.describe("Samples endpoint with useRawValue search param @yamcs", () => { }); test('When in table view, samples endpoint and useRawValue are not used for scalar (number) type parameters', async ({ page }) => { - networkRequests = []; - filteredRequests = []; - page.on('request', (request) => networkRequests.push(request)); - // Go to baseURL - await page.goto("./", { waitUntil: "networkidle" }); - - // Change to fixed time - await setFixedTimeMode(page); - - const myProjectTreeItem = page.locator('.c-tree__item').filter({ hasText: 'myproject'}); - await expect(myProjectTreeItem).toBeVisible(); - const firstMyProjectTriangle = myProjectTreeItem.first().locator('span.c-disclosure-triangle'); - await firstMyProjectTriangle.click(); - const secondMyProjectTriangle = myProjectTreeItem.nth(1).locator('span.c-disclosure-triangle'); - await secondMyProjectTriangle.click(); - - await page.waitForLoadState('networkidle'); - await page.locator('text=Enum_Para_1').first().click(); - await page.waitForLoadState('networkidle'); + await page.getByLabel('Navigate to Enum_Para_1 yamcs').click(); //switch to table view networkRequests = []; - await page.locator("button[title='Change the current view']").click(); + await page.getByLabel('Open the View Switcher Menu').click(); await page.getByRole('menuitem', { name: /Telemetry Table/ }).click(); await page.waitForLoadState('networkidle'); // wait for debounced requests in YAMCS Latest Telemetry Provider to finish - await new Promise(resolve => setTimeout(resolve, 500)); + // FIXME: can we use waitForRequest? + await page.waitForTimeout(500); filteredRequests = filterNonFetchRequests(networkRequests); @@ -146,35 +111,40 @@ test.describe("Samples endpoint with useRawValue search param @yamcs", () => { expect(nonSampleRequests.length).toBe(filteredRequests.length); }); - test('When in table view, samples endpoint and useRawValue are not used for enum type parameters', async ({ page }) => { + test('When in table view and in unlimited mode, requests contain the "order=desc" parameter', async ({ page }) => { + await page.getByLabel('Navigate to Enum_Para_1 yamcs').click(); + + //switch to table view networkRequests = []; - filteredRequests = []; - page.on('request', (request) => networkRequests.push(request)); - // Go to baseURL - await page.goto("./", { waitUntil: "networkidle" }); + await page.getByLabel('Open the View Switcher Menu').click(); + await page.getByRole('menuitem', { name: /Telemetry Table/ }).click(); + await page.waitForLoadState('networkidle'); - // Change to fixed time - await setFixedTimeMode(page); + // wait for debounced requests in YAMCS Latest Telemetry Provider to finish + // FIXME: can we use waitForRequest? + await page.waitForTimeout(500); - const myProjectTreeItem = page.locator('.c-tree__item').filter({ hasText: 'myproject'}); - await expect(myProjectTreeItem).toBeVisible(); - const firstMyProjectTriangle = myProjectTreeItem.first().locator('span.c-disclosure-triangle'); - await firstMyProjectTriangle.click(); - const secondMyProjectTriangle = myProjectTreeItem.nth(1).locator('span.c-disclosure-triangle'); - await secondMyProjectTriangle.click(); + filteredRequests = filterNonFetchRequests(networkRequests); + // Verify we are in "Limited" mode + await expect(page.getByRole('button', { name: 'SHOW UNLIMITED' })).toBeVisible(); - await page.waitForLoadState('networkidle'); - await page.locator('text=Enum_Para_1').first().click(); - await page.waitForLoadState('networkidle'); + // Check if any request URL contains the 'order=desc' parameter + const hasOrderDesc = filteredRequests.some(request => request.url().includes('order=desc')); + expect(hasOrderDesc).toBe(true); + }); + + test('When in table view, samples endpoint and useRawValue are not used for enum type parameters', async ({ page }) => { + await page.getByLabel('Navigate to Enum_Para_1 yamcs').click(); //switch to table view networkRequests = []; - await page.locator("button[title='Change the current view']").click(); + await page.getByLabel('Open the View Switcher Menu').click(); await page.getByRole('menuitem', { name: /Telemetry Table/ }).click(); await page.waitForLoadState('networkidle'); // wait for debounced requests in YAMCS Latest Telemetry Provider to finish - await new Promise(resolve => setTimeout(resolve, 500)); + // FIXME: can we use waitForRequest? + await page.waitForTimeout(500); filteredRequests = filterNonFetchRequests(networkRequests); diff --git a/tests/e2e/yamcs/limits.e2e.spec.js b/tests/e2e/yamcs/limits.e2e.spec.mjs similarity index 62% rename from tests/e2e/yamcs/limits.e2e.spec.js rename to tests/e2e/yamcs/limits.e2e.spec.mjs index c7144c41..3d934bcf 100644 --- a/tests/e2e/yamcs/limits.e2e.spec.js +++ b/tests/e2e/yamcs/limits.e2e.spec.mjs @@ -24,10 +24,21 @@ MDB Limits Specific Tests */ -import { test, expect } from '../opensource/pluginFixtures.js'; -import { createDomainObjectWithDefaults, waitForPlotsToRender } from '../opensource/appActions.js'; +import { pluginFixtures, appActions } from 'openmct-e2e'; +const { test, expect } = pluginFixtures; +const { createDomainObjectWithDefaults, waitForPlotsToRender } = appActions; +const YAMCS_URL = 'http://localhost:8090/'; test.describe("Mdb runtime limits tests @yamcs", () => { + + test.beforeEach(async ({ page }) => { + await clearLimitsForParameter(page); + }); + + test.afterEach(async ({ page }) => { + await clearLimitsForParameter(page); + }); + test('Can show mdb limits when changed', async ({ page }) => { // Go to baseURL await page.goto("./", { waitUntil: "networkidle" }); @@ -70,22 +81,15 @@ test.describe("Mdb runtime limits tests @yamcs", () => { // Expand the "Detector_Temp" plot series options and enable limit lines await page.getByRole('tab', { name: 'Config' }).click(); - await page - .getByRole('list', { name: 'Plot Series Properties' }) - .locator('span') - .first() - .click(); - await page - .getByRole('list', { name: 'Plot Series Properties' }) - .locator('[title="Display limit lines"]~div input') - .check(); + await page.getByLabel('Expand Detector_Temp Plot').click(); + await page.getByLabel('Limit lines').check(); // Save (exit edit mode) await page.getByRole('button', { name: 'Save' }).click(); await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); - // Change the limits for the Detector_Temp parameter using the yamcs API) - const runTimeLimitChangeResponse = await page.request.patch('http://localhost:8090/api/mdb-overrides/myproject/realtime/parameters/myproject/Detector_Temp', { + // Change the limits for the Detector_Temp parameter using the yamcs API + const runTimeLimitChangeResponse = await page.request.patch(`${YAMCS_URL}api/mdb-overrides/myproject/realtime/parameters/myproject/Detector_Temp`, { data: { action: 'SET_DEFAULT_ALARMS', defaultAlarm: { @@ -107,12 +111,12 @@ test.describe("Mdb runtime limits tests @yamcs", () => { await assertLimitLinesExistAndAreVisible(page); }); - test('Can show changed mdb limits when you navigate away from the view and back', async ({ page }) => { + test('Can show changed mdb limits when you navigate away from the view and back and no new requests are made on resize', async ({ page }) => { // Go to baseURL await page.goto("./", { waitUntil: "networkidle" }); // Reset the limits for the Detector_Temp parameter using the yamcs API - const runTimeLimitResetResponse = await page.request.patch('http://localhost:8090/api/mdb-overrides/myproject/realtime/parameters/myproject/Detector_Temp', { + const runTimeLimitResetResponse = await page.request.patch(`${YAMCS_URL}api/mdb-overrides/myproject/realtime/parameters/myproject/Detector_Temp`, { data: {} }); await expect(runTimeLimitResetResponse).toBeOK(); @@ -123,27 +127,19 @@ test.describe("Mdb runtime limits tests @yamcs", () => { }); //Expand the myproject folder (/myproject) - const myProjectTreeItem = page.locator('.c-tree__item').filter({ hasText: 'myproject'}); - const firstMyProjectTriangle = myProjectTreeItem.first().locator('span.c-disclosure-triangle'); - await firstMyProjectTriangle.click(); - + await page.getByLabel('Expand myproject folder').click(); //Expand the myproject under the previous folder (/myproject/myproject) - const viperRoverTreeItem = page.locator('.c-tree__item').filter({ hasText: 'myproject'}); - const viperRoverProjectTriangle = viperRoverTreeItem.nth(1).locator('span.c-disclosure-triangle'); - await viperRoverProjectTriangle.click(); + await page.getByLabel('Expand myproject folder').click(); //Find the Detector_Temp parameter (/myproject/myproject/Detector_Temp) const detectorTreeItem = page.getByRole('treeitem', { name: /Detector_Temp/ }); - - // Enter edit mode for the overlay plot await page.getByLabel('Edit Object').click(); //Drag and drop the Detector_Temp telemetry endpoint into this overlay plot - const objectPane = page.locator('.c-object-view'); - await detectorTreeItem.dragTo(objectPane); + await detectorTreeItem.dragTo(page.locator('.c-object-view')); // Save (exit edit mode) - await page.getByRole('button', { name: 'Save' }).click(); + await page.getByLabel('Save').click(); await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); // Assert that no limit lines are shown by default @@ -155,15 +151,8 @@ test.describe("Mdb runtime limits tests @yamcs", () => { // Expand the "Detector_Temp" plot series options and enable limit lines await page.getByRole('tab', { name: 'Config' }).click(); - await page - .getByRole('list', { name: 'Plot Series Properties' }) - .locator('span') - .first() - .click(); - await page - .getByRole('list', { name: 'Plot Series Properties' }) - .locator('[title="Display limit lines"]~div input') - .check(); + await page.getByLabel('Expand Detector_Temp Plot').click(); + await page.getByLabel('Limit lines').check(); // Save (exit edit mode) await page.getByRole('button', { name: 'Save' }).click(); @@ -173,7 +162,7 @@ test.describe("Mdb runtime limits tests @yamcs", () => { await page.goto("./", { waitUntil: "networkidle" }); // Change the limits for the Detector_Temp parameter using the yamcs API - const runTimeLimitChangeResponse = await page.request.patch('http://localhost:8090/api/mdb-overrides/myproject/realtime/parameters/myproject/Detector_Temp', { + const runTimeLimitChangeResponse = await page.request.patch(`${YAMCS_URL}api/mdb-overrides/myproject/realtime/parameters/myproject/Detector_Temp`, { data: { action: 'SET_DEFAULT_ALARMS', defaultAlarm: { @@ -196,6 +185,41 @@ test.describe("Mdb runtime limits tests @yamcs", () => { // Ensure that the changed limits are now displayed without a reload await assertLimitLinesExistAndAreVisible(page); + await page.locator('.plot-legend-item').hover(); + await expect(page.locator('.c-plot-limit')).toHaveCount(2); + await assertExpectedLimitsValues(page.locator('.c-plot-limit'), { + minInclusive: -0.8, + maxInclusive: 0.5 + }); + + // Setting up checks for the absence of specific network responses after networkidle. + const responsesChecks = [ + checkForNoResponseAfterNetworkIdle(page, '**/api/mdb/myproject/space-systems'), + checkForNoResponseAfterNetworkIdle(page, '**/api/mdb/myproject/parameters?details=yes&limit=1000'), + checkForNoResponseAfterNetworkIdle(page, '**/api/user/'), + checkForNoResponseAfterNetworkIdle(page, '**/api/mdb-overrides/myproject/realtime') + ]; + + // Resize the chart container by showing the snapshot pane. + await page.getByLabel('Show Snapshots').click(); + // Wait for all checks to complete + const responsesNotFound = await Promise.all(responsesChecks); + // Ensure no network responses were found + const noResponsesFound = responsesNotFound.every(notFound => notFound); + expect(noResponsesFound).toBe(true); + + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/akhenry/openmct-yamcs/issues/447' + }); + // Ensure that the limits still show and have not changed + await assertLimitLinesExistAndAreVisible(page); + await page.locator('.plot-legend-item').hover(); + await expect(page.locator('.c-plot-limit')).toHaveCount(2); + await assertExpectedLimitsValues(page.locator('.c-plot-limit'), { + minInclusive: -0.8, + maxInclusive: 0.5 + }); }); }); @@ -208,10 +232,44 @@ async function assertLimitLinesExistAndAreVisible(page) { await waitForPlotsToRender(page); // Wait for limit lines to be created await page.waitForSelector('.c-plot-limit-line', { state: 'attached' }); - const limitLineCount = await page.locator('.c-plot-limit-line').count(); // There should be 2 limit lines created by default - expect(await page.locator('.c-plot-limit-line').count()).toBe(2); + await expect(page.locator('.c-plot-limit-line')).toHaveCount(2); + const limitLineCount = await page.locator('.c-plot-limit-line').count(); for (let i = 0; i < limitLineCount; i++) { await expect(page.locator('.c-plot-limit-line').nth(i)).toBeVisible(); } } + +/** + * Asserts that the limit line has the expected min and max values + * @param {import('@playwright/test').Locator} limitLine + * @param {{ minInclusive: number, maxInclusive: number }} expectedResults + */ +async function assertExpectedLimitsValues(limitLine, { minInclusive, maxInclusive }) { + await expect(limitLine.first()).toContainText(`${maxInclusive}`); + await expect(limitLine.nth(1)).toContainText(`${minInclusive}`); +} + +// Function to check for the absence of a network response after networkidle +async function checkForNoResponseAfterNetworkIdle(page, urlPattern) { + let responseReceived = false; + // Listen for the network response before navigating to ensure we catch early requests + page.on('response', response => { + if (response.url().match(urlPattern)) { + responseReceived = true; + } + }); + // Wait for the network to be idle + await page.waitForLoadState('networkidle'); + + // Return the inverse of responseReceived to indicate absence of response + return !responseReceived; +} + +async function clearLimitsForParameter(page) { + // clear the limits for the Detector_Temp parameter using the yamcs API + const runTimeLimitChangeResponse = await page.request.patch(`${YAMCS_URL}api/mdb-overrides/myproject/realtime/parameters/myproject/Detector_Temp`, { + data: {} + }); + await expect(runTimeLimitChangeResponse).toBeOK(); +} diff --git a/tests/e2e/yamcs/load.e2e.spec.mjs b/tests/e2e/yamcs/load.e2e.spec.mjs new file mode 100644 index 00000000..f0caf544 --- /dev/null +++ b/tests/e2e/yamcs/load.e2e.spec.mjs @@ -0,0 +1,74 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2022, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +/* +Open MCT load Specific Tests +*/ + +import { pluginFixtures } from 'openmct-e2e'; +const { test, expect } = pluginFixtures; +const YAMCS_URL = 'http://localhost:8090/'; + +test.describe("Tests to ensure that open mct loads correctly @yamcs", () => { + test.beforeEach(async ({ page }) => { + await clearCustomAlgorithm(page); + }); + + test.afterEach(async ({ page }) => { + await clearCustomAlgorithm(page); + }); + + test('Can load correctly when mdb algorithms are changed at runtime', async ({ page }) => { + // Go to baseURL + await page.goto("./", {waitUntil: "networkidle"}); + await expect(page.getByLabel('Navigate to myproject folder')).toBeVisible(); + + await updateCustomAlgorithm(page); + + await page.reload({waitUntil: "networkidle"}); + + await expect(page.getByLabel('Navigate to myproject folder')).toBeVisible(); + }); +}); + +async function clearCustomAlgorithm(page) { + // clear the custom algorithm for the copySunsensor using the yamcs API + const runTimeCustomAlgorithmResetResponse = await page.request.patch(`${YAMCS_URL}api/mdb/myproject/realtime/algorithms/myproject/copySunsensor`, { + data: { + "action": "RESET" + } + }); + await expect(runTimeCustomAlgorithmResetResponse).toBeOK(); +} + +async function updateCustomAlgorithm(page) { + // Change the custom algorithm for the copySunsensor using the yamcs API + const runTimeCustomAlgorithmChangeResponse = await page.request.patch(`${YAMCS_URL}api/mdb/myproject/realtime/algorithms/myproject/copySunsensor`, { + data: { + "action": "SET", + "algorithm": { + "text": "\n\t\t\t\t\tout0.setFloatValue(in.getEngValue().getFloatValue()); \n\t\t\t\t" + } + } + }); + await expect(runTimeCustomAlgorithmChangeResponse).toBeOK(); +} diff --git a/tests/e2e/yamcs/namesToParametersMap.json b/tests/e2e/yamcs/namesToParametersMap.json new file mode 100644 index 00000000..132d18bb --- /dev/null +++ b/tests/e2e/yamcs/namesToParametersMap.json @@ -0,0 +1,48 @@ +{ + "A": "/myproject/A", + "ADCS_Error_Flag": "/myproject/ADCS_Error_Flag", + "Battery1_Temp": "/myproject/Battery1_Temp", + "Battery1_Voltage": "/myproject/Battery1_Voltage", + "Battery2_Temp": "/myproject/Battery2_Temp", + "Battery2_Voltage": "/myproject/Battery2_Voltage", + "CCSDS_Packet_Length": "/myproject/CCSDS_Packet_Length", + "CDHS_Error_Flag": "/myproject/CDHS_Error_Flag", + "CDHS_Status": "/myproject/CDHS_Status", + "COMMS_Error_Flag": "/myproject/COMMS_Error_Flag", + "COMMS_Status": "/myproject/COMMS_Status", + "Contact_Golbasi_GS": "/myproject/Contact_Golbasi_GS", + "Contact_Svalbard": "/myproject/Contact_Svalbard", + "Detector_Temp": "/myproject/Detector_Temp", + "ElapsedSeconds": "/myproject/ElapsedSeconds", + "Enum_Para_1": "/myproject/Enum_Para_1", + "Enum_Para_2": "/myproject/Enum_Para_2", + "Enum_Para_3": "/myproject/Enum_Para_3", + "EpochUSNO": "/myproject/EpochUSNO", + "EPS_Error_Flag": "/myproject/EPS_Error_Flag", + "Gyro.x": "/myproject/Gyro.x", + "Gyro.y": "/myproject/Gyro.y", + "Gyro.z": "/myproject/Gyro.z", + "Height": "/myproject/Height", + "Latitude": "/myproject/Latitude", + "Longitude": "/myproject/Longitude", + "Magnetometer.x": "/myproject/Magnetometer.x", + "Magnetometer.y": "/myproject/Magnetometer.y", + "Magnetometer.z": "/myproject/Magnetometer.z", + "Mode_Day": "/myproject/Mode_Day", + "Mode_Night": "/myproject/Mode_Night", + "Mode_Payload": "/myproject/Mode_Payload", + "Mode_Safe": "/myproject/Mode_Safe", + "Mode_SBand": "/myproject/Mode_SBand", + "Mode_XBand": "/myproject/Mode_XBand", + "OrbitNumberCumulative": "/myproject/OrbitNumberCumulative", + "Payload_Error_Flag": "/myproject/Payload_Error_Flag", + "Payload_Status": "/myproject/Payload_Status", + "Position.x": "/myproject/Position.x", + "Position.y": "/myproject/Position.y", + "Position.z": "/myproject/Position.z", + "Shadow": "/myproject/Shadow", + "Sunsensor": "/myproject/Sunsensor", + "Velocity.x": "/myproject/Velocity.x", + "Velocity.y": "/myproject/Velocity.y", + "Velocity.z": "/myproject/Velocity.z" +} \ No newline at end of file diff --git a/tests/e2e/yamcs/network.e2e.spec.js b/tests/e2e/yamcs/network.e2e.spec.mjs similarity index 98% rename from tests/e2e/yamcs/network.e2e.spec.js rename to tests/e2e/yamcs/network.e2e.spec.mjs index 734ada70..e1559496 100644 --- a/tests/e2e/yamcs/network.e2e.spec.js +++ b/tests/e2e/yamcs/network.e2e.spec.mjs @@ -24,8 +24,9 @@ * This suite verifies the network requests made by the application to ensure correct interaction with YAMCS. */ -import { test, expect } from '../opensource/pluginFixtures.js'; -import { setFixedTimeMode } from '../opensource/appActions.js'; +import { pluginFixtures, appActions } from 'openmct-e2e'; +const { test, expect } = pluginFixtures; +const { setFixedTimeMode } = appActions; /** * This test suite checks the network requests made by Open MCT to YAMCS. diff --git a/tests/e2e/yamcs/quickstartSmoke.e2e.spec.js b/tests/e2e/yamcs/quickstartSmoke.e2e.spec.mjs similarity index 97% rename from tests/e2e/yamcs/quickstartSmoke.e2e.spec.js rename to tests/e2e/yamcs/quickstartSmoke.e2e.spec.mjs index 611cef9c..9388276c 100644 --- a/tests/e2e/yamcs/quickstartSmoke.e2e.spec.js +++ b/tests/e2e/yamcs/quickstartSmoke.e2e.spec.mjs @@ -33,7 +33,8 @@ comfortable running this test during a live mission?" Avoid creating or deleting Make no assumptions about the order that elements appear in the DOM. */ -import { test, expect } from '../opensource/baseFixtures.js'; +import { baseFixtures } from 'openmct-e2e'; +const { test, expect } = baseFixtures; test.describe("Quickstart smoke tests @yamcs", () => { test('Verify that the create button appears and that the Folder Domain Object is available for selection', async ({ page }) => { diff --git a/tests/e2e/yamcs/quickstartTools.e2e.spec.mjs b/tests/e2e/yamcs/quickstartTools.e2e.spec.mjs new file mode 100644 index 00000000..e7260c13 --- /dev/null +++ b/tests/e2e/yamcs/quickstartTools.e2e.spec.mjs @@ -0,0 +1,81 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2024, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +import { + enableLink, + disableLink, + isLinkEnabled, + latestParameterValues, + parameterArchive +} from './quickstartTools.mjs'; +import { pluginFixtures } from 'openmct-e2e'; +const { test, expect } = pluginFixtures; + +test.describe('Quickstart library functions', () => { + let yamcsURL; + + test.beforeEach(async ({page}) => { + // Go to baseURL so we can get relative URL + await page.goto('./', { waitUntil: 'domcontentloaded' }); + yamcsURL = new URL('/yamcs-proxy/', page.url()).toString(); + await enableLink(yamcsURL); + }); + test('Link can be disabled', async ({ page }) => { + await disableLink(yamcsURL); + expect(await isLinkEnabled(yamcsURL)).toBe(false); + }); + test('Link can be enabled', async ({ page }) => { + await disableLink(yamcsURL); + expect(await isLinkEnabled(yamcsURL)).toBe(false); + + await enableLink(yamcsURL); + expect(await isLinkEnabled(yamcsURL)).toBe(true); + }); + test('Latest values can be retrieved', async () => { + const latestValues = await latestParameterValues(['/myproject/Battery1_Temp', '/myproject/Battery1_Voltage'], yamcsURL); + expect(latestValues.length).toBe(2); + const areAllParameterValuesNumbers = latestValues.every((parameter) => { + return !isNaN(parameter.engValue.floatValue); + }); + + expect(areAllParameterValuesNumbers).toBe(true); + }); + test('Parameter archive values can be retrieved', async () => { + const now = new Date(); + const ONE_MINUTE = 60 * 1000; + const then = new Date(now - ONE_MINUTE); + const latestValues = await parameterArchive({ + start: then.toISOString(), + end: now.toISOString(), + parameterId: '/myproject/Battery1_Temp', + yamcsURL + }); + expect(latestValues.length).toBeGreaterThan(0); + + const areAllParameterValuesNumbers = latestValues.every((parameter) => { + return !isNaN(parameter.engValue.floatValue); + }); + + expect(areAllParameterValuesNumbers).toBe(true); + }); + +}); diff --git a/tests/e2e/yamcs/quickstartTools.mjs b/tests/e2e/yamcs/quickstartTools.mjs new file mode 100644 index 00000000..a755d81c --- /dev/null +++ b/tests/e2e/yamcs/quickstartTools.mjs @@ -0,0 +1,81 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2024, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +async function disableLink(yamcsURL) { + const url = new URL(`api/links/myproject/udp-in:disable`, yamcsURL); + await fetch(url.toString(), { + method: 'POST' + }); +} + +async function enableLink(yamcsURL) { + const url = new URL(`api/links/myproject/udp-in:enable`, yamcsURL); + await fetch(url.toString(), { + method: 'POST' + }); +} + +async function isLinkEnabled(yamcsURL) { + const url = new URL(`api/links/myproject/udp-in`, yamcsURL); + const response = await (await fetch(url.toString())).json(); + + return response.disabled !== true; +} + +async function latestParameterValues(parameterIds, yamcsURL) { + const parameterIdsRequest = { + fromCache: true, + id: parameterIds.map(parameterName => { + return { + name: parameterName + }; + }) + }; + const parameterIdsRequestSerialized = JSON.stringify(parameterIdsRequest); + const url = new URL('api/processors/myproject/realtime/parameters:batchGet', yamcsURL); + const response = await (await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: parameterIdsRequestSerialized + })).json(); + + return response.value; +} + +async function parameterArchive({start, end, parameterId, yamcsURL}) { + const url = new URL(`api/archive/myproject/parameters/${parameterId}`, `${yamcsURL}`); + url.searchParams.set('start', start); + url.searchParams.set('stop', end); + + const response = await (await fetch(url.toString())).json(); + + return response.parameter; +} + +export { + disableLink, + enableLink, + isLinkEnabled, + latestParameterValues, + parameterArchive +}; diff --git a/tests/e2e/yamcs/realtimeData.e2e.spec.mjs b/tests/e2e/yamcs/realtimeData.e2e.spec.mjs new file mode 100644 index 00000000..4aad789a --- /dev/null +++ b/tests/e2e/yamcs/realtimeData.e2e.spec.mjs @@ -0,0 +1,499 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2024, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +/** + * IMPORTANT: CANNOT BE RUN IN PARALLEL, ENABLES & DISABLES LINKS + */ +import { expect, test } from '@playwright/test'; +import { fileURLToPath } from 'url'; +import { latestParameterValues, disableLink, enableLink, parameterArchive } from './quickstartTools.mjs'; + +import fs from 'fs'; + +const namesToParametersMap = JSON.parse(fs.readFileSync(new URL('./namesToParametersMap.json', import.meta.url))); +const realTimeDisplayPath = fileURLToPath( + new URL('./test-data/e2e-real-time-test-layout.json', import.meta.url) +); + +// Wait 1s from when telemetry is received before sampling values in the UI. This is 1s because by default +// Open MCT is configured to release batches of telemetry every 1s. So depending on when it is sampled it +// may take up to 1s for telemetry to propagate to the UI from when it is received. +const TELEMETRY_PROPAGATION_TIME = 1000; +const THIRTY_MINUTES = 30 * 60 * 1000; + +test.describe('Realtime telemetry displays', () => { + let yamcsURL; + let websocketWorker; + + test.beforeEach(async ({ page }) => { + page.on('worker', worker => { + if (worker.url().startsWith('blob')) { + websocketWorker = worker; + } + }); + + // Go to baseURL + await page.goto('./', { waitUntil: 'domcontentloaded' }); + await page.evaluate((thirtyMinutes) => { + const openmct = window.openmct; + + openmct.install(openmct.plugins.RemoteClock({ + namespace: "taxonomy", + key: "~myproject~Battery1_Temp" + })); + + openmct.time.setClock('remote-clock'); + openmct.time.setClockOffsets({ + start: -thirtyMinutes, + end: 0 + }); + }, THIRTY_MINUTES); + yamcsURL = new URL('/yamcs-proxy/', page.url()).toString(); + await enableLink(yamcsURL); + + await page + .getByRole('treeitem', { + name: /My Items/ + }) + .click({ + button: 'right' + }); + + await page + .getByRole('menuitem', { + name: /Import from JSON/ + }) + .click(); + + // Upload memory-leak-detection.json + await page.setInputFiles('#fileElem', realTimeDisplayPath); + await page + .getByRole('button', { + name: 'Save' + }) + .click(); + + await expect(page.locator('a:has-text("e2e real-time test layout")')).toBeVisible(); + }); + test.afterEach(async ({ page }) => { + await enableLink(yamcsURL); + }); + + test.describe('A complex display', () => { + test.beforeEach(async ({ page }) => { + const searchBox = page.getByRole('searchbox', { name: 'Search Input' }); + await searchBox.click(); + // Fill Search input + await searchBox.fill("e2e real-time test layout"); + + const searchResults = page.getByLabel('Search Results Dropdown'); + + //Search Result Appears and is clicked + const layoutSearchResult = searchResults.getByText("e2e real-time test layout", { exact: true }); + await layoutSearchResult.click(); + }); + + test('renders correctly', async ({ page }) => { + let count = await page.getByLabel('lad name').count(); + expect(count).toBe(Object.entries(namesToParametersMap).length); + }); + + test('Correctly shows the latest values', async ({ page }) => { + // Wait a reasonable amount of time for new telemetry to come in. + // There is nothing significant about the number chosen. + const WAIT_FOR_MORE_TELEMETRY = 3000; + + const ladTable = await getLadTableByName(page, 'Test LAD Table'); + + // Let it run for a few seconds + await page.waitForTimeout(WAIT_FOR_MORE_TELEMETRY); + + // Disable playback + await disableLink(yamcsURL); + + // Wait 1 second for values to propagate to client and render on screen. + await page.waitForTimeout(TELEMETRY_PROPAGATION_TIME); + + const latestValueObjects = await latestParameterValues(Object.values(namesToParametersMap), yamcsURL); + const parameterNamesToLatestValues = toParameterNameToValueMap(latestValueObjects); + const tableValuesByParameterName = await getParameterValuesFromLadTable(ladTable); + const allAlphaNumericValuesByName = await getParameterValuesFromAllAlphaNumerics(page); + const allGaugeValuesByName = await getParameterValuesFromAllGauges(page); + const tableTimestampsByParameterName = await getParameterTimestampsFromLadTable(ladTable); + assertParameterMapsAreEqual(parameterNamesToLatestValues, tableValuesByParameterName); + assertParameterMapsAreEqual(parameterNamesToLatestValues, allAlphaNumericValuesByName); + assertParameterMapsAreEqual(allGaugeValuesByName, parameterNamesToLatestValues, 2); + + // Enable playback + await enableLink(yamcsURL); + + // Let it run for a few seconds to cycle through a few telemetry values + await page.waitForTimeout(WAIT_FOR_MORE_TELEMETRY); + + // Disable playback + await disableLink(yamcsURL); + + // Wait 1 second for values to propagate to client and render on screen. + await page.waitForTimeout(TELEMETRY_PROPAGATION_TIME); + + const secondLatestValueObjects = await latestParameterValues(Object.values(namesToParametersMap), yamcsURL); + const secondParameterNamesToLatestValues = toParameterNameToValueMap(secondLatestValueObjects); + const secondTableValuesByParameterName = await getParameterValuesFromLadTable(ladTable); + const secondTableTimestampsByParameterName = await getParameterTimestampsFromLadTable(ladTable); + const secondAlphaNumericValuesByName = await getParameterValuesFromAllAlphaNumerics(page); + const secondGaugeValuesByName = await getParameterValuesFromAllGauges(page); + + //First compare timestamps to make sure telemetry on screen is actually changing. + Object.keys(namesToParametersMap).forEach(key => { + expect(tableTimestampsByParameterName[key]).not.toBe(secondTableTimestampsByParameterName[key]); + }); + + // Next confirm that the values on screen are, again, the same as the latest values in Yamcs + assertParameterMapsAreEqual(secondParameterNamesToLatestValues, secondTableValuesByParameterName); + assertParameterMapsAreEqual(secondParameterNamesToLatestValues, secondAlphaNumericValuesByName); + assertParameterMapsAreEqual(secondGaugeValuesByName, parameterNamesToLatestValues, 2); + }); + + test('Correctly reconnects and shows the latest values after websocket drop', async ({ page }) => { + // Wait a reasonable amount of time for new telemetry to come in. + // There is nothing significant about the number chosen. + const WAIT_FOR_MORE_TELEMETRY = 3000; + + const ladTable = await getLadTableByName(page, 'Test LAD Table'); + + // Let it run for a few seconds + await page.waitForTimeout(WAIT_FOR_MORE_TELEMETRY); + + // Disable playback + await disableLink(yamcsURL); + + // Wait 1 second for values to propagate to client and render on screen. + await page.waitForTimeout(TELEMETRY_PROPAGATION_TIME); + + const latestValueObjects = await latestParameterValues(Object.values(namesToParametersMap), yamcsURL); + const parameterNamesToLatestValues = toParameterNameToValueMap(latestValueObjects); + const tableValuesByParameterName = await getParameterValuesFromLadTable(ladTable); + const allAlphaNumericValuesByName = await getParameterValuesFromAllAlphaNumerics(page); + const allGaugeValuesByName = await getParameterValuesFromAllGauges(page); + const tableTimestampsByParameterName = await getParameterTimestampsFromLadTable(ladTable); + assertParameterMapsAreEqual(parameterNamesToLatestValues, tableValuesByParameterName); + assertParameterMapsAreEqual(parameterNamesToLatestValues, allAlphaNumericValuesByName); + assertParameterMapsAreEqual(allGaugeValuesByName, parameterNamesToLatestValues, 2); + + // Enable playback + await enableLink(yamcsURL); + + // Drop the websocket + websocketWorker.evaluate(() => { + self.currentWebSocket.close(); + }); + + //Wait for websocket to be re-established + await page.waitForEvent('websocket'); + + // Let it run for a few seconds to cycle through a few telemetry values + await page.waitForTimeout(WAIT_FOR_MORE_TELEMETRY); + + // Disable playback + await disableLink(yamcsURL); + + // Wait 1 second for values to propagate to client and render on screen. + await page.waitForTimeout(TELEMETRY_PROPAGATION_TIME); + + const secondLatestValueObjects = await latestParameterValues(Object.values(namesToParametersMap), yamcsURL); + const secondParameterNamesToLatestValues = toParameterNameToValueMap(secondLatestValueObjects); + const secondTableValuesByParameterName = await getParameterValuesFromLadTable(ladTable); + const secondTableTimestampsByParameterName = await getParameterTimestampsFromLadTable(ladTable); + const secondAlphaNumericValuesByName = await getParameterValuesFromAllAlphaNumerics(page); + const secondGaugeValuesByName = await getParameterValuesFromAllGauges(page); + + //First compare timestamps to make sure telemetry on screen is actually changing. + Object.keys(namesToParametersMap).forEach(key => { + expect(tableTimestampsByParameterName[key]).not.toBe(secondTableTimestampsByParameterName[key]); + }); + + // Next confirm that the values on screen are, again, the same as the latest values in Yamcs + assertParameterMapsAreEqual(secondParameterNamesToLatestValues, secondTableValuesByParameterName); + assertParameterMapsAreEqual(secondParameterNamesToLatestValues, secondAlphaNumericValuesByName); + assertParameterMapsAreEqual(secondGaugeValuesByName, parameterNamesToLatestValues, 2); + }); + + test('Open MCT does not drop telemetry while app is loading', async ({ page }) => { + const notification = page.getByRole('alert'); + const count = await notification.count(); + + if (count > 0) { + const text = await notification.innerText(); + expect(text).not.toBe('Telemetry dropped due to client rate limiting.'); + } else { + expect(notification).toHaveCount(0); + } + }); + + test('Open MCT does drop telemetry when the UI is under load', async ({ page }) => { + // 1. Make sure the display is done loading, and populated with values (ie. we are in a steady state) + const ladTable = await getLadTableByName(page, 'Test LAD Table'); + await getParameterValuesFromLadTable(ladTable); + + // 2. Block the UI with a loop + await page.evaluate(() => { + return new Promise((resolveBlockingLoop) => { + //5s x 10Hz data = 50 telemetry values which should easily overrun the buffer length of 20. + let start = Date.now(); + let now = Date.now(); + // Block the UI thread for 5s + while (now - start < 5000) { + now = Date.now(); + } + + resolveBlockingLoop(); + }); + }); + // Check for telemetry dropped notification + const notification = page.getByRole('alert'); + expect(notification).toHaveCount(1); + const text = await notification.innerText(); + expect(text).toBe('Telemetry dropped due to client rate limiting.'); + }); + + test('Open MCT shows the latest telemetry after UI is temporarily blocked', async ({ page }) => { + const ladTable = await getLadTableByName(page, 'Test LAD Table'); + // 1. Subscribe to batched telemetry, + // 3. Confirm that it is correct and only the _oldest_ values missing + await page.evaluate(() => { + return new Promise((resolveBlockingLoop) => { + let start = Date.now(); + let now = Date.now(); + // Block the UI thread for 5s + while (now - start < 5000) { + now = Date.now(); + } + + resolveBlockingLoop(); + }); + }); + + // Disable playback + await disableLink(yamcsURL); + + // Wait 1 second for values to propagate to client and render on screen. + await page.waitForTimeout(TELEMETRY_PROPAGATION_TIME); + + const latestValueObjects = await latestParameterValues(Object.values(namesToParametersMap), yamcsURL); + const parameterNamesToLatestValues = toParameterNameToValueMap(latestValueObjects); + const tableValuesByParameterName = await getParameterValuesFromLadTable(ladTable); + assertParameterMapsAreEqual(parameterNamesToLatestValues, tableValuesByParameterName); + }); + }); + + test('Open MCT accurately batches telemetry when requested', async ({ page }) => { + + // 1. Subscribe to batched telemetry, + const telemetryValues = await page.evaluate(async () => { + const openmct = window.openmct; + const telemetryObject = await openmct.objects.get({ + namespace: 'taxonomy', + key: '~myproject~Battery1_Temp' + }); + + return new Promise((resolveWithTelemetry) => { + // First callback is the latest value for the parameter. + let haveReceivedLatest = false; + openmct.telemetry.subscribe(telemetryObject, (telemetry) => { + if (haveReceivedLatest === false) { + haveReceivedLatest = true; + } else { + resolveWithTelemetry(telemetry); + } + }, {strategy: 'batch'}); + }); + }); + await disableLink(yamcsURL); + sortOpenMctTelemetryAscending(telemetryValues); + + // 2. confirm that it is received as an array. + expect(telemetryValues.length).toBeGreaterThan(1); + const start = new Date(new Date(telemetryValues[0].timestamp).getTime() - 1).toISOString(); + const end = new Date(telemetryValues[telemetryValues.length - 1].timestamp).toISOString(); + const parameterArchiveTelemetry = await parameterArchive({ + start, + end, + parameterId: `/myproject/Battery1_Temp`, + yamcsURL + }); + const formattedParameterArchiveTelemetry = toOpenMctTelemetryFormat(parameterArchiveTelemetry); + sortOpenMctTelemetryAscending(formattedParameterArchiveTelemetry); + + telemetryValues.forEach((telemetry, index) => { + expect(telemetry.value).toBe(formattedParameterArchiveTelemetry[index].value); + expect(telemetry.timestamp).toBe(formattedParameterArchiveTelemetry[index].timestamp); + }); + }); + + function sortOpenMctTelemetryAscending(telemetry) { + return telemetry.sort((a, b) => { + if (a.timestamp < b.timestamp) { + return -1; + } else if (a.timestamp > b.timestamp) { + return 1; + } else if (a.timestamp === b.timestamp) { + return 0; + } else { + return undefined; + } + }); + } + + function assertParameterMapsAreEqual(parameterNamesToLatestValues, tableValuesByParameterName, toPrecision) { + Object.keys(parameterNamesToLatestValues).forEach((parameterName) => { + const valueInYamcs = parameterNamesToLatestValues[parameterName]; + const valueOnScreen = tableValuesByParameterName[parameterName]; + if (toPrecision !== undefined && !isNaN(valueInYamcs) && !isNaN(valueOnScreen)) { + const numericalValueInYamcs = parseFloat(valueInYamcs).toFixed(toPrecision); + const numericalValueOnScreen = parseFloat(valueInYamcs).toFixed(toPrecision); + + expect(numericalValueOnScreen).toBe(numericalValueInYamcs); + } else { + expect(valueOnScreen).toBe(valueInYamcs); + } + }); + } + + function toParameterNameToValueMap(latestParameterValueObjects) { + return latestParameterValueObjects.reduce((mapping, parameterValue) => { + mapping[parameterValue.id.name.substring(parameterValue.id.name.lastIndexOf('/') + 1)] = + String(parameterValue.engValue.floatValue + ?? parameterValue.engValue.stringValue + ?? parameterValue.engValue.uint32Value + ?? parameterValue.engValue.booleanValue); + + return mapping; + }, {}); + } + + function toOpenMctTelemetryFormat(listOfParameterValueObjects) { + return listOfParameterValueObjects.map((parameterValue) => { + return { + timestamp: parameterValue.generationTime, + value: parameterValue.engValue.floatValue + ?? parameterValue.engValue.stringValue + ?? parameterValue.engValue.uint32Value + ?? parameterValue.engValue.booleanValue + }; + }); + } + + async function getLadTableByName(page, ladTableName) { + const matchingLadTableFrames = await page.getByLabel("sub object frame").filter({ + has: page.getByLabel("object name", { + name: ladTableName + }) + }); + + return matchingLadTableFrames.getByLabel('lad table').first(); + + } + + /** + * @param {import('playwright').Page} page + * @returns {Promise<{parameterNameText: string, parameterValueText: string}[]>} + */ + async function getParameterValuesFromAllGauges(page) { + const allGauges = await (page.getByLabel('sub object frame', { exact: true}).filter({ + has: page.getByLabel('Gauge', { + exact: true + }) + })).all(); + const arrayOfValues = await Promise.all(allGauges.map(async (gauge) => { + const parameterNameText = await (gauge.getByLabel("object name")).innerText(); + const parameterValueText = await (gauge.getByLabel(/gauge value.*/)).innerText(); + + return { + parameterNameText, + parameterValueText + }; + })); + + return arrayOfValues.reduce((map, row) => { + map[row.parameterNameText] = row.parameterValueText; + + return map; + }, {}); + } + + async function getParameterValuesFromLadTable(ladTable) { + const allRows = await (await ladTable.getByLabel('lad row')).all(); + const arrayOfValues = await Promise.all(allRows.map(async (row) => { + const parameterNameText = await row.getByLabel('lad name').innerText(); + const parameterValueText = await row.getByLabel('lad value').innerText(); + + return { + parameterNameText, + parameterValueText + }; + })); + + return arrayOfValues.reduce((map, row) => { + map[row.parameterNameText] = row.parameterValueText; + + return map; + }, {}); + } + + async function getParameterValuesFromAllAlphaNumerics(page) { + const allAlphaNumerics = await (page.getByLabel('Alpha-numeric telemetry', {exact: true})).all(); + const arrayOfValues = await Promise.all(allAlphaNumerics.map(async (alphaNumeric) => { + const parameterNameText = await (alphaNumeric.getByLabel(/Alpha-numeric telemetry name.*/)).innerText(); + const parameterValueText = await (alphaNumeric.getByLabel(/Alpha-numeric telemetry value.*/)).innerText(); + + return { + parameterNameText, + parameterValueText + }; + })); + + return arrayOfValues.reduce((map, row) => { + map[row.parameterNameText] = row.parameterValueText; + + return map; + }, {}); + } + + async function getParameterTimestampsFromLadTable(ladTable) { + const allRows = await (await ladTable.getByLabel('lad row')).all(); + const arrayOfValues = await Promise.all(allRows.map(async (row) => { + const parameterNameText = await row.getByLabel('lad name').innerText(); + const parameterValueText = await row.getByLabel('lad timestamp').innerText(); + + return { + parameterNameText, + parameterValueText + }; + })); + + return arrayOfValues.reduce((map, row) => { + map[row.parameterNameText] = row.parameterValueText; + + return map; + }, {}); + } +}); diff --git a/tests/e2e/yamcs/search.e2e.spec.js b/tests/e2e/yamcs/search.e2e.spec.mjs similarity index 96% rename from tests/e2e/yamcs/search.e2e.spec.js rename to tests/e2e/yamcs/search.e2e.spec.mjs index 72984e40..5254a8c0 100644 --- a/tests/e2e/yamcs/search.e2e.spec.js +++ b/tests/e2e/yamcs/search.e2e.spec.mjs @@ -24,7 +24,8 @@ Search Specific Tests */ -import { test, expect } from '../opensource/pluginFixtures.js'; +import { pluginFixtures } from 'openmct-e2e'; +const { test, expect } = pluginFixtures; test.describe("Quickstart search tests @yamcs", () => { test('Validate aggregate in search result', async ({ page }) => { diff --git a/tests/e2e/yamcs/staleness.e2e.mjs b/tests/e2e/yamcs/staleness.e2e.mjs new file mode 100644 index 00000000..1494f9e4 --- /dev/null +++ b/tests/e2e/yamcs/staleness.e2e.mjs @@ -0,0 +1,44 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2024, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +/* +Staleness Specific Tests +*/ + +import { pluginFixtures } from 'openmct-e2e'; +const { test } = pluginFixtures; + +test.describe.fixme("Staleness tests @yamcs", () => { + // eslint-disable-next-line require-await + test('Staleness ', async ({ page }) => { + test.step('Indicator is displayed for historic data', () => { + // Create a plot + // Add a telemetry endpoint that has stale data to this plot + // Expect that there is indication of staleness for the plot + }); + + test.step('Indicator is removed when new data arrives in real time', () => { + // Wait for new data + // Expect that stale indication is removed + }); + }); +}); diff --git a/tests/e2e/yamcs/telemetryTables.e2e.spec.mjs b/tests/e2e/yamcs/telemetryTables.e2e.spec.mjs new file mode 100644 index 00000000..e151dfac --- /dev/null +++ b/tests/e2e/yamcs/telemetryTables.e2e.spec.mjs @@ -0,0 +1,80 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2022, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +/* +Telemetry Table Specific Tests +*/ + +import { pluginFixtures } from 'openmct-e2e'; +const { test, expect } = pluginFixtures; + +test.describe("Telemetry Tables tests @yamcs", () => { + + // An error will be thrown if an attempt to mutate an immutable object is made, this will cover + // that case as well as any other errors during the test + test.use({ failOnConsoleError: true }); + + test.beforeEach(async ({ page }) => { + // Go to baseURL + await page.goto("./", { waitUntil: "domcontentloaded" }); + await expect(page.getByText('Loading...')).toBeHidden(); + + // Expand myproject + await page.getByLabel('Expand myproject').click(); + }); + + test('Telemetry Tables viewing an unpersistable object, will not modify the configuration on mode change', async ({ page }) => { + // Navigat to the Events table + await page.getByLabel('Navigate to Events yamcs.').click(); + + // Find the mode switch button and click it, this will trigger a mutation on mutable objects configuration + await page.getByRole('button', { name: 'SHOW UNLIMITED' }).click(); + + // Assert that the 'SHOW LIMITED' button is now visible + await expect(page.getByRole('button', { name: 'SHOW LIMITED' })).toBeVisible(); + }); + + test('Telemetry tables when changing mode, will not change the sort order of the request', async ({ page }) => { + // Set up request promise for an events request in descending order + let eventRequestOrderDescending = page.waitForRequest(/.*\/api\/.*\/events.*order=desc$/); + + // Navigate to the Events table + await page.getByLabel('Navigate to Events yamcs.').click(); + await page.waitForLoadState('networkidle'); + + // Wait for the descending events request + await eventRequestOrderDescending; + + // Reset request promise for an events request in descending order + eventRequestOrderDescending = page.waitForRequest(/.*\/api\/.*\/events.*order=desc$/); + + // Find the mode switch button and click it, this will trigger another events request + await page.getByRole('button', { name: 'SHOW UNLIMITED' }).click(); + await page.waitForLoadState('networkidle'); + + await eventRequestOrderDescending; + + // Assert that the 'SHOW LIMITED' button is now visible + await expect(page.getByRole('button', { name: 'SHOW LIMITED' })).toBeVisible(); + }); + +}); diff --git a/tests/e2e/yamcs/test-data/e2e-real-time-test-layout.json b/tests/e2e/yamcs/test-data/e2e-real-time-test-layout.json new file mode 100644 index 00000000..12c44f15 --- /dev/null +++ b/tests/e2e/yamcs/test-data/e2e-real-time-test-layout.json @@ -0,0 +1 @@ +{"openmct":{"c965501f-86c7-4d63-8857-95618462ea2d":{"identifier":{"key":"c965501f-86c7-4d63-8857-95618462ea2d","namespace":""},"name":"e2e real-time test layout","type":"layout","composition":[{"key":"797785c7-035a-4ea2-b69b-d595cf48b49c","namespace":""},{"key":"~myproject~A","namespace":"taxonomy"},{"key":"~myproject~ADCS_Error_Flag","namespace":"taxonomy"},{"key":"~myproject~Battery1_Temp","namespace":"taxonomy"},{"key":"~myproject~Battery1_Voltage","namespace":"taxonomy"},{"key":"~myproject~Battery2_Temp","namespace":"taxonomy"},{"key":"~myproject~Battery2_Voltage","namespace":"taxonomy"},{"key":"~myproject~CCSDS_Packet_Length","namespace":"taxonomy"},{"key":"~myproject~CDHS_Error_Flag","namespace":"taxonomy"},{"key":"~myproject~CDHS_Status","namespace":"taxonomy"},{"key":"~myproject~COMMS_Error_Flag","namespace":"taxonomy"},{"key":"~myproject~COMMS_Status","namespace":"taxonomy"},{"key":"~myproject~Contact_Golbasi_GS","namespace":"taxonomy"},{"key":"~myproject~Contact_Svalbard","namespace":"taxonomy"},{"key":"~myproject~Detector_Temp","namespace":"taxonomy"},{"key":"~myproject~ElapsedSeconds","namespace":"taxonomy"},{"key":"~myproject~Enum_Para_1","namespace":"taxonomy"},{"key":"~myproject~Enum_Para_2","namespace":"taxonomy"},{"key":"~myproject~Enum_Para_3","namespace":"taxonomy"},{"key":"~myproject~EpochUSNO","namespace":"taxonomy"},{"key":"~myproject~EPS_Error_Flag","namespace":"taxonomy"},{"key":"~myproject~Gyro.x","namespace":"taxonomy"},{"key":"~myproject~Gyro.y","namespace":"taxonomy"},{"key":"~myproject~Gyro.z","namespace":"taxonomy"},{"key":"~myproject~Height","namespace":"taxonomy"},{"key":"~myproject~Latitude","namespace":"taxonomy"},{"key":"~myproject~Longitude","namespace":"taxonomy"},{"key":"~myproject~Magnetometer.x","namespace":"taxonomy"},{"key":"~myproject~Magnetometer.y","namespace":"taxonomy"},{"key":"~myproject~Magnetometer.z","namespace":"taxonomy"},{"key":"~myproject~Mode_Day","namespace":"taxonomy"},{"key":"~myproject~Mode_Night","namespace":"taxonomy"},{"key":"~myproject~Mode_Payload","namespace":"taxonomy"},{"key":"~myproject~Mode_Safe","namespace":"taxonomy"},{"key":"~myproject~Mode_SBand","namespace":"taxonomy"},{"key":"~myproject~Mode_XBand","namespace":"taxonomy"},{"key":"~myproject~OrbitNumberCumulative","namespace":"taxonomy"},{"key":"~myproject~Payload_Error_Flag","namespace":"taxonomy"},{"key":"~myproject~Payload_Status","namespace":"taxonomy"},{"key":"~myproject~Position.x","namespace":"taxonomy"},{"key":"~myproject~Position.y","namespace":"taxonomy"},{"key":"~myproject~Position.z","namespace":"taxonomy"},{"key":"~myproject~Shadow","namespace":"taxonomy"},{"key":"~myproject~Velocity.x","namespace":"taxonomy"},{"key":"~myproject~Velocity.y","namespace":"taxonomy"},{"key":"~myproject~Velocity.z","namespace":"taxonomy"},{"key":"1816debc-38b6-4680-8019-701689be4fc5","namespace":""},{"key":"cc400028-b156-43cc-8bae-de13171d5431","namespace":""},{"key":"798e729f-420c-45f6-aecf-5bbc44b55dad","namespace":""},{"key":"fada1f9c-668d-49c4-a653-81bb3d466922","namespace":""},{"key":"c3eb99c4-db3c-4a92-8967-3b3052efd0cf","namespace":""},{"key":"0dd60b42-66a3-4326-b829-2cde71025d32","namespace":""},{"key":"eeda19bc-6302-47b6-b115-81204b16aaa3","namespace":""},{"key":"3093a1ef-ac30-4ac3-abff-e4ef5ca858e6","namespace":""},{"key":"e4b66733-2e89-4bf8-8a12-5ef3f247e4ec","namespace":""},{"key":"a91d265d-b36d-41fe-9d11-b799e52036ed","namespace":""},{"key":"739c26b2-776a-4abb-949f-24a2066aee80","namespace":""},{"key":"94997ceb-00c8-4702-af70-afb014cdf629","namespace":""},{"key":"803121a1-d187-489f-a124-5e903657bedb","namespace":""},{"key":"c984a025-e4a8-4c25-9b0e-1723b5a30303","namespace":""},{"key":"95cac0f8-cc42-4357-aac8-77d7ea5e31c9","namespace":""},{"key":"1011058f-8dec-4426-87d9-58135f663ea6","namespace":""},{"key":"c1881c9a-a638-4bbe-b320-a0ea5d7f4e4f","namespace":""},{"key":"d510bf8a-f03d-4cac-b58c-1242cc039b22","namespace":""},{"key":"a211ae9a-f013-4a97-87ed-401baee94a3e","namespace":""},{"key":"f051b323-d22a-419a-a6e6-42ea6041b238","namespace":""},{"key":"77ce4615-53d4-4ae7-809c-8535a57a041b","namespace":""},{"key":"b425c7bd-6912-4e40-908e-6a21c73c7db3","namespace":""},{"key":"~myproject~Sunsensor","namespace":"taxonomy"}],"configuration":{"items":[{"width":84,"height":96,"x":1,"y":13,"identifier":{"key":"797785c7-035a-4ea2-b69b-d595cf48b49c","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"24a9bf1f-0423-4863-bc78-e828907da303"},{"identifier":{"key":"~myproject~A","namespace":"taxonomy"},"x":85,"y":6,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"211674f3-e3c2-4c6a-bfc9-d463c19fe692"},{"identifier":{"key":"~myproject~ADCS_Error_Flag","namespace":"taxonomy"},"x":85,"y":8,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"0d7d24fb-423b-4fdf-8e72-ce58e5110fa8"},{"identifier":{"key":"~myproject~Battery1_Temp","namespace":"taxonomy"},"x":85,"y":10,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"0f7b5287-0a2f-494e-81d4-5deeee850f10"},{"identifier":{"key":"~myproject~Battery1_Voltage","namespace":"taxonomy"},"x":85,"y":12,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"78ebfb21-0126-4064-85bc-d4d25a4ddb58"},{"identifier":{"key":"~myproject~Battery2_Temp","namespace":"taxonomy"},"x":85,"y":14,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"67bf67ae-e574-447b-a8b7-889c0e7608a9"},{"identifier":{"key":"~myproject~Battery2_Voltage","namespace":"taxonomy"},"x":85,"y":16,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"131b0b30-e720-4993-949c-7a830cce113e"},{"identifier":{"key":"~myproject~CCSDS_Packet_Length","namespace":"taxonomy"},"x":85,"y":18,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"78abfd26-16d1-48f8-bae1-1b9f843bd75e"},{"identifier":{"key":"~myproject~CDHS_Error_Flag","namespace":"taxonomy"},"x":85,"y":20,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"9456ef72-7970-4695-b949-6f89183b92f2"},{"identifier":{"key":"~myproject~CDHS_Status","namespace":"taxonomy"},"x":85,"y":22,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"a263ef3e-5093-4c3b-8f96-0c2f63403c95"},{"identifier":{"key":"~myproject~COMMS_Error_Flag","namespace":"taxonomy"},"x":85,"y":24,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"0aa18240-ee67-4f9d-949d-b637df870ca9"},{"identifier":{"key":"~myproject~COMMS_Status","namespace":"taxonomy"},"x":85,"y":26,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"f09c1fc7-f353-4ceb-a0c4-0c66fe23051a"},{"identifier":{"key":"~myproject~Contact_Golbasi_GS","namespace":"taxonomy"},"x":85,"y":28,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"8de22bbf-e0b8-435a-91be-6f79bb9187b9"},{"identifier":{"key":"~myproject~Contact_Svalbard","namespace":"taxonomy"},"x":85,"y":30,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"41d0c878-591c-4540-b072-67ace5b753d6"},{"identifier":{"key":"~myproject~Detector_Temp","namespace":"taxonomy"},"x":85,"y":32,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"1bb8a77e-03e3-4c97-b327-3ba351fddd59"},{"identifier":{"key":"~myproject~ElapsedSeconds","namespace":"taxonomy"},"x":85,"y":34,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"22b1edd6-a520-405b-9297-01cb220b7f25"},{"identifier":{"key":"~myproject~Enum_Para_1","namespace":"taxonomy"},"x":85,"y":36,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"fb336c08-ce84-4708-9105-f401ffd6c275"},{"identifier":{"key":"~myproject~Enum_Para_2","namespace":"taxonomy"},"x":85,"y":38,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"76d94b1e-e65b-484b-a051-ba873b0d4acc"},{"identifier":{"key":"~myproject~Enum_Para_3","namespace":"taxonomy"},"x":85,"y":40,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"52bba941-b28c-4f00-a846-773b0cbd1b9c"},{"identifier":{"key":"~myproject~EpochUSNO","namespace":"taxonomy"},"x":85,"y":42,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"2771956c-7b11-4cfb-ac01-546203b7dc21"},{"identifier":{"key":"~myproject~EPS_Error_Flag","namespace":"taxonomy"},"x":85,"y":44,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"f95748cd-2f63-44fb-88a8-540bee063a3c"},{"identifier":{"key":"~myproject~Gyro.x","namespace":"taxonomy"},"x":85,"y":46,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"c6265d87-332c-4295-beb6-01e19d7c367b"},{"identifier":{"key":"~myproject~Gyro.y","namespace":"taxonomy"},"x":85,"y":48,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"0517dab9-ec57-4d4c-8e1f-aec5f263e00a"},{"identifier":{"key":"~myproject~Gyro.z","namespace":"taxonomy"},"x":85,"y":50,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"68fb8115-6d53-46cb-b0cd-ee2682b8c233"},{"identifier":{"key":"~myproject~Height","namespace":"taxonomy"},"x":85,"y":52,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"4a1ce445-17c0-4fb2-b103-ae0c90cf5683"},{"identifier":{"key":"~myproject~Latitude","namespace":"taxonomy"},"x":85,"y":60,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"eaf4fdf1-b14b-45b2-b138-69e72e85fdbd"},{"identifier":{"key":"~myproject~Longitude","namespace":"taxonomy"},"x":85,"y":62,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"259174e8-4a30-449e-bc2f-dbfb58f0754d"},{"identifier":{"key":"~myproject~Magnetometer.x","namespace":"taxonomy"},"x":85,"y":64,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"3e427410-9906-454a-8cfc-fc6e2955dc2e"},{"identifier":{"key":"~myproject~Magnetometer.y","namespace":"taxonomy"},"x":85,"y":66,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"0d735f42-efd5-4122-afc4-f97067776d6a"},{"identifier":{"key":"~myproject~Magnetometer.z","namespace":"taxonomy"},"x":85,"y":68,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"e7ec6551-1e6c-4133-8521-9c2ceb408341"},{"identifier":{"key":"~myproject~Mode_Day","namespace":"taxonomy"},"x":85,"y":70,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"aa7bf8ef-581c-4ce4-9989-5a10e2bee67a"},{"identifier":{"key":"~myproject~Mode_Night","namespace":"taxonomy"},"x":85,"y":72,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"2d043561-9b84-4f30-9f1c-8ebb76c05d66"},{"identifier":{"key":"~myproject~Mode_Payload","namespace":"taxonomy"},"x":85,"y":74,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"eaf50270-1ec4-4c01-b8ee-1434a00e5880"},{"identifier":{"key":"~myproject~Mode_Safe","namespace":"taxonomy"},"x":85,"y":76,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"87a3749e-411b-4997-859d-16c5b245915e"},{"identifier":{"key":"~myproject~Mode_SBand","namespace":"taxonomy"},"x":85,"y":78,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"a6f0dffa-bf9d-4015-88a6-eb2ffd7b6843"},{"identifier":{"key":"~myproject~Mode_XBand","namespace":"taxonomy"},"x":85,"y":80,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"e2e052b0-c65b-4656-a950-b1e7daeab350"},{"identifier":{"key":"~myproject~OrbitNumberCumulative","namespace":"taxonomy"},"x":85,"y":82,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"1b25db44-8063-4bc7-ae97-00d6dc850516"},{"identifier":{"key":"~myproject~Payload_Error_Flag","namespace":"taxonomy"},"x":85,"y":84,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"caac2d53-dabf-4b93-bde9-fb9fd913a562"},{"identifier":{"key":"~myproject~Payload_Status","namespace":"taxonomy"},"x":85,"y":86,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"f9c1fc8b-6545-41ac-956c-8beb8df47675"},{"identifier":{"key":"~myproject~Position.x","namespace":"taxonomy"},"x":85,"y":88,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"25b25ba0-0983-4613-8f71-96e04f6091c1"},{"identifier":{"key":"~myproject~Position.y","namespace":"taxonomy"},"x":85,"y":90,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"7ea7c493-49af-469c-854e-cea5fcd58e6a"},{"identifier":{"key":"~myproject~Position.z","namespace":"taxonomy"},"x":85,"y":92,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"a7a6904f-e438-443d-b746-a790ffb86f6a"},{"identifier":{"key":"~myproject~Shadow","namespace":"taxonomy"},"x":85,"y":94,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"25e93f35-138c-4fff-b032-852647d642dc"},{"identifier":{"key":"~myproject~Velocity.x","namespace":"taxonomy"},"x":85,"y":96,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"b7085d65-8192-4e57-97b9-5efc7b6100e8"},{"identifier":{"key":"~myproject~Velocity.y","namespace":"taxonomy"},"x":85,"y":98,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"ce20ed52-545e-46f6-8d08-adcf4ede2737"},{"identifier":{"key":"~myproject~Velocity.z","namespace":"taxonomy"},"x":85,"y":100,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"da57e178-0680-4c88-886e-c05ff5cc0ec9"},{"width":32,"height":18,"x":124,"y":56,"identifier":{"key":"1816debc-38b6-4680-8019-701689be4fc5","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"d5a9f702-c463-4d26-bdde-20389902169d"},{"width":32,"height":18,"x":156,"y":56,"identifier":{"key":"cc400028-b156-43cc-8bae-de13171d5431","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"d42d03b9-c936-4a9d-8e4a-00a3df8ae384"},{"width":32,"height":18,"x":124,"y":2,"identifier":{"key":"798e729f-420c-45f6-aecf-5bbc44b55dad","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"d544d8df-07cc-4054-a076-d61426a9e82d"},{"width":32,"height":18,"x":156,"y":38,"identifier":{"key":"fada1f9c-668d-49c4-a653-81bb3d466922","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"ca660084-6262-4578-a520-591b187d88f1"},{"width":32,"height":18,"x":156,"y":2,"identifier":{"key":"c3eb99c4-db3c-4a92-8967-3b3052efd0cf","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"c97e1608-bc2b-43ee-ac72-8cf26f6dff3b"},{"width":32,"height":18,"x":124,"y":74,"identifier":{"key":"0dd60b42-66a3-4326-b829-2cde71025d32","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"06c85011-f501-4bc2-84f2-0a901ade5361"},{"width":32,"height":18,"x":124,"y":20,"identifier":{"key":"eeda19bc-6302-47b6-b115-81204b16aaa3","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"a04e01e6-2e43-42e2-8968-969288cc7580"},{"width":32,"height":18,"x":156,"y":20,"identifier":{"key":"3093a1ef-ac30-4ac3-abff-e4ef5ca858e6","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"25d60c4a-a7e4-4c89-b3bc-710bde8b205f"},{"width":32,"height":18,"x":124,"y":38,"identifier":{"key":"e4b66733-2e89-4bf8-8a12-5ef3f247e4ec","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"ff6578eb-8ba3-4004-ad5d-6eabad72a64a"},{"width":32,"height":18,"x":124,"y":92,"identifier":{"key":"a91d265d-b36d-41fe-9d11-b799e52036ed","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"94c9c35e-bf2c-43e5-9926-801947805262"},{"width":32,"height":18,"x":188,"y":20,"identifier":{"key":"739c26b2-776a-4abb-949f-24a2066aee80","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"2a683f8f-2683-4388-9395-1cbe10e61a8a"},{"width":32,"height":18,"x":188,"y":2,"identifier":{"key":"94997ceb-00c8-4702-af70-afb014cdf629","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"801c5226-dc3d-4ba6-a32f-b000bb49cf68"},{"width":32,"height":18,"x":188,"y":38,"identifier":{"key":"803121a1-d187-489f-a124-5e903657bedb","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"62d173e7-dceb-4a4f-b2aa-9a9b776cd799"},{"width":32,"height":18,"x":188,"y":56,"identifier":{"key":"c984a025-e4a8-4c25-9b0e-1723b5a30303","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"9f3b0d07-e282-4d20-986e-ec986072174f"},{"width":32,"height":18,"x":156,"y":74,"identifier":{"key":"95cac0f8-cc42-4357-aac8-77d7ea5e31c9","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"f979e809-017a-43fe-ae53-ac681131a6b6"},{"width":32,"height":18,"x":188,"y":74,"identifier":{"key":"1011058f-8dec-4426-87d9-58135f663ea6","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"29dd421b-c146-4812-9ae6-362c943d6364"},{"width":32,"height":18,"x":156,"y":92,"identifier":{"key":"c1881c9a-a638-4bbe-b320-a0ea5d7f4e4f","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"2f2f5de0-a7ab-4f5b-97af-667ae4fe1485"},{"width":32,"height":18,"x":188,"y":92,"identifier":{"key":"d510bf8a-f03d-4cac-b58c-1242cc039b22","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"962c557d-1220-49ce-a478-a2f513d4aadf"},{"width":21,"height":13,"x":1,"y":0,"identifier":{"key":"a211ae9a-f013-4a97-87ed-401baee94a3e","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"dea5d1fb-bef0-426a-bb94-de8c2b11e431"},{"width":21,"height":13,"x":22,"y":0,"identifier":{"key":"f051b323-d22a-419a-a6e6-42ea6041b238","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"3bed0cbd-3630-46e8-af19-d07f28ea77b0"},{"width":21,"height":13,"x":43,"y":0,"identifier":{"key":"77ce4615-53d4-4ae7-809c-8535a57a041b","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"845a3124-331a-47c4-88bb-25bb1db37dda"},{"width":21,"height":13,"x":64,"y":0,"identifier":{"key":"b425c7bd-6912-4e40-908e-6a21c73c7db3","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"262a783c-9e5c-4987-9e65-f8d8705b0772"},{"identifier":{"key":"~myproject~Sunsensor","namespace":"taxonomy"},"x":85,"y":102,"width":37,"height":2,"displayMode":"all","value":"value","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"b27f8bbe-7aea-4680-95be-2dac4ab0399d"}],"layoutGrid":[10,10],"objectStyles":{}},"modified":1709249767953,"location":"mine","modifiedBy":"guest","createdBy":"guest","created":1707870958033,"persisted":1709249767979},"797785c7-035a-4ea2-b69b-d595cf48b49c":{"identifier":{"key":"797785c7-035a-4ea2-b69b-d595cf48b49c","namespace":""},"name":"Test LAD Table","type":"LadTable","composition":[{"key":"~myproject~A","namespace":"taxonomy"},{"key":"~myproject~ADCS_Error_Flag","namespace":"taxonomy"},{"key":"~myproject~Battery1_Temp","namespace":"taxonomy"},{"key":"~myproject~Battery1_Voltage","namespace":"taxonomy"},{"key":"~myproject~Battery2_Temp","namespace":"taxonomy"},{"key":"~myproject~Battery2_Voltage","namespace":"taxonomy"},{"key":"~myproject~CCSDS_Packet_Length","namespace":"taxonomy"},{"key":"~myproject~CDHS_Error_Flag","namespace":"taxonomy"},{"key":"~myproject~CDHS_Status","namespace":"taxonomy"},{"key":"~myproject~COMMS_Error_Flag","namespace":"taxonomy"},{"key":"~myproject~COMMS_Status","namespace":"taxonomy"},{"key":"~myproject~Contact_Golbasi_GS","namespace":"taxonomy"},{"key":"~myproject~Contact_Svalbard","namespace":"taxonomy"},{"key":"~myproject~Detector_Temp","namespace":"taxonomy"},{"key":"~myproject~ElapsedSeconds","namespace":"taxonomy"},{"key":"~myproject~EpochUSNO","namespace":"taxonomy"},{"key":"~myproject~Enum_Para_1","namespace":"taxonomy"},{"key":"~myproject~Enum_Para_2","namespace":"taxonomy"},{"key":"~myproject~Enum_Para_3","namespace":"taxonomy"},{"key":"~myproject~EPS_Error_Flag","namespace":"taxonomy"},{"key":"~myproject~Gyro.x","namespace":"taxonomy"},{"key":"~myproject~Gyro.y","namespace":"taxonomy"},{"key":"~myproject~Gyro.z","namespace":"taxonomy"},{"key":"~myproject~Height","namespace":"taxonomy"},{"key":"~myproject~Latitude","namespace":"taxonomy"},{"key":"~myproject~Longitude","namespace":"taxonomy"},{"key":"~myproject~Magnetometer.x","namespace":"taxonomy"},{"key":"~myproject~Magnetometer.y","namespace":"taxonomy"},{"key":"~myproject~Magnetometer.z","namespace":"taxonomy"},{"key":"~myproject~Mode_Day","namespace":"taxonomy"},{"key":"~myproject~Mode_Night","namespace":"taxonomy"},{"key":"~myproject~Mode_Payload","namespace":"taxonomy"},{"key":"~myproject~Mode_Safe","namespace":"taxonomy"},{"key":"~myproject~Mode_SBand","namespace":"taxonomy"},{"key":"~myproject~Mode_XBand","namespace":"taxonomy"},{"key":"~myproject~OrbitNumberCumulative","namespace":"taxonomy"},{"key":"~myproject~Payload_Error_Flag","namespace":"taxonomy"},{"key":"~myproject~Payload_Status","namespace":"taxonomy"},{"key":"~myproject~Position.x","namespace":"taxonomy"},{"key":"~myproject~Position.y","namespace":"taxonomy"},{"key":"~myproject~Position.z","namespace":"taxonomy"},{"key":"~myproject~Shadow","namespace":"taxonomy"},{"key":"~myproject~Velocity.x","namespace":"taxonomy"},{"key":"~myproject~Velocity.y","namespace":"taxonomy"},{"key":"~myproject~Velocity.z","namespace":"taxonomy"},{"key":"~myproject~Sunsensor","namespace":"taxonomy"}],"modifiedBy":"guest","createdBy":"guest","created":1701396684986,"location":"c965501f-86c7-4d63-8857-95618462ea2d","modified":1708721945970,"persisted":1708721945972},"1816debc-38b6-4680-8019-701689be4fc5":{"identifier":{"key":"1816debc-38b6-4680-8019-701689be4fc5","namespace":""},"type":"telemetry.plot.overlay","composition":[{"key":"~myproject~Magnetometer.x","namespace":"taxonomy"}],"configuration":{"series":[{"identifier":{"key":"~myproject~Magnetometer.x","namespace":"taxonomy"}}]},"name":"Magnetometer.x","location":"c965501f-86c7-4d63-8857-95618462ea2d","modified":1708720530618,"modifiedBy":"guest","createdBy":"guest","created":1708720440462,"persisted":1708720530618},"cc400028-b156-43cc-8bae-de13171d5431":{"identifier":{"key":"cc400028-b156-43cc-8bae-de13171d5431","namespace":""},"type":"telemetry.plot.overlay","composition":[{"key":"~myproject~Magnetometer.y","namespace":"taxonomy"}],"configuration":{"series":[{"identifier":{"key":"~myproject~Magnetometer.y","namespace":"taxonomy"}}]},"name":"Magnetometer.y","location":"c965501f-86c7-4d63-8857-95618462ea2d","modified":1708720530625,"modifiedBy":"guest","createdBy":"guest","created":1708720446196,"persisted":1708720530625},"798e729f-420c-45f6-aecf-5bbc44b55dad":{"identifier":{"key":"798e729f-420c-45f6-aecf-5bbc44b55dad","namespace":""},"type":"telemetry.plot.overlay","composition":[{"key":"~myproject~Magnetometer.z","namespace":"taxonomy"}],"configuration":{"series":[{"identifier":{"key":"~myproject~Magnetometer.z","namespace":"taxonomy"}}]},"name":"Magnetometer.z","location":"c965501f-86c7-4d63-8857-95618462ea2d","modified":1708720530631,"modifiedBy":"guest","createdBy":"guest","created":1708720452504,"persisted":1708720530631},"fada1f9c-668d-49c4-a653-81bb3d466922":{"identifier":{"key":"fada1f9c-668d-49c4-a653-81bb3d466922","namespace":""},"type":"telemetry.plot.overlay","composition":[{"key":"~myproject~Position.x","namespace":"taxonomy"}],"configuration":{"series":[{"identifier":{"key":"~myproject~Position.x","namespace":"taxonomy"}}]},"name":"Position.x","location":"c965501f-86c7-4d63-8857-95618462ea2d","modified":1708720530637,"modifiedBy":"guest","createdBy":"guest","created":1708720459109,"persisted":1708720530637},"c3eb99c4-db3c-4a92-8967-3b3052efd0cf":{"identifier":{"key":"c3eb99c4-db3c-4a92-8967-3b3052efd0cf","namespace":""},"type":"telemetry.plot.overlay","composition":[{"key":"~myproject~Position.y","namespace":"taxonomy"}],"configuration":{"series":[{"identifier":{"key":"~myproject~Position.y","namespace":"taxonomy"}}]},"name":"Position.y","location":"c965501f-86c7-4d63-8857-95618462ea2d","modified":1708720530643,"modifiedBy":"guest","createdBy":"guest","created":1708720465842,"persisted":1708720530643},"0dd60b42-66a3-4326-b829-2cde71025d32":{"identifier":{"key":"0dd60b42-66a3-4326-b829-2cde71025d32","namespace":""},"type":"telemetry.plot.overlay","composition":[{"key":"~myproject~Position.z","namespace":"taxonomy"}],"configuration":{"series":[{"identifier":{"key":"~myproject~Position.z","namespace":"taxonomy"}}]},"name":"Position.z","location":"c965501f-86c7-4d63-8857-95618462ea2d","modified":1708720530649,"modifiedBy":"guest","createdBy":"guest","created":1708720471945,"persisted":1708720530649},"eeda19bc-6302-47b6-b115-81204b16aaa3":{"identifier":{"key":"eeda19bc-6302-47b6-b115-81204b16aaa3","namespace":""},"type":"telemetry.plot.overlay","composition":[{"key":"~myproject~Velocity.x","namespace":"taxonomy"}],"configuration":{"series":[{"identifier":{"key":"~myproject~Velocity.x","namespace":"taxonomy"}}]},"name":"Velocity.x","location":"c965501f-86c7-4d63-8857-95618462ea2d","modified":1708720530655,"modifiedBy":"guest","createdBy":"guest","created":1708720478416,"persisted":1708720530655},"3093a1ef-ac30-4ac3-abff-e4ef5ca858e6":{"identifier":{"key":"3093a1ef-ac30-4ac3-abff-e4ef5ca858e6","namespace":""},"type":"telemetry.plot.overlay","composition":[{"key":"~myproject~Velocity.y","namespace":"taxonomy"}],"configuration":{"series":[{"identifier":{"key":"~myproject~Velocity.y","namespace":"taxonomy"}}]},"name":"Velocity.y","location":"c965501f-86c7-4d63-8857-95618462ea2d","modified":1708720530661,"modifiedBy":"guest","createdBy":"guest","created":1708720484585,"persisted":1708720530661},"e4b66733-2e89-4bf8-8a12-5ef3f247e4ec":{"identifier":{"key":"e4b66733-2e89-4bf8-8a12-5ef3f247e4ec","namespace":""},"type":"telemetry.plot.overlay","composition":[{"key":"~myproject~Velocity.z","namespace":"taxonomy"}],"configuration":{"series":[{"identifier":{"key":"~myproject~Velocity.z","namespace":"taxonomy"}}]},"name":"Velocity.z","location":"c965501f-86c7-4d63-8857-95618462ea2d","modified":1708720530667,"modifiedBy":"guest","createdBy":"guest","created":1708720489953,"persisted":1708720530667},"a91d265d-b36d-41fe-9d11-b799e52036ed":{"identifier":{"key":"a91d265d-b36d-41fe-9d11-b799e52036ed","namespace":""},"type":"telemetry.plot.overlay","composition":[{"key":"~myproject~Battery1_Temp","namespace":"taxonomy"}],"configuration":{"series":[{"identifier":{"key":"~myproject~Battery1_Temp","namespace":"taxonomy"}}]},"name":"Battery1_Temp","location":"c965501f-86c7-4d63-8857-95618462ea2d","modified":1708720829096,"modifiedBy":"guest","createdBy":"guest","created":1708720730175,"persisted":1708720829096},"739c26b2-776a-4abb-949f-24a2066aee80":{"identifier":{"key":"739c26b2-776a-4abb-949f-24a2066aee80","namespace":""},"type":"telemetry.plot.overlay","composition":[{"key":"~myproject~Battery1_Voltage","namespace":"taxonomy"}],"configuration":{"series":[{"identifier":{"key":"~myproject~Battery1_Voltage","namespace":"taxonomy"}}]},"name":"Battery1_Voltage","location":"c965501f-86c7-4d63-8857-95618462ea2d","modified":1708720829103,"modifiedBy":"guest","createdBy":"guest","created":1708720736954,"persisted":1708720829103},"94997ceb-00c8-4702-af70-afb014cdf629":{"identifier":{"key":"94997ceb-00c8-4702-af70-afb014cdf629","namespace":""},"type":"telemetry.plot.overlay","composition":[{"key":"~myproject~Battery2_Temp","namespace":"taxonomy"}],"configuration":{"series":[{"identifier":{"key":"~myproject~Battery2_Temp","namespace":"taxonomy"}}]},"name":"Battery2_Temp","location":"c965501f-86c7-4d63-8857-95618462ea2d","modified":1708720829110,"modifiedBy":"guest","createdBy":"guest","created":1708720741955,"persisted":1708720829110},"803121a1-d187-489f-a124-5e903657bedb":{"identifier":{"key":"803121a1-d187-489f-a124-5e903657bedb","namespace":""},"type":"telemetry.plot.overlay","composition":[{"key":"~myproject~Battery2_Voltage","namespace":"taxonomy"}],"configuration":{"series":[{"identifier":{"key":"~myproject~Battery2_Voltage","namespace":"taxonomy"}}]},"name":"Battery2_Voltage","location":"c965501f-86c7-4d63-8857-95618462ea2d","modified":1708720829117,"modifiedBy":"guest","createdBy":"guest","created":1708720755105,"persisted":1708720829117},"c984a025-e4a8-4c25-9b0e-1723b5a30303":{"identifier":{"key":"c984a025-e4a8-4c25-9b0e-1723b5a30303","namespace":""},"type":"telemetry.plot.overlay","composition":[{"key":"~myproject~ElapsedSeconds","namespace":"taxonomy"}],"configuration":{"series":[{"identifier":{"key":"~myproject~ElapsedSeconds","namespace":"taxonomy"}}]},"name":"ElapsedSeconds","location":"c965501f-86c7-4d63-8857-95618462ea2d","modified":1708720829124,"modifiedBy":"guest","createdBy":"guest","created":1708720769807,"persisted":1708720829124},"95cac0f8-cc42-4357-aac8-77d7ea5e31c9":{"identifier":{"key":"95cac0f8-cc42-4357-aac8-77d7ea5e31c9","namespace":""},"type":"telemetry.plot.overlay","composition":[{"key":"~myproject~Gyro.x","namespace":"taxonomy"}],"configuration":{"series":[{"identifier":{"key":"~myproject~Gyro.x","namespace":"taxonomy"}}]},"name":"Gyro.x","location":"c965501f-86c7-4d63-8857-95618462ea2d","modified":1708720829132,"modifiedBy":"guest","createdBy":"guest","created":1708720777813,"persisted":1708720829132},"1011058f-8dec-4426-87d9-58135f663ea6":{"identifier":{"key":"1011058f-8dec-4426-87d9-58135f663ea6","namespace":""},"type":"telemetry.plot.overlay","composition":[{"key":"~myproject~Gyro.y","namespace":"taxonomy"}],"configuration":{"series":[{"identifier":{"key":"~myproject~Gyro.y","namespace":"taxonomy"}}]},"name":"Gyro.y","location":"c965501f-86c7-4d63-8857-95618462ea2d","modified":1708720829138,"modifiedBy":"guest","createdBy":"guest","created":1708720785052,"persisted":1708720829138},"c1881c9a-a638-4bbe-b320-a0ea5d7f4e4f":{"identifier":{"key":"c1881c9a-a638-4bbe-b320-a0ea5d7f4e4f","namespace":""},"type":"telemetry.plot.overlay","composition":[{"key":"~myproject~Gyro.z","namespace":"taxonomy"}],"configuration":{"series":[{"identifier":{"key":"~myproject~Gyro.z","namespace":"taxonomy"}}]},"name":"Gyro.z","location":"c965501f-86c7-4d63-8857-95618462ea2d","modified":1708720829144,"modifiedBy":"guest","createdBy":"guest","created":1708720792578,"persisted":1708720829144},"d510bf8a-f03d-4cac-b58c-1242cc039b22":{"identifier":{"key":"d510bf8a-f03d-4cac-b58c-1242cc039b22","namespace":""},"type":"telemetry.plot.overlay","composition":[{"key":"~myproject~Latitude","namespace":"taxonomy"}],"configuration":{"series":[{"identifier":{"key":"~myproject~Latitude","namespace":"taxonomy"}}]},"name":"Latitude","location":"c965501f-86c7-4d63-8857-95618462ea2d","modified":1708720829150,"modifiedBy":"guest","createdBy":"guest","created":1708720799474,"persisted":1708720829150},"a211ae9a-f013-4a97-87ed-401baee94a3e":{"identifier":{"key":"a211ae9a-f013-4a97-87ed-401baee94a3e","namespace":""},"name":"Battery1_Temp","type":"gauge","composition":[{"key":"~myproject~Battery1_Temp","namespace":"taxonomy"}],"configuration":{"gaugeController":{"gaugeType":"dial-filled","isDisplayMinMax":true,"isDisplayCurVal":true,"isDisplayUnits":true,"isUseTelemetryLimits":true,"limitLow":10,"limitHigh":90,"max":100,"min":0,"precision":2}},"modified":1709078820625,"location":"c965501f-86c7-4d63-8857-95618462ea2d","modifiedBy":"guest","createdBy":"guest","created":1708720863228,"persisted":1709078820625},"f051b323-d22a-419a-a6e6-42ea6041b238":{"identifier":{"key":"f051b323-d22a-419a-a6e6-42ea6041b238","namespace":""},"name":"Battery1_Voltage","type":"gauge","composition":[{"key":"~myproject~Battery1_Voltage","namespace":"taxonomy"}],"configuration":{"gaugeController":{"gaugeType":"dial-filled","isDisplayMinMax":true,"isDisplayCurVal":true,"isDisplayUnits":true,"isUseTelemetryLimits":true,"limitLow":10,"limitHigh":90,"max":100,"min":0,"precision":2}},"modified":1709078831357,"location":"c965501f-86c7-4d63-8857-95618462ea2d","modifiedBy":"guest","createdBy":"guest","created":1708720957879,"persisted":1709078831358},"77ce4615-53d4-4ae7-809c-8535a57a041b":{"identifier":{"key":"77ce4615-53d4-4ae7-809c-8535a57a041b","namespace":""},"name":"Battery2_Temp","type":"gauge","composition":[{"key":"~myproject~Battery2_Temp","namespace":"taxonomy"}],"configuration":{"gaugeController":{"gaugeType":"dial-filled","isDisplayMinMax":true,"isDisplayCurVal":true,"isDisplayUnits":true,"isUseTelemetryLimits":true,"limitLow":10,"limitHigh":90,"max":100,"min":0,"precision":2}},"modified":1709078840709,"location":"c965501f-86c7-4d63-8857-95618462ea2d","modifiedBy":"guest","createdBy":"guest","created":1708720982313,"persisted":1709078840709},"b425c7bd-6912-4e40-908e-6a21c73c7db3":{"identifier":{"key":"b425c7bd-6912-4e40-908e-6a21c73c7db3","namespace":""},"name":"Battery2_Voltage","type":"gauge","composition":[{"key":"~myproject~Battery2_Voltage","namespace":"taxonomy"}],"configuration":{"gaugeController":{"gaugeType":"dial-filled","isDisplayMinMax":true,"isDisplayCurVal":true,"isDisplayUnits":true,"isUseTelemetryLimits":true,"limitLow":10,"limitHigh":90,"max":100,"min":0,"precision":2}},"modified":1709078853693,"location":"c965501f-86c7-4d63-8857-95618462ea2d","modifiedBy":"guest","createdBy":"guest","created":1708721005947,"persisted":1709078853693}},"rootId":"c965501f-86c7-4d63-8857-95618462ea2d"} \ No newline at end of file diff --git a/tests/git-opensource-tests.sh b/tests/git-opensource-tests.sh index 419bbbf5..9c962a6f 100644 --- a/tests/git-opensource-tests.sh +++ b/tests/git-opensource-tests.sh @@ -21,6 +21,7 @@ REPO_URL=https://github.com/nasa/openmct.git REPO_PATH=e2e LOCAL_REPO_ROOT="e2e/opensource" +# remove the branch later git clone --no-checkout --depth 1 $REPO_URL "$LOCAL_REPO_ROOT" cd "$LOCAL_REPO_ROOT" git config core.sparsecheckout true @@ -30,6 +31,10 @@ git read-tree -m -u HEAD # moving back to /tests/ dir cd .. +# Move index.js to root +mv opensource/e2e/index.js ./opensource +# Move package.json, package-lock.json +mv opensource/e2e/package*.json ./opensource # Move fixtures and appActions mv opensource/e2e/*Fixtures.js ./opensource mv opensource/e2e/appActions.js ./opensource