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