Compare commits

..

1 Commits

Author SHA1 Message Date
Scott Bell
d053e65a39 add memory debugging 2024-02-16 12:04:18 +01:00
185 changed files with 1977 additions and 16132 deletions

View File

@@ -1,7 +1,4 @@
version: 2.1
orbs:
node: circleci/node@5.2.0
browser-tools: circleci/browser-tools@1.3.0
executors:
pw-focal-development:
docker:
@@ -10,22 +7,51 @@ executors:
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
PERCY_LOGLEVEL: "debug" # Enable DEBUG level logging for Percy (Issue: https://github.com/nasa/openmct/issues/5742)
PERCY_PARALLEL_TOTAL: 2
ubuntu:
machine:
image: ubuntu-2204:current
docker_layer_caching: true
parameters:
BUST_CACHE:
description: "Set this with the CircleCI UI Trigger Workflow button (boolean = true) to bust the cache!"
default: false
type: boolean
commands:
build_and_install:
description: "All steps used to build and install."
description: "All steps used to build and install. Will use cache if found"
parameters:
node-version:
type: string
steps:
- checkout
- restore_cache_cmd:
node-version: << parameters.node-version >>
- node/install:
node-version: << parameters.node-version >>
- node/install-packages
- run: npm install --no-audit --progress=false
restore_cache_cmd:
description: "Custom command for restoring cache with the ability to bust cache. When BUST_CACHE is set to true, jobs will not restore cache"
parameters:
node-version:
type: string
steps:
- when:
condition:
equal: [false, << pipeline.parameters.BUST_CACHE >>]
steps:
- restore_cache:
key: deps--{{ arch }}--{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}-{{ checksum ".circleci/config.yml" }}
save_cache_cmd:
description: "Custom command for saving cache."
parameters:
node-version:
type: string
steps:
- save_cache:
key: deps--{{ arch }}--{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}-{{ checksum ".circleci/config.yml" }}
paths:
- ~/.npm
- node_modules
generate_and_store_version_and_filesystem_artifacts:
description: "Track important packages and files"
steps:
@@ -45,6 +71,9 @@ commands:
steps:
- run: npm run cov:e2e:report || true
- run: npm run cov:e2e:<<parameters.suite>>:publish
orbs:
node: circleci/node@5.1.0
browser-tools: circleci/browser-tools@1.3.0
jobs:
npm-audit:
parameters:
@@ -82,6 +111,8 @@ jobs:
TESTFILES=$(circleci tests glob "src/**/*Spec.js")
echo "$TESTFILES" | circleci tests run --command="xargs npm run test" --verbose
- run: npm run cov:unit:publish
- save_cache_cmd:
node-version: <<parameters.node-version>>
- store_test_results:
path: dist/reports/tests/
- store_artifacts:
@@ -129,31 +160,6 @@ jobs:
equal: [42, 42] # Always generate version artifacts regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2
steps:
- generate_and_store_version_and_filesystem_artifacts
e2e-mobile:
executor: pw-focal-development
steps:
- build_and_install:
node-version: lts/hydrogen
- run: npm run test:e2e:mobile
- when:
condition:
equal: [42, 42] # Always run codecov reports regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2
steps:
- generate_e2e_code_cov_report:
suite: full
- store_test_results:
path: test-results/results.xml
- store_artifacts:
path: test-results
- store_artifacts:
path: coverage
- store_artifacts:
path: html-test-results
- when:
condition:
equal: [42, 42] # Always generate version artifacts regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2
steps:
- generate_and_store_version_and_filesystem_artifacts
e2e-couchdb:
executor: ubuntu
steps:
@@ -221,20 +227,15 @@ jobs:
equal: [42, 42] # Always run codecov reports regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2
steps:
- generate_and_store_version_and_filesystem_artifacts
visual-a11y:
visual-a11y-tests:
parameters:
suite:
type: string # ci or full
executor: pw-focal-development
parallelism: 2
steps:
- build_and_install:
node-version: lts/iron
- run:
command: |
mkdir test-results
TESTFILES=$(circleci tests glob "e2e/**/*.spec.js")
echo "$TESTFILES" | circleci tests run --command="xargs npm run test:e2e:visual:<<parameters.suite>>" --verbose --split-by=timings
node-version: lts/hydrogen
- run: npm run test:e2e:visual:<<parameters.suite>>
- store_test_results:
path: test-results/results.xml
- store_artifacts:
@@ -259,9 +260,8 @@ workflows:
- e2e-test:
name: e2e-stable
suite: stable
- e2e-mobile
- visual-a11y:
name: visual-a11y-ci
- visual-a11y-tests:
name: visual-test-ci
suite: ci
the-nightly: #These jobs do not run on PRs, but against master at night
@@ -277,11 +277,10 @@ workflows:
- e2e-test:
name: e2e-full-nightly
suite: full
- e2e-mobile
- perf-test
- mem-test
- visual-a11y:
name: visual-a11y-nightly
- perf-test
- visual-a11y-tests:
name: visual-test-nightly
suite: full
- e2e-couchdb
triggers:

View File

