mirror of
https://github.com/nasa/openmct.git
synced 2025-03-14 16:26:50 +00:00
Compare commits
43 Commits
omm-r5.3.0
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
28b5d7c41c | ||
|
ecd120387c | ||
|
a6517bb33e | ||
|
1fde0d9e38 | ||
|
5be103ea72 | ||
|
d74e1b19b6 | ||
|
5bb6a18cd4 | ||
|
14b947c101 | ||
|
61b982ab99 | ||
|
ba4d8a428b | ||
|
ea9947cab5 | ||
|
2010f2e377 | ||
|
3241e9ba57 | ||
|
057a5f997c | ||
|
078cd341a5 | ||
|
518b55cf0f | ||
|
3e23dceb64 | ||
|
7f8b5e09e5 | ||
|
7c2bb16bfd | ||
|
890ddcac4e | ||
|
d8c5095ebb | ||
|
ccf7ed91af | ||
|
2b8673941a | ||
|
703186adf1 | ||
|
c43ef64733 | ||
|
f4cf9c756b | ||
|
4415fe7952 | ||
|
83e4a124e2 | ||
|
47f0b66c7e | ||
|
55c023d1eb | ||
|
37b2660f27 | ||
|
43cc963328 | ||
|
ad30a0e2d0 | ||
|
29f1956d1a | ||
|
c498f7d20c | ||
|
a8fbabe695 | ||
|
e792403788 | ||
|
de122b91c2 | ||
|
fa8efa858b | ||
|
26578e849d | ||
|
fccae3bd49 | ||
|
440474b2e3 | ||
|
21a4335c4e |
@ -5,7 +5,7 @@ orbs:
|
||||
executors:
|
||||
pw-focal-development:
|
||||
docker:
|
||||
- image: mcr.microsoft.com/playwright:v1.45.2-focal
|
||||
- image: mcr.microsoft.com/playwright:v1.48.1-focal
|
||||
environment:
|
||||
NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed
|
||||
PERCY_POSTINSTALL_BROWSER: 'true' # Needed to store the percy browser in cache deps
|
||||
@ -37,6 +37,29 @@ commands:
|
||||
ls -latR >> /tmp/artifacts/dir.txt
|
||||
- store_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:
|
||||
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:
|
||||
@ -44,7 +67,15 @@ commands:
|
||||
type: string
|
||||
steps:
|
||||
- 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:
|
||||
npm-audit:
|
||||
parameters:
|
||||
@ -81,7 +112,15 @@ jobs:
|
||||
mkdir -p dist/reports/tests/
|
||||
TESTFILES=$(circleci tests glob "src/**/*Spec.js")
|
||||
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:
|
||||
path: dist/reports/tests/
|
||||
- store_artifacts:
|
||||
@ -96,7 +135,7 @@ jobs:
|
||||
suite: #ci or full
|
||||
type: string
|
||||
executor: pw-focal-development
|
||||
parallelism: 7
|
||||
parallelism: 8
|
||||
steps:
|
||||
- build_and_install:
|
||||
node-version: lts/hydrogen
|
||||
@ -159,7 +198,7 @@ jobs:
|
||||
steps:
|
||||
- build_and_install:
|
||||
node-version: lts/hydrogen
|
||||
- run: npx playwright@1.45.2 install #Necessary for bare ubuntu machine
|
||||
- run: npx playwright@1.48.1 install #Necessary for bare ubuntu machine
|
||||
- run: |
|
||||
export $(cat src/plugins/persistence/couch/.env.ci | xargs)
|
||||
docker compose -f src/plugins/persistence/couch/couchdb-compose.yaml up --detach
|
||||
@ -247,8 +286,8 @@ workflows:
|
||||
overall-circleci-commit-status: #These jobs run on every commit
|
||||
jobs:
|
||||
- lint:
|
||||
name: node20-lint
|
||||
node-version: lts/iron
|
||||
name: node22-lint
|
||||
node-version: '22'
|
||||
- unit-test:
|
||||
name: node18-chrome
|
||||
node-version: lts/hydrogen
|
||||
@ -259,12 +298,14 @@ workflows:
|
||||
- visual-a11y:
|
||||
name: visual-a11y-ci
|
||||
suite: ci
|
||||
- perf-test
|
||||
- mem-test
|
||||
|
||||
the-nightly: #These jobs do not run on PRs, but against master at night
|
||||
jobs:
|
||||
- unit-test:
|
||||
name: node20-chrome-nightly
|
||||
node-version: lts/iron
|
||||
name: node22-chrome-nightly
|
||||
node-version: '22'
|
||||
- unit-test:
|
||||
name: node18-chrome
|
||||
node-version: lts/hydrogen
|
||||
|
@ -483,7 +483,8 @@
|
||||
"countup",
|
||||
"darkmatter",
|
||||
"Undeletes",
|
||||
"SSSZ"
|
||||
"SSSZ",
|
||||
"pageerror"
|
||||
],
|
||||
"dictionaries": ["npm", "softwareTerms", "node", "html", "css", "bash", "en_US", "en-gb", "misc"],
|
||||
"ignorePaths": [
|
||||
|
23
.github/workflows/e2e-couchdb.yml
vendored
23
.github/workflows/e2e-couchdb.yml
vendored
@ -37,7 +37,7 @@ jobs:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- run: npx playwright@1.45.2 install
|
||||
- run: npx playwright@1.48.1 install
|
||||
|
||||
- name: Start CouchDB Docker Container and Init with Setup Scripts
|
||||
run: |
|
||||
@ -52,22 +52,33 @@ jobs:
|
||||
COMMIT_INFO_SHA: ${{github.event.pull_request.head.sha }}
|
||||
run: npm run test:e2e:couchdb
|
||||
|
||||
- name: Generate Code Coverage Report
|
||||
run: npm run cov:e2e:report
|
||||
|
||||
- name: Publish Results to Codecov.io
|
||||
env:
|
||||
SUPER_SECRET: ${{ secrets.CODECOV_TOKEN }}
|
||||
run: npm run cov:e2e:full:publish
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./coverage/e2e/lcov.info
|
||||
flags: e2e-full
|
||||
fail_ci_if_error: true
|
||||
verbose: true
|
||||
|
||||
- name: Archive test results
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: e2e-couchdb-test-results
|
||||
path: test-results
|
||||
overwrite: true
|
||||
|
||||
- name: Archive html test results
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: e2e-couchdb-html-test-results
|
||||
path: html-test-results
|
||||
overwrite: true
|
||||
|
||||
- name: Remove pr:e2e:couchdb label (if present)
|
||||
if: always()
|
||||
|
6
.github/workflows/e2e-flakefinder.yml
vendored
6
.github/workflows/e2e-flakefinder.yml
vendored
@ -30,7 +30,7 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
|
||||
- run: npx playwright@1.45.2 install
|
||||
- run: npx playwright@1.48.1 install
|
||||
- run: npm ci --no-audit --progress=false
|
||||
|
||||
- name: Run E2E Tests (Repeated 10 Times)
|
||||
@ -38,9 +38,11 @@ jobs:
|
||||
|
||||
- name: Archive test results
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: e2e-flakefinder-test-results
|
||||
path: test-results
|
||||
overwrite: true
|
||||
|
||||
- name: Remove pr:e2e:flakefinder label (if present)
|
||||
if: always()
|
||||
|
6
.github/workflows/e2e-perf.yml
vendored
6
.github/workflows/e2e-perf.yml
vendored
@ -28,16 +28,18 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
|
||||
- run: npx playwright@1.45.2 install
|
||||
- run: npx playwright@1.48.1 install
|
||||
- run: npm ci --no-audit --progress=false
|
||||
- run: npm run test:perf:localhost
|
||||
- run: npm run test:perf:contract
|
||||
- run: npm run test:perf:memory
|
||||
- name: Archive test results
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: e2e-perf-test-results
|
||||
path: test-results
|
||||
overwrite: true
|
||||
|
||||
- name: Remove pr:e2e:perf label (if present)
|
||||
if: always()
|
||||
|
6
.github/workflows/e2e-pr.yml
vendored
6
.github/workflows/e2e-pr.yml
vendored
@ -33,7 +33,7 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
|
||||
- run: npx playwright@1.45.2 install
|
||||
- run: npx playwright@1.47.2 install
|
||||
- run: npx playwright install chrome-beta
|
||||
- run: npm ci --no-audit --progress=false
|
||||
- run: npm run test:e2e:full -- --max-failures=40
|
||||
@ -45,9 +45,11 @@ jobs:
|
||||
npm run cov:e2e:full:publish
|
||||
- name: Archive test results
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: e2e-pr-test-results
|
||||
path: test-results
|
||||
overwrite: true
|
||||
|
||||
- name: Remove pr:e2e label (if present)
|
||||
if: always()
|
||||
|
@ -20,6 +20,19 @@ module.exports = {
|
||||
'playwright/no-get-by-title': 'error',
|
||||
'playwright/prefer-comparison-matcher': 'error'
|
||||
}
|
||||
},
|
||||
{
|
||||
// Disable no-raw-locators for .contract.perf.spec.js files until https://github.com/grafana/xk6-browser/issues/1226
|
||||
files: ['**/*.contract.perf.spec.js'],
|
||||
rules: {
|
||||
'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
|
||||
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
|
||||
|
||||
|
@ -227,6 +227,37 @@ async function createExampleTelemetryObject(page, parent = 'mine') {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Stable State Telemetry Object (State Generator) for use in visual tests
|
||||
* and tests against plotting telemetry (e.g. logPlot tests). This will change state every 2 seconds.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string | import('../src/api/objects/ObjectAPI').Identifier} [parent] the uuid or identifier of the parent object. Defaults to 'mine'
|
||||
* @returns {Promise<CreatedObjectInfo>} An object containing information about the telemetry object.
|
||||
*/
|
||||
async function createStableStateTelemetry(page, parent = 'mine') {
|
||||
const parentUrl = await getHashUrlToDomainObject(page, parent);
|
||||
|
||||
await page.goto(`${parentUrl}`);
|
||||
const createdObject = await createDomainObjectWithDefaults(page, {
|
||||
type: 'State Generator',
|
||||
name: 'Stable State Generator'
|
||||
});
|
||||
// edit the state generator to have a 1 second update rate
|
||||
await page.getByLabel('More actions').click();
|
||||
await page.getByRole('menuitem', { name: 'Edit Properties...' }).click();
|
||||
await page.getByLabel('State Duration (seconds)', { exact: true }).fill('2');
|
||||
await page.getByLabel('Save').click();
|
||||
// Wait until the URL is updated
|
||||
const uuid = await getFocusedObjectUuid(page);
|
||||
const url = await getHashUrlToDomainObject(page, uuid);
|
||||
|
||||
return {
|
||||
name: createdObject.name,
|
||||
uuid,
|
||||
url
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates directly to a given object url, in fixed time mode, with the given start and end bounds. Note: does not set
|
||||
* default view type.
|
||||
@ -479,6 +510,10 @@ async function setTimeConductorBounds(page, { submitChanges = true, ...bounds })
|
||||
// Open the time conductor popup
|
||||
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
|
||||
|
||||
// FIXME: https://github.com/nasa/openmct/pull/7818
|
||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
if (startDate) {
|
||||
await page.getByLabel('Start date').fill(startDate);
|
||||
}
|
||||
@ -629,15 +664,51 @@ async function getCanvasPixels(page, canvasSelector) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for telemetry and link it to an object. objectName should come from the domainObject.name function.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} parameterName
|
||||
* @param {string} objectName
|
||||
*/
|
||||
async function linkParameterToObject(page, parameterName, objectName) {
|
||||
await page.getByRole('searchbox', { name: 'Search Input' }).click();
|
||||
await page.getByRole('searchbox', { name: 'Search Input' }).fill(parameterName);
|
||||
await page.getByLabel('Object Results').getByText(parameterName).click();
|
||||
await page.getByLabel('More actions').click();
|
||||
await page.getByLabel('Create Link').click();
|
||||
await page.getByLabel('Modal Overlay').getByLabel('Search Input').click();
|
||||
await page.getByLabel('Modal Overlay').getByLabel('Search Input').fill(objectName);
|
||||
await page.getByLabel('Modal Overlay').getByLabel(`Navigate to ${objectName}`).click();
|
||||
await page.getByLabel('Save').click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename the currently viewed `domainObject` from the browse bar.
|
||||
*
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} newName
|
||||
*/
|
||||
async function renameCurrentObjectFromBrowseBar(page, newName) {
|
||||
const nameInput = page.getByLabel('Browse bar object name');
|
||||
await nameInput.click();
|
||||
await nameInput.fill('');
|
||||
await nameInput.fill(newName);
|
||||
// Click the browse bar container to save changes
|
||||
await page.getByLabel('Browse bar', { exact: true }).click();
|
||||
}
|
||||
|
||||
export {
|
||||
createDomainObjectWithDefaults,
|
||||
createExampleTelemetryObject,
|
||||
createNotification,
|
||||
createPlanFromJSON,
|
||||
createStableStateTelemetry,
|
||||
expandEntireTree,
|
||||
getCanvasPixels,
|
||||
linkParameterToObject,
|
||||
navigateToObjectWithFixedTimeBounds,
|
||||
navigateToObjectWithRealTime,
|
||||
renameCurrentObjectFromBrowseBar,
|
||||
setEndOffset,
|
||||
setFixedIndependentTimeConductorBounds,
|
||||
setFixedTimeMode,
|
||||
|
@ -25,6 +25,7 @@ import { expect } from '../pluginFixtures.js';
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function navigateToFaultManagementWithExample(page) {
|
||||
await page.addInitScript({
|
||||
@ -36,6 +37,7 @@ export async function navigateToFaultManagementWithExample(page) {
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function navigateToFaultManagementWithStaticExample(page) {
|
||||
await page.addInitScript({
|
||||
@ -47,6 +49,7 @@ export async function navigateToFaultManagementWithStaticExample(page) {
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function navigateToFaultManagementWithoutExample(page) {
|
||||
await page.addInitScript({
|
||||
@ -58,6 +61,7 @@ export async function navigateToFaultManagementWithoutExample(page) {
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function navigateToFaultItemInTree(page) {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
@ -77,6 +81,8 @@ async function navigateToFaultItemInTree(page) {
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {number} rowNumber
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function acknowledgeFault(page, rowNumber) {
|
||||
await openFaultRowMenu(page, rowNumber);
|
||||
@ -86,6 +92,8 @@ export async function acknowledgeFault(page, rowNumber) {
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {...number} nums
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function shelveMultipleFaults(page, ...nums) {
|
||||
const selectRows = nums.map((num) => {
|
||||
@ -99,6 +107,8 @@ export async function shelveMultipleFaults(page, ...nums) {
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {...number} nums
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function acknowledgeMultipleFaults(page, ...nums) {
|
||||
const selectRows = nums.map((num) => {
|
||||
@ -106,50 +116,43 @@ export async function acknowledgeMultipleFaults(page, ...nums) {
|
||||
});
|
||||
await Promise.all(selectRows);
|
||||
|
||||
await page.locator('button:has-text("Acknowledge")').click();
|
||||
await page.getByLabel('Acknowledge selected faults').click();
|
||||
await page.getByLabel('Save').click();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {number} rowNumber
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function shelveFault(page, rowNumber) {
|
||||
await openFaultRowMenu(page, rowNumber);
|
||||
await page.locator('.c-menu >> text="Shelve"').click();
|
||||
// Click [aria-label="Save"]
|
||||
await page.getByLabel('Shelve', { exact: true }).click();
|
||||
await page.getByLabel('Save').click();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
export async function changeViewTo(page, view) {
|
||||
await page.locator('.c-fault-mgmt__search-row select').first().selectOption(view);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {'severity' | 'newest-first' | 'oldest-first'} sort
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
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 {'acknowledged' | 'shelved' | 'standard view'} view
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function enterSearchTerm(page, term) {
|
||||
await page.locator('.c-fault-mgmt-search [aria-label="Search Input"]').fill(term);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
export async function clearSearch(page) {
|
||||
await enterSearchTerm(page, '');
|
||||
export async function changeViewTo(page, view) {
|
||||
await page.getByTitle('View Filter').getByRole('combobox').selectOption(view);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {number} rowNumber
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function selectFaultItem(page, rowNumber) {
|
||||
await page
|
||||
@ -165,71 +168,37 @@ export async function selectFaultItem(page, rowNumber) {
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
export async function getHighestSeverity(page) {
|
||||
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
|
||||
* @param {number} rowNumber
|
||||
* @returns {import('@playwright/test').Locator}
|
||||
*/
|
||||
export function getFault(page, rowNumber) {
|
||||
const fault = page.locator(
|
||||
`.c-faults-list-view-item-body > .c-fault-mgmt__list >> nth=${rowNumber - 1}`
|
||||
);
|
||||
const fault = page.getByLabel('Fault triggered at').nth(rowNumber - 1);
|
||||
|
||||
return fault;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} name
|
||||
* @returns {import('@playwright/test').Locator}
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {number} rowNumber
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
export async function getFaultName(page, rowNumber) {
|
||||
const faultName = await page
|
||||
.locator(`.c-fault-mgmt__list-faultname >> nth=${rowNumber - 1}`)
|
||||
.getByLabel('Fault name', { exact: true })
|
||||
.nth(rowNumber - 1)
|
||||
.textContent();
|
||||
|
||||
return faultName;
|
||||
@ -237,21 +206,13 @@ export async function getFaultName(page, rowNumber) {
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
export async function getFaultSeverity(page, rowNumber) {
|
||||
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
|
||||
* @param {number} rowNumber
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
export async function getFaultNamespace(page, rowNumber) {
|
||||
const faultNamespace = await page
|
||||
.locator(`.c-fault-mgmt__list-path >> nth=${rowNumber - 1}`)
|
||||
.getByLabel('Fault namespace')
|
||||
.nth(rowNumber - 1)
|
||||
.textContent();
|
||||
|
||||
return faultNamespace;
|
||||
@ -259,10 +220,13 @@ export async function getFaultNamespace(page, rowNumber) {
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {number} rowNumber
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
export async function getFaultTriggerTime(page, rowNumber) {
|
||||
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();
|
||||
|
||||
return faultTriggerTime.toString().trim();
|
||||
@ -270,11 +234,14 @@ export async function getFaultTriggerTime(page, rowNumber) {
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {number} rowNumber
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function openFaultRowMenu(page, rowNumber) {
|
||||
// select
|
||||
await page
|
||||
.getByLabel('Disposition actions')
|
||||
.getByLabel('Fault triggered at')
|
||||
.nth(rowNumber - 1)
|
||||
.getByLabel('Disposition Actions')
|
||||
.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
|
||||
};
|
@ -129,6 +129,7 @@ export async function setBoundsToSpanAllActivities(page, planJson, planObjectUrl
|
||||
*/
|
||||
export function getEarliestStartTime(planJson) {
|
||||
const activities = Object.values(planJson).flat();
|
||||
|
||||
return Math.min(...activities.map((activity) => activity.start));
|
||||
}
|
||||
|
||||
@ -139,6 +140,7 @@ export function getEarliestStartTime(planJson) {
|
||||
*/
|
||||
export function getLatestEndTime(planJson) {
|
||||
const activities = Object.values(planJson).flat();
|
||||
|
||||
return Math.max(...activities.map((activity) => activity.end));
|
||||
}
|
||||
|
||||
@ -151,6 +153,7 @@ export function getFirstActivity(planJson) {
|
||||
const groups = Object.keys(planJson);
|
||||
const firstGroupKey = groups[0];
|
||||
const firstGroupItems = planJson[firstGroupKey];
|
||||
|
||||
return firstGroupItems[0];
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openmct-e2e",
|
||||
"version": "4.0.0-next",
|
||||
"version": "4.1.0-next",
|
||||
"description": "The Open MCT e2e framework",
|
||||
"type": "module",
|
||||
"module": "index.js",
|
||||
@ -16,7 +16,7 @@
|
||||
"devDependencies": {
|
||||
"@percy/cli": "1.27.4",
|
||||
"@percy/playwright": "1.0.4",
|
||||
"@playwright/test": "1.45.2",
|
||||
"@playwright/test": "1.48.1",
|
||||
"@axe-core/playwright": "4.8.5"
|
||||
},
|
||||
"author": {
|
||||
|
22
e2e/test-data/condition_set_storage.json
Normal file
22
e2e/test-data/condition_set_storage.json
Normal file
File diff suppressed because one or more lines are too long
BIN
e2e/test-data/rick space roll.jpg
Normal file
BIN
e2e/test-data/rick space roll.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
@ -26,8 +26,10 @@ import {
|
||||
createExampleTelemetryObject,
|
||||
createNotification,
|
||||
createPlanFromJSON,
|
||||
createStableStateTelemetry,
|
||||
expandEntireTree,
|
||||
getCanvasPixels,
|
||||
linkParameterToObject,
|
||||
navigateToObjectWithFixedTimeBounds,
|
||||
navigateToObjectWithRealTime,
|
||||
setEndOffset,
|
||||
@ -339,4 +341,23 @@ test.describe('AppActions @framework', () => {
|
||||
// Expect this step to fail
|
||||
await waitForPlotsToRender(page, { timeout: 1000 });
|
||||
});
|
||||
test('createStableStateTelemetry', async ({ page }) => {
|
||||
const stableStateTelemetry = await createStableStateTelemetry(page);
|
||||
expect(stableStateTelemetry.name).toBe('Stable State Generator');
|
||||
expect(stableStateTelemetry.url).toBe(`./#/browse/mine/${stableStateTelemetry.uuid}`);
|
||||
expect(stableStateTelemetry.uuid).toBeDefined();
|
||||
});
|
||||
test('linkParameterToObject', async ({ page }) => {
|
||||
const displayLayout = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Display Layout',
|
||||
name: 'Test Display Layout'
|
||||
});
|
||||
const exampleTelemetry = await createExampleTelemetryObject(page);
|
||||
|
||||
await linkParameterToObject(page, exampleTelemetry.name, displayLayout.name);
|
||||
await page.goto(displayLayout.url);
|
||||
await expect(page.getByRole('main').getByText('Test Display Layout')).toBeVisible();
|
||||
await expandEntireTree(page);
|
||||
await expect(page.getByLabel('Navigate to VIPER Rover').first()).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
@ -286,6 +286,55 @@ test.describe('Generate Visual Test Data @localStorage @generatedata @clock', ()
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Generate Conditional Styling Data @localStorage @generatedata', () => {
|
||||
test('Generate basic condition set', async ({ page, context }) => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
// Create a Condition Set
|
||||
const conditionSet = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Condition Set',
|
||||
name: 'Test Condition Set'
|
||||
});
|
||||
|
||||
// Create a Telemetry Object (Sine Wave Generator)
|
||||
const swg = await createExampleTelemetryObject(page, conditionSet.uuid);
|
||||
|
||||
// Edit the Telemetry Object to have a 10hz data rate (Gotta go fast!)
|
||||
await page.goto(swg.url);
|
||||
await page.getByLabel('More actions').click();
|
||||
await page.getByRole('menuitem', { name: 'Edit Properties...' }).click();
|
||||
await page.getByLabel('Period', { exact: true }).fill('5');
|
||||
await page.getByLabel('Save').click();
|
||||
|
||||
// Edit the Condition Set
|
||||
await page.goto(conditionSet.url);
|
||||
await page.getByLabel('Edit Object').click();
|
||||
|
||||
// Add a Condition to the Condition Set
|
||||
await page.getByLabel('Add Condition').click();
|
||||
await page.getByLabel('Condition Name Input').first().fill('Test Condition');
|
||||
await page.getByLabel('Condition Output Type').first().selectOption('String');
|
||||
await page.getByLabel('Condition Output String').first().fill('Test Condition Met');
|
||||
|
||||
// Condition: True if sine value > 0 (half the time)
|
||||
await page.getByLabel('Criterion Telemetry Selection').selectOption(swg.name);
|
||||
await page.getByLabel('Criterion Metadata Selection').selectOption('Sine');
|
||||
await page.getByLabel('Criterion Comparison Selection').selectOption('is greater than');
|
||||
await page.getByLabel('Criterion Input').first().fill('0');
|
||||
|
||||
// Rename default condition
|
||||
await page.getByLabel('Condition Output String').nth(1).fill('Test Condition Unmet');
|
||||
await page.getByLabel('Save').click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
// Save localStorage for future test execution
|
||||
await context.storageState({
|
||||
path: fileURLToPath(
|
||||
new URL('../../../e2e/test-data/condition_set_storage.json', import.meta.url)
|
||||
)
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Validate Overlay Plot with Telemetry Object @localStorage @generatedata', () => {
|
||||
test.use({
|
||||
storageState: fileURLToPath(
|
||||
|
@ -0,0 +1,67 @@
|
||||
/*****************************************************************************
|
||||
* 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 { createDomainObjectWithDefaults, setRealTimeMode } from '../../../appActions.js';
|
||||
import { MISSION_TIME } from '../../../constants.js';
|
||||
import { expect, test } from '../../../pluginFixtures.js';
|
||||
|
||||
const TELEMETRY_RATE = 2500;
|
||||
|
||||
test.describe('Example Event Generator Acknowledge with Controlled Clock @clock', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.clock.install({ time: MISSION_TIME });
|
||||
await page.clock.resume();
|
||||
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
await setRealTimeMode(page);
|
||||
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Event Message Generator with Acknowledge'
|
||||
});
|
||||
});
|
||||
|
||||
test('Rows are updatable in place', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/7938'
|
||||
});
|
||||
|
||||
await test.step('First telemetry datum gets added as new row', async () => {
|
||||
await page.clock.fastForward(TELEMETRY_RATE);
|
||||
const rows = page.getByLabel('table content').getByLabel('Table Row');
|
||||
const acknowledgeCell = rows.first().getByLabel('acknowledge table cell');
|
||||
|
||||
await expect(rows).toHaveCount(1);
|
||||
await expect(acknowledgeCell).not.toHaveAttribute('title', 'OK');
|
||||
});
|
||||
|
||||
await test.step('Incoming Telemetry datum matching an existing rows in place update key has data merged to existing row', async () => {
|
||||
await page.clock.fastForward(TELEMETRY_RATE * 2);
|
||||
const rows = page.getByLabel('table content').getByLabel('Table Row');
|
||||
const acknowledgeCell = rows.first().getByLabel('acknowledge table cell');
|
||||
|
||||
await expect(rows).toHaveCount(1);
|
||||
await expect(acknowledgeCell).toHaveAttribute('title', 'OK');
|
||||
});
|
||||
});
|
||||
});
|
@ -130,7 +130,7 @@ test.describe('Persistence operations @addInit', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Persistence operations @couchdb', () => {
|
||||
test.describe('Persistence operations @couchdb @network', () => {
|
||||
test.use({ failOnConsoleError: false });
|
||||
test('Editing object properties should generate a single persistence operation', async ({
|
||||
page
|
||||
@ -170,7 +170,7 @@ test.describe('Persistence operations @couchdb', () => {
|
||||
})
|
||||
.toEqual(1);
|
||||
});
|
||||
test('Can create an object after a conflict error @couchdb @2p', async ({
|
||||
test('Can create an object after a conflict error @couchdb @network @2p', async ({
|
||||
page,
|
||||
openmctConfig
|
||||
}) => {
|
||||
|
@ -24,7 +24,9 @@ import {
|
||||
createDomainObjectWithDefaults,
|
||||
createPlanFromJSON,
|
||||
navigateToObjectWithFixedTimeBounds,
|
||||
setFixedIndependentTimeConductorBounds
|
||||
setFixedIndependentTimeConductorBounds,
|
||||
setFixedTimeMode,
|
||||
setTimeConductorBounds
|
||||
} from '../../../appActions.js';
|
||||
import { expect, test } from '../../../pluginFixtures.js';
|
||||
|
||||
@ -74,21 +76,14 @@ const testPlan = {
|
||||
};
|
||||
|
||||
test.describe('Time Strip', () => {
|
||||
test('Create two Time Strips, add a single Plan to both, and verify they can have separate Independent Time Contexts', async ({
|
||||
page
|
||||
}) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/5627'
|
||||
});
|
||||
|
||||
// Constant locators
|
||||
const activityBounds = page.locator('.activity-bounds');
|
||||
let timestrip;
|
||||
let plan;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Goto baseURL
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
const timestrip = await test.step('Create a Time Strip', async () => {
|
||||
timestrip = await test.step('Create a Time Strip', async () => {
|
||||
const createdTimeStrip = await createDomainObjectWithDefaults(page, { type: 'Time Strip' });
|
||||
const objectName = await page.locator('.l-browse-bar__object-name').innerText();
|
||||
expect(objectName).toBe(createdTimeStrip.name);
|
||||
@ -96,7 +91,7 @@ test.describe('Time Strip', () => {
|
||||
return createdTimeStrip;
|
||||
});
|
||||
|
||||
const plan = await test.step('Create a Plan and add it to the timestrip', async () => {
|
||||
plan = await test.step('Create a Plan and add it to the timestrip', async () => {
|
||||
const createdPlan = await createPlanFromJSON(page, {
|
||||
name: 'Test Plan',
|
||||
json: testPlan
|
||||
@ -110,6 +105,22 @@ test.describe('Time Strip', () => {
|
||||
.dragTo(page.getByLabel('Object View'));
|
||||
await page.getByLabel('Save').click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
return createdPlan;
|
||||
});
|
||||
});
|
||||
test('Create two Time Strips, add a single Plan to both, and verify they can have separate Independent Time Contexts', async ({
|
||||
page
|
||||
}) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/5627'
|
||||
});
|
||||
|
||||
// Constant locators
|
||||
const activityBounds = page.locator('.activity-bounds');
|
||||
|
||||
await test.step('Set time strip to fixed timespan mode and verify activities', async () => {
|
||||
const startBound = testPlan.TEST_GROUP[0].start;
|
||||
const endBound = testPlan.TEST_GROUP[testPlan.TEST_GROUP.length - 1].end;
|
||||
|
||||
@ -119,8 +130,6 @@ test.describe('Time Strip', () => {
|
||||
// Verify all events are displayed
|
||||
const eventCount = await page.locator('.activity-bounds').count();
|
||||
expect(eventCount).toEqual(testPlan.TEST_GROUP.length);
|
||||
|
||||
return createdPlan;
|
||||
});
|
||||
|
||||
await test.step('TimeStrip can use the Independent Time Conductor', async () => {
|
||||
@ -177,4 +186,48 @@ test.describe('Time Strip', () => {
|
||||
expect(await activityBounds.count()).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
test('Time strip now line', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/7817'
|
||||
});
|
||||
|
||||
await test.step('Is displayed in realtime mode', async () => {
|
||||
await expect(page.getByLabel('Now Marker')).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Is hidden when out of bounds of the time axis', async () => {
|
||||
// Switch to fixed timespan mode
|
||||
await setFixedTimeMode(page);
|
||||
// Get the end bounds
|
||||
const endBounds = await page.getByLabel('End bounds').textContent();
|
||||
|
||||
// Add 2 minutes to end bound datetime and use it as the new end time
|
||||
let endTimeStamp = new Date(endBounds);
|
||||
endTimeStamp.setUTCMinutes(endTimeStamp.getUTCMinutes() + 2);
|
||||
const endDate = endTimeStamp.toISOString().split('T')[0];
|
||||
const milliseconds = endTimeStamp.getMilliseconds();
|
||||
const endTime = endTimeStamp.toISOString().split('T')[1].replace(`.${milliseconds}Z`, '');
|
||||
|
||||
// Subtract 1 minute from the end bound and use it as the new start time
|
||||
let startTimeStamp = new Date(endBounds);
|
||||
startTimeStamp.setUTCMinutes(startTimeStamp.getUTCMinutes() + 1);
|
||||
const startDate = startTimeStamp.toISOString().split('T')[0];
|
||||
const startMilliseconds = startTimeStamp.getMilliseconds();
|
||||
const startTime = startTimeStamp
|
||||
.toISOString()
|
||||
.split('T')[1]
|
||||
.replace(`.${startMilliseconds}Z`, '');
|
||||
// Set fixed timespan mode to the future so that "now" is out of bounds.
|
||||
await setTimeConductorBounds(page, {
|
||||
startDate,
|
||||
endDate,
|
||||
startTime,
|
||||
endTime
|
||||
});
|
||||
|
||||
await expect(page.getByLabel('Now Marker')).toBeHidden();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -287,6 +287,41 @@ test.describe('Basic Condition Set Use', () => {
|
||||
description: 'https://github.com/nasa/openmct/issues/7484'
|
||||
});
|
||||
});
|
||||
|
||||
test('ConditionSet has add criteria button enabled/disabled when composition is and is not available', async ({
|
||||
page
|
||||
}) => {
|
||||
const exampleTelemetry = await createExampleTelemetryObject(page);
|
||||
|
||||
await page.getByLabel('Show selected item in tree').click();
|
||||
await page.goto(conditionSet.url);
|
||||
// Change the object to edit mode
|
||||
await page.getByLabel('Edit Object').click();
|
||||
|
||||
// Create a condition
|
||||
await page.locator('#addCondition').click();
|
||||
await page.locator('#conditionCollection').getByRole('textbox').nth(0).fill('First Condition');
|
||||
|
||||
// Validate that the add criteria button is disabled
|
||||
await expect(page.getByLabel('Add Criteria - Disabled')).toHaveAttribute('disabled');
|
||||
|
||||
// Add Telemetry to ConditionSet
|
||||
const sineWaveGeneratorTreeItem = page
|
||||
.getByRole('tree', {
|
||||
name: 'Main Tree'
|
||||
})
|
||||
.getByRole('treeitem', {
|
||||
name: exampleTelemetry.name
|
||||
});
|
||||
const conditionCollection = page.locator('#conditionCollection');
|
||||
await sineWaveGeneratorTreeItem.dragTo(conditionCollection);
|
||||
|
||||
// Validate that the add criteria button is enabled and adds a new criterion
|
||||
await expect(page.getByLabel('Add Criteria - Enabled')).not.toHaveAttribute('disabled');
|
||||
await page.getByLabel('Add Criteria - Enabled').click();
|
||||
const numOfUnnamedCriteria = await page.getByLabel('Criterion Telemetry Selection').count();
|
||||
expect(numOfUnnamedCriteria).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Condition Set Composition', () => {
|
||||
|
@ -429,7 +429,7 @@ test.describe('Display Layout', () => {
|
||||
await expect(page.getByText('2021-12-30 01:11:00.000Z')).toBeHidden();
|
||||
});
|
||||
|
||||
test('When multiple plots are contained in a layout, we only ask for annotations once @couchdb', async ({
|
||||
test('When multiple plots are contained in a layout, we only ask for annotations once @couchdb @network', async ({
|
||||
page
|
||||
}) => {
|
||||
await setFixedTimeMode(page);
|
||||
@ -507,8 +507,140 @@ test.describe('Display Layout', () => {
|
||||
// In real time mode, we don't fetch annotations at all
|
||||
await expect.poll(() => networkRequests, { timeout: 10000 }).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('Same objects with different request options have unique subscriptions', async ({
|
||||
page
|
||||
}) => {
|
||||
// Expand My Items
|
||||
await page.getByLabel('Expand My Items folder').click();
|
||||
|
||||
// Create a Display Layout
|
||||
const displayLayout = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Display Layout',
|
||||
name: 'Test Display'
|
||||
});
|
||||
|
||||
// Create a State Generator, set to higher frequency updates
|
||||
const stateGenerator = await createDomainObjectWithDefaults(page, {
|
||||
type: 'State Generator',
|
||||
name: 'State Generator'
|
||||
});
|
||||
const stateGeneratorTreeItem = page.getByRole('treeitem', {
|
||||
name: stateGenerator.name
|
||||
});
|
||||
await stateGeneratorTreeItem.click({ button: 'right' });
|
||||
await page.getByLabel('Edit Properties...').click();
|
||||
await page.getByLabel('State Duration (seconds)', { exact: true }).fill('0.1');
|
||||
await page.getByLabel('Save').click();
|
||||
|
||||
// Create a Table for filtering ON values
|
||||
const tableFilterOnValue = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Telemetry Table',
|
||||
name: 'Table Filter On Value'
|
||||
});
|
||||
const tableFilterOnTreeItem = page.getByRole('treeitem', {
|
||||
name: tableFilterOnValue.name
|
||||
});
|
||||
|
||||
// Create a Table for filtering OFF values
|
||||
const tableFilterOffValue = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Telemetry Table',
|
||||
name: 'Table Filter Off Value'
|
||||
});
|
||||
const tableFilterOffTreeItem = page.getByRole('treeitem', {
|
||||
name: tableFilterOffValue.name
|
||||
});
|
||||
|
||||
// Navigate to ON filtering table and add state generator and setup filters
|
||||
await page.goto(tableFilterOnValue.url);
|
||||
await stateGeneratorTreeItem.dragTo(page.getByLabel('Object View'));
|
||||
await selectFilterOption(page, '1');
|
||||
await page.getByLabel('Save').click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
// Navigate to OFF filtering table and add state generator and setup filters
|
||||
await page.goto(tableFilterOffValue.url);
|
||||
await stateGeneratorTreeItem.dragTo(page.getByLabel('Object View'));
|
||||
await selectFilterOption(page, '0');
|
||||
await page.getByLabel('Save').click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
// Navigate to the display layout and edit it
|
||||
await page.goto(displayLayout.url);
|
||||
|
||||
// Add the tables to the display layout
|
||||
await page.getByLabel('Edit Object').click();
|
||||
await tableFilterOffTreeItem.dragTo(page.getByLabel('Layout Grid'), {
|
||||
targetPosition: { x: 10, y: 300 }
|
||||
});
|
||||
await page.locator('.c-frame-edit > div:nth-child(4)').dragTo(page.getByLabel('Layout Grid'), {
|
||||
targetPosition: { x: 400, y: 500 },
|
||||
// eslint-disable-next-line playwright/no-force-option
|
||||
force: true
|
||||
});
|
||||
await tableFilterOnTreeItem.dragTo(page.getByLabel('Layout Grid'), {
|
||||
targetPosition: { x: 10, y: 100 }
|
||||
});
|
||||
await page.locator('.c-frame-edit > div:nth-child(4)').dragTo(page.getByLabel('Layout Grid'), {
|
||||
targetPosition: { x: 400, y: 300 },
|
||||
// eslint-disable-next-line playwright/no-force-option
|
||||
force: true
|
||||
});
|
||||
await page.getByLabel('Save').click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
// Get the tables so we can verify filtering is working as expected
|
||||
const tableFilterOn = page.getByLabel(`${tableFilterOnValue.name} Frame`, {
|
||||
exact: true
|
||||
});
|
||||
const tableFilterOff = page.getByLabel(`${tableFilterOffValue.name} Frame`, {
|
||||
exact: true
|
||||
});
|
||||
|
||||
// Verify filtering is working correctly
|
||||
|
||||
// Check that no filtered values appear for at least 2 seconds
|
||||
const VERIFICATION_TIME = 2000; // 2 seconds
|
||||
const CHECK_INTERVAL = 100; // Check every 100ms
|
||||
|
||||
// Create a promise that will check for filtered values periodically
|
||||
const checkForCorrectValues = new Promise((resolve, reject) => {
|
||||
const interval = setInterval(async () => {
|
||||
const offCount = await tableFilterOn.locator('td[title="OFF"]').count();
|
||||
const onCount = await tableFilterOff.locator('td[title="ON"]').count();
|
||||
if (offCount > 0 || onCount > 0) {
|
||||
clearInterval(interval);
|
||||
reject(
|
||||
new Error(
|
||||
`Found ${offCount} OFF and ${onCount} ON values when expecting 0 OFF and 0 ON`
|
||||
)
|
||||
);
|
||||
}
|
||||
}, CHECK_INTERVAL);
|
||||
|
||||
// After VERIFICATION_TIME, if no filtered values were found, resolve successfully
|
||||
setTimeout(() => {
|
||||
clearInterval(interval);
|
||||
resolve();
|
||||
}, VERIFICATION_TIME);
|
||||
});
|
||||
|
||||
await expect(checkForCorrectValues).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
async function selectFilterOption(page, filterOption) {
|
||||
await page.getByRole('tab', { name: 'Filters' }).click();
|
||||
await page
|
||||
.getByLabel('Inspector Views')
|
||||
.locator('li')
|
||||
.filter({ hasText: 'State Generator' })
|
||||
.locator('span')
|
||||
.click();
|
||||
await page.getByRole('switch').click();
|
||||
await page.selectOption('select[name="setSelectionThreshold"]', filterOption);
|
||||
}
|
||||
|
||||
async function addAndRemoveDrawingObjectAndAssert(page, layoutObject, DISPLAY_LAYOUT_NAME) {
|
||||
await expect(page.getByLabel(layoutObject, { exact: true })).toHaveCount(0);
|
||||
await addLayoutObject(page, DISPLAY_LAYOUT_NAME, layoutObject);
|
||||
|
@ -24,19 +24,13 @@ import {
|
||||
acknowledgeFault,
|
||||
acknowledgeMultipleFaults,
|
||||
changeViewTo,
|
||||
clearSearch,
|
||||
enterSearchTerm,
|
||||
getFault,
|
||||
getFaultByName,
|
||||
getFaultName,
|
||||
getFaultNamespace,
|
||||
getFaultResultCount,
|
||||
getFaultSeverity,
|
||||
getFaultTriggerTime,
|
||||
getHighestSeverity,
|
||||
getLowestSeverity,
|
||||
navigateToFaultManagementWithExample,
|
||||
navigateToFaultManagementWithoutExample,
|
||||
navigateToFaultManagementWithStaticExample,
|
||||
selectFaultItem,
|
||||
shelveFault,
|
||||
shelveMultipleFaults,
|
||||
@ -46,7 +40,7 @@ import { expect, test } from '../../../../pluginFixtures.js';
|
||||
|
||||
test.describe('The Fault Management Plugin using example faults', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await navigateToFaultManagementWithExample(page);
|
||||
await navigateToFaultManagementWithStaticExample(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);
|
||||
});
|
||||
|
||||
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
|
||||
}) => {
|
||||
await selectFaultItem(page, 1);
|
||||
@ -67,9 +61,7 @@ test.describe('The Fault Management Plugin using example faults', () => {
|
||||
.getByLabel('Source inspector properties')
|
||||
.getByLabel('inspector property value');
|
||||
|
||||
await expect(
|
||||
page.locator('.c-faults-list-view-item-body > .c-fault-mgmt__list').first()
|
||||
).toHaveClass(/is-selected/);
|
||||
await expect(page.getByLabel('Fault triggered at').first()).toHaveClass(/is-selected/);
|
||||
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, 2);
|
||||
|
||||
const selectedRows = page.locator(
|
||||
'.c-fault-mgmt__list.is-selected .c-fault-mgmt__list-faultname'
|
||||
);
|
||||
expect(await selectedRows.count()).toEqual(2);
|
||||
const selectedRows = page.getByRole('checkbox', { checked: true });
|
||||
await expect(selectedRows).toHaveCount(2);
|
||||
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
const firstSelectedFaultName = await selectedRows.nth(0).textContent();
|
||||
const secondSelectedFaultName = await selectedRows.nth(1).textContent();
|
||||
const firstNameInInspectorCount = await page
|
||||
.locator(`.c-inspector__properties >> :text("${firstSelectedFaultName}")`)
|
||||
.count();
|
||||
const secondNameInInspectorCount = await page
|
||||
.locator(`.c-inspector__properties >> :text("${secondSelectedFaultName}")`)
|
||||
.count();
|
||||
|
||||
expect(firstNameInInspectorCount).toEqual(0);
|
||||
expect(secondNameInInspectorCount).toEqual(0);
|
||||
await expect(
|
||||
page.locator(`.c-inspector__properties >> :text("${firstSelectedFaultName}")`)
|
||||
).toHaveCount(0);
|
||||
await expect(
|
||||
page.locator(`.c-inspector__properties >> :text("${secondSelectedFaultName}")`)
|
||||
).toHaveCount(0);
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
// should be all faults (5)
|
||||
let faultResultCount = await getFaultResultCount(page);
|
||||
expect(faultResultCount).toEqual(5);
|
||||
await expect(page.getByLabel('Fault triggered at')).toHaveCount(5);
|
||||
|
||||
// search namespace
|
||||
await enterSearchTerm(page, faultThreeNamespace);
|
||||
await page
|
||||
.getByLabel('Fault Management Object View')
|
||||
.getByLabel('Search Input')
|
||||
.fill(faultThreeNamespace);
|
||||
|
||||
faultResultCount = await getFaultResultCount(page);
|
||||
expect(faultResultCount).toEqual(1);
|
||||
await expect(page.getByLabel('Fault triggered at')).toHaveCount(1);
|
||||
expect(await getFaultNamespace(page, 1)).toEqual(faultThreeNamespace);
|
||||
|
||||
// all faults
|
||||
await clearSearch(page);
|
||||
faultResultCount = await getFaultResultCount(page);
|
||||
expect(faultResultCount).toEqual(5);
|
||||
await page.getByLabel('Fault Management Object View').getByLabel('Search Input').fill('');
|
||||
await expect(page.getByLabel('Fault triggered at')).toHaveCount(5);
|
||||
|
||||
// search name
|
||||
await enterSearchTerm(page, faultTwoName);
|
||||
await page
|
||||
.getByLabel('Fault Management Object View')
|
||||
.getByLabel('Search Input')
|
||||
.fill(faultTwoName);
|
||||
|
||||
faultResultCount = await getFaultResultCount(page);
|
||||
expect(faultResultCount).toEqual(1);
|
||||
await expect(page.getByLabel('Fault triggered at')).toHaveCount(1);
|
||||
expect(await getFaultName(page, 1)).toEqual(faultTwoName);
|
||||
|
||||
// all faults
|
||||
await clearSearch(page);
|
||||
faultResultCount = await getFaultResultCount(page);
|
||||
expect(faultResultCount).toEqual(5);
|
||||
await page.getByLabel('Fault Management Object View').getByLabel('Search Input').fill('');
|
||||
await expect(page.getByLabel('Fault triggered at')).toHaveCount(5);
|
||||
|
||||
// search triggerTime
|
||||
await enterSearchTerm(page, faultFiveTriggerTime);
|
||||
await page
|
||||
.getByLabel('Fault Management Object View')
|
||||
.getByLabel('Search Input')
|
||||
.fill(faultFiveTriggerTime);
|
||||
|
||||
faultResultCount = await getFaultResultCount(page);
|
||||
expect(faultResultCount).toEqual(1);
|
||||
await expect(page.getByLabel('Fault triggered at')).toHaveCount(1);
|
||||
expect(await getFaultTriggerTime(page, 1)).toEqual(faultFiveTriggerTime);
|
||||
});
|
||||
|
||||
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 faultFiveName = 'Example Fault 5';
|
||||
let firstFaultName = await getFaultName(page, 1);
|
||||
@ -237,10 +240,19 @@ test.describe('The Fault Management Plugin using example faults', () => {
|
||||
|
||||
await sortFaultsBy(page, 'severity');
|
||||
|
||||
const sortedHighestSeverity = await getFaultSeverity(page, 1);
|
||||
const sortedLowestSeverity = await getFaultSeverity(page, 5);
|
||||
expect(sortedHighestSeverity).toEqual(highestSeverity);
|
||||
expect(sortedLowestSeverity).toEqual(lowestSeverity);
|
||||
const firstFaultSeverityLabel = await page
|
||||
.getByLabel('Severity:')
|
||||
.first()
|
||||
.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 }) => {
|
||||
const faultCount = await page.locator('c-fault-mgmt__list').count();
|
||||
|
||||
expect(faultCount).toEqual(0);
|
||||
await expect(page.getByLabel('Fault triggered at')).toHaveCount(0);
|
||||
|
||||
await changeViewTo(page, 'acknowledged');
|
||||
const acknowledgedCount = await page.locator('c-fault-mgmt__list').count();
|
||||
expect(acknowledgedCount).toEqual(0);
|
||||
await expect(page.getByLabel('Fault triggered at')).toHaveCount(0);
|
||||
|
||||
await changeViewTo(page, 'shelved');
|
||||
const shelvedCount = await page.locator('c-fault-mgmt__list').count();
|
||||
expect(shelvedCount).toEqual(0);
|
||||
await expect(page.getByLabel('Fault triggered at')).toHaveCount(0);
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
expect(faultCount).toEqual(0);
|
||||
await expect(page.getByLabel('Fault triggered at')).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
|
@ -28,7 +28,9 @@ import { v4 as uuid } from 'uuid';
|
||||
|
||||
import {
|
||||
createDomainObjectWithDefaults,
|
||||
createExampleTelemetryObject
|
||||
createExampleTelemetryObject,
|
||||
setRealTimeMode,
|
||||
setStartOffset
|
||||
} from '../../../../appActions.js';
|
||||
import { expect, test } from '../../../../pluginFixtures.js';
|
||||
|
||||
@ -166,6 +168,57 @@ test.describe('Gauge', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('Gauge does not break when an object is missing', async ({ page }) => {
|
||||
// Set up error listeners
|
||||
const pageErrors = [];
|
||||
|
||||
// Listen for uncaught exceptions
|
||||
page.on('pageerror', (err) => {
|
||||
pageErrors.push(err.message);
|
||||
});
|
||||
|
||||
await setRealTimeMode(page);
|
||||
|
||||
// Create a Gauge
|
||||
const gauge = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Gauge',
|
||||
name: 'Gauge with missing object'
|
||||
});
|
||||
|
||||
// Create a Sine Wave Generator in the Gauge with a loading delay
|
||||
const missingSWG = await createExampleTelemetryObject(page, gauge.uuid);
|
||||
|
||||
// Remove the object from local storage
|
||||
await page.evaluate(
|
||||
([missingObject]) => {
|
||||
const mct = localStorage.getItem('mct');
|
||||
const mctObjects = JSON.parse(mct);
|
||||
delete mctObjects[missingObject.uuid];
|
||||
localStorage.setItem('mct', JSON.stringify(mctObjects));
|
||||
},
|
||||
[missingSWG]
|
||||
);
|
||||
|
||||
// Verify start bounds
|
||||
await expect(page.getByLabel('Start offset: 00:30:00')).toBeVisible();
|
||||
|
||||
// Nav to the Gauge
|
||||
await page.goto(gauge.url, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// adjust time bounds and ensure they are updated
|
||||
await setStartOffset(page, {
|
||||
startHours: '00',
|
||||
startMins: '45',
|
||||
startSecs: '00'
|
||||
});
|
||||
|
||||
// Verify start bounds changed
|
||||
await expect(page.getByLabel('Start offset: 00:45:00')).toBeVisible();
|
||||
|
||||
// // Verify no errors were thrown
|
||||
expect(pageErrors).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('Gauge enforces composition policy', async ({ page }) => {
|
||||
// Create a Gauge
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
|
@ -30,16 +30,19 @@ import {
|
||||
navigateToObjectWithRealTime,
|
||||
setRealTimeMode
|
||||
} 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';
|
||||
|
||||
const panHotkey = process.platform === 'linux' ? ['Shift', 'Alt'] : ['Alt'];
|
||||
const tagHotkey = ['Shift', 'Alt'];
|
||||
const expectedAltText = process.platform === 'linux' ? 'Shift+Alt drag to pan' : 'Alt drag to pan';
|
||||
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.
|
||||
test.describe('Example Imagery Object', () => {
|
||||
@ -93,9 +96,6 @@ test.describe('Example Imagery Object', () => {
|
||||
expect(newPage.url()).toContain('.jpg');
|
||||
});
|
||||
|
||||
// this requires CORS to be enabled in some fashion
|
||||
test.fixme('Can right click on image and save it as a file', async ({ page }) => {});
|
||||
|
||||
test('Can adjust image brightness/contrast by dragging the sliders', async ({
|
||||
page,
|
||||
browserName
|
||||
@ -357,15 +357,10 @@ test.describe('Example Imagery Object', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Example Imagery in Display Layout @clock', () => {
|
||||
test.describe('Example Imagery in Display Layout', () => {
|
||||
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' });
|
||||
|
||||
@ -428,12 +423,7 @@ test.describe('Example Imagery in Display Layout @clock', () => {
|
||||
await expect.soft(pausePlayButton).toHaveClass(/is-paused/);
|
||||
});
|
||||
|
||||
test('Imagery View operations @clock', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/5265'
|
||||
});
|
||||
|
||||
test('Imagery View operations', async ({ page }) => {
|
||||
// Edit mode
|
||||
await page.getByLabel('Edit Object').click();
|
||||
|
||||
@ -526,14 +516,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;
|
||||
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' });
|
||||
@ -562,7 +547,7 @@ test.describe('Example Imagery in Flexible layout @clock', () => {
|
||||
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.info().annotations.push({
|
||||
type: 'issue',
|
||||
@ -573,14 +558,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;
|
||||
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' });
|
||||
|
||||
tabsView = await createDomainObjectWithDefaults(page, { type: 'Tabs View' });
|
||||
@ -607,7 +588,8 @@ test.describe('Example Imagery in Tabs View @clock', () => {
|
||||
// Wait for image thumbnail auto-scroll to complete
|
||||
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);
|
||||
});
|
||||
});
|
||||
@ -668,16 +650,19 @@ test.describe('Example Imagery in Time Strip', () => {
|
||||
* 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
|
||||
* 5. Imagery is updated as new images stream in, regardless of pause status
|
||||
* 6. Old images are discarded when new images stream in
|
||||
* 7. Image brightness/contrast can be adjusted by dragging the sliders
|
||||
* 6. Old images are discarded when their timestamps fall out of bounds
|
||||
* 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
|
||||
*/
|
||||
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 mainImage = page.locator('.c-imagery__main-image__image');
|
||||
await expect(thumbnailImages.first()).toHaveAttribute('src', thumbnailUrlParamsRegexp);
|
||||
await expect(mainImage).not.toHaveAttribute('src', thumbnailUrlParamsRegexp);
|
||||
});
|
||||
|
||||
// Click previous image button
|
||||
const previousImageButton = page.getByLabel('Previous image');
|
||||
await expect(previousImageButton).toBeVisible();
|
||||
@ -736,19 +721,6 @@ async function performImageryViewOperationsAndAssert(page, layoutObject) {
|
||||
// Unpause imagery
|
||||
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
|
||||
await page.locator('[role=toolbar] button[title="Brightness and contrast"]').click();
|
||||
|
||||
@ -815,24 +787,6 @@ async function assertBackgroundImageBrightness(page, 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
|
||||
*/
|
||||
@ -918,14 +872,17 @@ async function mouseZoomOnImageAndAssert(page, factor = 2) {
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function buttonZoomOnImageAndAssert(page) {
|
||||
await test.step('Can zoom using buttons', async () => {
|
||||
// Lock the zoom and pan so it doesn't reset if a new image comes in
|
||||
await page.getByLabel('Focused Image Element').hover({ trial: true });
|
||||
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 lockButton.isVisible();
|
||||
// if (!(await lockButton.isVisible())) {
|
||||
// await page.getByLabel('Focused Image Element').hover({ trial: true });
|
||||
// }
|
||||
await lockButton.click();
|
||||
|
||||
await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty(
|
||||
@ -974,6 +931,7 @@ async function buttonZoomOnImageAndAssert(page) {
|
||||
|
||||
const finalBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
|
||||
expect(finalBoundingBox).toEqual(initialBoundingBox);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1035,24 +993,6 @@ async function resetImageryPanAndZoom(page) {
|
||||
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
|
||||
*/
|
||||
|
@ -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,93 @@
|
||||
/*****************************************************************************
|
||||
* 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 verifies modifying the image location of the example imagery object.
|
||||
*/
|
||||
|
||||
import { createDomainObjectWithDefaults } from '../../../../appActions.js';
|
||||
import { expect, test } from '../../../../pluginFixtures.js';
|
||||
|
||||
test.describe('Example Imagery Object Custom Images', () => {
|
||||
let exampleImagery;
|
||||
test.beforeEach(async ({ page }) => {
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Create a default 'Example Imagery' object
|
||||
exampleImagery = await createDomainObjectWithDefaults(page, {
|
||||
name: 'Example Imagery',
|
||||
type: 'Example Imagery'
|
||||
});
|
||||
|
||||
// Verify that the created object is focused
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText(exampleImagery.name);
|
||||
await page.getByLabel('Focused Image Element').hover({ trial: true });
|
||||
|
||||
// Wait for image thumbnail auto-scroll to complete
|
||||
await expect(page.getByLabel('Image Thumbnail from').last()).toBeInViewport();
|
||||
});
|
||||
// this requires CORS to be enabled in some fashion
|
||||
test.fixme('Can right click on image and save it as a file', async ({ page }) => {});
|
||||
test('Can provide a custom image location for the example imagery object', async ({ page }) => {
|
||||
// Modify Example Imagery to create a really stable image which will never let us down
|
||||
await page.getByRole('button', { name: 'More actions' }).click();
|
||||
await page.getByRole('menuitem', { name: 'Edit Properties...' }).click();
|
||||
await page
|
||||
.locator('#imageLocation-textarea')
|
||||
.fill(
|
||||
'https://raw.githubusercontent.com/nasa/openmct/554f77c42fec81cf0f63e62b278012cb08d82af9/e2e/test-data/rick.jpg,https://raw.githubusercontent.com/nasa/openmct/554f77c42fec81cf0f63e62b278012cb08d82af9/e2e/test-data/rick.jpg'
|
||||
);
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Wait for the thumbnails to finish their scroll animation
|
||||
// (Wait until the rightmost thumbnail is in view)
|
||||
await expect(page.getByLabel('Image Thumbnail from').last()).toBeInViewport();
|
||||
|
||||
await expect(page.getByLabel('Image Wrapper')).toBeVisible();
|
||||
});
|
||||
test.fixme('Can provide a custom image with spaces in name', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/7903'
|
||||
});
|
||||
await page.goto(exampleImagery.url, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Modify Example Imagery to create a really stable image which will never let us down
|
||||
await page.getByRole('button', { name: 'More actions' }).click();
|
||||
await page.getByRole('menuitem', { name: 'Edit Properties...' }).click();
|
||||
await page
|
||||
.locator('#imageLocation-textarea')
|
||||
.fill(
|
||||
'https://raw.githubusercontent.com/nasa/openmct/d8c64f183400afb70137221fc1a035e091bea912/e2e/test-data/rick%20space%20roll.jpg'
|
||||
);
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Wait for the thumbnails to finish their scroll animation
|
||||
// (Wait until the rightmost thumbnail is in view)
|
||||
await expect(page.getByLabel('Image Thumbnail from').last()).toBeInViewport();
|
||||
|
||||
await expect(page.getByLabel('Image Wrapper')).toBeVisible();
|
||||
});
|
||||
});
|
@ -26,7 +26,10 @@ This test suite is dedicated to tests which verify the basic operations surround
|
||||
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
import { createDomainObjectWithDefaults } from '../../../../appActions.js';
|
||||
import {
|
||||
createDomainObjectWithDefaults,
|
||||
renameCurrentObjectFromBrowseBar
|
||||
} from '../../../../appActions.js';
|
||||
import { copy, paste, selectAll } from '../../../../helper/hotkeys/hotkeys.js';
|
||||
import * as nbUtils from '../../../../helper/notebookUtils.js';
|
||||
import { expect, streamToString, test } from '../../../../pluginFixtures.js';
|
||||
@ -596,4 +599,61 @@ test.describe('Notebook entry tests', () => {
|
||||
await expect(await page.locator(`text="${TEST_TEXT.repeat(1)}"`).count()).toEqual(1);
|
||||
await expect(await page.locator(`text="${TEST_TEXT.repeat(2)}"`).count()).toEqual(0);
|
||||
});
|
||||
|
||||
test('When changing the name of a notebook in the browse bar, new notebook changes are not lost', async ({
|
||||
page
|
||||
}) => {
|
||||
const TEST_TEXT = 'Do not lose me!';
|
||||
const FIRST_NEW_NAME = 'New Name';
|
||||
const SECOND_NEW_NAME = 'Second New Name';
|
||||
|
||||
await page.goto(notebookObject.url);
|
||||
|
||||
await page.getByLabel('Expand My Items folder').click();
|
||||
|
||||
await renameCurrentObjectFromBrowseBar(page, FIRST_NEW_NAME);
|
||||
|
||||
// verify the name change in tree and browse bar
|
||||
await verifyNameChange(page, FIRST_NEW_NAME);
|
||||
|
||||
// enter one entry
|
||||
await enterAndCommitTextEntry(page, TEST_TEXT);
|
||||
|
||||
// verify the entry is present
|
||||
await expect(await page.locator(`text="${TEST_TEXT}"`).count()).toEqual(1);
|
||||
|
||||
// change the name
|
||||
await renameCurrentObjectFromBrowseBar(page, SECOND_NEW_NAME);
|
||||
|
||||
// verify the name change in tree and browse bar
|
||||
await verifyNameChange(page, SECOND_NEW_NAME);
|
||||
|
||||
// verify the entry is still present
|
||||
await expect(await page.locator(`text="${TEST_TEXT}"`).count()).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Enter text into the last notebook entry and commit it.
|
||||
*
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} text
|
||||
*/
|
||||
async function enterAndCommitTextEntry(page, text) {
|
||||
await nbUtils.addNotebookEntry(page);
|
||||
await nbUtils.enterTextInLastEntry(page, text);
|
||||
await nbUtils.commitEntry(page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the name change in the tree and browse bar.
|
||||
*
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} newName
|
||||
*/
|
||||
async function verifyNameChange(page, newName) {
|
||||
await expect(
|
||||
page.getByRole('treeitem').locator('.is-navigated-object .c-tree__item__name')
|
||||
).toHaveText(newName);
|
||||
await expect(page.getByLabel('Browse bar object name')).toHaveText(newName);
|
||||
}
|
||||
|
@ -24,20 +24,26 @@
|
||||
This test suite is dedicated to tests which verify the basic operations surrounding Notebooks with CouchDB.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Disable no-networkidle eslint rule until we can engineer more deterministic network-event
|
||||
* driven tests.
|
||||
*/
|
||||
/* eslint-disable playwright/no-networkidle */
|
||||
|
||||
import { createDomainObjectWithDefaults } from '../../../../appActions.js';
|
||||
import * as nbUtils from '../../../../helper/notebookUtils.js';
|
||||
import { expect, test } from '../../../../pluginFixtures.js';
|
||||
|
||||
test.describe('Notebook Tests with CouchDB @couchdb', () => {
|
||||
test.describe('Notebook Tests with CouchDB @couchdb @network', () => {
|
||||
let testNotebook;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Navigate to baseURL
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
// Create Notebook
|
||||
testNotebook = await createDomainObjectWithDefaults(page, { type: 'Notebook' });
|
||||
await page.goto(testNotebook.url, { waitUntil: 'domcontentloaded' });
|
||||
await page.goto(testNotebook.url, { waitUntil: 'networkidle' });
|
||||
});
|
||||
|
||||
test('Inspect Notebook Entry Network Requests', async ({ page }) => {
|
||||
@ -58,7 +64,7 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => {
|
||||
page.getByLabel('Add Page').click()
|
||||
]);
|
||||
// Ensures that there are no other network requests
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Assert that only two requests are made
|
||||
// Network Requests are:
|
||||
@ -77,7 +83,7 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => {
|
||||
// 2) The shared worker event from 👆 POST request
|
||||
notebookElementsRequests = [];
|
||||
await nbUtils.enterTextEntry(page, 'First Entry');
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
await page.waitForLoadState('networkidle');
|
||||
expect(notebookElementsRequests.length).toBeLessThanOrEqual(2);
|
||||
|
||||
// Add some tags
|
||||
@ -141,7 +147,7 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => {
|
||||
// 4) The shared worker event from 👆 POST request
|
||||
notebookElementsRequests = [];
|
||||
await nbUtils.enterTextEntry(page, 'Fourth Entry');
|
||||
page.waitForLoadState('domcontentloaded');
|
||||
page.waitForLoadState('networkidle');
|
||||
|
||||
expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(4);
|
||||
|
||||
@ -153,7 +159,7 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => {
|
||||
// 4) The shared worker event from 👆 POST request
|
||||
notebookElementsRequests = [];
|
||||
await nbUtils.enterTextEntry(page, 'Fifth Entry');
|
||||
page.waitForLoadState('domcontentloaded');
|
||||
page.waitForLoadState('networkidle');
|
||||
|
||||
expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(4);
|
||||
|
||||
@ -164,7 +170,7 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => {
|
||||
// 4) The shared worker event from 👆 POST request
|
||||
notebookElementsRequests = [];
|
||||
await nbUtils.enterTextEntry(page, 'Sixth Entry');
|
||||
page.waitForLoadState('domcontentloaded');
|
||||
page.waitForLoadState('networkidle');
|
||||
|
||||
expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(4);
|
||||
});
|
||||
@ -227,7 +233,7 @@ async function addTagAndAwaitNetwork(page, tagName) {
|
||||
page.locator(`[aria-label="Autocomplete Options"] >> text=${tagName}`).click(),
|
||||
expect(page.locator(`[aria-label="Tag"]:has-text("${tagName}")`)).toBeVisible()
|
||||
]);
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
await page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -246,5 +252,5 @@ async function removeTagAndAwaitNetwork(page, tagName) {
|
||||
)
|
||||
]);
|
||||
await expect(page.locator(`[aria-label="Tag"]:has-text("${tagName}")`)).toBeHidden();
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
await page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
@ -0,0 +1,72 @@
|
||||
/*****************************************************************************
|
||||
* 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 { 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();
|
||||
});
|
||||
});
|
@ -108,4 +108,42 @@ test.describe('Plot Controls', () => {
|
||||
// Expect before and after plot points to match
|
||||
await expect(plotPixelSizeAtPause).toEqual(plotPixelSizeAfterWait);
|
||||
});
|
||||
|
||||
/*
|
||||
Test to verify that switching a plot's time context from global to
|
||||
its own independent time context and then back to global context works correctly.
|
||||
|
||||
After switching from fixed time mode (ITC) to real time mode (global context),
|
||||
the pause control for the plot should be available, indicating that it is following the right context.
|
||||
*/
|
||||
test('Plots follow the right time context', async ({ page }) => {
|
||||
// Set global time conductor to real-time mode
|
||||
await setRealTimeMode(page);
|
||||
|
||||
// hover over plot for plot controls
|
||||
await page.getByLabel('Plot Canvas').hover();
|
||||
// Ensure pause control is visible since global time conductor is in Real time mode.
|
||||
await expect(page.getByTitle('Pause incoming real-time data')).toBeVisible();
|
||||
|
||||
// Toggle independent time conductor ON
|
||||
await page.getByLabel('Enable Independent Time Conductor').click();
|
||||
|
||||
// Bring up the independent time conductor popup and switch to fixed time mode
|
||||
await page.getByLabel('Independent Time Conductor Settings').click();
|
||||
await page.getByLabel('Independent Time Conductor Mode Menu').click();
|
||||
await page.getByRole('menuitem', { name: /Fixed Timespan/ }).click();
|
||||
|
||||
// hover over plot for plot controls
|
||||
await page.getByLabel('Plot Canvas').hover();
|
||||
// Ensure pause control is no longer visible since the plot is following the independent time context
|
||||
await expect(page.getByTitle('Pause incoming real-time data')).toBeHidden();
|
||||
|
||||
// Toggle independent time conductor OFF - Note that the global time conductor is still in Real time mode
|
||||
await page.getByLabel('Disable Independent Time Conductor').click();
|
||||
|
||||
// hover over plot for plot controls
|
||||
await page.getByLabel('Plot Canvas').hover();
|
||||
// Ensure pause control is visible since the global time conductor is in real time mode
|
||||
await expect(page.getByTitle('Pause incoming real-time data')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
@ -0,0 +1,58 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2025, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
* This test suite is dedicated to testing the rendering and interaction of plots.
|
||||
*
|
||||
*/
|
||||
|
||||
import { createDomainObjectWithDefaults } from '../../../../appActions.js';
|
||||
import { expect, test } from '../../../../pluginFixtures.js';
|
||||
|
||||
test.describe('Plot Controls in compact mode', () => {
|
||||
let timeStrip;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Open a browser, navigate to the main page, and wait until all networkevents to resolve
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
timeStrip = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Time Strip'
|
||||
});
|
||||
|
||||
// Create an overlay plot with a sine wave generator
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
parent: timeStrip.uuid
|
||||
});
|
||||
await page.goto(`${timeStrip.url}`);
|
||||
});
|
||||
|
||||
test('Plots show cursor guides', async ({ page }) => {
|
||||
// hover over plot for plot controls
|
||||
await page.getByLabel('Plot Canvas').hover();
|
||||
// click on cursor guides control
|
||||
await page.getByTitle('Toggle cursor guides').click();
|
||||
await page.getByLabel('Plot Canvas').hover();
|
||||
await expect(page.getByLabel('Vertical cursor guide')).toBeVisible();
|
||||
await expect(page.getByLabel('Horizontal cursor guide')).toBeVisible();
|
||||
});
|
||||
});
|
@ -1,5 +1,5 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2023, United States Government
|
||||
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
|
@ -0,0 +1,163 @@
|
||||
/*****************************************************************************
|
||||
* 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 tests which verify the basic operations surrounding conditionSets and styling
|
||||
*/
|
||||
|
||||
import {
|
||||
createDomainObjectWithDefaults,
|
||||
linkParameterToObject,
|
||||
setRealTimeMode
|
||||
} from '../../../../appActions.js';
|
||||
import { MISSION_TIME } from '../../../../constants.js';
|
||||
import { expect, test } from '../../../../pluginFixtures.js';
|
||||
|
||||
test.describe('Conditionally Styling, using a Condition Set', () => {
|
||||
let stateGenerator;
|
||||
let conditionSet;
|
||||
let displayLayout;
|
||||
const STATE_CHANGE_INTERVAL = '1';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Install the clock and set the time to the mission time such that the state generator will be controllable
|
||||
await page.clock.install({ time: MISSION_TIME });
|
||||
await page.clock.resume();
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
// Create Condition Set, State Generator, and Display Layout
|
||||
conditionSet = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Condition Set',
|
||||
name: 'Test Condition Set'
|
||||
});
|
||||
stateGenerator = await createDomainObjectWithDefaults(page, {
|
||||
type: 'State Generator',
|
||||
name: 'One Second State Generator'
|
||||
});
|
||||
// edit the state generator to have a 1 second update rate
|
||||
await page.getByTitle('More actions').click();
|
||||
await page.getByRole('menuitem', { name: 'Edit Properties...' }).click();
|
||||
await page.getByLabel('State Duration (seconds)', { exact: true }).fill(STATE_CHANGE_INTERVAL);
|
||||
await page.getByLabel('Save').click();
|
||||
|
||||
displayLayout = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Display Layout',
|
||||
name: 'Test Display Layout'
|
||||
});
|
||||
});
|
||||
|
||||
test('Conditional styling, using a Condition Set, will style correctly based on the output @clock', async ({
|
||||
page
|
||||
}) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/7840'
|
||||
});
|
||||
|
||||
// set up the condition set to use the state generator
|
||||
await page.goto(conditionSet.url, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Add the State Generator to the Condition Set by dragging from the main tree
|
||||
await page.getByLabel('Show selected item in tree').click();
|
||||
await page
|
||||
.getByRole('tree', {
|
||||
name: 'Main Tree'
|
||||
})
|
||||
.getByRole('treeitem', {
|
||||
name: stateGenerator.name
|
||||
})
|
||||
.dragTo(page.locator('#conditionCollection'));
|
||||
|
||||
// Add the state generator to the first criterion such that there is a condition named 'OFF' when the state generator is off
|
||||
await page.getByLabel('Add Condition').click();
|
||||
await page
|
||||
.getByLabel('Criterion Telemetry Selection')
|
||||
.selectOption({ label: stateGenerator.name });
|
||||
await page.getByLabel('Criterion Metadata Selection').selectOption({ label: 'State' });
|
||||
await page.getByLabel('Criterion Comparison Selection').selectOption({ label: 'is' });
|
||||
await page.getByLabel('Condition Name Input').first().fill('OFF');
|
||||
await page.getByLabel('Save').click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
await linkParameterToObject(page, stateGenerator.name, displayLayout.name);
|
||||
|
||||
//Add a box to the display layout
|
||||
await page.goto(displayLayout.url, { waitUntil: 'domcontentloaded' });
|
||||
await page.getByLabel('Edit Object').click();
|
||||
|
||||
//Add a box to the display layout and move it to the right
|
||||
//TEMP: Click the layout such that the state generator is deselected
|
||||
await page.getByLabel('Test Display Layout Layout Grid').locator('div').nth(1).click();
|
||||
await page.getByLabel('Add Drawing Object').click();
|
||||
await page.getByText('Box').click();
|
||||
await page.getByLabel('X:').click();
|
||||
await page.getByLabel('X:').fill('10');
|
||||
await page.getByLabel('X:').press('Enter');
|
||||
|
||||
// set up conditional styling such that the box is red when the state generator condition is 'OFF'
|
||||
await page.getByRole('tab', { name: 'Styles' }).click();
|
||||
await page.getByRole('button', { name: 'Use Conditional Styling...' }).click();
|
||||
await page.getByLabel('Modal Overlay').getByLabel('Expand My Items folder').click();
|
||||
await page.getByLabel('Modal Overlay').getByLabel(`Preview ${conditionSet.name}`).click();
|
||||
await page.getByText('Ok').click();
|
||||
await page.getByLabel('Set background color').first().click();
|
||||
await page.getByLabel('#ff0000').click();
|
||||
await page.getByLabel('Save', { exact: true }).click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
await setRealTimeMode(page);
|
||||
|
||||
//Pause at a time when the state generator is 'OFF' which is 20 minutes in the future
|
||||
await page.clock.pauseAt(new Date(MISSION_TIME + 1200000));
|
||||
|
||||
const redBG = 'background-color: rgb(255, 0, 0);';
|
||||
const defaultBG = 'background-color: rgb(102, 102, 102);';
|
||||
const textElement = page.getByLabel('Alpha-numeric telemetry value').locator('div:first-child');
|
||||
const styledElement = page.getByLabel('Box', { exact: true });
|
||||
|
||||
await page.clock.resume();
|
||||
|
||||
// Check if the style is red when text is 'OFF'
|
||||
await expect(textElement).toHaveText('OFF');
|
||||
await waitForStyleChange(styledElement, redBG);
|
||||
|
||||
// Fast forward to the next state change
|
||||
await page.clock.fastForward(STATE_CHANGE_INTERVAL * 1000);
|
||||
|
||||
// Check if the style is not red when text is 'ON'
|
||||
await expect(textElement).toHaveText('ON');
|
||||
await waitForStyleChange(styledElement, defaultBG);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Wait for the style of an element to change to the expected style.
|
||||
* @param {import('@playwright/test').Locator} element - The element to check.
|
||||
* @param {string} expectedStyle - The expected style to wait for.
|
||||
* @param {number} timeout - The timeout in milliseconds.
|
||||
*/
|
||||
async function waitForStyleChange(element, expectedStyle, timeout = 0) {
|
||||
await expect(async () => {
|
||||
const style = await element.getAttribute('style');
|
||||
|
||||
// eslint-disable-next-line playwright/prefer-web-first-assertions
|
||||
expect(style).toBe(expectedStyle);
|
||||
}).toPass({ timeout: 1000 }); // timeout allows for the style to be applied
|
||||
}
|
@ -0,0 +1,114 @@
|
||||
/*****************************************************************************
|
||||
* 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 { fileURLToPath } from 'url';
|
||||
|
||||
import {
|
||||
createDomainObjectWithDefaults,
|
||||
navigateToObjectWithRealTime
|
||||
} from '../../../../../appActions.js';
|
||||
import { expect, test } from '../../../../../pluginFixtures.js';
|
||||
|
||||
const TINY_IMAGE_BASE64 =
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII';
|
||||
|
||||
test.describe('Display Layout Conditional Styling', () => {
|
||||
test.use({
|
||||
storageState: fileURLToPath(
|
||||
new URL('../../../../../test-data/condition_set_storage.json', import.meta.url)
|
||||
)
|
||||
});
|
||||
|
||||
let displayLayout;
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
displayLayout = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Display Layout',
|
||||
name: 'Test Display Layout'
|
||||
});
|
||||
});
|
||||
|
||||
test('Image Drawing Object can have visibility toggled conditionally', async ({ page }) => {
|
||||
await page.getByLabel('Edit Object').click();
|
||||
|
||||
// Add Image Drawing Object to the layout
|
||||
await page.getByLabel('Add Drawing Object').click();
|
||||
await page.getByLabel('Image').click();
|
||||
await page.getByLabel('Image URL').fill(TINY_IMAGE_BASE64);
|
||||
await page.getByText('Ok').click();
|
||||
|
||||
// Use the "Test Condition Set" for conditional styling on the image
|
||||
await page.getByRole('tab', { name: 'Styles' }).click();
|
||||
await page.getByRole('button', { name: 'Use Conditional Styling...' }).click();
|
||||
await page.getByLabel('Modal Overlay').getByLabel('Expand My Items folder').click();
|
||||
await page.getByLabel('Modal Overlay').getByLabel('Preview Test Condition Set').click();
|
||||
await page.getByText('Ok').click();
|
||||
|
||||
// Set the image to be hidden when the condition is met
|
||||
await page.getByTitle('Visible').first().click();
|
||||
await page.getByLabel('Save Style').first().click();
|
||||
await page.getByLabel('Save', { exact: true }).click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
// Switch to real-time mode and verify that the image toggles visibility
|
||||
await navigateToObjectWithRealTime(page, displayLayout.url);
|
||||
await expect(page.getByLabel('Image View')).toBeVisible();
|
||||
await expect(page.getByLabel('Image View')).toBeHidden();
|
||||
|
||||
// Reload the page and verify that the image toggles visibility
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
await expect(page.getByLabel('Image View')).toBeVisible();
|
||||
await expect(page.getByLabel('Image View')).toBeHidden();
|
||||
});
|
||||
|
||||
test('Alphanumeric object can have visibility toggled conditionally', async ({ page }) => {
|
||||
await page.getByLabel('Edit Object').click();
|
||||
|
||||
// Add Alphanumeric Object to the layout
|
||||
await page.getByLabel('Expand My Items folder').click();
|
||||
await page.getByLabel('Expand Test Condition Set').click();
|
||||
await page.getByLabel('Preview VIPER Rover Heading').dragTo(page.getByLabel('Layout Grid'));
|
||||
|
||||
// Use the "Test Condition Set" for conditional styling on the alphanumeric
|
||||
await page.getByRole('tab', { name: 'Styles' }).click();
|
||||
await page.getByRole('button', { name: 'Use Conditional Styling...' }).click();
|
||||
await page.getByLabel('Modal Overlay').getByLabel('Expand My Items folder').click();
|
||||
await page.getByLabel('Modal Overlay').getByLabel('Preview Test Condition Set').click();
|
||||
await page.getByText('Ok').click();
|
||||
|
||||
// Set the alphanumeric to be hidden when the condition is met
|
||||
await page.getByTitle('Visible').first().click();
|
||||
await page.getByLabel('Save Style').first().click();
|
||||
await page.getByLabel('Save', { exact: true }).click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
// Switch to real-time mode and verify that the image toggles visibility
|
||||
await navigateToObjectWithRealTime(page, displayLayout.url);
|
||||
await expect(page.getByLabel('Alpha-numeric telemetry', { exact: true })).toBeVisible();
|
||||
await expect(page.getByLabel('Alpha-numeric telemetry', { exact: true })).toBeHidden();
|
||||
|
||||
// Reload the page and verify that the alphanumeric toggles visibility
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
await expect(page.getByLabel('Alpha-numeric telemetry', { exact: true })).toBeVisible();
|
||||
await expect(page.getByLabel('Alpha-numeric telemetry', { exact: true })).toBeHidden();
|
||||
});
|
||||
});
|
@ -57,7 +57,7 @@ test.describe('Tabs View', () => {
|
||||
await page.goto(tabsView.url);
|
||||
|
||||
// select first tab
|
||||
await page.getByLabel(`${table.name} tab`, { exact: true }).click();
|
||||
await page.getByLabel(`${table.name} tab - selected`, { exact: true }).click();
|
||||
// ensure table header visible
|
||||
await expect(page.getByRole('searchbox', { name: 'message filter input' })).toBeVisible();
|
||||
|
||||
@ -92,6 +92,38 @@ test.describe('Tabs View', () => {
|
||||
// no canvas (i.e., sine wave generator) in the document should be visible
|
||||
await expect(page.locator('canvas[id=webglContext]')).toBeHidden();
|
||||
});
|
||||
|
||||
test('Changing the displayed tab should not be persisted if the view is locked', async ({
|
||||
page
|
||||
}) => {
|
||||
await page.goto(tabsView.url);
|
||||
//lock the view
|
||||
await page.getByLabel('Unlocked for editing, click to lock.', { exact: true }).click();
|
||||
// get the initial tab index
|
||||
const initialTab = page.getByLabel(/- selected/);
|
||||
// switch to a different tab in the view
|
||||
const swgTab = page.getByLabel(`${sineWaveGenerator.name} tab`, { exact: true });
|
||||
await swgTab.click();
|
||||
await page.getByLabel(`${sineWaveGenerator.name} Object View`).isVisible();
|
||||
// navigate away from the tabbed view and back
|
||||
await page.getByRole('treeitem', { name: 'My Items' }).click();
|
||||
await page.goto(tabsView.url);
|
||||
// check that the initial tab is displayed
|
||||
const lockedSelectedTab = page.getByLabel(/- selected/);
|
||||
await expect(lockedSelectedTab).toHaveText(await initialTab.textContent());
|
||||
|
||||
//unlock the view
|
||||
await page.getByLabel('Locked for editing. Click to unlock.', { exact: true }).click();
|
||||
// switch to a different tab in the view
|
||||
await swgTab.click();
|
||||
await page.getByLabel(`${sineWaveGenerator.name} Object View`).isVisible();
|
||||
// navigate away from the tabbed view and back
|
||||
await page.getByRole('treeitem', { name: 'My Items' }).click();
|
||||
await page.goto(tabsView.url);
|
||||
// check that the newly selected tab is displayed
|
||||
const unlockedSelectedTab = page.getByLabel(/- selected/);
|
||||
await expect(unlockedSelectedTab).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Tabs View CRUD', () => {
|
||||
|
@ -117,7 +117,8 @@ test.describe('Telemetry Table', () => {
|
||||
|
||||
endTimeStamp.setUTCMinutes(endTimeStamp.getUTCMinutes() - 5);
|
||||
const endDate = endTimeStamp.toISOString().split('T')[0];
|
||||
const endTime = endTimeStamp.toISOString().split('T')[1];
|
||||
const milliseconds = endTimeStamp.getMilliseconds();
|
||||
const endTime = endTimeStamp.toISOString().split('T')[1].replace(`.${milliseconds}Z`, '');
|
||||
|
||||
await setTimeConductorBounds(page, { endDate, endTime });
|
||||
|
||||
|
@ -24,65 +24,210 @@ import {
|
||||
setEndOffset,
|
||||
setFixedTimeMode,
|
||||
setRealTimeMode,
|
||||
setStartOffset,
|
||||
setTimeConductorBounds
|
||||
setStartOffset
|
||||
} from '../../../../appActions.js';
|
||||
import { expect, test } from '../../../../pluginFixtures.js';
|
||||
|
||||
test.describe('Time conductor operations', () => {
|
||||
test('validate start time does not exceed end time', async ({ page }) => {
|
||||
const DAY = '2024-01-01';
|
||||
const DAY_AFTER = '2024-01-02';
|
||||
const ONE_O_CLOCK = '01:00:00';
|
||||
const TWO_O_CLOCK = '02:00:00';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
const year = new Date().getFullYear();
|
||||
});
|
||||
|
||||
// Set initial valid time bounds
|
||||
const startDate = `${year}-01-01`;
|
||||
const startTime = '01:00:00';
|
||||
const endDate = `${year}-01-01`;
|
||||
const endTime = '02:00:00';
|
||||
await setTimeConductorBounds(page, { startDate, startTime, endDate, endTime });
|
||||
test('validate date and time inputs are validated on input event', async ({ page }) => {
|
||||
const submitButtonLocator = page.getByLabel('Submit time bounds');
|
||||
|
||||
// Open the time conductor popup
|
||||
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
|
||||
|
||||
// Test invalid start date
|
||||
const invalidStartDate = `${year}-01-02`;
|
||||
await test.step('invalid start date disables submit button', async () => {
|
||||
const initialStartDate = await page.getByLabel('Start date').inputValue();
|
||||
const invalidStartDate = `${initialStartDate.substring(0, 5)}${initialStartDate.substring(6)}`;
|
||||
|
||||
await page.getByLabel('Start date').fill(invalidStartDate);
|
||||
await expect(page.getByLabel('Submit time bounds')).toBeDisabled();
|
||||
await page.getByLabel('Start date').fill(startDate);
|
||||
await expect(page.getByLabel('Submit time bounds')).toBeEnabled();
|
||||
await expect(submitButtonLocator).toBeDisabled();
|
||||
await page.getByLabel('Start date').fill(initialStartDate);
|
||||
await expect(submitButtonLocator).toBeEnabled();
|
||||
});
|
||||
|
||||
// Test invalid end date
|
||||
const invalidEndDate = `${year - 1}-12-31`;
|
||||
await page.getByLabel('End date').fill(invalidEndDate);
|
||||
await expect(page.getByLabel('Submit time bounds')).toBeDisabled();
|
||||
await page.getByLabel('End date').fill(endDate);
|
||||
await expect(page.getByLabel('Submit time bounds')).toBeEnabled();
|
||||
await test.step('invalid start time disables submit button', async () => {
|
||||
const initialStartTime = await page.getByLabel('Start time').inputValue();
|
||||
const invalidStartTime = `${initialStartTime.substring(0, 5)}${initialStartTime.substring(6)}`;
|
||||
|
||||
// Test invalid start time
|
||||
const invalidStartTime = '42:00:00';
|
||||
await page.getByLabel('Start time').fill(invalidStartTime);
|
||||
await expect(page.getByLabel('Submit time bounds')).toBeDisabled();
|
||||
await page.getByLabel('Start time').fill(startTime);
|
||||
await expect(page.getByLabel('Submit time bounds')).toBeEnabled();
|
||||
await expect(submitButtonLocator).toBeDisabled();
|
||||
await page.getByLabel('Start time').fill(initialStartTime);
|
||||
await expect(submitButtonLocator).toBeEnabled();
|
||||
});
|
||||
|
||||
await test.step('disable/enable submit button also works with multiple invalid inputs', async () => {
|
||||
const initialEndDate = await page.getByLabel('End date').inputValue();
|
||||
const invalidEndDate = `${initialEndDate.substring(0, 5)}${initialEndDate.substring(6)}`;
|
||||
const initialStartTime = await page.getByLabel('Start time').inputValue();
|
||||
const invalidStartTime = `${initialStartTime.substring(0, 5)}${initialStartTime.substring(6)}`;
|
||||
|
||||
await page.getByLabel('Start time').fill(invalidStartTime);
|
||||
await expect(submitButtonLocator).toBeDisabled();
|
||||
await page.getByLabel('End date').fill(invalidEndDate);
|
||||
await expect(submitButtonLocator).toBeDisabled();
|
||||
await page.getByLabel('End date').fill(initialEndDate);
|
||||
await expect(submitButtonLocator).toBeDisabled();
|
||||
await page.getByLabel('Start time').fill(initialStartTime);
|
||||
await expect(submitButtonLocator).toBeEnabled();
|
||||
});
|
||||
});
|
||||
|
||||
test('validate date and time inputs validation is reported on change event', async ({ page }) => {
|
||||
// Open the time conductor popup
|
||||
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
|
||||
|
||||
await test.step('invalid start date is reported on change event, not on input event', async () => {
|
||||
const initialStartDate = await page.getByLabel('Start date').inputValue();
|
||||
const invalidStartDate = `${initialStartDate.substring(0, 5)}${initialStartDate.substring(6)}`;
|
||||
|
||||
await page.getByLabel('Start date').fill(invalidStartDate);
|
||||
await expect(page.getByLabel('Start date')).not.toHaveAttribute('title', 'Invalid Date');
|
||||
await page.getByLabel('Start date').press('Tab');
|
||||
await expect(page.getByLabel('Start date')).toHaveAttribute('title', 'Invalid Date');
|
||||
await page.getByLabel('Start date').fill(initialStartDate);
|
||||
await expect(page.getByLabel('Start date')).not.toHaveAttribute('title', 'Invalid Date');
|
||||
});
|
||||
|
||||
await test.step('invalid start time is reported on change event, not on input event', async () => {
|
||||
const initialStartTime = await page.getByLabel('Start time').inputValue();
|
||||
const invalidStartTime = `${initialStartTime.substring(0, 5)}${initialStartTime.substring(6)}`;
|
||||
|
||||
await page.getByLabel('Start time').fill(invalidStartTime);
|
||||
await expect(page.getByLabel('Start time')).not.toHaveAttribute('title', 'Invalid Time');
|
||||
await page.getByLabel('Start time').press('Tab');
|
||||
await expect(page.getByLabel('Start time')).toHaveAttribute('title', 'Invalid Time');
|
||||
await page.getByLabel('Start time').fill(initialStartTime);
|
||||
await expect(page.getByLabel('Start time')).not.toHaveAttribute('title', 'Invalid Time');
|
||||
});
|
||||
|
||||
await test.step('invalid end date is reported on change event, not on input event', async () => {
|
||||
const initialEndDate = await page.getByLabel('End date').inputValue();
|
||||
const invalidEndDate = `${initialEndDate.substring(0, 5)}${initialEndDate.substring(6)}`;
|
||||
|
||||
await page.getByLabel('End date').fill(invalidEndDate);
|
||||
await expect(page.getByLabel('End date')).not.toHaveAttribute('title', 'Invalid Date');
|
||||
await page.getByLabel('End date').press('Tab');
|
||||
await expect(page.getByLabel('End date')).toHaveAttribute('title', 'Invalid Date');
|
||||
await page.getByLabel('End date').fill(initialEndDate);
|
||||
await expect(page.getByLabel('End date')).not.toHaveAttribute('title', 'Invalid Date');
|
||||
});
|
||||
|
||||
await test.step('invalid end time is reported on change event, not on input event', async () => {
|
||||
const initialEndTime = await page.getByLabel('End time').inputValue();
|
||||
const invalidEndTime = `${initialEndTime.substring(0, 5)}${initialEndTime.substring(6)}`;
|
||||
|
||||
// Test invalid end time
|
||||
const invalidEndTime = '43:00:00';
|
||||
await page.getByLabel('End time').fill(invalidEndTime);
|
||||
await expect(page.getByLabel('Submit time bounds')).toBeDisabled();
|
||||
await page.getByLabel('End time').fill(endTime);
|
||||
await expect(page.getByLabel('Submit time bounds')).toBeEnabled();
|
||||
await expect(page.getByLabel('End time')).not.toHaveAttribute('title', 'Invalid Time');
|
||||
await page.getByLabel('End time').press('Tab');
|
||||
await expect(page.getByLabel('End time')).toHaveAttribute('title', 'Invalid Time');
|
||||
await page.getByLabel('End time').fill(initialEndTime);
|
||||
await expect(page.getByLabel('End time')).not.toHaveAttribute('title', 'Invalid Time');
|
||||
});
|
||||
});
|
||||
|
||||
// Submit valid time bounds
|
||||
test('validate start time does not exceed end time on submit', async ({ page }) => {
|
||||
// Open the time conductor popup
|
||||
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
|
||||
|
||||
// FIXME: https://github.com/nasa/openmct/pull/7818
|
||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await page.getByLabel('Start date').fill(DAY);
|
||||
await page.getByLabel('Start time').fill(TWO_O_CLOCK);
|
||||
await page.getByLabel('End date').fill(DAY);
|
||||
await page.getByLabel('End time').fill(ONE_O_CLOCK);
|
||||
await page.getByLabel('Submit time bounds').click();
|
||||
|
||||
// Verify the submitted time bounds
|
||||
await expect(page.getByLabel('Start bounds')).toHaveText(
|
||||
new RegExp(`${startDate} ${startTime}.000Z`)
|
||||
await expect(page.getByLabel('Start date')).toHaveAttribute(
|
||||
'title',
|
||||
'Specified start date exceeds end bound'
|
||||
);
|
||||
await expect(page.getByLabel('End bounds')).toHaveText(
|
||||
new RegExp(`${endDate} ${endTime}.000Z`)
|
||||
await expect(page.getByLabel('Start bounds')).not.toHaveText(`${DAY} ${TWO_O_CLOCK}.000Z`);
|
||||
await expect(page.getByLabel('End bounds')).not.toHaveText(`${DAY} ${ONE_O_CLOCK}.000Z`);
|
||||
|
||||
await page.getByLabel('Start date').fill(DAY);
|
||||
await page.getByLabel('Start time').fill(ONE_O_CLOCK);
|
||||
await page.getByLabel('End date').fill(DAY);
|
||||
await page.getByLabel('End time').fill(TWO_O_CLOCK);
|
||||
await page.getByLabel('Submit time bounds').click();
|
||||
|
||||
await expect(page.getByLabel('Start bounds')).toHaveText(`${DAY} ${ONE_O_CLOCK}.000Z`);
|
||||
await expect(page.getByLabel('End bounds')).toHaveText(`${DAY} ${TWO_O_CLOCK}.000Z`);
|
||||
});
|
||||
|
||||
test('validate start datetime does not exceed end datetime on submit', async ({ page }) => {
|
||||
// Open the time conductor popup
|
||||
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
|
||||
|
||||
// FIXME: https://github.com/nasa/openmct/pull/7818
|
||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await page.getByLabel('Start date').fill(DAY_AFTER);
|
||||
await page.getByLabel('Start time').fill(ONE_O_CLOCK);
|
||||
await page.getByLabel('End date').fill(DAY);
|
||||
await page.getByLabel('End time').fill(ONE_O_CLOCK);
|
||||
await page.getByLabel('Submit time bounds').click();
|
||||
|
||||
await expect(page.getByLabel('Start date')).toHaveAttribute(
|
||||
'title',
|
||||
'Specified start date exceeds end bound'
|
||||
);
|
||||
await expect(page.getByLabel('Start bounds')).not.toHaveText(
|
||||
`${DAY_AFTER} ${ONE_O_CLOCK}.000Z`
|
||||
);
|
||||
await expect(page.getByLabel('End bounds')).not.toHaveText(`${DAY} ${ONE_O_CLOCK}.000Z`);
|
||||
|
||||
await page.getByLabel('Start date').fill(DAY);
|
||||
await page.getByLabel('Start time').fill(ONE_O_CLOCK);
|
||||
await page.getByLabel('End date').fill(DAY_AFTER);
|
||||
await page.getByLabel('End time').fill(ONE_O_CLOCK);
|
||||
await page.getByLabel('Submit time bounds').click();
|
||||
|
||||
await expect(page.getByLabel('Start bounds')).toHaveText(`${DAY} ${ONE_O_CLOCK}.000Z`);
|
||||
await expect(page.getByLabel('End bounds')).toHaveText(`${DAY_AFTER} ${ONE_O_CLOCK}.000Z`);
|
||||
});
|
||||
|
||||
test('cancelling form does not set bounds', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/7791'
|
||||
});
|
||||
|
||||
// Open the time conductor popup
|
||||
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
|
||||
|
||||
await page.getByLabel('Start date').fill(DAY);
|
||||
await page.getByLabel('Start time').fill(ONE_O_CLOCK);
|
||||
await page.getByLabel('End date').fill(DAY_AFTER);
|
||||
await page.getByLabel('End time').fill(ONE_O_CLOCK);
|
||||
await page.getByLabel('Discard changes and close time popup').click();
|
||||
|
||||
await expect(page.getByLabel('Start bounds')).not.toHaveText(`${DAY} ${ONE_O_CLOCK}.000Z`);
|
||||
await expect(page.getByLabel('End bounds')).not.toHaveText(`${DAY_AFTER} ${ONE_O_CLOCK}.000Z`);
|
||||
|
||||
// Open the time conductor popup
|
||||
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
|
||||
|
||||
await page.getByLabel('Start date').fill(DAY);
|
||||
await page.getByLabel('Start time').fill(ONE_O_CLOCK);
|
||||
await page.getByLabel('End date').fill(DAY_AFTER);
|
||||
await page.getByLabel('End time').fill(ONE_O_CLOCK);
|
||||
await page.getByLabel('Submit time bounds').click();
|
||||
|
||||
await expect(page.getByLabel('Start bounds')).toHaveText(`${DAY} ${ONE_O_CLOCK}.000Z`);
|
||||
await expect(page.getByLabel('End bounds')).toHaveText(`${DAY_AFTER} ${ONE_O_CLOCK}.000Z`);
|
||||
});
|
||||
});
|
||||
|
||||
@ -131,77 +276,6 @@ test.describe('Global Time Conductor', () => {
|
||||
await expect(page.getByLabel('End offset: 01:30:31')).toBeVisible();
|
||||
});
|
||||
|
||||
test('Input field validation: fixed time mode', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/7791'
|
||||
});
|
||||
// Switch to fixed time mode
|
||||
await setFixedTimeMode(page);
|
||||
|
||||
// Define valid time bounds for testing
|
||||
const validBounds = {
|
||||
startDate: '2024-04-20',
|
||||
startTime: '00:04:20',
|
||||
endDate: '2024-04-20',
|
||||
endTime: '16:04:20'
|
||||
};
|
||||
// Set valid time conductor bounds ✌️
|
||||
await setTimeConductorBounds(page, validBounds);
|
||||
|
||||
// Verify that the time bounds are set correctly
|
||||
await expect(page.getByLabel(`Start bounds: 2024-04-20 00:04:20.000Z`)).toBeVisible();
|
||||
await expect(page.getByLabel(`End bounds: 2024-04-20 16:04:20.000Z`)).toBeVisible();
|
||||
|
||||
// Open the Time Conductor Mode popup
|
||||
await page.getByLabel('Time Conductor Mode').click();
|
||||
|
||||
// Test invalid start date
|
||||
const invalidStartDate = '2024-04-21';
|
||||
await page.getByLabel('Start date').fill(invalidStartDate);
|
||||
await expect(page.getByLabel('Submit time bounds')).toBeDisabled();
|
||||
await page.getByLabel('Start date').fill(validBounds.startDate);
|
||||
await expect(page.getByLabel('Submit time bounds')).toBeEnabled();
|
||||
|
||||
// Test invalid end date
|
||||
const invalidEndDate = '2024-04-19';
|
||||
await page.getByLabel('End date').fill(invalidEndDate);
|
||||
await expect(page.getByLabel('Submit time bounds')).toBeDisabled();
|
||||
await page.getByLabel('End date').fill(validBounds.endDate);
|
||||
await expect(page.getByLabel('Submit time bounds')).toBeEnabled();
|
||||
|
||||
// Test invalid start time
|
||||
const invalidStartTime = '16:04:21';
|
||||
await page.getByLabel('Start time').fill(invalidStartTime);
|
||||
await expect(page.getByLabel('Submit time bounds')).toBeDisabled();
|
||||
await page.getByLabel('Start time').fill(validBounds.startTime);
|
||||
await expect(page.getByLabel('Submit time bounds')).toBeEnabled();
|
||||
|
||||
// Test invalid end time
|
||||
const invalidEndTime = '00:04:19';
|
||||
await page.getByLabel('End time').fill(invalidEndTime);
|
||||
await expect(page.getByLabel('Submit time bounds')).toBeDisabled();
|
||||
await page.getByLabel('End time').fill(validBounds.endTime);
|
||||
await expect(page.getByLabel('Submit time bounds')).toBeEnabled();
|
||||
|
||||
// Verify that the time bounds remain unchanged after invalid inputs
|
||||
await expect(page.getByLabel(`Start bounds: 2024-04-20 00:04:20.000Z`)).toBeVisible();
|
||||
await expect(page.getByLabel(`End bounds: 2024-04-20 16:04:20.000Z`)).toBeVisible();
|
||||
|
||||
// Discard changes and verify that bounds remain unchanged
|
||||
await setTimeConductorBounds(page, {
|
||||
startDate: validBounds.startDate,
|
||||
startTime: '04:20:00',
|
||||
endDate: validBounds.endDate,
|
||||
endTime: '04:20:20',
|
||||
submitChanges: false
|
||||
});
|
||||
|
||||
// Verify that the original time bounds are still displayed after discarding changes
|
||||
await expect(page.getByLabel(`Start bounds: 2024-04-20 00:04:20.000Z`)).toBeVisible();
|
||||
await expect(page.getByLabel(`End bounds: 2024-04-20 16:04:20.000Z`)).toBeVisible();
|
||||
});
|
||||
|
||||
/**
|
||||
* Verify that offsets and url params are preserved when switching
|
||||
* between fixed timespan and real-time mode.
|
||||
|
@ -168,7 +168,7 @@ test.describe('Grand Search', () => {
|
||||
await expect(page.getByText('No results found')).toBeVisible();
|
||||
});
|
||||
|
||||
test('Validate single object in search result @couchdb', async ({ page }) => {
|
||||
test('Validate single object in search result @couchdb @network', async ({ page }) => {
|
||||
// Create a folder object
|
||||
const folderName = uuid();
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
@ -191,7 +191,7 @@ test.describe('Grand Search', () => {
|
||||
await expect(searchResults).toContainText(folderName);
|
||||
});
|
||||
|
||||
test('Search results are debounced @couchdb', async ({ page }) => {
|
||||
test('Search results are debounced @couchdb @network', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/6179'
|
||||
@ -221,7 +221,9 @@ test.describe('Grand Search', () => {
|
||||
await expect(page.getByRole('list', { name: 'Object Results' })).toContainText('Clock A');
|
||||
});
|
||||
|
||||
test('Slowly typing after search debounce will abort requests @couchdb', async ({ page }) => {
|
||||
test('Slowly typing after search debounce will abort requests @couchdb @network', async ({
|
||||
page
|
||||
}) => {
|
||||
let requestWasAborted = false;
|
||||
await createObjectsForSearch(page);
|
||||
page.on('requestfailed', (request) => {
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2023, United States Government
|
||||
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
|
@ -28,7 +28,7 @@ test.describe('Main Tree', () => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
});
|
||||
|
||||
test('Creating a child object within a folder and immediately opening it shows the created object in the tree @couchdb', async ({
|
||||
test('Creating a child object within a folder and immediately opening it shows the created object in the tree @couchdb @network', async ({
|
||||
page
|
||||
}) => {
|
||||
test.info().annotations.push({
|
||||
@ -88,7 +88,7 @@ test.describe('Main Tree', () => {
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('Creating a child object on one tab and expanding its parent on the other shows the correct composition @couchdb @2p', async ({
|
||||
test('Creating a child object on one tab and expanding its parent on the other shows the correct composition @couchdb @network @2p', async ({
|
||||
page
|
||||
}) => {
|
||||
test.info().annotations.push({
|
||||
@ -187,7 +187,7 @@ test.describe('Main Tree', () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
test('Opening and closing an item before the request has been fulfilled will abort the request @couchdb', async ({
|
||||
test('Opening and closing an item before the request has been fulfilled will abort the request @couchdb @network', async ({
|
||||
page
|
||||
}) => {
|
||||
let requestWasAborted = false;
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2023, United States Government
|
||||
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
|
@ -164,7 +164,7 @@ test.describe('Performance tests', () => {
|
||||
await page.evaluate(() => window.performance.mark('background-image-visible'));
|
||||
|
||||
// Get Current number of images in thumbstrip
|
||||
await page.locator('.c-imagery__thumb').waitFor({ state: 'visible' });
|
||||
await expect(page.locator('.c-imagery__thumb').last()).toBeInViewport();
|
||||
const thumbCount = await page.locator('.c-imagery__thumb').count();
|
||||
console.log('number of thumbs rendered ' + thumbCount);
|
||||
await page.locator('.c-imagery__thumb').last().click();
|
||||
|
@ -64,7 +64,7 @@ test.describe('Tabs View', () => {
|
||||
page.goto(tabsView.url);
|
||||
|
||||
// select first tab
|
||||
await page.getByLabel(`${table.name} tab`, { exact: true }).click();
|
||||
await page.getByLabel(`${table.name} tab - selected`, { exact: true }).click();
|
||||
// ensure table header visible
|
||||
await expect(page.getByRole('searchbox', { name: 'message filter input' })).toBeVisible();
|
||||
|
||||
|
@ -85,16 +85,6 @@ test.describe('Visual - Default @a11y', () => {
|
||||
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) => {
|
||||
await scanForA11yViolations(page, testInfo.title);
|
||||
});
|
||||
|
@ -22,7 +22,11 @@
|
||||
|
||||
import percySnapshot from '@percy/playwright';
|
||||
|
||||
import { createDomainObjectWithDefaults } from '../../appActions.js';
|
||||
import {
|
||||
createDomainObjectWithDefaults,
|
||||
createStableStateTelemetry,
|
||||
linkParameterToObject
|
||||
} from '../../appActions.js';
|
||||
import { MISSION_TIME, VISUAL_FIXED_URL } from '../../constants.js';
|
||||
import { test } from '../../pluginFixtures.js';
|
||||
|
||||
@ -47,16 +51,13 @@ test.describe('Visual - Display Layout @clock', () => {
|
||||
name: 'Child Right Layout',
|
||||
parent: parentLayout.uuid
|
||||
});
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
name: 'SWG 1',
|
||||
parent: child1Layout.uuid
|
||||
});
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
name: 'SWG 2',
|
||||
parent: child2Layout.uuid
|
||||
});
|
||||
|
||||
const stableStateTelemetry = await createStableStateTelemetry(page);
|
||||
await linkParameterToObject(page, stableStateTelemetry.name, child1Layout.name);
|
||||
await linkParameterToObject(page, stableStateTelemetry.name, child2Layout.name);
|
||||
|
||||
// Pause the clock at a time where the telemetry is stable 20 minutes in the future
|
||||
await page.clock.pauseAt(new Date(MISSION_TIME + 1200000));
|
||||
|
||||
await page.goto(parentLayout.url, { waitUntil: 'domcontentloaded' });
|
||||
await page.getByRole('button', { name: 'Edit Object' }).click();
|
||||
|
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.goto(notebook.url);
|
||||
await page.goto(notebook.url, { waitUntil: 'networkidle' });
|
||||
|
||||
await page
|
||||
.getByLabel('Navigate to Dropped Overlay Plot')
|
||||
|
@ -26,14 +26,25 @@ import fs from 'fs';
|
||||
import { createDomainObjectWithDefaults, createPlanFromJSON } from '../../appActions.js';
|
||||
import { scanForA11yViolations, test } from '../../avpFixtures.js';
|
||||
import { VISUAL_FIXED_URL } from '../../constants.js';
|
||||
import { setBoundsToSpanAllActivities, setDraftStatusForPlan } from '../../helper/planningUtils.js';
|
||||
import {
|
||||
getFirstActivity,
|
||||
setBoundsToSpanAllActivities,
|
||||
setDraftStatusForPlan
|
||||
} from '../../helper/planningUtils.js';
|
||||
|
||||
const examplePlanSmall2 = JSON.parse(
|
||||
fs.readFileSync(new URL('../../test-data/examplePlans/ExamplePlan_Small2.json', import.meta.url))
|
||||
);
|
||||
|
||||
const FIRST_ACTIVITY_SMALL_2 = getFirstActivity(examplePlanSmall2);
|
||||
|
||||
test.describe('Visual - Gantt Chart @a11y', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Set the clock to the end of the first activity in the plan
|
||||
// This is so we can see the "now" line in the plan view
|
||||
await page.clock.install({ time: FIRST_ACTIVITY_SMALL_2.end + 10000 });
|
||||
await page.clock.resume();
|
||||
|
||||
await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });
|
||||
});
|
||||
test('Gantt Chart View', async ({ page, theme }) => {
|
||||
|
@ -27,14 +27,21 @@ import { createDomainObjectWithDefaults, createPlanFromJSON } from '../../appAct
|
||||
import { scanForA11yViolations, test } from '../../avpFixtures.js';
|
||||
import { waitForAnimations } from '../../baseFixtures.js';
|
||||
import { VISUAL_FIXED_URL } from '../../constants.js';
|
||||
import { setBoundsToSpanAllActivities } from '../../helper/planningUtils.js';
|
||||
import { getFirstActivity, setBoundsToSpanAllActivities } from '../../helper/planningUtils.js';
|
||||
|
||||
const examplePlanSmall2 = JSON.parse(
|
||||
fs.readFileSync(new URL('../../test-data/examplePlans/ExamplePlan_Small2.json', import.meta.url))
|
||||
);
|
||||
|
||||
const FIRST_ACTIVITY_SMALL_2 = getFirstActivity(examplePlanSmall2);
|
||||
|
||||
test.describe('Visual - Time Strip @a11y', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Set the clock to the end of the first activity in the plan
|
||||
// This is so we can see the "now" line in the plan view
|
||||
await page.clock.install({ time: FIRST_ACTIVITY_SMALL_2.end + 10000 });
|
||||
await page.clock.resume();
|
||||
|
||||
await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });
|
||||
});
|
||||
test('Time Strip View', async ({ page, theme }) => {
|
||||
|
@ -42,6 +42,7 @@ const examplePlanSmall2 = JSON.parse(
|
||||
);
|
||||
|
||||
const FIRST_ACTIVITY_SMALL_1 = getFirstActivity(examplePlanSmall1);
|
||||
const FIRST_ACTIVITY_SMALL_2 = getFirstActivity(examplePlanSmall2);
|
||||
|
||||
test.describe('Visual - Timelist progress bar @clock @a11y', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
@ -59,6 +60,11 @@ test.describe('Visual - Timelist progress bar @clock @a11y', () => {
|
||||
|
||||
test.describe('Visual - Plan View @a11y', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Set the clock to the end of the first activity in the plan
|
||||
// This is so we can see the "now" line in the plan view
|
||||
await page.clock.install({ time: FIRST_ACTIVITY_SMALL_2.end + 10000 });
|
||||
await page.clock.resume();
|
||||
|
||||
await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });
|
||||
});
|
||||
|
||||
|
@ -53,7 +53,7 @@ test.describe('Grand Search @a11y', () => {
|
||||
theme
|
||||
}) => {
|
||||
// Navigate to display layout
|
||||
await page.goto(displayLayout.url);
|
||||
await page.goto(displayLayout.url, { waitUntil: 'networkidle' });
|
||||
|
||||
// Search for the object
|
||||
await page.getByRole('searchbox', { name: 'Search Input' }).click();
|
||||
|
@ -46,6 +46,24 @@ class EventMetadataProvider {
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const inPlaceUpdateMetadataValue = {
|
||||
key: 'messageId',
|
||||
name: 'row identifier',
|
||||
format: 'string',
|
||||
useToUpdateInPlace: true
|
||||
};
|
||||
const eventAcknowledgeMetadataValue = {
|
||||
key: 'acknowledge',
|
||||
name: 'Acknowledge',
|
||||
format: 'string'
|
||||
};
|
||||
|
||||
const eventGeneratorWithAcknowledge = structuredClone(this.METADATA_BY_TYPE.eventGenerator);
|
||||
eventGeneratorWithAcknowledge.values.push(inPlaceUpdateMetadataValue);
|
||||
eventGeneratorWithAcknowledge.values.push(eventAcknowledgeMetadataValue);
|
||||
|
||||
this.METADATA_BY_TYPE.eventGeneratorWithAcknowledge = eventGeneratorWithAcknowledge;
|
||||
}
|
||||
|
||||
supportsMetadata(domainObject) {
|
||||
|
@ -0,0 +1,70 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* Module defining EventTelemetryProvider. Created by chacskaylo on 06/18/2015.
|
||||
*/
|
||||
|
||||
import EventTelemetryProvider from './EventTelemetryProvider.js';
|
||||
|
||||
class EventWithAcknowledgeTelemetryProvider extends EventTelemetryProvider {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.unAcknowledgedData = undefined;
|
||||
}
|
||||
|
||||
generateData(firstObservedTime, count, startTime, duration, name) {
|
||||
if (this.unAcknowledgedData === undefined) {
|
||||
const unAcknowledgedData = super.generateData(
|
||||
firstObservedTime,
|
||||
count,
|
||||
startTime,
|
||||
duration,
|
||||
name
|
||||
);
|
||||
unAcknowledgedData.messageId = unAcknowledgedData.message;
|
||||
this.unAcknowledgedData = unAcknowledgedData;
|
||||
|
||||
return this.unAcknowledgedData;
|
||||
} else {
|
||||
const acknowledgedData = {
|
||||
...this.unAcknowledgedData,
|
||||
acknowledge: 'OK'
|
||||
};
|
||||
|
||||
this.unAcknowledgedData = undefined;
|
||||
|
||||
return acknowledgedData;
|
||||
}
|
||||
}
|
||||
|
||||
supportsRequest(domainObject) {
|
||||
return false;
|
||||
}
|
||||
|
||||
supportsSubscribe(domainObject) {
|
||||
return domainObject.type === 'eventGeneratorWithAcknowledge';
|
||||
}
|
||||
}
|
||||
|
||||
export default EventWithAcknowledgeTelemetryProvider;
|
@ -21,6 +21,7 @@
|
||||
*****************************************************************************/
|
||||
import EventMetadataProvider from './EventMetadataProvider.js';
|
||||
import EventTelemetryProvider from './EventTelemetryProvider.js';
|
||||
import EventWithAcknowledgeTelemetryProvider from './EventWithAcknowledgeTelemetryProvider.js';
|
||||
|
||||
export default function EventGeneratorPlugin(options) {
|
||||
return function install(openmct) {
|
||||
@ -38,5 +39,20 @@ export default function EventGeneratorPlugin(options) {
|
||||
});
|
||||
openmct.telemetry.addProvider(new EventTelemetryProvider());
|
||||
openmct.telemetry.addProvider(new EventMetadataProvider());
|
||||
|
||||
openmct.types.addType('eventGeneratorWithAcknowledge', {
|
||||
name: 'Event Message Generator with Acknowledge',
|
||||
description:
|
||||
'For development use. Creates sample event message data stream and updates the event row with an acknowledgement.',
|
||||
cssClass: 'icon-generator-events',
|
||||
creatable: true,
|
||||
initialize: function (object) {
|
||||
object.telemetry = {
|
||||
duration: 2.5
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
openmct.telemetry.addProvider(new EventWithAcknowledgeTelemetryProvider());
|
||||
};
|
||||
}
|
||||
|
@ -20,6 +20,7 @@
|
||||
* 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';
|
||||
|
||||
export default function (staticFaults = false) {
|
||||
@ -56,6 +57,9 @@ export default function (staticFaults = false) {
|
||||
return Promise.resolve({
|
||||
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 MOONWALK_TIMESTAMP = 14159040000;
|
||||
const NAMESPACE = '/Example/fault-';
|
||||
const getRandom = {
|
||||
severity: () => SEVERITIES[Math.floor(Math.random() * 3)],
|
||||
@ -13,7 +36,8 @@ const getRandom = {
|
||||
|
||||
val = num;
|
||||
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 {
|
||||
@ -43,14 +67,7 @@ const getRandom = {
|
||||
}
|
||||
};
|
||||
|
||||
export function shelveFault(
|
||||
fault,
|
||||
opts = {
|
||||
shelved: true,
|
||||
comment: '',
|
||||
shelveDuration: 90000
|
||||
}
|
||||
) {
|
||||
export function shelveFault(fault, opts = { shelved: true, comment: '', shelveDuration: 90000 }) {
|
||||
fault.shelved = true;
|
||||
|
||||
setTimeout(() => {
|
||||
@ -65,8 +82,8 @@ export function acknowledgeFault(fault) {
|
||||
export function randomFaults(staticFaults, count = 5) {
|
||||
let faults = [];
|
||||
|
||||
for (let x = 1, y = count + 1; x < y; x++) {
|
||||
faults.push(getRandom.fault(x, staticFaults));
|
||||
for (let i = 1; i <= count; i++) {
|
||||
faults.push(getRandom.fault(i, staticFaults));
|
||||
}
|
||||
|
||||
return faults;
|
||||
|
@ -108,6 +108,16 @@ const METADATA_BY_TYPE = {
|
||||
string: 'ON'
|
||||
}
|
||||
],
|
||||
filters: [
|
||||
{
|
||||
singleSelectionThreshold: true,
|
||||
comparator: 'equals',
|
||||
possibleValues: [
|
||||
{ label: 'OFF', value: 0 },
|
||||
{ label: 'ON', value: 1 }
|
||||
]
|
||||
}
|
||||
],
|
||||
hints: {
|
||||
range: 1
|
||||
}
|
||||
|
@ -34,14 +34,16 @@ StateGeneratorProvider.prototype.supportsSubscribe = function (domainObject) {
|
||||
return domainObject.type === 'example.state-generator';
|
||||
};
|
||||
|
||||
StateGeneratorProvider.prototype.subscribe = function (domainObject, callback) {
|
||||
StateGeneratorProvider.prototype.subscribe = function (domainObject, callback, options) {
|
||||
var duration = domainObject.telemetry.duration * 1000;
|
||||
|
||||
var interval = setInterval(function () {
|
||||
var interval = setInterval(() => {
|
||||
var now = Date.now();
|
||||
var datum = pointForTimestamp(now, duration, domainObject.name);
|
||||
if (!this.shouldBeFiltered(datum, options)) {
|
||||
datum.value = String(datum.value);
|
||||
callback(datum);
|
||||
}
|
||||
}, duration);
|
||||
|
||||
return function () {
|
||||
@ -63,9 +65,25 @@ StateGeneratorProvider.prototype.request = function (domainObject, options) {
|
||||
|
||||
var data = [];
|
||||
while (start <= end && data.length < 5000) {
|
||||
data.push(pointForTimestamp(start, duration, domainObject.name));
|
||||
const point = pointForTimestamp(start, duration, domainObject.name);
|
||||
|
||||
if (!this.shouldBeFiltered(point, options)) {
|
||||
data.push(point);
|
||||
}
|
||||
start += duration;
|
||||
}
|
||||
|
||||
return Promise.resolve(data);
|
||||
};
|
||||
|
||||
StateGeneratorProvider.prototype.shouldBeFiltered = function (point, options) {
|
||||
const valueToFilter = options?.filters?.state?.equals?.[0];
|
||||
|
||||
if (!valueToFilter) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { value } = point;
|
||||
|
||||
return value !== Number(valueToFilter);
|
||||
};
|
||||
|
@ -20,6 +20,8 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import { seededRandom } from 'utils/random.js';
|
||||
|
||||
const DEFAULT_IMAGE_SAMPLES = [
|
||||
'https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg',
|
||||
'https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18732.jpg',
|
||||
@ -162,8 +164,8 @@ export default function () {
|
||||
};
|
||||
}
|
||||
|
||||
function getCompassValues(min, max) {
|
||||
return min + Math.random() * (max - min);
|
||||
function getCompassValues(min, max, timestamp) {
|
||||
return min + seededRandom(timestamp) * (max - min);
|
||||
}
|
||||
|
||||
function getImageSamples(configuration) {
|
||||
@ -283,9 +285,9 @@ function pointForTimestamp(timestamp, name, imageSamples, delay) {
|
||||
utc: Math.floor(timestamp / delay) * delay,
|
||||
local: Math.floor(timestamp / delay) * delay,
|
||||
url,
|
||||
sunOrientation: getCompassValues(0, 360),
|
||||
cameraAzimuth: getCompassValues(0, 360),
|
||||
heading: getCompassValues(0, 360),
|
||||
sunOrientation: getCompassValues(0, 360, timestamp),
|
||||
cameraAzimuth: getCompassValues(0, 360, timestamp),
|
||||
heading: getCompassValues(0, 360, timestamp),
|
||||
transformations: navCamTransformations,
|
||||
imageDownloadName
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2023, United States Government
|
||||
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
|
291
package-lock.json
generated
291
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "openmct",
|
||||
"version": "4.0.0-next",
|
||||
"version": "4.1.0-next",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "openmct",
|
||||
"version": "4.0.0-next",
|
||||
"version": "4.1.0-next",
|
||||
"license": "Apache-2.0",
|
||||
"workspaces": [
|
||||
"e2e"
|
||||
@ -24,7 +24,6 @@
|
||||
"@vue/compiler-sfc": "3.4.3",
|
||||
"babel-loader": "9.1.0",
|
||||
"babel-plugin-istanbul": "6.1.1",
|
||||
"codecov": "3.8.3",
|
||||
"comma-separated-values": "3.6.4",
|
||||
"copy-webpack-plugin": "12.0.2",
|
||||
"cspell": "7.3.8",
|
||||
@ -67,6 +66,7 @@
|
||||
"moment": "2.30.1",
|
||||
"moment-duration-format": "2.3.2",
|
||||
"moment-timezone": "0.5.41",
|
||||
"nano": "10.1.4",
|
||||
"npm-run-all2": "6.1.2",
|
||||
"nyc": "15.1.0",
|
||||
"painterro": "1.2.87",
|
||||
@ -93,18 +93,18 @@
|
||||
"webpack-merge": "5.10.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.14.2 <22"
|
||||
"node": ">=18.14.2 <23"
|
||||
}
|
||||
},
|
||||
"e2e": {
|
||||
"name": "openmct-e2e",
|
||||
"version": "4.0.0-next",
|
||||
"version": "4.1.0-next",
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@axe-core/playwright": "4.8.5",
|
||||
"@percy/cli": "1.27.4",
|
||||
"@percy/playwright": "1.0.4",
|
||||
"@playwright/test": "1.45.2"
|
||||
"@playwright/test": "1.48.1"
|
||||
}
|
||||
},
|
||||
"e2e/node_modules/@percy/cli": {
|
||||
@ -1548,12 +1548,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.45.2",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.45.2.tgz",
|
||||
"integrity": "sha512-JxG9eq92ET75EbVi3s+4sYbcG7q72ECeZNbdBlaMkGcNbiDQ4cAi8U2QP5oKkOx+1gpaiL1LDStmzCaEM1Z6fQ==",
|
||||
"version": "1.48.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.48.1.tgz",
|
||||
"integrity": "sha512-s9RtWoxkOLmRJdw3oFvhFbs9OJS0BzrLUc8Hf6l2UdCNd1rqeEyD4BhCJkvzeEoD1FsK4mirsWwGerhVmYKtZg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.45.2"
|
||||
"playwright": "1.48.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
@ -1586,15 +1587,6 @@
|
||||
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==",
|
||||
"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": {
|
||||
"version": "1.19.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
|
||||
@ -2308,18 +2300,6 @@
|
||||
"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": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
|
||||
@ -2454,16 +2434,6 @@
|
||||
"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": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||
@ -2494,6 +2464,12 @@
|
||||
"@mdn/browser-compat-data": "^5.2.34"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/axe-core": {
|
||||
"version": "4.8.4",
|
||||
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.8.4.tgz",
|
||||
@ -2503,6 +2479,17 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.7.7",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz",
|
||||
"integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-loader": {
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.0.tgz",
|
||||
@ -2797,9 +2784,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001597",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001597.tgz",
|
||||
"integrity": "sha512-7LjJvmQU6Sj7bL0j5b5WY/3n7utXUJvAe1lxhsHDbLmwX9mdL86Yjtr+5SRCyf8qME4M7pU2hswj0FpyBVCv9w==",
|
||||
"version": "1.0.30001660",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001660.tgz",
|
||||
"integrity": "sha512-GacvNTTuATm26qC74pt+ad1fW15mlQ/zuTzzY1ZoIzECTP8HURDfF43kNxPgf7H1jmelCBQTTbBNxdSXOA7Bqg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@ -2814,7 +2801,8 @@
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
]
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "2.4.2",
|
||||
@ -3012,26 +3000,6 @@
|
||||
"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": {
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||
@ -3062,6 +3030,18 @@
|
||||
"node": ">=0.1.90"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/comma-separated-values": {
|
||||
"version": "3.6.4",
|
||||
"resolved": "https://registry.npmjs.org/comma-separated-values/-/comma-separated-values-3.6.4.tgz",
|
||||
@ -4152,6 +4132,15 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/depd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||
@ -5581,21 +5570,6 @@
|
||||
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
|
||||
"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": {
|
||||
"version": "1.0.16",
|
||||
"resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz",
|
||||
@ -5820,6 +5794,20 @@
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
@ -6386,20 +6374,6 @@
|
||||
"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": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz",
|
||||
@ -6430,19 +6404,6 @@
|
||||
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
|
||||
"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": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
|
||||
@ -6485,15 +6446,6 @@
|
||||
"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": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/image-size/-/image-size-1.1.1.tgz",
|
||||
@ -7997,6 +7949,35 @@
|
||||
"multicast-dns": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/nano": {
|
||||
"version": "10.1.4",
|
||||
"resolved": "https://registry.npmjs.org/nano/-/nano-10.1.4.tgz",
|
||||
"integrity": "sha512-bJOFIPLExIbF6mljnfExXX9Cub4W0puhDjVMp+qV40xl/DBvgKao7St4+6/GB6EoHZap7eFnrnx4mnp5KYgwJA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"axios": "^1.7.4",
|
||||
"node-abort-controller": "^3.1.1",
|
||||
"qs": "^6.13.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/nano/node_modules/qs": {
|
||||
"version": "6.13.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"side-channel": "^1.0.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.7",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
|
||||
@ -8036,6 +8017,12 @@
|
||||
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/node-abort-controller": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz",
|
||||
"integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||
@ -8763,12 +8750,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.45.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.45.2.tgz",
|
||||
"integrity": "sha512-ReywF2t/0teRvNBpfIgh5e4wnrI/8Su8ssdo5XsQKpjxJj+jspm00jSoz9BTg91TT0c9HRjXO7LBNVrgYj9X0g==",
|
||||
"version": "1.48.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.1.tgz",
|
||||
"integrity": "sha512-j8CiHW/V6HxmbntOfyB4+T/uk08tBy6ph0MpBXwuoofkSnLmlfdYNNkFTYD6ofzzlSqLA1fwH4vwvVFvJgLN0w==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.45.2"
|
||||
"playwright-core": "1.48.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
@ -8781,10 +8769,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.45.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.45.2.tgz",
|
||||
"integrity": "sha512-ha175tAWb0dTK0X4orvBIqi3jGEt701SMxMhyujxNrgd8K0Uy5wMSwwcQHtyB4om7INUkfndx02XnQ2p6dvLDw==",
|
||||
"version": "1.48.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.1.tgz",
|
||||
"integrity": "sha512-Yw/t4VAFX/bBr1OzwCuOMZkY1Cnb4z/doAFSwf4huqAGWmf9eMNjmK7NiOljCdLmxeRYcGPPmcDgU0zOlzP0YA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
@ -8798,6 +8787,7 @@
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
@ -9283,6 +9273,12 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/pump": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
|
||||
@ -10345,15 +10341,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": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz",
|
||||
@ -10536,12 +10523,6 @@
|
||||
"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": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.3.tgz",
|
||||
@ -10607,31 +10588,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": {
|
||||
"version": "5.29.1",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.29.1.tgz",
|
||||
@ -11012,15 +10968,6 @@
|
||||
"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": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
|
11
package.json
11
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openmct",
|
||||
"version": "4.0.0-next",
|
||||
"version": "4.1.0-next",
|
||||
"description": "The Open MCT core platform",
|
||||
"module": "dist/openmct.js",
|
||||
"main": "dist/openmct.js",
|
||||
@ -27,7 +27,6 @@
|
||||
"@vue/compiler-sfc": "3.4.3",
|
||||
"babel-loader": "9.1.0",
|
||||
"babel-plugin-istanbul": "6.1.1",
|
||||
"codecov": "3.8.3",
|
||||
"comma-separated-values": "3.6.4",
|
||||
"copy-webpack-plugin": "12.0.2",
|
||||
"cspell": "7.3.8",
|
||||
@ -70,6 +69,7 @@
|
||||
"moment": "2.30.1",
|
||||
"moment-duration-format": "2.3.2",
|
||||
"moment-timezone": "0.5.41",
|
||||
"nano": "10.1.4",
|
||||
"npm-run-all2": "6.1.2",
|
||||
"nyc": "15.1.0",
|
||||
"painterro": "1.2.87",
|
||||
@ -128,12 +128,9 @@
|
||||
"test:perf:contract": "npm test --workspace e2e -- --config=playwright-performance-dev.config.js",
|
||||
"test:perf:localhost": "npm test --workspace e2e -- --config=playwright-performance-prod.config.js --project=chrome",
|
||||
"test:perf:memory": "npm test --workspace e2e -- --config=playwright-performance-prod.config.js --project=chrome-memory",
|
||||
"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\\-2024/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'",
|
||||
"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"
|
||||
},
|
||||
"homepage": "https://nasa.github.io/openmct",
|
||||
@ -142,7 +139,7 @@
|
||||
"url": "git+https://github.com/nasa/openmct.git"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.14.2 <22"
|
||||
"node": ">=18.14.2 <23"
|
||||
},
|
||||
"browserslist": [
|
||||
"Firefox ESR",
|
||||
|
@ -20,6 +20,32 @@
|
||||
* 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: 'Unlimited',
|
||||
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 {
|
||||
/**
|
||||
* @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) {
|
||||
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() {
|
||||
return (
|
||||
@ -45,48 +77,82 @@ export default class FaultManagementAPI {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('openmct').DomainObject} domainObject
|
||||
* @returns {Promise.<FaultAPIResponse[]>}
|
||||
* Requests fault data for a given domain object.
|
||||
* 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) {
|
||||
if (!this.provider?.supportsRequest(domainObject)) {
|
||||
return Promise.reject();
|
||||
return Promise.reject('Provider does not support request operation');
|
||||
}
|
||||
|
||||
return this.provider.request(domainObject);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('openmct').DomainObject} domainObject
|
||||
* @param {Function} callback
|
||||
* @returns {Function} unsubscribe
|
||||
* Subscribes to fault data updates for a given domain object.
|
||||
* This method checks if the current provider supports the subscribe operation for the given domain object.
|
||||
* 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) {
|
||||
if (!this.provider?.supportsSubscribe(domainObject)) {
|
||||
return Promise.reject();
|
||||
return Promise.reject('Provider does not support subscribe operation');
|
||||
}
|
||||
|
||||
return this.provider.subscribe(domainObject, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Fault} fault
|
||||
* @param {*} ackData
|
||||
* Acknowledges a fault using the provider's acknowledgeFault method.
|
||||
*
|
||||
* @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) {
|
||||
return this.provider.acknowledgeFault(fault, ackData);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Fault} fault
|
||||
* @param {*} shelveData
|
||||
* @returns {Promise.<T>}
|
||||
* Shelves a fault using the provider's shelveFault method.
|
||||
*
|
||||
* @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) {
|
||||
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[] | undefined}
|
||||
*/
|
||||
getShelveDurations() {
|
||||
if (!this.provider) {
|
||||
return;
|
||||
}
|
||||
|
||||
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 unlimited
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} TriggerValueInfo
|
||||
* @property {number} value
|
||||
|
@ -20,25 +20,46 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import installWorker from './WebSocketWorker.js';
|
||||
|
||||
/**
|
||||
* Describes the strategy to be used when batching WebSocket messages
|
||||
*
|
||||
* @typedef BatchingStrategy
|
||||
* @property {Function} shouldBatchMessage a function that accepts a single
|
||||
* argument - the raw message received from the websocket. Every message
|
||||
* 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.
|
||||
* @typedef RequestIdleCallbackOptions
|
||||
* @prop {Number} timeout If the number of milliseconds represented by this
|
||||
* parameter has elapsed and the callback has not already been called, invoke
|
||||
* the callback.
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback
|
||||
*/
|
||||
|
||||
/**
|
||||
* Provides a reliable and convenient WebSocket abstraction layer that handles
|
||||
* a lot of boilerplate common to managing WebSocket connections such as:
|
||||
* Mocks requestIdleCallback for Safari using setTimeout. Functionality will be
|
||||
* 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
|
||||
* - Reconnecting on error, with a fallback strategy
|
||||
* - 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.
|
||||
*
|
||||
*/
|
||||
// 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 {
|
||||
#worker;
|
||||
#openmct;
|
||||
#showingRateLimitNotification;
|
||||
#maxBatchSize;
|
||||
#applicationIsInitializing;
|
||||
#maxBatchWait;
|
||||
#maxBufferSize;
|
||||
#throttleRate;
|
||||
#firstBatchReceived;
|
||||
#lastBatchReceived;
|
||||
#peakBufferSize = Number.NEGATIVE_INFINITY;
|
||||
|
||||
/**
|
||||
* @param {import('openmct.js').OpenMCT} openmct
|
||||
*/
|
||||
constructor(openmct) {
|
||||
super();
|
||||
// Install worker, register listeners etc.
|
||||
@ -74,9 +92,8 @@ class BatchingWebSocket extends EventTarget {
|
||||
this.#worker = new Worker(workerUrl);
|
||||
this.#openmct = openmct;
|
||||
this.#showingRateLimitNotification = false;
|
||||
this.#maxBatchSize = Number.POSITIVE_INFINITY;
|
||||
this.#maxBatchWait = ONE_SECOND;
|
||||
this.#applicationIsInitializing = true;
|
||||
this.#maxBufferSize = Number.POSITIVE_INFINITY;
|
||||
this.#throttleRate = ONE_SECOND;
|
||||
this.#firstBatchReceived = false;
|
||||
|
||||
const routeMessageToHandler = this.#routeMessageToHandler.bind(this);
|
||||
@ -89,20 +106,6 @@ class BatchingWebSocket extends EventTarget {
|
||||
},
|
||||
{ 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
|
||||
* 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
|
||||
* @param {number} maxBufferSize the maximum length of the receive buffer in characters.
|
||||
* 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.
|
||||
* 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.
|
||||
*
|
||||
* This should be set appropriately for the expected data rate. eg. If telemetry
|
||||
* is received at 10Hz for each telemetry point, then a minimal combination of batch
|
||||
* size and rate is 10 and 1000 respectively. Ideally you would add some margin, so
|
||||
* 15 would probably be a better batch size.
|
||||
* This should be set appropriately for the expected data rate. eg. If typical usage
|
||||
* sees 2000 messages arriving at a client per second, with an average message size
|
||||
* of 500 bytes, then 2000 * 500 = 1000000 characters will be right on the limit.
|
||||
* 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) {
|
||||
this.#maxBatchSize = maxBatchSize;
|
||||
if (!this.#applicationIsInitializing) {
|
||||
this.#sendMaxBatchSizeToWorker(this.#maxBatchSize);
|
||||
setMaxBufferSize(maxBatchSize) {
|
||||
this.#maxBufferSize = maxBatchSize;
|
||||
this.#sendMaxBufferSizeToWorker(this.#maxBufferSize);
|
||||
}
|
||||
setThrottleRate(throttleRate) {
|
||||
this.#throttleRate = throttleRate;
|
||||
this.#sendThrottleRateToWorker(this.#throttleRate);
|
||||
}
|
||||
setMaxBatchWait(wait) {
|
||||
this.#maxBatchWait = wait;
|
||||
this.#sendBatchWaitToWorker(this.#maxBatchWait);
|
||||
}
|
||||
#sendMaxBatchSizeToWorker(maxBatchSize) {
|
||||
setThrottleMessagePattern(throttleMessagePattern) {
|
||||
this.#worker.postMessage({
|
||||
type: 'setMaxBatchSize',
|
||||
maxBatchSize
|
||||
type: 'setThrottleMessagePattern',
|
||||
throttleMessagePattern
|
||||
});
|
||||
}
|
||||
|
||||
#sendBatchWaitToWorker(maxBatchWait) {
|
||||
#sendMaxBufferSizeToWorker(maxBufferSize) {
|
||||
this.#worker.postMessage({
|
||||
type: 'setMaxBatchWait',
|
||||
maxBatchWait
|
||||
type: 'setMaxBufferSize',
|
||||
maxBufferSize
|
||||
});
|
||||
}
|
||||
|
||||
#sendThrottleRateToWorker(throttleRate) {
|
||||
this.#worker.postMessage({
|
||||
type: 'setThrottleRate',
|
||||
throttleRate
|
||||
});
|
||||
}
|
||||
|
||||
@ -203,9 +197,38 @@ class BatchingWebSocket extends EventTarget {
|
||||
|
||||
#routeMessageToHandler(message) {
|
||||
if (message.data.type === 'batch') {
|
||||
this.start = Date.now();
|
||||
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(
|
||||
'Telemetry dropped due to client rate limiting.',
|
||||
{ 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.`);
|
||||
this.#waitUntilIdleAndRequestNextBatch(batch);
|
||||
} 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();
|
||||
}
|
||||
} else {
|
||||
if (waitedFor > ONE_SECOND) {
|
||||
if (waitedFor > this.#throttleRate) {
|
||||
console.warn(`Warning, batch processing took ${waitedFor}ms`);
|
||||
}
|
||||
this.#readyForNextBatch();
|
||||
}
|
||||
},
|
||||
{ timeout: ONE_SECOND }
|
||||
{ timeout: this.#throttleRate }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -231,31 +231,109 @@ export default class TelemetryAPI {
|
||||
* @returns {TelemetryRequestOptions} the options, with defaults filled in
|
||||
*/
|
||||
standardizeRequestOptions(options = {}) {
|
||||
if (!Object.hasOwn(options, 'start')) {
|
||||
const bounds = options.timeContext?.getBounds();
|
||||
if (bounds?.start) {
|
||||
options.start = options.timeContext.getBounds().start;
|
||||
} else {
|
||||
options.start = this.openmct.time.getBounds().start;
|
||||
}
|
||||
}
|
||||
|
||||
if (!Object.hasOwn(options, 'end')) {
|
||||
const bounds = options.timeContext?.getBounds();
|
||||
if (bounds?.end) {
|
||||
options.end = options.timeContext.getBounds().end;
|
||||
} else {
|
||||
options.end = this.openmct.time.getBounds().end;
|
||||
}
|
||||
if (!Object.hasOwn(options, 'timeContext')) {
|
||||
options.timeContext = this.openmct.time;
|
||||
}
|
||||
|
||||
if (!Object.hasOwn(options, 'domain')) {
|
||||
options.domain = this.openmct.time.getTimeSystem().key;
|
||||
options.domain = options.timeContext.getTimeSystem().key;
|
||||
}
|
||||
|
||||
if (!Object.hasOwn(options, 'start')) {
|
||||
options.start = options.timeContext.getBounds().start;
|
||||
}
|
||||
|
||||
if (!Object.hasOwn(options, 'end')) {
|
||||
options.end = options.timeContext.getBounds().end;
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes objects for consistent serialization by:
|
||||
* 1. Removing non-plain objects (class instances) and functions
|
||||
* 2. Sorting object keys alphabetically to ensure consistent ordering
|
||||
*/
|
||||
sanitizeForSerialization(key, value) {
|
||||
// Handle null and primitives directly
|
||||
if (value === null || typeof value !== 'object') {
|
||||
return value;
|
||||
}
|
||||
|
||||
// Remove functions and non-plain objects (except arrays)
|
||||
if (
|
||||
typeof value === 'function' ||
|
||||
(Object.getPrototypeOf(value) !== Object.prototype && !Array.isArray(value))
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// For plain objects, just sort the keys
|
||||
if (!Array.isArray(value)) {
|
||||
const sortedObject = {};
|
||||
const sortedKeys = Object.keys(value).sort();
|
||||
|
||||
sortedKeys.forEach((objectKey) => {
|
||||
sortedObject[objectKey] = value[objectKey];
|
||||
});
|
||||
|
||||
return sortedObject;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a numeric hash value for an options object. The hash is consistent
|
||||
* for equivalent option objects regardless of property order.
|
||||
*
|
||||
* This is used to create compact, unique cache keys for telemetry subscriptions with
|
||||
* different options configurations. The hash function ensures that identical options
|
||||
* objects will always generate the same hash value, while different options objects
|
||||
* (even with small differences) will generate different hash values.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} options The options object to hash
|
||||
* @returns {number} A positive integer hash of the options object
|
||||
*/
|
||||
#hashOptions(options) {
|
||||
const sanitizedOptionsString = JSON.stringify(
|
||||
options,
|
||||
this.sanitizeForSerialization.bind(this)
|
||||
);
|
||||
|
||||
let hash = 0;
|
||||
const prime = 31;
|
||||
const modulus = 1e9 + 9; // Large prime number
|
||||
|
||||
for (let i = 0; i < sanitizedOptionsString.length; i++) {
|
||||
const char = sanitizedOptionsString.charCodeAt(i);
|
||||
// Calculate new hash value while keeping numbers manageable
|
||||
hash = Math.floor((hash * prime + char) % modulus);
|
||||
}
|
||||
|
||||
return Math.abs(hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a unique cache key for a telemetry subscription based on the
|
||||
* domain object identifier and options (which includes strategy).
|
||||
*
|
||||
* Uses a hash of the options object to create compact cache keys while still
|
||||
* ensuring unique keys for different subscription configurations.
|
||||
*
|
||||
* @private
|
||||
* @param {import('openmct').DomainObject} domainObject The domain object being subscribed to
|
||||
* @param {Object} options The subscription options object (including strategy)
|
||||
* @returns {string} A unique key string for caching the subscription
|
||||
*/
|
||||
#getSubscriptionCacheKey(domainObject, options) {
|
||||
const keyString = makeKeyString(domainObject.identifier);
|
||||
|
||||
return `${keyString}:${this.#hashOptions(options)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a request interceptor that transforms a request via module:openmct.TelemetryAPI.request
|
||||
* The request will be modified when it is received and will be returned in it's modified state
|
||||
@ -424,16 +502,14 @@ export default class TelemetryAPI {
|
||||
this.#subscribeCache = {};
|
||||
}
|
||||
|
||||
const keyString = makeKeyString(domainObject.identifier);
|
||||
const supportedStrategy = supportsBatching ? requestedStrategy : SUBSCRIBE_STRATEGY.LATEST;
|
||||
// Override the requested strategy with the strategy supported by the provider
|
||||
const optionsWithSupportedStrategy = {
|
||||
...options,
|
||||
strategy: supportedStrategy
|
||||
};
|
||||
// If batching is supported, we need to cache a subscription for each strategy -
|
||||
// latest and batched.
|
||||
const cacheKey = `${keyString}:${supportedStrategy}`;
|
||||
|
||||
const cacheKey = this.#getSubscriptionCacheKey(domainObject, optionsWithSupportedStrategy);
|
||||
let subscriber = this.#subscribeCache[cacheKey];
|
||||
|
||||
if (!subscriber) {
|
||||
|
@ -269,36 +269,40 @@ describe('Telemetry API', () => {
|
||||
|
||||
await telemetryAPI.request(domainObject);
|
||||
const { signal } = new AbortController();
|
||||
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(jasmine.any(Object), {
|
||||
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(domainObject, {
|
||||
signal,
|
||||
start: 0,
|
||||
end: 1,
|
||||
domain: 'system'
|
||||
domain: 'system',
|
||||
timeContext: openmct.time
|
||||
});
|
||||
|
||||
expect(telemetryProvider.request).toHaveBeenCalledWith(jasmine.any(Object), {
|
||||
expect(telemetryProvider.request).toHaveBeenCalledWith(domainObject, {
|
||||
signal,
|
||||
start: 0,
|
||||
end: 1,
|
||||
domain: 'system'
|
||||
domain: 'system',
|
||||
timeContext: openmct.time
|
||||
});
|
||||
|
||||
telemetryProvider.supportsRequest.calls.reset();
|
||||
telemetryProvider.request.calls.reset();
|
||||
|
||||
await telemetryAPI.request(domainObject, {});
|
||||
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(jasmine.any(Object), {
|
||||
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(domainObject, {
|
||||
signal,
|
||||
start: 0,
|
||||
end: 1,
|
||||
domain: 'system'
|
||||
domain: 'system',
|
||||
timeContext: openmct.time
|
||||
});
|
||||
|
||||
expect(telemetryProvider.request).toHaveBeenCalledWith(jasmine.any(Object), {
|
||||
expect(telemetryProvider.request).toHaveBeenCalledWith(domainObject, {
|
||||
signal,
|
||||
start: 0,
|
||||
end: 1,
|
||||
domain: 'system'
|
||||
domain: 'system',
|
||||
timeContext: openmct.time
|
||||
});
|
||||
});
|
||||
|
||||
@ -313,18 +317,20 @@ describe('Telemetry API', () => {
|
||||
domain: 'someDomain'
|
||||
});
|
||||
const { signal } = new AbortController();
|
||||
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(jasmine.any(Object), {
|
||||
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(domainObject, {
|
||||
start: 20,
|
||||
end: 30,
|
||||
domain: 'someDomain',
|
||||
signal
|
||||
signal,
|
||||
timeContext: openmct.time
|
||||
});
|
||||
|
||||
expect(telemetryProvider.request).toHaveBeenCalledWith(jasmine.any(Object), {
|
||||
expect(telemetryProvider.request).toHaveBeenCalledWith(domainObject, {
|
||||
start: 20,
|
||||
end: 30,
|
||||
domain: 'someDomain',
|
||||
signal
|
||||
signal,
|
||||
timeContext: openmct.time
|
||||
});
|
||||
});
|
||||
describe('telemetry batching support', () => {
|
||||
|
@ -62,9 +62,6 @@ export default class TelemetryCollection extends EventEmitter {
|
||||
this.futureBuffer = [];
|
||||
this.parseTime = undefined;
|
||||
this.metadata = this.openmct.telemetry.getMetadata(domainObject);
|
||||
if (!Object.hasOwn(options, 'timeContext')) {
|
||||
options.timeContext = this.openmct.time;
|
||||
}
|
||||
this.options = options;
|
||||
this.unsubscribe = undefined;
|
||||
this.pageState = undefined;
|
||||
@ -84,6 +81,9 @@ export default class TelemetryCollection extends EventEmitter {
|
||||
this._error(LOADED_ERROR);
|
||||
}
|
||||
|
||||
if (!Object.hasOwn(this.options, 'timeContext')) {
|
||||
this.options.timeContext = this.openmct.time;
|
||||
}
|
||||
this._setTimeSystem(this.options.timeContext.getTimeSystem());
|
||||
this.lastBounds = this.options.timeContext.getBounds();
|
||||
this._watchBounds();
|
||||
@ -116,8 +116,7 @@ export default class TelemetryCollection extends EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* This will start the requests for historical and realtime data,
|
||||
* as well as setting up initial values and watchers
|
||||
* @returns {Array} All bounded telemetry
|
||||
*/
|
||||
getAll() {
|
||||
return this.boundedTelemetry;
|
||||
@ -128,7 +127,7 @@ export default class TelemetryCollection extends EventEmitter {
|
||||
* @private
|
||||
*/
|
||||
async _requestHistoricalTelemetry() {
|
||||
let options = this.openmct.telemetry.standardizeRequestOptions({ ...this.options });
|
||||
const options = this.openmct.telemetry.standardizeRequestOptions({ ...this.options });
|
||||
const historicalProvider = this.openmct.telemetry.findRequestProvider(
|
||||
this.domainObject,
|
||||
options
|
||||
|
@ -24,10 +24,6 @@ export default function installWorker() {
|
||||
const ONE_SECOND = 1000;
|
||||
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.
|
||||
* On an error or dropout, will automatically reconnect.
|
||||
@ -215,17 +211,17 @@ export default function installWorker() {
|
||||
case 'message':
|
||||
this.#websocket.enqueueMessage(message.data.message);
|
||||
break;
|
||||
case 'setBatchingStrategy':
|
||||
this.setBatchingStrategy(message);
|
||||
break;
|
||||
case 'readyForNextBatch':
|
||||
this.#messageBatcher.readyForNextBatch();
|
||||
break;
|
||||
case 'setMaxBatchSize':
|
||||
this.#messageBatcher.setMaxBatchSize(message.data.maxBatchSize);
|
||||
case 'setMaxBufferSize':
|
||||
this.#messageBatcher.setMaxBufferSize(message.data.maxBufferSize);
|
||||
break;
|
||||
case 'setMaxBatchWait':
|
||||
this.#messageBatcher.setMaxBatchWait(message.data.maxBatchWait);
|
||||
case 'setThrottleRate':
|
||||
this.#messageBatcher.setThrottleRate(message.data.throttleRate);
|
||||
break;
|
||||
case 'setThrottleMessagePattern':
|
||||
this.#messageBatcher.setThrottleMessagePattern(message.data.throttleMessagePattern);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown message type: ${type}`);
|
||||
@ -238,122 +234,69 @@ export default function installWorker() {
|
||||
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
|
||||
* Worker interface and back to the main thread.
|
||||
* Responsible for buffering messages
|
||||
*/
|
||||
class WebSocketToWorkerMessageBroker {
|
||||
#worker;
|
||||
#messageBatcher;
|
||||
|
||||
constructor(messageBatcher, worker) {
|
||||
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;
|
||||
class MessageBuffer {
|
||||
#buffer;
|
||||
#currentBufferLength;
|
||||
#dropped;
|
||||
#maxBufferSize;
|
||||
#readyForNextBatch;
|
||||
#worker;
|
||||
#throttledSendNextBatch;
|
||||
#throttleMessagePattern;
|
||||
|
||||
constructor(worker) {
|
||||
// No dropping telemetry unless we're explicitly told to.
|
||||
this.#maxBatchSize = Number.POSITIVE_INFINITY;
|
||||
this.#maxBufferSize = Number.POSITIVE_INFINITY;
|
||||
this.#readyForNextBatch = false;
|
||||
this.#worker = worker;
|
||||
this.#resetBatch();
|
||||
this.setMaxBatchWait(ONE_SECOND);
|
||||
this.setThrottleRate(ONE_SECOND);
|
||||
}
|
||||
#resetBatch() {
|
||||
this.#batch = {};
|
||||
this.#hasBatch = false;
|
||||
//this.#batch = {};
|
||||
this.#buffer = [];
|
||||
this.#currentBufferLength = 0;
|
||||
this.#dropped = false;
|
||||
}
|
||||
/**
|
||||
* @param {BatchingStrategy} strategy
|
||||
*/
|
||||
setBatchingStrategy(strategy) {
|
||||
this.#batchingStrategy = strategy;
|
||||
|
||||
addMessageToBuffer(message) {
|
||||
this.#buffer.push(message);
|
||||
this.#currentBufferLength += message.length;
|
||||
|
||||
for (
|
||||
let i = 0;
|
||||
this.#currentBufferLength > this.#maxBufferSize && i < this.#buffer.length;
|
||||
i++
|
||||
) {
|
||||
const messageToConsider = this.#buffer[i];
|
||||
if (this.#shouldThrottle(messageToConsider)) {
|
||||
this.#buffer.splice(i, 1);
|
||||
this.#currentBufferLength -= messageToConsider.length;
|
||||
this.#dropped = true;
|
||||
}
|
||||
/**
|
||||
* Applies the `shouldBatchMessage` function from the supplied batching strategy
|
||||
* to each message to determine if it should be added to a batch. If not batched,
|
||||
* the message is immediately sent over the worker to the main thread.
|
||||
* @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
|
||||
* @returns
|
||||
*/
|
||||
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) {
|
||||
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
|
||||
@ -362,21 +305,33 @@ export default function installWorker() {
|
||||
* any new data is available.
|
||||
*/
|
||||
readyForNextBatch() {
|
||||
if (this.#hasBatch) {
|
||||
if (this.#hasData()) {
|
||||
this.#throttledSendNextBatch();
|
||||
} else {
|
||||
this.#readyForNextBatch = true;
|
||||
}
|
||||
}
|
||||
#sendNextBatch() {
|
||||
const batch = this.#batch;
|
||||
const buffer = this.#buffer;
|
||||
const dropped = this.#dropped;
|
||||
const currentBufferLength = this.#currentBufferLength;
|
||||
|
||||
this.#resetBatch();
|
||||
this.#worker.postMessage({
|
||||
type: 'batch',
|
||||
batch
|
||||
dropped,
|
||||
currentBufferLength: currentBufferLength,
|
||||
maxBufferSize: this.#maxBufferSize,
|
||||
batch: buffer
|
||||
});
|
||||
|
||||
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 messageBatcher = new MessageBatcher(self);
|
||||
const workerBroker = new WorkerToWebSocketMessageBroker(websocket, messageBatcher);
|
||||
const websocketBroker = new WebSocketToWorkerMessageBroker(messageBatcher, self);
|
||||
const messageBuffer = new MessageBuffer(self);
|
||||
const workerBroker = new WorkerToWebSocketMessageBroker(websocket, messageBuffer);
|
||||
|
||||
self.addEventListener('message', (message) => {
|
||||
workerBroker.routeMessageToHandler(message);
|
||||
});
|
||||
websocket.registerMessageCallback((data) => {
|
||||
websocketBroker.routeMessageToHandler(data);
|
||||
messageBuffer.addMessageToBuffer(data);
|
||||
});
|
||||
|
||||
self.websocketInstance = websocket;
|
||||
|
@ -359,6 +359,18 @@ class IndependentTimeContext extends TimeContext {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean}
|
||||
* @override
|
||||
*/
|
||||
isFixed() {
|
||||
if (this.upstreamTimeContext) {
|
||||
return this.upstreamTimeContext.isFixed(...arguments);
|
||||
} else {
|
||||
return super.isFixed(...arguments);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {number}
|
||||
* @override
|
||||
@ -400,7 +412,7 @@ class IndependentTimeContext extends TimeContext {
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the time context to the global time context
|
||||
* Reset the time context from the global time context
|
||||
*/
|
||||
resetContext() {
|
||||
if (this.upstreamTimeContext) {
|
||||
@ -428,6 +440,10 @@ class IndependentTimeContext extends TimeContext {
|
||||
// Emit bounds so that views that are changing context get the upstream bounds
|
||||
this.emit('bounds', this.getBounds());
|
||||
this.emit(TIME_CONTEXT_EVENTS.boundsChanged, this.getBounds());
|
||||
// Also emit the mode in case it's different from previous time context
|
||||
if (this.getMode()) {
|
||||
this.emit(TIME_CONTEXT_EVENTS.modeChanged, this.#copy(this.getMode()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -502,6 +518,10 @@ class IndependentTimeContext extends TimeContext {
|
||||
// Emit bounds so that views that are changing context get the upstream bounds
|
||||
this.emit('bounds', this.getBounds());
|
||||
this.emit(TIME_CONTEXT_EVENTS.boundsChanged, this.getBounds());
|
||||
// Also emit the mode in case it's different from the global time context
|
||||
if (this.getMode()) {
|
||||
this.emit(TIME_CONTEXT_EVENTS.modeChanged, this.#copy(this.getMode()));
|
||||
}
|
||||
// now that the view's context is set, tell others to check theirs in case they were following this view's context.
|
||||
this.globalTimeContext.emit('refreshContext', viewKey);
|
||||
}
|
||||
|
@ -23,6 +23,7 @@
|
||||
import { FIXED_MODE_KEY, REALTIME_MODE_KEY } from '@/api/time/constants';
|
||||
import IndependentTimeContext from '@/api/time/IndependentTimeContext';
|
||||
|
||||
import { TIME_CONTEXT_EVENTS } from './constants';
|
||||
import GlobalTimeContext from './GlobalTimeContext.js';
|
||||
|
||||
/**
|
||||
@ -142,7 +143,7 @@ class TimeAPI extends GlobalTimeContext {
|
||||
addIndependentContext(key, value, clockKey) {
|
||||
let timeContext = this.getIndependentContext(key);
|
||||
|
||||
//stop following upstream time context since the view has it's own
|
||||
//stop following upstream time context since the view has its own
|
||||
timeContext.resetContext();
|
||||
|
||||
if (clockKey) {
|
||||
@ -152,6 +153,9 @@ class TimeAPI extends GlobalTimeContext {
|
||||
timeContext.setMode(FIXED_MODE_KEY, value);
|
||||
}
|
||||
|
||||
// Also emit the mode in case it's different from the previous time context
|
||||
timeContext.emit(TIME_CONTEXT_EVENTS.modeChanged, structuredClone(timeContext.getMode()));
|
||||
|
||||
// Notify any nested views to update, pass in the viewKey so that particular view can skip getting an upstream context
|
||||
this.emit('refreshContext', key);
|
||||
|
||||
|
@ -257,7 +257,9 @@ export default {
|
||||
|
||||
return {
|
||||
end,
|
||||
start
|
||||
start,
|
||||
size: 1,
|
||||
strategy: 'latest'
|
||||
};
|
||||
},
|
||||
loadComposition() {
|
||||
@ -330,7 +332,11 @@ export default {
|
||||
this.domainObject.configuration.axes.xKey === undefined ||
|
||||
this.domainObject.configuration.axes.yKey === undefined
|
||||
) {
|
||||
return;
|
||||
const { xKey, yKey } = this.identifyAxesKeys(axisMetadata);
|
||||
this.openmct.objects.mutate(this.domainObject, 'configuration.axes', {
|
||||
xKey,
|
||||
yKey
|
||||
});
|
||||
}
|
||||
|
||||
let xValues = [];
|
||||
@ -429,6 +435,30 @@ export default {
|
||||
subscribeToAll() {
|
||||
const telemetryObjects = Object.values(this.telemetryObjects);
|
||||
telemetryObjects.forEach(this.subscribeToObject);
|
||||
},
|
||||
identifyAxesKeys(metadata) {
|
||||
const { xAxisMetadata, yAxisMetadata } = metadata;
|
||||
|
||||
let xKey;
|
||||
let yKey;
|
||||
|
||||
// If xAxisMetadata contains array values, use the first one for xKey
|
||||
const arrayValues = xAxisMetadata.filter((metaDatum) => metaDatum.isArrayValue);
|
||||
const nonArrayValues = xAxisMetadata.filter((metaDatum) => !metaDatum.isArrayValue);
|
||||
|
||||
if (arrayValues.length > 0) {
|
||||
xKey = arrayValues[0].key;
|
||||
yKey = arrayValues.length > 1 ? arrayValues[1].key : yAxisMetadata.key;
|
||||
} else if (nonArrayValues.length > 0) {
|
||||
xKey = nonArrayValues[0].key;
|
||||
yKey = 'none';
|
||||
} else {
|
||||
// Fallback if no valid xKey or yKey is found
|
||||
xKey = 'none';
|
||||
yKey = 'none';
|
||||
}
|
||||
|
||||
return { xKey, yKey };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -39,7 +39,7 @@ export default class ConditionManager extends EventEmitter {
|
||||
this.shouldEvaluateNewTelemetry = this.shouldEvaluateNewTelemetry.bind(this);
|
||||
|
||||
this.compositionLoad = this.composition.load();
|
||||
this.subscriptions = {};
|
||||
this.telemetryCollections = {};
|
||||
this.telemetryObjects = {};
|
||||
this.testData = {
|
||||
conditionTestInputs: this.conditionSetDomainObject.configuration.conditionTestData,
|
||||
@ -48,55 +48,46 @@ export default class ConditionManager extends EventEmitter {
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
async requestLatestValue(endpoint) {
|
||||
const options = {
|
||||
subscribeToTelemetry(telemetryObject) {
|
||||
const keyString = this.openmct.objects.makeKeyString(telemetryObject.identifier);
|
||||
|
||||
if (this.telemetryCollections[keyString]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const requestOptions = {
|
||||
size: 1,
|
||||
strategy: 'latest'
|
||||
};
|
||||
const latestData = await this.openmct.telemetry.request(endpoint, options);
|
||||
|
||||
if (!latestData) {
|
||||
throw new Error('Telemetry request failed by returning a falsy response');
|
||||
}
|
||||
if (latestData.length === 0) {
|
||||
return;
|
||||
}
|
||||
this.telemetryReceived(endpoint, latestData[0]);
|
||||
}
|
||||
|
||||
subscribeToTelemetry(endpoint) {
|
||||
const telemetryKeyString = this.openmct.objects.makeKeyString(endpoint.identifier);
|
||||
if (this.subscriptions[telemetryKeyString]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const metadata = this.openmct.telemetry.getMetadata(endpoint);
|
||||
|
||||
this.telemetryObjects[telemetryKeyString] = Object.assign({}, endpoint, {
|
||||
telemetryMetaData: metadata ? metadata.valueMetadatas : []
|
||||
});
|
||||
|
||||
// get latest telemetry value (in case subscription is cached and no new data is coming in)
|
||||
this.requestLatestValue(endpoint);
|
||||
|
||||
this.subscriptions[telemetryKeyString] = this.openmct.telemetry.subscribe(
|
||||
endpoint,
|
||||
this.telemetryReceived.bind(this, endpoint)
|
||||
this.telemetryCollections[keyString] = this.openmct.telemetry.requestCollection(
|
||||
telemetryObject,
|
||||
requestOptions
|
||||
);
|
||||
|
||||
const metadata = this.openmct.telemetry.getMetadata(telemetryObject);
|
||||
const telemetryMetaData = metadata ? metadata.valueMetadatas : [];
|
||||
|
||||
this.telemetryObjects[keyString] = { ...telemetryObject, telemetryMetaData };
|
||||
|
||||
this.telemetryCollections[keyString].on(
|
||||
'add',
|
||||
this.telemetryReceived.bind(this, telemetryObject)
|
||||
);
|
||||
this.telemetryCollections[keyString].load();
|
||||
|
||||
this.updateConditionTelemetryObjects();
|
||||
}
|
||||
|
||||
unsubscribeFromTelemetry(endpointIdentifier) {
|
||||
const id = this.openmct.objects.makeKeyString(endpointIdentifier);
|
||||
if (!this.subscriptions[id]) {
|
||||
console.log('no subscription to remove');
|
||||
|
||||
const keyString = this.openmct.objects.makeKeyString(endpointIdentifier);
|
||||
if (!this.telemetryCollections[keyString]) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.subscriptions[id]();
|
||||
delete this.subscriptions[id];
|
||||
delete this.telemetryObjects[id];
|
||||
this.telemetryCollections[keyString].destroy();
|
||||
this.telemetryCollections[keyString] = null;
|
||||
this.telemetryObjects[keyString] = null;
|
||||
this.removeConditionTelemetryObjects();
|
||||
|
||||
//force re-computation of condition set result as we might be in a state where
|
||||
@ -107,7 +98,7 @@ export default class ConditionManager extends EventEmitter {
|
||||
this.timeSystems,
|
||||
this.openmct.time.getTimeSystem()
|
||||
);
|
||||
this.updateConditionResults({ id: id });
|
||||
this.updateConditionResults({ id: keyString });
|
||||
this.updateCurrentCondition(latestTimestamp);
|
||||
|
||||
if (Object.keys(this.telemetryObjects).length === 0) {
|
||||
@ -410,11 +401,13 @@ export default class ConditionManager extends EventEmitter {
|
||||
return this.openmct.time.getBounds().end >= currentTimestamp;
|
||||
}
|
||||
|
||||
telemetryReceived(endpoint, datum) {
|
||||
telemetryReceived(endpoint, data) {
|
||||
if (!this.isTelemetryUsed(endpoint)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const datum = data[0];
|
||||
|
||||
const normalizedDatum = this.createNormalizedDatum(datum, endpoint);
|
||||
const timeSystemKey = this.openmct.time.getTimeSystem().key;
|
||||
let timestamp = {};
|
||||
@ -507,8 +500,9 @@ export default class ConditionManager extends EventEmitter {
|
||||
destroy() {
|
||||
this.composition.off('add', this.subscribeToTelemetry, this);
|
||||
this.composition.off('remove', this.unsubscribeFromTelemetry, this);
|
||||
Object.values(this.subscriptions).forEach((unsubscribe) => unsubscribe());
|
||||
delete this.subscriptions;
|
||||
Object.values(this.telemetryCollections).forEach((telemetryCollection) =>
|
||||
telemetryCollection.destroy()
|
||||
);
|
||||
|
||||
this.conditions.forEach((condition) => {
|
||||
condition.destroy();
|
||||
|
@ -27,14 +27,16 @@
|
||||
aria-label="Condition Set Condition Collection"
|
||||
>
|
||||
<div class="c-cs__header c-section__header">
|
||||
<span
|
||||
<button
|
||||
class="c-disclosure-triangle c-tree__item__view-control is-enabled"
|
||||
:class="{ 'c-disclosure-triangle--expanded': expanded }"
|
||||
@click="expanded = !expanded"
|
||||
></span>
|
||||
:aria-expanded="expanded"
|
||||
aria-controls="conditionContent"
|
||||
@click="toggleExpanded"
|
||||
></button>
|
||||
<div class="c-cs__header-label c-section__label">Conditions</div>
|
||||
</div>
|
||||
<div v-if="expanded" class="c-cs__content">
|
||||
<div v-if="expanded" id="conditionContent" class="c-cs__content">
|
||||
<div
|
||||
v-show="isEditing"
|
||||
class="hint"
|
||||
@ -54,9 +56,10 @@
|
||||
v-show="isEditing"
|
||||
id="addCondition"
|
||||
class="c-button c-button--major icon-plus labeled"
|
||||
aria-labelledby="addConditionButtonLabel"
|
||||
@click="addCondition"
|
||||
>
|
||||
<span class="c-cs-button__label">Add Condition</span>
|
||||
<span id="addConditionButtonLabel" class="c-cs-button__label">Add Condition</span>
|
||||
</button>
|
||||
|
||||
<div class="c-cs__conditions-h" :class="{ 'is-active-dragging': isDragging }">
|
||||
|
@ -160,8 +160,10 @@
|
||||
</div>
|
||||
</template>
|
||||
<div class="c-cdef__separator c-row-separator"></div>
|
||||
<div class="c-cdef__controls" :disabled="!telemetry.length">
|
||||
<div class="c-cdef__controls">
|
||||
<button
|
||||
:disabled="!telemetry.length"
|
||||
:aria-label="`Add Criteria - ${!telemetry.length ? 'Disabled' : 'Enabled'}`"
|
||||
class="c-cdef__add-criteria-button c-button c-button--labeled icon-plus"
|
||||
@click="addCriteria"
|
||||
>
|
||||
|
@ -28,11 +28,7 @@
|
||||
{ 'is-style-invisible': styleItem.style && styleItem.style.isStyleInvisible },
|
||||
{ 'c-style-thumb--mixed': mixedStyles.indexOf('backgroundColor') > -1 }
|
||||
]"
|
||||
:style="[
|
||||
styleItem.style.imageUrl
|
||||
? { backgroundImage: 'url(' + styleItem.style.imageUrl + ')' }
|
||||
: itemStyle
|
||||
]"
|
||||
:style="[encodedImageUrl ? { backgroundImage: 'url(' + encodedImageUrl + ')' } : itemStyle]"
|
||||
class="c-style-thumb"
|
||||
>
|
||||
<span
|
||||
@ -62,7 +58,7 @@
|
||||
@change="updateStyleValue"
|
||||
/>
|
||||
<ToolbarButton
|
||||
v-if="hasProperty(styleItem.style.imageUrl)"
|
||||
v-if="hasProperty(encodedImageUrl)"
|
||||
class="c-style__toolbar-button--image-url"
|
||||
:options="imageUrlOption"
|
||||
@change="updateStyleValue"
|
||||
@ -93,6 +89,8 @@ import ToolbarButton from '@/ui/toolbar/components/ToolbarButton.vue';
|
||||
import ToolbarColorPicker from '@/ui/toolbar/components/ToolbarColorPicker.vue';
|
||||
import ToolbarToggleButton from '@/ui/toolbar/components/ToolbarToggleButton.vue';
|
||||
|
||||
import { encode_url } from '../../../../utils/encoding';
|
||||
|
||||
export default {
|
||||
name: 'StyleEditor',
|
||||
components: {
|
||||
@ -183,11 +181,14 @@ export default {
|
||||
},
|
||||
property: 'imageUrl',
|
||||
formKeys: ['url'],
|
||||
value: { url: this.styleItem.style.imageUrl },
|
||||
value: { url: this.encodedImageUrl },
|
||||
isEditing: this.isEditing,
|
||||
nonSpecific: this.mixedStyles.indexOf('imageUrl') > -1
|
||||
};
|
||||
},
|
||||
encodedImageUrl() {
|
||||
return encode_url(this.styleItem.style.imageUrl);
|
||||
},
|
||||
isStyleInvisibleOption() {
|
||||
return {
|
||||
value: this.styleItem.style.isStyleInvisible,
|
||||
|
@ -720,31 +720,57 @@ describe('the plugin', function () {
|
||||
};
|
||||
});
|
||||
|
||||
it('should evaluate as old when telemetry is not received in the allotted time', (done) => {
|
||||
it('should evaluate as old when telemetry is not received in the allotted time', async () => {
|
||||
let onAddResolve;
|
||||
const onAddCalledPromise = new Promise((resolve) => {
|
||||
onAddResolve = resolve;
|
||||
});
|
||||
const mockTelemetryCollection = {
|
||||
load: jasmine.createSpy('load'),
|
||||
on: jasmine.createSpy('on').and.callFake((event, callback) => {
|
||||
if (event === 'add') {
|
||||
onAddResolve();
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
openmct.telemetry = jasmine.createSpyObj('telemetry', [
|
||||
'subscribe',
|
||||
'getMetadata',
|
||||
'request',
|
||||
'getValueFormatter',
|
||||
'abortAllRequests'
|
||||
'abortAllRequests',
|
||||
'requestCollection'
|
||||
]);
|
||||
openmct.telemetry.request.and.returnValue(Promise.resolve([]));
|
||||
openmct.telemetry.getMetadata.and.returnValue({
|
||||
...testTelemetryObject.telemetry,
|
||||
valueMetadatas: []
|
||||
valueMetadatas: testTelemetryObject.telemetry.values,
|
||||
valuesForHints: jasmine
|
||||
.createSpy('valuesForHints')
|
||||
.and.returnValue(testTelemetryObject.telemetry.values),
|
||||
value: jasmine.createSpy('value').and.callFake((key) => {
|
||||
return testTelemetryObject.telemetry.values.find((value) => value.key === key);
|
||||
})
|
||||
});
|
||||
openmct.telemetry.request.and.returnValue(Promise.resolve([]));
|
||||
openmct.telemetry.requestCollection.and.returnValue(mockTelemetryCollection);
|
||||
openmct.telemetry.getValueFormatter.and.returnValue({
|
||||
parse: function (value) {
|
||||
return value;
|
||||
}
|
||||
});
|
||||
|
||||
let conditionMgr = new ConditionManager(conditionSetDomainObject, openmct);
|
||||
conditionMgr.on('conditionSetResultUpdated', mockListener);
|
||||
conditionMgr.telemetryObjects = {
|
||||
'test-object': testTelemetryObject
|
||||
};
|
||||
conditionMgr.updateConditionTelemetryObjects();
|
||||
setTimeout(() => {
|
||||
// Wait for the 'on' callback to be called
|
||||
await onAddCalledPromise;
|
||||
|
||||
// Simulate the passage of time and no data received
|
||||
await new Promise((resolve) => setTimeout(resolve, 400));
|
||||
|
||||
expect(mockListener).toHaveBeenCalledWith({
|
||||
output: 'Any old telemetry',
|
||||
id: {
|
||||
@ -754,16 +780,9 @@ describe('the plugin', function () {
|
||||
conditionId: '39584410-cbf9-499e-96dc-76f27e69885d',
|
||||
utc: undefined
|
||||
});
|
||||
done();
|
||||
}, 400);
|
||||
});
|
||||
|
||||
it('should not evaluate as old when telemetry is received in the allotted time', (done) => {
|
||||
openmct.telemetry.getMetadata = jasmine.createSpy('getMetadata');
|
||||
openmct.telemetry.getMetadata.and.returnValue({
|
||||
...testTelemetryObject.telemetry,
|
||||
valueMetadatas: testTelemetryObject.telemetry.values
|
||||
});
|
||||
it('should not evaluate as old when telemetry is received in the allotted time', async () => {
|
||||
const testDatum = {
|
||||
'some-key2': '',
|
||||
utc: 1,
|
||||
@ -771,8 +790,49 @@ describe('the plugin', function () {
|
||||
'some-key': null,
|
||||
id: 'test-object'
|
||||
};
|
||||
openmct.telemetry.request = jasmine.createSpy('request');
|
||||
|
||||
let onAddResolve;
|
||||
let onAddCallback;
|
||||
const onAddCalledPromise = new Promise((resolve) => {
|
||||
onAddResolve = resolve;
|
||||
});
|
||||
|
||||
const mockTelemetryCollection = {
|
||||
load: jasmine.createSpy('load'),
|
||||
on: jasmine.createSpy('on').and.callFake((event, callback) => {
|
||||
if (event === 'add') {
|
||||
onAddCallback = callback;
|
||||
onAddResolve();
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
openmct.telemetry = jasmine.createSpyObj('telemetry', [
|
||||
'getMetadata',
|
||||
'getValueFormatter',
|
||||
'request',
|
||||
'subscribe',
|
||||
'requestCollection'
|
||||
]);
|
||||
openmct.telemetry.subscribe.and.returnValue(function () {});
|
||||
openmct.telemetry.request.and.returnValue(Promise.resolve([testDatum]));
|
||||
openmct.telemetry.getMetadata.and.returnValue({
|
||||
...testTelemetryObject.telemetry,
|
||||
valueMetadatas: testTelemetryObject.telemetry.values,
|
||||
valuesForHints: jasmine
|
||||
.createSpy('valuesForHints')
|
||||
.and.returnValue(testTelemetryObject.telemetry.values),
|
||||
value: jasmine.createSpy('value').and.callFake((key) => {
|
||||
return testTelemetryObject.telemetry.values.find((value) => value.key === key);
|
||||
})
|
||||
});
|
||||
openmct.telemetry.requestCollection.and.returnValue(mockTelemetryCollection);
|
||||
openmct.telemetry.getValueFormatter.and.returnValue({
|
||||
parse: function (value) {
|
||||
return value;
|
||||
}
|
||||
});
|
||||
|
||||
const date = 1;
|
||||
conditionSetDomainObject.configuration.conditionCollection[0].configuration.criteria[0].input =
|
||||
['0.4'];
|
||||
@ -782,8 +842,16 @@ describe('the plugin', function () {
|
||||
'test-object': testTelemetryObject
|
||||
};
|
||||
conditionMgr.updateConditionTelemetryObjects();
|
||||
conditionMgr.telemetryReceived(testTelemetryObject, testDatum);
|
||||
setTimeout(() => {
|
||||
|
||||
// Wait for the 'on' callback to be called
|
||||
await onAddCalledPromise;
|
||||
|
||||
// Simulate receiving telemetry data
|
||||
onAddCallback([testDatum]);
|
||||
|
||||
// Wait a bit for the condition manager to process the data
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
expect(mockListener).toHaveBeenCalledWith({
|
||||
output: 'Default',
|
||||
id: {
|
||||
@ -793,8 +861,6 @@ describe('the plugin', function () {
|
||||
conditionId: '2532d90a-e0d6-4935-b546-3123522da2de',
|
||||
utc: date
|
||||
});
|
||||
done();
|
||||
}, 300);
|
||||
});
|
||||
});
|
||||
|
||||
@ -902,17 +968,25 @@ describe('the plugin', function () {
|
||||
openmct.telemetry.getMetadata = jasmine.createSpy('getMetadata');
|
||||
openmct.telemetry.getMetadata.and.returnValue({
|
||||
...testTelemetryObject.telemetry,
|
||||
valueMetadatas: []
|
||||
valueMetadatas: testTelemetryObject.telemetry.values,
|
||||
valuesForHints: jasmine
|
||||
.createSpy('valuesForHints')
|
||||
.and.returnValue(testTelemetryObject.telemetry.values),
|
||||
value: jasmine.createSpy('value').and.callFake((key) => {
|
||||
return testTelemetryObject.telemetry.values.find((value) => value.key === key);
|
||||
})
|
||||
});
|
||||
conditionMgr.on('conditionSetResultUpdated', mockListener);
|
||||
conditionMgr.telemetryObjects = {
|
||||
'test-object': testTelemetryObject
|
||||
};
|
||||
conditionMgr.updateConditionTelemetryObjects();
|
||||
conditionMgr.telemetryReceived(testTelemetryObject, {
|
||||
conditionMgr.telemetryReceived(testTelemetryObject, [
|
||||
{
|
||||
'some-key': 2,
|
||||
utc: date
|
||||
});
|
||||
}
|
||||
]);
|
||||
let result = conditionMgr.conditions.map((condition) => condition.result);
|
||||
expect(result[2]).toBeUndefined();
|
||||
});
|
||||
@ -1002,26 +1076,37 @@ describe('the plugin', function () {
|
||||
}
|
||||
};
|
||||
openmct.$injector = jasmine.createSpyObj('$injector', ['get']);
|
||||
// const mockTransactionService = jasmine.createSpyObj(
|
||||
// 'transactionService',
|
||||
// ['commit']
|
||||
// );
|
||||
openmct.telemetry = jasmine.createSpyObj('telemetry', [
|
||||
'isTelemetryObject',
|
||||
'request',
|
||||
'subscribe',
|
||||
'getMetadata',
|
||||
'getValueFormatter',
|
||||
'request'
|
||||
'requestCollection'
|
||||
]);
|
||||
openmct.telemetry.isTelemetryObject.and.returnValue(true);
|
||||
openmct.telemetry.subscribe.and.returnValue(function () {});
|
||||
openmct.telemetry.request.and.returnValue(Promise.resolve([]));
|
||||
openmct.telemetry.isTelemetryObject.and.returnValue(true);
|
||||
openmct.telemetry.getMetadata.and.returnValue(testTelemetryObject.telemetry);
|
||||
openmct.telemetry.getMetadata.and.returnValue({
|
||||
...testTelemetryObject.telemetry,
|
||||
valueMetadatas: testTelemetryObject.telemetry.values,
|
||||
valuesForHints: jasmine
|
||||
.createSpy('valuesForHints')
|
||||
.and.returnValue(testTelemetryObject.telemetry.values),
|
||||
value: jasmine.createSpy('value').and.callFake((key) => {
|
||||
return testTelemetryObject.telemetry.values.find((value) => value.key === key);
|
||||
})
|
||||
});
|
||||
openmct.telemetry.getValueFormatter.and.returnValue({
|
||||
parse: function (value) {
|
||||
return value;
|
||||
}
|
||||
});
|
||||
openmct.telemetry.getMetadata.and.returnValue(testTelemetryObject.telemetry);
|
||||
openmct.telemetry.request.and.returnValue(Promise.resolve([]));
|
||||
openmct.telemetry.requestCollection.and.returnValue({
|
||||
load: jasmine.createSpy('load'),
|
||||
on: jasmine.createSpy('on')
|
||||
});
|
||||
|
||||
const styleRuleManger = new StyleRuleManager(stylesObject, openmct, null, true);
|
||||
spyOn(styleRuleManger, 'subscribeToConditionSet');
|
||||
|
@ -29,12 +29,13 @@
|
||||
@end-move="endMove"
|
||||
>
|
||||
<template #content>
|
||||
<div class="c-image-view" :style="style"></div>
|
||||
<div v-show="showImage" aria-label="Image View" class="c-image-view" :style="style"></div>
|
||||
</template>
|
||||
</LayoutFrame>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { encode_url } from '../../../utils/encoding';
|
||||
import conditionalStylesMixin from '../mixins/objectStyles-mixin.js';
|
||||
import LayoutFrame from './LayoutFrame.vue';
|
||||
|
||||
@ -76,13 +77,16 @@ export default {
|
||||
},
|
||||
emits: ['move', 'end-move'],
|
||||
computed: {
|
||||
showImage() {
|
||||
return this.isEditing || !this.itemStyle?.isStyleInvisible;
|
||||
},
|
||||
style() {
|
||||
let backgroundImage = 'url(' + this.item.url + ')';
|
||||
let backgroundImage = `url('${encode_url(this.item.url)}')`;
|
||||
let border = '1px solid ' + this.item.stroke;
|
||||
|
||||
if (this.itemStyle) {
|
||||
if (this.itemStyle.imageUrl !== undefined) {
|
||||
backgroundImage = 'url(' + this.itemStyle.imageUrl + ')';
|
||||
backgroundImage = `url('${encode_url(this.itemStyle.imageUrl)}')`;
|
||||
}
|
||||
|
||||
border = this.itemStyle.border;
|
||||
|
@ -31,9 +31,10 @@
|
||||
<template #content>
|
||||
<div
|
||||
v-if="domainObject"
|
||||
v-show="showTelemetry"
|
||||
ref="telemetryViewWrapper"
|
||||
class="c-telemetry-view u-style-receiver"
|
||||
:class="[itemClasses]"
|
||||
:class="classNames"
|
||||
:style="styleObject"
|
||||
:data-font-size="item.fontSize"
|
||||
:data-font="item.font"
|
||||
@ -151,7 +152,10 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
itemClasses() {
|
||||
showTelemetry() {
|
||||
return this.isEditing || !this.itemStyle?.isStyleInvisible;
|
||||
},
|
||||
classNames() {
|
||||
let classes = [];
|
||||
|
||||
if (this.status) {
|
||||
|
@ -109,8 +109,9 @@ class DuplicateAction {
|
||||
let currentParentKeystring = this.openmct.objects.makeKeyString(currentParent.identifier);
|
||||
let parentCandidateKeystring = this.openmct.objects.makeKeyString(parentCandidate.identifier);
|
||||
let objectKeystring = this.openmct.objects.makeKeyString(this.object.identifier);
|
||||
const isLocked = parentCandidate.locked === true;
|
||||
|
||||
if (!this.openmct.objects.isPersistable(parentCandidate.identifier)) {
|
||||
if (isLocked || !this.openmct.objects.isPersistable(parentCandidate.identifier)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -139,10 +140,9 @@ class DuplicateAction {
|
||||
const parentType = parent && this.openmct.types.get(parent.type);
|
||||
const child = objectPath[0];
|
||||
const childType = child && this.openmct.types.get(child.type);
|
||||
const locked = child.locked ? child.locked : parent && parent.locked;
|
||||
const isPersistable = this.openmct.objects.isPersistable(child.identifier);
|
||||
|
||||
if (locked || !isPersistable) {
|
||||
if (!isPersistable) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -21,7 +21,12 @@
|
||||
-->
|
||||
|
||||
<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">
|
||||
<input
|
||||
type="checkbox"
|
||||
@ -33,29 +38,44 @@
|
||||
<div class="c-fault-mgmt-item">
|
||||
<div
|
||||
class="c-fault-mgmt__list-severity"
|
||||
:aria-label="fault.severity"
|
||||
:title="fault.severity"
|
||||
:aria-label="severityAriaLabel"
|
||||
:class="['is-severity-' + severity]"
|
||||
></div>
|
||||
</div>
|
||||
<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__list-path">{{ fault.namespace }}</div>
|
||||
<div class="c-fault-mgmt__list-faultname">{{ fault.name }}</div>
|
||||
<div class="c-fault-mgmt__list-path" aria-label="Fault namespace">
|
||||
{{ fault.namespace }}
|
||||
</div>
|
||||
<div class="c-fault-mgmt__list-faultname" aria-label="Fault name">{{ fault.name }}</div>
|
||||
</div>
|
||||
<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__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 }}
|
||||
</div>
|
||||
</div>
|
||||
<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 }}
|
||||
</div>
|
||||
</div>
|
||||
<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 }}
|
||||
</div>
|
||||
</div>
|
||||
@ -98,7 +118,7 @@ export default {
|
||||
emits: ['acknowledge-selected', 'shelve-selected', 'toggle-selected', 'clear-all-selected'],
|
||||
computed: {
|
||||
checkBoxAriaLabel() {
|
||||
return `Select fault: ${this.fault.name}`;
|
||||
return `Select fault: ${this.fault.name || 'Unknown'} in ${this.fault.namespace || 'Unknown'}`;
|
||||
},
|
||||
classesFromState() {
|
||||
const exclusiveStates = [
|
||||
@ -165,6 +185,12 @@ export default {
|
||||
classname += SEVERITY_CLASS[triggerValueInfo.monitoringResult] || '';
|
||||
|
||||
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: {
|
||||
|
@ -68,7 +68,6 @@
|
||||
import {
|
||||
FAULT_MANAGEMENT_ALARMS,
|
||||
FAULT_MANAGEMENT_GLOBAL_ALARMS,
|
||||
FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS,
|
||||
FILTER_ITEMS,
|
||||
SORT_ITEMS
|
||||
} from './constants.js';
|
||||
@ -88,6 +87,13 @@ const SEARCH_KEYS = [
|
||||
'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 {
|
||||
components: {
|
||||
FaultManagementListHeader,
|
||||
@ -111,23 +117,18 @@ export default {
|
||||
},
|
||||
filteredFaultsList() {
|
||||
const filterName = FILTER_ITEMS[this.filterIndex];
|
||||
let list = this.faultsList;
|
||||
|
||||
// Exclude shelved alarms from all views except the Shelved view
|
||||
if (filterName !== 'Shelved') {
|
||||
list = list.filter((fault) => fault.shelved !== true);
|
||||
}
|
||||
let list = this.faultsList.filter((fault) =>
|
||||
filterName === 'Shelved' ? fault.shelved : !fault.shelved
|
||||
);
|
||||
|
||||
if (filterName === 'Acknowledged') {
|
||||
list = list.filter((fault) => fault.acknowledged);
|
||||
} else if (filterName === 'Unacknowledged') {
|
||||
list = list.filter((fault) => !fault.acknowledged);
|
||||
} else if (filterName === 'Shelved') {
|
||||
list = list.filter((fault) => fault.shelved);
|
||||
}
|
||||
|
||||
if (this.searchTerm.length > 0) {
|
||||
list = list.filter(this.filterUsingSearchTerm);
|
||||
list = filterFaultsByTerm(list, this.searchTerm);
|
||||
}
|
||||
|
||||
list.sort(SORT_ITEMS[this.sortBy].sortFunction);
|
||||
@ -138,6 +139,9 @@ export default {
|
||||
return this.openmct.faults.supportsActions();
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.shelveDurations = this.openmct.faults.getShelveDurations();
|
||||
},
|
||||
mounted() {
|
||||
this.unsubscribe = this.openmct.faults.subscribe(this.domainObject, this.updateFault);
|
||||
},
|
||||
@ -158,14 +162,13 @@ export default {
|
||||
});
|
||||
}
|
||||
},
|
||||
updateFaultList() {
|
||||
this.openmct.faults.request(this.domainObject).then((faultsData) => {
|
||||
async updateFaultList() {
|
||||
const faultsData = await this.openmct.faults.request(this.domainObject);
|
||||
if (faultsData?.length > 0) {
|
||||
this.faultsList = faultsData.map((fd) => fd.fault);
|
||||
} else {
|
||||
this.faultsList = [];
|
||||
}
|
||||
});
|
||||
},
|
||||
filterUsingSearchTerm(fault) {
|
||||
if (!fault) {
|
||||
@ -223,14 +226,29 @@ export default {
|
||||
);
|
||||
},
|
||||
async toggleAcknowledgeSelected(faults = this.selectedFaults) {
|
||||
let title = '';
|
||||
if (faults.length > 1) {
|
||||
title = `Acknowledge ${faults.length} selected faults`;
|
||||
} else if (faults.length === 1) {
|
||||
title = `Acknowledge fault: ${faults[0].name}`;
|
||||
}
|
||||
const title = this.getAcknowledgeTitle(faults);
|
||||
|
||||
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,
|
||||
sections: [
|
||||
{
|
||||
@ -253,17 +271,11 @@ export default {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const data = await this.openmct.forms.showForm(formStructure);
|
||||
},
|
||||
acknowledgeFaults(faults, data) {
|
||||
faults.forEach((fault) => {
|
||||
this.openmct.faults.acknowledgeFault(fault, data);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
this.resetSelectedFaultMap();
|
||||
}
|
||||
},
|
||||
resetSelectedFaultMap() {
|
||||
Object.keys(this.selectedFaultMap).forEach((key) => {
|
||||
@ -273,7 +285,7 @@ export default {
|
||||
async toggleShelveSelected(faults = this.selectedFaults, shelveData = {}) {
|
||||
const { shelved = true } = shelveData;
|
||||
if (shelved) {
|
||||
let title =
|
||||
const title =
|
||||
faults.length > 1
|
||||
? `Shelve ${faults.length} selected faults`
|
||||
: `Shelve fault: ${faults[0].name}`;
|
||||
@ -295,10 +307,10 @@ export default {
|
||||
key: 'shelveDuration',
|
||||
control: 'select',
|
||||
name: 'Shelve duration',
|
||||
options: FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS,
|
||||
options: this.shelveDurations,
|
||||
required: false,
|
||||
cssClass: 'l-input-lg',
|
||||
value: FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS[0].value
|
||||
value: this.shelveDurations[0].value
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -319,18 +331,16 @@ export default {
|
||||
|
||||
shelveData.comment = data.comment || '';
|
||||
shelveData.shelveDuration =
|
||||
data.shelveDuration !== undefined
|
||||
? data.shelveDuration
|
||||
: FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS[0].value;
|
||||
data.shelveDuration === undefined ? this.shelveDurations[0].value : data.shelveDuration;
|
||||
} else {
|
||||
shelveData = {
|
||||
shelved: false
|
||||
};
|
||||
}
|
||||
|
||||
Object.values(faults).forEach((selectedFault) => {
|
||||
this.openmct.faults.shelveFault(selectedFault, shelveData);
|
||||
});
|
||||
await Promise.all(
|
||||
faults.map((selectedFault) => this.openmct.faults.shelveFault(selectedFault, shelveData))
|
||||
);
|
||||
|
||||
this.selectedFaultMap = {};
|
||||
},
|
||||
|
@ -42,24 +42,6 @@ export const FAULT_MANAGEMENT_TYPE = 'faultManagement';
|
||||
export const FAULT_MANAGEMENT_INSPECTOR = 'faultManagementInspector';
|
||||
export const FAULT_MANAGEMENT_ALARMS = 'alarms';
|
||||
export const FAULT_MANAGEMENT_GLOBAL_ALARMS = 'global-alarm-status';
|
||||
export const FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS = [
|
||||
{
|
||||
name: '5 Minutes',
|
||||
value: 300000
|
||||
},
|
||||
{
|
||||
name: '10 Minutes',
|
||||
value: 600000
|
||||
},
|
||||
{
|
||||
name: '15 Minutes',
|
||||
value: 900000
|
||||
},
|
||||
{
|
||||
name: 'Indefinite',
|
||||
value: 0
|
||||
}
|
||||
];
|
||||
export const FAULT_MANAGEMENT_VIEW = 'faultManagement.view';
|
||||
export const FAULT_MANAGEMENT_NAMESPACE = 'faults.taxonomy';
|
||||
export const FILTER_ITEMS = ['Standard View', 'Acknowledged', 'Unacknowledged', 'Shelved'];
|
||||
|
@ -27,9 +27,9 @@ To define a filter, you'll need to add a new `filter` property to the domain obj
|
||||
singleSelectionThreshold: true,
|
||||
comparator: 'equals',
|
||||
possibleValues: [
|
||||
{ name: 'Apple', value: 'apple' },
|
||||
{ name: 'Banana', value: 'banana' },
|
||||
{ name: 'Orange', value: 'orange' }
|
||||
{ label: 'Apple', value: 'apple' },
|
||||
{ label: 'Banana', value: 'banana' },
|
||||
{ label: 'Orange', value: 'orange' }
|
||||
]
|
||||
}]
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ class EditPropertiesAction extends PropertiesAction {
|
||||
const definition = this._getTypeDefinition(object.type);
|
||||
const persistable = this.openmct.objects.isPersistable(object.identifier);
|
||||
|
||||
return persistable && definition && definition.creatable;
|
||||
return persistable && definition && definition.creatable && !object.locked;
|
||||
}
|
||||
|
||||
invoke(objectPath) {
|
||||
|
@ -649,6 +649,11 @@ export default {
|
||||
},
|
||||
request(domainObject = this.telemetryObject) {
|
||||
this.metadata = this.openmct.telemetry.getMetadata(domainObject);
|
||||
|
||||
if (!this.metadata) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.formats = this.openmct.telemetry.getFormatMap(this.metadata);
|
||||
const LimitEvaluator = this.openmct.telemetry.getLimits(domainObject);
|
||||
LimitEvaluator.limits().then(this.updateLimits);
|
||||
|
@ -38,7 +38,7 @@
|
||||
<img
|
||||
ref="img"
|
||||
class="c-thumb__image"
|
||||
:src="`${image.thumbnailUrl || image.url}`"
|
||||
:src="imageSrc"
|
||||
fetchpriority="low"
|
||||
@load="imageLoadCompleted"
|
||||
/>
|
||||
@ -54,6 +54,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { encode_url } from '../../../utils/encoding';
|
||||
|
||||
const THUMB_PADDING = 4;
|
||||
const BORDER_WIDTH = 2;
|
||||
|
||||
@ -96,6 +98,9 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
imageSrc() {
|
||||
return `${encode_url(this.image.thumbnailUrl) || encode_url(this.image.url)}`;
|
||||
},
|
||||
ariaLabel() {
|
||||
return `Image thumbnail from ${this.image.formattedTime}${this.showAnnotationIndicator ? ', has annotations' : ''}`;
|
||||
},
|
||||
|
@ -370,6 +370,7 @@ export default {
|
||||
createImageWrapper(index, image, showImagePlaceholders) {
|
||||
const id = `${ID_PREFIX}${image.time}`;
|
||||
let imageWrapper = document.createElement('div');
|
||||
imageWrapper.ariaLabel = id;
|
||||
imageWrapper.classList.add(IMAGE_WRAPPER_CLASS);
|
||||
imageWrapper.style.left = `${this.xScale(image.time)}px`;
|
||||
this.setNSAttributesForElement(imageWrapper, {
|
||||
|
@ -222,6 +222,7 @@ import { TIME_CONTEXT_EVENTS } from '@/api/time/constants.js';
|
||||
import imageryData from '@/plugins/imagery/mixins/imageryData.js';
|
||||
import { VIEW_LARGE_ACTION_KEY } from '@/plugins/viewLargeAction/viewLargeAction.js';
|
||||
|
||||
import { encode_url } from '../../../utils/encoding';
|
||||
import eventHelpers from '../lib/eventHelpers.js';
|
||||
import AnnotationsCanvas from './AnnotationsCanvas.vue';
|
||||
import Compass from './Compass/CompassComponent.vue';
|
||||
@ -364,7 +365,7 @@ export default {
|
||||
filter: `brightness(${this.filters.brightness}%) contrast(${this.filters.contrast}%)`,
|
||||
backgroundImage: `${
|
||||
this.imageUrl
|
||||
? `url(${this.imageUrl}),
|
||||
? `url(${encode_url(this.imageUrl)}),
|
||||
repeating-linear-gradient(
|
||||
45deg,
|
||||
transparent,
|
||||
@ -620,7 +621,7 @@ export default {
|
||||
if (matchIndex > -1) {
|
||||
this.setFocusedImage(matchIndex);
|
||||
} else {
|
||||
this.paused();
|
||||
this.paused(false);
|
||||
}
|
||||
}
|
||||
|
||||
@ -789,7 +790,7 @@ export default {
|
||||
},
|
||||
getVisibleLayerStyles(layer) {
|
||||
return {
|
||||
backgroundImage: `url(${layer.source})`,
|
||||
backgroundImage: `url(${encode_url(layer.source)})`,
|
||||
transform: `scale(${this.zoomFactor}) translate(${this.imageTranslateX / 2}px, ${
|
||||
this.imageTranslateY / 2
|
||||
}px)`,
|
||||
@ -1082,7 +1083,7 @@ export default {
|
||||
paused(state) {
|
||||
this.isPaused = Boolean(state);
|
||||
|
||||
if (!state) {
|
||||
if (!this.isPaused) {
|
||||
this.previousFocusedImage = null;
|
||||
this.setFocusedImage(this.nextImageIndex);
|
||||
this.autoScroll = true;
|
||||
|
@ -90,16 +90,17 @@ export default {
|
||||
dataCleared() {
|
||||
this.imageHistory = [];
|
||||
},
|
||||
dataRemoved(dataToRemove) {
|
||||
this.imageHistory = this.imageHistory.filter((existingDatum) => {
|
||||
const shouldKeep = dataToRemove.some((datumToRemove) => {
|
||||
const existingDatumTimestamp = this.parseTime(existingDatum);
|
||||
const datumToRemoveTimestamp = this.parseTime(datumToRemove);
|
||||
|
||||
return existingDatumTimestamp !== datumToRemoveTimestamp;
|
||||
dataRemoved(removed) {
|
||||
const removedTimestamps = {};
|
||||
removed.forEach((_removed) => {
|
||||
const removedTimestamp = this.parseTime(_removed);
|
||||
removedTimestamps[removedTimestamp] = true;
|
||||
});
|
||||
|
||||
return shouldKeep;
|
||||
this.imageHistory = this.imageHistory.filter((image) => {
|
||||
const imageTimestamp = this.parseTime(image);
|
||||
|
||||
return !removedTimestamps[imageTimestamp];
|
||||
});
|
||||
},
|
||||
setDataTimeContext() {
|
||||
|
@ -96,6 +96,8 @@ export default {
|
||||
const createdTimestamp = this.domainObject.created;
|
||||
const createdBy = this.domainObject.createdBy ? this.domainObject.createdBy : UNKNOWN_USER;
|
||||
const modifiedBy = this.domainObject.modifiedBy ? this.domainObject.modifiedBy : UNKNOWN_USER;
|
||||
const locked = this.domainObject.locked;
|
||||
const lockedBy = this.domainObject.lockedBy ?? UNKNOWN_USER;
|
||||
const modifiedTimestamp = this.domainObject.modified
|
||||
? this.domainObject.modified
|
||||
: this.domainObject.created;
|
||||
@ -148,6 +150,13 @@ export default {
|
||||
});
|
||||
}
|
||||
|
||||
if (locked === true) {
|
||||
details.push({
|
||||
name: 'Locked By',
|
||||
value: lockedBy
|
||||
});
|
||||
}
|
||||
|
||||
if (version) {
|
||||
details.push({
|
||||
name: 'Version',
|
||||
|
@ -19,14 +19,23 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
const PERFORMANCE_OVERLAY_RENDER_INTERVAL = 1000;
|
||||
|
||||
export default function PerformanceIndicator() {
|
||||
return function install(openmct) {
|
||||
let frames = 0;
|
||||
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.description('Performance Indicator');
|
||||
indicator.statusClass('s-status-info');
|
||||
indicator.on('click', showOverlay);
|
||||
|
||||
openmct.indicators.add(indicator);
|
||||
|
||||
let rafHandle = requestAnimationFrame(incrementFrames);
|
||||
@ -58,5 +67,58 @@ export default function PerformanceIndicator() {
|
||||
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 }
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
191
src/plugins/persistence/couch/scripts/lockObjects.mjs
Normal file
191
src/plugins/persistence/couch/scripts/lockObjects.mjs
Normal file
@ -0,0 +1,191 @@
|
||||
import http from 'http';
|
||||
import nano from 'nano';
|
||||
import { parseArgs } from 'util';
|
||||
|
||||
const COUCH_URL = process.env.OPENMCT_COUCH_URL || 'http://127.0.0.1:5984';
|
||||
const COUCH_DB_NAME = process.env.OPENMCT_DATABASE_NAME || 'openmct';
|
||||
|
||||
const {
|
||||
values: { couchUrl, database, lock, unlock, startObjectKeystring, user, pass }
|
||||
} = parseArgs({
|
||||
options: {
|
||||
couchUrl: {
|
||||
type: 'string',
|
||||
default: COUCH_URL
|
||||
},
|
||||
database: {
|
||||
type: 'string',
|
||||
short: 'd',
|
||||
default: COUCH_DB_NAME
|
||||
},
|
||||
lock: {
|
||||
type: 'boolean',
|
||||
short: 'l'
|
||||
},
|
||||
unlock: {
|
||||
type: 'boolean',
|
||||
short: 'u'
|
||||
},
|
||||
startObjectKeystring: {
|
||||
type: 'string',
|
||||
short: 'o',
|
||||
default: 'mine'
|
||||
},
|
||||
user: {
|
||||
type: 'string'
|
||||
},
|
||||
pass: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const BATCH_SIZE = 100;
|
||||
const SOCKET_POOL_SIZE = 100;
|
||||
|
||||
const locked = lock === true;
|
||||
console.info(`Connecting to ${couchUrl}/${database}`);
|
||||
console.info(`${locked ? 'Locking' : 'Unlocking'} all children of ${startObjectKeystring}`);
|
||||
|
||||
const poolingAgent = new http.Agent({
|
||||
keepAlive: true,
|
||||
maxSockets: SOCKET_POOL_SIZE
|
||||
});
|
||||
|
||||
const db = nano({
|
||||
url: couchUrl,
|
||||
requestDefaults: {
|
||||
agent: poolingAgent
|
||||
}
|
||||
}).use(database);
|
||||
db.auth(user, pass);
|
||||
|
||||
if (!unlock && !lock) {
|
||||
throw new Error('Either -l or -u option is required');
|
||||
}
|
||||
|
||||
const startObjectIdentifier = keystringToIdentifier(startObjectKeystring);
|
||||
const documentBatch = [];
|
||||
const alreadySeen = new Set();
|
||||
let updatedDocumentCount = 0;
|
||||
|
||||
await processObjectTreeFrom(startObjectIdentifier);
|
||||
//Persist final batch
|
||||
await persistBatch();
|
||||
console.log(`Processed ${updatedDocumentCount} documents`);
|
||||
|
||||
function processObjectTreeFrom(parentObjectIdentifier) {
|
||||
//1. Fetch document for identifier;
|
||||
return fetchDocument(parentObjectIdentifier)
|
||||
.then(async (document) => {
|
||||
if (document !== undefined) {
|
||||
if (!alreadySeen.has(document._id)) {
|
||||
alreadySeen.add(document._id);
|
||||
//2. Lock or unlock object
|
||||
document.model.locked = locked;
|
||||
document.model.disallowUnlock = locked;
|
||||
|
||||
if (locked) {
|
||||
document.model.lockedBy = 'script';
|
||||
} else {
|
||||
delete document.model.lockedBy;
|
||||
}
|
||||
//3. Push document to a batch
|
||||
documentBatch.push(document);
|
||||
//4. Persist batch if necessary, reporting failures
|
||||
await persistBatchIfNeeded();
|
||||
//5. Repeat for each composee
|
||||
const composition = document.model.composition || [];
|
||||
return Promise.all(
|
||||
composition.map((composee) => {
|
||||
return processObjectTreeFrom(composee);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(`Error ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchDocument(identifierOrKeystring) {
|
||||
let keystring;
|
||||
if (typeof identifierOrKeystring === 'object') {
|
||||
keystring = identifierToKeystring(identifierOrKeystring);
|
||||
} else {
|
||||
keystring = identifierOrKeystring;
|
||||
}
|
||||
|
||||
try {
|
||||
const document = await db.get(keystring);
|
||||
|
||||
return document;
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function persistBatchIfNeeded() {
|
||||
if (documentBatch.length >= BATCH_SIZE) {
|
||||
return persistBatch();
|
||||
} else {
|
||||
//Noop - batch is not big enough yet
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async function persistBatch() {
|
||||
try {
|
||||
const localBatch = [].concat(documentBatch);
|
||||
|
||||
//Immediately clear the shared batch array. This asynchronous process is non-blocking, and
|
||||
//we don't want to try and persist the same batch multiple times while we are waiting for
|
||||
//the subsequent bulk operation to complete.
|
||||
updatedDocumentCount += documentBatch.length;
|
||||
|
||||
documentBatch.splice(0, documentBatch.length);
|
||||
const response = await db.bulk({ docs: localBatch });
|
||||
|
||||
if (response instanceof Array) {
|
||||
response.forEach((r) => {
|
||||
console.info(JSON.stringify(r));
|
||||
});
|
||||
} else {
|
||||
console.info(JSON.stringify(response));
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Array) {
|
||||
error.forEach((e) => console.error(JSON.stringify(e)));
|
||||
} else {
|
||||
console.error(`${error.statusCode} - ${error.reason}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function keystringToIdentifier(keystring) {
|
||||
const tokens = keystring.split(':');
|
||||
if (tokens.length === 2) {
|
||||
return {
|
||||
namespace: tokens[0],
|
||||
key: tokens[1]
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
namespace: '',
|
||||
key: tokens[0]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function identifierToKeystring(identifier) {
|
||||
if (typeof identifier === 'string') {
|
||||
return identifier;
|
||||
} else if (typeof identifier === 'object') {
|
||||
if (identifier.namespace) {
|
||||
return `${identifier.namespace}:${identifier.key}`;
|
||||
} else {
|
||||
return identifier.key;
|
||||
}
|
||||
}
|
||||
}
|
@ -160,6 +160,24 @@ add_index_and_views() {
|
||||
echo "Unable to create annotation_keystring_index"
|
||||
echo $response
|
||||
fi
|
||||
|
||||
# Add auth database for locked objects
|
||||
response=$(curl --silent --user "${CURL_USERPASS_ARG}" --request PUT "$COUCH_BASE_LOCAL"/"$OPENMCT_DATABASE_NAME"/_design/auth \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"_id": "_design/auth",
|
||||
"language": "javascript",
|
||||
"validate_doc_update": "function (newDoc, oldDoc, userCtx) { if (userCtx.roles.indexOf('\''_admin'\'') !== -1) { return; } else if (oldDoc === null) { return; } else if (oldDoc.model.type === '\''timer'\'' || oldDoc.model.type === '\''notebook'\'' || oldDoc.model.type === '\''restricted-notebook'\'') { if (oldDoc.model.name !== newDoc.model.name) { throw ({ forbidden: '\''Read-only object'\'' }); } else { return; } } else if (oldDoc.model.locked === true && oldDoc.model.disallowUnlock === true) { throw ({ forbidden: '\''Read-only object'\'' }); } else { return; }}"
|
||||
}')
|
||||
|
||||
if [[ $response =~ "\"ok\":true" ]]; then
|
||||
echo "Successfully created _design/auth design document for locked objects"
|
||||
elif [[ $response =~ "\"error\":\"conflict\"" ]]; then
|
||||
echo "_design/auth already exists, skipping creation"
|
||||
else
|
||||
echo "Unable to create _design/auth"
|
||||
echo $response
|
||||
fi
|
||||
}
|
||||
|
||||
# Main script execution
|
||||
|
@ -69,7 +69,6 @@ const INNER_TEXT_PADDING = 15;
|
||||
const TEXT_LEFT_PADDING = 5;
|
||||
const ROW_PADDING = 5;
|
||||
const SWIMLANE_PADDING = 3;
|
||||
const RESIZE_POLL_INTERVAL = 200;
|
||||
const ROW_HEIGHT = 22;
|
||||
const MAX_TEXT_WIDTH = 300;
|
||||
const MIN_ACTIVITY_WIDTH = 2;
|
||||
@ -143,13 +142,15 @@ export default {
|
||||
this.canvasContext = canvas.getContext('2d');
|
||||
this.setDimensions();
|
||||
this.setTimeContext();
|
||||
this.resizeTimer = setInterval(this.resize, RESIZE_POLL_INTERVAL);
|
||||
this.handleConfigurationChange(this.configuration);
|
||||
this.planViewConfiguration.on('change', this.handleConfigurationChange);
|
||||
this.loadComposition();
|
||||
|
||||
this.resizeObserver = new ResizeObserver(this.resize);
|
||||
this.resizeObserver.observe(this.$refs.plan);
|
||||
},
|
||||
beforeUnmount() {
|
||||
clearInterval(this.resizeTimer);
|
||||
this.resizeObserver.disconnect();
|
||||
this.stopFollowingTimeContext();
|
||||
if (this.unlisten) {
|
||||
this.unlisten();
|
||||
@ -242,11 +243,12 @@ export default {
|
||||
if (this.planObject) {
|
||||
this.showReplacePlanDialog(domainObject);
|
||||
} else {
|
||||
this.swimlaneVisibility = this.configuration.swimlaneVisibility;
|
||||
this.setupPlan(domainObject);
|
||||
this.swimlaneVisibility = this.configuration.swimlaneVisibility;
|
||||
}
|
||||
},
|
||||
handleConfigurationChange(newConfiguration) {
|
||||
this.configuration = this.planViewConfiguration.getConfiguration();
|
||||
Object.keys(newConfiguration).forEach((key) => {
|
||||
this[key] = newConfiguration[key];
|
||||
});
|
||||
@ -422,7 +424,10 @@ export default {
|
||||
return currentRow || SWIMLANE_PADDING;
|
||||
},
|
||||
generateActivities() {
|
||||
const groupNames = getValidatedGroups(this.domainObject, this.planData);
|
||||
if (!this.planObject) {
|
||||
return;
|
||||
}
|
||||
const groupNames = getValidatedGroups(this.planObject, this.planData);
|
||||
|
||||
if (!groupNames.length) {
|
||||
return;
|
||||
|
@ -1,5 +1,5 @@
|
||||
<!--
|
||||
Open MCT, Copyright (c) 2014-2023, United States Government
|
||||
Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
as represented by the Administrator of the National Aeronautics and Space
|
||||
Administration. All rights reserved.
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
<!--
|
||||
Open MCT, Copyright (c) 2014-2023, United States Government
|
||||
Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
as represented by the Administrator of the National Aeronautics and Space
|
||||
Administration. All rights reserved.
|
||||
|
||||
|
@ -164,11 +164,13 @@
|
||||
<div
|
||||
v-show="cursorGuide"
|
||||
ref="cursorGuideVertical"
|
||||
aria-label="Vertical cursor guide"
|
||||
class="c-cursor-guide--v js-cursor-guide--v"
|
||||
></div>
|
||||
<div
|
||||
v-show="cursorGuide"
|
||||
ref="cursorGuideHorizontal"
|
||||
aria-label="Horizontal cursor guide"
|
||||
class="c-cursor-guide--h js-cursor-guide--h"
|
||||
></div>
|
||||
</div>
|
||||
@ -537,6 +539,7 @@ export default {
|
||||
this.followTimeContext();
|
||||
},
|
||||
followTimeContext() {
|
||||
this.updateMode();
|
||||
this.updateDisplayBounds(this.timeContext.getBounds());
|
||||
this.timeContext.on('modeChanged', this.updateMode);
|
||||
this.timeContext.on('boundsChanged', this.updateDisplayBounds);
|
||||
@ -854,13 +857,11 @@ export default {
|
||||
|
||||
this.canvas = this.$refs.chartContainer.querySelectorAll('canvas')[1];
|
||||
|
||||
if (!this.options.compact) {
|
||||
this.listenTo(this.canvas, 'mousemove', this.trackMousePosition, this);
|
||||
this.listenTo(this.canvas, 'mouseleave', this.untrackMousePosition, this);
|
||||
this.listenTo(this.canvas, 'mousedown', this.onMouseDown, this);
|
||||
this.listenTo(this.canvas, 'click', this.selectNearbyAnnotations, this);
|
||||
this.listenTo(this.canvas, 'wheel', this.wheelZoom, this);
|
||||
}
|
||||
},
|
||||
|
||||
marqueeAnnotations(annotationsToSelect) {
|
||||
@ -1115,6 +1116,7 @@ export default {
|
||||
this.listenTo(window, 'mouseup', this.onMouseUp, this);
|
||||
this.listenTo(window, 'mousemove', this.trackMousePosition, this);
|
||||
|
||||
if (!this.options.compact) {
|
||||
// track frozen state on mouseDown to be read on mouseUp
|
||||
const isFrozen =
|
||||
this.config.xAxis.get('frozen') === true && this.config.yAxis.get('frozen') === true;
|
||||
@ -1129,6 +1131,7 @@ export default {
|
||||
} else {
|
||||
return this.startMarquee(event, false);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onMouseUp(event) {
|
||||
@ -1158,11 +1161,15 @@ export default {
|
||||
},
|
||||
|
||||
isMouseClick() {
|
||||
if (!this.marquee) {
|
||||
// We may not have a marquee if we've disabled pan/zoom, but we still need to know if it's a mouse click for highlights and lock points.
|
||||
if (!this.marquee && !this.positionOverPlot) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { start, end } = this.marquee;
|
||||
const { start, end } = this.marquee ?? {
|
||||
start: this.positionOverPlot,
|
||||
end: this.positionOverPlot
|
||||
};
|
||||
const someYPositionOverPlot = start.y.some((y) => y);
|
||||
|
||||
return start.x === end.x && someYPositionOverPlot;
|
||||
|
@ -162,14 +162,6 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
gridLines(newGridLines) {
|
||||
this.gridLines = newGridLines;
|
||||
},
|
||||
cursorGuide(newCursorGuide) {
|
||||
this.cursorGuide = newCursorGuide;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
eventHelpers.extend(this);
|
||||
this.imageExporter = new ImageExporter(this.openmct);
|
||||
|
@ -128,35 +128,22 @@ export default {
|
||||
}
|
||||
},
|
||||
updateStyle(styleObj) {
|
||||
let elemToStyle = this.getStyleReceiver();
|
||||
const elemToStyle = this.getStyleReceiver();
|
||||
|
||||
if (!styleObj || elemToStyle === undefined) {
|
||||
if (!styleObj || !elemToStyle) {
|
||||
return;
|
||||
}
|
||||
|
||||
let keys = Object.keys(styleObj);
|
||||
|
||||
keys.forEach((key) => {
|
||||
if (elemToStyle) {
|
||||
if (typeof styleObj[key] === 'string' && styleObj[key].indexOf('__no_value') > -1) {
|
||||
if (elemToStyle.style[key]) {
|
||||
elemToStyle.style[key] = '';
|
||||
// handle visibility separately
|
||||
if (styleObj.isStyleInvisible !== undefined) {
|
||||
elemToStyle.classList.toggle(STYLE_CONSTANTS.isStyleInvisible, styleObj.isStyleInvisible);
|
||||
styleObj.isStyleInvisible = null;
|
||||
}
|
||||
|
||||
Object.entries(styleObj).forEach(([key, value]) => {
|
||||
if (typeof value !== 'string' || !value.includes('__no_value')) {
|
||||
elemToStyle.style[key] = value;
|
||||
} else {
|
||||
if (
|
||||
!styleObj.isStyleInvisible &&
|
||||
elemToStyle.classList.contains(STYLE_CONSTANTS.isStyleInvisible)
|
||||
) {
|
||||
elemToStyle.classList.remove(STYLE_CONSTANTS.isStyleInvisible);
|
||||
} else if (
|
||||
styleObj.isStyleInvisible &&
|
||||
!elemToStyle.classList.contains(styleObj.isStyleInvisible)
|
||||
) {
|
||||
elemToStyle.classList.add(styleObj.isStyleInvisible);
|
||||
}
|
||||
|
||||
elemToStyle.style[key] = styleObj[key];
|
||||
}
|
||||
elemToStyle.style[key] = ''; // remove the property
|
||||
}
|
||||
});
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user