mirror of
https://github.com/nasa/openmct.git
synced 2025-02-04 18:20:43 +00:00
Merge remote-tracking branch 'origin/telemetry-comps' into combined-rodap-stuff
This commit is contained in:
commit
2a068dea0e
@ -37,6 +37,29 @@ commands:
|
|||||||
ls -latR >> /tmp/artifacts/dir.txt
|
ls -latR >> /tmp/artifacts/dir.txt
|
||||||
- store_artifacts:
|
- store_artifacts:
|
||||||
path: /tmp/artifacts/
|
path: /tmp/artifacts/
|
||||||
|
download_verify_codecov_cli:
|
||||||
|
description: 'Download and verify Codecov CLI'
|
||||||
|
steps:
|
||||||
|
- run:
|
||||||
|
name: Download and verify Codecov CLI
|
||||||
|
command: |
|
||||||
|
# Download Codecov CLI
|
||||||
|
curl -Os https://cli.codecov.io/latest/linux/codecov
|
||||||
|
|
||||||
|
# Import Codecov's GPG key
|
||||||
|
curl https://keybase.io/codecovsecurity/pgp_keys.asc | gpg --no-default-keyring --keyring trustedkeys.gpg --import
|
||||||
|
|
||||||
|
# Download and verify the SHA256SUM and its signature
|
||||||
|
curl -Os https://cli.codecov.io/latest/linux/codecov.SHA256SUM
|
||||||
|
curl -Os https://cli.codecov.io/latest/linux/codecov.SHA256SUM.sig
|
||||||
|
gpgv codecov.SHA256SUM.sig codecov.SHA256SUM
|
||||||
|
|
||||||
|
# Verify the checksum
|
||||||
|
shasum -a 256 -c codecov.SHA256SUM
|
||||||
|
|
||||||
|
# Make the codecov executable
|
||||||
|
[[ $EUID -ne 0 ]] && sudo chmod +x codecov || chmod +x codecov
|
||||||
|
./codecov --help
|
||||||
generate_e2e_code_cov_report:
|
generate_e2e_code_cov_report:
|
||||||
description: "Generate e2e code coverage artifacts and publish to codecov.io. Needed to that we can ignore the exit code status of the npm run test"
|
description: "Generate e2e code coverage artifacts and publish to codecov.io. Needed to that we can ignore the exit code status of the npm run test"
|
||||||
parameters:
|
parameters:
|
||||||
@ -44,7 +67,15 @@ commands:
|
|||||||
type: string
|
type: string
|
||||||
steps:
|
steps:
|
||||||
- run: npm run cov:e2e:report || true
|
- run: npm run cov:e2e:report || true
|
||||||
- run: npm run cov:e2e:<<parameters.suite>>:publish
|
- download_verify_codecov_cli
|
||||||
|
- run:
|
||||||
|
name: Upload coverage report to Codecov
|
||||||
|
command: |
|
||||||
|
./codecov --verbose upload-process --disable-search \
|
||||||
|
-t $CODECOV_TOKEN \
|
||||||
|
-n 'e2e-<<parameters.suite>>'-${CIRCLE_WORKFLOW_ID} \
|
||||||
|
-F e2e-<<parameters.suite>> \
|
||||||
|
-f ./coverage/e2e/lcov.info
|
||||||
jobs:
|
jobs:
|
||||||
npm-audit:
|
npm-audit:
|
||||||
parameters:
|
parameters:
|
||||||
@ -81,7 +112,15 @@ jobs:
|
|||||||
mkdir -p dist/reports/tests/
|
mkdir -p dist/reports/tests/
|
||||||
TESTFILES=$(circleci tests glob "src/**/*Spec.js")
|
TESTFILES=$(circleci tests glob "src/**/*Spec.js")
|
||||||
echo "$TESTFILES" | circleci tests run --command="xargs npm run test" --verbose
|
echo "$TESTFILES" | circleci tests run --command="xargs npm run test" --verbose
|
||||||
- run: npm run cov:unit:publish
|
- download_verify_codecov_cli
|
||||||
|
- run:
|
||||||
|
name: Upload coverage report to Codecov
|
||||||
|
command: |
|
||||||
|
./codecov --verbose upload-process --disable-search \
|
||||||
|
-t $CODECOV_TOKEN \
|
||||||
|
-n 'unit-test'-${CIRCLE_WORKFLOW_ID} \
|
||||||
|
-F unit \
|
||||||
|
-f ./coverage/unit/lcov.info
|
||||||
- store_test_results:
|
- store_test_results:
|
||||||
path: dist/reports/tests/
|
path: dist/reports/tests/
|
||||||
- store_artifacts:
|
- store_artifacts:
|
||||||
|
13
.github/workflows/e2e-couchdb.yml
vendored
13
.github/workflows/e2e-couchdb.yml
vendored
@ -51,11 +51,18 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
COMMIT_INFO_SHA: ${{github.event.pull_request.head.sha }}
|
COMMIT_INFO_SHA: ${{github.event.pull_request.head.sha }}
|
||||||
run: npm run test:e2e:couchdb
|
run: npm run test:e2e:couchdb
|
||||||
|
|
||||||
|
- name: Generate Code Coverage Report
|
||||||
|
run: npm run cov:e2e:report
|
||||||
|
|
||||||
- name: Publish Results to Codecov.io
|
- name: Publish Results to Codecov.io
|
||||||
env:
|
uses: codecov/codecov-action@v4
|
||||||
SUPER_SECRET: ${{ secrets.CODECOV_TOKEN }}
|
with:
|
||||||
run: npm run cov:e2e:full:publish
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
files: ./coverage/e2e/lcov.info
|
||||||
|
flags: e2e-full
|
||||||
|
fail_ci_if_error: true
|
||||||
|
verbose: true
|
||||||
|
|
||||||
- name: Archive test results
|
- name: Archive test results
|
||||||
if: success() || failure()
|
if: success() || failure()
|
||||||
|
@ -27,6 +27,12 @@ module.exports = {
|
|||||||
rules: {
|
rules: {
|
||||||
'playwright/no-raw-locators': 'off'
|
'playwright/no-raw-locators': 'off'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.visual.spec.js'],
|
||||||
|
rules: {
|
||||||
|
'playwright/no-networkidle': 'off' //https://github.com/nasa/openmct/issues/7549
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
@ -469,6 +469,7 @@ By adhering to this principle, we can create tests that are both robust and refl
|
|||||||
//Select from object
|
//Select from object
|
||||||
await percySnapshot(page, `object selected (theme: ${theme})`)
|
await percySnapshot(page, `object selected (theme: ${theme})`)
|
||||||
```
|
```
|
||||||
|
8. **Use `networkidle` to wait for network requests to complete**: This is necessary to ensure that all network requests have completed before taking a snapshot. This ensures that icons are loaded and other assets are available. https://github.com/nasa/openmct/issues/7549
|
||||||
|
|
||||||
#### How to write a great network test
|
#### How to write a great network test
|
||||||
|
|
||||||
|
@ -25,6 +25,7 @@ import { expect } from '../pluginFixtures.js';
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('@playwright/test').Page} page
|
* @param {import('@playwright/test').Page} page
|
||||||
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
export async function navigateToFaultManagementWithExample(page) {
|
export async function navigateToFaultManagementWithExample(page) {
|
||||||
await page.addInitScript({
|
await page.addInitScript({
|
||||||
@ -36,6 +37,7 @@ export async function navigateToFaultManagementWithExample(page) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('@playwright/test').Page} page
|
* @param {import('@playwright/test').Page} page
|
||||||
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
export async function navigateToFaultManagementWithStaticExample(page) {
|
export async function navigateToFaultManagementWithStaticExample(page) {
|
||||||
await page.addInitScript({
|
await page.addInitScript({
|
||||||
@ -47,6 +49,7 @@ export async function navigateToFaultManagementWithStaticExample(page) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('@playwright/test').Page} page
|
* @param {import('@playwright/test').Page} page
|
||||||
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
export async function navigateToFaultManagementWithoutExample(page) {
|
export async function navigateToFaultManagementWithoutExample(page) {
|
||||||
await page.addInitScript({
|
await page.addInitScript({
|
||||||
@ -58,6 +61,7 @@ export async function navigateToFaultManagementWithoutExample(page) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('@playwright/test').Page} page
|
* @param {import('@playwright/test').Page} page
|
||||||
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async function navigateToFaultItemInTree(page) {
|
async function navigateToFaultItemInTree(page) {
|
||||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||||
@ -77,6 +81,8 @@ async function navigateToFaultItemInTree(page) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('@playwright/test').Page} page
|
* @param {import('@playwright/test').Page} page
|
||||||
|
* @param {number} rowNumber
|
||||||
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
export async function acknowledgeFault(page, rowNumber) {
|
export async function acknowledgeFault(page, rowNumber) {
|
||||||
await openFaultRowMenu(page, rowNumber);
|
await openFaultRowMenu(page, rowNumber);
|
||||||
@ -86,6 +92,8 @@ export async function acknowledgeFault(page, rowNumber) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('@playwright/test').Page} page
|
* @param {import('@playwright/test').Page} page
|
||||||
|
* @param {...number} nums
|
||||||
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
export async function shelveMultipleFaults(page, ...nums) {
|
export async function shelveMultipleFaults(page, ...nums) {
|
||||||
const selectRows = nums.map((num) => {
|
const selectRows = nums.map((num) => {
|
||||||
@ -99,6 +107,8 @@ export async function shelveMultipleFaults(page, ...nums) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('@playwright/test').Page} page
|
* @param {import('@playwright/test').Page} page
|
||||||
|
* @param {...number} nums
|
||||||
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
export async function acknowledgeMultipleFaults(page, ...nums) {
|
export async function acknowledgeMultipleFaults(page, ...nums) {
|
||||||
const selectRows = nums.map((num) => {
|
const selectRows = nums.map((num) => {
|
||||||
@ -106,50 +116,43 @@ export async function acknowledgeMultipleFaults(page, ...nums) {
|
|||||||
});
|
});
|
||||||
await Promise.all(selectRows);
|
await Promise.all(selectRows);
|
||||||
|
|
||||||
await page.locator('button:has-text("Acknowledge")').click();
|
await page.getByLabel('Acknowledge selected faults').click();
|
||||||
await page.getByLabel('Save').click();
|
await page.getByLabel('Save').click();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('@playwright/test').Page} page
|
* @param {import('@playwright/test').Page} page
|
||||||
|
* @param {number} rowNumber
|
||||||
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
export async function shelveFault(page, rowNumber) {
|
export async function shelveFault(page, rowNumber) {
|
||||||
await openFaultRowMenu(page, rowNumber);
|
await openFaultRowMenu(page, rowNumber);
|
||||||
await page.locator('.c-menu >> text="Shelve"').click();
|
await page.getByLabel('Shelve', { exact: true }).click();
|
||||||
// Click [aria-label="Save"]
|
|
||||||
await page.getByLabel('Save').click();
|
await page.getByLabel('Save').click();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('@playwright/test').Page} page
|
* @param {import('@playwright/test').Page} page
|
||||||
*/
|
* @param {'severity' | 'newest-first' | 'oldest-first'} sort
|
||||||
export async function changeViewTo(page, view) {
|
* @returns {Promise<void>}
|
||||||
await page.locator('.c-fault-mgmt__search-row select').first().selectOption(view);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {import('@playwright/test').Page} page
|
|
||||||
*/
|
*/
|
||||||
export async function sortFaultsBy(page, sort) {
|
export async function sortFaultsBy(page, sort) {
|
||||||
await page.locator('.c-fault-mgmt__list-header-sortButton select').selectOption(sort);
|
await page.getByTitle('Sort By').getByRole('combobox').selectOption(sort);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('@playwright/test').Page} page
|
* @param {import('@playwright/test').Page} page
|
||||||
|
* @param {'acknowledged' | 'shelved' | 'standard view'} view
|
||||||
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
export async function enterSearchTerm(page, term) {
|
export async function changeViewTo(page, view) {
|
||||||
await page.locator('.c-fault-mgmt-search [aria-label="Search Input"]').fill(term);
|
await page.getByTitle('View Filter').getByRole('combobox').selectOption(view);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {import('@playwright/test').Page} page
|
|
||||||
*/
|
|
||||||
export async function clearSearch(page) {
|
|
||||||
await enterSearchTerm(page, '');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('@playwright/test').Page} page
|
* @param {import('@playwright/test').Page} page
|
||||||
|
* @param {number} rowNumber
|
||||||
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
export async function selectFaultItem(page, rowNumber) {
|
export async function selectFaultItem(page, rowNumber) {
|
||||||
await page
|
await page
|
||||||
@ -165,71 +168,37 @@ export async function selectFaultItem(page, rowNumber) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('@playwright/test').Page} page
|
* @param {import('@playwright/test').Page} page
|
||||||
*/
|
* @param {number} rowNumber
|
||||||
export async function getHighestSeverity(page) {
|
* @returns {import('@playwright/test').Locator}
|
||||||
const criticalCount = await page.locator('[title=CRITICAL]').count();
|
|
||||||
const warningCount = await page.locator('[title=WARNING]').count();
|
|
||||||
|
|
||||||
if (criticalCount > 0) {
|
|
||||||
return 'CRITICAL';
|
|
||||||
} else if (warningCount > 0) {
|
|
||||||
return 'WARNING';
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'WATCH';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {import('@playwright/test').Page} page
|
|
||||||
*/
|
|
||||||
export async function getLowestSeverity(page) {
|
|
||||||
const warningCount = await page.locator('[title=WARNING]').count();
|
|
||||||
const watchCount = await page.locator('[title=WATCH]').count();
|
|
||||||
|
|
||||||
if (watchCount > 0) {
|
|
||||||
return 'WATCH';
|
|
||||||
} else if (warningCount > 0) {
|
|
||||||
return 'WARNING';
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'CRITICAL';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {import('@playwright/test').Page} page
|
|
||||||
*/
|
|
||||||
export async function getFaultResultCount(page) {
|
|
||||||
const count = await page.locator('.c-faults-list-view-item-body > .c-fault-mgmt__list').count();
|
|
||||||
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {import('@playwright/test').Page} page
|
|
||||||
*/
|
*/
|
||||||
export function getFault(page, rowNumber) {
|
export function getFault(page, rowNumber) {
|
||||||
const fault = page.locator(
|
const fault = page.getByLabel('Fault triggered at').nth(rowNumber - 1);
|
||||||
`.c-faults-list-view-item-body > .c-fault-mgmt__list >> nth=${rowNumber - 1}`
|
|
||||||
);
|
|
||||||
|
|
||||||
return fault;
|
return fault;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('@playwright/test').Page} page
|
* @param {import('@playwright/test').Page} page
|
||||||
|
* @param {string} name
|
||||||
|
* @returns {import('@playwright/test').Locator}
|
||||||
*/
|
*/
|
||||||
export function getFaultByName(page, name) {
|
export function getFaultByName(page, name) {
|
||||||
const fault = page.locator(`.c-fault-mgmt__list-faultname:has-text("${name}")`);
|
const fault = page.getByLabel('Fault triggered at').filter({
|
||||||
|
hasText: name
|
||||||
|
});
|
||||||
|
|
||||||
return fault;
|
return fault;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('@playwright/test').Page} page
|
* @param {import('@playwright/test').Page} page
|
||||||
|
* @param {number} rowNumber
|
||||||
|
* @returns {Promise<string>}
|
||||||
*/
|
*/
|
||||||
export async function getFaultName(page, rowNumber) {
|
export async function getFaultName(page, rowNumber) {
|
||||||
const faultName = await page
|
const faultName = await page
|
||||||
.locator(`.c-fault-mgmt__list-faultname >> nth=${rowNumber - 1}`)
|
.getByLabel('Fault name', { exact: true })
|
||||||
|
.nth(rowNumber - 1)
|
||||||
.textContent();
|
.textContent();
|
||||||
|
|
||||||
return faultName;
|
return faultName;
|
||||||
@ -237,21 +206,13 @@ export async function getFaultName(page, rowNumber) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('@playwright/test').Page} page
|
* @param {import('@playwright/test').Page} page
|
||||||
*/
|
* @param {number} rowNumber
|
||||||
export async function getFaultSeverity(page, rowNumber) {
|
* @returns {Promise<string>}
|
||||||
const faultSeverity = await page
|
|
||||||
.locator(`.c-faults-list-view-item-body .c-fault-mgmt__list-severity >> nth=${rowNumber - 1}`)
|
|
||||||
.getAttribute('title');
|
|
||||||
|
|
||||||
return faultSeverity;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {import('@playwright/test').Page} page
|
|
||||||
*/
|
*/
|
||||||
export async function getFaultNamespace(page, rowNumber) {
|
export async function getFaultNamespace(page, rowNumber) {
|
||||||
const faultNamespace = await page
|
const faultNamespace = await page
|
||||||
.locator(`.c-fault-mgmt__list-path >> nth=${rowNumber - 1}`)
|
.getByLabel('Fault namespace')
|
||||||
|
.nth(rowNumber - 1)
|
||||||
.textContent();
|
.textContent();
|
||||||
|
|
||||||
return faultNamespace;
|
return faultNamespace;
|
||||||
@ -259,10 +220,13 @@ export async function getFaultNamespace(page, rowNumber) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('@playwright/test').Page} page
|
* @param {import('@playwright/test').Page} page
|
||||||
|
* @param {number} rowNumber
|
||||||
|
* @returns {Promise<string>}
|
||||||
*/
|
*/
|
||||||
export async function getFaultTriggerTime(page, rowNumber) {
|
export async function getFaultTriggerTime(page, rowNumber) {
|
||||||
const faultTriggerTime = await page
|
const faultTriggerTime = await page
|
||||||
.locator(`.c-fault-mgmt__list-trigTime >> nth=${rowNumber - 1} >> .c-fault-mgmt-item__value`)
|
.getByLabel('Last Trigger Time')
|
||||||
|
.nth(rowNumber - 1)
|
||||||
.textContent();
|
.textContent();
|
||||||
|
|
||||||
return faultTriggerTime.toString().trim();
|
return faultTriggerTime.toString().trim();
|
||||||
@ -270,11 +234,14 @@ export async function getFaultTriggerTime(page, rowNumber) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('@playwright/test').Page} page
|
* @param {import('@playwright/test').Page} page
|
||||||
|
* @param {number} rowNumber
|
||||||
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
export async function openFaultRowMenu(page, rowNumber) {
|
export async function openFaultRowMenu(page, rowNumber) {
|
||||||
// select
|
// select
|
||||||
await page
|
await page
|
||||||
.getByLabel('Disposition actions')
|
.getByLabel('Fault triggered at')
|
||||||
.nth(rowNumber - 1)
|
.nth(rowNumber - 1)
|
||||||
|
.getByLabel('Disposition Actions')
|
||||||
.click();
|
.click();
|
||||||
}
|
}
|
||||||
|
33
e2e/helper/imageryUtils.js
Normal file
33
e2e/helper/imageryUtils.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { createDomainObjectWithDefaults } from '../appActions.js';
|
||||||
|
import { expect } from '../pluginFixtures.js';
|
||||||
|
|
||||||
|
const IMAGE_LOAD_DELAY = 5 * 1000;
|
||||||
|
const FIVE_MINUTES = 1000 * 60 * 5;
|
||||||
|
const THIRTY_SECONDS = 1000 * 30;
|
||||||
|
const MOUSE_WHEEL_DELTA_Y = 120;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
*/
|
||||||
|
async function createImageryViewWithShortDelay(page, { name, parent }) {
|
||||||
|
await createDomainObjectWithDefaults(page, {
|
||||||
|
name,
|
||||||
|
type: 'Example Imagery',
|
||||||
|
parent
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery');
|
||||||
|
await page.getByLabel('More actions').click();
|
||||||
|
await page.getByLabel('Edit Properties').click();
|
||||||
|
// Clear and set Image load delay to minimum value
|
||||||
|
await page.locator('input[type="number"]').fill(`${IMAGE_LOAD_DELAY}`);
|
||||||
|
await page.getByLabel('Save').click();
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
createImageryViewWithShortDelay,
|
||||||
|
FIVE_MINUTES,
|
||||||
|
IMAGE_LOAD_DELAY,
|
||||||
|
MOUSE_WHEEL_DELTA_Y,
|
||||||
|
THIRTY_SECONDS
|
||||||
|
};
|
@ -24,19 +24,13 @@ import {
|
|||||||
acknowledgeFault,
|
acknowledgeFault,
|
||||||
acknowledgeMultipleFaults,
|
acknowledgeMultipleFaults,
|
||||||
changeViewTo,
|
changeViewTo,
|
||||||
clearSearch,
|
|
||||||
enterSearchTerm,
|
|
||||||
getFault,
|
getFault,
|
||||||
getFaultByName,
|
getFaultByName,
|
||||||
getFaultName,
|
getFaultName,
|
||||||
getFaultNamespace,
|
getFaultNamespace,
|
||||||
getFaultResultCount,
|
|
||||||
getFaultSeverity,
|
|
||||||
getFaultTriggerTime,
|
getFaultTriggerTime,
|
||||||
getHighestSeverity,
|
|
||||||
getLowestSeverity,
|
|
||||||
navigateToFaultManagementWithExample,
|
|
||||||
navigateToFaultManagementWithoutExample,
|
navigateToFaultManagementWithoutExample,
|
||||||
|
navigateToFaultManagementWithStaticExample,
|
||||||
selectFaultItem,
|
selectFaultItem,
|
||||||
shelveFault,
|
shelveFault,
|
||||||
shelveMultipleFaults,
|
shelveMultipleFaults,
|
||||||
@ -46,7 +40,7 @@ import { expect, test } from '../../../../pluginFixtures.js';
|
|||||||
|
|
||||||
test.describe('The Fault Management Plugin using example faults', () => {
|
test.describe('The Fault Management Plugin using example faults', () => {
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await navigateToFaultManagementWithExample(page);
|
await navigateToFaultManagementWithStaticExample(page);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Shows a criticality icon for every fault', async ({ page }) => {
|
test('Shows a criticality icon for every fault', async ({ page }) => {
|
||||||
@ -56,7 +50,7 @@ test.describe('The Fault Management Plugin using example faults', () => {
|
|||||||
expect(faultCount).toEqual(criticalityIconCount);
|
expect(faultCount).toEqual(criticalityIconCount);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('When selecting a fault, it has an "is-selected" class and it\'s information shows in the inspector', async ({
|
test('When selecting a fault, it has an "is-selected" class and its information shows in the inspector', async ({
|
||||||
page
|
page
|
||||||
}) => {
|
}) => {
|
||||||
await selectFaultItem(page, 1);
|
await selectFaultItem(page, 1);
|
||||||
@ -67,9 +61,7 @@ test.describe('The Fault Management Plugin using example faults', () => {
|
|||||||
.getByLabel('Source inspector properties')
|
.getByLabel('Source inspector properties')
|
||||||
.getByLabel('inspector property value');
|
.getByLabel('inspector property value');
|
||||||
|
|
||||||
await expect(
|
await expect(page.getByLabel('Fault triggered at').first()).toHaveClass(/is-selected/);
|
||||||
page.locator('.c-faults-list-view-item-body > .c-fault-mgmt__list').first()
|
|
||||||
).toHaveClass(/is-selected/);
|
|
||||||
await expect(inspectorFaultName).toHaveCount(1);
|
await expect(inspectorFaultName).toHaveCount(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -79,23 +71,18 @@ test.describe('The Fault Management Plugin using example faults', () => {
|
|||||||
await selectFaultItem(page, 1);
|
await selectFaultItem(page, 1);
|
||||||
await selectFaultItem(page, 2);
|
await selectFaultItem(page, 2);
|
||||||
|
|
||||||
const selectedRows = page.locator(
|
const selectedRows = page.getByRole('checkbox', { checked: true });
|
||||||
'.c-fault-mgmt__list.is-selected .c-fault-mgmt__list-faultname'
|
await expect(selectedRows).toHaveCount(2);
|
||||||
);
|
|
||||||
expect(await selectedRows.count()).toEqual(2);
|
|
||||||
|
|
||||||
await page.getByRole('tab', { name: 'Config' }).click();
|
await page.getByRole('tab', { name: 'Config' }).click();
|
||||||
const firstSelectedFaultName = await selectedRows.nth(0).textContent();
|
const firstSelectedFaultName = await selectedRows.nth(0).textContent();
|
||||||
const secondSelectedFaultName = await selectedRows.nth(1).textContent();
|
const secondSelectedFaultName = await selectedRows.nth(1).textContent();
|
||||||
const firstNameInInspectorCount = await page
|
await expect(
|
||||||
.locator(`.c-inspector__properties >> :text("${firstSelectedFaultName}")`)
|
page.locator(`.c-inspector__properties >> :text("${firstSelectedFaultName}")`)
|
||||||
.count();
|
).toHaveCount(0);
|
||||||
const secondNameInInspectorCount = await page
|
await expect(
|
||||||
.locator(`.c-inspector__properties >> :text("${secondSelectedFaultName}")`)
|
page.locator(`.c-inspector__properties >> :text("${secondSelectedFaultName}")`)
|
||||||
.count();
|
).toHaveCount(0);
|
||||||
|
|
||||||
expect(firstNameInInspectorCount).toEqual(0);
|
|
||||||
expect(secondNameInInspectorCount).toEqual(0);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Allows you to shelve a fault', async ({ page }) => {
|
test('Allows you to shelve a fault', async ({ page }) => {
|
||||||
@ -186,44 +173,60 @@ test.describe('The Fault Management Plugin using example faults', () => {
|
|||||||
const faultFiveTriggerTime = await getFaultTriggerTime(page, 5);
|
const faultFiveTriggerTime = await getFaultTriggerTime(page, 5);
|
||||||
|
|
||||||
// should be all faults (5)
|
// should be all faults (5)
|
||||||
let faultResultCount = await getFaultResultCount(page);
|
await expect(page.getByLabel('Fault triggered at')).toHaveCount(5);
|
||||||
expect(faultResultCount).toEqual(5);
|
|
||||||
|
|
||||||
// search namespace
|
// search namespace
|
||||||
await enterSearchTerm(page, faultThreeNamespace);
|
await page
|
||||||
|
.getByLabel('Fault Management Object View')
|
||||||
|
.getByLabel('Search Input')
|
||||||
|
.fill(faultThreeNamespace);
|
||||||
|
|
||||||
faultResultCount = await getFaultResultCount(page);
|
await expect(page.getByLabel('Fault triggered at')).toHaveCount(1);
|
||||||
expect(faultResultCount).toEqual(1);
|
|
||||||
expect(await getFaultNamespace(page, 1)).toEqual(faultThreeNamespace);
|
expect(await getFaultNamespace(page, 1)).toEqual(faultThreeNamespace);
|
||||||
|
|
||||||
// all faults
|
// all faults
|
||||||
await clearSearch(page);
|
await page.getByLabel('Fault Management Object View').getByLabel('Search Input').fill('');
|
||||||
faultResultCount = await getFaultResultCount(page);
|
await expect(page.getByLabel('Fault triggered at')).toHaveCount(5);
|
||||||
expect(faultResultCount).toEqual(5);
|
|
||||||
|
|
||||||
// search name
|
// search name
|
||||||
await enterSearchTerm(page, faultTwoName);
|
await page
|
||||||
|
.getByLabel('Fault Management Object View')
|
||||||
|
.getByLabel('Search Input')
|
||||||
|
.fill(faultTwoName);
|
||||||
|
|
||||||
faultResultCount = await getFaultResultCount(page);
|
await expect(page.getByLabel('Fault triggered at')).toHaveCount(1);
|
||||||
expect(faultResultCount).toEqual(1);
|
|
||||||
expect(await getFaultName(page, 1)).toEqual(faultTwoName);
|
expect(await getFaultName(page, 1)).toEqual(faultTwoName);
|
||||||
|
|
||||||
// all faults
|
// all faults
|
||||||
await clearSearch(page);
|
await page.getByLabel('Fault Management Object View').getByLabel('Search Input').fill('');
|
||||||
faultResultCount = await getFaultResultCount(page);
|
await expect(page.getByLabel('Fault triggered at')).toHaveCount(5);
|
||||||
expect(faultResultCount).toEqual(5);
|
|
||||||
|
|
||||||
// search triggerTime
|
// search triggerTime
|
||||||
await enterSearchTerm(page, faultFiveTriggerTime);
|
await page
|
||||||
|
.getByLabel('Fault Management Object View')
|
||||||
|
.getByLabel('Search Input')
|
||||||
|
.fill(faultFiveTriggerTime);
|
||||||
|
|
||||||
faultResultCount = await getFaultResultCount(page);
|
await expect(page.getByLabel('Fault triggered at')).toHaveCount(1);
|
||||||
expect(faultResultCount).toEqual(1);
|
|
||||||
expect(await getFaultTriggerTime(page, 1)).toEqual(faultFiveTriggerTime);
|
expect(await getFaultTriggerTime(page, 1)).toEqual(faultFiveTriggerTime);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Allows you to sort faults', async ({ page }) => {
|
test('Allows you to sort faults', async ({ page }) => {
|
||||||
const highestSeverity = await getHighestSeverity(page);
|
/**
|
||||||
const lowestSeverity = await getLowestSeverity(page);
|
* Compares two severity levels and returns a number indicating their relative order.
|
||||||
|
*
|
||||||
|
* @param {'CRITICAL' | 'WARNING' | 'WATCH'} severity1 - The first severity level to compare.
|
||||||
|
* @param {'CRITICAL' | 'WARNING' | 'WATCH'} severity2 - The second severity level to compare.
|
||||||
|
* @returns {number} - A negative number if severity1 is less severe than severity2,
|
||||||
|
* a positive number if severity1 is more severe than severity2,
|
||||||
|
* or 0 if they are equally severe.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line func-style
|
||||||
|
const compareSeverity = (severity1, severity2) => {
|
||||||
|
const severityOrder = ['WATCH', 'WARNING', 'CRITICAL'];
|
||||||
|
return severityOrder.indexOf(severity1) - severityOrder.indexOf(severity2);
|
||||||
|
};
|
||||||
|
|
||||||
const faultOneName = 'Example Fault 1';
|
const faultOneName = 'Example Fault 1';
|
||||||
const faultFiveName = 'Example Fault 5';
|
const faultFiveName = 'Example Fault 5';
|
||||||
let firstFaultName = await getFaultName(page, 1);
|
let firstFaultName = await getFaultName(page, 1);
|
||||||
@ -237,10 +240,19 @@ test.describe('The Fault Management Plugin using example faults', () => {
|
|||||||
|
|
||||||
await sortFaultsBy(page, 'severity');
|
await sortFaultsBy(page, 'severity');
|
||||||
|
|
||||||
const sortedHighestSeverity = await getFaultSeverity(page, 1);
|
const firstFaultSeverityLabel = await page
|
||||||
const sortedLowestSeverity = await getFaultSeverity(page, 5);
|
.getByLabel('Severity:')
|
||||||
expect(sortedHighestSeverity).toEqual(highestSeverity);
|
.first()
|
||||||
expect(sortedLowestSeverity).toEqual(lowestSeverity);
|
.getAttribute('aria-label');
|
||||||
|
const firstFaultSeverity = firstFaultSeverityLabel.split(' ').slice(1).join(' ');
|
||||||
|
|
||||||
|
const lastFaultSeverityLabel = await page
|
||||||
|
.getByLabel('Severity:')
|
||||||
|
.last()
|
||||||
|
.getAttribute('aria-label');
|
||||||
|
const lastFaultSeverity = lastFaultSeverityLabel.split(' ').slice(1).join(' ');
|
||||||
|
|
||||||
|
expect(compareSeverity(firstFaultSeverity, lastFaultSeverity)).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -250,24 +262,18 @@ test.describe('The Fault Management Plugin without using example faults', () =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Shows no faults when no faults are provided', async ({ page }) => {
|
test('Shows no faults when no faults are provided', async ({ page }) => {
|
||||||
const faultCount = await page.locator('c-fault-mgmt__list').count();
|
await expect(page.getByLabel('Fault triggered at')).toHaveCount(0);
|
||||||
|
|
||||||
expect(faultCount).toEqual(0);
|
|
||||||
|
|
||||||
await changeViewTo(page, 'acknowledged');
|
await changeViewTo(page, 'acknowledged');
|
||||||
const acknowledgedCount = await page.locator('c-fault-mgmt__list').count();
|
await expect(page.getByLabel('Fault triggered at')).toHaveCount(0);
|
||||||
expect(acknowledgedCount).toEqual(0);
|
|
||||||
|
|
||||||
await changeViewTo(page, 'shelved');
|
await changeViewTo(page, 'shelved');
|
||||||
const shelvedCount = await page.locator('c-fault-mgmt__list').count();
|
await expect(page.getByLabel('Fault triggered at')).toHaveCount(0);
|
||||||
expect(shelvedCount).toEqual(0);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Will return no faults when searching', async ({ page }) => {
|
test('Will return no faults when searching', async ({ page }) => {
|
||||||
await enterSearchTerm(page, 'fault');
|
await page.getByLabel('Fault Management Object View').getByLabel('Search Input').fill('fault');
|
||||||
|
|
||||||
const faultCount = await page.locator('c-fault-mgmt__list').count();
|
await expect(page.getByLabel('Fault triggered at')).toHaveCount(0);
|
||||||
|
|
||||||
expect(faultCount).toEqual(0);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -30,16 +30,19 @@ import {
|
|||||||
navigateToObjectWithRealTime,
|
navigateToObjectWithRealTime,
|
||||||
setRealTimeMode
|
setRealTimeMode
|
||||||
} from '../../../../appActions.js';
|
} from '../../../../appActions.js';
|
||||||
import { MISSION_TIME } from '../../../../constants.js';
|
import {
|
||||||
|
createImageryViewWithShortDelay,
|
||||||
|
FIVE_MINUTES,
|
||||||
|
IMAGE_LOAD_DELAY,
|
||||||
|
MOUSE_WHEEL_DELTA_Y,
|
||||||
|
THIRTY_SECONDS
|
||||||
|
} from '../../../../helper/imageryUtils.js';
|
||||||
import { expect, test } from '../../../../pluginFixtures.js';
|
import { expect, test } from '../../../../pluginFixtures.js';
|
||||||
|
|
||||||
const panHotkey = process.platform === 'linux' ? ['Shift', 'Alt'] : ['Alt'];
|
const panHotkey = process.platform === 'linux' ? ['Shift', 'Alt'] : ['Alt'];
|
||||||
const tagHotkey = ['Shift', 'Alt'];
|
const tagHotkey = ['Shift', 'Alt'];
|
||||||
const expectedAltText = process.platform === 'linux' ? 'Shift+Alt drag to pan' : 'Alt drag to pan';
|
const expectedAltText = process.platform === 'linux' ? 'Shift+Alt drag to pan' : 'Alt drag to pan';
|
||||||
const thumbnailUrlParamsRegexp = /\?w=100&h=100/;
|
const thumbnailUrlParamsRegexp = /\?w=100&h=100/;
|
||||||
const IMAGE_LOAD_DELAY = 5 * 1000;
|
|
||||||
const MOUSE_WHEEL_DELTA_Y = 120;
|
|
||||||
const FIVE_MINUTES = 1000 * 60 * 5;
|
|
||||||
const THIRTY_SECONDS = 1000 * 30;
|
|
||||||
|
|
||||||
//The following block of tests verifies the basic functionality of example imagery and serves as a template for Imagery objects embedded in other objects.
|
//The following block of tests verifies the basic functionality of example imagery and serves as a template for Imagery objects embedded in other objects.
|
||||||
test.describe('Example Imagery Object', () => {
|
test.describe('Example Imagery Object', () => {
|
||||||
@ -357,15 +360,10 @@ test.describe('Example Imagery Object', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe('Example Imagery in Display Layout @clock', () => {
|
test.describe('Example Imagery in Display Layout', () => {
|
||||||
let displayLayout;
|
let displayLayout;
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
// We mock the clock so that we don't need to wait for time driven events
|
|
||||||
// to verify functionality.
|
|
||||||
await page.clock.install({ time: MISSION_TIME });
|
|
||||||
await page.clock.resume();
|
|
||||||
|
|
||||||
// Go to baseURL
|
// Go to baseURL
|
||||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||||
|
|
||||||
@ -428,12 +426,7 @@ test.describe('Example Imagery in Display Layout @clock', () => {
|
|||||||
await expect.soft(pausePlayButton).toHaveClass(/is-paused/);
|
await expect.soft(pausePlayButton).toHaveClass(/is-paused/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Imagery View operations @clock', async ({ page }) => {
|
test('Imagery View operations', async ({ page }) => {
|
||||||
test.info().annotations.push({
|
|
||||||
type: 'issue',
|
|
||||||
description: 'https://github.com/nasa/openmct/issues/5265'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Edit mode
|
// Edit mode
|
||||||
await page.getByLabel('Edit Object').click();
|
await page.getByLabel('Edit Object').click();
|
||||||
|
|
||||||
@ -526,14 +519,9 @@ test.describe('Example Imagery in Display Layout @clock', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe('Example Imagery in Flexible layout @clock', () => {
|
test.describe('Example Imagery in Flexible layout', () => {
|
||||||
let flexibleLayout;
|
let flexibleLayout;
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
// We mock the clock so that we don't need to wait for time driven events
|
|
||||||
// to verify functionality.
|
|
||||||
await page.clock.install({ time: MISSION_TIME });
|
|
||||||
await page.clock.resume();
|
|
||||||
|
|
||||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||||
|
|
||||||
flexibleLayout = await createDomainObjectWithDefaults(page, { type: 'Flexible Layout' });
|
flexibleLayout = await createDomainObjectWithDefaults(page, { type: 'Flexible Layout' });
|
||||||
@ -562,7 +550,7 @@ test.describe('Example Imagery in Flexible layout @clock', () => {
|
|||||||
await page.getByRole('button', { name: 'Close' }).click();
|
await page.getByRole('button', { name: 'Close' }).click();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Imagery View operations @clock', async ({ page, browserName }) => {
|
test('Imagery View operations', async ({ page, browserName }) => {
|
||||||
test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox');
|
test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox');
|
||||||
test.info().annotations.push({
|
test.info().annotations.push({
|
||||||
type: 'issue',
|
type: 'issue',
|
||||||
@ -573,14 +561,10 @@ test.describe('Example Imagery in Flexible layout @clock', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe('Example Imagery in Tabs View @clock', () => {
|
test.describe('Example Imagery in Tabs View', () => {
|
||||||
let tabsView;
|
let tabsView;
|
||||||
test.beforeEach(async ({ page }) => {
|
|
||||||
// We mock the clock so that we don't need to wait for time driven events
|
|
||||||
// to verify functionality.
|
|
||||||
await page.clock.install({ time: MISSION_TIME });
|
|
||||||
await page.clock.resume();
|
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||||
|
|
||||||
tabsView = await createDomainObjectWithDefaults(page, { type: 'Tabs View' });
|
tabsView = await createDomainObjectWithDefaults(page, { type: 'Tabs View' });
|
||||||
@ -607,7 +591,8 @@ test.describe('Example Imagery in Tabs View @clock', () => {
|
|||||||
// Wait for image thumbnail auto-scroll to complete
|
// Wait for image thumbnail auto-scroll to complete
|
||||||
await expect(page.getByLabel('Image Thumbnail from').last()).toBeInViewport();
|
await expect(page.getByLabel('Image Thumbnail from').last()).toBeInViewport();
|
||||||
});
|
});
|
||||||
test('Imagery View operations @clock', async ({ page }) => {
|
|
||||||
|
test('Imagery View operations', async ({ page }) => {
|
||||||
await performImageryViewOperationsAndAssert(page, tabsView);
|
await performImageryViewOperationsAndAssert(page, tabsView);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -668,16 +653,19 @@ test.describe('Example Imagery in Time Strip', () => {
|
|||||||
* 3. Can pan the image using the pan hotkey + mouse drag
|
* 3. Can pan the image using the pan hotkey + mouse drag
|
||||||
* 4. Clicking on the left arrow button pauses imagery and moves to the previous image
|
* 4. Clicking on the left arrow button pauses imagery and moves to the previous image
|
||||||
* 5. Imagery is updated as new images stream in, regardless of pause status
|
* 5. Imagery is updated as new images stream in, regardless of pause status
|
||||||
* 6. Old images are discarded when new images stream in
|
* 6. Old images are discarded when their timestamps fall out of bounds
|
||||||
* 7. Image brightness/contrast can be adjusted by dragging the sliders
|
* 7. Multiple images can be discarded when their timestamps fall out of bounds
|
||||||
|
* 8. Image brightness/contrast can be adjusted by dragging the sliders
|
||||||
* @param {import('@playwright/test').Page} page
|
* @param {import('@playwright/test').Page} page
|
||||||
*/
|
*/
|
||||||
async function performImageryViewOperationsAndAssert(page, layoutObject) {
|
async function performImageryViewOperationsAndAssert(page, layoutObject) {
|
||||||
// Verify that imagery thumbnails use a thumbnail url
|
await test.step('Verify that imagery thumbnails use a thumbnail url', async () => {
|
||||||
const thumbnailImages = page.getByLabel('Image thumbnail from').locator('.c-thumb__image');
|
const thumbnailImages = page.getByLabel('Image thumbnail from').locator('.c-thumb__image');
|
||||||
const mainImage = page.locator('.c-imagery__main-image__image');
|
const mainImage = page.locator('.c-imagery__main-image__image');
|
||||||
await expect(thumbnailImages.first()).toHaveAttribute('src', thumbnailUrlParamsRegexp);
|
await expect(thumbnailImages.first()).toHaveAttribute('src', thumbnailUrlParamsRegexp);
|
||||||
await expect(mainImage).not.toHaveAttribute('src', thumbnailUrlParamsRegexp);
|
await expect(mainImage).not.toHaveAttribute('src', thumbnailUrlParamsRegexp);
|
||||||
|
});
|
||||||
|
|
||||||
// Click previous image button
|
// Click previous image button
|
||||||
const previousImageButton = page.getByLabel('Previous image');
|
const previousImageButton = page.getByLabel('Previous image');
|
||||||
await expect(previousImageButton).toBeVisible();
|
await expect(previousImageButton).toBeVisible();
|
||||||
@ -736,19 +724,6 @@ async function performImageryViewOperationsAndAssert(page, layoutObject) {
|
|||||||
// Unpause imagery
|
// Unpause imagery
|
||||||
await page.locator('.pause-play').click();
|
await page.locator('.pause-play').click();
|
||||||
|
|
||||||
// verify that old images are discarded
|
|
||||||
const lastImageInBounds = page.getByLabel('Image thumbnail from').first();
|
|
||||||
const lastImageTimestamp = await lastImageInBounds.getAttribute('title');
|
|
||||||
expect(lastImageTimestamp).not.toBeNull();
|
|
||||||
|
|
||||||
// go forward in time to ensure old images are discarded
|
|
||||||
await page.clock.fastForward(IMAGE_LOAD_DELAY);
|
|
||||||
await page.clock.resume();
|
|
||||||
await expect(page.getByLabel(lastImageTimestamp)).toBeHidden();
|
|
||||||
|
|
||||||
//Get background-image url from background-image css prop
|
|
||||||
await assertBackgroundImageUrlFromBackgroundCss(page);
|
|
||||||
|
|
||||||
// Open the image filter menu
|
// Open the image filter menu
|
||||||
await page.locator('[role=toolbar] button[title="Brightness and contrast"]').click();
|
await page.locator('[role=toolbar] button[title="Brightness and contrast"]').click();
|
||||||
|
|
||||||
@ -815,24 +790,6 @@ async function assertBackgroundImageBrightness(page, expected) {
|
|||||||
expect(actual).toBe(expected);
|
expect(actual).toBe(expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {import('@playwright/test').Page} page
|
|
||||||
*/
|
|
||||||
async function assertBackgroundImageUrlFromBackgroundCss(page) {
|
|
||||||
const backgroundImage = page.getByLabel('Focused Image Element');
|
|
||||||
const backgroundImageUrl = await backgroundImage.evaluate((el) => {
|
|
||||||
return window
|
|
||||||
.getComputedStyle(el)
|
|
||||||
.getPropertyValue('background-image')
|
|
||||||
.match(/url\(([^)]+)\)/)[1];
|
|
||||||
});
|
|
||||||
|
|
||||||
// go forward in time to ensure old images are discarded
|
|
||||||
await page.clock.fastForward(IMAGE_LOAD_DELAY);
|
|
||||||
await page.clock.resume();
|
|
||||||
await expect(backgroundImage).not.toHaveJSProperty('background-image', backgroundImageUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('@playwright/test').Page} page
|
* @param {import('@playwright/test').Page} page
|
||||||
*/
|
*/
|
||||||
@ -918,62 +875,66 @@ async function mouseZoomOnImageAndAssert(page, factor = 2) {
|
|||||||
* @param {import('@playwright/test').Page} page
|
* @param {import('@playwright/test').Page} page
|
||||||
*/
|
*/
|
||||||
async function buttonZoomOnImageAndAssert(page) {
|
async function buttonZoomOnImageAndAssert(page) {
|
||||||
// Lock the zoom and pan so it doesn't reset if a new image comes in
|
await test.step('Can zoom using buttons', async () => {
|
||||||
await page.getByLabel('Focused Image Element').hover({ trial: true });
|
// Lock the zoom and pan so it doesn't reset if a new image comes in
|
||||||
const lockButton = page.getByRole('button', {
|
|
||||||
name: 'Lock current zoom and pan across all images'
|
|
||||||
});
|
|
||||||
if (!(await lockButton.isVisible())) {
|
|
||||||
await page.getByLabel('Focused Image Element').hover({ trial: true });
|
await page.getByLabel('Focused Image Element').hover({ trial: true });
|
||||||
}
|
const lockButton = page.getByRole('button', {
|
||||||
await lockButton.click();
|
name: 'Lock current zoom and pan across all images'
|
||||||
|
});
|
||||||
|
|
||||||
await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty(
|
await lockButton.isVisible();
|
||||||
'style.transform',
|
// if (!(await lockButton.isVisible())) {
|
||||||
'scale(1) translate(0px, 0px)'
|
// await page.getByLabel('Focused Image Element').hover({ trial: true });
|
||||||
);
|
// }
|
||||||
|
await lockButton.click();
|
||||||
|
|
||||||
// Get initial image dimensions
|
await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty(
|
||||||
const initialBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
|
'style.transform',
|
||||||
|
'scale(1) translate(0px, 0px)'
|
||||||
|
);
|
||||||
|
|
||||||
// Zoom in twice via button
|
// Get initial image dimensions
|
||||||
await zoomIntoImageryByButton(page);
|
const initialBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
|
||||||
await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty(
|
|
||||||
'style.transform',
|
|
||||||
'scale(2) translate(0px, 0px)'
|
|
||||||
);
|
|
||||||
await zoomIntoImageryByButton(page);
|
|
||||||
await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty(
|
|
||||||
'style.transform',
|
|
||||||
'scale(3) translate(0px, 0px)'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Get and assert zoomed in image dimensions
|
// Zoom in twice via button
|
||||||
const zoomedInBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
|
await zoomIntoImageryByButton(page);
|
||||||
expect(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
|
await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty(
|
||||||
expect(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width);
|
'style.transform',
|
||||||
|
'scale(2) translate(0px, 0px)'
|
||||||
|
);
|
||||||
|
await zoomIntoImageryByButton(page);
|
||||||
|
await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty(
|
||||||
|
'style.transform',
|
||||||
|
'scale(3) translate(0px, 0px)'
|
||||||
|
);
|
||||||
|
|
||||||
// Zoom out once via button
|
// Get and assert zoomed in image dimensions
|
||||||
await zoomOutOfImageryByButton(page);
|
const zoomedInBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
|
||||||
await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty(
|
expect(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
|
||||||
'style.transform',
|
expect(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width);
|
||||||
'scale(2) translate(0px, 0px)'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Get and assert zoomed out image dimensions
|
// Zoom out once via button
|
||||||
const zoomedOutBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
|
await zoomOutOfImageryByButton(page);
|
||||||
expect(zoomedOutBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height);
|
await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty(
|
||||||
expect(zoomedOutBoundingBox.width).toBeLessThan(zoomedInBoundingBox.width);
|
'style.transform',
|
||||||
|
'scale(2) translate(0px, 0px)'
|
||||||
|
);
|
||||||
|
|
||||||
// Zoom out again via button, assert against the initial image dimensions
|
// Get and assert zoomed out image dimensions
|
||||||
await zoomOutOfImageryByButton(page);
|
const zoomedOutBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
|
||||||
await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty(
|
expect(zoomedOutBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height);
|
||||||
'style.transform',
|
expect(zoomedOutBoundingBox.width).toBeLessThan(zoomedInBoundingBox.width);
|
||||||
'scale(1) translate(0px, 0px)'
|
|
||||||
);
|
|
||||||
|
|
||||||
const finalBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
|
// Zoom out again via button, assert against the initial image dimensions
|
||||||
expect(finalBoundingBox).toEqual(initialBoundingBox);
|
await zoomOutOfImageryByButton(page);
|
||||||
|
await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty(
|
||||||
|
'style.transform',
|
||||||
|
'scale(1) translate(0px, 0px)'
|
||||||
|
);
|
||||||
|
|
||||||
|
const finalBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
|
||||||
|
expect(finalBoundingBox).toEqual(initialBoundingBox);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1035,24 +996,6 @@ async function resetImageryPanAndZoom(page) {
|
|||||||
await expect(page.locator('.c-thumb__viewable-area')).toBeHidden();
|
await expect(page.locator('.c-thumb__viewable-area')).toBeHidden();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {import('@playwright/test').Page} page
|
|
||||||
*/
|
|
||||||
async function createImageryViewWithShortDelay(page, { name, parent }) {
|
|
||||||
await createDomainObjectWithDefaults(page, {
|
|
||||||
name,
|
|
||||||
type: 'Example Imagery',
|
|
||||||
parent
|
|
||||||
});
|
|
||||||
|
|
||||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery');
|
|
||||||
await page.getByLabel('More actions').click();
|
|
||||||
await page.getByLabel('Edit Properties').click();
|
|
||||||
// Clear and set Image load delay to minimum value
|
|
||||||
await page.locator('input[type="number"]').fill(`${IMAGE_LOAD_DELAY}`);
|
|
||||||
await page.getByLabel('Save').click();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('@playwright/test').Page} page
|
* @param {import('@playwright/test').Page} page
|
||||||
*/
|
*/
|
||||||
|
@ -0,0 +1,489 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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 how imagery functions over time.
|
||||||
|
It only assumes that example imagery is present.
|
||||||
|
It uses https://playwright.dev/docs/clock to have control over time
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
createDomainObjectWithDefaults,
|
||||||
|
navigateToObjectWithRealTime,
|
||||||
|
setRealTimeMode,
|
||||||
|
setStartOffset
|
||||||
|
} from '../../../../appActions.js';
|
||||||
|
import { MISSION_TIME } from '../../../../constants.js';
|
||||||
|
import {
|
||||||
|
createImageryViewWithShortDelay,
|
||||||
|
FIVE_MINUTES,
|
||||||
|
IMAGE_LOAD_DELAY,
|
||||||
|
THIRTY_SECONDS
|
||||||
|
} from '../../../../helper/imageryUtils.js';
|
||||||
|
import { expect, test } from '../../../../pluginFixtures.js';
|
||||||
|
|
||||||
|
test.describe('Example Imagery Object with Controlled Clock @clock', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
// We mock the clock so that we don't need to wait for time driven events
|
||||||
|
// to verify functionality.
|
||||||
|
await page.clock.install({ time: MISSION_TIME });
|
||||||
|
await page.clock.resume();
|
||||||
|
|
||||||
|
//Go to baseURL
|
||||||
|
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||||
|
|
||||||
|
// Create a default 'Example Imagery' object
|
||||||
|
// Click the Create button
|
||||||
|
await page.getByRole('button', { name: 'Create' }).click();
|
||||||
|
|
||||||
|
// Click text=Example Imagery
|
||||||
|
await page.getByRole('menuitem', { name: 'Example Imagery' }).click();
|
||||||
|
|
||||||
|
// Clear and set Image load delay to minimum value
|
||||||
|
await page.locator('input[type="number"]').clear();
|
||||||
|
await page.locator('input[type="number"]').fill(`${IMAGE_LOAD_DELAY}`);
|
||||||
|
|
||||||
|
await page.getByLabel('Save').click();
|
||||||
|
|
||||||
|
// Verify that the created object is focused
|
||||||
|
await expect(page.locator('.l-browse-bar__object-name')).toContainText(
|
||||||
|
'Unnamed Example Imagery'
|
||||||
|
);
|
||||||
|
await page.getByLabel('Focused Image Element').hover({ trial: true });
|
||||||
|
|
||||||
|
// set realtime mode
|
||||||
|
await setRealTimeMode(page);
|
||||||
|
await setStartOffset(page, { startMins: '05' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Imagery Time Bounding', async ({ page }) => {
|
||||||
|
test.info().annotations.push({
|
||||||
|
type: 'issue',
|
||||||
|
description: 'https://github.com/nasa/openmct/issues/5265'
|
||||||
|
});
|
||||||
|
test.info().annotations.push({
|
||||||
|
type: 'issue',
|
||||||
|
description: 'https://github.com/nasa/openmct/issues/7825'
|
||||||
|
});
|
||||||
|
|
||||||
|
// verify that old images are discarded
|
||||||
|
const lastImageInBounds = page.getByLabel('Image thumbnail from').first();
|
||||||
|
const lastImageTimestamp = await lastImageInBounds.getAttribute('title');
|
||||||
|
expect(lastImageTimestamp).not.toBeNull();
|
||||||
|
|
||||||
|
// go forward in time to ensure old images are discarded
|
||||||
|
await page.clock.fastForward(IMAGE_LOAD_DELAY);
|
||||||
|
await page.clock.resume();
|
||||||
|
await expect(page.getByLabel(lastImageTimestamp)).toBeHidden();
|
||||||
|
|
||||||
|
// go way forward in time to ensure multiple images are discarded
|
||||||
|
const IMAGES_TO_DISCARD_COUNT = 5;
|
||||||
|
|
||||||
|
const lastImageToDiscard = page
|
||||||
|
.getByLabel('Image thumbnail from')
|
||||||
|
.nth(IMAGES_TO_DISCARD_COUNT - 1);
|
||||||
|
const lastImageToDiscardTimestamp = await lastImageToDiscard.getAttribute('title');
|
||||||
|
expect(lastImageToDiscardTimestamp).not.toBeNull();
|
||||||
|
|
||||||
|
const imageAfterLastImageToDiscard = page
|
||||||
|
.getByLabel('Image thumbnail from')
|
||||||
|
.nth(IMAGES_TO_DISCARD_COUNT);
|
||||||
|
const imageAfterLastImageToDiscardTimestamp =
|
||||||
|
await imageAfterLastImageToDiscard.getAttribute('title');
|
||||||
|
expect(imageAfterLastImageToDiscardTimestamp).not.toBeNull();
|
||||||
|
|
||||||
|
await page.clock.fastForward(IMAGE_LOAD_DELAY * IMAGES_TO_DISCARD_COUNT);
|
||||||
|
await page.clock.resume();
|
||||||
|
|
||||||
|
await expect(page.getByLabel(lastImageToDiscardTimestamp)).toBeHidden();
|
||||||
|
await expect(page.getByLabel(imageAfterLastImageToDiscardTimestamp)).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Get background-image url from background-image css prop', async ({ page }) => {
|
||||||
|
await assertBackgroundImageUrlFromBackgroundCss(page);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Example Imagery in Display Layout with Controlled Clock @clock', () => {
|
||||||
|
let displayLayout;
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
// We mock the clock so that we don't need to wait for time driven events
|
||||||
|
// to verify functionality.
|
||||||
|
await page.clock.install({ time: MISSION_TIME });
|
||||||
|
await page.clock.resume();
|
||||||
|
|
||||||
|
// Go to baseURL
|
||||||
|
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||||
|
|
||||||
|
displayLayout = await createDomainObjectWithDefaults(page, { type: 'Display Layout' });
|
||||||
|
|
||||||
|
// Create Example Imagery inside Display Layout
|
||||||
|
await createImageryViewWithShortDelay(page, {
|
||||||
|
name: 'Unnamed Example Imagery',
|
||||||
|
parent: displayLayout.uuid
|
||||||
|
});
|
||||||
|
|
||||||
|
// set realtime mode
|
||||||
|
await navigateToObjectWithRealTime(
|
||||||
|
page,
|
||||||
|
displayLayout.url,
|
||||||
|
`${FIVE_MINUTES}`,
|
||||||
|
`${THIRTY_SECONDS}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Imagery Time Bounding', async ({ page }) => {
|
||||||
|
test.info().annotations.push({
|
||||||
|
type: 'issue',
|
||||||
|
description: 'https://github.com/nasa/openmct/issues/5265'
|
||||||
|
});
|
||||||
|
test.info().annotations.push({
|
||||||
|
type: 'issue',
|
||||||
|
description: 'https://github.com/nasa/openmct/issues/7825'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Edit mode
|
||||||
|
await page.getByLabel('Edit Object').click();
|
||||||
|
|
||||||
|
// Click on example imagery to expose toolbar
|
||||||
|
await page.locator('.c-so-view__header').click();
|
||||||
|
|
||||||
|
// Adjust object height
|
||||||
|
await page.locator('div[title="Resize object height"] > input').click();
|
||||||
|
await page.locator('div[title="Resize object height"] > input').fill('50');
|
||||||
|
|
||||||
|
// Adjust object width
|
||||||
|
await page.locator('div[title="Resize object width"] > input').click();
|
||||||
|
await page.locator('div[title="Resize object width"] > input').fill('50');
|
||||||
|
|
||||||
|
await expect(page.getByLabel('Image Thumbnail from').last()).toBeInViewport();
|
||||||
|
|
||||||
|
// verify that old images are discarded
|
||||||
|
const lastImageInBounds = page.getByLabel('Image thumbnail from').first();
|
||||||
|
const lastImageTimestamp = await lastImageInBounds.getAttribute('title');
|
||||||
|
expect(lastImageTimestamp).not.toBeNull();
|
||||||
|
|
||||||
|
// go forward in time to ensure old images are discarded
|
||||||
|
await page.clock.fastForward(IMAGE_LOAD_DELAY);
|
||||||
|
await page.clock.resume();
|
||||||
|
await expect(page.getByLabel(lastImageTimestamp)).toBeHidden();
|
||||||
|
|
||||||
|
// go way forward in time to ensure multiple images are discarded
|
||||||
|
const IMAGES_TO_DISCARD_COUNT = 5;
|
||||||
|
|
||||||
|
const lastImageToDiscard = page
|
||||||
|
.getByLabel('Image thumbnail from')
|
||||||
|
.nth(IMAGES_TO_DISCARD_COUNT - 1);
|
||||||
|
const lastImageToDiscardTimestamp = await lastImageToDiscard.getAttribute('title');
|
||||||
|
expect(lastImageToDiscardTimestamp).not.toBeNull();
|
||||||
|
|
||||||
|
const imageAfterLastImageToDiscard = page
|
||||||
|
.getByLabel('Image thumbnail from')
|
||||||
|
.nth(IMAGES_TO_DISCARD_COUNT);
|
||||||
|
const imageAfterLastImageToDiscardTimestamp =
|
||||||
|
await imageAfterLastImageToDiscard.getAttribute('title');
|
||||||
|
expect(imageAfterLastImageToDiscardTimestamp).not.toBeNull();
|
||||||
|
|
||||||
|
await page.clock.fastForward(IMAGE_LOAD_DELAY * IMAGES_TO_DISCARD_COUNT);
|
||||||
|
await page.clock.resume();
|
||||||
|
|
||||||
|
await expect(page.getByLabel(lastImageToDiscardTimestamp)).toBeHidden();
|
||||||
|
await expect(page.getByLabel(imageAfterLastImageToDiscardTimestamp)).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Get background-image url from background-image css prop @clock', async ({ page }) => {
|
||||||
|
await assertBackgroundImageUrlFromBackgroundCss(page);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Example Imagery in Flexible layout with Controlled Clock @clock', () => {
|
||||||
|
let flexibleLayout;
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
// We mock the clock so that we don't need to wait for time driven events
|
||||||
|
// to verify functionality.
|
||||||
|
await page.clock.install({ time: MISSION_TIME });
|
||||||
|
await page.clock.resume();
|
||||||
|
|
||||||
|
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||||
|
|
||||||
|
flexibleLayout = await createDomainObjectWithDefaults(page, { type: 'Flexible Layout' });
|
||||||
|
|
||||||
|
// Create Example Imagery inside the Flexible Layout
|
||||||
|
await createImageryViewWithShortDelay(page, {
|
||||||
|
name: 'Unnamed Example Imagery',
|
||||||
|
parent: flexibleLayout.uuid
|
||||||
|
});
|
||||||
|
|
||||||
|
// set realtime mode
|
||||||
|
await navigateToObjectWithRealTime(
|
||||||
|
page,
|
||||||
|
flexibleLayout.url,
|
||||||
|
`${FIVE_MINUTES}`,
|
||||||
|
`${THIRTY_SECONDS}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Wait for image thumbnail auto-scroll to complete
|
||||||
|
await expect(page.getByLabel('Image Thumbnail from').last()).toBeInViewport();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Imagery Time Bounding @clock', async ({ page, browserName }) => {
|
||||||
|
test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox');
|
||||||
|
test.info().annotations.push({
|
||||||
|
type: 'issue',
|
||||||
|
description: 'https://github.com/nasa/openmct/issues/5326'
|
||||||
|
});
|
||||||
|
test.info().annotations.push({
|
||||||
|
type: 'issue',
|
||||||
|
description: 'https://github.com/nasa/openmct/issues/7825'
|
||||||
|
});
|
||||||
|
|
||||||
|
// verify that old images are discarded
|
||||||
|
const lastImageInBounds = page.getByLabel('Image thumbnail from').first();
|
||||||
|
const lastImageTimestamp = await lastImageInBounds.getAttribute('title');
|
||||||
|
expect(lastImageTimestamp).not.toBeNull();
|
||||||
|
|
||||||
|
// go forward in time to ensure old images are discarded
|
||||||
|
await page.clock.fastForward(IMAGE_LOAD_DELAY);
|
||||||
|
await page.clock.resume();
|
||||||
|
await expect(page.getByLabel(lastImageTimestamp)).toBeHidden();
|
||||||
|
|
||||||
|
// go way forward in time to ensure multiple images are discarded
|
||||||
|
const IMAGES_TO_DISCARD_COUNT = 5;
|
||||||
|
|
||||||
|
const lastImageToDiscard = page
|
||||||
|
.getByLabel('Image thumbnail from')
|
||||||
|
.nth(IMAGES_TO_DISCARD_COUNT - 1);
|
||||||
|
const lastImageToDiscardTimestamp = await lastImageToDiscard.getAttribute('title');
|
||||||
|
expect(lastImageToDiscardTimestamp).not.toBeNull();
|
||||||
|
|
||||||
|
const imageAfterLastImageToDiscard = page
|
||||||
|
.getByLabel('Image thumbnail from')
|
||||||
|
.nth(IMAGES_TO_DISCARD_COUNT);
|
||||||
|
const imageAfterLastImageToDiscardTimestamp =
|
||||||
|
await imageAfterLastImageToDiscard.getAttribute('title');
|
||||||
|
expect(imageAfterLastImageToDiscardTimestamp).not.toBeNull();
|
||||||
|
|
||||||
|
await page.clock.fastForward(IMAGE_LOAD_DELAY * IMAGES_TO_DISCARD_COUNT);
|
||||||
|
await page.clock.resume();
|
||||||
|
|
||||||
|
await expect(page.getByLabel(lastImageToDiscardTimestamp)).toBeHidden();
|
||||||
|
await expect(page.getByLabel(imageAfterLastImageToDiscardTimestamp)).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Get background-image url from background-image css prop @clock', async ({ page }) => {
|
||||||
|
await assertBackgroundImageUrlFromBackgroundCss(page);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Example Imagery in Tabs View with Controlled Clock @clock', () => {
|
||||||
|
let timeStripObject;
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
// We mock the clock so that we don't need to wait for time driven events
|
||||||
|
// to verify functionality.
|
||||||
|
await page.clock.install({ time: MISSION_TIME });
|
||||||
|
await page.clock.resume();
|
||||||
|
|
||||||
|
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||||
|
|
||||||
|
timeStripObject = await createDomainObjectWithDefaults(page, { type: 'Tabs View' });
|
||||||
|
await page.goto(timeStripObject.url);
|
||||||
|
|
||||||
|
/* Create Example Imagery with minimum Image Load Delay */
|
||||||
|
// Click the Create button
|
||||||
|
await page.getByRole('button', { name: 'Create' }).click();
|
||||||
|
|
||||||
|
// Click text=Example Imagery
|
||||||
|
await page.getByRole('menuitem', { name: 'Example Imagery' }).click();
|
||||||
|
|
||||||
|
// Clear and set Image load delay to minimum value
|
||||||
|
await page.locator('input[type="number"]').clear();
|
||||||
|
await page.locator('input[type="number"]').fill(`${IMAGE_LOAD_DELAY}`);
|
||||||
|
|
||||||
|
await page.getByLabel('Save').click();
|
||||||
|
|
||||||
|
await expect(page.locator('.l-browse-bar__object-name')).toContainText(
|
||||||
|
'Unnamed Example Imagery'
|
||||||
|
);
|
||||||
|
|
||||||
|
// set realtime mode
|
||||||
|
await navigateToObjectWithRealTime(
|
||||||
|
page,
|
||||||
|
timeStripObject.url,
|
||||||
|
`${FIVE_MINUTES}`,
|
||||||
|
`${THIRTY_SECONDS}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Wait for image thumbnail auto-scroll to complete
|
||||||
|
await expect(page.getByLabel('Image Thumbnail from').last()).toBeInViewport();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Imagery Time Bounding @clock', async ({ page }) => {
|
||||||
|
test.info().annotations.push({
|
||||||
|
type: 'issue',
|
||||||
|
description: 'https://github.com/nasa/openmct/issues/5265'
|
||||||
|
});
|
||||||
|
test.info().annotations.push({
|
||||||
|
type: 'issue',
|
||||||
|
description: 'https://github.com/nasa/openmct/issues/7825'
|
||||||
|
});
|
||||||
|
|
||||||
|
// verify that old images are discarded
|
||||||
|
const lastImageInBounds = page.getByLabel('Image thumbnail from').first();
|
||||||
|
const lastImageTimestamp = await lastImageInBounds.getAttribute('title');
|
||||||
|
expect(lastImageTimestamp).not.toBeNull();
|
||||||
|
|
||||||
|
// go forward in time to ensure old images are discarded
|
||||||
|
await page.clock.fastForward(IMAGE_LOAD_DELAY);
|
||||||
|
await page.clock.resume();
|
||||||
|
await expect(page.getByLabel(lastImageTimestamp)).toBeHidden();
|
||||||
|
|
||||||
|
// go way forward in time to ensure multiple images are discarded
|
||||||
|
const IMAGES_TO_DISCARD_COUNT = 5;
|
||||||
|
|
||||||
|
const lastImageToDiscard = page
|
||||||
|
.getByLabel('Image thumbnail from')
|
||||||
|
.nth(IMAGES_TO_DISCARD_COUNT - 1);
|
||||||
|
const lastImageToDiscardTimestamp = await lastImageToDiscard.getAttribute('title');
|
||||||
|
expect(lastImageToDiscardTimestamp).not.toBeNull();
|
||||||
|
|
||||||
|
const imageAfterLastImageToDiscard = page
|
||||||
|
.getByLabel('Image thumbnail from')
|
||||||
|
.nth(IMAGES_TO_DISCARD_COUNT);
|
||||||
|
const imageAfterLastImageToDiscardTimestamp =
|
||||||
|
await imageAfterLastImageToDiscard.getAttribute('title');
|
||||||
|
expect(imageAfterLastImageToDiscardTimestamp).not.toBeNull();
|
||||||
|
|
||||||
|
await page.clock.fastForward(IMAGE_LOAD_DELAY * IMAGES_TO_DISCARD_COUNT);
|
||||||
|
await page.clock.resume();
|
||||||
|
|
||||||
|
await expect(page.getByLabel(lastImageToDiscardTimestamp)).toBeHidden();
|
||||||
|
await expect(page.getByLabel(imageAfterLastImageToDiscardTimestamp)).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Get background-image url from background-image css prop @clock', async ({ page }) => {
|
||||||
|
await assertBackgroundImageUrlFromBackgroundCss(page);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Example Imagery in Time Strip with Controlled Clock @clock', () => {
|
||||||
|
let timeStripObject;
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
// We mock the clock so that we don't need to wait for time driven events
|
||||||
|
// to verify functionality.
|
||||||
|
await page.clock.install({ time: MISSION_TIME });
|
||||||
|
await page.clock.resume();
|
||||||
|
|
||||||
|
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||||
|
|
||||||
|
timeStripObject = await createDomainObjectWithDefaults(page, { type: 'Time Strip' });
|
||||||
|
await page.goto(timeStripObject.url);
|
||||||
|
|
||||||
|
/* Create Example Imagery with minimum Image Load Delay */
|
||||||
|
// Click the Create button
|
||||||
|
await page.getByRole('button', { name: 'Create' }).click();
|
||||||
|
|
||||||
|
// Click text=Example Imagery
|
||||||
|
await page.getByRole('menuitem', { name: 'Example Imagery' }).click();
|
||||||
|
|
||||||
|
// Clear and set Image load delay to minimum value
|
||||||
|
await page.locator('input[type="number"]').clear();
|
||||||
|
await page.locator('input[type="number"]').fill(`${IMAGE_LOAD_DELAY}`);
|
||||||
|
|
||||||
|
await page.getByLabel('Save').click();
|
||||||
|
|
||||||
|
await expect(page.locator('.l-browse-bar__object-name')).toContainText(
|
||||||
|
'Unnamed Example Imagery'
|
||||||
|
);
|
||||||
|
|
||||||
|
// set realtime mode
|
||||||
|
await navigateToObjectWithRealTime(
|
||||||
|
page,
|
||||||
|
timeStripObject.url,
|
||||||
|
`${FIVE_MINUTES}`,
|
||||||
|
`${THIRTY_SECONDS}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Wait for image thumbnail auto-scroll to complete
|
||||||
|
await expect(page.getByLabel('wrapper-').last()).toBeInViewport();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Imagery Time Bounding @clock', async ({ page }) => {
|
||||||
|
test.info().annotations.push({
|
||||||
|
type: 'issue',
|
||||||
|
description: 'https://github.com/nasa/openmct/issues/5265'
|
||||||
|
});
|
||||||
|
test.info().annotations.push({
|
||||||
|
type: 'issue',
|
||||||
|
description: 'https://github.com/nasa/openmct/issues/7825'
|
||||||
|
});
|
||||||
|
|
||||||
|
// verify that old images are discarded
|
||||||
|
const lastImageInBounds = page.getByLabel('wrapper-').first();
|
||||||
|
const lastImageTimestamp = await lastImageInBounds.getAttribute('aria-label');
|
||||||
|
expect(lastImageTimestamp).not.toBeNull();
|
||||||
|
|
||||||
|
// go forward in time to ensure old images are discarded
|
||||||
|
await page.clock.fastForward(IMAGE_LOAD_DELAY);
|
||||||
|
await page.clock.resume();
|
||||||
|
await expect(page.getByLabel(lastImageTimestamp)).toBeHidden();
|
||||||
|
|
||||||
|
// go way forward in time to ensure multiple images are discarded
|
||||||
|
const IMAGES_TO_DISCARD_COUNT = 5;
|
||||||
|
|
||||||
|
const lastImageToDiscard = page.getByLabel('wrapper-').nth(IMAGES_TO_DISCARD_COUNT - 1);
|
||||||
|
const lastImageToDiscardTimestamp = await lastImageToDiscard.getAttribute('aria-label');
|
||||||
|
expect(lastImageToDiscardTimestamp).not.toBeNull();
|
||||||
|
|
||||||
|
const imageAfterLastImageToDiscard = page.getByLabel('wrapper-').nth(IMAGES_TO_DISCARD_COUNT);
|
||||||
|
const imageAfterLastImageToDiscardTimestamp =
|
||||||
|
await imageAfterLastImageToDiscard.getAttribute('aria-label');
|
||||||
|
expect(imageAfterLastImageToDiscardTimestamp).not.toBeNull();
|
||||||
|
|
||||||
|
await page.clock.fastForward(IMAGE_LOAD_DELAY * IMAGES_TO_DISCARD_COUNT);
|
||||||
|
await page.clock.resume();
|
||||||
|
|
||||||
|
await expect(page.getByLabel(lastImageToDiscardTimestamp)).toBeHidden();
|
||||||
|
await expect(page.getByLabel(imageAfterLastImageToDiscardTimestamp)).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
*/
|
||||||
|
async function assertBackgroundImageUrlFromBackgroundCss(page) {
|
||||||
|
const backgroundImage = page.getByLabel('Focused Image Element');
|
||||||
|
const backgroundImageUrl = await backgroundImage.evaluate((el) => {
|
||||||
|
return window
|
||||||
|
.getComputedStyle(el)
|
||||||
|
.getPropertyValue('background-image')
|
||||||
|
.match(/url\(([^)]+)\)/)[1];
|
||||||
|
});
|
||||||
|
|
||||||
|
// go forward in time to ensure old images are discarded
|
||||||
|
await page.clock.fastForward(IMAGE_LOAD_DELAY);
|
||||||
|
await page.clock.resume();
|
||||||
|
await expect(backgroundImage).not.toHaveJSProperty('background-image', backgroundImageUrl);
|
||||||
|
}
|
@ -0,0 +1,72 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2023, 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 { expect, test } from '../../../../pluginFixtures.js';
|
||||||
|
|
||||||
|
test.describe('The performance indicator', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||||
|
await page.evaluate(() => {
|
||||||
|
const openmct = window.openmct;
|
||||||
|
openmct.install(openmct.plugins.PerformanceIndicator());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can be installed', ({ page }) => {
|
||||||
|
const performanceIndicator = page.getByTitle('Performance Indicator');
|
||||||
|
expect(performanceIndicator).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Shows a numerical FPS value', async ({ page }) => {
|
||||||
|
// Frames Per Second. We need to wait at least 1 second to get a value.
|
||||||
|
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
await expect(page.getByTitle('Performance Indicator')).toHaveText(/\d\d? fps/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Supports showing optional extended performance information in an overlay for debugging', async ({
|
||||||
|
page
|
||||||
|
}) => {
|
||||||
|
const performanceMeasurementLabel = 'Some measurement';
|
||||||
|
const performanceMeasurementValue = 'Some value';
|
||||||
|
|
||||||
|
await page.evaluate(
|
||||||
|
({ performanceMeasurementLabel: label, performanceMeasurementValue: value }) => {
|
||||||
|
const openmct = window.openmct;
|
||||||
|
openmct.performance.measurements.set(label, value);
|
||||||
|
},
|
||||||
|
{ performanceMeasurementLabel, performanceMeasurementValue }
|
||||||
|
);
|
||||||
|
const performanceIndicator = page.getByTitle('Performance Indicator');
|
||||||
|
await performanceIndicator.click();
|
||||||
|
//Performance overlay is a crude debugging tool, it's evaluated once per second.
|
||||||
|
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
const performanceOverlay = page.getByTitle('Performance Overlay');
|
||||||
|
await expect(performanceOverlay).toBeVisible();
|
||||||
|
await expect(performanceOverlay).toHaveText(new RegExp(`${performanceMeasurementLabel}.*`));
|
||||||
|
await expect(performanceOverlay).toHaveText(new RegExp(`.*${performanceMeasurementValue}`));
|
||||||
|
|
||||||
|
//Confirm that it disappears if we click on it again.
|
||||||
|
await performanceIndicator.click();
|
||||||
|
await expect(performanceOverlay).toBeHidden();
|
||||||
|
});
|
||||||
|
});
|
@ -85,16 +85,6 @@ test.describe('Visual - Default @a11y', () => {
|
|||||||
await percySnapshot(page, `Display Layout Create Menu (theme: '${theme}')`);
|
await percySnapshot(page, `Display Layout Create Menu (theme: '${theme}')`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Visual - Default Gauge', async ({ page, theme }) => {
|
|
||||||
await createDomainObjectWithDefaults(page, {
|
|
||||||
type: 'Gauge',
|
|
||||||
name: 'Default Gauge'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Take a snapshot of the newly created Gauge object
|
|
||||||
await percySnapshot(page, `Default Gauge (theme: '${theme}')`);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.afterEach(async ({ page }, testInfo) => {
|
test.afterEach(async ({ page }, testInfo) => {
|
||||||
await scanForA11yViolations(page, testInfo.title);
|
await scanForA11yViolations(page, testInfo.title);
|
||||||
});
|
});
|
||||||
|
81
e2e/tests/visual-a11y/gauge.visual.spec.js
Normal file
81
e2e/tests/visual-a11y/gauge.visual.spec.js
Normal file
@ -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 percySnapshot from '@percy/playwright';
|
||||||
|
|
||||||
|
import { createDomainObjectWithDefaults } from '../../appActions.js';
|
||||||
|
import { scanForA11yViolations, test } from '../../avpFixtures.js';
|
||||||
|
import { VISUAL_FIXED_URL } from '../../constants.js';
|
||||||
|
|
||||||
|
test.describe('Visual - Gauges', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Visual - Default Gauge', async ({ page, theme }) => {
|
||||||
|
await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Gauge',
|
||||||
|
name: 'Default Gauge'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Take a snapshot of the newly created Gauge object
|
||||||
|
await percySnapshot(page, `Default Gauge (theme: '${theme}')`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Visual - Needle Gauge with State Generator', async ({ page, theme }) => {
|
||||||
|
const needleGauge = await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Gauge',
|
||||||
|
name: 'Needle Gauge'
|
||||||
|
});
|
||||||
|
|
||||||
|
//Modify the Gauge to be a Needle Gauge
|
||||||
|
await page.getByLabel('More actions').click();
|
||||||
|
await page.getByLabel('Edit Properties...').click();
|
||||||
|
await page.getByLabel('Gauge type', { exact: true }).selectOption('dial-needle');
|
||||||
|
await page.getByText('Ok').click();
|
||||||
|
|
||||||
|
//Add a State Generator to the Gauge
|
||||||
|
await page.goto(needleGauge.url + '?hideTree=true&hideInspector=true', {
|
||||||
|
waitUntil: 'domcontentloaded'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Take a snapshot of the newly created Gauge object
|
||||||
|
await percySnapshot(page, `Needle Gauge with no telemetry source (theme: '${theme}')`);
|
||||||
|
|
||||||
|
//Add a State Generator to the Gauge. Note this requires that snapshots are taken within 5 seconds
|
||||||
|
await page.getByLabel('Create', { exact: true }).click();
|
||||||
|
await page.getByLabel('State Generator').click();
|
||||||
|
await page.getByLabel('Modal Overlay').getByLabel('Navigate to Needle Gauge').click();
|
||||||
|
await page.getByLabel('Save').click();
|
||||||
|
|
||||||
|
//Add a State Generator to the Gauge
|
||||||
|
await page.goto(needleGauge.url + '?hideTree=true&hideInspector=true', {
|
||||||
|
waitUntil: 'domcontentloaded'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Take a snapshot of the newly created Gauge object
|
||||||
|
await percySnapshot(page, `Needle Gauge with State Generator (theme: '${theme}')`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterEach(async ({ page }, testInfo) => {
|
||||||
|
await scanForA11yViolations(page, testInfo.title);
|
||||||
|
});
|
||||||
|
});
|
@ -98,7 +98,7 @@ test.describe('Visual - Notebook @a11y', () => {
|
|||||||
|
|
||||||
await page.getByLabel('Expand My Items folder').click();
|
await page.getByLabel('Expand My Items folder').click();
|
||||||
|
|
||||||
await page.goto(notebook.url);
|
await page.goto(notebook.url, { waitUntil: 'networkidle' });
|
||||||
|
|
||||||
await page
|
await page
|
||||||
.getByLabel('Navigate to Dropped Overlay Plot')
|
.getByLabel('Navigate to Dropped Overlay Plot')
|
||||||
|
@ -53,7 +53,7 @@ test.describe('Grand Search @a11y', () => {
|
|||||||
theme
|
theme
|
||||||
}) => {
|
}) => {
|
||||||
// Navigate to display layout
|
// Navigate to display layout
|
||||||
await page.goto(displayLayout.url);
|
await page.goto(displayLayout.url, { waitUntil: 'networkidle' });
|
||||||
|
|
||||||
// Search for the object
|
// Search for the object
|
||||||
await page.getByRole('searchbox', { name: 'Search Input' }).click();
|
await page.getByRole('searchbox', { name: 'Search Input' }).click();
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
|
import { DEFAULT_SHELVE_DURATIONS } from '../../src/api/faultmanagement/FaultManagementAPI.js';
|
||||||
import { acknowledgeFault, randomFaults, shelveFault } from './utils.js';
|
import { acknowledgeFault, randomFaults, shelveFault } from './utils.js';
|
||||||
|
|
||||||
export default function (staticFaults = false) {
|
export default function (staticFaults = false) {
|
||||||
@ -56,6 +57,9 @@ export default function (staticFaults = false) {
|
|||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
success: true
|
success: true
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
getShelveDurations() {
|
||||||
|
return DEFAULT_SHELVE_DURATIONS;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,27 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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 SEVERITIES = ['WATCH', 'WARNING', 'CRITICAL'];
|
const SEVERITIES = ['WATCH', 'WARNING', 'CRITICAL'];
|
||||||
|
const MOONWALK_TIMESTAMP = 14159040000;
|
||||||
const NAMESPACE = '/Example/fault-';
|
const NAMESPACE = '/Example/fault-';
|
||||||
const getRandom = {
|
const getRandom = {
|
||||||
severity: () => SEVERITIES[Math.floor(Math.random() * 3)],
|
severity: () => SEVERITIES[Math.floor(Math.random() * 3)],
|
||||||
@ -13,7 +36,8 @@ const getRandom = {
|
|||||||
|
|
||||||
val = num;
|
val = num;
|
||||||
severity = SEVERITIES[severityIndex - 1];
|
severity = SEVERITIES[severityIndex - 1];
|
||||||
time = num;
|
// Subtract `num` from the timestamp so that the faults are in order
|
||||||
|
time = MOONWALK_TIMESTAMP - num; // Mon, 21 Jul 1969 02:56:00 GMT 🌔👨🚀👨🚀👨🚀
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -43,14 +67,7 @@ const getRandom = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export function shelveFault(
|
export function shelveFault(fault, opts = { shelved: true, comment: '', shelveDuration: 90000 }) {
|
||||||
fault,
|
|
||||||
opts = {
|
|
||||||
shelved: true,
|
|
||||||
comment: '',
|
|
||||||
shelveDuration: 90000
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
fault.shelved = true;
|
fault.shelved = true;
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -65,8 +82,8 @@ export function acknowledgeFault(fault) {
|
|||||||
export function randomFaults(staticFaults, count = 5) {
|
export function randomFaults(staticFaults, count = 5) {
|
||||||
let faults = [];
|
let faults = [];
|
||||||
|
|
||||||
for (let x = 1, y = count + 1; x < y; x++) {
|
for (let i = 1; i <= count; i++) {
|
||||||
faults.push(getRandom.fault(x, staticFaults));
|
faults.push(getRandom.fault(i, staticFaults));
|
||||||
}
|
}
|
||||||
|
|
||||||
return faults;
|
return faults;
|
||||||
|
152
package-lock.json
generated
152
package-lock.json
generated
@ -24,7 +24,6 @@
|
|||||||
"@vue/compiler-sfc": "3.4.3",
|
"@vue/compiler-sfc": "3.4.3",
|
||||||
"babel-loader": "9.1.0",
|
"babel-loader": "9.1.0",
|
||||||
"babel-plugin-istanbul": "6.1.1",
|
"babel-plugin-istanbul": "6.1.1",
|
||||||
"codecov": "3.8.3",
|
|
||||||
"comma-separated-values": "3.6.4",
|
"comma-separated-values": "3.6.4",
|
||||||
"copy-webpack-plugin": "12.0.2",
|
"copy-webpack-plugin": "12.0.2",
|
||||||
"cspell": "7.3.8",
|
"cspell": "7.3.8",
|
||||||
@ -1599,15 +1598,6 @@
|
|||||||
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==",
|
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@tootallnate/once": {
|
|
||||||
"version": "1.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz",
|
|
||||||
"integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==",
|
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/body-parser": {
|
"node_modules/@types/body-parser": {
|
||||||
"version": "1.19.5",
|
"version": "1.19.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
|
||||||
@ -2321,18 +2311,6 @@
|
|||||||
"node": ">=8.9"
|
"node": ">=8.9"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/agent-base": {
|
|
||||||
"version": "6.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
|
||||||
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"debug": "4"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 6.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/aggregate-error": {
|
"node_modules/aggregate-error": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
|
||||||
@ -2467,16 +2445,6 @@
|
|||||||
"sprintf-js": "~1.0.2"
|
"sprintf-js": "~1.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/argv": {
|
|
||||||
"version": "0.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/argv/-/argv-0.0.2.tgz",
|
|
||||||
"integrity": "sha512-dEamhpPEwRUBpLNHeuCm/v+g0anFByHahxodVO/BbAarHVBBg2MccCwf9K+o1Pof+2btdnkJelYVUWjW/VrATw==",
|
|
||||||
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
|
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.6.10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/array-flatten": {
|
"node_modules/array-flatten": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||||
@ -3026,26 +2994,6 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/codecov": {
|
|
||||||
"version": "3.8.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/codecov/-/codecov-3.8.3.tgz",
|
|
||||||
"integrity": "sha512-Y8Hw+V3HgR7V71xWH2vQ9lyS358CbGCldWlJFR0JirqoGtOoas3R3/OclRTvgUYFK29mmJICDPauVKmpqbwhOA==",
|
|
||||||
"deprecated": "https://about.codecov.io/blog/codecov-uploader-deprecation-plan/",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"argv": "0.0.2",
|
|
||||||
"ignore-walk": "3.0.4",
|
|
||||||
"js-yaml": "3.14.1",
|
|
||||||
"teeny-request": "7.1.1",
|
|
||||||
"urlgrey": "1.0.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"codecov": "bin/codecov"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
"version": "1.9.3",
|
"version": "1.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||||
@ -5620,21 +5568,6 @@
|
|||||||
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
|
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/fast-url-parser": {
|
|
||||||
"version": "1.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/fast-url-parser/-/fast-url-parser-1.1.3.tgz",
|
|
||||||
"integrity": "sha512-5jOCVXADYNuRkKFzNJ0dCCewsZiYo0dz8QNYljkOpFC6r2U4OBmKtvm/Tsuh4w1YYdDqDb31a8TVhBJ2OJKdqQ==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"punycode": "^1.3.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/fast-url-parser/node_modules/punycode": {
|
|
||||||
"version": "1.4.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
|
|
||||||
"integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/fastest-levenshtein": {
|
"node_modules/fastest-levenshtein": {
|
||||||
"version": "1.0.16",
|
"version": "1.0.16",
|
||||||
"resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz",
|
"resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz",
|
||||||
@ -6438,20 +6371,6 @@
|
|||||||
"node": ">=8.0.0"
|
"node": ">=8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/http-proxy-agent": {
|
|
||||||
"version": "4.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz",
|
|
||||||
"integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@tootallnate/once": "1",
|
|
||||||
"agent-base": "6",
|
|
||||||
"debug": "4"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/http-proxy-middleware": {
|
"node_modules/http-proxy-middleware": {
|
||||||
"version": "2.0.6",
|
"version": "2.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz",
|
||||||
@ -6482,19 +6401,6 @@
|
|||||||
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
|
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/https-proxy-agent": {
|
|
||||||
"version": "5.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
|
|
||||||
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"agent-base": "6",
|
|
||||||
"debug": "4"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/human-signals": {
|
"node_modules/human-signals": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
|
||||||
@ -6537,15 +6443,6 @@
|
|||||||
"node": ">= 4"
|
"node": ">= 4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ignore-walk": {
|
|
||||||
"version": "3.0.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.4.tgz",
|
|
||||||
"integrity": "sha512-PY6Ii8o1jMRA1z4F2hRkH/xN59ox43DavKvD3oDpfurRlOJyAHpifIwpbdv1n4jt4ov0jSpw3kQ4GhJnpBL6WQ==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"minimatch": "^3.0.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/image-size": {
|
"node_modules/image-size": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/image-size/-/image-size-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/image-size/-/image-size-1.1.1.tgz",
|
||||||
@ -10438,15 +10335,6 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/stream-events": {
|
|
||||||
"version": "1.0.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz",
|
|
||||||
"integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"stubs": "^3.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/streamroller": {
|
"node_modules/streamroller": {
|
||||||
"version": "3.1.5",
|
"version": "3.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz",
|
||||||
@ -10629,12 +10517,6 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/stubs": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz",
|
|
||||||
"integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/style-loader": {
|
"node_modules/style-loader": {
|
||||||
"version": "3.3.3",
|
"version": "3.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.3.tgz",
|
||||||
@ -10700,31 +10582,6 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/teeny-request": {
|
|
||||||
"version": "7.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-7.1.1.tgz",
|
|
||||||
"integrity": "sha512-iwY6rkW5DDGq8hE2YgNQlKbptYpY5Nn2xecjQiNjOXWbKzPGUfmeUBCSQbbr306d7Z7U2N0TPl+/SwYRfua1Dg==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"http-proxy-agent": "^4.0.0",
|
|
||||||
"https-proxy-agent": "^5.0.0",
|
|
||||||
"node-fetch": "^2.6.1",
|
|
||||||
"stream-events": "^1.0.5",
|
|
||||||
"uuid": "^8.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/teeny-request/node_modules/uuid": {
|
|
||||||
"version": "8.3.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
|
||||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
|
||||||
"dev": true,
|
|
||||||
"bin": {
|
|
||||||
"uuid": "dist/bin/uuid"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/terser": {
|
"node_modules/terser": {
|
||||||
"version": "5.29.1",
|
"version": "5.29.1",
|
||||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.29.1.tgz",
|
"resolved": "https://registry.npmjs.org/terser/-/terser-5.29.1.tgz",
|
||||||
@ -11114,15 +10971,6 @@
|
|||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/urlgrey": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/urlgrey/-/urlgrey-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-hJfIzMPJmI9IlLkby8QrsCykQ+SXDeO2W5Q9QTW3QpqZVTx4a/K7p8/5q+/isD8vsbVaFgql/gvAoQCRQ2Cb5w==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"fast-url-parser": "^1.1.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/util-deprecate": {
|
"node_modules/util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
|
@ -27,7 +27,6 @@
|
|||||||
"@vue/compiler-sfc": "3.4.3",
|
"@vue/compiler-sfc": "3.4.3",
|
||||||
"babel-loader": "9.1.0",
|
"babel-loader": "9.1.0",
|
||||||
"babel-plugin-istanbul": "6.1.1",
|
"babel-plugin-istanbul": "6.1.1",
|
||||||
"codecov": "3.8.3",
|
|
||||||
"comma-separated-values": "3.6.4",
|
"comma-separated-values": "3.6.4",
|
||||||
"copy-webpack-plugin": "12.0.2",
|
"copy-webpack-plugin": "12.0.2",
|
||||||
"cspell": "7.3.8",
|
"cspell": "7.3.8",
|
||||||
@ -132,9 +131,6 @@
|
|||||||
"update-about-dialog-copyright": "perl -pi -e 's/20\\d\\d\\-202\\d/2014\\-2023/gm' ./src/ui/layout/AboutDialog.vue",
|
"update-about-dialog-copyright": "perl -pi -e 's/20\\d\\d\\-202\\d/2014\\-2023/gm' ./src/ui/layout/AboutDialog.vue",
|
||||||
"update-copyright-date": "npm run update-about-dialog-copyright && grep -lr --null --include=*.{js,scss,vue,ts,sh,html,md,frag} 'Copyright (c) 20' . | xargs -r0 perl -pi -e 's/Copyright\\s\\(c\\)\\s20\\d\\d\\-20\\d\\d/Copyright \\(c\\)\\ 2014\\-2024/gm'",
|
"update-copyright-date": "npm run update-about-dialog-copyright && grep -lr --null --include=*.{js,scss,vue,ts,sh,html,md,frag} 'Copyright (c) 20' . | xargs -r0 perl -pi -e 's/Copyright\\s\\(c\\)\\s20\\d\\d\\-20\\d\\d/Copyright \\(c\\)\\ 2014\\-2024/gm'",
|
||||||
"cov:e2e:report": "nyc report --reporter=lcovonly --report-dir=./coverage/e2e",
|
"cov:e2e:report": "nyc report --reporter=lcovonly --report-dir=./coverage/e2e",
|
||||||
"cov:e2e:full:publish": "codecov --disable=gcov -f ./coverage/e2e/lcov.info -F e2e-full",
|
|
||||||
"cov:e2e:ci:publish": "codecov --disable=gcov -f ./coverage/e2e/lcov.info -F e2e-ci",
|
|
||||||
"cov:unit:publish": "codecov --disable=gcov -f ./coverage/unit/lcov.info -F unit",
|
|
||||||
"prepare": "npm run build:prod && npx tsc"
|
"prepare": "npm run build:prod && npx tsc"
|
||||||
},
|
},
|
||||||
"homepage": "https://nasa.github.io/openmct",
|
"homepage": "https://nasa.github.io/openmct",
|
||||||
@ -161,4 +157,4 @@
|
|||||||
"keywords": [
|
"keywords": [
|
||||||
"nasa"
|
"nasa"
|
||||||
]
|
]
|
||||||
}
|
}
|
@ -20,6 +20,32 @@
|
|||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
|
/** @type {ShelveDuration[]} */
|
||||||
|
export const DEFAULT_SHELVE_DURATIONS = [
|
||||||
|
{
|
||||||
|
name: '5 Minutes',
|
||||||
|
value: 300000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '10 Minutes',
|
||||||
|
value: 600000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '15 Minutes',
|
||||||
|
value: 900000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Indefinite',
|
||||||
|
value: null
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides an API for managing faults within Open MCT.
|
||||||
|
* It allows for the addition of a fault provider, checking for provider support, and
|
||||||
|
* performing various operations such as requesting, subscribing to, acknowledging,
|
||||||
|
* and shelving faults.
|
||||||
|
*/
|
||||||
export default class FaultManagementAPI {
|
export default class FaultManagementAPI {
|
||||||
/**
|
/**
|
||||||
* @param {import("openmct").OpenMCT} openmct
|
* @param {import("openmct").OpenMCT} openmct
|
||||||
@ -29,14 +55,20 @@ export default class FaultManagementAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {*} provider
|
* Sets the provider for the Fault Management API.
|
||||||
|
* The provider should implement methods for acknowledging and shelving faults.
|
||||||
|
*
|
||||||
|
* @param {*} provider - The provider to be set.
|
||||||
*/
|
*/
|
||||||
addProvider(provider) {
|
addProvider(provider) {
|
||||||
this.provider = provider;
|
this.provider = provider;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {boolean}
|
* Checks if the current provider supports fault management actions.
|
||||||
|
* Specifically, it checks if the provider has methods for acknowledging and shelving faults.
|
||||||
|
*
|
||||||
|
* @returns {boolean} - Returns true if the provider supports fault management actions, otherwise false.
|
||||||
*/
|
*/
|
||||||
supportsActions() {
|
supportsActions() {
|
||||||
return (
|
return (
|
||||||
@ -45,48 +77,78 @@ export default class FaultManagementAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('openmct').DomainObject} domainObject
|
* Requests fault data for a given domain object.
|
||||||
* @returns {Promise.<FaultAPIResponse[]>}
|
* This method checks if the current provider supports the request operation for the given domain object.
|
||||||
|
* If supported, it delegates the request to the provider's request method.
|
||||||
|
* If not supported, it returns a rejected promise.
|
||||||
|
*
|
||||||
|
* @param {import('openmct').DomainObject} domainObject - The domain object for which fault data is requested.
|
||||||
|
* @returns {Promise.<FaultAPIResponse[]>} - A promise that resolves to an array of fault API responses.
|
||||||
*/
|
*/
|
||||||
request(domainObject) {
|
request(domainObject) {
|
||||||
if (!this.provider?.supportsRequest(domainObject)) {
|
if (!this.provider?.supportsRequest(domainObject)) {
|
||||||
return Promise.reject();
|
return Promise.reject('Provider does not support request operation');
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.provider.request(domainObject);
|
return this.provider.request(domainObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('openmct').DomainObject} domainObject
|
* Subscribes to fault data updates for a given domain object.
|
||||||
* @param {Function} callback
|
* This method checks if the current provider supports the subscribe operation for the given domain object.
|
||||||
* @returns {Function} unsubscribe
|
* If supported, it delegates the subscription to the provider's subscribe method.
|
||||||
|
* If not supported, it returns a rejected promise.
|
||||||
|
*
|
||||||
|
* @param {import('openmct').DomainObject} domainObject - The domain object for which to subscribe to fault data updates.
|
||||||
|
* @param {Function} callback - The callback function to be called with fault data updates.
|
||||||
|
* @returns {Function} unsubscribe - A function to unsubscribe from the fault data updates.
|
||||||
*/
|
*/
|
||||||
subscribe(domainObject, callback) {
|
subscribe(domainObject, callback) {
|
||||||
if (!this.provider?.supportsSubscribe(domainObject)) {
|
if (!this.provider?.supportsSubscribe(domainObject)) {
|
||||||
return Promise.reject();
|
return Promise.reject('Provider does not support subscribe operation');
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.provider.subscribe(domainObject, callback);
|
return this.provider.subscribe(domainObject, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Fault} fault
|
* Acknowledges a fault using the provider's acknowledgeFault method.
|
||||||
* @param {*} ackData
|
*
|
||||||
|
* @param {Fault} fault - The fault object to be acknowledged.
|
||||||
|
* @param {*} ackData - Additional data required for acknowledging the fault.
|
||||||
|
* @returns {Promise.<T>} - A promise that resolves when the fault is acknowledged.
|
||||||
*/
|
*/
|
||||||
acknowledgeFault(fault, ackData) {
|
acknowledgeFault(fault, ackData) {
|
||||||
return this.provider.acknowledgeFault(fault, ackData);
|
return this.provider.acknowledgeFault(fault, ackData);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Fault} fault
|
* Shelves a fault using the provider's shelveFault method.
|
||||||
* @param {*} shelveData
|
*
|
||||||
* @returns {Promise.<T>}
|
* @param {Fault} fault - The fault object to be shelved.
|
||||||
|
* @param {*} shelveData - Additional data required for shelving the fault.
|
||||||
|
* @returns {Promise.<T>} - A promise that resolves when the fault is shelved.
|
||||||
*/
|
*/
|
||||||
shelveFault(fault, shelveData) {
|
shelveFault(fault, shelveData) {
|
||||||
return this.provider.shelveFault(fault, shelveData);
|
return this.provider.shelveFault(fault, shelveData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the available shelve durations from the provider, or the default durations if the
|
||||||
|
* provider does not provide any.
|
||||||
|
* @returns {ShelveDuration[]}
|
||||||
|
*/
|
||||||
|
getShelveDurations() {
|
||||||
|
return this.provider?.getShelveDurations() ?? DEFAULT_SHELVE_DURATIONS;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} TriggerValueInfo
|
* @typedef {Object} TriggerValueInfo
|
||||||
* @property {number} value
|
* @property {number} value
|
||||||
|
@ -20,25 +20,46 @@
|
|||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
import installWorker from './WebSocketWorker.js';
|
import installWorker from './WebSocketWorker.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Describes the strategy to be used when batching WebSocket messages
|
* @typedef RequestIdleCallbackOptions
|
||||||
*
|
* @prop {Number} timeout If the number of milliseconds represented by this
|
||||||
* @typedef BatchingStrategy
|
* parameter has elapsed and the callback has not already been called, invoke
|
||||||
* @property {Function} shouldBatchMessage a function that accepts a single
|
* the callback.
|
||||||
* argument - the raw message received from the websocket. Every message
|
* @see https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback
|
||||||
* received will be evaluated against this function so it should be performant.
|
|
||||||
* Note also that this function is executed in a worker, so it must be
|
|
||||||
* completely self-contained with no external dependencies. The function
|
|
||||||
* should return `true` if the message should be batched, and `false` if not.
|
|
||||||
* @property {Function} getBatchIdFromMessage a function that accepts a
|
|
||||||
* single argument - the raw message received from the websocket. Only messages
|
|
||||||
* where `shouldBatchMessage` has evaluated to true will be passed into this
|
|
||||||
* function. The function should return a unique value on which to batch the
|
|
||||||
* messages. For example a telemetry, channel, or parameter identifier.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides a reliable and convenient WebSocket abstraction layer that handles
|
* Mocks requestIdleCallback for Safari using setTimeout. Functionality will be
|
||||||
* a lot of boilerplate common to managing WebSocket connections such as:
|
* identical to setTimeout in Safari, which is to fire the callback function
|
||||||
|
* after the provided timeout period.
|
||||||
|
*
|
||||||
|
* In browsers that support requestIdleCallback, this const is just a
|
||||||
|
* pointer to the native function.
|
||||||
|
*
|
||||||
|
* @param {Function} callback a callback to be invoked during the next idle period, or
|
||||||
|
* after the specified timeout
|
||||||
|
* @param {RequestIdleCallbackOptions} options
|
||||||
|
* @see https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
function requestIdleCallbackPolyfill(callback, options) {
|
||||||
|
return (
|
||||||
|
// eslint-disable-next-line compat/compat
|
||||||
|
window.requestIdleCallback ??
|
||||||
|
((fn, { timeout }) =>
|
||||||
|
setTimeout(() => {
|
||||||
|
fn({ didTimeout: false });
|
||||||
|
}, timeout))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const requestIdleCallback = requestIdleCallbackPolyfill();
|
||||||
|
|
||||||
|
const ONE_SECOND = 1000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a WebSocket abstraction layer that handles a lot of boilerplate common
|
||||||
|
* to managing WebSocket connections such as:
|
||||||
* - Establishing a WebSocket connection to a server
|
* - Establishing a WebSocket connection to a server
|
||||||
* - Reconnecting on error, with a fallback strategy
|
* - Reconnecting on error, with a fallback strategy
|
||||||
* - Queuing messages so that clients can send messages without concern for the current
|
* - Queuing messages so that clients can send messages without concern for the current
|
||||||
@ -49,22 +70,19 @@ import installWorker from './WebSocketWorker.js';
|
|||||||
* and batching of messages without blocking either the UI or server.
|
* and batching of messages without blocking either the UI or server.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
// Shim for Internet Explorer, I mean Safari. It doesn't support requestIdleCallback, but it's in a tech preview, so it will be dropping soon.
|
|
||||||
const requestIdleCallback =
|
|
||||||
// eslint-disable-next-line compat/compat
|
|
||||||
window.requestIdleCallback ?? ((fn, { timeout }) => setTimeout(fn, timeout));
|
|
||||||
const ONE_SECOND = 1000;
|
|
||||||
const FIVE_SECONDS = 5 * ONE_SECOND;
|
|
||||||
|
|
||||||
class BatchingWebSocket extends EventTarget {
|
class BatchingWebSocket extends EventTarget {
|
||||||
#worker;
|
#worker;
|
||||||
#openmct;
|
#openmct;
|
||||||
#showingRateLimitNotification;
|
#showingRateLimitNotification;
|
||||||
#maxBatchSize;
|
#maxBufferSize;
|
||||||
#applicationIsInitializing;
|
#throttleRate;
|
||||||
#maxBatchWait;
|
|
||||||
#firstBatchReceived;
|
#firstBatchReceived;
|
||||||
|
#lastBatchReceived;
|
||||||
|
#peakBufferSize = Number.NEGATIVE_INFINITY;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('openmct.js').OpenMCT} openmct
|
||||||
|
*/
|
||||||
constructor(openmct) {
|
constructor(openmct) {
|
||||||
super();
|
super();
|
||||||
// Install worker, register listeners etc.
|
// Install worker, register listeners etc.
|
||||||
@ -74,9 +92,8 @@ class BatchingWebSocket extends EventTarget {
|
|||||||
this.#worker = new Worker(workerUrl);
|
this.#worker = new Worker(workerUrl);
|
||||||
this.#openmct = openmct;
|
this.#openmct = openmct;
|
||||||
this.#showingRateLimitNotification = false;
|
this.#showingRateLimitNotification = false;
|
||||||
this.#maxBatchSize = Number.POSITIVE_INFINITY;
|
this.#maxBufferSize = Number.POSITIVE_INFINITY;
|
||||||
this.#maxBatchWait = ONE_SECOND;
|
this.#throttleRate = ONE_SECOND;
|
||||||
this.#applicationIsInitializing = true;
|
|
||||||
this.#firstBatchReceived = false;
|
this.#firstBatchReceived = false;
|
||||||
|
|
||||||
const routeMessageToHandler = this.#routeMessageToHandler.bind(this);
|
const routeMessageToHandler = this.#routeMessageToHandler.bind(this);
|
||||||
@ -89,20 +106,6 @@ class BatchingWebSocket extends EventTarget {
|
|||||||
},
|
},
|
||||||
{ once: true }
|
{ once: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
openmct.once('start', () => {
|
|
||||||
// An idle callback is a pretty good indication that a complex display is done loading. At that point set the batch size more conservatively.
|
|
||||||
// Force it after 5 seconds if it hasn't happened yet.
|
|
||||||
requestIdleCallback(
|
|
||||||
() => {
|
|
||||||
this.#applicationIsInitializing = false;
|
|
||||||
this.setMaxBatchSize(this.#maxBatchSize);
|
|
||||||
},
|
|
||||||
{
|
|
||||||
timeout: FIVE_SECONDS
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -137,57 +140,48 @@ class BatchingWebSocket extends EventTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the strategy used to both decide which raw messages to batch, and how to group
|
* @param {number} maxBufferSize the maximum length of the receive buffer in characters.
|
||||||
* them.
|
|
||||||
* @param {BatchingStrategy} strategy The batching strategy to use when evaluating
|
|
||||||
* raw messages from the WebSocket.
|
|
||||||
*/
|
|
||||||
setBatchingStrategy(strategy) {
|
|
||||||
const serializedStrategy = {
|
|
||||||
shouldBatchMessage: strategy.shouldBatchMessage.toString(),
|
|
||||||
getBatchIdFromMessage: strategy.getBatchIdFromMessage.toString()
|
|
||||||
};
|
|
||||||
|
|
||||||
this.#worker.postMessage({
|
|
||||||
type: 'setBatchingStrategy',
|
|
||||||
serializedStrategy
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {number} maxBatchSize the maximum length of a batch of messages. For example,
|
|
||||||
* the maximum number of telemetry values to batch before dropping them
|
|
||||||
* Note that this is a fail-safe that is only invoked if performance drops to the
|
* Note that this is a fail-safe that is only invoked if performance drops to the
|
||||||
* point where Open MCT cannot keep up with the amount of telemetry it is receiving.
|
* point where Open MCT cannot keep up with the amount of telemetry it is receiving.
|
||||||
* In this event it will sacrifice the oldest telemetry in the batch in favor of the
|
* In this event it will sacrifice the oldest telemetry in the batch in favor of the
|
||||||
* most recent telemetry. The user will be informed that telemetry has been dropped.
|
* most recent telemetry. The user will be informed that telemetry has been dropped.
|
||||||
*
|
*
|
||||||
* This should be set appropriately for the expected data rate. eg. If telemetry
|
* This should be set appropriately for the expected data rate. eg. If typical usage
|
||||||
* is received at 10Hz for each telemetry point, then a minimal combination of batch
|
* sees 2000 messages arriving at a client per second, with an average message size
|
||||||
* size and rate is 10 and 1000 respectively. Ideally you would add some margin, so
|
* of 500 bytes, then 2000 * 500 = 1000000 characters will be right on the limit.
|
||||||
* 15 would probably be a better batch size.
|
* In this scenario, a buffer size of 1500000 character might be more appropriate
|
||||||
|
* to allow some overhead for bursty telemetry, and temporary UI load during page
|
||||||
|
* load.
|
||||||
|
*
|
||||||
|
* The PerformanceIndicator plugin (openmct.plugins.PerformanceIndicator) gives
|
||||||
|
* statistics on buffer utilization. It can be used to scale the buffer appropriately.
|
||||||
*/
|
*/
|
||||||
setMaxBatchSize(maxBatchSize) {
|
setMaxBufferSize(maxBatchSize) {
|
||||||
this.#maxBatchSize = maxBatchSize;
|
this.#maxBufferSize = maxBatchSize;
|
||||||
if (!this.#applicationIsInitializing) {
|
this.#sendMaxBufferSizeToWorker(this.#maxBufferSize);
|
||||||
this.#sendMaxBatchSizeToWorker(this.#maxBatchSize);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
setMaxBatchWait(wait) {
|
setThrottleRate(throttleRate) {
|
||||||
this.#maxBatchWait = wait;
|
this.#throttleRate = throttleRate;
|
||||||
this.#sendBatchWaitToWorker(this.#maxBatchWait);
|
this.#sendThrottleRateToWorker(this.#throttleRate);
|
||||||
}
|
}
|
||||||
#sendMaxBatchSizeToWorker(maxBatchSize) {
|
setThrottleMessagePattern(throttleMessagePattern) {
|
||||||
this.#worker.postMessage({
|
this.#worker.postMessage({
|
||||||
type: 'setMaxBatchSize',
|
type: 'setThrottleMessagePattern',
|
||||||
maxBatchSize
|
throttleMessagePattern
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#sendBatchWaitToWorker(maxBatchWait) {
|
#sendMaxBufferSizeToWorker(maxBufferSize) {
|
||||||
this.#worker.postMessage({
|
this.#worker.postMessage({
|
||||||
type: 'setMaxBatchWait',
|
type: 'setMaxBufferSize',
|
||||||
maxBatchWait
|
maxBufferSize
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#sendThrottleRateToWorker(throttleRate) {
|
||||||
|
this.#worker.postMessage({
|
||||||
|
type: 'setThrottleRate',
|
||||||
|
throttleRate
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -203,9 +197,38 @@ class BatchingWebSocket extends EventTarget {
|
|||||||
|
|
||||||
#routeMessageToHandler(message) {
|
#routeMessageToHandler(message) {
|
||||||
if (message.data.type === 'batch') {
|
if (message.data.type === 'batch') {
|
||||||
this.start = Date.now();
|
|
||||||
const batch = message.data.batch;
|
const batch = message.data.batch;
|
||||||
if (batch.dropped === true && !this.#showingRateLimitNotification) {
|
const now = performance.now();
|
||||||
|
|
||||||
|
let currentBufferLength = message.data.currentBufferLength;
|
||||||
|
let maxBufferSize = message.data.maxBufferSize;
|
||||||
|
let parameterCount = batch.length;
|
||||||
|
if (this.#peakBufferSize < currentBufferLength) {
|
||||||
|
this.#peakBufferSize = currentBufferLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.#openmct.performance !== undefined) {
|
||||||
|
if (!isNaN(this.#lastBatchReceived)) {
|
||||||
|
const elapsed = (now - this.#lastBatchReceived) / 1000;
|
||||||
|
this.#lastBatchReceived = now;
|
||||||
|
this.#openmct.performance.measurements.set(
|
||||||
|
'Parameters/s',
|
||||||
|
Math.floor(parameterCount / elapsed)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.#openmct.performance.measurements.set(
|
||||||
|
'Buff. Util. (bytes)',
|
||||||
|
`${currentBufferLength} / ${maxBufferSize}`
|
||||||
|
);
|
||||||
|
this.#openmct.performance.measurements.set(
|
||||||
|
'Peak Buff. Util. (bytes)',
|
||||||
|
`${this.#peakBufferSize} / ${maxBufferSize}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.start = Date.now();
|
||||||
|
const dropped = message.data.dropped;
|
||||||
|
if (dropped === true && !this.#showingRateLimitNotification) {
|
||||||
const notification = this.#openmct.notifications.alert(
|
const notification = this.#openmct.notifications.alert(
|
||||||
'Telemetry dropped due to client rate limiting.',
|
'Telemetry dropped due to client rate limiting.',
|
||||||
{ hint: 'Refresh individual telemetry views to retrieve dropped telemetry if needed.' }
|
{ hint: 'Refresh individual telemetry views to retrieve dropped telemetry if needed.' }
|
||||||
@ -240,18 +263,16 @@ class BatchingWebSocket extends EventTarget {
|
|||||||
console.warn(`Event loop is too busy to process batch.`);
|
console.warn(`Event loop is too busy to process batch.`);
|
||||||
this.#waitUntilIdleAndRequestNextBatch(batch);
|
this.#waitUntilIdleAndRequestNextBatch(batch);
|
||||||
} else {
|
} else {
|
||||||
// After ingesting a telemetry batch, wait until the event loop is idle again before
|
|
||||||
// informing the worker we are ready for another batch.
|
|
||||||
this.#readyForNextBatch();
|
this.#readyForNextBatch();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (waitedFor > ONE_SECOND) {
|
if (waitedFor > this.#throttleRate) {
|
||||||
console.warn(`Warning, batch processing took ${waitedFor}ms`);
|
console.warn(`Warning, batch processing took ${waitedFor}ms`);
|
||||||
}
|
}
|
||||||
this.#readyForNextBatch();
|
this.#readyForNextBatch();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ timeout: ONE_SECOND }
|
{ timeout: this.#throttleRate }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -119,8 +119,7 @@ export default class TelemetryCollection extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This will start the requests for historical and realtime data,
|
* @returns {Array} All bounded telemetry
|
||||||
* as well as setting up initial values and watchers
|
|
||||||
*/
|
*/
|
||||||
getAll() {
|
getAll() {
|
||||||
return this.boundedTelemetry;
|
return this.boundedTelemetry;
|
||||||
|
@ -24,10 +24,6 @@ export default function installWorker() {
|
|||||||
const ONE_SECOND = 1000;
|
const ONE_SECOND = 1000;
|
||||||
const FALLBACK_AND_WAIT_MS = [1000, 5000, 5000, 10000, 10000, 30000];
|
const FALLBACK_AND_WAIT_MS = [1000, 5000, 5000, 10000, 10000, 30000];
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {import('./BatchingWebSocket').BatchingStrategy} BatchingStrategy
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides a WebSocket connection that is resilient to errors and dropouts.
|
* Provides a WebSocket connection that is resilient to errors and dropouts.
|
||||||
* On an error or dropout, will automatically reconnect.
|
* On an error or dropout, will automatically reconnect.
|
||||||
@ -215,17 +211,17 @@ export default function installWorker() {
|
|||||||
case 'message':
|
case 'message':
|
||||||
this.#websocket.enqueueMessage(message.data.message);
|
this.#websocket.enqueueMessage(message.data.message);
|
||||||
break;
|
break;
|
||||||
case 'setBatchingStrategy':
|
|
||||||
this.setBatchingStrategy(message);
|
|
||||||
break;
|
|
||||||
case 'readyForNextBatch':
|
case 'readyForNextBatch':
|
||||||
this.#messageBatcher.readyForNextBatch();
|
this.#messageBatcher.readyForNextBatch();
|
||||||
break;
|
break;
|
||||||
case 'setMaxBatchSize':
|
case 'setMaxBufferSize':
|
||||||
this.#messageBatcher.setMaxBatchSize(message.data.maxBatchSize);
|
this.#messageBatcher.setMaxBufferSize(message.data.maxBufferSize);
|
||||||
break;
|
break;
|
||||||
case 'setMaxBatchWait':
|
case 'setThrottleRate':
|
||||||
this.#messageBatcher.setMaxBatchWait(message.data.maxBatchWait);
|
this.#messageBatcher.setThrottleRate(message.data.throttleRate);
|
||||||
|
break;
|
||||||
|
case 'setThrottleMessagePattern':
|
||||||
|
this.#messageBatcher.setThrottleMessagePattern(message.data.throttleMessagePattern);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown message type: ${type}`);
|
throw new Error(`Unknown message type: ${type}`);
|
||||||
@ -238,122 +234,69 @@ export default function installWorker() {
|
|||||||
disconnect() {
|
disconnect() {
|
||||||
this.#websocket.disconnect();
|
this.#websocket.disconnect();
|
||||||
}
|
}
|
||||||
setBatchingStrategy(message) {
|
|
||||||
const { serializedStrategy } = message.data;
|
|
||||||
const batchingStrategy = {
|
|
||||||
// eslint-disable-next-line no-new-func
|
|
||||||
shouldBatchMessage: new Function(`return ${serializedStrategy.shouldBatchMessage}`)(),
|
|
||||||
// eslint-disable-next-line no-new-func
|
|
||||||
getBatchIdFromMessage: new Function(`return ${serializedStrategy.getBatchIdFromMessage}`)()
|
|
||||||
// Will also include maximum batch length here
|
|
||||||
};
|
|
||||||
this.#messageBatcher.setBatchingStrategy(batchingStrategy);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Received messages from the WebSocket, and passes them along to the
|
* Responsible for buffering messages
|
||||||
* Worker interface and back to the main thread.
|
|
||||||
*/
|
*/
|
||||||
class WebSocketToWorkerMessageBroker {
|
class MessageBuffer {
|
||||||
#worker;
|
#buffer;
|
||||||
#messageBatcher;
|
#currentBufferLength;
|
||||||
|
#dropped;
|
||||||
constructor(messageBatcher, worker) {
|
#maxBufferSize;
|
||||||
this.#messageBatcher = messageBatcher;
|
|
||||||
this.#worker = worker;
|
|
||||||
}
|
|
||||||
|
|
||||||
routeMessageToHandler(data) {
|
|
||||||
if (this.#messageBatcher.shouldBatchMessage(data)) {
|
|
||||||
this.#messageBatcher.addMessageToBatch(data);
|
|
||||||
} else {
|
|
||||||
this.#worker.postMessage({
|
|
||||||
type: 'message',
|
|
||||||
message: data
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Responsible for batching messages according to the defined batching strategy.
|
|
||||||
*/
|
|
||||||
class MessageBatcher {
|
|
||||||
#batch;
|
|
||||||
#batchingStrategy;
|
|
||||||
#hasBatch = false;
|
|
||||||
#maxBatchSize;
|
|
||||||
#readyForNextBatch;
|
#readyForNextBatch;
|
||||||
#worker;
|
#worker;
|
||||||
#throttledSendNextBatch;
|
#throttledSendNextBatch;
|
||||||
|
#throttleMessagePattern;
|
||||||
|
|
||||||
constructor(worker) {
|
constructor(worker) {
|
||||||
// No dropping telemetry unless we're explicitly told to.
|
// No dropping telemetry unless we're explicitly told to.
|
||||||
this.#maxBatchSize = Number.POSITIVE_INFINITY;
|
this.#maxBufferSize = Number.POSITIVE_INFINITY;
|
||||||
this.#readyForNextBatch = false;
|
this.#readyForNextBatch = false;
|
||||||
this.#worker = worker;
|
this.#worker = worker;
|
||||||
this.#resetBatch();
|
this.#resetBatch();
|
||||||
this.setMaxBatchWait(ONE_SECOND);
|
this.setThrottleRate(ONE_SECOND);
|
||||||
}
|
}
|
||||||
#resetBatch() {
|
#resetBatch() {
|
||||||
this.#batch = {};
|
//this.#batch = {};
|
||||||
this.#hasBatch = false;
|
this.#buffer = [];
|
||||||
|
this.#currentBufferLength = 0;
|
||||||
|
this.#dropped = false;
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* @param {BatchingStrategy} strategy
|
addMessageToBuffer(message) {
|
||||||
*/
|
this.#buffer.push(message);
|
||||||
setBatchingStrategy(strategy) {
|
this.#currentBufferLength += message.length;
|
||||||
this.#batchingStrategy = strategy;
|
|
||||||
}
|
for (
|
||||||
/**
|
let i = 0;
|
||||||
* Applies the `shouldBatchMessage` function from the supplied batching strategy
|
this.#currentBufferLength > this.#maxBufferSize && i < this.#buffer.length;
|
||||||
* to each message to determine if it should be added to a batch. If not batched,
|
i++
|
||||||
* the message is immediately sent over the worker to the main thread.
|
) {
|
||||||
* @param {any} message the message received from the WebSocket. See the WebSocket
|
const messageToConsider = this.#buffer[i];
|
||||||
* documentation for more details -
|
if (this.#shouldThrottle(messageToConsider)) {
|
||||||
* https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent/data
|
this.#buffer.splice(i, 1);
|
||||||
* @returns
|
this.#currentBufferLength -= messageToConsider.length;
|
||||||
*/
|
this.#dropped = true;
|
||||||
shouldBatchMessage(message) {
|
}
|
||||||
return (
|
|
||||||
this.#batchingStrategy.shouldBatchMessage &&
|
|
||||||
this.#batchingStrategy.shouldBatchMessage(message)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Adds the given message to a batch. The batch group that the message is added
|
|
||||||
* to will be determined by the value returned by `getBatchIdFromMessage`.
|
|
||||||
* @param {any} message the message received from the WebSocket. See the WebSocket
|
|
||||||
* documentation for more details -
|
|
||||||
* https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent/data
|
|
||||||
*/
|
|
||||||
addMessageToBatch(message) {
|
|
||||||
const batchId = this.#batchingStrategy.getBatchIdFromMessage(message);
|
|
||||||
let batch = this.#batch[batchId];
|
|
||||||
if (batch === undefined) {
|
|
||||||
this.#hasBatch = true;
|
|
||||||
batch = this.#batch[batchId] = [message];
|
|
||||||
} else {
|
|
||||||
batch.push(message);
|
|
||||||
}
|
|
||||||
if (batch.length > this.#maxBatchSize) {
|
|
||||||
console.warn(
|
|
||||||
`Exceeded max batch size of ${this.#maxBatchSize} for ${batchId}. Dropping value.`
|
|
||||||
);
|
|
||||||
batch.shift();
|
|
||||||
this.#batch.dropped = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.#readyForNextBatch) {
|
if (this.#readyForNextBatch) {
|
||||||
this.#throttledSendNextBatch();
|
this.#throttledSendNextBatch();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setMaxBatchSize(maxBatchSize) {
|
|
||||||
this.#maxBatchSize = maxBatchSize;
|
#shouldThrottle(message) {
|
||||||
|
return (
|
||||||
|
this.#throttleMessagePattern !== undefined && this.#throttleMessagePattern.test(message)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
setMaxBatchWait(maxBatchWait) {
|
|
||||||
this.#throttledSendNextBatch = throttle(this.#sendNextBatch.bind(this), maxBatchWait);
|
setMaxBufferSize(maxBufferSize) {
|
||||||
|
this.#maxBufferSize = maxBufferSize;
|
||||||
|
}
|
||||||
|
setThrottleRate(throttleRate) {
|
||||||
|
this.#throttledSendNextBatch = throttle(this.#sendNextBatch.bind(this), throttleRate);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Indicates that client code is ready to receive the next batch of
|
* Indicates that client code is ready to receive the next batch of
|
||||||
@ -362,21 +305,33 @@ export default function installWorker() {
|
|||||||
* any new data is available.
|
* any new data is available.
|
||||||
*/
|
*/
|
||||||
readyForNextBatch() {
|
readyForNextBatch() {
|
||||||
if (this.#hasBatch) {
|
if (this.#hasData()) {
|
||||||
this.#throttledSendNextBatch();
|
this.#throttledSendNextBatch();
|
||||||
} else {
|
} else {
|
||||||
this.#readyForNextBatch = true;
|
this.#readyForNextBatch = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#sendNextBatch() {
|
#sendNextBatch() {
|
||||||
const batch = this.#batch;
|
const buffer = this.#buffer;
|
||||||
|
const dropped = this.#dropped;
|
||||||
|
const currentBufferLength = this.#currentBufferLength;
|
||||||
|
|
||||||
this.#resetBatch();
|
this.#resetBatch();
|
||||||
this.#worker.postMessage({
|
this.#worker.postMessage({
|
||||||
type: 'batch',
|
type: 'batch',
|
||||||
batch
|
dropped,
|
||||||
|
currentBufferLength: currentBufferLength,
|
||||||
|
maxBufferSize: this.#maxBufferSize,
|
||||||
|
batch: buffer
|
||||||
});
|
});
|
||||||
|
|
||||||
this.#readyForNextBatch = false;
|
this.#readyForNextBatch = false;
|
||||||
this.#hasBatch = false;
|
}
|
||||||
|
#hasData() {
|
||||||
|
return this.#currentBufferLength > 0;
|
||||||
|
}
|
||||||
|
setThrottleMessagePattern(priorityMessagePattern) {
|
||||||
|
this.#throttleMessagePattern = new RegExp(priorityMessagePattern, 'm');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -408,15 +363,14 @@ export default function installWorker() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const websocket = new ResilientWebSocket(self);
|
const websocket = new ResilientWebSocket(self);
|
||||||
const messageBatcher = new MessageBatcher(self);
|
const messageBuffer = new MessageBuffer(self);
|
||||||
const workerBroker = new WorkerToWebSocketMessageBroker(websocket, messageBatcher);
|
const workerBroker = new WorkerToWebSocketMessageBroker(websocket, messageBuffer);
|
||||||
const websocketBroker = new WebSocketToWorkerMessageBroker(messageBatcher, self);
|
|
||||||
|
|
||||||
self.addEventListener('message', (message) => {
|
self.addEventListener('message', (message) => {
|
||||||
workerBroker.routeMessageToHandler(message);
|
workerBroker.routeMessageToHandler(message);
|
||||||
});
|
});
|
||||||
websocket.registerMessageCallback((data) => {
|
websocket.registerMessageCallback((data) => {
|
||||||
websocketBroker.routeMessageToHandler(data);
|
messageBuffer.addMessageToBuffer(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
self.websocketInstance = websocket;
|
self.websocketInstance = websocket;
|
||||||
|
@ -257,7 +257,9 @@ export default {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
end,
|
end,
|
||||||
start
|
start,
|
||||||
|
size: 1,
|
||||||
|
strategy: 'latest'
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
loadComposition() {
|
loadComposition() {
|
||||||
|
@ -21,7 +21,12 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="c-fault-mgmt__list data-selectable" :class="classesFromState">
|
<div
|
||||||
|
role="listitem"
|
||||||
|
:aria-label="listItemAriaLabel"
|
||||||
|
class="c-fault-mgmt__list data-selectable"
|
||||||
|
:class="classesFromState"
|
||||||
|
>
|
||||||
<div class="c-fault-mgmt-item c-fault-mgmt__list-checkbox">
|
<div class="c-fault-mgmt-item c-fault-mgmt__list-checkbox">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@ -33,29 +38,44 @@
|
|||||||
<div class="c-fault-mgmt-item">
|
<div class="c-fault-mgmt-item">
|
||||||
<div
|
<div
|
||||||
class="c-fault-mgmt__list-severity"
|
class="c-fault-mgmt__list-severity"
|
||||||
:aria-label="fault.severity"
|
:aria-label="severityAriaLabel"
|
||||||
:title="fault.severity"
|
|
||||||
:class="['is-severity-' + severity]"
|
:class="['is-severity-' + severity]"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="c-fault-mgmt-item c-fault-mgmt__list-content">
|
<div class="c-fault-mgmt-item c-fault-mgmt__list-content">
|
||||||
<div class="c-fault-mgmt-item c-fault-mgmt__list-pathname">
|
<div class="c-fault-mgmt-item c-fault-mgmt__list-pathname">
|
||||||
<div class="c-fault-mgmt__list-path">{{ fault.namespace }}</div>
|
<div class="c-fault-mgmt__list-path" aria-label="Fault namespace">
|
||||||
<div class="c-fault-mgmt__list-faultname">{{ fault.name }}</div>
|
{{ fault.namespace }}
|
||||||
|
</div>
|
||||||
|
<div class="c-fault-mgmt__list-faultname" aria-label="Fault name">{{ fault.name }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="c-fault-mgmt__list-content-right">
|
<div class="c-fault-mgmt__list-content-right">
|
||||||
<div class="c-fault-mgmt-item c-fault-mgmt__list-trigVal">
|
<div class="c-fault-mgmt-item c-fault-mgmt__list-trigVal">
|
||||||
<div class="c-fault-mgmt-item__value" :class="tripValueClassname" title="Trip Value">
|
<div
|
||||||
|
class="c-fault-mgmt-item__value"
|
||||||
|
:class="tripValueClassname"
|
||||||
|
title="Trip Value"
|
||||||
|
aria-label="Trip Value"
|
||||||
|
>
|
||||||
{{ fault.triggerValueInfo.value }}
|
{{ fault.triggerValueInfo.value }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="c-fault-mgmt-item c-fault-mgmt__list-curVal">
|
<div class="c-fault-mgmt-item c-fault-mgmt__list-curVal">
|
||||||
<div class="c-fault-mgmt-item__value" :class="liveValueClassname" title="Live Value">
|
<div
|
||||||
|
class="c-fault-mgmt-item__value"
|
||||||
|
:class="liveValueClassname"
|
||||||
|
title="Live Value"
|
||||||
|
aria-label="Live Value"
|
||||||
|
>
|
||||||
{{ fault.currentValueInfo.value }}
|
{{ fault.currentValueInfo.value }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="c-fault-mgmt-item c-fault-mgmt__list-trigTime">
|
<div class="c-fault-mgmt-item c-fault-mgmt__list-trigTime">
|
||||||
<div class="c-fault-mgmt-item__value" title="Last Trigger Time">
|
<div
|
||||||
|
class="c-fault-mgmt-item__value"
|
||||||
|
title="Last Trigger Time"
|
||||||
|
aria-label="Last Trigger Time"
|
||||||
|
>
|
||||||
{{ fault.triggerTime }}
|
{{ fault.triggerTime }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -98,7 +118,7 @@ export default {
|
|||||||
emits: ['acknowledge-selected', 'shelve-selected', 'toggle-selected', 'clear-all-selected'],
|
emits: ['acknowledge-selected', 'shelve-selected', 'toggle-selected', 'clear-all-selected'],
|
||||||
computed: {
|
computed: {
|
||||||
checkBoxAriaLabel() {
|
checkBoxAriaLabel() {
|
||||||
return `Select fault: ${this.fault.name}`;
|
return `Select fault: ${this.fault.name || 'Unknown'} in ${this.fault.namespace || 'Unknown'}`;
|
||||||
},
|
},
|
||||||
classesFromState() {
|
classesFromState() {
|
||||||
const exclusiveStates = [
|
const exclusiveStates = [
|
||||||
@ -165,6 +185,12 @@ export default {
|
|||||||
classname += SEVERITY_CLASS[triggerValueInfo.monitoringResult] || '';
|
classname += SEVERITY_CLASS[triggerValueInfo.monitoringResult] || '';
|
||||||
|
|
||||||
return classname.trim();
|
return classname.trim();
|
||||||
|
},
|
||||||
|
listItemAriaLabel() {
|
||||||
|
return `Fault triggered at ${this.fault.triggerTime || 'Unknown'} with severity ${this.fault.severity || 'Unknown'} in ${this.fault.namespace || 'Unknown'}`;
|
||||||
|
},
|
||||||
|
severityAriaLabel() {
|
||||||
|
return `Severity: ${this.fault.severity || 'Unknown'}`;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -68,7 +68,6 @@
|
|||||||
import {
|
import {
|
||||||
FAULT_MANAGEMENT_ALARMS,
|
FAULT_MANAGEMENT_ALARMS,
|
||||||
FAULT_MANAGEMENT_GLOBAL_ALARMS,
|
FAULT_MANAGEMENT_GLOBAL_ALARMS,
|
||||||
FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS,
|
|
||||||
FILTER_ITEMS,
|
FILTER_ITEMS,
|
||||||
SORT_ITEMS
|
SORT_ITEMS
|
||||||
} from './constants.js';
|
} from './constants.js';
|
||||||
@ -88,6 +87,13 @@ const SEARCH_KEYS = [
|
|||||||
'namespace'
|
'namespace'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Helper function for filtering faults
|
||||||
|
function filterFaultsByTerm(faults, searchTerm) {
|
||||||
|
return faults.filter((fault) =>
|
||||||
|
SEARCH_KEYS.some((key) => fault[key]?.toString().toLowerCase().includes(searchTerm))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
FaultManagementListHeader,
|
FaultManagementListHeader,
|
||||||
@ -111,23 +117,18 @@ export default {
|
|||||||
},
|
},
|
||||||
filteredFaultsList() {
|
filteredFaultsList() {
|
||||||
const filterName = FILTER_ITEMS[this.filterIndex];
|
const filterName = FILTER_ITEMS[this.filterIndex];
|
||||||
let list = this.faultsList;
|
let list = this.faultsList.filter((fault) =>
|
||||||
|
filterName === 'Shelved' ? fault.shelved : !fault.shelved
|
||||||
// Exclude shelved alarms from all views except the Shelved view
|
);
|
||||||
if (filterName !== 'Shelved') {
|
|
||||||
list = list.filter((fault) => fault.shelved !== true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filterName === 'Acknowledged') {
|
if (filterName === 'Acknowledged') {
|
||||||
list = list.filter((fault) => fault.acknowledged);
|
list = list.filter((fault) => fault.acknowledged);
|
||||||
} else if (filterName === 'Unacknowledged') {
|
} else if (filterName === 'Unacknowledged') {
|
||||||
list = list.filter((fault) => !fault.acknowledged);
|
list = list.filter((fault) => !fault.acknowledged);
|
||||||
} else if (filterName === 'Shelved') {
|
|
||||||
list = list.filter((fault) => fault.shelved);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.searchTerm.length > 0) {
|
if (this.searchTerm.length > 0) {
|
||||||
list = list.filter(this.filterUsingSearchTerm);
|
list = filterFaultsByTerm(list, this.searchTerm);
|
||||||
}
|
}
|
||||||
|
|
||||||
list.sort(SORT_ITEMS[this.sortBy].sortFunction);
|
list.sort(SORT_ITEMS[this.sortBy].sortFunction);
|
||||||
@ -138,6 +139,9 @@ export default {
|
|||||||
return this.openmct.faults.supportsActions();
|
return this.openmct.faults.supportsActions();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
created() {
|
||||||
|
this.shelveDurations = this.openmct.faults.getShelveDurations();
|
||||||
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.unsubscribe = this.openmct.faults.subscribe(this.domainObject, this.updateFault);
|
this.unsubscribe = this.openmct.faults.subscribe(this.domainObject, this.updateFault);
|
||||||
},
|
},
|
||||||
@ -158,14 +162,13 @@ export default {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateFaultList() {
|
async updateFaultList() {
|
||||||
this.openmct.faults.request(this.domainObject).then((faultsData) => {
|
const faultsData = await this.openmct.faults.request(this.domainObject);
|
||||||
if (faultsData?.length > 0) {
|
if (faultsData?.length > 0) {
|
||||||
this.faultsList = faultsData.map((fd) => fd.fault);
|
this.faultsList = faultsData.map((fd) => fd.fault);
|
||||||
} else {
|
} else {
|
||||||
this.faultsList = [];
|
this.faultsList = [];
|
||||||
}
|
}
|
||||||
});
|
|
||||||
},
|
},
|
||||||
filterUsingSearchTerm(fault) {
|
filterUsingSearchTerm(fault) {
|
||||||
if (!fault) {
|
if (!fault) {
|
||||||
@ -223,14 +226,29 @@ export default {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
async toggleAcknowledgeSelected(faults = this.selectedFaults) {
|
async toggleAcknowledgeSelected(faults = this.selectedFaults) {
|
||||||
let title = '';
|
const title = this.getAcknowledgeTitle(faults);
|
||||||
if (faults.length > 1) {
|
|
||||||
title = `Acknowledge ${faults.length} selected faults`;
|
|
||||||
} else if (faults.length === 1) {
|
|
||||||
title = `Acknowledge fault: ${faults[0].name}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const formStructure = {
|
const formStructure = this.getAcknowledgeFormStructure(title);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await this.openmct.forms.showForm(formStructure);
|
||||||
|
this.acknowledgeFaults(faults, data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
} finally {
|
||||||
|
this.resetSelectedFaultMap();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getAcknowledgeTitle(faults) {
|
||||||
|
if (faults.length > 1) {
|
||||||
|
return `Acknowledge ${faults.length} selected faults`;
|
||||||
|
} else if (faults.length === 1) {
|
||||||
|
return `Acknowledge fault: ${faults[0].name}`;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
getAcknowledgeFormStructure(title) {
|
||||||
|
return {
|
||||||
title,
|
title,
|
||||||
sections: [
|
sections: [
|
||||||
{
|
{
|
||||||
@ -253,17 +271,11 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
},
|
||||||
try {
|
acknowledgeFaults(faults, data) {
|
||||||
const data = await this.openmct.forms.showForm(formStructure);
|
faults.forEach((fault) => {
|
||||||
faults.forEach((fault) => {
|
this.openmct.faults.acknowledgeFault(fault, data);
|
||||||
this.openmct.faults.acknowledgeFault(fault, data);
|
});
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
} finally {
|
|
||||||
this.resetSelectedFaultMap();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
resetSelectedFaultMap() {
|
resetSelectedFaultMap() {
|
||||||
Object.keys(this.selectedFaultMap).forEach((key) => {
|
Object.keys(this.selectedFaultMap).forEach((key) => {
|
||||||
@ -273,7 +285,7 @@ export default {
|
|||||||
async toggleShelveSelected(faults = this.selectedFaults, shelveData = {}) {
|
async toggleShelveSelected(faults = this.selectedFaults, shelveData = {}) {
|
||||||
const { shelved = true } = shelveData;
|
const { shelved = true } = shelveData;
|
||||||
if (shelved) {
|
if (shelved) {
|
||||||
let title =
|
const title =
|
||||||
faults.length > 1
|
faults.length > 1
|
||||||
? `Shelve ${faults.length} selected faults`
|
? `Shelve ${faults.length} selected faults`
|
||||||
: `Shelve fault: ${faults[0].name}`;
|
: `Shelve fault: ${faults[0].name}`;
|
||||||
@ -295,10 +307,10 @@ export default {
|
|||||||
key: 'shelveDuration',
|
key: 'shelveDuration',
|
||||||
control: 'select',
|
control: 'select',
|
||||||
name: 'Shelve duration',
|
name: 'Shelve duration',
|
||||||
options: FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS,
|
options: this.shelveDurations,
|
||||||
required: false,
|
required: false,
|
||||||
cssClass: 'l-input-lg',
|
cssClass: 'l-input-lg',
|
||||||
value: FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS[0].value
|
value: this.shelveDurations[0].value
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -318,19 +330,16 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
shelveData.comment = data.comment || '';
|
shelveData.comment = data.comment || '';
|
||||||
shelveData.shelveDuration =
|
shelveData.shelveDuration = data.shelveDuration ?? this.shelveDurations[0].value;
|
||||||
data.shelveDuration !== undefined
|
|
||||||
? data.shelveDuration
|
|
||||||
: FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS[0].value;
|
|
||||||
} else {
|
} else {
|
||||||
shelveData = {
|
shelveData = {
|
||||||
shelved: false
|
shelved: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.values(faults).forEach((selectedFault) => {
|
await Promise.all(
|
||||||
this.openmct.faults.shelveFault(selectedFault, shelveData);
|
faults.map((selectedFault) => this.openmct.faults.shelveFault(selectedFault, shelveData))
|
||||||
});
|
);
|
||||||
|
|
||||||
this.selectedFaultMap = {};
|
this.selectedFaultMap = {};
|
||||||
},
|
},
|
||||||
|
@ -370,6 +370,7 @@ export default {
|
|||||||
createImageWrapper(index, image, showImagePlaceholders) {
|
createImageWrapper(index, image, showImagePlaceholders) {
|
||||||
const id = `${ID_PREFIX}${image.time}`;
|
const id = `${ID_PREFIX}${image.time}`;
|
||||||
let imageWrapper = document.createElement('div');
|
let imageWrapper = document.createElement('div');
|
||||||
|
imageWrapper.ariaLabel = id;
|
||||||
imageWrapper.classList.add(IMAGE_WRAPPER_CLASS);
|
imageWrapper.classList.add(IMAGE_WRAPPER_CLASS);
|
||||||
imageWrapper.style.left = `${this.xScale(image.time)}px`;
|
imageWrapper.style.left = `${this.xScale(image.time)}px`;
|
||||||
this.setNSAttributesForElement(imageWrapper, {
|
this.setNSAttributesForElement(imageWrapper, {
|
||||||
|
@ -620,7 +620,7 @@ export default {
|
|||||||
if (matchIndex > -1) {
|
if (matchIndex > -1) {
|
||||||
this.setFocusedImage(matchIndex);
|
this.setFocusedImage(matchIndex);
|
||||||
} else {
|
} else {
|
||||||
this.paused();
|
this.paused(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1082,7 +1082,7 @@ export default {
|
|||||||
paused(state) {
|
paused(state) {
|
||||||
this.isPaused = Boolean(state);
|
this.isPaused = Boolean(state);
|
||||||
|
|
||||||
if (!state) {
|
if (!this.isPaused) {
|
||||||
this.previousFocusedImage = null;
|
this.previousFocusedImage = null;
|
||||||
this.setFocusedImage(this.nextImageIndex);
|
this.setFocusedImage(this.nextImageIndex);
|
||||||
this.autoScroll = true;
|
this.autoScroll = true;
|
||||||
|
@ -90,16 +90,17 @@ export default {
|
|||||||
dataCleared() {
|
dataCleared() {
|
||||||
this.imageHistory = [];
|
this.imageHistory = [];
|
||||||
},
|
},
|
||||||
dataRemoved(dataToRemove) {
|
dataRemoved(removed) {
|
||||||
this.imageHistory = this.imageHistory.filter((existingDatum) => {
|
const removedTimestamps = {};
|
||||||
const shouldKeep = dataToRemove.some((datumToRemove) => {
|
removed.forEach((_removed) => {
|
||||||
const existingDatumTimestamp = this.parseTime(existingDatum);
|
const removedTimestamp = this.parseTime(_removed);
|
||||||
const datumToRemoveTimestamp = this.parseTime(datumToRemove);
|
removedTimestamps[removedTimestamp] = true;
|
||||||
|
});
|
||||||
|
|
||||||
return existingDatumTimestamp !== datumToRemoveTimestamp;
|
this.imageHistory = this.imageHistory.filter((image) => {
|
||||||
});
|
const imageTimestamp = this.parseTime(image);
|
||||||
|
|
||||||
return shouldKeep;
|
return !removedTimestamps[imageTimestamp];
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
setDataTimeContext() {
|
setDataTimeContext() {
|
||||||
|
@ -19,14 +19,23 @@
|
|||||||
* this source code distribution or the Licensing information page available
|
* this source code distribution or the Licensing information page available
|
||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
const PERFORMANCE_OVERLAY_RENDER_INTERVAL = 1000;
|
||||||
|
|
||||||
export default function PerformanceIndicator() {
|
export default function PerformanceIndicator() {
|
||||||
return function install(openmct) {
|
return function install(openmct) {
|
||||||
let frames = 0;
|
let frames = 0;
|
||||||
let lastCalculated = performance.now();
|
let lastCalculated = performance.now();
|
||||||
const indicator = openmct.indicators.simpleIndicator();
|
openmct.performance = {
|
||||||
|
measurements: new Map()
|
||||||
|
};
|
||||||
|
|
||||||
|
const indicator = openmct.indicators.simpleIndicator();
|
||||||
|
indicator.key = 'performance-indicator';
|
||||||
indicator.text('~ fps');
|
indicator.text('~ fps');
|
||||||
|
indicator.description('Performance Indicator');
|
||||||
indicator.statusClass('s-status-info');
|
indicator.statusClass('s-status-info');
|
||||||
|
indicator.on('click', showOverlay);
|
||||||
|
|
||||||
openmct.indicators.add(indicator);
|
openmct.indicators.add(indicator);
|
||||||
|
|
||||||
let rafHandle = requestAnimationFrame(incrementFrames);
|
let rafHandle = requestAnimationFrame(incrementFrames);
|
||||||
@ -58,5 +67,58 @@ export default function PerformanceIndicator() {
|
|||||||
indicator.statusClass('s-status-error');
|
indicator.statusClass('s-status-error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showOverlay() {
|
||||||
|
const overlayStylesText = `
|
||||||
|
#c-performance-indicator--overlay {
|
||||||
|
background-color:rgba(0,0,0,0.5);
|
||||||
|
position: absolute;
|
||||||
|
width: 300px;
|
||||||
|
left: calc(50% - 300px);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
const overlayMarkup = `
|
||||||
|
<div id="c-performance-indicator--overlay" title="Performance Overlay">
|
||||||
|
<table id="c-performance-indicator--table">
|
||||||
|
<tr class="c-performance-indicator--row"><td class="c-performance-indicator--measurement-name"></td><td class="c-performance-indicator--measurement-value"></td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
const overlayTemplate = document.createElement('div');
|
||||||
|
overlayTemplate.innerHTML = overlayMarkup;
|
||||||
|
const overlay = overlayTemplate.cloneNode(true);
|
||||||
|
overlay.querySelector('.c-performance-indicator--row').remove();
|
||||||
|
const overlayStyles = document.createElement('style');
|
||||||
|
overlayStyles.appendChild(document.createTextNode(overlayStylesText));
|
||||||
|
|
||||||
|
document.head.appendChild(overlayStyles);
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
|
||||||
|
indicator.off('click', showOverlay);
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
overlay.querySelector('#c-performance-indicator--table').innerHTML = '';
|
||||||
|
|
||||||
|
for (const [name, value] of openmct.performance.measurements.entries()) {
|
||||||
|
const newRow = overlayTemplate
|
||||||
|
.querySelector('.c-performance-indicator--row')
|
||||||
|
.cloneNode(true);
|
||||||
|
newRow.querySelector('.c-performance-indicator--measurement-name').innerText = name;
|
||||||
|
newRow.querySelector('.c-performance-indicator--measurement-value').innerText = value;
|
||||||
|
overlay.querySelector('#c-performance-indicator--table').appendChild(newRow);
|
||||||
|
}
|
||||||
|
}, PERFORMANCE_OVERLAY_RENDER_INTERVAL);
|
||||||
|
|
||||||
|
indicator.on(
|
||||||
|
'click',
|
||||||
|
() => {
|
||||||
|
overlayStyles.remove();
|
||||||
|
overlay.remove();
|
||||||
|
indicator.on('click', showOverlay);
|
||||||
|
clearInterval(interval);
|
||||||
|
},
|
||||||
|
{ once: true, capture: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -99,6 +99,7 @@ export default {
|
|||||||
this.composition.off('remove', this.removeItem);
|
this.composition.off('remove', this.removeItem);
|
||||||
this.composition.off('reorder', this.reorder);
|
this.composition.off('reorder', this.reorder);
|
||||||
this.stopFollowingTimeContext();
|
this.stopFollowingTimeContext();
|
||||||
|
this.handleContentResize.cancel();
|
||||||
this.contentResizeObserver.disconnect();
|
this.contentResizeObserver.disconnect();
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
@ -461,7 +461,7 @@ $colorGaugeMeterTextValue: $colorGaugeTextValue; // Meter text value, overlaid o
|
|||||||
$colorGaugeRange: $colorBodyFg; // Range text color
|
$colorGaugeRange: $colorBodyFg; // Range text color
|
||||||
$colorGaugeLimitHigh: rgba($colorLimitRedBg, 0.4);
|
$colorGaugeLimitHigh: rgba($colorLimitRedBg, 0.4);
|
||||||
$colorGaugeLimitLow: $colorGaugeLimitHigh;
|
$colorGaugeLimitLow: $colorGaugeLimitHigh;
|
||||||
$colorGaugeNeedle: rgba(#fff, 0);
|
$colorGaugeNeedle: $colorGaugeValue; // Color of needle in a needle gauge.
|
||||||
$transitionTimeGauge: 150ms; // CSS transition time used to smooth needle gauge and meter value transitions
|
$transitionTimeGauge: 150ms; // CSS transition time used to smooth needle gauge and meter value transitions
|
||||||
$marginGaugeMeterValue: 10%; // Margin between meter value bar and bounds edges
|
$marginGaugeMeterValue: 10%; // Margin between meter value bar and bounds edges
|
||||||
$gaugeMeterValueShadow: rgba(255, 255, 255, 0);
|
$gaugeMeterValueShadow: rgba(255, 255, 255, 0);
|
||||||
|
@ -477,7 +477,7 @@ $colorGaugeMeterTextValue: $colorGaugeTextValue; // Meter text value, overlaid o
|
|||||||
$colorGaugeRange: $colorBodyFg; // Range text color
|
$colorGaugeRange: $colorBodyFg; // Range text color
|
||||||
$colorGaugeLimitHigh: rgba($colorLimitRedBg, 0.4);
|
$colorGaugeLimitHigh: rgba($colorLimitRedBg, 0.4);
|
||||||
$colorGaugeLimitLow: $colorGaugeLimitHigh;
|
$colorGaugeLimitLow: $colorGaugeLimitHigh;
|
||||||
$colorGaugeNeedle: rgba(#fff, 0);
|
$colorGaugeNeedle: $colorGaugeValue; // Color of needle in a needle gauge.
|
||||||
$transitionTimeGauge: 150ms; // CSS transition time used to smooth needle gauge and meter value transitions
|
$transitionTimeGauge: 150ms; // CSS transition time used to smooth needle gauge and meter value transitions
|
||||||
$marginGaugeMeterValue: 10%; // Margin between meter value bar and bounds edges
|
$marginGaugeMeterValue: 10%; // Margin between meter value bar and bounds edges
|
||||||
$gaugeMeterValueShadow: rgba(255, 255, 255, 0);
|
$gaugeMeterValueShadow: rgba(255, 255, 255, 0);
|
||||||
|
@ -460,7 +460,7 @@ $colorGaugeMeterTextValue: $colorGaugeTextValue; // Meter text value, overlaid o
|
|||||||
$colorGaugeRange: $colorBodyFg; // Range text color
|
$colorGaugeRange: $colorBodyFg; // Range text color
|
||||||
$colorGaugeLimitHigh: rgba($colorLimitRedBg, 0.2);
|
$colorGaugeLimitHigh: rgba($colorLimitRedBg, 0.2);
|
||||||
$colorGaugeLimitLow: $colorGaugeLimitHigh;
|
$colorGaugeLimitLow: $colorGaugeLimitHigh;
|
||||||
$colorGaugeNeedle: rgba(#fff, 0);
|
$colorGaugeNeedle: $colorGaugeValue; // Color of needle in a needle gauge.
|
||||||
$transitionTimeGauge: 150ms; // CSS transition time used to smooth needle gauge and meter value transitions
|
$transitionTimeGauge: 150ms; // CSS transition time used to smooth needle gauge and meter value transitions
|
||||||
$marginGaugeMeterValue: 10%; // Margin between meter value bar and bounds edges
|
$marginGaugeMeterValue: 10%; // Margin between meter value bar and bounds edges
|
||||||
$gaugeMeterValueShadow: rgba(255, 255, 255, 0);
|
$gaugeMeterValueShadow: rgba(255, 255, 255, 0);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user