From 67fac94533b53c9a06946b6588e0d02b186f649d Mon Sep 17 00:00:00 2001 From: Jesse Mazzella Date: Tue, 1 Oct 2024 16:56:09 -0700 Subject: [PATCH] fix(VIPERGC-574): update fault provider for new YAMCS API and provide shelve durations (#481) * update api endpoints for fault management * refactor: general cleanup, rename functions, add consts * feat: provider can specify its own shelveDurations * fix: omit 'state' from acknowledge request body as per new API * test: initial setup. set alarms and clear them before/after suite * test(FaultManagement): shows faults of differing severities * test(FaultManagement): add tests for shelving and acknowledging a fault * test: update test locators * fix: be explicit about `#master` in build:example:master * fix: respond to comments * refactor: pass in processor too * fix: lint * fix: stabilize fault management yamcs tests * chore: bump playwright to 1.47.2 * test: increase shelve duration to make test more stable * fix: increase shelve duration summore * build: try using docker override file for host network mode - this is so we can avoid binding to 0.0.0.0 in the dev server * Revert "build: try using docker override file for host network mode" This reverts commit 95924a7e1137842d09187a0ac6ca873b82db40e2. --------- Co-authored-by: Jamie V --- .github/workflows/yamcs-quickstart-e2e.yml | 2 +- package.json | 2 +- src/openmct-yamcs.js | 5 +- .../fault-action-provider.js | 90 ++++-- .../fault-mgmt-constants.js | 12 +- .../historical-fault-provider.js | 23 +- .../realtime-fault-provider.js | 12 +- src/providers/fault-mgmt-providers/utils.js | 59 +++- .../yamcs-fault-provider.js | 7 +- tests/e2e/yamcs/faultManagement.e2e.spec.mjs | 266 +++++++++++++++++- 10 files changed, 404 insertions(+), 74 deletions(-) diff --git a/.github/workflows/yamcs-quickstart-e2e.yml b/.github/workflows/yamcs-quickstart-e2e.yml index ad5f2461..5efa438b 100644 --- a/.github/workflows/yamcs-quickstart-e2e.yml +++ b/.github/workflows/yamcs-quickstart-e2e.yml @@ -59,7 +59,7 @@ jobs: elif [ "${{ matrix.openmct-version }}" = "stable" ]; then npm run build:example fi - - run: npx playwright@1.45.2 install chromium + - run: npx playwright@1.47.2 install chromium - name: Check that yamcs is available run: | docker ps -a diff --git a/package.json b/package.json index 03f0da86..8e16c70c 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "lint:fix": "eslint src example --fix", "build:dist": "webpack --config ./.webpack/webpack.prod.mjs", "build:example": "npm install openmct@unstable --no-save", - "build:example:master": "npm install nasa/openmct --no-save", + "build:example:master": "npm install nasa/openmct#master --no-save", "build:example:currentbranch": "npm install nasa/openmct#$(git rev-parse --abbrev-ref HEAD) --no-save --verbose", "postbuild:example": "node check-optional-dependencies.mjs", "start": "npx webpack serve --config ./.webpack/webpack.dev.mjs", diff --git a/src/openmct-yamcs.js b/src/openmct-yamcs.js index 4d0cd741..04bb2bc8 100644 --- a/src/openmct-yamcs.js +++ b/src/openmct-yamcs.js @@ -28,7 +28,6 @@ import LimitProvider from './providers/limit-provider.js'; import EventLimitProvider from './providers/event-limit-provider.js'; import UserProvider from './providers/user/user-provider.js'; -import { faultModelConvertor } from './providers/fault-mgmt-providers/utils.js'; import YamcsFaultProvider from './providers/fault-mgmt-providers/yamcs-fault-provider.js'; import { OBJECT_TYPES } from './const.js'; @@ -76,9 +75,9 @@ export default function install( openmct.faults.addProvider(new YamcsFaultProvider(openmct, { - faultModelConvertor, historicalEndpoint: configuration.yamcsHistoricalEndpoint, - yamcsInstance: configuration.yamcsInstance + yamcsInstance: configuration.yamcsInstance, + yamcsProcessor: configuration.yamcsProcessor })); const stalenessProvider = new YamcsStalenessProvider( diff --git a/src/providers/fault-mgmt-providers/fault-action-provider.js b/src/providers/fault-mgmt-providers/fault-action-provider.js index 61e83b9e..ad986293 100644 --- a/src/providers/fault-mgmt-providers/fault-action-provider.js +++ b/src/providers/fault-mgmt-providers/fault-action-provider.js @@ -1,7 +1,7 @@ -import { FAULT_MANAGEMENT_ALARMS, FAULT_MANAGEMENT_DEFAULT_SHELVE_DURATION } from './fault-mgmt-constants.js'; +import { FAULT_MGMT_ALARMS, FAULT_MGMT_ACTIONS } from './fault-mgmt-constants.js'; export default class FaultActionProvider { - constructor(url, instance, processor = 'realtime') { + constructor(url, instance, processor) { this.url = url; this.instance = instance; this.processor = processor; @@ -9,52 +9,92 @@ export default class FaultActionProvider { acknowledgeFault(fault, { comment = '' } = {}) { const payload = { - comment, - state: 'acknowledged' + comment }; - const options = this._getOptions(payload); - const url = this._getUrl(fault); + const options = this.#getOptions(payload); + const url = this.#getUrl(fault, FAULT_MGMT_ACTIONS.ACKNOWLEDGE); - return this._sendRequest(url, options); + return this.#sendRequest(url, options); } - shelveFault(fault, { shelved = true, comment = '', shelveDuration = FAULT_MANAGEMENT_DEFAULT_SHELVE_DURATION } = {}) { - let payload = {}; + /** + * Shelves or unshelves a fault. + * @param {FaultModel} fault the fault to perform the action on + * @param {Object} options the options to perform the action with + * @param {boolean} options.shelved whether to shelve or unshelve the fault + * @param {string} options.comment the comment to add to the fault + * @param {number} options.shelveDuration the duration to shelve the fault for + * @returns {Promise} the response from the server + */ + shelveFault(fault, { shelved = true, comment = '', shelveDuration } = {}) { + const payload = {}; + const action = shelved ? FAULT_MGMT_ACTIONS.SHELVE : FAULT_MGMT_ACTIONS.UNSHELVE; + if (shelved) { payload.comment = comment; payload.shelveDuration = shelveDuration; - payload.state = 'shelved'; - } else { - payload.state = 'unshelved'; } - const options = this._getOptions(payload); - let url = this._getUrl(fault); + const options = this.#getOptions(payload); + const url = this.#getUrl(fault, action); - return this._sendRequest(url, options); + return this.#sendRequest(url, options); } - _getOptions(payload) { + /** + * @typedef {Object} ShelveDuration + * @property {string} name - The name of the shelve duration + * @property {number|null} value - The value of the shelve duration in milliseconds, or null for indefinite + */ + + /** + * @returns {ShelveDuration[]} the list of shelve durations + */ + getShelveDurations() { + return [ + { + name: '5 Minutes', + value: 300000 + }, + { + name: '10 Minutes', + value: 600000 + }, + { + name: '15 Minutes', + value: 900000 + }, + { + name: 'Indefinite', + value: null + } + ]; + } + + #getOptions(payload) { return { body: JSON.stringify(payload), - // credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, - method: 'PATCH', + method: 'POST', mode: 'cors' }; } - _getUrl(fault) { - let url = `${this.url}api/processors/${this.instance}/${this.processor}/${FAULT_MANAGEMENT_ALARMS}`; - url += `${fault.namespace}/${fault.name}`; - url += `/${fault.seqNum}`; - - return url; + /** + * @param {FaultModel} fault the fault to perform the action on + * @param {'acknowledge' | 'shelve' | 'unshelve' | 'clear'} action the action to perform on the fault + * @returns {string} the URL to perform the action on the fault + */ + #getUrl(fault, action) { + return `${this.url}api/processors/${this.instance}/${this.processor}/${FAULT_MGMT_ALARMS}` + + `${fault.namespace}/${fault.name}/${fault.seqNum}:${action}`; } - _sendRequest(url, options) { + #sendRequest(url, options) { return fetch(url, options); } } + +/** @typedef {import('./utils.js').FaultModel} FaultModel */ diff --git a/src/providers/fault-mgmt-providers/fault-mgmt-constants.js b/src/providers/fault-mgmt-providers/fault-mgmt-constants.js index dfb37a34..3205a400 100644 --- a/src/providers/fault-mgmt-providers/fault-mgmt-constants.js +++ b/src/providers/fault-mgmt-providers/fault-mgmt-constants.js @@ -1,3 +1,9 @@ -export const FAULT_MANAGEMENT_ALARMS = 'alarms'; -export const FAULT_MANAGEMENT_TYPE = 'faultManagement'; -export const FAULT_MANAGEMENT_DEFAULT_SHELVE_DURATION = 90000; +export const FAULT_MGMT_ALARMS = 'alarms'; +export const FAULT_MGMT_TYPE = 'faultManagement'; +export const DEFAULT_SHELVE_DURATION = 90000; +export const FAULT_MGMT_ACTIONS = Object.freeze({ + SHELVE: 'shelve', + UNSHELVE: 'unshelve', + ACKNOWLEDGE: 'acknowledge', + CLEAR: 'clear' +}); diff --git a/src/providers/fault-mgmt-providers/historical-fault-provider.js b/src/providers/fault-mgmt-providers/historical-fault-provider.js index c7a5ce10..4c1d7ee1 100644 --- a/src/providers/fault-mgmt-providers/historical-fault-provider.js +++ b/src/providers/fault-mgmt-providers/historical-fault-provider.js @@ -1,23 +1,34 @@ -import { FAULT_MANAGEMENT_ALARMS, FAULT_MANAGEMENT_TYPE } from './fault-mgmt-constants.js'; +import { FAULT_MGMT_ALARMS, FAULT_MGMT_TYPE } from './fault-mgmt-constants.js'; +import { convertDataToFaultModel } from './utils.js'; export default class HistoricalFaultProvider { - constructor(faultModelConverter, url, instance, processor = 'realtime') { - this.faultModelConverter = faultModelConverter; + constructor(url, instance, processor) { this.url = url; this.instance = instance; this.processor = processor; } + /** + * @param {import('openmct').DomainObject} domainObject + * @returns {boolean} + */ supportsRequest(domainObject) { - return domainObject.type === FAULT_MANAGEMENT_TYPE; + return domainObject.type === FAULT_MGMT_TYPE; } + /** + * @returns {Promise} + */ async request() { - let url = `${this.url}api/processors/${this.instance}/${this.processor}/${FAULT_MANAGEMENT_ALARMS}`; + const url = `${this.url}api/processors/${this.instance}/${this.processor}/${FAULT_MGMT_ALARMS}`; const res = await fetch(url); const faultsData = await res.json(); - return faultsData.alarms?.map(this.faultModelConverter); + return faultsData.alarms?.map(convertDataToFaultModel); } } + +/** + * @typedef {import('./utils.js').FaultModel} FaultModel + */ diff --git a/src/providers/fault-mgmt-providers/realtime-fault-provider.js b/src/providers/fault-mgmt-providers/realtime-fault-provider.js index c1952b3f..3c64622b 100644 --- a/src/providers/fault-mgmt-providers/realtime-fault-provider.js +++ b/src/providers/fault-mgmt-providers/realtime-fault-provider.js @@ -1,11 +1,11 @@ -import { FAULT_MANAGEMENT_TYPE } from './fault-mgmt-constants.js'; +import { FAULT_MGMT_TYPE } from './fault-mgmt-constants.js'; import { DATA_TYPES, NAMESPACE, OBJECT_TYPES } from '../../const.js'; +import { convertDataToFaultModel } from './utils.js'; export default class RealtimeFaultProvider { #openmct; - constructor(openmct, faultModelConverter, instance) { + constructor(openmct, instance) { this.#openmct = openmct; - this.faultModelConverter = faultModelConverter; this.instance = instance; this.lastSubscriptionId = 1; @@ -30,7 +30,7 @@ export default class RealtimeFaultProvider { } supportsSubscribe(domainObject) { - return domainObject.type === FAULT_MANAGEMENT_TYPE; + return domainObject.type === FAULT_MGMT_TYPE; } subscribe(domainObject, callback) { @@ -53,8 +53,6 @@ export default class RealtimeFaultProvider { } handleResponse(type, response, callback) { - const faultData = this.faultModelConverter(response, type); - - callback(faultData); + callback(convertDataToFaultModel(response, type)); } } diff --git a/src/providers/fault-mgmt-providers/utils.js b/src/providers/fault-mgmt-providers/utils.js index b5319483..51c25f16 100644 --- a/src/providers/fault-mgmt-providers/utils.js +++ b/src/providers/fault-mgmt-providers/utils.js @@ -1,3 +1,4 @@ +/* eslint-disable func-style */ /***************************************************************************** * Open MCT, Copyright (c) 2014-2021, United States Government * as represented by the Administrator of the National Aeronautics and Space @@ -19,15 +20,22 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ - import { getValue } from '../../utils.js'; -function faultModelConvertor(faultData, type) { +/** + * Converts fault data to a FaultModel. + * + * @param {Object} faultData + * @param {string} [type] + * @returns {FaultModel} + */ +const convertDataToFaultModel = (faultData, type) => { + const parameterDetail = faultData?.parameterDetail; + const currentValueDetail = parameterDetail?.currentValue; + const triggerValueDetail = parameterDetail?.triggerValue; - const currentValue = faultData?.parameterDetail?.currentValue - && getValue(faultData.parameterDetail.currentValue); - const triggerValue = faultData?.parameterDetail?.triggerValue - && getValue(faultData.parameterDetail.triggerValue); + const currentValue = currentValueDetail ? getValue(currentValueDetail) : undefined; + const triggerValue = triggerValueDetail ? getValue(triggerValueDetail) : undefined; return { type: type || faultData?.type, @@ -35,8 +43,8 @@ function faultModelConvertor(faultData, type) { acknowledged: Boolean(faultData?.acknowledged), currentValueInfo: { value: currentValue, - rangeCondition: faultData?.parameterDetail?.currentValue?.rangeCondition, - monitoringResult: faultData?.parameterDetail?.currentValue?.monitoringResult + rangeCondition: currentValueDetail?.rangeCondition, + monitoringResult: currentValueDetail?.monitoringResult }, id: `id-${faultData?.id?.namespace}-${faultData?.id?.name}`, name: faultData?.id?.name, @@ -44,17 +52,38 @@ function faultModelConvertor(faultData, type) { seqNum: faultData?.seqNum, severity: faultData?.severity, shelved: Boolean(faultData?.shelveInfo), - shortDescription: faultData?.parameterDetail?.parameter?.shortDescription, + shortDescription: parameterDetail?.parameter?.shortDescription, triggerTime: faultData?.triggerTime, triggerValueInfo: { value: triggerValue, - rangeCondition: faultData?.parameterDetail?.triggerValue?.rangeCondition, - monitoringResult: faultData?.parameterDetail?.triggerValue?.monitoringResult + rangeCondition: triggerValueDetail?.rangeCondition, + monitoringResult: triggerValueDetail?.monitoringResult } } }; -} - -export { - faultModelConvertor }; + +export { convertDataToFaultModel }; + +/** + * @typedef {Object} FaultModel + * @property {string} type + * @property {Object} fault + * @property {boolean} fault.acknowledged + * @property {Object} fault.currentValueInfo + * @property {*} fault.currentValueInfo.value + * @property {string} fault.currentValueInfo.rangeCondition + * @property {string} fault.currentValueInfo.monitoringResult + * @property {string} fault.id + * @property {string} fault.name + * @property {string} fault.namespace + * @property {number} fault.seqNum + * @property {string} fault.severity + * @property {boolean} fault.shelved + * @property {string} fault.shortDescription + * @property {number} fault.triggerTime + * @property {Object} fault.triggerValueInfo + * @property {*} fault.triggerValueInfo.value + * @property {string} fault.triggerValueInfo.rangeCondition + * @property {string} fault.triggerValueInfo.monitoringResult + */ diff --git a/src/providers/fault-mgmt-providers/yamcs-fault-provider.js b/src/providers/fault-mgmt-providers/yamcs-fault-provider.js index 81cb7cdc..71a545ae 100644 --- a/src/providers/fault-mgmt-providers/yamcs-fault-provider.js +++ b/src/providers/fault-mgmt-providers/yamcs-fault-provider.js @@ -2,10 +2,11 @@ import HistoricalFaultProvider from './historical-fault-provider.js'; import RealtimeFaultProvider from './realtime-fault-provider.js'; import FaultActionProvider from './fault-action-provider.js'; +const DEFAULT_PROCESSOR = 'realtime'; + export default class YamcsFaultProvider { - constructor(openmct, { faultModelConvertor, historicalEndpoint, yamcsInstance, yamcsProcessor } = {}) { + constructor(openmct, { historicalEndpoint, yamcsInstance, yamcsProcessor = DEFAULT_PROCESSOR } = {}) { this.historicalFaultProvider = new HistoricalFaultProvider( - faultModelConvertor, historicalEndpoint, yamcsInstance, yamcsProcessor @@ -13,7 +14,6 @@ export default class YamcsFaultProvider { this.realtimeFaultProvider = new RealtimeFaultProvider( openmct, - faultModelConvertor, yamcsInstance ); @@ -29,5 +29,6 @@ export default class YamcsFaultProvider { this.supportsSubscribe = this.realtimeFaultProvider.supportsSubscribe.bind(this.realtimeFaultProvider); this.acknowledgeFault = this.faultActionProvider.acknowledgeFault.bind(this.faultActionProvider); this.shelveFault = this.faultActionProvider.shelveFault.bind(this.faultActionProvider); + this.getShelveDurations = this.faultActionProvider.getShelveDurations.bind(this.faultActionProvider); } } diff --git a/tests/e2e/yamcs/faultManagement.e2e.spec.mjs b/tests/e2e/yamcs/faultManagement.e2e.spec.mjs index d3713788..6082427a 100644 --- a/tests/e2e/yamcs/faultManagement.e2e.spec.mjs +++ b/tests/e2e/yamcs/faultManagement.e2e.spec.mjs @@ -25,19 +25,265 @@ Staleness Specific Tests */ import { pluginFixtures } from 'openmct-e2e'; -const { test } = pluginFixtures; +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')); -test.describe.fixme("Fault management tests @yamcs", () => { - // eslint-disable-next-line require-await - test('Show faults ', async ({ page }) => { - test.step('for historic alarm violations', () => { - // Navigate to fault management in the tree - // Expect that there is indication of a fault }); - test.step('show historic and live faults when new alarms are triggered in real time', () => { - // Wait for new data - // Expect that live faults are displayed + 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() + } + }); +}