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