@@ -8,7 +8,7 @@ Closes <!--- Insert Issue Number(s) this PR addresses. Start by typing # will op
* [ ] Have you followed the guidelines in our [Contributing document](https://github.com/nasa/openmct/blob/master/CONTRIBUTING.md)?
* [ ] Have you checked to ensure there aren't other open [Pull Requests](https://github.com/nasa/openmct/pulls) for the same update/change?
* [ ] Is this a [notable change](../docs/src/process/release.md) that will require a special callout in the release notes? For example, will this break compatibility with existing APIs or projects that consume these plugins?
* [ ] Is this change backwards compatible? For example, developers won't need to change how they are calling the API or how they've extended core plugins such as Tables or Plots.
### Author Checklist
@@ -17,6 +17,7 @@ Closes <!--- Insert Issue Number(s) this PR addresses. Start by typing # will op
* [ ] Has this been smoke tested?
* [ ] Have you associated this PR with a `type:` label? Note: this is not necessarily the same as the original issue.
* [ ] Have you associated a milestone with this PR? Note: leave blank if unsure.
* [ ] Is this a breaking change to be called out in the release notes?
* [ ] Testing instructions included in associated issue OR is this a dependency/testcase change?
### Reviewer Checklist

5
.github/release.yml vendored
View File

@@ -1,8 +1,5 @@
changelog:
categories:
- title: 💥 Notable Changes
labels:
- notable_change
- title: 🏕 Features
labels:
- type:feature
@@ -23,4 +20,4 @@ changelog:
- dependencies
- title: 🐛 Bug Fixes
labels:
- "*"
- '*'

View File

@@ -28,7 +28,7 @@ jobs:
restore-keys: |
${{ runner.os }}-node-
- run: npm ci --no-audit --progress=false
- run: npm install --cache ~/.npm --no-audit --progress=false
- name: Login to DockerHub
uses: docker/login-action@v3

View File

@@ -1,15 +1,15 @@
name: 'pr:e2e:flakefinder'
on:
# push:
# branches: master
push:
branches: master
workflow_dispatch:
# pull_request:
# types:
# - labeled
# - opened
# schedule:
# - cron: '0 0 * * *'
pull_request:
types:
- labeled
- opened
schedule:
- cron: '0 0 * * *'
jobs:
e2e-flakefinder:
@@ -31,7 +31,7 @@ jobs:
${{ runner.os }}-node-
- run: npx playwright@1.39.0 install
- run: npm ci --no-audit --progress=false
- run: npm install --cache ~/.npm --no-audit --progress=false
- name: Run E2E Tests (Repeated 10 Times)
run: npm run test:e2e:stable -- --retries=0 --repeat-each=10 --max-failures=50

View File

@@ -29,7 +29,7 @@ jobs:
${{ runner.os }}-node-
- run: npx playwright@1.39.0 install
- run: npm ci --no-audit --progress=false
- run: npm install --cache ~/.npm --no-audit --progress=false
- run: npm run test:perf:localhost
- run: npm run test:perf:contract
- run: npm run test:perf:memory

View File

@@ -35,7 +35,7 @@ jobs:
- run: npx playwright@1.39.0 install
- run: npx playwright install chrome-beta
- run: npm ci --no-audit --progress=false
- run: npm install --cache ~/.npm --no-audit --progress=false
- run: npm run test:e2e:full -- --max-failures=40
- run: npm run cov:e2e:report || true
- shell: bash

View File

@@ -15,7 +15,7 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: lts/hydrogen
- run: npm ci
- run: npm install
- run: |
echo "//registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN" >> ~/.npmrc
npm whoami
@@ -31,7 +31,7 @@ jobs:
with:
node-version: lts/hydrogen
registry-url: https://registry.npmjs.org/
- run: npm ci
- run: npm install
- run: npm publish --access=public --tag unstable
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -45,7 +45,7 @@ jobs:
restore-keys: |
${{ runner.os }}-${{ matrix.node_version }}-
- run: npm ci --no-audit --progress=false
- run: npm install --cache ~/.npm --no-audit --progress=false
- run: npm test

3
.gitignore vendored
View File

@@ -47,3 +47,6 @@ index.html.bak
.nyc_output
coverage
codecov
# :(
package-lock.json

3
.npmrc
View File

@@ -2,3 +2,6 @@ loglevel=warn
#Prevent folks from ignoring an important error when building from source
engine-strict=true
# Dont include lockfile
package-lock=false

View File

@@ -5,6 +5,7 @@
// List of extensions which should be recommended for users of this workspace.
"recommendations": [
"Vue.volar",
"Vue.vscode-typescript-vue-plugin",
"dbaeumer.vscode-eslint",
"rvest.vs-code-prettier-eslint"
],

View File

@@ -5,8 +5,11 @@ information to pull requests.
*/
import config from './webpack.dev.js';
// eslint-disable-next-line no-undef
const CI = process.env.CI === 'true';
config.devtool = CI ? false : undefined;
config.devtool = 'source-map';
config.devServer.hot = false;
config.module.rules.push({

View File

@@ -16,6 +16,8 @@ The [CodeQL GitHub Actions workflow](https://github.com/nasa/openmct/blob/master
CodeQL is run for every pull-request in GitHub Actions.
The project is also monitored by [LGTM](https://lgtm.com/projects/g/nasa/openmct/) and is available to public.
### ESLint
Static analysis is run for every push on the master branch and every pull request on all branches in Github Actions.

View File

@@ -91,14 +91,12 @@ There are a few reasons that your GitHub PR could be failing beyond simple faile
### Local=Pass and CI=Fail
Although rare, it is possible that your test can pass locally but fail in CI.
### Reset your workspace
It's possible that you're running with dependencies or a local environment which is out of sync with the branch you're working on. Make sure to execute the following:
```sh
nvm use
npm run clean
npm install
```
#### Busting Cache
In certain circumstances, the CircleCI cache can become stale. In order to bust the cache, we've implemented a runtime boolean parameter in Circle CI creatively name BUST_CACHE. To execute:
1. Navigate to the branch in Circle CI believed to have stale cache.
1. Click on the 'Trigger Pipeline' button.
1. Add Parameter -> Parameter Type = boolean , Name = BUST_CACHE ,Value = true
1. Click 'Trigger Pipeline'
#### Run tests in the same container as CI

View File

@@ -1,30 +0,0 @@
# Release of NASA Open MCT NPM Package
This document outlines the process and key considerations for releasing a new version of the NASA Open MCT project as an NPM (Node Package Manager) package.
## 1. Pre-requisites
Before releasing a new version of the NASA Open MCT NPM package, ensure all dependencies are updated, and comprehensive tests are performed. This ensures compatibility and performance of the Open MCT within the Node.js ecosystem.
## 2. Versioning
Versioning is a critical step for package release. The Open MCT team follows [Semantic Versioning (SemVer)](https://semver.org) that consists of three major components: MAJOR.MINOR.PATCH. These ensure a structured process for updating, bug fixes, backward compatibility, and software progress.
## 3. Changelog Maintenance
A comprehensive changelog file, `CHANGELOG.md`, documents any changes, adding a high level of transparencies for anyone desiring to look into the status of new and past progress. It includes the summation of any major new enhancements, changes, bug fixes, and the credits to the users responsible for each unique progress.
## 4. Notable Changes Labels on GitHub PRs
For the Open MCT package, we leverage GitHub's Pull Request (PR) mechanisms extensively, with three important PR labels dedicated to signifying 'notable_changes':
- **Breaking Change** Highlights the integration of changes that are suspected to break, or without a doubt will break, backward compatibility. These should signal to users the upgrade might be seamless only if dependency and integration factors are properly managed, if not, one should expect to manage atypical technical snags.
- **API change** Signifies when a contribution makes any complete or under layer changes to the communication or its supporting access processes. This label flags required see-through insight on how the web-based control panel sees and manipulates any value and or network logs.
- **Default Behavior Change:** In the incident an update either adjusts a form to or integrates a not previously kept setting or plugin. i.e. autoscale is enabled by default when working with plots.
## 6. Community & Contributions
A flat community and the rounded center are kept in continuous celebration, with the given station open for two open-specifying dialogues, research, and all-for development probing. State the ownership for a handed looped, a welcome for even structure-core and architectural draft and impend.
Thank you for your collaboration and commitment to moving the project onto a text big club.

View File

@@ -229,7 +229,7 @@ Current list of test tags:
|Test Tag|Description|
|:-:|-|
|`@mobile` | Test case or test suite is compatible with Playwright's iPad support and Open MCT's read-only mobile view (i.e. no create button).|
|`@ipad` | Test case or test suite is compatible with Playwright's iPad support and Open MCT's read-only mobile view (i.e. no create button).|
|`@a11y` | Test case or test suite to execute playwright-axe accessibility checks and generate a11y reports.|
|`@gds` | Denotes a GDS Test Case used in the VIPER Mission.|
|`@addInit` | Initializes the browser with an injected and artificial state. Useful for loading non-default plugins. Likely will not work outside of `npm start`.|
@@ -238,7 +238,6 @@ Current list of test tags:
|`@unstable` | A new test or test which is known to be flaky.|
|`@2p` | Indicates that multiple users are involved, or multiple tabs/pages are used. Useful for testing multi-user interactivity.|
|`@generatedata` | Indicates that a test is used to generate testdata or test the generated test data. Usually to be associated with localstorage, but this may grow over time.|
|`@clock` | A test which modifies the clock. These have expanded out of the visual tests and into the functional tests.
### Continuous Integration
@@ -330,15 +329,9 @@ In terms of operating system testing, we're only limited by what the CI provider
#### **Mobile**
We have a Mission-need to support iPad and mobile devices. To run our test suites with mobile devices, please see our `playwright-mobile.config.js` projects.
We have the Mission-need to support iPad. To run our iPad suite, please see our `playwright-*.config.js` with the 'iPad' project.
In general, our test suite is not designed to run against mobile devices as the mobile experience is a focused version of the application. Core functionality is missing (chiefly the 'Create' button). To bypass the object creation, we leverage the `storageState` properties for starting the mobile tests with localstorage.
For now, the mobile tests will exist in the /tests/mobile/ suites and be executed with the
```sh
npm run test:e2e:mobile
```
command.
In general, our test suite is not designed to run against mobile devices as the mobile experience is a focused version of the application. Core functionality is missing (chiefly the 'Create' button) and so this will likely turn into a separate suite.
#### **Skipping or executing tests based on browser, os, and/os browser version:**
@@ -448,7 +441,6 @@ By adhering to this principle, we can create tests that are both robust and refl
- Utilize `percyCSS` to ignore time-based elements. For more details, consult our [percyCSS file](./.percy.ci.yml).
- Use Open MCT's fixed-time mode unless explicitly testing realtime clock
- Employ the `createExampleTelemetryObject` appAction to source telemetry and specify a `name` to avoid autogenerated names.
- Avoid creating objects with a time component like timers and clocks.
5. **Hide the Tree and Inspector**: Generally, your test will not require comparisons involving the tree and inspector. These aspects are covered in component-specific tests (explained below). To exclude them from the comparison by default, navigate to the root of the main view with the tree and inspector hidden:
- `await page.goto('./#/browse/mine?hideTree=true&hideInspector=true')`
@@ -516,30 +508,6 @@ test.describe('foo test suite', () => {
- Working with multiple pages
There are instances where multiple browser pages will needed to verify multi-page or multi-tab application behavior. Make sure to use the `@2p` annotation as well as name each page appropriately: i.e. `page1` and `page2` or `tab1` and `tab2` depending on the intended use case. Generally pages should be used unless testing `sharedWorker` code, specifically.
- Working with file downloads and JSON data
Open MCT has the capability of exporting certain objects in the form of a JSON file handled by the chrome browser. The best example of this type of test can be found in the exportAsJson test.
```js
const [download] = await Promise.all([
page.waitForEvent('download'), // Waits for the download event
page.getByLabel('Export as JSON').click() // Triggers the download
]);
// Wait for the download process to complete
const path = await download.path();
// Read the contents of the downloaded file using readFile from fs/promises
const fileContents = await fs.readFile(path, 'utf8');
const jsonData = JSON.parse(fileContents);
// Use the function to retrieve the key
const key = getFirstKeyFromOpenMctJson(jsonData);
// Verify the contents of the JSON file
expect(jsonData.openmct[key]).toHaveProperty('name', 'e2e folder');
```
### Reporting
Test Reporting is done through official Playwright reporters and the CI Systems which execute them.

View File

@@ -392,8 +392,6 @@ async function setTimeConductorMode(page, isFixedTimespan = true) {
await page.getByRole('menuitem', { name: /Real-Time/ }).click();
await page.waitForURL(/tc\.mode=local/);
}
//dismiss the time conductor popup
await page.getByLabel('Discard changes and close time popup').click();
}
/**
@@ -507,14 +505,15 @@ async function setTimeConductorBounds(page, startDate, endDate) {
* @param {string} startDate
* @param {string} endDate
*/
async function setIndependentTimeConductorBounds(page, { start, end }) {
// Activate Independent Time Conductor
await page.getByLabel('Enable Independent Time Conductor').click();
async function setIndependentTimeConductorBounds(page, startDate, endDate) {
// Activate Independent Time Conductor in Fixed Time Mode
await page.getByRole('switch').click();
// Bring up the time conductor popup
await page.getByLabel('Independent Time Conductor Settings').click();
await page.click('.c-conductor-holder--compact .c-compact-tc');
await expect(page.locator('.itc-popout')).toBeInViewport();
await setTimeBounds(page, start, end);
await setTimeBounds(page, startDate, endDate);
await page.keyboard.press('Enter');
}
@@ -664,6 +663,5 @@ export {
setRealTimeMode,
setStartOffset,
setTimeConductorBounds,
setTimeConductorMode,
waitForPlotsToRender
};

View File

@@ -21,12 +21,10 @@
*****************************************************************************/
import { fileURLToPath } from 'url';
import { expect } from '../pluginFixtures.js';
/**
* @param {import('@playwright/test').Page} page
*/
export async function navigateToFaultManagementWithExample(page) {
async function navigateToFaultManagementWithExample(page) {
await page.addInitScript({
path: fileURLToPath(new URL('./addInitExampleFaultProvider.js', import.meta.url))
});
@@ -37,7 +35,7 @@ export async function navigateToFaultManagementWithExample(page) {
/**
* @param {import('@playwright/test').Page} page
*/
export async function navigateToFaultManagementWithStaticExample(page) {
async function navigateToFaultManagementWithStaticExample(page) {
await page.addInitScript({
path: fileURLToPath(new URL('./addInitExampleFaultProviderStatic.js', import.meta.url))
});
@@ -48,7 +46,7 @@ export async function navigateToFaultManagementWithStaticExample(page) {
/**
* @param {import('@playwright/test').Page} page
*/
export async function navigateToFaultManagementWithoutExample(page) {
async function navigateToFaultManagementWithoutExample(page) {
await page.addInitScript({
path: fileURLToPath(new URL('./addInitFaultManagementPlugin.js', import.meta.url))
});
@@ -59,7 +57,7 @@ export async function navigateToFaultManagementWithoutExample(page) {
/**
* @param {import('@playwright/test').Page} page
*/
export async function navigateToFaultItemInTree(page) {
async function navigateToFaultItemInTree(page) {
await page.goto('./', { waitUntil: 'networkidle' });
const faultManagementTreeItem = page
@@ -77,95 +75,88 @@ export async function navigateToFaultItemInTree(page) {
/**
* @param {import('@playwright/test').Page} page
*/
export async function acknowledgeFault(page, rowNumber) {
async function acknowledgeFault(page, rowNumber) {
await openFaultRowMenu(page, rowNumber);
await page.getByLabel('Acknowledge', { exact: true }).click();
await page.getByLabel('Save').click();
await page.locator('.c-menu >> text="Acknowledge"').click();
// Click [aria-label="Save"]
await page.locator('[aria-label="Save"]').click();
}
/**
* @param {import('@playwright/test').Page} page
*/
export async function shelveMultipleFaults(page, ...nums) {
async function shelveMultipleFaults(page, ...nums) {
const selectRows = nums.map((num) => {
return selectFaultItem(page, num);
});
await Promise.all(selectRows);
await page.getByLabel('Shelve selected faults').click();
await page.getByLabel('Save').click();
await page.locator('button:has-text("Shelve")').click();
await page.locator('[aria-label="Save"]').click();
}
/**
* @param {import('@playwright/test').Page} page
*/
export async function acknowledgeMultipleFaults(page, ...nums) {
async function acknowledgeMultipleFaults(page, ...nums) {
const selectRows = nums.map((num) => {
return selectFaultItem(page, num);
});
await Promise.all(selectRows);
await page.locator('button:has-text("Acknowledge")').click();
await page.getByLabel('Save').click();
await page.locator('[aria-label="Save"]').click();
}
/**
* @param {import('@playwright/test').Page} page
*/
export async function shelveFault(page, rowNumber) {
async function shelveFault(page, rowNumber) {
await openFaultRowMenu(page, rowNumber);
await page.locator('.c-menu >> text="Shelve"').click();
// Click [aria-label="Save"]
await page.getByLabel('Save').click();
await page.locator('[aria-label="Save"]').click();
}
/**
* @param {import('@playwright/test').Page} page
*/
export async function changeViewTo(page, view) {
async function changeViewTo(page, view) {
await page.locator('.c-fault-mgmt__search-row select').first().selectOption(view);
}
/**
* @param {import('@playwright/test').Page} page
*/
export async function sortFaultsBy(page, sort) {
async function sortFaultsBy(page, sort) {
await page.locator('.c-fault-mgmt__list-header-sortButton select').selectOption(sort);
}
/**
* @param {import('@playwright/test').Page} page
*/
export async function enterSearchTerm(page, term) {
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) {
async function clearSearch(page) {
await enterSearchTerm(page, '');
}
/**
* @param {import('@playwright/test').Page} page
*/
export async function selectFaultItem(page, rowNumber) {
await page
.getByLabel('Select fault')
.nth(rowNumber - 1)
.check({
// Need force here because checkbox state is changed by an event emitted by the checkbox
// eslint-disable-next-line playwright/no-force-option
force: true
});
await expect(page.getByLabel('Select fault').nth(rowNumber - 1)).toBeChecked();
async function selectFaultItem(page, rowNumber) {
await page.locator(`.c-fault-mgmt-item > input >> nth=${rowNumber - 1}`).check();
}
/**
* @param {import('@playwright/test').Page} page
*/
export async function getHighestSeverity(page) {
async function getHighestSeverity(page) {
const criticalCount = await page.locator('[title=CRITICAL]').count();
const warningCount = await page.locator('[title=WARNING]').count();
@@ -181,7 +172,7 @@ export async function getHighestSeverity(page) {
/**
* @param {import('@playwright/test').Page} page
*/
export async function getLowestSeverity(page) {
async function getLowestSeverity(page) {
const warningCount = await page.locator('[title=WARNING]').count();
const watchCount = await page.locator('[title=WATCH]').count();
@@ -197,7 +188,7 @@ export async function getLowestSeverity(page) {
/**
* @param {import('@playwright/test').Page} page
*/
export async function getFaultResultCount(page) {
async function getFaultResultCount(page) {
const count = await page.locator('.c-faults-list-view-item-body > .c-fault-mgmt__list').count();
return count;
@@ -206,7 +197,7 @@ export async function getFaultResultCount(page) {
/**
* @param {import('@playwright/test').Page} page
*/
export function getFault(page, rowNumber) {
function getFault(page, rowNumber) {
const fault = page.locator(
`.c-faults-list-view-item-body > .c-fault-mgmt__list >> nth=${rowNumber - 1}`
);
@@ -217,7 +208,7 @@ export function getFault(page, rowNumber) {
/**
* @param {import('@playwright/test').Page} page
*/
export function getFaultByName(page, name) {
function getFaultByName(page, name) {
const fault = page.locator(`.c-fault-mgmt__list-faultname:has-text("${name}")`);
return fault;
@@ -226,7 +217,7 @@ export function getFaultByName(page, name) {
/**
* @param {import('@playwright/test').Page} page
*/
export async function getFaultName(page, rowNumber) {
async function getFaultName(page, rowNumber) {
const faultName = await page
.locator(`.c-fault-mgmt__list-faultname >> nth=${rowNumber - 1}`)
.textContent();
@@ -237,7 +228,7 @@ export async function getFaultName(page, rowNumber) {
/**
* @param {import('@playwright/test').Page} page
*/
export async function getFaultSeverity(page, rowNumber) {
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');
@@ -248,7 +239,7 @@ export async function getFaultSeverity(page, rowNumber) {
/**
* @param {import('@playwright/test').Page} page
*/
export async function getFaultNamespace(page, rowNumber) {
async function getFaultNamespace(page, rowNumber) {
const faultNamespace = await page
.locator(`.c-fault-mgmt__list-path >> nth=${rowNumber - 1}`)
.textContent();
@@ -259,7 +250,7 @@ export async function getFaultNamespace(page, rowNumber) {
/**
* @param {import('@playwright/test').Page} page
*/
export async function getFaultTriggerTime(page, rowNumber) {
async function getFaultTriggerTime(page, rowNumber) {
const faultTriggerTime = await page
.locator(`.c-fault-mgmt__list-trigTime >> nth=${rowNumber - 1} >> .c-fault-mgmt-item__value`)
.textContent();
@@ -270,10 +261,35 @@ export async function getFaultTriggerTime(page, rowNumber) {
/**
* @param {import('@playwright/test').Page} page
*/
export async function openFaultRowMenu(page, rowNumber) {
async function openFaultRowMenu(page, rowNumber) {
// select
await page
.getByLabel('Disposition actions')
.nth(rowNumber - 1)
.locator(`.c-fault-mgmt-item > .c-fault-mgmt__list-action-button >> nth=${rowNumber - 1}`)
.click();
}
export {
acknowledgeFault,
acknowledgeMultipleFaults,
changeViewTo,
clearSearch,
enterSearchTerm,
getFault,
getFaultByName,
getFaultName,
getFaultNamespace,
getFaultResultCount,
getFaultSeverity,
getFaultTriggerTime,
getHighestSeverity,
getLowestSeverity,
navigateToFaultItemInTree,
navigateToFaultManagementWithExample,
navigateToFaultManagementWithoutExample,
navigateToFaultManagementWithStaticExample,
openFaultRowMenu,
selectFaultItem,
shelveFault,
shelveMultipleFaults,
sortFaultsBy
};

View File

@@ -20,7 +20,6 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { createDomainObjectWithDefaults, createPlanFromJSON } from '../appActions.js';
import { expect } from '../pluginFixtures.js';
/**
@@ -143,18 +142,6 @@ export function getLatestEndTime(planJson) {
return Math.max(...activities.map((activity) => activity.end));
}
/**
*
* @param {object} planJson
* @returns {object}
*/
export function getFirstActivity(planJson) {
const groups = Object.keys(planJson);
const firstGroupKey = groups[0];
const firstGroupItems = planJson[firstGroupKey];
return firstGroupItems[0];
}
/**
* Uses the Open MCT API to set the status of a plan to 'draft'.
* @param {import('@playwright/test').Page} page
@@ -185,55 +172,3 @@ export async function addPlanGetInterceptor(page) {
});
});
}
/**
* Create a Plan from JSON and add it to a Timelist and Navigate to the Plan view
* @param {import('@playwright/test').Page} page
*/
export async function createTimelistWithPlanAndSetActivityInProgress(page, planJson) {
await page.goto('./', { waitUntil: 'domcontentloaded' });
const timelist = await createDomainObjectWithDefaults(page, {
name: 'Time List',
type: 'Time List'
});
await createPlanFromJSON(page, {
name: 'Test Plan',
json: planJson,
parent: timelist.uuid
});
// Ensure that all activities are shown in the expanded view
const groups = Object.keys(planJson);
const firstGroupKey = groups[0];
const firstGroupItems = planJson[firstGroupKey];
const firstActivityForPlan = firstGroupItems[0];
const lastActivity = firstGroupItems[firstGroupItems.length - 1];
const startBound = firstActivityForPlan.start;
const endBound = lastActivity.end;
// Switch to fixed time mode with all plan events within the bounds
await page.goto(
`${timelist.url}?tc.mode=fixed&tc.startBound=${startBound}&tc.endBound=${endBound}&tc.timeSystem=utc&view=timelist.view`
);
// Change the object to edit mode
await page.getByRole('button', { name: 'Edit Object' }).click();
// Find the display properties section in the inspector
await page.getByRole('tab', { name: 'View Properties' }).click();
// Switch to expanded view and save the setting
await page.getByLabel('Display Style').selectOption({ label: 'Expanded' });
// Click on the "Save" button
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
const anActivity = page.getByRole('row').nth(0);
// Set the activity to in progress
await anActivity.click();
await page.getByRole('tab', { name: 'Activity' }).click();
await page.getByLabel('Activity Status', { exact: true }).selectOption({ label: 'In progress' });
}

View File

@@ -10,7 +10,6 @@ const NUM_WORKERS = 2;
const config = {
retries: 2, //Retries 2 times for a total of 3 runs. When running sharded and with max-failures=5, this should ensure that flake is managed without failing the full suite
testDir: 'tests',
grepInvert: /@mobile/, //Ignore mobile tests
testIgnore: '**/*.perf.spec.js', //Ignore performance tests and define in playwright-perfromance.config.js
timeout: 60 * 1000,
webServer: {

View File

@@ -1,11 +1,13 @@
// playwright.config.js
// @ts-check
// eslint-disable-next-line no-unused-vars
import { devices } from '@playwright/test';
/** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = {
retries: 0,
testDir: 'tests',
testMatch: '**/*.e2e.spec.js', // only run e2e tests
testIgnore: '**/*.perf.spec.js',
timeout: 30 * 1000,
webServer: {
@@ -33,6 +35,7 @@ const config = {
},
{
name: 'MMOC',
testMatch: '**/*.e2e.spec.js', // only run e2e tests
grepInvert: /@snapshot/,
use: {
browserName: 'chromium',
@@ -44,6 +47,8 @@ const config = {
},
{
name: 'safari',
testMatch: '**/*.e2e.spec.js', // only run e2e tests
grep: /@ipad/, // only run ipad tests due to this bug https://github.com/microsoft/playwright/issues/8340
grepInvert: /@snapshot/,
use: {
browserName: 'webkit'
@@ -51,6 +56,7 @@ const config = {
},
{
name: 'firefox',
testMatch: '**/*.e2e.spec.js', // only run e2e tests
grepInvert: /@snapshot/,
use: {
browserName: 'firefox'
@@ -58,6 +64,7 @@ const config = {
},
{
name: 'canary',
testMatch: '**/*.e2e.spec.js', // only run e2e tests
grepInvert: /@snapshot/,
use: {
browserName: 'chromium',
@@ -66,11 +73,22 @@ const config = {
},
{
name: 'chrome-beta',
testMatch: '**/*.e2e.spec.js', // only run e2e tests
grepInvert: /@snapshot/,
use: {
browserName: 'chromium',
channel: 'chrome-beta'
}
},
{
name: 'ipad',
testMatch: '**/*.e2e.spec.js', // only run e2e tests
grep: /@ipad/,
grepInvert: /@snapshot/,
use: {
browserName: 'webkit',
...devices['iPad (gen 7) landscape'] // Complete List https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json
}
}
],
reporter: [

View File

@@ -1,69 +0,0 @@
// playwright.config.js
// @ts-check
import { devices } from '@playwright/test';
const MAX_FAILURES = 5;
import { fileURLToPath } from 'url';
/** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = {
retries: 1, //Retries 2 times for a total of 3 runs. When running sharded and with max-failures=5, this should ensure that flake is managed without failing the full suite
testDir: 'tests',
testIgnore: '**/*.perf.spec.js', //Ignore performance tests and define in playwright-perfromance.config.js
timeout: 30 * 1000,
webServer: {
command: 'npm run start:coverage',
url: 'http://localhost:8080/#',
timeout: 200 * 1000,
reuseExistingServer: true //This was originally disabled to prevent differences in local debugging vs. CI. However, it significantly speeds up local debugging.
},
maxFailures: MAX_FAILURES, //Limits failures to 5 to reduce CI Waste
workers: 1, //Limit to 1 due to resource constraints similar to https://github.com/percy/cli/discussions/1067
use: {
baseURL: 'http://localhost:8080/',
headless: true,
ignoreHTTPSErrors: true,
screenshot: 'only-on-failure',
trace: 'on-first-retry',
video: 'off'
},
projects: [
{
name: 'ipad',
grep: /@mobile/,
use: {
storageState: fileURLToPath(
new URL('./test-data/display_layout_with_child_layouts.json', import.meta.url)
),
browserName: 'webkit',
...devices['iPad (gen 7) landscape'] // Complete List https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json
}
},
{
name: 'iphone',
grep: /@mobile/,
use: {
storageState: fileURLToPath(
new URL('./test-data/display_layout_with_child_layouts.json', import.meta.url)
),
browserName: 'webkit',
...devices['iPhone 14 Pro'] // Complete List https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json
}
}
],
reporter: [
['list'],
[
'html',
{
open: 'never',
outputFolder: '../html-test-results' //Must be in different location due to https://github.com/microsoft/playwright/issues/12840
}
],
['junit', { outputFile: '../test-results/results.xml' }]
]
};
export default config;

View File

@@ -1,9 +1,6 @@
// playwright.config.js
// @ts-check
import { devices } from '@playwright/test';
import { fileURLToPath } from 'url';
/** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = {
retries: 0, //Retries are not needed with watch mode
@@ -31,28 +28,6 @@ const config = {
use: {
browserName: 'chromium'
}
},
{
name: 'ipad',
grep: /@mobile/,
use: {
storageState: fileURLToPath(
new URL('./test-data/display_layout_with_child_layouts.json', import.meta.url)
),
browserName: 'webkit',
...devices['iPad (gen 7) landscape'] // Complete List https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json
}
},
{
name: 'iphone',
grep: /@mobile/,
use: {
storageState: fileURLToPath(
new URL('./test-data/display_layout_with_child_layouts.json', import.meta.url)
),
browserName: 'webkit',
...devices['iPhone 14 Pro'] // Complete List https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json
}
}
],
reporter: [

File diff suppressed because one or more lines are too long

View File

@@ -174,6 +174,6 @@ test.describe('AppActions', () => {
type: 'Folder'
});
await openObjectTreeContextMenu(page, folder.url);
await expect(page.getByLabel(`${folder.name} Context Menu`)).toBeVisible();
await expect(page.getByLabel('Menu')).toBeVisible();
});
});

View File

@@ -33,18 +33,13 @@
import { fileURLToPath } from 'url';
import {
createDomainObjectWithDefaults,
createExampleTelemetryObject,
setIndependentTimeConductorBounds,
setTimeConductorBounds
} from '../../appActions.js';
import { createDomainObjectWithDefaults, createExampleTelemetryObject } from '../../appActions.js';
import { MISSION_TIME } from '../../constants.js';
import { expect, test } from '../../pluginFixtures.js';
const overlayPlotName = 'Overlay Plot with Telemetry Object';
test.describe('Generate Visual Test Data @localStorage @generatedata @clock', () => {
test.describe('Generate Visual Test Data @localStorage @generatedata', () => {
test.use({
clockOptions: {
now: MISSION_TIME,
@@ -94,53 +89,6 @@ test.describe('Generate Visual Test Data @localStorage @generatedata @clock', ()
});
});
test('Generate display layout with 1 child overlay plot', async ({ page, context }) => {
const parent = await createDomainObjectWithDefaults(page, {
type: 'Display Layout',
name: 'Parent Display Layout'
});
const overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot',
name: 'Child Overlay Plot 1',
parent: parent.uuid
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'Child SWG 1',
parent: overlayPlot.uuid
});
await page.goto(parent.url, { waitUntil: 'domcontentloaded' });
await setIndependentTimeConductorBounds(page, {
start: '2024-11-12 19:11:11.000Z',
end: '2024-11-12 20:11:11.000Z'
});
const NEW_GLOBAL_START_BOUNDS = '2024-11-11 19:11:11.000Z';
const NEW_GLOBAL_END_BOUNDS = '2024-11-11 20:11:11.000Z';
await setTimeConductorBounds(page, NEW_GLOBAL_START_BOUNDS, NEW_GLOBAL_END_BOUNDS);
// Verify that the global time conductor bounds have been updated
expect(
await page.getByLabel('Global Time Conductor').getByLabel('Start bounds').textContent()
).toEqual(NEW_GLOBAL_START_BOUNDS);
expect(
await page.getByLabel('Global Time Conductor').getByLabel('End bounds').textContent()
).toEqual(NEW_GLOBAL_END_BOUNDS);
//Save localStorage for future test execution
await context.storageState({
path: fileURLToPath(
new URL(
'../../../e2e/test-data/display_layout_with_child_overlay_plot.json',
import.meta.url
)
)
});
});
test('Generate flexible layout with 2 child display layouts', async ({ page, context }) => {
// Create Display Layout
const parent = await createDomainObjectWithDefaults(page, {

View File

@@ -22,13 +22,29 @@
import fs from 'fs';
import { createDomainObjectWithDefaults, createPlanFromJSON } from '../../../appActions.js';
import { getEarliestStartTime } from '../../../helper/planningUtils';
import { expect, test } from '../../../pluginFixtures.js';
const examplePlanSmall3 = JSON.parse(
fs.readFileSync(
new URL('../../../test-data/examplePlans/ExamplePlan_Small3.json', import.meta.url)
)
);
const examplePlanSmall1 = JSON.parse(
fs.readFileSync(
new URL('../../../test-data/examplePlans/ExamplePlan_Small1.json', import.meta.url)
)
);
// eslint-disable-next-line no-unused-vars
const START_TIME_COLUMN = 0;
// eslint-disable-next-line no-unused-vars
const END_TIME_COLUMN = 1;
const TIME_TO_FROM_COLUMN = 2;
// eslint-disable-next-line no-unused-vars
const ACTIVITY_COLUMN = 3;
const HEADER_ROW = 0;
const NUM_COLUMNS = 5;
test.describe('Time List', () => {
test("Create a Time List, add a single Plan to it, verify all the activities are displayed with no milliseconds and selecting an activity shows it's properties", async ({
page
@@ -145,7 +161,7 @@ test("View a timelist in expanded view, verify all the activities are displayed
await expect(eventCount).toEqual(firstGroupItems.length);
});
await test.step('Shows activity properties when a row is selected in the expanded view', async () => {
await test.step('Shows activity properties when a row is selected', async () => {
await page.getByRole('row').nth(2).click();
// Find the activity state section in the inspector
@@ -155,10 +171,167 @@ test("View a timelist in expanded view, verify all the activities are displayed
'Not started'
);
});
});
await test.step("Verify absence of progress indication for an activity that's not in progress", async () => {
// When an activity is not in progress, the progress pie is not visible
const hidden = await page.getByRole('row').locator('path').nth(1).isHidden();
await expect(hidden).toBe(true);
/**
* The regular expression used to parse the countdown string.
* Some examples of valid Countdown strings:
* ```
* '35D 02:03:04'
* '-1D 01:02:03'
* '01:02:03'
* '-05:06:07'
* ```
*/
const COUNTDOWN_REGEXP = /(-)?(\d+D\s)?(\d{2}):(\d{2}):(\d{2})/;
/**
* @typedef {Object} CountdownOrUpObject
* @property {string} sign - The sign of the countdown ('-' if the countdown is negative, '+' otherwise).
* @property {string} days - The number of days in the countdown (undefined if there are no days).
* @property {string} hours - The number of hours in the countdown.
* @property {string} minutes - The number of minutes in the countdown.
* @property {string} seconds - The number of seconds in the countdown.
* @property {string} toString - The countdown string.
*/
/**
* Object representing the indices of the capture groups in a countdown regex match.
*
* @typedef {{ SIGN: number, DAYS: number, HOURS: number, MINUTES: number, SECONDS: number, REGEXP: RegExp }}
* @property {number} SIGN - The index for the sign capture group (1 if a '-' sign is present, otherwise undefined).
* @property {number} DAYS - The index for the days capture group (2 for the number of days, otherwise undefined).
* @property {number} HOURS - The index for the hours capture group (3 for the hour part of the time).
* @property {number} MINUTES - The index for the minutes capture group (4 for the minute part of the time).
* @property {number} SECONDS - The index for the seconds capture group (5 for the second part of the time).
*/
const COUNTDOWN = Object.freeze({
SIGN: 1,
DAYS: 2,
HOURS: 3,
MINUTES: 4,
SECONDS: 5
});
test.describe('Time List with controlled clock', () => {
test.use({
clockOptions: {
now: getEarliestStartTime(examplePlanSmall3),
shouldAdvanceTime: true
}
});
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
});
test('Time List shows current events and counts down correctly in real-time mode', async ({
page
}) => {
await test.step('Create a Time List, add a Plan to it, and switch to real-time mode', async () => {
// Create Time List
const timelist = await createDomainObjectWithDefaults(page, {
type: 'Time List'
});
// Create a Plan with events that count down and up.
// Add it as a child to the Time List.
await createPlanFromJSON(page, {
json: examplePlanSmall3,
parent: timelist.uuid
});
// Navigate to the Time List in real-time mode
await page.goto(
`${timelist.url}?tc.mode=local&tc.startDelta=900000&tc.endDelta=1800000&tc.timeSystem=utc&view=grid`
);
});
const countUpCells = [
getCellByIndex(page, 1, TIME_TO_FROM_COLUMN),
getCellByIndex(page, 2, TIME_TO_FROM_COLUMN)
];
const countdownCells = [
getCellByIndex(page, 3, TIME_TO_FROM_COLUMN),
getCellByIndex(page, 4, TIME_TO_FROM_COLUMN)
];
// Verify that the countdown cells are counting down
for (let i = 0; i < countdownCells.length; i++) {
await test.step(`Countdown cell ${i + 1} counts down`, async () => {
const countdownCell = countdownCells[i];
// Get the initial countdown timestamp object
const beforeCountdown = await getAndAssertCountdownOrUpObject(page, i + 3);
// should not have a '-' sign
await expect(countdownCell).not.toHaveText('-');
// Wait until it changes
await expect(countdownCell).not.toHaveText(beforeCountdown.toString());
// Get the new countdown timestamp object
const afterCountdown = await getAndAssertCountdownOrUpObject(page, i + 3);
// Verify that the new countdown timestamp object is less than the old one
expect(Number(afterCountdown.seconds)).toBeLessThan(Number(beforeCountdown.seconds));
});
}
// Verify that the count-up cells are counting up
for (let i = 0; i < countUpCells.length; i++) {
await test.step(`Count-up cell ${i + 1} counts up`, async () => {
const countUpCell = countUpCells[i];
// Get the initial count-up timestamp object
const beforeCountUp = await getAndAssertCountdownOrUpObject(page, i + 1);
// should not have a '+' sign
await expect(countUpCell).not.toHaveText('+');
// Wait until it changes
await expect(countUpCell).not.toHaveText(beforeCountUp.toString());
// Get the new count-up timestamp object
const afterCountUp = await getAndAssertCountdownOrUpObject(page, i + 1);
// Verify that the new count-up timestamp object is greater than the old one
expect(Number(afterCountUp.seconds)).toBeGreaterThan(Number(beforeCountUp.seconds));
});
}
});
});
/**
* Get the cell at the given row and column indices.
* @param {import('@playwright/test').Page} page
* @param {number} rowIndex
* @param {number} columnIndex
* @returns {import('@playwright/test').Locator} cell
*/
function getCellByIndex(page, rowIndex, columnIndex) {
return page.getByRole('cell').nth(rowIndex * NUM_COLUMNS + columnIndex);
}
/**
* Return the innerText of the cell at the given row and column indices.
* @param {import('@playwright/test').Page} page
* @param {number} rowIndex
* @param {number} columnIndex
* @returns {Promise<string>} text
*/
async function getCellTextByIndex(page, rowIndex, columnIndex) {
const text = await getCellByIndex(page, rowIndex, columnIndex).innerText();
return text;
}
/**
* Get the text from the countdown (or countup) cell in the given row, assert that it matches the countdown/countup
* regex, and return an object representing the countdown.
* @param {import('@playwright/test').Page} page
* @param {number} rowIndex the row index
* @returns {Promise<CountdownOrUpObject>} The countdown (or countup) object
*/
async function getAndAssertCountdownOrUpObject(page, rowIndex) {
const timeToFrom = await getCellTextByIndex(page, HEADER_ROW + rowIndex, TIME_TO_FROM_COLUMN);
expect(timeToFrom).toMatch(COUNTDOWN_REGEXP);
const match = timeToFrom.match(COUNTDOWN_REGEXP);
return {
sign: match[COUNTDOWN.SIGN],
days: match[COUNTDOWN.DAYS],
hours: match[COUNTDOWN.HOURS],
minutes: match[COUNTDOWN.MINUTES],
seconds: match[COUNTDOWN.SECONDS],
toString: () => timeToFrom
};
}

View File

@@ -1,290 +0,0 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/*
Collection of Time List tests set to run with browser clock manipulate made possible with the
clockOptions plugin fixture.
*/
import fs from 'fs';
import { createDomainObjectWithDefaults, createPlanFromJSON } from '../../../appActions.js';
import {
createTimelistWithPlanAndSetActivityInProgress,
getEarliestStartTime,
getFirstActivity
} from '../../../helper/planningUtils';
import { expect, test } from '../../../pluginFixtures.js';
const examplePlanSmall3 = JSON.parse(
fs.readFileSync(
new URL('../../../test-data/examplePlans/ExamplePlan_Small3.json', import.meta.url)
)
);
const examplePlanSmall1 = JSON.parse(
fs.readFileSync(
new URL('../../../test-data/examplePlans/ExamplePlan_Small1.json', import.meta.url)
)
);
const TIME_TO_FROM_COLUMN = 2;
const HEADER_ROW = 0;
const NUM_COLUMNS = 5;
const FULL_CIRCLE_PATH =
'M3.061616997868383e-15,-50A50,50,0,1,1,-3.061616997868383e-15,50A50,50,0,1,1,3.061616997868383e-15,-50Z';
/**
* The regular expression used to parse the countdown string.
* Some examples of valid Countdown strings:
* ```
* '35D 02:03:04'
* '-1D 01:02:03'
* '01:02:03'
* '-05:06:07'
* ```
*/
const COUNTDOWN_REGEXP = /(-)?(\d+D\s)?(\d{2}):(\d{2}):(\d{2})/;
/**
* @typedef {Object} CountdownOrUpObject
* @property {string} sign - The sign of the countdown ('-' if the countdown is negative, '+' otherwise).
* @property {string} days - The number of days in the countdown (undefined if there are no days).
* @property {string} hours - The number of hours in the countdown.
* @property {string} minutes - The number of minutes in the countdown.
* @property {string} seconds - The number of seconds in the countdown.
* @property {string} toString - The countdown string.
*/
/**
* Object representing the indices of the capture groups in a countdown regex match.
*
* @typedef {{ SIGN: number, DAYS: number, HOURS: number, MINUTES: number, SECONDS: number, REGEXP: RegExp }}
* @property {number} SIGN - The index for the sign capture group (1 if a '-' sign is present, otherwise undefined).
* @property {number} DAYS - The index for the days capture group (2 for the number of days, otherwise undefined).
* @property {number} HOURS - The index for the hours capture group (3 for the hour part of the time).
* @property {number} MINUTES - The index for the minutes capture group (4 for the minute part of the time).
* @property {number} SECONDS - The index for the seconds capture group (5 for the second part of the time).
*/
const COUNTDOWN = Object.freeze({
SIGN: 1,
DAYS: 2,
HOURS: 3,
MINUTES: 4,
SECONDS: 5
});
test.describe('Time List with controlled clock @clock', () => {
test.use({
clockOptions: {
now: getEarliestStartTime(examplePlanSmall3),
shouldAdvanceTime: true
}
});
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
// Create Time List
const timelist = await createDomainObjectWithDefaults(page, {
type: 'Time List'
});
// Create a Plan with events that count down and up.
// Add it as a child to the Time List.
await createPlanFromJSON(page, {
json: examplePlanSmall3,
parent: timelist.uuid
});
// Navigate to the Time List in real-time mode
await page.goto(
`${timelist.url}?tc.mode=local&tc.startDelta=900000&tc.endDelta=1800000&tc.timeSystem=utc&view=grid`
);
//Expand the viewport to show the entire time list
await page.getByLabel('Collapse Inspect Pane').click();
await page.getByLabel('Collapse Browse Pane').click();
});
test('Time List shows current events and counts down correctly in real-time mode', async ({
page
}) => {
const countUpCells = [
getTimeListCellByIndex(page, 1, TIME_TO_FROM_COLUMN),
getTimeListCellByIndex(page, 2, TIME_TO_FROM_COLUMN)
];
const countdownCells = [
getTimeListCellByIndex(page, 3, TIME_TO_FROM_COLUMN),
getTimeListCellByIndex(page, 4, TIME_TO_FROM_COLUMN)
];
// Verify that the countdown cells are counting down
for (let i = 0; i < countdownCells.length; i++) {
await test.step(`Countdown cell ${i + 1} counts down`, async () => {
const countdownCell = countdownCells[i];
// Get the initial countdown timestamp object
const beforeCountdown = await getAndAssertCountdownOrUpObject(page, i + 3);
// should not have a '-' sign
await expect(countdownCell).not.toHaveText('-');
// Wait until it changes
await expect(countdownCell).not.toHaveText(beforeCountdown.toString());
// Get the new countdown timestamp object
const afterCountdown = await getAndAssertCountdownOrUpObject(page, i + 3);
// Verify that the new countdown timestamp object is less than the old one
expect(Number(afterCountdown.seconds)).toBeLessThan(Number(beforeCountdown.seconds));
});
}
// Verify that the count-up cells are counting up
for (let i = 0; i < countUpCells.length; i++) {
await test.step(`Count-up cell ${i + 1} counts up`, async () => {
const countUpCell = countUpCells[i];
// Get the initial count-up timestamp object
const beforeCountUp = await getAndAssertCountdownOrUpObject(page, i + 1);
// should not have a '+' sign
await expect(countUpCell).not.toHaveText('+');
// Wait until it changes
await expect(countUpCell).not.toHaveText(beforeCountUp.toString());
// Get the new count-up timestamp object
const afterCountUp = await getAndAssertCountdownOrUpObject(page, i + 1);
// Verify that the new count-up timestamp object is greater than the old one
expect(Number(afterCountUp.seconds)).toBeGreaterThan(Number(beforeCountUp.seconds));
});
}
});
});
test.describe('Activity progress when activity is in the future @clock', () => {
const firstActivity = getFirstActivity(examplePlanSmall1);
test.use({
clockOptions: {
now: firstActivity.start - 1,
shouldAdvanceTime: true
}
});
test.beforeEach(async ({ page }) => {
await createTimelistWithPlanAndSetActivityInProgress(page, examplePlanSmall1);
});
test('progress pie is empty', async ({ page }) => {
const anActivity = page.getByRole('row').nth(0);
// Progress pie shows no progress when now is less than the start time
await expect(anActivity.getByLabel('Activity in progress').locator('path')).not.toHaveAttribute(
'd'
);
});
});
test.describe('Activity progress when now is between start and end of the activity @clock', () => {
const firstActivity = getFirstActivity(examplePlanSmall1);
test.beforeEach(async ({ page }) => {
await createTimelistWithPlanAndSetActivityInProgress(page, examplePlanSmall1);
});
test.use({
clockOptions: {
now: firstActivity.start + 50000,
shouldAdvanceTime: true
}
});
test('progress pie is partially filled', async ({ page }) => {
const anActivity = page.getByRole('row').nth(0);
const pathElement = anActivity.getByLabel('Activity in progress').locator('path');
// Progress pie shows progress when now is greater than the start time
await expect(pathElement).toHaveAttribute('d');
});
});
test.describe('Activity progress when now is after end of the activity @clock', () => {
const firstActivity = getFirstActivity(examplePlanSmall1);
test.use({
clockOptions: {
now: firstActivity.end + 10000,
shouldAdvanceTime: true
}
});
test.beforeEach(async ({ page }) => {
await createTimelistWithPlanAndSetActivityInProgress(page, examplePlanSmall1);
});
test('progress pie is full', async ({ page }) => {
const anActivity = page.getByRole('row').nth(0);
// Progress pie is completely full and doesn't update if now is greater than the end time
await expect(anActivity.getByLabel('Activity in progress').locator('path')).toHaveAttribute(
'd',
FULL_CIRCLE_PATH
);
});
});
/**
* Get the cell at the given row and column indices.
* @param {import('@playwright/test').Page} page
* @param {number} rowIndex
* @param {number} columnIndex
* @returns {import('@playwright/test').Locator} cell
*/
function getTimeListCellByIndex(page, rowIndex, columnIndex) {
return page.getByRole('cell').nth(rowIndex * NUM_COLUMNS + columnIndex);
}
/**
* Return the innerText of the cell at the given row and column indices.
* @param {import('@playwright/test').Page} page
* @param {number} rowIndex
* @param {number} columnIndex
* @returns {Promise<string>} text
*/
async function getTimeListCellTextByIndex(page, rowIndex, columnIndex) {
const text = await getTimeListCellByIndex(page, rowIndex, columnIndex).innerText();
return text;
}
/**
* Get the text from the countdown (or countup) cell in the given row, assert that it matches the countdown/countup
* regex, and return an object representing the countdown.
* @param {import('@playwright/test').Page} page
* @param {number} rowIndex the row index
* @returns {Promise<CountdownOrUpObject>} The countdown (or countup) object
*/
async function getAndAssertCountdownOrUpObject(page, rowIndex) {
const timeToFrom = await getTimeListCellTextByIndex(
page,
HEADER_ROW + rowIndex,
TIME_TO_FROM_COLUMN
);
expect(timeToFrom).toMatch(COUNTDOWN_REGEXP);
const match = timeToFrom.match(COUNTDOWN_REGEXP);
return {
sign: match[COUNTDOWN.SIGN],
days: match[COUNTDOWN.DAYS],
hours: match[COUNTDOWN.HOURS],
minutes: match[COUNTDOWN.MINUTES],
seconds: match[COUNTDOWN.SECONDS],
toString: () => timeToFrom
};
}

View File

@@ -131,10 +131,7 @@ test.describe('Time Strip', () => {
const startBoundString = new Date(startBound).toISOString().replace('T', ' ');
const endBoundString = new Date(endBound).toISOString().replace('T', ' ');
await setIndependentTimeConductorBounds(page, {
start: startBoundString,
end: endBoundString
});
await setIndependentTimeConductorBounds(page, startBoundString, endBoundString);
expect(await activityBounds.count()).toEqual(1);
});
@@ -163,10 +160,7 @@ test.describe('Time Strip', () => {
const startBoundString = new Date(startBound).toISOString().replace('T', ' ');
const endBoundString = new Date(endBound).toISOString().replace('T', ' ');
await setIndependentTimeConductorBounds(page, {
start: startBoundString,
end: endBoundString
});
await setIndependentTimeConductorBounds(page, startBoundString, endBoundString);
// Verify that two events are displayed
expect(await activityBounds.count()).toEqual(2);

View File

@@ -286,29 +286,19 @@ test.describe('Basic Condition Set Use', () => {
await page.locator('button[title="Save"]').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await page.getByLabel('Open the View Switcher Menu').click();
await page.click('button[title="Change the current view"]');
await expect(page.getByRole('menuitem', { name: /Lad Table/ })).toBeHidden();
await expect(page.getByRole('menuitem', { name: /Conditions View/ })).toBeVisible();
await expect(page.getByRole('menuitem', { name: /Plot/ })).toBeVisible();
await expect(page.getByRole('menuitem', { name: /Telemetry Table/ })).toBeVisible();
await page.getByLabel('Plot').click();
await expect(
page.getByLabel('Plot Legend Collapsed').getByText('Test Condition Set')
).toBeVisible();
await page.getByLabel('Open the View Switcher Menu').click();
await page.getByLabel('Telemetry Table').click();
await expect(page.getByRole('searchbox', { name: 'output filter input' })).toBeVisible();
await page.getByLabel('Open the View Switcher Menu').click();
await page.getByLabel('Conditions View').click();
await expect(page.getByText('Current Output')).toBeVisible();
});
test('ConditionSet has correct outputs when telemetry is and is not available', async ({
page
}) => {
const exampleTelemetry = await createExampleTelemetryObject(page);
await page.getByLabel('Show selected item in tree').click();
await page.getByTitle('Show selected item in tree').click();
await page.goto(conditionSet.url);
// Change the object to edit mode
await page.getByLabel('Edit Object').click();
@@ -388,90 +378,4 @@ test.describe('Basic Condition Set Use', () => {
await page.goto(conditionSet.url);
await expect(outputValue).toHaveText('---');
});
test('ConditionSet has correct outputs when test data is enabled', 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 two conditions
await page.locator('#addCondition').click();
await page.locator('#addCondition').click();
await page.locator('#conditionCollection').getByRole('textbox').nth(0).fill('First Condition');
await page.locator('#conditionCollection').getByRole('textbox').nth(1).fill('Second Condition');
// 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);
// Modify First Criterion
const firstCriterionTelemetry = page.locator(
'[aria-label="Criterion Telemetry Selection"] >> nth=0'
);
firstCriterionTelemetry.selectOption({ label: exampleTelemetry.name });
const firstCriterionMetadata = page.locator(
'[aria-label="Criterion Metadata Selection"] >> nth=0'
);
firstCriterionMetadata.selectOption({ label: 'Sine' });
const firstCriterionComparison = page.locator(
'[aria-label="Criterion Comparison Selection"] >> nth=0'
);
firstCriterionComparison.selectOption({ label: 'is greater than or equal to' });
const firstCriterionInput = page.locator('[aria-label="Criterion Input"] >> nth=0');
await firstCriterionInput.fill('0');
// Modify Second Criterion
const secondCriterionTelemetry = page.locator(
'[aria-label="Criterion Telemetry Selection"] >> nth=1'
);
await secondCriterionTelemetry.selectOption({ label: exampleTelemetry.name });
const secondCriterionMetadata = page.locator(
'[aria-label="Criterion Metadata Selection"] >> nth=1'
);
await secondCriterionMetadata.selectOption({ label: 'Sine' });
const secondCriterionComparison = page.locator(
'[aria-label="Criterion Comparison Selection"] >> nth=1'
);
await secondCriterionComparison.selectOption({ label: 'is less than' });
const secondCriterionInput = page.locator('[aria-label="Criterion Input"] >> nth=1');
await secondCriterionInput.fill('0');
// Enable test data
await page.getByLabel('Apply Test Data').nth(1).click();
const testDataTelemetry = page.locator('[aria-label="Test Data Telemetry Selection"] >> nth=0');
await testDataTelemetry.selectOption({ label: exampleTelemetry.name });
const testDataMetadata = page.locator('[aria-label="Test Data Metadata Selection"] >> nth=0');
await testDataMetadata.selectOption({ label: 'Sine' });
const testInput = page.locator('[aria-label="Test Data Input"] >> nth=0');
await testInput.fill('0');
// Validate that the condition set is evaluating and outputting
// the correct value when the underlying telemetry subscription is active.
let outputValue = page.locator('[aria-label="Current Output Value"]');
await expect(outputValue).toHaveText('false');
await page.goto(exampleTelemetry.url);
});
test.fixme('Ensure condition sets work with telemetry like operator status', ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7484'
});
});
});

View File

@@ -23,7 +23,6 @@ import { fileURLToPath } from 'url';
import {
createDomainObjectWithDefaults,
navigateToObjectWithFixedTimeBounds,
setFixedTimeMode,
setIndependentTimeConductorBounds,
setRealTimeMode,
@@ -31,120 +30,12 @@ import {
} from '../../../../appActions.js';
import { expect, test } from '../../../../pluginFixtures.js';
const CHILD_LAYOUT_STORAGE_STATE_PATH = fileURLToPath(
const LOCALSTORAGE_PATH = fileURLToPath(
new URL('../../../../test-data/display_layout_with_child_layouts.json', import.meta.url)
);
const CHILD_PLOT_STORAGE_STATE_PATH = fileURLToPath(
new URL('../../../../test-data/display_layout_with_child_overlay_plot.json', import.meta.url)
);
const TINY_IMAGE_BASE64 =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII';
test.describe('Display Layout Sub-object Actions @localStorage', () => {
const INIT_ITC_START_BOUNDS = '2024-11-12 19:11:11.000Z';
const INIT_ITC_END_BOUNDS = '2024-11-12 20:11:11.000Z';
const NEW_GLOBAL_START_BOUNDS = '2024-11-11 19:11:11.000Z';
const NEW_GLOBAL_END_BOUNDS = '2024-11-11 20:11:11.000Z';
test.use({
storageState: CHILD_PLOT_STORAGE_STATE_PATH
});
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
await page.getByLabel('Expand My Items folder').click();
const waitForMyItemsNavigation = page.waitForURL(`**/mine/?*`);
await page
.getByLabel('Main Tree')
.getByLabel('Navigate to Parent Display Layout layout Object')
.click();
// Wait for the URL to change to the display layout
await waitForMyItemsNavigation;
});
test('Open in New Tab action preserves time bounds @2p', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7524'
});
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/6982'
});
const TEST_FIXED_START_TIME = 1731352271000; // 2024-11-11 19:11:11.000Z
const TEST_FIXED_END_TIME = TEST_FIXED_START_TIME + 3600000; // 2024-11-11 20:11:11.000Z
// Verify the ITC has the expected initial bounds
expect(
await page
.getByLabel('Child Overlay Plot 1 Frame Controls')
.getByLabel('Start bounds')
.textContent()
).toEqual(INIT_ITC_START_BOUNDS);
expect(
await page
.getByLabel('Child Overlay Plot 1 Frame Controls')
.getByLabel('End bounds')
.textContent()
).toEqual(INIT_ITC_END_BOUNDS);
// Update the global fixed bounds to 2024-11-11 19:11:11.000Z / 2024-11-11 20:11:11.000Z
const url = page.url().split('?')[0];
await navigateToObjectWithFixedTimeBounds(
page,
url,
TEST_FIXED_START_TIME,
TEST_FIXED_END_TIME
);
// ITC bounds should still match the initial ITC bounds
expect(
await page
.getByLabel('Child Overlay Plot 1 Frame Controls')
.getByLabel('Start bounds')
.textContent()
).toEqual(INIT_ITC_START_BOUNDS);
expect(
await page
.getByLabel('Child Overlay Plot 1 Frame Controls')
.getByLabel('End bounds')
.textContent()
).toEqual(INIT_ITC_END_BOUNDS);
// Open the Child Overlay Plot 1 in a new tab
await page.getByLabel('View menu items').click();
const pagePromise = page.context().waitForEvent('page');
await page.getByLabel('Open In New Tab').click();
const newPage = await pagePromise;
await newPage.waitForLoadState('domcontentloaded');
// Verify that the global time conductor bounds in the new page match the updated global bounds
expect(
await newPage.getByLabel('Global Time Conductor').getByLabel('Start bounds').textContent()
).toEqual(NEW_GLOBAL_START_BOUNDS);
expect(
await newPage.getByLabel('Global Time Conductor').getByLabel('End bounds').textContent()
).toEqual(NEW_GLOBAL_END_BOUNDS);
// Verify that the ITC is enabled in the new page
await expect(newPage.getByLabel('Disable Independent Time Conductor')).toBeVisible();
// Verify that the ITC bounds in the new page match the original ITC bounds
expect(
await newPage
.getByLabel('Independent Time Conductor Panel')
.getByLabel('Start bounds')
.textContent()
).toEqual(INIT_ITC_START_BOUNDS);
expect(
await newPage
.getByLabel('Independent Time Conductor Panel')
.getByLabel('End bounds')
.textContent()
).toEqual(INIT_ITC_END_BOUNDS);
});
});
test.describe('Display Layout Toolbar Actions @localStorage', () => {
const PARENT_DISPLAY_LAYOUT_NAME = 'Parent Display Layout';
const CHILD_DISPLAY_LAYOUT_NAME1 = 'Child Layout 1';
@@ -159,7 +50,7 @@ test.describe('Display Layout Toolbar Actions @localStorage', () => {
await page.getByLabel('Edit Object').click();
});
test.use({
storageState: CHILD_LAYOUT_STORAGE_STATE_PATH
storageState: LOCALSTORAGE_PATH
});
test('can add/remove Text element to a single layout', async ({ page }) => {
@@ -272,7 +163,7 @@ test.describe('Display Layout', () => {
expect(trimmedDisplayValue).toBe(formattedTelemetryValue);
// ensure we can right click on the alpha-numeric widget and view historical data
await page.getByLabel(/Alpha-numeric telemetry value of.*/).click({
await page.getByLabel('Sine', { exact: true }).click({
button: 'right'
});
await page.getByLabel('View Historical Data').click();
@@ -445,7 +336,7 @@ test.describe('Display Layout', () => {
const startDate = '2021-12-30 01:01:00.000Z';
const endDate = '2021-12-30 01:11:00.000Z';
await setIndependentTimeConductorBounds(page, { start: startDate, end: endDate });
await setIndependentTimeConductorBounds(page, startDate, endDate);
// check image date
await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible();

View File

@@ -20,46 +20,25 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import {
acknowledgeFault,
acknowledgeMultipleFaults,
changeViewTo,
clearSearch,
enterSearchTerm,
getFault,
getFaultByName,
getFaultName,
getFaultNamespace,
getFaultResultCount,
getFaultSeverity,
getFaultTriggerTime,
getHighestSeverity,
getLowestSeverity,
navigateToFaultManagementWithExample,
navigateToFaultManagementWithoutExample,
selectFaultItem,
shelveFault,
shelveMultipleFaults,
sortFaultsBy
} from '../../../../helper/faultUtils.js';
import * as utils from '../../../../helper/faultUtils.js';
import { expect, test } from '../../../../pluginFixtures.js';
test.describe('The Fault Management Plugin using example faults', () => {
test.beforeEach(async ({ page }) => {
await navigateToFaultManagementWithExample(page);
await utils.navigateToFaultManagementWithExample(page);
});
test('Shows a criticality icon for every fault', async ({ page }) => {
test('Shows a criticality icon for every fault @unstable', async ({ page }) => {
const faultCount = await page.locator('c-fault-mgmt__list').count();
const criticalityIconCount = await page.locator('c-fault-mgmt__list-severity').count();
expect(faultCount).toEqual(criticalityIconCount);
expect.soft(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 it\'s information shows in the inspector @unstable', async ({
page
}) => {
await selectFaultItem(page, 1);
await utils.selectFaultItem(page, 1);
await page.getByRole('tab', { name: 'Config' }).click();
const selectedFaultName = await page
@@ -69,22 +48,22 @@ test.describe('The Fault Management Plugin using example faults', () => {
.locator(`.c-inspector__properties >> :text("${selectedFaultName}")`)
.count();
await expect(
page.locator('.c-faults-list-view-item-body > .c-fault-mgmt__list').first()
).toHaveClass(/is-selected/);
expect(inspectorFaultNameCount).toEqual(1);
await expect
.soft(page.locator('.c-faults-list-view-item-body > .c-fault-mgmt__list').first())
.toHaveClass(/is-selected/);
expect.soft(inspectorFaultNameCount).toEqual(1);
});
test('When selecting multiple faults, no specific fault information is shown in the inspector', async ({
test('When selecting multiple faults, no specific fault information is shown in the inspector @unstable', async ({
page
}) => {
await selectFaultItem(page, 1);
await selectFaultItem(page, 2);
await utils.selectFaultItem(page, 1);
await utils.selectFaultItem(page, 2);
const selectedRows = page.locator(
'.c-fault-mgmt__list.is-selected .c-fault-mgmt__list-faultname'
);
expect(await selectedRows.count()).toEqual(2);
expect.soft(await selectedRows.count()).toEqual(2);
await page.getByRole('tab', { name: 'Config' }).click();
const firstSelectedFaultName = await selectedRows.nth(0).textContent();
@@ -96,180 +75,180 @@ test.describe('The Fault Management Plugin using example faults', () => {
.locator(`.c-inspector__properties >> :text("${secondSelectedFaultName}")`)
.count();
expect(firstNameInInspectorCount).toEqual(0);
expect(secondNameInInspectorCount).toEqual(0);
expect.soft(firstNameInInspectorCount).toEqual(0);
expect.soft(secondNameInInspectorCount).toEqual(0);
});
test('Allows you to shelve a fault', async ({ page }) => {
const shelvedFaultName = await getFaultName(page, 2);
const beforeShelvedFault = getFaultByName(page, shelvedFaultName);
test('Allows you to shelve a fault @unstable', async ({ page }) => {
const shelvedFaultName = await utils.getFaultName(page, 2);
const beforeShelvedFault = utils.getFaultByName(page, shelvedFaultName);
await expect(beforeShelvedFault).toHaveCount(1);
expect.soft(await beforeShelvedFault.count()).toBe(1);
await shelveFault(page, 2);
await utils.shelveFault(page, 2);
// check it is removed from standard view
const afterShelvedFault = getFaultByName(page, shelvedFaultName);
expect(await afterShelvedFault.count()).toBe(0);
const afterShelvedFault = utils.getFaultByName(page, shelvedFaultName);
expect.soft(await afterShelvedFault.count()).toBe(0);
await changeViewTo(page, 'shelved');
await utils.changeViewTo(page, 'shelved');
const shelvedViewFault = getFaultByName(page, shelvedFaultName);
const shelvedViewFault = utils.getFaultByName(page, shelvedFaultName);
expect(await shelvedViewFault.count()).toBe(1);
expect.soft(await shelvedViewFault.count()).toBe(1);
});
test('Allows you to acknowledge a fault', async ({ page }) => {
const acknowledgedFaultName = await getFaultName(page, 3);
test('Allows you to acknowledge a fault @unstable', async ({ page }) => {
const acknowledgedFaultName = await utils.getFaultName(page, 3);
await acknowledgeFault(page, 3);
await utils.acknowledgeFault(page, 3);
const fault = getFault(page, 3);
await expect(fault).toHaveClass(/is-acknowledged/);
const fault = utils.getFault(page, 3);
await expect.soft(fault).toHaveClass(/is-acknowledged/);
await changeViewTo(page, 'acknowledged');
await utils.changeViewTo(page, 'acknowledged');
const acknowledgedViewFaultName = await getFaultName(page, 1);
expect(acknowledgedFaultName).toEqual(acknowledgedViewFaultName);
const acknowledgedViewFaultName = await utils.getFaultName(page, 1);
expect.soft(acknowledgedFaultName).toEqual(acknowledgedViewFaultName);
});
test('Allows you to shelve multiple faults', async ({ page }) => {
const shelvedFaultNameOne = await getFaultName(page, 1);
const shelvedFaultNameFour = await getFaultName(page, 4);
test('Allows you to shelve multiple faults @unstable', async ({ page }) => {
const shelvedFaultNameOne = await utils.getFaultName(page, 1);
const shelvedFaultNameFour = await utils.getFaultName(page, 4);
const beforeShelvedFaultOne = getFaultByName(page, shelvedFaultNameOne);
const beforeShelvedFaultFour = getFaultByName(page, shelvedFaultNameFour);
const beforeShelvedFaultOne = utils.getFaultByName(page, shelvedFaultNameOne);
const beforeShelvedFaultFour = utils.getFaultByName(page, shelvedFaultNameFour);
await expect(beforeShelvedFaultOne).toHaveCount(1);
await expect(beforeShelvedFaultFour).toHaveCount(1);
expect.soft(await beforeShelvedFaultOne.count()).toBe(1);
expect.soft(await beforeShelvedFaultFour.count()).toBe(1);
await shelveMultipleFaults(page, 1, 4);
await utils.shelveMultipleFaults(page, 1, 4);
// check it is removed from standard view
const afterShelvedFaultOne = getFaultByName(page, shelvedFaultNameOne);
const afterShelvedFaultFour = getFaultByName(page, shelvedFaultNameFour);
await expect(afterShelvedFaultOne).toHaveCount(0);
await expect(afterShelvedFaultFour).toHaveCount(0);
const afterShelvedFaultOne = utils.getFaultByName(page, shelvedFaultNameOne);
const afterShelvedFaultFour = utils.getFaultByName(page, shelvedFaultNameFour);
expect.soft(await afterShelvedFaultOne.count()).toBe(0);
expect.soft(await afterShelvedFaultFour.count()).toBe(0);
await changeViewTo(page, 'shelved');
await utils.changeViewTo(page, 'shelved');
const shelvedViewFaultOne = getFaultByName(page, shelvedFaultNameOne);
const shelvedViewFaultFour = getFaultByName(page, shelvedFaultNameFour);
const shelvedViewFaultOne = utils.getFaultByName(page, shelvedFaultNameOne);
const shelvedViewFaultFour = utils.getFaultByName(page, shelvedFaultNameFour);
await expect(shelvedViewFaultOne).toHaveCount(1);
await expect(shelvedViewFaultFour).toHaveCount(1);
expect.soft(await shelvedViewFaultOne.count()).toBe(1);
expect.soft(await shelvedViewFaultFour.count()).toBe(1);
});
test('Allows you to acknowledge multiple faults', async ({ page }) => {
const acknowledgedFaultNameTwo = await getFaultName(page, 2);
const acknowledgedFaultNameFive = await getFaultName(page, 5);
test('Allows you to acknowledge multiple faults @unstable', async ({ page }) => {
const acknowledgedFaultNameTwo = await utils.getFaultName(page, 2);
const acknowledgedFaultNameFive = await utils.getFaultName(page, 5);
await acknowledgeMultipleFaults(page, 2, 5);
await utils.acknowledgeMultipleFaults(page, 2, 5);
const faultTwo = getFault(page, 2);
const faultFive = getFault(page, 5);
const faultTwo = utils.getFault(page, 2);
const faultFive = utils.getFault(page, 5);
// check they have been acknowledged
await expect(faultTwo).toHaveClass(/is-acknowledged/);
await expect(faultFive).toHaveClass(/is-acknowledged/);
await expect.soft(faultTwo).toHaveClass(/is-acknowledged/);
await expect.soft(faultFive).toHaveClass(/is-acknowledged/);
await changeViewTo(page, 'acknowledged');
await utils.changeViewTo(page, 'acknowledged');
const acknowledgedViewFaultTwo = getFaultByName(page, acknowledgedFaultNameTwo);
const acknowledgedViewFaultFive = getFaultByName(page, acknowledgedFaultNameFive);
const acknowledgedViewFaultTwo = utils.getFaultByName(page, acknowledgedFaultNameTwo);
const acknowledgedViewFaultFive = utils.getFaultByName(page, acknowledgedFaultNameFive);
await expect(acknowledgedViewFaultTwo).toHaveCount(1);
await expect(acknowledgedViewFaultFive).toHaveCount(1);
expect.soft(await acknowledgedViewFaultTwo.count()).toBe(1);
expect.soft(await acknowledgedViewFaultFive.count()).toBe(1);
});
test('Allows you to search faults', async ({ page }) => {
const faultThreeNamespace = await getFaultNamespace(page, 3);
const faultTwoName = await getFaultName(page, 2);
const faultFiveTriggerTime = await getFaultTriggerTime(page, 5);
test('Allows you to search faults @unstable', async ({ page }) => {
const faultThreeNamespace = await utils.getFaultNamespace(page, 3);
const faultTwoName = await utils.getFaultName(page, 2);
const faultFiveTriggerTime = await utils.getFaultTriggerTime(page, 5);
// should be all faults (5)
let faultResultCount = await getFaultResultCount(page);
expect(faultResultCount).toEqual(5);
let faultResultCount = await utils.getFaultResultCount(page);
expect.soft(faultResultCount).toEqual(5);
// search namespace
await enterSearchTerm(page, faultThreeNamespace);
await utils.enterSearchTerm(page, faultThreeNamespace);
faultResultCount = await getFaultResultCount(page);
expect(faultResultCount).toEqual(1);
expect(await getFaultNamespace(page, 1)).toEqual(faultThreeNamespace);
faultResultCount = await utils.getFaultResultCount(page);
expect.soft(faultResultCount).toEqual(1);
expect.soft(await utils.getFaultNamespace(page, 1)).toEqual(faultThreeNamespace);
// all faults
await clearSearch(page);
faultResultCount = await getFaultResultCount(page);
expect(faultResultCount).toEqual(5);
await utils.clearSearch(page);
faultResultCount = await utils.getFaultResultCount(page);
expect.soft(faultResultCount).toEqual(5);
// search name
await enterSearchTerm(page, faultTwoName);
await utils.enterSearchTerm(page, faultTwoName);
faultResultCount = await getFaultResultCount(page);
expect(faultResultCount).toEqual(1);
expect(await getFaultName(page, 1)).toEqual(faultTwoName);
faultResultCount = await utils.getFaultResultCount(page);
expect.soft(faultResultCount).toEqual(1);
expect.soft(await utils.getFaultName(page, 1)).toEqual(faultTwoName);
// all faults
await clearSearch(page);
faultResultCount = await getFaultResultCount(page);
expect(faultResultCount).toEqual(5);
await utils.clearSearch(page);
faultResultCount = await utils.getFaultResultCount(page);
expect.soft(faultResultCount).toEqual(5);
// search triggerTime
await enterSearchTerm(page, faultFiveTriggerTime);
await utils.enterSearchTerm(page, faultFiveTriggerTime);
faultResultCount = await getFaultResultCount(page);
expect(faultResultCount).toEqual(1);
expect(await getFaultTriggerTime(page, 1)).toEqual(faultFiveTriggerTime);
faultResultCount = await utils.getFaultResultCount(page);
expect.soft(faultResultCount).toEqual(1);
expect.soft(await utils.getFaultTriggerTime(page, 1)).toEqual(faultFiveTriggerTime);
});
test('Allows you to sort faults', async ({ page }) => {
const highestSeverity = await getHighestSeverity(page);
const lowestSeverity = await getLowestSeverity(page);
test('Allows you to sort faults @unstable', async ({ page }) => {
const highestSeverity = await utils.getHighestSeverity(page);
const lowestSeverity = await utils.getLowestSeverity(page);
const faultOneName = 'Example Fault 1';
const faultFiveName = 'Example Fault 5';
let firstFaultName = await getFaultName(page, 1);
let firstFaultName = await utils.getFaultName(page, 1);
expect(firstFaultName).toEqual(faultOneName);
expect.soft(firstFaultName).toEqual(faultOneName);
await sortFaultsBy(page, 'oldest-first');
await utils.sortFaultsBy(page, 'oldest-first');
firstFaultName = await getFaultName(page, 1);
expect(firstFaultName).toEqual(faultFiveName);
firstFaultName = await utils.getFaultName(page, 1);
expect.soft(firstFaultName).toEqual(faultFiveName);
await sortFaultsBy(page, 'severity');
await utils.sortFaultsBy(page, 'severity');
const sortedHighestSeverity = await getFaultSeverity(page, 1);
const sortedLowestSeverity = await getFaultSeverity(page, 5);
expect(sortedHighestSeverity).toEqual(highestSeverity);
expect(sortedLowestSeverity).toEqual(lowestSeverity);
const sortedHighestSeverity = await utils.getFaultSeverity(page, 1);
const sortedLowestSeverity = await utils.getFaultSeverity(page, 5);
expect.soft(sortedHighestSeverity).toEqual(highestSeverity);
expect.soft(sortedLowestSeverity).toEqual(lowestSeverity);
});
});
test.describe('The Fault Management Plugin without using example faults', () => {
test.beforeEach(async ({ page }) => {
await navigateToFaultManagementWithoutExample(page);
await utils.navigateToFaultManagementWithoutExample(page);
});
test('Shows no faults when no faults are provided', async ({ page }) => {
test('Shows no faults when no faults are provided @unstable', async ({ page }) => {
const faultCount = await page.locator('c-fault-mgmt__list').count();
expect(faultCount).toEqual(0);
expect.soft(faultCount).toEqual(0);
await changeViewTo(page, 'acknowledged');
await utils.changeViewTo(page, 'acknowledged');
const acknowledgedCount = await page.locator('c-fault-mgmt__list').count();
expect(acknowledgedCount).toEqual(0);
expect.soft(acknowledgedCount).toEqual(0);
await changeViewTo(page, 'shelved');
await utils.changeViewTo(page, 'shelved');
const shelvedCount = await page.locator('c-fault-mgmt__list').count();
expect(shelvedCount).toEqual(0);
expect.soft(shelvedCount).toEqual(0);
});
test('Will return no faults when searching', async ({ page }) => {
await enterSearchTerm(page, 'fault');
test('Will return no faults when searching @unstable', async ({ page }) => {
await utils.enterSearchTerm(page, 'fault');
const faultCount = await page.locator('c-fault-mgmt__list').count();
expect(faultCount).toEqual(0);
expect.soft(faultCount).toEqual(0);
});
});

View File

@@ -248,10 +248,11 @@ test.describe('Flexible Layout', () => {
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// flip on independent time conductor
await setIndependentTimeConductorBounds(page, {
start: '2021-12-30 01:01:00.000Z',
end: '2021-12-30 01:11:00.000Z'
});
await setIndependentTimeConductorBounds(
page,
'2021-12-30 01:01:00.000Z',
'2021-12-30 01:11:00.000Z'
);
// check image date
await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible();
@@ -289,7 +290,7 @@ test.describe('Flexible Layout Toolbar Actions @localStorage', () => {
await page.getByTitle('Add Container').click();
expect(await containerHandles.count()).toEqual(3);
await page.getByTitle('Remove Container').click();
await expect(page.getByRole('dialog', { name: 'Overlay' })).toContainText(
await expect(page.getByRole('dialog', { name: 'Overlay' })).toHaveText(
'This action will permanently delete this container from this Flexible Layout. Do you want to continue?'
);
await page.getByRole('button', { name: 'OK', exact: true }).click();
@@ -299,7 +300,7 @@ test.describe('Flexible Layout Toolbar Actions @localStorage', () => {
expect(await page.getByRole('group', { name: 'Frame' }).count()).toEqual(2);
await page.getByRole('group', { name: 'Child Layout 1' }).click();
await page.getByTitle('Remove Frame').click();
await expect(page.getByRole('dialog', { name: 'Overlay' })).toContainText(
await expect(page.getByRole('dialog', { name: 'Overlay' })).toHaveText(
'This action will remove this frame from this Flexible Layout. Do you want to continue?'
);
await page.getByRole('button', { name: 'OK', exact: true }).click();

View File

@@ -175,13 +175,13 @@ test.describe('Gauge', () => {
});
// Try to create a Folder into the Gauge. Should be disallowed.
await page.getByRole('button', { name: 'Create' }).click();
await page.getByRole('button', { name: /Create/ }).click();
await page.getByRole('menuitem', { name: /Folder/ }).click();
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
await page.getByLabel('Cancel').click();
// Try to create a Display Layout into the Gauge. Should be disallowed.
await page.getByRole('button', { name: 'Create' }).click();
await page.getByRole('button', { name: /Create/ }).click();
await page.getByRole('menuitem', { name: /Display Layout/ }).click();
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
});

View File

@@ -363,7 +363,7 @@ test.describe('Example Imagery in Display Layout', () => {
await page.locator('li[title="View Large"]').click();
await expect(pausePlayButton).toHaveClass(/is-paused/);
await page.getByRole('button', { name: 'Close' }).click();
await page.locator('[aria-label="Close"]').click();
await expect.soft(pausePlayButton).not.toHaveClass(/is-paused/);
});
@@ -386,7 +386,7 @@ test.describe('Example Imagery in Display Layout', () => {
await page.locator('li[title="View Large"]').click();
await expect(pausePlayButton).toHaveClass(/is-paused/);
await page.getByRole('button', { name: 'Close' }).click();
await page.locator('[aria-label="Close"]').click();
await expect.soft(pausePlayButton).toHaveClass(/is-paused/);
});
@@ -509,7 +509,7 @@ test.describe('Example Imagery in Flexible layout', () => {
await page.getByRole('button', { name: 'Background Image', state: 'visible' });
// Close the large view
await page.getByRole('button', { name: 'Close' }).click();
await page.getByLabel('Close').click();
});
test.beforeEach(async ({ page }) => {

View File

@@ -37,8 +37,6 @@ test.describe('Testing numeric data with inspector data visualization (i.e., dat
});
test('Can click on telemetry and see data in inspector @2p', async ({ page, context }) => {
const initStartBounds = await page.getByLabel('Start bounds').textContent();
const initEndBounds = await page.getByLabel('End bounds').textContent();
const exampleDataVisualizationSource = await createDomainObjectWithDefaults(page, {
type: 'Example Data Visualization Source'
});
@@ -80,9 +78,5 @@ test.describe('Testing numeric data with inspector data visualization (i.e., dat
await newPage.waitForLoadState();
// expect new tab title to contain 'Second Sine Wave Generator'
await expect(newPage).toHaveTitle('Second Sine Wave Generator');
// Verify that "Open in New Tab" preserves the time bounds
expect(initStartBounds).toEqual(await newPage.getByLabel('Start bounds').textContent());
expect(initEndBounds).toEqual(await newPage.getByLabel('End bounds').textContent());
});
});

View File

@@ -308,7 +308,7 @@ test.describe('Notebook entry tests', () => {
await page.goto(notebookObject.url);
// Reveal the notebook in the tree
await page.getByLabel('Show selected item in tree').click();
await page.getByTitle('Show selected item in tree').click();
await page
.getByRole('treeitem', { name: overlayPlot.name })
@@ -332,7 +332,7 @@ test.describe('Notebook entry tests', () => {
await page.goto(notebookObject.url);
// Reveal the notebook in the tree
await page.getByLabel('Show selected item in tree').click();
await page.getByTitle('Show selected item in tree').click();
await nbUtils.enterTextEntry(page, 'Entry to drop into');
await page
@@ -377,7 +377,7 @@ test.describe('Notebook entry tests', () => {
await page.goto(notebookObject.url);
// Reveal the notebook in the tree
await page.getByLabel('Show selected item in tree').click();
await page.getByTitle('Show selected item in tree').click();
await nbUtils.enterTextEntry(page, `This should be a link: ${TEST_LINK} is it?`);
@@ -404,7 +404,7 @@ test.describe('Notebook entry tests', () => {
await page.goto(notebookObject.url);
// Reveal the notebook in the tree
await page.getByLabel('Show selected item in tree').click();
await page.getByTitle('Show selected item in tree').click();
await nbUtils.enterTextEntry(page, `This should NOT be a link: ${TEST_LINK} is it?`);
@@ -421,7 +421,7 @@ test.describe('Notebook entry tests', () => {
await page.goto(notebookObject.url);
// Reveal the notebook in the tree
await page.getByLabel('Show selected item in tree').click();
await page.getByTitle('Show selected item in tree').click();
await nbUtils.enterTextEntry(page, `This should NOT be a link: ${TEST_LINK} is it?`);
@@ -438,7 +438,7 @@ test.describe('Notebook entry tests', () => {
await page.goto(notebookObject.url);
// Reveal the notebook in the tree
await page.getByLabel('Show selected item in tree').click();
await page.getByTitle('Show selected item in tree').click();
await nbUtils.enterTextEntry(page, `This should be a link: ${INVALID_TEST_LINK} is it?`);
@@ -455,7 +455,7 @@ test.describe('Notebook entry tests', () => {
await page.goto(notebookObject.url);
// Reveal the notebook in the tree
await page.getByLabel('Show selected item in tree').click();
await page.getByTitle('Show selected item in tree').click();
await nbUtils.enterTextEntry(page, `This should be a link: ${TEST_LINK} is it?`);
@@ -483,7 +483,7 @@ test.describe('Notebook entry tests', () => {
await page.goto(notebookObject.url);
// Reveal the notebook in the tree
await page.getByLabel('Show selected item in tree').click();
await page.getByTitle('Show selected item in tree').click();
await nbUtils.enterTextEntry(
page,

View File

@@ -68,7 +68,7 @@ test.describe('Snapshot image tests', () => {
// expect large image to be displayed
await expect(page.getByRole('dialog').getByText('favicon-96x96.png')).toBeVisible();
await page.getByRole('button', { name: 'Close' }).click();
await page.getByLabel('Close').click();
// drop another image onto the entry
await page.dispatchEvent('.c-snapshots', 'drop', { dataTransfer: dropTransfer });

View File

@@ -71,89 +71,42 @@ test.describe('Snapshot Container tests', () => {
test.beforeEach(async ({ page }) => {
//Navigate to baseURL
await page.goto('./', { waitUntil: 'domcontentloaded' });
await page.getByLabel('Open the Notebook Snapshot Menu').click();
// Create Notebook
// const notebook = await createDomainObjectWithDefaults(page, {
// type: 'Notebook',
// name: "Test Notebook"
// });
// // Create Overlay Plot
// const snapShotObject = await createDomainObjectWithDefaults(page, {
// type: 'Overlay Plot',
// name: "Dropped Overlay Plot"
// });
await page.getByLabel('Take a Notebook Snapshot').click();
await page.getByRole('menuitem', { name: 'Save to Notebook Snapshots' }).click();
await page.getByLabel('Show Snapshots').click();
});
test('A snapshot can be Quick Viewed from Container with 3 dot action menu', async ({ page }) => {
await page.getByLabel('My Items Notebook Embed').getByLabel('More actions').click();
await page.locator('.c-snapshot.c-ne__embed').first().getByTitle('More actions').click();
await page.getByRole('menuitem', { name: 'Quick View' }).click();
await expect(page.getByLabel('Modal Overlay')).toBeVisible();
await expect(page.getByLabel('Preview Container')).toBeVisible();
});
test('A snapshot can be Viewed, Annotated, display deleted, and saved from Container with 3 dot action menu', async ({
page
}) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7552'
});
//Open Snapshot Object View
await page.getByLabel('My Items Notebook Embed').getByLabel('More actions').click();
await page.getByRole('menuitem', { name: 'View Snapshot' }).click();
await expect(page.getByRole('dialog', { name: 'Modal Overlay' })).toBeVisible();
await expect(page.locator('#snapshotDescriptor')).toHaveText(
/SNAPSHOT \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/
);
// Open Annotation Editor with Painterro
await page.getByLabel('Annotate this snapshot').click();
await expect(page.locator('#snap-annotation-canvas')).toBeVisible();
// Clear the canvas
await page.getByRole('button', { name: 'Put text [T]' }).click();
// Click in the Painterro canvas to add a text annotation
await page.locator('.ptro-crp-el').click();
await page.locator('.ptro-text-tool-input').fill('...is there life on mars?');
// When working with Painterro, we need to check that the Apply button is hidden after clicking
await page.getByTitle('Apply').click();
await expect(page.getByTitle('Apply')).toBeHidden();
// Save and exit annotation window
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('button', { name: 'Done' }).click();
// Open up annotation again
await page.getByRole('img', { name: 'My Items thumbnail' }).click();
await expect(page.getByLabel('Modal Overlay').getByRole('img')).toBeVisible();
});
test('A snapshot can be Annotated and saved as a JPG and PNG', async ({ page }) => {
//Open Snapshot Object View
await page.getByLabel('My Items Notebook Embed').getByLabel('More actions').click();
await page.getByRole('menuitem', { name: 'View Snapshot' }).click();
await expect(page.getByRole('dialog', { name: 'Modal Overlay' })).toBeVisible();
// Open Annotation Editor with Painterro
await page.getByLabel('Annotate this snapshot').click();
await expect(page.locator('#snap-annotation-canvas')).toBeVisible();
// Clear the canvas
await page.getByRole('button', { name: 'Put text [T]' }).click();
// Click in the Painterro canvas to add a text annotation
await page.locator('.ptro-crp-el').click();
await page.locator('.ptro-text-tool-input').fill('...is there life on mars?');
// When working with Painterro, we need to check that the Apply button is hidden after clicking
await page.getByTitle('Apply').click();
await expect(page.getByTitle('Apply')).toBeHidden();
// Save and exit annotation window
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('button', { name: 'Done' }).click();
// Open up annotation again
await page.getByRole('img', { name: 'My Items thumbnail' }).click();
await expect(page.getByLabel('Modal Overlay').getByRole('img')).toBeVisible();
// Save as JPG
await Promise.all([
page.waitForEvent('download'), // Waits for the download event
page.getByLabel('Export as JPG').click() // Triggers the download
]);
// Save as PNG
await expect(page.getByLabel('Modal Overlay').getByRole('img')).toBeVisible();
await Promise.all([
page.waitForEvent('download'), // Waits for the download event
page.getByLabel('Export as PNG').click() // Triggers the download
]);
await expect(page.locator('.c-overlay__outer')).toBeVisible();
});
test.fixme(
'A snapshot can be Viewed, Annotated, display deleted, and saved from Container with 3 dot action menu',
async ({ page }) => {
await page.locator('.c-snapshot.c-ne__embed').first().getByTitle('More actions').click();
await page.getByRole('menuitem', { name: ' View Snapshot' }).click();
await expect(page.locator('.c-overlay__outer')).toBeVisible();
await page.getByTitle('Annotate').click();
await expect(page.locator('#snap-annotation-canvas')).toBeVisible();
await page.getByRole('button', { name: '' }).click();
// await expect(page.locator('#snap-annotation-canvas')).not.toBeVisible();
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('button', { name: 'Done' }).click();
//await expect(await page.locator)
}
);
test.fixme('5 Snapshots can be added to a container', async ({ page }) => {});
test.fixme(
'5 Snapshots can be added to a container and Deleted with Delete All action',
@@ -163,6 +116,10 @@ test.describe('Snapshot Container tests', () => {
'A snapshot can be Deleted from Container with 3 dot action menu',
async ({ page }) => {}
);
test.fixme(
'A snapshot can be Navigated To from Container with 3 dot action menu',
async ({ page }) => {}
);
test.fixme(
'A snapshot can be Navigated To Item in Time from Container with 3 dot action menu',
async ({ page }) => {}
@@ -194,4 +151,11 @@ test.describe('Snapshot Container tests', () => {
//Snapshot removed from container?
}
);
test.fixme(
'Verify Embedded options for PNG, JPG, and Annotate work correctly',
async ({ page }) => {
//Add snapshot to container
//Verify PNG, JPG, and Annotate buttons work correctly
}
);
});

View File

@@ -24,60 +24,138 @@
Tests to verify log plot functionality when objects are missing
*/
import { createDomainObjectWithDefaults } from '../../../../appActions.js';
import { expect, test } from '../../../../pluginFixtures.js';
test.describe('Handle missing object for plots', () => {
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
});
test('Displays empty div for missing stacked plot item', async ({ page, browserName }) => {
test('Displays empty div for missing stacked plot item @unstable', async ({
page,
browserName,
openmctConfig
}) => {
// eslint-disable-next-line playwright/no-skipped-test
test.skip(browserName === 'firefox', 'Firefox failing due to console events being missed');
let warningReceived = false;
const { myItemsFolderName } = openmctConfig;
const errorLogs = [];
page.on('console', (message) => {
if (message.type() === 'warning' && message.text().includes('Missing domain object')) {
warningReceived = true;
errorLogs.push(message.text());
}
});
const stackedPlot = await createDomainObjectWithDefaults(page, {
type: 'Stacked Plot'
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: stackedPlot.uuid
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: stackedPlot.uuid
});
//Make stacked plot
await makeStackedPlot(page, myItemsFolderName);
//Gets local storage and deletes the last sine wave generator in the stacked plot
const mct = await page.evaluate(() => window.localStorage.getItem('mct'));
const parsedData = JSON.parse(mct);
const key = Object.entries(parsedData).find(([, value]) => value.type === 'generator')?.[0];
const localStorage = await page.evaluate(() => window.localStorage);
const parsedData = JSON.parse(localStorage.mct);
const keys = Object.keys(parsedData);
const lastKey = keys[keys.length - 1];
delete parsedData[key];
delete parsedData[lastKey];
//Sets local storage with missing object
const jsonData = JSON.stringify(parsedData);
await page.evaluate((data) => {
window.localStorage.setItem('mct', data);
}, jsonData);
await page.evaluate(`window.localStorage.setItem('mct', '${JSON.stringify(parsedData)}')`);
//Reloads page and clicks on stacked plot
await page.reload({ waitUntil: 'domcontentloaded' });
await page.goto(stackedPlot.url);
await Promise.all([page.reload(), page.waitForLoadState('networkidle')]);
//Verify Main section is there on load
await expect(page.locator('.l-browse-bar__object-name')).toContainText(stackedPlot.name);
await expect
.soft(page.locator('.l-browse-bar__object-name'))
.toContainText('Unnamed Stacked Plot');
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
await Promise.all([
page.waitForNavigation(),
page.locator('text=Unnamed Stacked Plot').first().click()
]);
//Check that there is only one stacked item plot with a plot, the missing one will be empty
await expect(page.getByLabel('Stacked Plot Item')).toHaveCount(1);
//Verify that console.warn was thrown
expect(warningReceived).toBe(true);
await expect(page.locator('.c-plot--stacked-container:has(.gl-plot)')).toHaveCount(1);
//Verify that console.warn is thrown
expect(errorLogs).toHaveLength(1);
});
});
/**
* This is used the create a stacked plot object
* @private
*/
async function makeStackedPlot(page, myItemsFolderName) {
// fresh page with time range from 2022-03-29 22:00:00.000Z to 2022-03-29 22:00:30.000Z
await page.goto('./', { waitUntil: 'domcontentloaded' });
// create stacked plot
await page.locator('button.c-create-button').click();
await page.locator('li[role="menuitem"]:has-text("Stacked Plot")').click();
await Promise.all([
page.waitForNavigation({ waitUntil: 'networkidle' }),
page.locator('button:has-text("OK")').click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
// save the stacked plot
await saveStackedPlot(page);
// create a sinewave generator
await createSineWaveGenerator(page);
// click on stacked plot
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
await Promise.all([
page.waitForNavigation(),
page.locator('text=Unnamed Stacked Plot').first().click()
]);
// create a second sinewave generator
await createSineWaveGenerator(page);
// click on stacked plot
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
await Promise.all([
page.waitForNavigation(),
page.locator('text=Unnamed Stacked Plot').first().click()
]);
}
/**
* This is used to save a stacked plot object
* @private
*/
async function saveStackedPlot(page) {
// save stacked plot
await page
.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button')
.nth(1)
.click();
await Promise.all([
page.locator('text=Save and Finish Editing').click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
//Wait until Save Banner is gone
await page.locator('.c-message-banner__close-button').click();
await page.waitForSelector('.c-message-banner__message', { state: 'detached' });
}
/**
* This is used to create a sine wave generator object
* @private
*/
async function createSineWaveGenerator(page) {
//Create sine wave generator
await page.locator('button.c-create-button').click();
await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click();
await Promise.all([
page.waitForNavigation({ waitUntil: 'networkidle' }),
page.locator('button:has-text("OK")').click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
}

View File

@@ -33,15 +33,15 @@ import {
import { expect, test } from '../../../../pluginFixtures.js';
test.describe('Overlay Plot', () => {
let overlayPlot;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot'
});
});
test('Plot legend color is in sync with plot series color', async ({ page }) => {
const overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot'
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: overlayPlot.uuid
@@ -68,6 +68,9 @@ test.describe('Overlay Plot', () => {
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7403'
});
const overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot'
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
@@ -127,6 +130,10 @@ test.describe('Overlay Plot', () => {
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/6338'
});
// Create an Overlay Plot with a default SWG
const overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot'
});
const swgA = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
@@ -189,60 +196,13 @@ test.describe('Overlay Plot', () => {
await assertLimitLinesExistAndAreVisible(page);
});
test('Limit lines adjust when series is resized', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/6987'
});
// Create an Overlay Plot with a default SWG
overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot'
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: overlayPlot.uuid
});
await page.goto(overlayPlot.url);
// Assert that no limit lines are shown by default
await page.waitForSelector('.js-limit-area', { state: 'attached' });
expect(await page.locator('.c-plot-limit-line').count()).toBe(0);
// Enter edit mode
await page.getByLabel('Edit Object').click();
// Expand the "Sine Wave Generator" plot series options and enable limit lines
await page.getByRole('tab', { name: 'Config' }).click();
await page
.getByRole('list', { name: 'Plot Series Properties' })
.locator('span')
.first()
.click();
await page
.getByRole('list', { name: 'Plot Series Properties' })
.getByRole('checkbox', { name: 'Limit lines' })
.check();
await assertLimitLinesExistAndAreVisible(page);
// Save (exit edit mode)
await page.locator('button[title="Save"]').click();
await page.locator('li[title="Save and Finish Editing"]').click();
const initialCoords = await assertLimitLinesExistAndAreVisible(page);
// Resize the chart container by showing the snapshot pane.
await page.getByLabel('Show Snapshots').click();
const newCoords = await assertLimitLinesExistAndAreVisible(page);
// We just need to know that the first limit line redrew somewhere lower than the initial y position.
expect(newCoords.y).toBeGreaterThan(initialCoords.y);
});
test('The elements pool supports dragging series into multiple y-axis buckets', async ({
page
}) => {
const overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot'
});
const swgA = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: overlayPlot.uuid
@@ -332,6 +292,10 @@ test.describe('Overlay Plot', () => {
description: 'https://github.com/nasa/openmct/issues/7421'
});
const overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot'
});
const swgA = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: overlayPlot.uuid
@@ -345,53 +309,12 @@ test.describe('Overlay Plot', () => {
await page.getByRole('tab', { name: 'Elements' }).click();
await page.locator(`#inspector-elements-tree >> text=${swgA.name}`).click();
const plotPixels = await getCanvasPixels(page, '.js-overlay canvas');
const plotPixelSize = plotPixels.length;
expect(plotPixelSize).toBeGreaterThan(0);
}
);
test('Can remove an item via the elements pool action menu', async ({ page }) => {
const swgA = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: overlayPlot.uuid
});
const swgB = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: overlayPlot.uuid
});
await page.goto(overlayPlot.url);
// Wait for plot series data to load and be drawn
await waitForPlotsToRender(page);
await page.getByLabel('Edit Object').click();
await page.getByRole('tab', { name: 'Elements' }).click();
const swgAElementsPoolItem = page.getByLabel(`Preview ${swgA.name}`);
await expect(swgAElementsPoolItem).toBeVisible();
await swgAElementsPoolItem.click({ button: 'right' });
await page.getByRole('menuitem', { name: 'Remove' }).click();
await page.getByRole('button', { name: 'OK', exact: true }).click();
await expect(swgAElementsPoolItem).toBeHidden();
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7530'
});
await test.step('Verify that the legend is correct after removing a series', async () => {
await page.getByLabel('Plot Canvas').hover();
await page.mouse.move(50, 0, {
steps: 10
});
await expect(page.getByLabel('Plot Legend Item')).toHaveCount(1);
await expect(page.getByLabel(`Plot Legend Item for ${swgA.name}`)).toBeHidden();
await expect(page.getByLabel(`Plot Legend Item for ${swgB.name}`)).toBeVisible();
});
});
});
/**
@@ -409,7 +332,4 @@ async function assertLimitLinesExistAndAreVisible(page) {
for (let i = 0; i < limitLineCount; i++) {
await expect(page.locator('.c-plot-limit-line').nth(i)).toBeVisible();
}
const firstLimitLineCoords = await page.locator('.c-plot-limit-line').first().boundingBox();
return firstLimitLineCoords;
}

View File

@@ -54,12 +54,10 @@ test.describe('Plots work in Previews', () => {
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// right click on the plot and select view large
await page.getByLabel(/Alpha-numeric telemetry value of.*/).click({
button: 'right'
});
await page.getByLabel('Sine', { exact: true }).click({ button: 'right' });
await page.getByLabel('View Historical Data').click();
await expect(page.getByLabel('Preview Container').getByLabel('Plot Canvas')).toBeVisible();
await page.getByRole('button', { name: 'Close' }).click();
await page.getByLabel('Close').click();
await page.getByLabel('Expand Test Display Layout layout').click();
// change to a plot and ensure embiggen works
@@ -75,7 +73,7 @@ test.describe('Plots work in Previews', () => {
await expect(page.getByLabel('Preview Container')).toBeHidden();
await page.getByLabel('Large View').click();
await expect(page.getByLabel('Preview Container').getByLabel('Plot Canvas')).toBeVisible();
await page.getByRole('button', { name: 'Close' }).click();
await page.getByLabel('Close').click();
// get last sinewave tree item (in the display layout)
await page
@@ -85,6 +83,6 @@ test.describe('Plots work in Previews', () => {
.click({ button: 'right' });
await page.getByLabel('View', { exact: true }).click();
await expect(page.getByLabel('Preview Container').getByLabel('Plot Canvas')).toBeVisible();
await page.getByRole('button', { name: 'Close' }).click();
await page.getByLabel('Close').click();
});
});

View File

@@ -114,9 +114,7 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(defaultFrameBorderColor),
NO_STYLE_RGBA,
hexToRGB(setTextColor),
page
.getByRole('group', { name: 'StackedPlot1 Frame' })
.getByLabel('Stacked Plot Style Target')
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
);
// Check styles on StackedPlot2. Note: https://github.com/nasa/openmct/issues/7337
@@ -124,9 +122,7 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(defaultFrameBorderColor),
NO_STYLE_RGBA,
hexToRGB(setTextColor),
page
.getByRole('group', { name: 'StackedPlot2 Frame' })
.getByLabel('Stacked Plot Style Target')
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target')
);
});
@@ -147,9 +143,7 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(defaultBorderTargetColor),
NO_STYLE_RGBA,
hexToRGB(defaultTextColor),
page
.getByRole('group', { name: 'StackedPlot1 Frame' })
.getByLabel('Stacked Plot Style Target')
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
);
// Check styles on StackedPlot2
@@ -157,9 +151,7 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(defaultBorderTargetColor),
NO_STYLE_RGBA,
hexToRGB(defaultTextColor),
page
.getByRole('group', { name: 'StackedPlot2 Frame' })
.getByLabel('Stacked Plot Style Target')
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target')
);
// Set styles using setStyles function on StackedPlot1 but not StackedPlot2
@@ -168,7 +160,7 @@ test.describe('Flexible Layout styling', () => {
setBorderColor,
setBackgroundColor,
setTextColor,
page.getByRole('group', { name: 'StackedPlot1 Frame' })
page.getByLabel('StackedPlot1 Frame')
);
// Check styles on StackedPlot1
@@ -176,9 +168,7 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(setBorderColor),
hexToRGB(setBackgroundColor),
hexToRGB(setTextColor),
page
.getByRole('group', { name: 'StackedPlot1 Frame' })
.getByLabel('Stacked Plot Style Target')
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
);
// Check styles on StackedPlot2
@@ -186,9 +176,7 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(defaultBorderTargetColor),
NO_STYLE_RGBA,
hexToRGB(defaultTextColor),
page
.getByRole('group', { name: 'StackedPlot2 Frame' })
.getByLabel('Stacked Plot Style Target')
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target')
);
// Save Flexible Layout
@@ -203,9 +191,7 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(setBorderColor),
hexToRGB(setBackgroundColor),
hexToRGB(setTextColor),
page
.getByRole('group', { name: 'StackedPlot1 Frame' })
.getByLabel('Stacked Plot Style Target')
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
);
// Check styles on StackedPlot2
@@ -213,9 +199,7 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(defaultBorderTargetColor),
NO_STYLE_RGBA,
hexToRGB(defaultTextColor),
page
.getByRole('group', { name: 'StackedPlot2 Frame' })
.getByLabel('Stacked Plot Style Target')
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target')
);
});
@@ -257,9 +241,7 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(setBorderColor),
hexToRGB(setBackgroundColor),
hexToRGB(setTextColor),
page
.getByRole('group', { name: 'StackedPlot1 Frame' })
.getByLabel('Stacked Plot Style Target')
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
);
// Check styles on StackedPlot2 to verify they are the default
@@ -267,9 +249,7 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(defaultBorderTargetColor),
NO_STYLE_RGBA,
hexToRGB(defaultTextColor),
page
.getByRole('group', { name: 'StackedPlot2 Frame' })
.getByLabel('Stacked Plot Style Target')
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target')
);
// Set styles using setStyles function on StackedPlot2
@@ -278,7 +258,7 @@ test.describe('Flexible Layout styling', () => {
setBorderColor,
setBackgroundColor,
setTextColor,
page.getByRole('group', { name: 'StackedPlot2 Frame' })
page.getByLabel('StackedPlot2 Frame')
);
// Check styles on StackedPlot2
@@ -286,9 +266,7 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(setBorderColor),
hexToRGB(setBackgroundColor),
hexToRGB(setTextColor),
page
.getByRole('group', { name: 'StackedPlot2 Frame' })
.getByLabel('Stacked Plot Style Target')
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target')
);
// Save Flexible Layout
@@ -303,9 +281,7 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(setBorderColor),
hexToRGB(setBackgroundColor),
hexToRGB(setTextColor),
page
.getByRole('group', { name: 'StackedPlot1 Frame' })
.getByLabel('Stacked Plot Style Target')
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
);
// Check styles on StackedPlot2
@@ -313,9 +289,7 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(setBorderColor),
hexToRGB(setBackgroundColor),
hexToRGB(setTextColor),
page
.getByRole('group', { name: 'StackedPlot2 Frame' })
.getByLabel('Stacked Plot Style Target')
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target')
);
// Directly navigate to the flexible layout
@@ -352,9 +326,7 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(setBorderColor),
hexToRGB(setBackgroundColor),
hexToRGB(setTextColor),
page
.getByRole('group', { name: 'StackedPlot1 Frame' })
.getByLabel('Stacked Plot Style Target')
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
);
// Check styles on StackedPlot2 matches previous set colors
@@ -362,9 +334,7 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(setBorderColor),
hexToRGB(setBackgroundColor),
hexToRGB(setTextColor),
page
.getByRole('group', { name: 'StackedPlot2 Frame' })
.getByLabel('Stacked Plot Style Target')
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target')
);
});
@@ -386,7 +356,7 @@ test.describe('Flexible Layout styling', () => {
setBorderColor,
setBackgroundColor,
setTextColor,
page.getByRole('group', { name: 'StackedPlot1 Frame' })
page.getByLabel('StackedPlot1 Frame')
);
// Check styles using checkStyles function
@@ -394,9 +364,7 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(setBorderColor),
hexToRGB(setBackgroundColor),
hexToRGB(setTextColor),
page
.getByRole('group', { name: 'StackedPlot1 Frame' })
.getByLabel('Stacked Plot Style Target')
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
);
// Save Flexible Layout
@@ -418,7 +386,7 @@ test.describe('Flexible Layout styling', () => {
'No Style',
'No Style',
'No Style',
page.getByRole('group', { name: 'StackedPlot1 Frame' })
page.getByLabel('StackedPlot1 Frame')
);
// Check styles using checkStyles function
@@ -426,9 +394,7 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(defaultBorderTargetColor),
NO_STYLE_RGBA,
hexToRGB(inheritedColor),
page
.getByRole('group', { name: 'StackedPlot1 Frame' })
.getByLabel('Stacked Plot Style Target')
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
);
// Save Flexible Layout
await page.getByRole('button', { name: 'Save' }).click();
@@ -442,9 +408,7 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(defaultBorderTargetColor),
NO_STYLE_RGBA,
hexToRGB(inheritedColor),
page
.getByRole('group', { name: 'StackedPlot1 Frame' })
.getByLabel('Stacked Plot Style Target')
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
);
});

View File

@@ -67,7 +67,7 @@ test.describe('Style Inspector Options', () => {
await expect(page.getByRole('tab', { name: 'Styles' })).toBeVisible();
// Select Stacked Layout Column
await page.getByRole('group', { name: 'Stacked Plot Frame' }).click();
await page.getByLabel('Stacked Plot Frame').click();
// The overall Flex Layout or Stacked Plot itself MUST be style-able.
await expect(page.getByRole('tab', { name: 'Styles' })).toBeVisible();

View File

@@ -53,7 +53,7 @@ test.describe('Preview mode', () => {
await expect(page.getByLabel('Export Table Data')).toBeVisible();
await expect(page.getByLabel('Export Marked Rows')).toBeVisible();
await page.getByRole('menuitem', { name: 'Pause' }).click();
await page.getByRole('button', { name: 'Close' }).click();
await page.getByLabel('Close').click();
await expandEntireTree(page);

View File

@@ -20,31 +20,10 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import {
createDomainObjectWithDefaults,
setTimeConductorBounds,
setTimeConductorMode
} from '../../../../appActions.js';
import { createDomainObjectWithDefaults, setTimeConductorBounds } from '../../../../appActions.js';
import { expect, test } from '../../../../pluginFixtures.js';
test.describe('Telemetry Table', () => {
let table;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
table = await createDomainObjectWithDefaults(page, { type: 'Telemetry Table' });
});
test('Limits to 50 rows by default', async ({ page }) => {
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: table.uuid
});
await page.goto(table.url);
await setTimeConductorMode(page, false);
const rows = page.getByLabel('table content').getByLabel('Table Row');
await expect(rows).toHaveCount(50);
});
test('unpauses and filters data when paused by button and user changes bounds', async ({
page
}) => {
@@ -55,6 +34,7 @@ test.describe('Telemetry Table', () => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
const table = await createDomainObjectWithDefaults(page, { type: 'Telemetry Table' });
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: table.uuid
@@ -98,6 +78,7 @@ test.describe('Telemetry Table', () => {
test('Supports filtering telemetry by regular text search', async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
const table = await createDomainObjectWithDefaults(page, { type: 'Telemetry Table' });
await createDomainObjectWithDefaults(page, {
type: 'Event Message Generator',
parent: table.uuid
@@ -140,6 +121,7 @@ test.describe('Telemetry Table', () => {
test('Supports filtering using Regex', async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
const table = await createDomainObjectWithDefaults(page, { type: 'Telemetry Table' });
await createDomainObjectWithDefaults(page, {
type: 'Event Message Generator',
parent: table.uuid

View File

@@ -1,64 +0,0 @@
/*****************************************************************************
* 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,
setIndependentTimeConductorBounds
} from '../../../../appActions.js';
import { expect, test } from '../../../../pluginFixtures.js';
const FIXED_TIME =
'./#/browse/mine?tc.mode=fixed&tc.startBound=1693592063607&tc.endBound=1693593893607&tc.timeSystem=utc&view=grid&hideInspector=true&hideTree=true';
test.describe('Datepicker operations', () => {
test.beforeEach(async ({ page }) => {
await page.goto(FIXED_TIME);
});
test('Verify that user can use the datepicker in the TC', async ({ page }) => {
await page.getByLabel('Time Conductor Mode').click();
// Click on the date picker that is left-most on the screen
await page.getByLabel('Global Time Conductor').locator('a').first().click();
await expect(page.getByRole('dialog')).toBeVisible();
// Click on the first cell
await page.getByText('27 239').click();
// Expect datepicker to close and time conductor date setting to be changed
await expect(page.getByRole('dialog')).toHaveCount(0);
});
test('Verify that user can use the datepicker in the ITC', async ({ page }) => {
const createdTimeList = await createDomainObjectWithDefaults(page, { type: 'Time List' });
await page.goto(createdTimeList.url, { waitUntil: 'domcontentloaded' });
await setIndependentTimeConductorBounds(page, {
start: '2024-11-12 19:11:11.000Z',
end: '2024-11-12 20:11:11.000Z'
});
// Open ITC
await page.getByLabel('Start bounds').nth(0).click();
// Click on the datepicker icon
await page.locator('form a').first().click();
await expect(page.getByRole('dialog')).toBeVisible();
// Click on the first cell
await page.getByText('7 342').click();
// Expect datepicker to close and time conductor date setting to be changed
await expect(page.getByRole('dialog')).toHaveCount(0);
});
});

View File

@@ -66,7 +66,7 @@ test.describe('Timer', () => {
});
});
test.describe('Timer with target date @clock', () => {
test.describe('Timer with target date', () => {
let timer;
test.beforeEach(async ({ page }) => {

View File

@@ -191,7 +191,7 @@ test.describe('Recent Objects', () => {
// Navigate to the clock and reveal it in the tree
await page.goto(clock.url);
await page.getByLabel('Show selected item in tree').click();
await page.getByTitle('Show selected item in tree').click();
// Right click the clock and create an alias using the "link" context menu action
const clockTreeItem = page
@@ -298,7 +298,7 @@ test.describe('Recent Objects', () => {
// Assert that the list is empty
expect(await recentObjectsList.locator('.c-recentobjects-listitem').count()).toBe(0);
});
test('Verify functionality of "clear" and "collapse pane" buttons', async ({ page }) => {
test('Ensure clear recent objects button is active or inactive', async ({ page }) => {
// Assert that the list initially contains 3 objects (clock, folder, my items)
expect(await recentObjectsList.locator('.c-recentobjects-listitem').count()).toBe(3);
@@ -331,24 +331,6 @@ test.describe('Recent Objects', () => {
expect(await page.getByRole('button', { name: 'Clear Recently Viewed' }).isEnabled()).toBe(
true
);
// Assert initial state of pane and collapse the Recent Objects panel
await expect(page.getByLabel('Expand Recently Viewed Pane')).toBeHidden();
await expect(page.getByLabel('Collapse Recently Viewed Pane')).toBeVisible();
await page.getByLabel('Collapse Recently Viewed Pane').click();
// Assert that the "Expand Recently Viewed Pane" button is visible
// and that the "Collapse Recently Viewed Pane" button is hidden
await expect(page.getByLabel('Expand Recently Viewed Pane')).toBeVisible();
await expect(page.getByLabel('Collapse Recently Viewed Pane')).toBeHidden();
// Expand the Recent Objects panel by clicking on the "Expand Recently Viewed Pane" button
await page.getByLabel('Expand Recently Viewed Pane').click();
// Assert that the "Expand Recently Viewed Pane" button is hidden
// and that the "Collapse Recently Viewed Pane" button is visible
await expect(page.getByLabel('Expand Recently Viewed Pane')).toBeHidden();
await expect(page.getByLabel('Collapse Recently Viewed Pane')).toBeVisible();
});
function assertInitialRecentObjectsListState() {

View File

@@ -48,7 +48,7 @@ test('Verify that the create button appears and that the Folder Domain Object is
await expect(page.locator(':nth-match(:text("Folder"), 2)')).toBeEnabled();
});
test('Verify that My Items Tree appears', async ({ page, openmctConfig }) => {
test('Verify that My Items Tree appears @ipad', async ({ page, openmctConfig }) => {
const { myItemsFolderName } = openmctConfig;
//Go to baseURL
await page.goto('./');

View File

@@ -109,7 +109,7 @@ test.describe('Verify tooltips', () => {
async function getToolTip(object) {
await page.locator('.c-create-button').hover();
await page.getByLabel('lad name').getByText(object.name).hover();
await page.getByRole('cell', { name: object.name }).hover();
let tooltipText = await page.locator('.c-tooltip').textContent();
return tooltipText.replace('\n', '').trim();
}

View File

@@ -40,7 +40,7 @@ test.describe('Main Tree', () => {
type: 'Folder'
});
await page.getByLabel('Show selected item in tree').click();
await page.getByTitle('Show selected item in tree').click();
const clock = await createDomainObjectWithDefaults(page, {
type: 'Clock',

View File

@@ -1,75 +0,0 @@
/*****************************************************************************
* 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 } from '../../../appActions.js';
import { expect, test } from '../../../baseFixtures.js';
// We don't need cspell to check this. It doesn't know latin.
/* cSpell:disable */
const loremIpsum = `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Molestie at elementum eu facilisis sed. Feugiat pretium nibh ipsum consequat. Amet consectetur adipiscing elit duis tristique sollicitudin nibh sit amet. Eget nullam non nisi est sit amet. A pellentesque sit amet porttitor eget dolor morbi non arcu. Ullamcorper sit amet risus nullam eget felis eget nunc. In tellus integer feugiat scelerisque varius morbi enim nunc. Ac feugiat sed lectus vestibulum mattis ullamcorper. Nulla facilisi morbi tempus iaculis urna id volutpat. Massa vitae tortor condimentum lacinia quis vel eros donec. Ornare quam viverra orci sagittis eu. Vestibulum sed arcu non odio. In egestas erat imperdiet sed euismod nisi porta lorem. Vitae auctor eu augue ut lectus arcu bibendum at. Donec adipiscing tristique risus nec feugiat in fermentum posuere urna. Velit euismod in pellentesque massa placerat duis ultricies. Nulla facilisi nullam vehicula ipsum a arcu cursus vitae. Aliquam malesuada bibendum arcu vitae elementum curabitur.
Vel eros donec ac odio tempor orci. Et netus et malesuada fames ac turpis egestas sed tempus. Turpis egestas pretium aenean pharetra magna ac placerat. Euismod elementum nisi quis eleifend. Vitae auctor eu augue ut lectus arcu. At imperdiet dui accumsan sit amet nulla facilisi. Est velit egestas dui id ornare arcu odio ut sem. Ornare arcu dui vivamus arcu felis. Luctus venenatis lectus magna fringilla. At elementum eu facilisis sed. Tristique et egestas quis ipsum suspendisse ultrices gravida dictum. Enim eu turpis egestas pretium aenean pharetra magna ac placerat. Lobortis scelerisque fermentum dui faucibus in. Tempor orci eu lobortis elementum nibh tellus molestie nunc non. Dignissim convallis aenean et tortor at risus. Enim tortor at auctor urna nunc id cursus. Libero volutpat sed cras ornare arcu dui vivamus. Scelerisque fermentum dui faucibus in ornare quam viverra.
Odio ut sem nulla pharetra. Neque vitae tempus quam pellentesque nec. A arcu cursus vitae congue mauris. Turpis nunc eget lorem dolor sed viverra ipsum nunc aliquet. Nibh tellus molestie nunc non blandit massa enim nec. Risus feugiat in ante metus dictum at tempor commodo ullamcorper. Nec tincidunt praesent semper feugiat nibh sed pulvinar proin gravida. Pulvinar elementum integer enim neque. Bibendum ut tristique et egestas. Nibh praesent tristique magna sit. Lectus magna fringilla urna porttitor. Eu non diam phasellus vestibulum lorem sed risus. Rhoncus mattis rhoncus urna neque. Rutrum tellus pellentesque eu tincidunt tortor aliquam. Pharetra convallis posuere morbi leo urna molestie at elementum. Quis commodo odio aenean sed adipiscing. Enim sit amet venenatis urna cursus eget nunc.
Enim nec dui nunc mattis. Cursus turpis massa tincidunt dui ut. Donec adipiscing tristique risus nec feugiat in. Eleifend mi in nulla posuere sollicitudin. Donec enim diam vulputate ut pharetra sit. Ultricies mi eget mauris pharetra et ultrices neque. Eros in cursus turpis massa tincidunt dui. Cursus risus at ultrices mi tempus imperdiet nulla malesuada. Morbi enim nunc faucibus a pellentesque sit. Porttitor rhoncus dolor purus non. Ac tortor vitae purus faucibus.
Proin libero nunc consequat interdum varius sit amet mattis vulputate. Metus dictum at tempor commodo ullamcorper a lacus vestibulum sed. Quisque non tellus orci ac auctor augue mauris. Id ornare arcu odio ut. Rhoncus est pellentesque elit ullamcorper dignissim. Senectus et netus et malesuada fames ac turpis egestas. Volutpat ac tincidunt vitae semper quis lectus nulla. Adipiscing elit duis tristique sollicitudin. Ipsum faucibus vitae aliquet nec ullamcorper sit. Gravida neque convallis a cras semper auctor neque vitae tempus. Porttitor leo a diam sollicitudin tempor id. Dictum non consectetur a erat nam at lectus. At volutpat diam ut venenatis tellus in. Morbi enim nunc faucibus a pellentesque sit amet. Cursus in hac habitasse platea. Sed augue lacus viverra vitae.
`;
test.describe('Inspector tests', () => {
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
});
test('Content in inspector can be scrolled to vertically', async ({ page }) => {
const folderWithOverflowingTitle = await createDomainObjectWithDefaults(page, {
type: 'Folder',
name: loremIpsum
});
await page.goto(folderWithOverflowingTitle.url);
const inspectorPropertiesLocator = page
.getByRole('tabpanel', { name: 'Inspector Views' })
.getByLabel('Inspector Properties Details');
const inspectorPropertiesList = inspectorPropertiesLocator.getByRole('list');
const firstInspectorPropertyValue = inspectorPropertiesList
.getByRole('listitem')
.first()
.getByLabel('value', { exact: false });
const lastInspectorPropertyValue = inspectorPropertiesList
.getByRole('listitem')
.last()
.getByLabel('value', { exact: false });
// inspector content partially in viewport, but not all the way in viewport
await expect(inspectorPropertiesLocator).toBeInViewport();
await expect(inspectorPropertiesLocator).not.toBeInViewport({ ratio: 0.9 });
await expect(firstInspectorPropertyValue).toBeInViewport();
await expect(lastInspectorPropertyValue).not.toBeInViewport();
// using page.mouse.wheel to scroll the inspector content by the height of the content
// because click and scrollIntoView will scroll even if scrollbar not available
await inspectorPropertiesLocator.hover();
const offset = await inspectorPropertiesLocator.evaluate((el) => el.offsetHeight);
await page.mouse.wheel(0, offset);
await expect(lastInspectorPropertyValue).toBeInViewport();
});
});

View File

@@ -1,95 +0,0 @@
/*****************************************************************************
* 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 can quickly verify that any openmct installation is
operable and that any type of testing can proceed.
Ideally, smoke tests should make zero assumptions about how and where they are run. This makes them
more resilient to change and therefor a better indicator of failure. Smoke tests will also run quickly
as they cover a very "thin surface" of functionality.
When deciding between authoring new smoke tests or functional tests, ask yourself "would I feel
comfortable running this test during a live mission?" Avoid creating or deleting Domain Objects.
Make no assumptions about the order that elements appear in the DOM.
*/
import { expect, test } from '../../pluginFixtures.js';
test.describe('Smoke tests for @mobile', () => {
test.beforeEach(async ({ page }) => {
//For now, this test is going to be hardcoded against './test-data/display_layout_with_child_layouts.json'
await page.goto('./');
});
test('Verify that My Items Tree appears @mobile', async ({ page }) => {
//My Items to be visible
await expect(page.getByRole('treeitem', { name: 'My Items' })).toBeVisible();
});
test('Verify that user can search @mobile', async ({ page }) => {
await page.getByRole('searchbox', { name: 'Search Input' }).click();
await page.getByRole('searchbox', { name: 'Search Input' }).fill('Parent Display Layout');
//Search Results appear in search modal
await expect(
page.getByLabel('Object Results').getByText('Parent Display Layout')
).toBeVisible();
//Clicking on the search result takes you to the object
await page.getByLabel('Object Results').getByText('Parent Display Layout').click();
await page.getByTitle('Collapse Browse Pane').click();
await expect(page.getByRole('main').getByText('Parent Display Layout')).toBeVisible();
});
test('Verify that user can change time conductor @mobile', async ({ page }) => {
//Collapse Browse Pane to get more Time Conductor space
await page.getByLabel('Collapse Browse Pane').click();
//Open Time Conductor and change to Real Time Mode and set offset hour by 1 hour
// Disabling line because we're intentionally obscuring the text
// eslint-disable-next-line playwright/no-force-option
await page.getByLabel('Time Conductor Mode').click({ force: true });
await page.getByLabel('Time Conductor Mode Menu').click();
await page.getByLabel('Real-Time').click();
await page.getByLabel('Start offset hours').fill('01');
await page.getByLabel('Submit time offsets').click();
await expect(page.getByLabel('Start offset: 01:30:00')).toBeVisible();
});
test('Remove Object and confirmation dialog @mobile', async ({ page }) => {
await page.getByRole('searchbox', { name: 'Search Input' }).click();
await page.getByRole('searchbox', { name: 'Search Input' }).fill('Parent Display Layout');
//Search Results appear in search modal
//Clicking on the search result takes you to the object
await page.getByLabel('Object Results').getByText('Parent Display Layout').click();
await page.getByTitle('Collapse Browse Pane').click();
await expect(page.getByRole('main').getByText('Parent Display Layout')).toBeVisible();
//Verify both objects are in view
await expect(await page.getByLabel('Child Layout 1 Layout')).toBeVisible();
await expect(await page.getByLabel('Child Layout 2 Layout')).toBeVisible();
//Remove First Object to bring up confirmation dialog
await page.getByLabel('View menu items').nth(1).click();
await page.getByLabel('Remove').click();
await page.getByRole('button', { name: 'OK' }).click();
//Verify that the object is removed
await expect(await page.getByLabel('Child Layout 1 Layout')).toBeVisible();
expect(await page.getByLabel('Child Layout 2 Layout').count()).toBe(0);
});
});

View File

@@ -178,7 +178,7 @@ test.describe('Performance tests', () => {
console.log('jpgResourceTiming ' + JSON.stringify(jpgResourceTiming));
// Click Close Icon
await page.getByRole('button', { name: 'Close' }).click();
await page.locator('[aria-label="Close"]').click();
await page.evaluate(() => window.performance.mark('view-large-close-button'));
//await client.send('HeapProfiler.enable');

View File

@@ -25,12 +25,11 @@ Tests the branding associated with the default deployment. At least the about mo
*/
import percySnapshot from '@percy/playwright';
import { fileURLToPath } from 'url';
import { expect, test } from '../../../avpFixtures.js';
import { VISUAL_URL } from '../../../constants.js';
//Declare the component scope of the visual test for Percy
//Declare the scope of the visual test
const header = '.l-shell__head';
test.describe('Visual - Header @a11y', () => {
@@ -69,34 +68,14 @@ test.describe('Visual - Header @a11y', () => {
});
test('show snapshot button', async ({ page, theme }) => {
await page.getByLabel('Open the Notebook Snapshot Menu').click();
await page.getByLabel('Take a Notebook Snapshot').click();
await page.getByRole('menuitem', { name: 'Save to Notebook Snapshots' }).click();
await percySnapshot(page, `Notebook Snapshot Show button (theme: '${theme}')`, {
scope: header
});
await expect(page.getByLabel('Show Snapshots')).toBeVisible();
});
});
//Header test with all mission status options. Right now, this is just Mission Status, but should grow over time
test.describe('Mission Header @a11y', () => {
test.beforeEach(async ({ page }) => {
await page.addInitScript({
path: fileURLToPath(new URL('../../../helper/addInitExampleUser.js', import.meta.url))
});
await page.goto('./', { waitUntil: 'domcontentloaded' });
await expect(page.getByText('Select Role')).toBeVisible();
// set role
await page.getByRole('button', { name: 'Select', exact: true }).click();
// dismiss role confirmation popup
await page.getByRole('button', { name: 'Dismiss' }).click();
});
test('Mission status panel', async ({ page, theme }) => {
await percySnapshot(page, `Header default with Mission Header (theme: '${theme}')`, {
scope: header
});
await expect(await page.getByLabel('Show Snapshots')).toBeVisible();
});
});
// Skipping for https://github.com/nasa/openmct/issues/7421

View File

@@ -28,7 +28,7 @@ import { MISSION_TIME, VISUAL_URL } from '../../../constants.js';
//Declare the scope of the visual test
const inspectorPane = '.l-shell__pane-inspector';
test.describe('Visual - Inspector @ally @clock', () => {
test.describe('Visual - Inspector @ally', () => {
test.beforeEach(async ({ page }) => {
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
});

View File

@@ -30,7 +30,7 @@ import percySnapshot from '@percy/playwright';
import { MISSION_TIME, VISUAL_URL } from '../../constants.js';
import { expect, test } from '../../pluginFixtures.js';
test.describe('Visual - Controlled Clock @clock', () => {
test.describe('Visual - Controlled Clock', () => {
test.beforeEach(async ({ page }) => {
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
});

View File

@@ -93,14 +93,4 @@ test.describe('Visual - Display Layout', () => {
await page.getByLabel('Parent Layout Layout', { exact: true }).click();
await percySnapshot(page, `Parent outer layout selected (theme: '${theme}')`);
});
test('Toolbar does not overflow into inspector', async ({ page, theme }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7036'
});
await page.getByLabel('Expand Inspect Pane').click();
await page.getByLabel('Resize Inspect Pane').dragTo(page.getByLabel('X:'));
await percySnapshot(page, `Toolbar does not overflow into inspector (theme: '${theme}')`);
});
});

View File

@@ -20,26 +20,18 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import percySnapshot from '@percy/playwright';
import { fileURLToPath } from 'url';
import {
acknowledgeFault,
changeViewTo,
navigateToFaultManagementWithoutExample,
navigateToFaultManagementWithStaticExample,
openFaultRowMenu,
selectFaultItem,
shelveFault
} from '../../helper/faultUtils.js';
import * as utils from '../../helper/faultUtils.js';
import { expect, test } from '../../pluginFixtures.js';
test.describe('Fault Management Visual Tests - without example', () => {
test.beforeEach(async ({ page }) => {
await navigateToFaultManagementWithoutExample(page);
await page.getByLabel('Collapse Inspect Pane').click();
await page.getByLabel('Click to collapse items').click();
});
test.describe('Fault Management Visual Tests', () => {
test('icon test', async ({ page, theme }) => {
await page.addInitScript({
path: fileURLToPath(new URL('../../helper/addInitFaultManagementPlugin.js', import.meta.url))
});
await page.goto('./', { waitUntil: 'domcontentloaded' });
test('fault management icon appears in tree', async ({ page, theme }) => {
// Wait for status bar to load
await expect(
page.getByRole('status', {
@@ -59,20 +51,14 @@ test.describe('Fault Management Visual Tests - without example', () => {
await percySnapshot(page, `Fault Management icon appears in tree (theme: '${theme}')`);
});
});
test.describe('Fault Management Visual Tests', () => {
test.beforeEach(async ({ page }) => {
await navigateToFaultManagementWithStaticExample(page);
await page.getByLabel('Collapse Inspect Pane').click();
await page.getByLabel('Click to collapse items').click();
});
test('fault list and acknowledged faults', async ({ page, theme }) => {
await utils.navigateToFaultManagementWithStaticExample(page);
await percySnapshot(page, `Shows a list of faults in the standard view (theme: '${theme}')`);
await acknowledgeFault(page, 1);
await changeViewTo(page, 'acknowledged');
await utils.acknowledgeFault(page, 1);
await utils.changeViewTo(page, 'acknowledged');
await percySnapshot(
page,
@@ -81,12 +67,14 @@ test.describe('Fault Management Visual Tests', () => {
});
test('shelved faults', async ({ page, theme }) => {
await shelveFault(page, 1);
await changeViewTo(page, 'shelved');
await utils.navigateToFaultManagementWithStaticExample(page);
await utils.shelveFault(page, 1);
await utils.changeViewTo(page, 'shelved');
await percySnapshot(page, `Shelved faults appear in the shelved view (theme: '${theme}')`);
await openFaultRowMenu(page, 1);
await utils.openFaultRowMenu(page, 1);
await percySnapshot(
page,
@@ -95,7 +83,9 @@ test.describe('Fault Management Visual Tests', () => {
});
test('3-dot menu for fault', async ({ page, theme }) => {
await openFaultRowMenu(page, 1);
await utils.navigateToFaultManagementWithStaticExample(page);
await utils.openFaultRowMenu(page, 1);
await percySnapshot(
page,
@@ -104,7 +94,9 @@ test.describe('Fault Management Visual Tests', () => {
});
test('ability to acknowledge or shelve', async ({ page, theme }) => {
await selectFaultItem(page, 1);
await utils.navigateToFaultManagementWithStaticExample(page);
await utils.selectFaultItem(page, 1);
await percySnapshot(
page,

View File

@@ -23,7 +23,6 @@
import percySnapshot from '@percy/playwright';
import { createDomainObjectWithDefaults, setRealTimeMode } from '../../appActions.js';
import { waitForAnimations } from '../../baseFixtures.js';
import { VISUAL_URL } from '../../constants.js';
import { expect, test } from '../../pluginFixtures.js';
@@ -76,10 +75,11 @@ test.describe('Visual - Example Imagery', () => {
await page.goto(exampleImagery.url, { waitUntil: 'domcontentloaded' });
await setRealTimeMode(page, true);
//Temporary to close the dialog
await page.getByLabel('Submit time offsets').click();
await expect(page.getByLabel('Image Wrapper')).toBeVisible();
await waitForAnimations(page.locator('.animate-scroll'));
await percySnapshot(page, `Example Imagery in Real Time (theme: ${theme})`);
});

View File

@@ -32,12 +32,12 @@ test.describe('Mission Status Visual Tests @a11y', () => {
});
await page.goto('./', { waitUntil: 'domcontentloaded' });
await expect(page.getByText('Select Role')).toBeVisible();
// Description should be empty https://github.com/nasa/openmct/issues/6978
await expect(page.locator('c-message__action-text')).toBeHidden();
// set role
await page.getByRole('button', { name: 'Select', exact: true }).click();
// dismiss role confirmation popup
await page.getByRole('button', { name: 'Dismiss' }).click();
await page.getByLabel('Collapse Inspect Pane').click();
await page.getByLabel('Collapse Browse Pane').click();
});
test('Mission status panel', async ({ page, theme }) => {
await page.getByLabel('Toggle Mission Status Panel').click();

View File

@@ -23,7 +23,7 @@
import percySnapshot from '@percy/playwright';
import { createDomainObjectWithDefaults, expandTreePaneItemByName } from '../../appActions.js';
import { expect, test } from '../../avpFixtures.js';
import { test } from '../../avpFixtures.js';
import { VISUAL_URL } from '../../constants.js';
import { enterTextEntry, startAndAddRestrictedNotebookObject } from '../../helper/notebookUtils.js';
@@ -39,44 +39,6 @@ test.describe('Visual - Restricted Notebook @a11y', () => {
});
});
test.describe('Visual - Notebook Snapshot @a11y', () => {
test.beforeEach(async ({ page }) => {
await page.goto('./?hideTree=true&hideInspector=true', { waitUntil: 'domcontentloaded' });
});
test('Visual check for Snapshot Annotation', async ({ page, theme }) => {
await page.getByLabel('Open the Notebook Snapshot Menu').click();
await page.getByRole('menuitem', { name: 'Save to Notebook Snapshots' }).click();
await page.getByLabel('Show Snapshots').click();
await page.getByLabel('My Items Notebook Embed').getByLabel('More actions').click();
await page.getByRole('menuitem', { name: 'View Snapshot' }).click();
await page.getByLabel('Annotate this snapshot').click();
await expect(page.locator('#snap-annotation-canvas')).toBeVisible();
// Clear the canvas
await page.getByRole('button', { name: 'Put text [T]' }).click();
// Click in the Painterro canvas to add a text annotation
await page.locator('.ptro-crp-el').click();
await page.locator('.ptro-text-tool-input').fill('...is there life on mars?');
await percySnapshot(page, `Notebook Snapshot with text entry open (theme: '${theme}')`);
// When working with Painterro, we need to check that the Apply button is hidden after clicking
await page.getByTitle('Apply').click();
await expect(page.getByTitle('Apply')).toBeHidden();
// Save and exit annotation window
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('button', { name: 'Done' }).click();
// Open up annotation again
await page.getByRole('img', { name: 'My Items thumbnail' }).click();
await expect(page.getByLabel('Modal Overlay').getByRole('img')).toBeVisible();
// Take a snapshot
await percySnapshot(page, `Notebook Snapshot with annotation (theme: '${theme}')`);
});
});
test.describe('Visual - Notebook @a11y', () => {
let notebook;
test.beforeEach(async ({ page }) => {

View File

@@ -26,41 +26,13 @@ import fs from 'fs';
import { createDomainObjectWithDefaults, createPlanFromJSON } from '../../appActions.js';
import { test } from '../../avpFixtures.js';
import { VISUAL_URL } from '../../constants.js';
import {
createTimelistWithPlanAndSetActivityInProgress,
getFirstActivity,
setBoundsToSpanAllActivities,
setDraftStatusForPlan
} from '../../helper/planningUtils.js';
import { setBoundsToSpanAllActivities, setDraftStatusForPlan } from '../../helper/planningUtils.js';
const examplePlanSmall1 = JSON.parse(
fs.readFileSync(new URL('../../test-data/examplePlans/ExamplePlan_Small1.json', import.meta.url))
);
const examplePlanSmall2 = JSON.parse(
const examplePlanSmall = JSON.parse(
fs.readFileSync(new URL('../../test-data/examplePlans/ExamplePlan_Small2.json', import.meta.url))
);
test.describe('Visual - Timelist progress bar @clock', () => {
const firstActivity = getFirstActivity(examplePlanSmall1);
test.use({
clockOptions: {
now: firstActivity.end + 10000,
shouldAdvanceTime: true
}
});
test.beforeEach(async ({ page }) => {
await createTimelistWithPlanAndSetActivityInProgress(page, examplePlanSmall1);
await page.getByLabel('Click to collapse items').click();
});
test('progress pie is full', async ({ page, theme }) => {
// Progress pie is completely full and doesn't update if now is greater than the end time
await percySnapshot(page, `Time List with Activity in Progress (theme: ${theme})`);
});
});
const snapshotScope = '.l-shell__pane-main .l-pane__contents';
test.describe('Visual - Planning', () => {
test.beforeEach(async ({ page }) => {
@@ -70,59 +42,42 @@ test.describe('Visual - Planning', () => {
test('Plan View', async ({ page, theme }) => {
const plan = await createPlanFromJSON(page, {
name: 'Plan Visual Test',
json: examplePlanSmall2
});
await setBoundsToSpanAllActivities(page, examplePlanSmall2, plan.url);
await percySnapshot(page, `Plan View (theme: ${theme})`);
});
test('Resize Plan View @2p', async ({ browser, theme }) => {
// need to set viewport to null to allow for resizing
const newContext = await browser.newContext({
viewport: null
});
const newPage = await newContext.newPage();
await newPage.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
const plan = await createPlanFromJSON(newPage, {
name: 'Plan Visual Test',
json: examplePlanSmall2
json: examplePlanSmall
});
await setBoundsToSpanAllActivities(newPage, examplePlanSmall2, plan.url);
// resize the window
await newPage.setViewportSize({ width: 800, height: 600 });
await percySnapshot(newPage, `Plan View resized (theme: ${theme})`);
await setBoundsToSpanAllActivities(page, examplePlanSmall, plan.url);
await percySnapshot(page, `Plan View (theme: ${theme})`, {
scope: snapshotScope
});
});
test('Plan View w/ draft status', async ({ page, theme }) => {
const plan = await createPlanFromJSON(page, {
name: 'Plan Visual Test (Draft)',
json: examplePlanSmall2
json: examplePlanSmall
});
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
await setDraftStatusForPlan(page, plan);
await setBoundsToSpanAllActivities(page, examplePlanSmall2, plan.url);
await percySnapshot(page, `Plan View w/ draft status (theme: ${theme})`);
await setBoundsToSpanAllActivities(page, examplePlanSmall, plan.url);
await percySnapshot(page, `Plan View w/ draft status (theme: ${theme})`, {
scope: snapshotScope
});
});
});
test.describe('Visual - Gantt Chart', () => {
test.beforeEach(async ({ page }) => {
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
});
test('Gantt Chart View', async ({ page, theme }) => {
const ganttChart = await createDomainObjectWithDefaults(page, {
type: 'Gantt Chart',
name: 'Gantt Chart Visual Test'
});
await createPlanFromJSON(page, {
json: examplePlanSmall2,
json: examplePlanSmall,
parent: ganttChart.uuid
});
await setBoundsToSpanAllActivities(page, examplePlanSmall2, ganttChart.url);
await percySnapshot(page, `Gantt Chart View (theme: ${theme}) - Clipped Activity Names`);
await setBoundsToSpanAllActivities(page, examplePlanSmall, ganttChart.url);
await percySnapshot(page, `Gantt Chart View (theme: ${theme}) - Clipped Activity Names`, {
scope: snapshotScope
});
// Expand the inspect pane and uncheck the 'Clip Activity Names' option
await page.getByRole('button', { name: 'Expand Inspect Pane' }).click();
@@ -138,7 +93,9 @@ test.describe('Visual - Gantt Chart', () => {
// Dismiss the notification
await page.getByLabel('Dismiss').click();
await percySnapshot(page, `Gantt Chart View (theme: ${theme}) - Unclipped Activity Names`);
await percySnapshot(page, `Gantt Chart View (theme: ${theme}) - Unclipped Activity Names`, {
scope: snapshotScope
});
});
test('Gantt Chart View w/ draft status', async ({ page, theme }) => {
@@ -147,7 +104,7 @@ test.describe('Visual - Gantt Chart', () => {
name: 'Gantt Chart Visual Test (Draft)'
});
const plan = await createPlanFromJSON(page, {
json: examplePlanSmall2,
json: examplePlanSmall,
parent: ganttChart.uuid
});
@@ -155,8 +112,10 @@ test.describe('Visual - Gantt Chart', () => {
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
await setBoundsToSpanAllActivities(page, examplePlanSmall2, ganttChart.url);
await percySnapshot(page, `Gantt Chart View w/ draft status (theme: ${theme})`);
await setBoundsToSpanAllActivities(page, examplePlanSmall, ganttChart.url);
await percySnapshot(page, `Gantt Chart View w/ draft status (theme: ${theme})`, {
scope: snapshotScope
});
// Expand the inspect pane and uncheck the 'Clip Activity Names' option
await page.getByRole('button', { name: 'Expand Inspect Pane' }).click();
@@ -174,12 +133,14 @@ test.describe('Visual - Gantt Chart', () => {
await percySnapshot(
page,
`Gantt Chart View w/ draft status (theme: ${theme}) - Unclipped Activity Names`
`Gantt Chart View w/ draft status (theme: ${theme}) - Unclipped Activity Names`,
{
scope: snapshotScope
}
);
});
// Skipping for https://github.com/nasa/openmct/issues/7421
// test.afterEach(async ({ page }, testInfo) => {
// await scanForA11yViolations(page, testInfo.title);
// });
});
// Skipping for https://github.com/nasa/openmct/issues/7421
// test.afterEach(async ({ page }, testInfo) => {
// await scanForA11yViolations(page, testInfo.title);
// });

View File

@@ -91,7 +91,7 @@ test.describe('Flexible Layout styling @a11y', () => {
setBorderColor,
setBackgroundColor,
setTextColor,
page.getByRole('group', { name: 'StackedPlot1 Frame' })
page.getByLabel('StackedPlot1 Frame')
);
await percySnapshot(

View File

@@ -53,11 +53,11 @@ test.describe('Visual - Telemetry Views', () => {
await page.goto(telemetry.url, { waitUntil: 'domcontentloaded' });
//Click this button to see telemetry display options
await page.getByLabel('Open the View Switcher Menu').click();
await page.getByRole('button', { name: 'Plot' }).click();
await page.getByLabel('Telemetry Table').click();
//Get Table View in place
await expect(page.getByLabel('Expand Columns')).toBeInViewport();
expect(await page.getByLabel('Expand Columns')).toBeInViewport();
await percySnapshot(page, `Default Telemetry Table View (theme: ${theme})`);

View File

@@ -55,7 +55,6 @@
</template>
<script>
const ONE_HOUR = 60 * 60 * 1000;
export default {
inject: ['openmct', 'domainObject'],
data() {
@@ -78,10 +77,6 @@ export default {
selectItem(item, event) {
event.stopPropagation();
const bounds = this.openmct.time.getBounds();
const otherBounds = {
start: bounds.start - ONE_HOUR,
end: bounds.end + ONE_HOUR
};
const selection = [
{
element: this.$el,
@@ -93,9 +88,6 @@ export default {
icon: item.type.cssClass
},
dataRanges: [
{
bounds: otherBounds
},
{
bounds
}

View File

@@ -20,13 +20,13 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { acknowledgeFault, randomFaults, shelveFault } from './utils.js';
import utils from './utils.js';
export default function (staticFaults = false) {
return function install(openmct) {
openmct.install(openmct.plugins.FaultManagement());
const faultsData = randomFaults(staticFaults);
const faultsData = utils.randomFaults(staticFaults);
openmct.faults.addProvider({
request(domainObject, options) {
@@ -44,14 +44,14 @@ export default function (staticFaults = false) {
return domainObject.type === 'faultManagement';
},
acknowledgeFault(fault, { comment = '' }) {
acknowledgeFault(fault);
utils.acknowledgeFault(fault);
return Promise.resolve({
success: true
});
},
shelveFault(fault, duration) {
shelveFault(fault, duration);
utils.shelveFault(fault, duration);
return Promise.resolve({
success: true

View File

@@ -43,7 +43,7 @@ const getRandom = {
}
};
export function shelveFault(
function shelveFault(
fault,
opts = {
shelved: true,
@@ -58,11 +58,11 @@ export function shelveFault(
}, opts.shelveDuration);
}
export function acknowledgeFault(fault) {
function acknowledgeFault(fault) {
fault.acknowledged = true;
}
export function randomFaults(staticFaults, count = 5) {
function randomFaults(staticFaults, count = 5) {
let faults = [];
for (let x = 1, y = count + 1; x < y; x++) {
@@ -71,3 +71,9 @@ export function randomFaults(staticFaults, count = 5) {
return faults;
}
export default {
randomFaults,
shelveFault,
acknowledgeFault
};

View File

@@ -19,7 +19,7 @@
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />

View File

@@ -47,7 +47,7 @@ if (document.currentScript) {
* @property {*} inspectorViews
* @property {*} propertyEditors
* @property {*} toolbars
* @property {import('./src/api/types/TypeRegistry').default} types
* @property {*} types
* @property {import('./src/api/objects/ObjectAPI').default} objects
* @property {import('./src/api/telemetry/TelemetryAPI').default} telemetry
* @property {import('./src/api/indicators/IndicatorAPI').default} indicators
@@ -67,7 +67,6 @@ if (document.currentScript) {
* @property {import('./src/api/annotation/AnnotationAPI').default} annotation
* @property {{(plugin: OpenMCTPlugin) => void}} install
* @property {{() => string}} getAssetPath
* @property {{(assetPath: string) => void}} setAssetPath
* @property {{(domElement: HTMLElement, isHeadlessMode: boolean) => void}} start
* @property {{() => void}} startHeadless
* @property {{() => void}} destroy

12284
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,33 +1,31 @@
{
"name": "openmct",
"version": "4.0.0-rc1",
"version": "4.0.0-next",
"description": "The Open MCT core platform",
"type": "module",
"main": "dist/openmct.js",
"devDependencies": {
"@axe-core/playwright": "4.8.5",
"@axe-core/playwright": "4.8.2",
"@babel/eslint-parser": "7.23.3",
"@braintree/sanitize-url": "6.0.4",
"@percy/cli": "1.27.4",
"@percy/playwright": "1.0.4",
"@playwright/test": "1.39.0",
"@types/d3-axis": "3.0.6",
"@types/d3-shape": "3.0.0",
"@types/d3-scale": "4.0.8",
"@types/d3-selection": "3.0.10",
"@types/eventemitter3": "1.2.0",
"@types/jasmine": "5.1.2",
"@types/lodash": "4.17.0",
"@types/lodash": "4.14.192",
"@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",
"copy-webpack-plugin": "11.0.0",
"cspell": "7.3.8",
"css-loader": "6.10.0",
"css-loader": "6.8.1",
"d3-axis": "3.0.0",
"d3-shape": "3.0.0",
"d3-scale": "4.0.2",
"d3-selection": "3.0.0",
"eslint": "8.56.0",
@@ -38,14 +36,14 @@
"eslint-plugin-prettier": "5.1.3",
"eslint-plugin-simple-import-sort": "10.0.0",
"eslint-plugin-unicorn": "49.0.0",
"eslint-plugin-vue": "9.22.0",
"eslint-plugin-vue": "9.18.1",
"eslint-plugin-you-dont-need-lodash-underscore": "6.13.0",
"eventemitter3": "1.2.0",
"file-saver": "2.0.5",
"flatbush": "4.2.0",
"git-rev-sync": "3.0.2",
"html2canvas": "1.4.1",
"imports-loader": "5.0.0",
"imports-loader": "4.0.1",
"jasmine-core": "5.1.1",
"karma": "6.4.2",
"karma-chrome-launcher": "3.2.0",
@@ -56,42 +54,42 @@
"karma-junit-reporter": "2.0.1",
"karma-sourcemap-loader": "0.4.0",
"karma-spec-reporter": "0.0.36",
"karma-webpack": "5.0.1",
"karma-webpack": "5.0.0",
"location-bar": "3.0.1",
"lodash": "4.17.21",
"marked": "12.0.0",
"marked": "11.2.0",
"mini-css-extract-plugin": "2.7.6",
"moment": "2.30.1",
"moment-duration-format": "2.3.2",
"moment-timezone": "0.5.41",
"npm-run-all2": "6.1.2",
"npm-run-all2": "6.1.1",
"nyc": "15.1.0",
"painterro": "1.2.87",
"plotly.js-basic-dist-min": "2.29.1",
"plotly.js-basic-dist-min": "2.20.0",
"plotly.js-gl2d-dist-min": "2.20.0",
"prettier": "3.2.5",
"prettier-eslint": "16.3.0",
"printj": "1.3.1",
"resolve-url-loader": "5.0.0",
"sanitize-html": "2.12.1",
"sass": "1.71.1",
"sass-loader": "14.1.1",
"sanitize-html": "2.11.0",
"sass": "1.68.0",
"sass-loader": "14.0.0",
"sinon": "17.0.0",
"style-loader": "3.3.3",
"terser-webpack-plugin": "5.3.9",
"tiny-emitter": "2.1.0",
"typescript": "5.3.3",
"uuid": "9.0.1",
"vue": "3.4.19",
"vue-eslint-parser": "9.4.2",
"vue": "3.3.8",
"vue-eslint-parser": "9.3.2",
"vue-loader": "16.8.3",
"webpack": "5.90.3",
"webpack": "5.89.0",
"webpack-cli": "5.1.1",
"webpack-dev-server": "5.0.2",
"webpack-dev-server": "4.15.1",
"webpack-merge": "5.10.0"
},
"scripts": {
"clean": "rm -rf ./dist ./node_modules ./coverage ./html-test-results ./test-results ./.nyc_output ",
"clean": "rm -rf ./dist ./node_modules ./package-lock.json ./coverage ./html-test-results ./test-results ./.nyc_output ",
"start": "npx webpack serve --config ./.webpack/webpack.dev.js",
"start:prod": "npx webpack serve --config ./.webpack/webpack.prod.js",
"start:coverage": "npx webpack serve --config ./.webpack/webpack.coverage.js",
@@ -109,7 +107,6 @@
"test:debug": "KARMA_DEBUG=true karma start karma.conf.cjs",
"test:e2e": "npx playwright test",
"test:e2e:a11y": "npx playwright test --config=e2e/playwright-visual-a11y.config.js --project=chrome --grep @a11y",
"test:e2e:mobile": "npx playwright test --config=e2e/playwright-mobile.config.js",
"test:e2e:couchdb": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @couchdb --workers=1",
"test:e2e:stable": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep-invert \"@unstable|@couchdb|@generatedata\"",
"test:e2e:unstable": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @unstable",
@@ -156,4 +153,4 @@
"keywords": [
"nasa"
]
}
}

View File

@@ -21,7 +21,7 @@
*****************************************************************************/
import _ from 'lodash';
import { makeKeyString, parseKeyString } from '../objects/object-utils.js';
import objectUtils from '../objects/object-utils.js';
/**
* @typedef {import('../objects/ObjectAPI').DomainObject} DomainObject
@@ -223,18 +223,18 @@ export default class CompositionProvider {
* @param {DomainObject} oldDomainObject
*/
#onMutation(newDomainObject, oldDomainObject) {
const id = makeKeyString(oldDomainObject.identifier);
const id = objectUtils.makeKeyString(oldDomainObject.identifier);
const listeners = this.#listeningTo[id];
if (!listeners) {
return;
}
const oldComposition = oldDomainObject.composition.map(makeKeyString);
const newComposition = newDomainObject.composition.map(makeKeyString);
const oldComposition = oldDomainObject.composition.map(objectUtils.makeKeyString);
const newComposition = newDomainObject.composition.map(objectUtils.makeKeyString);
const added = _.difference(newComposition, oldComposition).map(parseKeyString);
const removed = _.difference(oldComposition, newComposition).map(parseKeyString);
const added = _.difference(newComposition, oldComposition).map(objectUtils.parseKeyString);
const removed = _.difference(oldComposition, newComposition).map(objectUtils.parseKeyString);
function notify(value) {
return function (listener) {

View File

@@ -21,7 +21,7 @@
*****************************************************************************/
import { toRaw } from 'vue';
import { makeKeyString } from '../objects/object-utils.js';
import objectUtils from '../objects/object-utils.js';
import CompositionProvider from './CompositionProvider.js';
/**
@@ -91,7 +91,7 @@ export default class DefaultCompositionProvider extends CompositionProvider {
this.establishTopicListener();
/** @type {string} */
const keyString = makeKeyString(domainObject.identifier);
const keyString = objectUtils.makeKeyString(domainObject.identifier);
let objectListeners = this.listeningTo[keyString];
if (!objectListeners) {
@@ -120,7 +120,7 @@ export default class DefaultCompositionProvider extends CompositionProvider {
*/
off(domainObject, event, callback, context) {
/** @type {string} */
const keyString = makeKeyString(domainObject.identifier);
const keyString = objectUtils.makeKeyString(domainObject.identifier);
const objectListeners = this.listeningTo[keyString];
const index = objectListeners[event].findIndex((l) => {
@@ -228,7 +228,7 @@ export default class DefaultCompositionProvider extends CompositionProvider {
this.publicAPI.objects.mutate(domainObject, 'composition', newComposition);
/** @type {string} */
let id = makeKeyString(domainObject.identifier);
let id = objectUtils.makeKeyString(domainObject.identifier);
const listeners = this.listeningTo[id];
if (!listeners) {

View File

@@ -29,13 +29,10 @@
:key="action.name"
role="menuitem"
:aria-disabled="action.isDisabled"
:aria-label="action.name"
aria-describedby="item-description"
:class="action.cssClass"
:aria-label="action.name"
:title="action.description"
@click="action.onItemClicked"
@mouseover="toggleItem(action)"
@mouseleave="toggleItem()"
>
{{ action.name }}
</li>
@@ -55,23 +52,16 @@
v-for="action in options.actions"
:key="action.name"
role="menuitem"
aria-describedby="item-description"
:aria-disabled="action.isDisabled"
:class="action.cssClass"
:aria-label="action.name"
:title="action.description"
@click="action.onItemClicked"
@mouseover="toggleItem(action)"
@mouseleave="toggleItem()"
>
{{ action.name }}
</li>
<li v-if="options.actions.length === 0">No actions defined.</li>
</ul>
<div v-if="hoveredItem" id="item-description" class="visually-hidden" aria-live="polite">
<span v-if="hoveredItem.name">{{ hoveredItem.name }}</span>
<span v-if="hoveredItem.description">: {{ hoveredItem.description }}</span>
</div>
</div>
</template>
@@ -80,21 +70,11 @@ import popupMenuMixin from '../mixins/popupMenuMixin.js';
export default {
mixins: [popupMenuMixin],
inject: ['options'],
data() {
return {
hoveredItem: null
};
},
computed: {
optionsLabel() {
const label = this.options.label ? `${this.options.label} Context Menu` : 'Context Menu';
const label = this.options.label ? `${this.options.label} Menu` : 'Menu';
return label;
}
},
methods: {
toggleItem(action) {
this.hoveredItem = action ?? null;
}
}
};
</script>

View File

@@ -38,8 +38,8 @@
:key="action.name"
role="menuitem"
:aria-disabled="action.isDisabled"
aria-describedby="item-description"
:class="action.cssClass"
:title="action.description"
@click="action.onItemClicked"
@mouseover="toggleItemDescription(action)"
@mouseleave="toggleItemDescription()"
@@ -64,7 +64,7 @@
role="menuitem"
:class="action.cssClass"
:aria-label="action.name"
aria-describedby="item-description"
:title="action.description"
@click="action.onItemClicked"
@mouseover="toggleItemDescription(action)"
@mouseleave="toggleItemDescription()"
@@ -74,13 +74,13 @@
<li v-if="options.actions.length === 0">No actions defined.</li>
</ul>
<div aria-live="polite" class="c-super-menu__item-description">
<div :class="itemDescriptionIconClass"></div>
<div class="c-super-menu__item-description">
<div :class="['l-item-description__icon', 'bg-' + hoveredItem.cssClass]"></div>
<div class="l-item-description__name">
{{ hoveredItemName }}
{{ hoveredItem.name }}
</div>
<div id="item-description" class="l-item-description__description">
{{ hoveredItemDescription }}
<div class="l-item-description__description">
{{ hoveredItem.description }}
</div>
</div>
</div>
@@ -90,39 +90,26 @@ import popupMenuMixin from '../mixins/popupMenuMixin.js';
export default {
mixins: [popupMenuMixin],
inject: ['options'],
data() {
data: function () {
return {
hoveredItem: null
hoveredItem: {}
};
},
computed: {
optionsLabel() {
const label = this.options.label ? `${this.options.label} Super Menu` : 'Super Menu';
return label;
},
itemDescriptionIconClass() {
const iconClass = ['l-item-description__icon'];
if (this.hoveredItem) {
iconClass.push('bg-' + this.hoveredItem.cssClass);
}
return iconClass;
},
hoveredItemName() {
return this.hoveredItem?.name ?? '';
},
hoveredItemDescription() {
return this.hoveredItem?.description ?? '';
}
},
methods: {
toggleItemDescription(action = null) {
toggleItemDescription(action = {}) {
const hoveredItem = {
name: action?.name,
description: action?.description,
cssClass: action?.cssClass
name: action.name,
description: action.description,
cssClass: action.cssClass
};
this.hoveredItem = hoveredItem;
this.hoveredItem = Object.assign({}, this.hoveredItem, hoveredItem);
}
}
};

View File

@@ -22,7 +22,7 @@
import EventEmitter from 'EventEmitter';
import _ from 'lodash';
import { makeKeyString, refresh } from './object-utils.js';
import utils from './object-utils.js';
const ANY_OBJECT_EVENT = 'mutation';
@@ -152,7 +152,7 @@ class MutableDomainObject {
mutable.$observe('$_synchronize_model', (updatedObject) => {
let clone = JSON.parse(JSON.stringify(updatedObject));
refresh(mutable, clone);
utils.refresh(mutable, clone);
});
return mutable;
@@ -168,7 +168,7 @@ class MutableDomainObject {
}
function qualifiedEventName(object, eventName) {
let keystring = makeKeyString(object.identifier);
let keystring = utils.makeKeyString(object.identifier);
return [keystring, eventName].join(':');
}

View File

@@ -21,7 +21,7 @@
*****************************************************************************/
import EventEmitter from 'EventEmitter';
import { identifierEquals, makeKeyString, parseKeyString, refresh } from 'objectUtils';
import utils from 'objectUtils';
import ConflictError from './ConflictError.js';
import InMemorySearchProvider from './InMemorySearchProvider.js';
@@ -82,19 +82,8 @@ import Transaction from './Transaction.js';
* @memberof module:openmct
*/
export default class ObjectAPI {
#makeKeyString;
#parseKeyString;
#identifierEquals;
#refresh;
#openmct;
constructor(typeRegistry, openmct) {
this.#makeKeyString = makeKeyString;
this.#parseKeyString = parseKeyString;
this.#identifierEquals = identifierEquals;
this.#refresh = refresh;
this.#openmct = openmct;
this.openmct = openmct;
this.typeRegistry = typeRegistry;
this.SEARCH_TYPES = Object.freeze({
OBJECTS: 'OBJECTS',
@@ -217,14 +206,14 @@ export default class ObjectAPI {
* has been saved, or be rejected if it cannot be saved
*/
get(identifier, abortSignal, forceRemote = false) {
let keystring = this.#makeKeyString(identifier);
let keystring = this.makeKeyString(identifier);
if (!forceRemote) {
if (this.cache[keystring] !== undefined) {
return this.cache[keystring];
}
identifier = parseKeyString(identifier);
identifier = utils.parseKeyString(identifier);
if (this.isTransactionActive()) {
let dirtyObject = this.transaction.getDirtyObject(identifier);
@@ -238,7 +227,7 @@ export default class ObjectAPI {
const provider = this.getProvider(identifier);
if (!provider) {
throw new Error(`No Provider Matched for keyString "${this.#makeKeyString(identifier)}"`);
throw new Error(`No Provider Matched for keyString "${this.makeKeyString(identifier)}"`);
}
if (!provider.get) {
@@ -249,7 +238,7 @@ export default class ObjectAPI {
.get(identifier, abortSignal)
.then((domainObject) => {
delete this.cache[keystring];
if (!domainObject && abortSignal?.aborted) {
if (!domainObject && abortSignal.aborted) {
// we've aborted the request
return;
}
@@ -336,7 +325,7 @@ export default class ObjectAPI {
*/
getMutable(identifier) {
if (!this.supportsMutation(identifier)) {
throw new Error(`Object "${this.#makeKeyString(identifier)}" does not support mutation.`);
throw new Error(`Object "${this.makeKeyString(identifier)}" does not support mutation.`);
}
return this.get(identifier).then((object) => {
@@ -363,7 +352,7 @@ export default class ObjectAPI {
}
isPersistable(idOrKeyString) {
let identifier = parseKeyString(idOrKeyString);
let identifier = utils.parseKeyString(idOrKeyString);
let provider = this.getProvider(identifier);
if (provider?.isReadOnly) {
return !provider.isReadOnly();
@@ -373,7 +362,7 @@ export default class ObjectAPI {
}
isMissing(domainObject) {
let identifier = makeKeyString(domainObject.identifier);
let identifier = utils.makeKeyString(domainObject.identifier);
let missingName = 'Missing: ' + identifier;
return domainObject.name === missingName;
@@ -453,21 +442,21 @@ export default class ObjectAPI {
if (error instanceof this.errors.Conflict) {
// Synchronized objects will resolve their own conflicts
if (this.SYNCHRONIZED_OBJECT_TYPES.includes(domainObject.type)) {
this.#openmct.notifications.info(
`Conflict detected while saving "${this.#makeKeyString(
this.openmct.notifications.info(
`Conflict detected while saving "${this.makeKeyString(
domainObject.name
)}", attempting to resolve`
);
} else {
this.#openmct.notifications.error(
`Conflict detected while saving ${this.#makeKeyString(domainObject.identifier)}`
this.openmct.notifications.error(
`Conflict detected while saving ${this.makeKeyString(domainObject.identifier)}`
);
if (this.isTransactionActive()) {
this.endTransaction();
}
await this.#refresh(domainObject);
await this.refresh(domainObject);
}
}
@@ -476,7 +465,7 @@ export default class ObjectAPI {
}
async #getCurrentUsername() {
const user = await this.#openmct.user.getCurrentUser();
const user = await this.openmct.user.getCurrentUser();
let username;
if (user !== undefined) {
@@ -565,7 +554,7 @@ export default class ObjectAPI {
*/
getRelativePath(objectPath) {
return objectPath
.map((p) => this.#makeKeyString(p.identifier))
.map((p) => this.makeKeyString(p.identifier))
.reverse()
.join('/');
}
@@ -585,13 +574,13 @@ export default class ObjectAPI {
}
let sourceTelemetry = null;
if (telemetryIdentifier && this.#identifierEquals(identifier, telemetryIdentifier)) {
if (telemetryIdentifier && utils.identifierEquals(identifier, telemetryIdentifier)) {
sourceTelemetry = identifier;
} else if (objectDetails.composition) {
sourceTelemetry = objectDetails.composition[0];
if (telemetryIdentifier) {
sourceTelemetry = objectDetails.composition.find((telemetrySource) =>
this.#identifierEquals(telemetrySource, telemetryIdentifier)
utils.identifierEquals(telemetrySource, telemetryIdentifier)
);
}
}
@@ -677,7 +666,7 @@ export default class ObjectAPI {
mutableObject = MutableDomainObject.createMutable(domainObject, this.eventEmitter);
// Check if provider supports realtime updates
let identifier = parseKeyString(mutableObject.identifier);
let identifier = utils.parseKeyString(mutableObject.identifier);
let provider = this.getProvider(identifier);
if (
@@ -717,7 +706,7 @@ export default class ObjectAPI {
if (domainObject.isMutable) {
domainObject.$refresh(refreshedObject);
} else {
refresh(domainObject, refreshedObject);
utils.refresh(domainObject, refreshedObject);
}
return domainObject;
@@ -756,7 +745,7 @@ export default class ObjectAPI {
* @returns {string} A string representation of the given identifier, including namespace and key
*/
makeKeyString(identifier) {
return makeKeyString(identifier);
return utils.makeKeyString(identifier);
}
/**
@@ -764,7 +753,7 @@ export default class ObjectAPI {
* @returns {module:openmct.ObjectAPI~Identifier} An identifier object
*/
parseKeyString(keyString) {
return parseKeyString(keyString);
return utils.parseKeyString(keyString);
}
/**
@@ -772,9 +761,9 @@ export default class ObjectAPI {
* @param {module:openmct.ObjectAPI~Identifier[]} identifiers
*/
areIdsEqual(...identifiers) {
const firstIdentifier = this.#parseKeyString(identifiers[0]);
const firstIdentifier = utils.parseKeyString(identifiers[0]);
return identifiers.map(this.#parseKeyString).every((identifier) => {
return identifiers.map(utils.parseKeyString).every((identifier) => {
return (
identifier === firstIdentifier ||
(identifier.namespace === firstIdentifier.namespace &&
@@ -802,7 +791,7 @@ export default class ObjectAPI {
}
return path.some((pathElement) => {
const identifierToCheck = this.#parseKeyString(keyStringToCheck);
const identifierToCheck = utils.parseKeyString(keyStringToCheck);
return this.areIdsEqual(identifierToCheck, pathElement.identifier);
});
@@ -825,7 +814,7 @@ export default class ObjectAPI {
if (location && !this.#pathContainsDomainObject(location, path)) {
// if we have a location, and we don't already have this in our constructed path,
// then keep walking up the path
return this.getOriginalPath(this.#parseKeyString(location), path, abortSignal);
return this.getOriginalPath(utils.parseKeyString(location), path, abortSignal);
} else {
return path;
}
@@ -864,8 +853,8 @@ export default class ObjectAPI {
await Promise.all(
keyStrings.map((keyString) =>
this.supportsMutation(keyString)
? this.getMutable(this.#parseKeyString(keyString))
: this.get(this.#parseKeyString(keyString))
? this.getMutable(utils.parseKeyString(keyString))
: this.get(utils.parseKeyString(keyString))
)
)
).reverse();
@@ -877,7 +866,7 @@ export default class ObjectAPI {
return (
objectPath !== undefined &&
objectPath.length > 1 &&
domainObject.location !== this.#makeKeyString(objectPath[1].identifier)
domainObject.location !== this.makeKeyString(objectPath[1].identifier)
);
}

View File

@@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { isIdentifier } from './object-utils.js';
import utils from './object-utils.js';
export default class RootRegistry {
constructor(openmct) {
@@ -47,12 +47,12 @@ export default class RootRegistry {
}
_isValid(rootItem) {
if (isIdentifier(rootItem) || typeof rootItem === 'function') {
if (utils.isIdentifier(rootItem) || typeof rootItem === 'function') {
return true;
}
if (Array.isArray(rootItem)) {
return rootItem.every(isIdentifier);
return rootItem.every(utils.isIdentifier);
}
return false;

View File

@@ -1,4 +1,4 @@
import { makeKeyString, parseKeyString } from 'objectUtils';
import utils from 'objectUtils';
import Transaction from './Transaction.js';
@@ -9,7 +9,7 @@ let transaction;
describe('Transaction Class', () => {
beforeEach(() => {
objectAPI = {
makeKeyString: (identifier) => makeKeyString(identifier),
makeKeyString: (identifier) => utils.makeKeyString(identifier),
save: () => Promise.resolve(true),
mutate: (object, prop, value) => {
object[prop] = value;
@@ -18,7 +18,7 @@ describe('Transaction Class', () => {
},
refresh: (object) => Promise.resolve(object),
areIdsEqual: (...identifiers) => {
return identifiers.map(parseKeyString).every((identifier) => {
return identifiers.map(utils.parseKeyString).every((identifier) => {
return (
identifier === identifiers[0] ||
(identifier.namespace === identifiers[0].namespace &&

View File

@@ -24,7 +24,7 @@
* Utility for checking if a thing is an Open MCT Identifier.
* @private
*/
export function isIdentifier(thing) {
function isIdentifier(thing) {
return (
typeof thing === 'object' &&
Object.prototype.hasOwnProperty.call(thing, 'key') &&
@@ -36,7 +36,7 @@ export function isIdentifier(thing) {
* Utility for checking if a thing is a key string. Not perfect.
* @private
*/
export function isKeyString(thing) {
function isKeyString(thing) {
return typeof thing === 'string';
}
@@ -49,7 +49,7 @@ export function isKeyString(thing) {
* @param keyString
* @returns identifier
*/
export function parseKeyString(keyString) {
function parseKeyString(keyString) {
if (isIdentifier(keyString)) {
return keyString;
}
@@ -86,7 +86,7 @@ export function parseKeyString(keyString) {
* @param identifier
* @returns keyString
*/
export function makeKeyString(identifier) {
function makeKeyString(identifier) {
if (!identifier) {
throw new Error('Cannot make key string from null identifier');
}
@@ -112,7 +112,7 @@ export function makeKeyString(identifier) {
* @param domainObject
* @returns oldFormatModel
*/
export function toOldFormat(model) {
function toOldFormat(model) {
model = JSON.parse(JSON.stringify(model));
delete model.identifier;
if (model.composition) {
@@ -131,7 +131,7 @@ export function toOldFormat(model) {
* @param keyString
* @returns domainObject
*/
export function toNewFormat(model, keyString) {
function toNewFormat(model, keyString) {
model = JSON.parse(JSON.stringify(model));
model.identifier = parseKeyString(keyString);
if (model.composition) {
@@ -148,7 +148,7 @@ export function toNewFormat(model, keyString) {
* @param otherIdentifier
* @returns Boolean true if identifiers are equal.
*/
export function identifierEquals(a, b) {
function identifierEquals(a, b) {
return a.key === b.key && a.namespace === b.namespace;
}
@@ -160,12 +160,23 @@ export function identifierEquals(a, b) {
* @param otherDomainOBject
* @returns Boolean true if objects are equal.
*/
export function objectEquals(a, b) {
function objectEquals(a, b) {
return identifierEquals(a.identifier, b.identifier);
}
export function refresh(oldObject, newObject) {
function refresh(oldObject, newObject) {
let deleted = _.difference(Object.keys(oldObject), Object.keys(newObject));
deleted.forEach((propertyName) => delete oldObject[propertyName]);
Object.assign(oldObject, newObject);
}
export default {
isIdentifier: isIdentifier,
toOldFormat: toOldFormat,
toNewFormat: toNewFormat,
makeKeyString: makeKeyString,
parseKeyString: parseKeyString,
equals: objectEquals,
identifierEquals: identifierEquals,
refresh: refresh
};

View File

@@ -1,4 +1,4 @@
import { makeKeyString, parseKeyString, toNewFormat, toOldFormat } from 'objectUtils';
import objectUtils from 'objectUtils';
describe('objectUtils', function () {
describe('keyString util', function () {
@@ -31,27 +31,27 @@ describe('objectUtils', function () {
Object.keys(EXPECTATIONS).forEach(function (keyString) {
it('parses "' + keyString + '".', function () {
expect(parseKeyString(keyString)).toEqual(EXPECTATIONS[keyString]);
expect(objectUtils.parseKeyString(keyString)).toEqual(EXPECTATIONS[keyString]);
});
it('parses and re-encodes "' + keyString + '"', function () {
const identifier = parseKeyString(keyString);
expect(makeKeyString(identifier)).toEqual(keyString);
const identifier = objectUtils.parseKeyString(keyString);
expect(objectUtils.makeKeyString(identifier)).toEqual(keyString);
});
it('is idempotent for "' + keyString + '".', function () {
const identifier = parseKeyString(keyString);
let again = parseKeyString(identifier);
const identifier = objectUtils.parseKeyString(keyString);
let again = objectUtils.parseKeyString(identifier);
expect(identifier).toEqual(again);
again = parseKeyString(again);
again = parseKeyString(again);
again = objectUtils.parseKeyString(again);
again = objectUtils.parseKeyString(again);
expect(identifier).toEqual(again);
let againKeyString = makeKeyString(again);
let againKeyString = objectUtils.makeKeyString(again);
expect(againKeyString).toEqual(keyString);
againKeyString = makeKeyString(againKeyString);
againKeyString = makeKeyString(againKeyString);
againKeyString = makeKeyString(againKeyString);
againKeyString = objectUtils.makeKeyString(againKeyString);
againKeyString = objectUtils.makeKeyString(againKeyString);
againKeyString = objectUtils.makeKeyString(againKeyString);
expect(againKeyString).toEqual(keyString);
});
});
@@ -60,7 +60,7 @@ describe('objectUtils', function () {
describe('old object conversions', function () {
it('translate ids', function () {
expect(
toNewFormat(
objectUtils.toNewFormat(
{
prop: 'someValue'
},
@@ -77,7 +77,7 @@ describe('objectUtils', function () {
it('translates composition', function () {
expect(
toNewFormat(
objectUtils.toNewFormat(
{
prop: 'someValue',
composition: ['anotherObjectId', 'scratch:anotherObjectId']
@@ -107,7 +107,7 @@ describe('objectUtils', function () {
describe('new object conversions', function () {
it('removes ids', function () {
expect(
toOldFormat({
objectUtils.toOldFormat({
prop: 'someValue',
identifier: {
namespace: '',
@@ -121,7 +121,7 @@ describe('objectUtils', function () {
it('translates composition', function () {
expect(
toOldFormat({
objectUtils.toOldFormat({
prop: 'someValue',
composition: [
{

View File

@@ -20,7 +20,7 @@
at runtime from the About dialog for additional information.
-->
<template>
<div class="c-overlay js-overlay" role="dialog" aria-modal="true" aria-label="Modal Overlay">
<div class="c-overlay js-overlay">
<div class="c-overlay__blocker" @click="destroy"></div>
<div class="c-overlay__outer">
<button
@@ -29,26 +29,27 @@
class="c-click-icon c-overlay__close-button icon-x"
@click.stop="destroy"
></button>
<div class="c-overlay__content-wrapper">
<div
ref="element"
class="c-overlay__contents js-notebook-snapshot-item-wrapper"
<div
ref="element"
class="c-overlay__contents js-notebook-snapshot-item-wrapper"
tabindex="0"
aria-modal="true"
aria-label="Overlay"
role="dialog"
></div>
<div v-if="buttons" class="c-overlay__button-bar">
<button
v-for="(button, index) in buttons"
ref="buttons"
:key="index"
class="c-button js-overlay__button"
tabindex="0"
></div>
<div v-if="buttons" class="c-overlay__button-bar">
<button
v-for="(button, index) in buttons"
ref="buttons"
:key="index"
class="c-button js-overlay__button"
tabindex="0"
:class="{ 'c-button--major': focusIndex === index }"
@focus="focusIndex = index"
@click="buttonClickHandler(button.callback)"
>
{{ button.label }}
</button>
</div>
:class="{ 'c-button--major': focusIndex === index }"
@focus="focusIndex = index"
@click="buttonClickHandler(button.callback)"
>
{{ button.label }}
</button>
</div>
</div>
</div>
@@ -58,7 +59,7 @@
export default {
inject: ['dismiss', 'element', 'buttons', 'dismissable'],
emits: ['destroy'],
data() {
data: function () {
return {
focusIndex: -1
};

View File

@@ -15,7 +15,7 @@
&__icon {
// Holds a background SVG graphic
$s: 50px;
$s: 80px;
flex: 0 0 auto;
min-width: $s;
min-height: $s;

View File

@@ -19,9 +19,7 @@
z-index: 70;
&__blocker {
// Mobile-first: use the blocker to create a full look to dialogs
@include abs();
background: $colorBodyBg;
display: none; // Mobile-first
}
&__outer {
@@ -29,13 +27,7 @@
background: $colorBodyBg;
display: flex;
flex-direction: column;
body.mobile .l-overlay-fit & {
// Vertically center small dialogs in mobile
top: 50%;
bottom: auto;
transform: translateY(-50%);
}
padding: $overlayInnerMargin;
}
&__close-button {
@@ -47,32 +39,12 @@
z-index: 99;
}
&__content-wrapper {
display: flex;
height: 100%;
overflow: auto;
flex-direction: column;
gap: $interiorMargin;
body.desktop & {
overflow: hidden;
}
.l-overlay-fit &,
.l-overlay-dialog & {
margin: $overlayInnerMargin;
}
}
&__contents {
flex: 1 1 auto;
display: flex;
flex-direction: column;
outline: none;
overflow: auto;
body.mobile & {
flex: none;
}
}
&__top-bar {
@@ -106,10 +78,6 @@
display: flex;
justify-content: flex-end;
margin-top: $interiorMargin;
body.mobile & {
justify-content: flex-end;
padding-right: $interiorMargin;
}
> * + * {
margin-left: $interiorMargin;

View File

@@ -20,6 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import installWorker from './WebSocketWorker.js';
const DEFAULT_RATE_MS = 1000;
/**
* Describes the strategy to be used when batching WebSocket messages
*
@@ -50,21 +51,11 @@ import installWorker from './WebSocketWorker.js';
*
* @memberof module:openmct.telemetry
*/
// 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;
#firstBatchReceived;
#rate;
constructor(openmct) {
super();
@@ -75,10 +66,7 @@ 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.#firstBatchReceived = false;
this.#rate = DEFAULT_RATE_MS;
const routeMessageToHandler = this.#routeMessageToHandler.bind(this);
this.#worker.addEventListener('message', routeMessageToHandler);
@@ -90,20 +78,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
}
);
});
}
/**
@@ -155,6 +129,14 @@ class BatchingWebSocket extends EventTarget {
});
}
/**
* When using batching, sets the rate at which batches of messages are released.
* @param {Number} rate the amount of time to wait, in ms, between batches.
*/
setRate(rate) {
this.#rate = rate;
}
/**
* @param {Number} maxBatchSize the maximum length of a batch of messages. For example,
* the maximum number of telemetry values to batch before dropping them
@@ -169,29 +151,12 @@ class BatchingWebSocket extends EventTarget {
* 15 would probably be a better batch size.
*/
setMaxBatchSize(maxBatchSize) {
this.#maxBatchSize = maxBatchSize;
if (!this.#applicationIsInitializing) {
this.#sendMaxBatchSizeToWorker(this.#maxBatchSize);
}
}
setMaxBatchWait(wait) {
this.#maxBatchWait = wait;
this.#sendBatchWaitToWorker(this.#maxBatchWait);
}
#sendMaxBatchSizeToWorker(maxBatchSize) {
this.#worker.postMessage({
type: 'setMaxBatchSize',
maxBatchSize
});
}
#sendBatchWaitToWorker(maxBatchWait) {
this.#worker.postMessage({
type: 'setMaxBatchWait',
maxBatchWait
});
}
/**
* Disconnect the associated WebSocket. Generally speaking there is no need to call
* this manually.
@@ -204,9 +169,7 @@ 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) {
if (message.data.batch.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.' }
@@ -216,45 +179,16 @@ class BatchingWebSocket extends EventTarget {
this.#showingRateLimitNotification = false;
});
}
this.dispatchEvent(new CustomEvent('batch', { detail: batch }));
this.#waitUntilIdleAndRequestNextBatch(batch);
this.dispatchEvent(new CustomEvent('batch', { detail: message.data.batch }));
setTimeout(() => {
this.#readyForNextBatch();
}, this.#rate);
} else if (message.data.type === 'message') {
this.dispatchEvent(new CustomEvent('message', { detail: message.data.message }));
} else if (message.data.type === 'reconnected') {
this.dispatchEvent(new CustomEvent('reconnected'));
} else {
throw new Error(`Unknown message type: ${message.data.type}`);
}
}
#waitUntilIdleAndRequestNextBatch(batch) {
requestIdleCallback(
(state) => {
if (this.#firstBatchReceived === false) {
this.#firstBatchReceived = true;
}
const now = Date.now();
const waitedFor = now - this.start;
if (state.didTimeout === true) {
if (document.visibilityState === 'visible') {
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) {
console.warn(`Warning, batch processing took ${waitedFor}ms`);
}
this.#readyForNextBatch();
}
},
{ timeout: ONE_SECOND }
);
}
}
export default BatchingWebSocket;

View File

@@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { makeKeyString } from 'objectUtils';
import objectUtils from 'objectUtils';
import CustomStringFormatter from '../../plugins/displayLayout/CustomStringFormatter.js';
import BatchingWebSocket from './BatchingWebSocket.js';
@@ -85,7 +85,6 @@ const SUBSCRIBE_STRATEGY = {
export default class TelemetryAPI {
#isGreedyLAD;
#subscribeCache;
#hasReturnedFirstData;
get SUBSCRIBE_STRATEGY() {
return SUBSCRIBE_STRATEGY;
@@ -109,7 +108,15 @@ export default class TelemetryAPI {
this.#isGreedyLAD = true;
this.BatchingWebSocket = BatchingWebSocket;
this.#subscribeCache = {};
this.#hasReturnedFirstData = false;
this.itemsGarbageCollected = 0;
// eslint-disable-next-line no-undef
this.registry = new FinalizationRegistry((heldValue) => {
this.itemsGarbageCollected++;
console.debug(
`🗑️ 🛜 TELEMETRY API garbage collected: ${this.itemsGarbageCollected} - ${JSON.stringify(heldValue)}`
);
});
}
abortAllRequests() {
@@ -385,10 +392,11 @@ export default class TelemetryAPI {
arguments[1] = await this.applyRequestInterceptors(domainObject, arguments[1]);
try {
const telemetry = await provider.request(...arguments);
if (!this.#hasReturnedFirstData) {
this.#hasReturnedFirstData = true;
performance.mark('firstHistoricalDataReturned');
}
// add each piece of data individually to be registry
telemetry.forEach((datum) => {
this.registry.register(datum, `${new Date()} Data with: ${JSON.stringify(datum)}`);
});
return telemetry;
} catch (error) {
if (error.name !== 'AbortError') {
@@ -434,7 +442,7 @@ export default class TelemetryAPI {
this.#subscribeCache = {};
}
const keyString = makeKeyString(domainObject.identifier);
const keyString = objectUtils.makeKeyString(domainObject.identifier);
const supportedStrategy = supportsBatching ? requestedStrategy : SUBSCRIBE_STRATEGY.LATEST;
// Override the requested strategy with the strategy supported by the provider
const optionsWithSupportedStrategy = {
@@ -467,9 +475,14 @@ export default class TelemetryAPI {
} else {
subscriber.latestCallbacks.push(callback);
}
const telemetryAPI = this;
// Guarantees that view receive telemetry in the expected form
function invokeCallbackWithRequestedStrategy(data) {
// add each piece of data individually to be registry
data.forEach((datum) => {
const heldValue = `${new Date()} Data with: ${JSON.stringify(datum)}`;
telemetryAPI.registry.register(datum, heldValue);
});
invokeCallbacksWithArray(data, subscriber.batchCallbacks);
invokeCallbacksWithSingleValue(data, subscriber.latestCallbacks);
}
@@ -546,7 +559,7 @@ export default class TelemetryAPI {
this.stalenessSubscriberCache = {};
}
const keyString = makeKeyString(domainObject.identifier);
const keyString = objectUtils.makeKeyString(domainObject.identifier);
let stalenessSubscriber = this.stalenessSubscriberCache[keyString];
if (!stalenessSubscriber) {
@@ -605,7 +618,7 @@ export default class TelemetryAPI {
this.limitsSubscribeCache = {};
}
const keyString = makeKeyString(domainObject.identifier);
const keyString = objectUtils.makeKeyString(domainObject.identifier);
let subscriber = this.limitsSubscribeCache[keyString];
if (!subscriber) {

View File

@@ -442,12 +442,8 @@ export default class TelemetryCollection extends EventEmitter {
} else {
this.timeKey = undefined;
// missing objects will never have a domain, if one happens to get through
// to this point this warning/notification does not apply
if (!this.openmct.objects.isMissing(this.domainObject)) {
this._warn(TIMESYSTEM_KEY_WARNING);
this.openmct.notifications.alert(TIMESYSTEM_KEY_NOTIFICATION);
}
this._warn(TIMESYSTEM_KEY_WARNING);
this.openmct.notifications.alert(TIMESYSTEM_KEY_NOTIFICATION);
}
let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);

View File

@@ -21,7 +21,6 @@
*****************************************************************************/
/* eslint-disable max-classes-per-file */
export default function installWorker() {
const ONE_SECOND = 1000;
const FALLBACK_AND_WAIT_MS = [1000, 5000, 5000, 10000, 10000, 30000];
/**
@@ -45,13 +44,6 @@ export default function installWorker() {
#currentWaitIndex = 0;
#messageCallbacks = [];
#wsUrl;
#reconnecting = false;
#worker;
constructor(worker) {
super();
this.#worker = worker;
}
/**
* Establish a new WebSocket connection to the given URL
@@ -70,9 +62,6 @@ export default function installWorker() {
this.#isConnecting = true;
this.#webSocket = new WebSocket(url);
//Exposed to e2e tests so that the websocket can be manipulated during tests. Cannot find any other way to do this.
// Playwright does not support forcing websocket state changes.
this.#worker.currentWebSocket = this.#webSocket;
const boundConnected = this.#connected.bind(this);
this.#webSocket.addEventListener('open', boundConnected);
@@ -111,17 +100,12 @@ export default function installWorker() {
}
#connected() {
console.info('Websocket connected.');
console.debug('Websocket connected.');
this.#isConnected = true;
this.#isConnecting = false;
this.#currentWaitIndex = 0;
if (this.#reconnecting) {
this.#worker.postMessage({
type: 'reconnected'
});
this.#reconnecting = false;
}
this.dispatchEvent(new Event('connected'));
this.#flushQueue();
}
@@ -154,7 +138,6 @@ export default function installWorker() {
if (this.#reconnectTimeoutHandle) {
return;
}
this.#reconnecting = true;
this.#reconnectTimeoutHandle = setTimeout(() => {
this.connect(this.#wsUrl);
@@ -224,9 +207,6 @@ export default function installWorker() {
case 'setMaxBatchSize':
this.#messageBatcher.setMaxBatchSize(message.data.maxBatchSize);
break;
case 'setMaxBatchWait':
this.#messageBatcher.setMaxBatchWait(message.data.maxBatchWait);
break;
default:
throw new Error(`Unknown message type: ${type}`);
}
@@ -265,6 +245,7 @@ export default function installWorker() {
}
routeMessageToHandler(data) {
//Implement batching here
if (this.#messageBatcher.shouldBatchMessage(data)) {
this.#messageBatcher.addMessageToBatch(data);
} else {
@@ -286,15 +267,12 @@ export default function installWorker() {
#maxBatchSize;
#readyForNextBatch;
#worker;
#throttledSendNextBatch;
constructor(worker) {
// No dropping telemetry unless we're explicitly told to.
this.#maxBatchSize = Number.POSITIVE_INFINITY;
this.#maxBatchSize = 10;
this.#readyForNextBatch = false;
this.#worker = worker;
this.#resetBatch();
this.setMaxBatchWait(ONE_SECOND);
}
#resetBatch() {
this.#batch = {};
@@ -332,29 +310,23 @@ export default function installWorker() {
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;
this.#batch.dropped = this.#batch.dropped || true;
}
if (this.#readyForNextBatch) {
this.#throttledSendNextBatch();
this.#sendNextBatch();
} else {
this.#hasBatch = true;
}
}
setMaxBatchSize(maxBatchSize) {
this.#maxBatchSize = maxBatchSize;
}
setMaxBatchWait(maxBatchWait) {
this.#throttledSendNextBatch = throttle(this.#sendNextBatch.bind(this), maxBatchWait);
}
/**
* Indicates that client code is ready to receive the next batch of
* messages. If a batch is available, it will be immediately sent.
@@ -363,7 +335,7 @@ export default function installWorker() {
*/
readyForNextBatch() {
if (this.#hasBatch) {
this.#throttledSendNextBatch();
this.#sendNextBatch();
} else {
this.#readyForNextBatch = true;
}
@@ -380,34 +352,7 @@ export default function installWorker() {
}
}
function throttle(callback, wait) {
let last = 0;
let throttling = false;
return function (...args) {
if (throttling) {
return;
}
const now = performance.now();
const timeSinceLast = now - last;
if (timeSinceLast >= wait) {
last = now;
callback(...args);
} else if (!throttling) {
throttling = true;
setTimeout(() => {
last = performance.now();
throttling = false;
callback(...args);
}, wait - timeSinceLast);
}
};
}
const websocket = new ResilientWebSocket(self);
const websocket = new ResilientWebSocket();
const messageBatcher = new MessageBatcher(self);
const workerBroker = new WorkerToWebSocketMessageBroker(websocket, messageBatcher);
const websocketBroker = new WebSocketToWorkerMessageBroker(messageBatcher, self);
@@ -418,6 +363,4 @@ export default function installWorker() {
websocket.registerMessageCallback((data) => {
websocketBroker.routeMessageToHandler(data);
});
self.websocketInstance = websocket;
}

View File

@@ -24,13 +24,11 @@
<tr
ref="tableRow"
class="js-lad-table__body__row c-table__selectable-row"
aria-label="lad row"
@click="clickedRow"
@contextmenu.prevent="showContextMenu"
>
<td
ref="tableCell"
aria-label="lad name"
class="js-first-data"
@mouseover.ctrl="showToolTip"
@mouseleave="hideToolTip"
@@ -60,7 +58,7 @@
const CONTEXT_MENU_ACTIONS = ['viewDatumAction', 'viewHistoricalData', 'remove'];
const BLANK_VALUE = '---';
import { objectPathToUrl } from '/src/tools/url.js';
import identifierToString from '/src/tools/url.js';
import PreviewAction from '@/ui/preview/PreviewAction.js';
import tooltipHelpers from '../../../api/tooltips/tooltipMixins.js';
@@ -262,7 +260,7 @@ export default {
event.preventDefault();
this.preview(this.objectPath);
} else {
const resultUrl = objectPathToUrl(this.openmct, this.objectPath);
const resultUrl = identifierToString(this.openmct, this.objectPath);
this.openmct.router.navigate(resultUrl);
}
},

View File

@@ -22,7 +22,7 @@
<template>
<div class="c-lad-table-wrapper u-style-receiver js-style-receiver" :class="staleClass">
<table aria-label="lad table" class="c-table c-lad-table" :class="applyLayoutClass">
<table class="c-table c-lad-table" :class="applyLayoutClass">
<thead>
<tr>
<th>Name</th>

View File

@@ -25,7 +25,6 @@
aria-label="Clock Indicator"
class="c-indicator t-indicator-clock icon-clock no-minify c-indicator--not-clickable"
role="complementary"
aria-live="off"
>
<span class="label c-indicator__label">
{{ timeTextValue }}

View File

@@ -56,38 +56,20 @@ export default class ConditionManager extends EventEmitter {
);
}
async requestLatestValue(endpoint) {
const options = {
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]) {
const id = this.openmct.objects.makeKeyString(endpoint.identifier);
if (this.subscriptions[id]) {
console.log('subscription already exists');
return;
}
const metadata = this.openmct.telemetry.getMetadata(endpoint);
this.telemetryObjects[telemetryKeyString] = Object.assign({}, endpoint, {
this.telemetryObjects[id] = 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(
this.subscriptions[id] = this.openmct.telemetry.subscribe(
endpoint,
this.telemetryReceived.bind(this, endpoint)
);
@@ -109,7 +91,7 @@ export default class ConditionManager extends EventEmitter {
//force re-computation of condition set result as we might be in a state where
// there is no telemetry datum coming in for a while or at all.
const latestTimestamp = getLatestTimestamp(
let latestTimestamp = getLatestTimestamp(
{},
{},
this.timeSystems,
@@ -352,54 +334,57 @@ export default class ConditionManager extends EventEmitter {
return currentCondition;
}
async requestLADConditionSetOutput(options) {
requestLADConditionSetOutput(options) {
if (!this.conditions.length) {
return [];
return Promise.resolve([]);
}
await this.compositionLoad;
return this.compositionLoad.then(() => {
let latestTimestamp;
let conditionResults = {};
let nextLegOptions = { ...options };
delete nextLegOptions.onPartialResponse;
let latestTimestamp;
let conditionResults = {};
let nextLegOptions = { ...options };
delete nextLegOptions.onPartialResponse;
const results = await Promise.all(
this.conditions.map((condition) => condition.requestLADConditionResult(nextLegOptions))
);
results.forEach((resultObj) => {
const {
id,
data,
data: { result }
} = resultObj;
if (this.findConditionById(id)) {
conditionResults[id] = Boolean(result);
}
latestTimestamp = getLatestTimestamp(
latestTimestamp,
data,
this.timeSystems,
this.openmct.time.timeSystem()
const conditionRequests = this.conditions.map((condition) =>
condition.requestLADConditionResult(nextLegOptions)
);
return Promise.all(conditionRequests).then((results) => {
results.forEach((resultObj) => {
const {
id,
data,
data: { result }
} = resultObj;
if (this.findConditionById(id)) {
conditionResults[id] = Boolean(result);
}
latestTimestamp = getLatestTimestamp(
latestTimestamp,
data,
this.timeSystems,
this.openmct.time.timeSystem()
);
});
if (!Object.values(latestTimestamp).some((timeSystem) => timeSystem)) {
return [];
}
const currentCondition = this.getCurrentConditionLAD(conditionResults);
const currentOutput = Object.assign(
{
output: currentCondition.configuration.output,
id: this.conditionSetDomainObject.identifier,
conditionId: currentCondition.id
},
latestTimestamp
);
return [currentOutput];
});
});
if (!Object.values(latestTimestamp).some((timeSystem) => timeSystem)) {
return [];
}
const currentCondition = this.getCurrentConditionLAD(conditionResults);
const currentOutput = {
output: currentCondition.configuration.output,
id: this.conditionSetDomainObject.identifier,
conditionId: currentCondition.id,
...latestTimestamp
};
return [currentOutput];
}
isTelemetryUsed(endpoint) {
@@ -424,7 +409,7 @@ export default class ConditionManager extends EventEmitter {
}
const normalizedDatum = this.createNormalizedDatum(datum, endpoint);
const timeSystemKey = this.openmct.time.getTimeSystem().key;
const timeSystemKey = this.openmct.time.timeSystem().key;
let timestamp = {};
const currentTimestamp = normalizedDatum[timeSystemKey];
timestamp[timeSystemKey] = currentTimestamp;

View File

@@ -40,10 +40,12 @@ export default class ConditionSetTelemetryProvider {
return domainObject.type === 'conditionSet';
}
async request(domainObject, options) {
request(domainObject, options) {
let conditionManager = this.getConditionManager(domainObject);
let latestOutput = await conditionManager.requestLADConditionSetOutput(options);
return latestOutput;
return conditionManager.requestLADConditionSetOutput(options).then((latestOutput) => {
return latestOutput;
});
}
subscribe(domainObject, callback) {

View File

@@ -31,10 +31,10 @@
<div class="c-cs__header-label c-section__label">Test Data</div>
</div>
<div v-if="expanded" class="c-cs__content">
<div :class="['c-cs__test-data__controls c-cdef__controls', { disabled: !telemetry.length }]">
<div class="c-cs__test-data__controls c-cdef__controls" :disabled="!telemetry.length">
<label class="c-toggle-switch">
<input type="checkbox" :checked="isApplied" @change="applyTestData" />
<span class="c-toggle-switch__slider" aria-label="Apply Test Data"></span>
<span class="c-toggle-switch__slider"></span>
<span class="c-toggle-switch__label">Apply Test Data</span>
</label>
</div>
@@ -47,11 +47,7 @@
<span class="c-cs-test__label">Set</span>
<span class="c-cs-test__controls">
<span class="c-cdef__control">
<select
v-model="testInput.telemetry"
aria-label="Test Data Telemetry Selection"
@change="updateMetadata(testInput)"
>
<select v-model="testInput.telemetry" @change="updateMetadata(testInput)">
<option value="">- Select Telemetry -</option>
<option
v-for="(telemetryOption, index) in telemetry"
@@ -63,11 +59,7 @@
</select>
</span>
<span v-if="testInput.telemetry" class="c-cdef__control">
<select
v-model="testInput.metadata"
aria-label="Test Data Metadata Selection"
@change="updateTestData"
>
<select v-model="testInput.metadata" @change="updateTestData">
<option value="">- Select Field -</option>
<option
v-for="(option, index) in telemetryMetadataOptions[getId(testInput.telemetry)]"
@@ -84,7 +76,6 @@
placeholder="Enter test input"
type="text"
class="c-cdef__control__input"
aria-label="Test Data Input"
@change="updateTestData"
/>
</span>

View File

@@ -66,8 +66,7 @@ describe('the plugin', function () {
format: 'utc',
hints: {
domain: 1
},
source: 'utc'
}
},
{
key: 'testSource',
@@ -721,23 +720,6 @@ describe('the plugin', function () {
});
it('should evaluate as old when telemetry is not received in the allotted time', (done) => {
openmct.telemetry = jasmine.createSpyObj('telemetry', [
'subscribe',
'getMetadata',
'request',
'getValueFormatter',
'abortAllRequests'
]);
openmct.telemetry.getMetadata.and.returnValue({
...testTelemetryObject.telemetry,
valueMetadatas: []
});
openmct.telemetry.request.and.returnValue(Promise.resolve([]));
openmct.telemetry.getValueFormatter.and.returnValue({
parse: function (value) {
return value;
}
});
let conditionMgr = new ConditionManager(conditionSetDomainObject, openmct);
conditionMgr.on('conditionSetResultUpdated', mockListener);
conditionMgr.telemetryObjects = {
@@ -759,20 +741,6 @@ describe('the plugin', function () {
});
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
});
const testDatum = {
'some-key2': '',
utc: 1,
testSource: '',
'some-key': null,
id: 'test-object'
};
openmct.telemetry.request = jasmine.createSpy('request');
openmct.telemetry.request.and.returnValue(Promise.resolve([testDatum]));
const date = 1;
conditionSetDomainObject.configuration.conditionCollection[0].configuration.criteria[0].input =
['0.4'];
@@ -782,7 +750,9 @@ describe('the plugin', function () {
'test-object': testTelemetryObject
};
conditionMgr.updateConditionTelemetryObjects();
conditionMgr.telemetryReceived(testTelemetryObject, testDatum);
conditionMgr.telemetryReceived(testTelemetryObject, {
utc: date
});
setTimeout(() => {
expect(mockListener).toHaveBeenCalledWith({
output: 'Default',
@@ -898,12 +868,6 @@ describe('the plugin', function () {
it('should stop evaluating conditions when a condition evaluates to true', () => {
const date = Date.now();
let conditionMgr = new ConditionManager(conditionSetDomainObject, openmct);
openmct.telemetry.getMetadata = jasmine.createSpy('getMetadata');
openmct.telemetry.getMetadata.and.returnValue({
...testTelemetryObject.telemetry,
valueMetadatas: []
});
conditionMgr.on('conditionSetResultUpdated', mockListener);
conditionMgr.telemetryObjects = {
'test-object': testTelemetryObject

View File

@@ -22,7 +22,6 @@
<template>
<div
aria-label="sub object frame"
class="l-layout__frame c-frame"
:class="{
'no-frame': !item.hasFrame,

View File

@@ -37,24 +37,24 @@
:style="styleObject"
:data-font-size="item.fontSize"
:data-font="item.font"
aria-label="Alpha-numeric telemetry"
@contextmenu.prevent="showContextMenu"
@mouseover.ctrl="showToolTip"
@mouseleave="hideToolTip"
>
<div class="is-status__indicator"></div>
<div
class="is-status__indicator"
:aria-label="`This item is ${status}`"
:title="`This item is ${status}`"
></div>
<div v-if="showLabel" class="c-telemetry-view__label">
<div
class="c-telemetry-view__label-text"
:aria-label="`Alpha-numeric telemetry name for ${domainObject.name}`"
>
<div class="c-telemetry-view__label-text">
{{ domainObject.name }}
</div>
</div>
<div
v-if="showValue"
:aria-label="`Alpha-numeric telemetry value of ${telemetryValue}`"
:aria-label="fieldName"
:title="fieldName"
class="c-telemetry-view__value"
:class="[telemetryClass]"

Some files were not shown because too many files have changed in this diff Show More